@struktoai/mirage-dsh 0.0.7-alpha.2 → 0.0.7
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/LICENSE +201 -0
- package/README.md +28 -29
- package/cordis.patch.yml +38 -6
- package/dist/{chunk-M4JOF35U.js → chunk-FRF6WPLN.js} +133 -44
- package/dist/chunk-FRF6WPLN.js.map +1 -0
- package/dist/chunk-G2GHA2F6.js +211 -0
- package/dist/chunk-G2GHA2F6.js.map +1 -0
- package/dist/{chunk-QTBMLMKT.js → chunk-KUIV2Z3M.js} +243 -326
- package/dist/chunk-KUIV2Z3M.js.map +1 -0
- package/dist/{chunk-4CZV24HW.js → chunk-OHZACFWO.js} +36 -14
- package/dist/chunk-OHZACFWO.js.map +1 -0
- package/dist/{chunk-2F2PJ7D7.js → chunk-OZ2F4R6T.js} +74 -17
- package/dist/chunk-OZ2F4R6T.js.map +1 -0
- package/dist/chunk-QH5H3VND.js +110 -0
- package/dist/chunk-QH5H3VND.js.map +1 -0
- package/dist/chunk-T4BSJAGN.js +64 -0
- package/dist/chunk-T4BSJAGN.js.map +1 -0
- package/dist/{fs-B3Zi9oo6.d.ts → fs-BNOIFi9o.d.ts} +48 -4
- package/dist/index.d.ts +8 -3
- package/dist/index.js +15 -6
- package/dist/plugin/fs.d.ts +1 -1
- package/dist/plugin/fs.js +2 -2
- package/dist/plugin/service.d.ts +2 -1
- package/dist/plugin/service.js +1 -1
- package/dist/plugin/shell.d.ts +1 -1
- package/dist/plugin/shell.js +3 -2
- package/dist/plugin/spill-store.d.ts +3 -0
- package/dist/plugin/spill-store.js +8 -0
- package/dist/plugin/spill-store.js.map +1 -0
- package/dist/plugin/subprocess.d.ts +8 -0
- package/dist/plugin/subprocess.js +9 -0
- package/dist/plugin/subprocess.js.map +1 -0
- package/dist/{service-BK6NR_RX.d.ts → service-vCjLPYMI.d.ts} +29 -17
- package/dist/{shell-BTf2wsDy.d.ts → shell-BAyBocjm.d.ts} +25 -35
- package/dist/spill-store-BfljgRi0.d.ts +38 -0
- package/dist/subprocess-CInesMpR.d.ts +23 -0
- package/package.json +24 -12
- package/dist/chunk-2F2PJ7D7.js.map +0 -1
- package/dist/chunk-4CZV24HW.js.map +0 -1
- package/dist/chunk-M4JOF35U.js.map +0 -1
- package/dist/chunk-QTBMLMKT.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/approval.ts","../src/service.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 type { AskHandler } from '@struktoai/mirage-core/policy/decisions'\nimport type { Decision } from '@struktoai/mirage-core/policy/types'\nimport { Outcome, Scope } from '@struktoai/mirage-core/policy/types'\nimport { shellQuote } from '@struktoai/mirage-core/utils/quote'\n\n/**\n * The tool an approval request is attributed to. A mirage ask is raised\n * by the command plane, which is what dsh's bash tool called, so the\n * audit trail names that tool rather than the head word of the line\n * (which the request's own reason already carries).\n */\nexport const APPROVAL_TOOL_NAME = 'bash'\n\n/**\n * The outcomes dsh's approval channel answers with.\n *\n * Declared here rather than imported: the type lives in\n * `@deepseek-ai/dsh-sandbox`, which this package does not depend on, and\n * dsh itself treats the channel structurally for the same reason (its\n * `EscalationApprover` is \"a minimal STRUCTURAL function shape, not the\n * approval service type\"). Staying structural is what lets any approver\n * be wired, dsh's own included.\n */\nexport type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'\n\n/**\n * The one question this bridge asks of dsh: `ctx.approval` narrowed to\n * the call it makes. An `ApprovalService` satisfies it structurally,\n * without this package importing the approval or agent packages.\n */\nexport interface Approver {\n request(req: {\n agent: object | undefined\n toolName: string\n callId: string\n reason: string\n signal?: AbortSignal\n }): Promise<ApprovalOutcome>\n}\n\n/**\n * dsh's approval channel, or null when the composition has none.\n *\n * Read through `ctx.get`, not as `ctx.approval`. A plain property read\n * of a service this plugin does not `inject` does not answer undefined,\n * it throws (`cannot get property \"approval\" without inject`), so\n * probing that way would turn every ask in a composition without an\n * approver into a thrown error instead of a pending one. `ctx.get` is\n * cordis's documented read \"without the inject requirement\", and its\n * default strictness answers only a provider whose fiber is live, so an\n * unloaded approval plugin reads as absent rather than as a stale\n * channel nobody is listening on.\n *\n * Declaring `inject` instead would be wrong in the other direction: it\n * would make an approval channel a load requirement, and a headless\n * composition that legitimately has none would fail to compose at all.\n *\n * Read at ask time rather than captured when the plugin loads, because\n * cordis composes plugins in whatever order the profile lists them and a\n * snapshot taken at load would miss an approver registered after this.\n *\n * @param ctx the cordis context to read.\n * @returns the approver, or null when nothing provides one.\n */\nexport function approverOf(ctx: Context): Approver | null {\n const held: unknown = ctx.get('approval')\n if (held === null || typeof held !== 'object') return null\n const { request } = held as { request?: unknown }\n return typeof request === 'function' ? (held as Approver) : null\n}\n\n/**\n * What the human is shown: the rule's reason, and the line it is about.\n *\n * The reason alone is the operator's sentence (\"deletes are reviewed\")\n * and names nothing being deleted, so the line is quoted beside it. The\n * words come off the record rather than being re-rendered, so what was\n * approved is what was asked.\n *\n * Each word is rendered as a shell would read it back, because this\n * string is the whole of what the human authorizes and the words are\n * already-expanded values, not source. Joined raw, `rm -- 'quarterly\n * report'` reads as two operands and a newline inside a name forges a\n * line break in the prompt, so the human would be nodding at something\n * other than the run. `shellQuote` is GNU's diagnostic rendering: an\n * ordinary name stays bare, and only a word a shell would read as\n * something else is dressed.\n *\n * @param record the ledger record being asked about.\n * @returns the prompt text.\n */\nexport function approvalReason(record: Decision): string {\n const line = [record.command, ...record.argv].map(shellQuote).join(' ')\n return `${record.reason}: ${line}`\n}\n\n/**\n * Bridge mirage's ask to dsh's approval channel: the handler that fills\n * `WorkspaceOptions.onAsk`.\n *\n * Mirage owns every outcome (`policy/decisions.ts`); this only decides\n * which of its branches an approval answer lands on. Every answer from a\n * live channel is one of mirage's two settled verbs, because\n * `request` blocks until the channel answers and so never reports \"no\n * answer yet\":\n *\n * * `allowed-once` is dsh's only yes and grants exactly the line that\n * asked, so it maps to ALLOW at ONCE and never SESSION. Widening one\n * nod into a standing grant is the one thing the human did not\n * consent to; a host that wants SESSION answers writes them to the\n * ledger itself (`ws.decisions.answer`), the surface built for it.\n * * `rejected` is an explicit refusal.\n * * `cancelled` is a refusal too: the human was shown the prompt and\n * dismissed it, which is an answer rather than the absence of one.\n * Leaving it pending would re-prompt them, since a retry reuses the\n * waiting record and would call `request` again.\n * * `unavailable` is a permission check that could not run, so it\n * fails closed. Pending here would be fail-open-into-retry.\n *\n * The three refusals match what dsh does with the same vocabulary for\n * its own escalations, where `approveEscalation` throws on a rejection,\n * a cancellation and an unanswerable ask alike.\n *\n * The run's own signal rides along, so a prompt raised for a line whose\n * run is then killed or times out is dismissed with it rather than left\n * on somebody's screen for a command that no longer exists. The ledger\n * stops waiting either way — it bounds the wait by the same signal — but\n * only the channel can take its prompt down.\n *\n * With no approver on the context the handler answers null, which is\n * mirage's \"nobody has answered yet\": the ask stays pending in the\n * ledger. That is deliberate, and it is why the service installs this\n * unconditionally rather than only under a composed approver — the two\n * are the same answer to mirage (`onAsk` unset and a null return both\n * render pending), and installing always is what lets an approver\n * composed after this plugin still be found.\n *\n * Refusing instead would rewrite an operator's `ask` rule as a `deny`,\n * and the embedder may well be answering `ws.decisions.pending()` out\n * of band.\n *\n * @param ctx the cordis context carrying `ctx.approval`.\n * @returns the handler to pass as `onAsk`.\n */\nexport function askThroughApproval(ctx: Context): AskHandler {\n return async (record: Decision, signal?: AbortSignal): Promise<Decision | null> => {\n const approver = approverOf(ctx)\n if (approver === null) return null\n const outcome = await approver.request({\n agent: undefined,\n toolName: APPROVAL_TOOL_NAME,\n callId: record.id,\n reason: approvalReason(record),\n ...(signal !== undefined ? { signal } : {}),\n })\n const verb = outcome === 'allowed-once' ? Outcome.ALLOW : Outcome.DENY\n return { ...record, outcome: verb, scope: Scope.ONCE }\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 { Service } from '@deepseek-ai/cordis'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { Explanation } from '@struktoai/mirage-core/policy/types'\nimport type { Runtime, RuntimeEntry } from '@struktoai/mirage-core/runtime/base'\nimport { buildRuntime } from '@struktoai/mirage-core/runtime/table'\nimport { parseMountMode } from '@struktoai/mirage-core/types'\nimport type { MountSpec } from '@struktoai/mirage-core/workspace/workspace/workspace'\nimport { Workspace, buildResource, parseSessionProfile } from '@struktoai/mirage-node'\nimport type { Mount, NodeWorkspaceOptions, SessionProfile } from '@struktoai/mirage-node'\nimport { askThroughApproval } from './approval.ts'\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n mirage: MirageService\n }\n}\n\n/**\n * A declarative mount, YAML-friendly: the resource is named by its\n * registry type and built through `buildResource`, so a dsh bundle\n * patch can mount anything without holding a live instance. Told apart\n * from a live `Mount` by the type of `resource`: a string names a\n * registry entry, an object is the instance itself.\n */\nexport interface MirageMountBlock {\n /** Resource registry name (`ram`, `s3`, `slack`, `redis`, ...). */\n resource: string\n /** Mount mode: `read`, `write`, or `exec`. Omitted = the workspace default. */\n mode?: string\n /** Resource constructor config, passed to the registry factory. */\n config?: Record<string, unknown>\n}\n\n/** One mount entry: a live spec or instance, or a declarative block. */\nexport type MirageMount = MountSpec | Mount | MirageMountBlock\n\n/** A runtime entry, YAML-friendly: a name, or a name with captures. */\nexport interface MirageRuntimeBlock {\n /** Runtime registry name (`monty`, `pyodide`, `quickjs`, ...). */\n name: string\n /** Command words this runtime captures (e.g. `['python', 'python3']`). */\n captures?: string[]\n}\n\n/**\n * Configuration for the shared mirage execution world. Exactly one of\n * `workspace` (adopt a live workspace the embedder owns and closes) or\n * `mounts` (construct a workspace here; the service closes it when its\n * plugin unloads) must be given.\n */\nexport interface MirageConfig {\n /** A live workspace to adopt; its lifecycle stays with the embedder. */\n workspace?: Workspace\n /** Mounts for a service-owned workspace, keyed by mount prefix. */\n mounts?: Record<string, MirageMount>\n /**\n * Runtimes for a service-owned workspace, built via `buildRuntime`.\n * The YAML-friendly twin of `workspaceOptions.runtimes`; pass one or\n * the other, not both.\n */\n runtimes?: (string | MirageRuntimeBlock)[]\n /**\n * The roles (`profiles:` in mirage config), each a whole permission\n * document: its `commands.allow` list, its `commands.ask` and\n * `commands.deny` rules, the mount sections carrying rules of their\n * own, and the paths and vars it hides. The YAML-friendly twin of\n * `workspaceOptions.profiles`; pass one or the other, not both.\n *\n * Written as the document, not as an already-parsed profile: every\n * entry goes through `parseSessionProfile`, so a misspelled field is a\n * load error rather than a first-session one. That parse is what lets\n * a dsh bundle patch carry a policy at all, since\n * `workspaceOptions.profiles` wants profiles already parsed and a\n * patch file holds plain YAML.\n */\n profiles?: Record<string, unknown>\n /**\n * Which role shapes a session created without one, the workspace's own\n * session included. A name `profiles` does not define is an error. The\n * YAML-friendly twin of `workspaceOptions.profile`.\n */\n profile?: string\n /** Options forwarded to the service-owned workspace's constructor. */\n workspaceOptions?: NodeWorkspaceOptions\n}\n\nfunction isMountBlock(entry: MirageMount): entry is MirageMountBlock {\n if (Array.isArray(entry)) return false\n return typeof (entry as MirageMountBlock).resource === 'string'\n}\n\nasync function resolveMount(entry: MirageMountBlock): Promise<MountSpec> {\n const resource = await buildResource(entry.resource, entry.config ?? {})\n if (entry.mode === undefined) return resource\n return [resource, parseMountMode(entry.mode)]\n}\n\n/**\n * Validate a `profiles` block: every role through the core document\n * validator, naming the role in the error so a typo in a patch file\n * points at the block that holds it.\n *\n * @param raw the roles as written, keyed by role name.\n * @returns the parsed roles, ready for `WorkspaceOptions.profiles`.\n */\nfunction parseProfiles(raw: Record<string, unknown>): Record<string, SessionProfile> {\n const out: Record<string, SessionProfile> = {}\n for (const [name, block] of Object.entries(raw)) {\n out[name] = parseSessionProfile(block, `profile \\`${name}\\``)\n }\n return out\n}\n\nfunction toRuntimeEntry(entry: string | MirageRuntimeBlock): RuntimeEntry {\n if (typeof entry === 'string') return entry\n return buildRuntime(entry.name, entry.captures === undefined ? {} : { captures: entry.captures })\n}\n\n// A name shorthand builds exactly as the workspace would build it; holding\n// the instance early is what lets `vfsOnly` classify the world before the\n// (asynchronous) mounts finish resolving.\nfunction builtEntry(entry: RuntimeEntry): Runtime {\n return typeof entry === 'string' ? buildRuntime(entry) : entry\n}\n\n/**\n * The one mirage `Workspace` behind every mirage-backed dsh provider\n * (`ctx.mirage`, mirroring how `ctx.e2b` owns one sandbox for the E2B\n * adapters). `MirageFileSystem` and `MirageShellExecutor` both inject it,\n * which is what keeps `ctx.fs` targets and `ctx.shell` commands in one\n * execution world: a `processPath` handed to the shell resolves there.\n *\n * Construction with live resources is synchronous; a declarative mount\n * block defers to `buildResource`, so consumers await `ready` (the\n * adapters do) and `workspace` throws until it resolves, mirroring how\n * the E2B service owner exposes its sandbox.\n */\nexport class MirageService extends Service {\n static readonly provide = 'mirage'\n\n /** Resolves to the shared workspace once every mount is built. */\n readonly ready: Promise<Workspace>\n\n private built: Workspace | null = null\n private readonly plannedRuntimes: readonly Runtime[] | undefined\n\n constructor(ctx: Context, config: MirageConfig = {}) {\n super(ctx, 'mirage')\n if (config.workspace !== undefined && config.mounts !== undefined) {\n throw new Error('mirage: pass either workspace or mounts, not both')\n }\n if (config.workspace !== undefined) {\n // An adopted workspace is already built, so runtimes and constructor\n // options can no longer reach it. Saying so beats accepting a config\n // whose runtimes silently never load, which is what the workspace and\n // mounts pair above already refuses to do.\n // `profiles` and `profile` join them for the same reason, and one\n // more: an adopted workspace's roles and its `onAsk` are the\n // embedder's own, so silently re-answering its asks through\n // `ctx.approval` would take over a seam it may already have filled.\n if (\n config.runtimes !== undefined ||\n config.workspaceOptions !== undefined ||\n config.profiles !== undefined ||\n config.profile !== undefined\n ) {\n throw new Error(\n 'mirage: runtimes, profiles, profile and workspaceOptions configure a service-owned workspace, not an adopted workspace',\n )\n }\n this.built = config.workspace\n this.ready = Promise.resolve(config.workspace)\n return\n }\n if (config.mounts === undefined) {\n throw new Error('mirage: one of workspace or mounts is required')\n }\n const options = { ...(config.workspaceOptions ?? {}) }\n if (config.runtimes !== undefined) {\n if (options.runtimes !== undefined) {\n throw new Error('mirage: pass runtimes or workspaceOptions.runtimes, not both')\n }\n options.runtimes = config.runtimes.map(toRuntimeEntry)\n }\n if (config.profiles !== undefined) {\n if (options.profiles !== undefined) {\n throw new Error('mirage: pass profiles or workspaceOptions.profiles, not both')\n }\n options.profiles = parseProfiles(config.profiles)\n }\n if (config.profile !== undefined) {\n if (options.profile !== undefined) {\n throw new Error('mirage: pass profile or workspaceOptions.profile, not both')\n }\n options.profile = config.profile\n }\n // How an asked line gets answered. Installed for every service-owned\n // workspace rather than only under a composed `ctx.approval`, because\n // the two are one answer to mirage (an unset `onAsk` and a handler\n // returning null both leave the ask pending) and installing always is\n // what finds an approver composed after this plugin. An explicit\n // `workspaceOptions.onAsk` is the embedder's decision and stands.\n options.onAsk ??= askThroughApproval(ctx)\n this.plannedRuntimes = options.runtimes?.map(builtEntry)\n if (this.plannedRuntimes !== undefined) options.runtimes = [...this.plannedRuntimes]\n const blocks = Object.values(config.mounts).some(isMountBlock)\n if (blocks) {\n this.ready = this.open(config.mounts, options)\n // A declarative mount resolves asynchronously and nothing awaits\n // `ready` until a provider's first call, so a failure here would\n // reach the process as an unhandled rejection and take the whole\n // harness down over a typo in a patch file. This only marks the\n // rejection observed; `ready` still rejects for every real caller,\n // and the disposer below still sees it.\n void this.ready.catch(() => undefined)\n } else {\n const owned = new Workspace(config.mounts as Record<string, MountSpec | Mount>, options)\n this.built = owned\n this.ready = Promise.resolve(owned)\n }\n ctx.effect(\n () => async () => {\n const ws = await this.ready.catch(() => null)\n if (ws !== null) await ws.close()\n },\n 'mirage workspace',\n )\n }\n\n /** The live workspace; throws until `ready` resolves. */\n get workspace(): Workspace {\n if (this.built === null) {\n throw new Error('mirage: workspace is not ready yet; await ctx.mirage.ready')\n }\n return this.built\n }\n\n /**\n * The decision ledger: every ask this world has raised and every\n * answer given, questions still waiting included.\n *\n * The host's handle on the ask layer. A composition with an approval\n * channel answers inline and rarely reads this; one without it finds\n * its pending questions here (`pending()`) and answers them out of\n * band (`answer(id, outcome, scope)`), which is what makes an `ask`\n * rule usable with no human at the keyboard.\n */\n get decisions(): Workspace['decisions'] {\n return this.workspace.decisions\n }\n\n /**\n * What a line would do here, without doing it: one `Explanation` per\n * command, carrying the outcome, the rule that spoke and the exact\n * exit code and stderr the agent would get.\n *\n * A dry run through the same gate the dispatcher uses, so a host\n * reading this and an agent typing the line cannot be told different\n * things. It spends no ONCE answer, records no question and never\n * reaches the approval channel, which is what makes it safe to call\n * about a line nobody typed — a pre-execute check, or an operator\n * asking why a command was refused.\n *\n * @param line the command line to judge.\n * @param sessionId the session to judge it in; the default when empty.\n * @returns one explanation per command of the line.\n */\n async explain(line: string, sessionId = ''): Promise<Explanation[]> {\n const ws = await this.ready\n return ws.explain(line, sessionId)\n }\n\n /**\n * True while every runtime in the world reaches only the vfs\n * (`Runtime.reach`), meaning the workspace dispatch is the single\n * gate for anything the agent can execute: every effect passes\n * mount modes, session grants, and policy before touching the real\n * backend behind a mount. The default world qualifies, and so do\n * the bridged engines, while the host `local` python or a remote\n * sandbox can act around the gate. Answers before `ready` from the\n * constructor-resolved entries (mounts resolve asynchronously,\n * runtimes never do) and from the live workspace afterwards, so a\n * later `addRuntime` is seen.\n */\n get vfsOnly(): boolean {\n const entries = this.built === null ? (this.plannedRuntimes ?? []) : this.built.runtimeEntries\n return entries.every((entry) => entry.reach === 'vfs')\n }\n\n private async open(\n mounts: Record<string, MirageMount>,\n options: NodeWorkspaceOptions,\n ): Promise<Workspace> {\n const resolved: Record<string, MountSpec | Mount> = {}\n for (const [prefix, entry] of Object.entries(mounts)) {\n resolved[prefix] = isMountBlock(entry) ? await resolveMount(entry) : entry\n }\n const ws = new Workspace(resolved, options)\n this.built = ws\n return ws\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fs.ts","../src/errors.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 { Buffer } from 'node:buffer'\nimport { posix } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'\nimport type {\n FsDirEntry,\n FsEditOutcome,\n FsEditRequest,\n FsInfo,\n FsPathInfo,\n FsTarget,\n FsWriteIntent,\n FsWriteOutcome,\n} from '@deepseek-ai/dsh-fs'\nimport type { Ops } from '@struktoai/mirage-core/ops/ops'\nimport { FileType } from '@struktoai/mirage-core/types'\nimport type { FileStat } from '@struktoai/mirage-core/types'\nimport { isMissingPath } from '@struktoai/mirage-core/utils/errors'\nimport { compareCodePoints } from '@struktoai/mirage-core/utils/sort'\nimport { assertNotAborted, mapMirageError } from './errors.ts'\nimport {\n applyLiteralEdit,\n decodeStrictText,\n detectsCrlf,\n normalizeLineEndings,\n restoreLineEndings,\n} from './text.ts'\nimport type {} from './service.ts'\n\ntype LinksSeam = NonNullable<Ops['links']>\n\n// Read off the seam's own signature rather than imported: the policy type\n// lives in `@deepseek-ai/dsh-sandbox`, which reaches this package only as a\n// transitive dependency of dsh-fs, and naming a fourth exact-pinned peer to\n// spell one parameter would couple the adapter to a package it never calls.\ntype SandboxPolicy = Parameters<FileSystem['writeText']>[4]\n\nconst DEFAULT_DIFF_BASIS_MAX_BYTES = 10 * 1024 * 1024\n\n/** Configuration for the mirage filesystem backend. */\nexport interface MirageFsConfig {\n /** Virtual base directory for relative paths. Defaults to `/`. */\n cwd?: string\n /** Exclusive byte limit on each overwrite-diff side. Defaults to 10 MiB. */\n diffBasisMaxBytes?: number\n}\n\n// The freshness token behind FsVersion. Backends that report a fingerprint\n// or revision get a content-derived token; the rest fall back to metadata\n// (modified + size), and a backend reporting neither yields a constant, so\n// stale guards degrade to always-fresh there instead of always-stale.\nfunction versionOf(stat: FileStat): FsVersion {\n if (stat.fingerprint !== null) return FsVersion(`fp:${stat.fingerprint}`)\n if (stat.revision !== null) return FsVersion(`rev:${stat.revision}`)\n if (stat.modified !== null || stat.size !== null) {\n return FsVersion(`meta:${stat.modified ?? ''}:${String(stat.size ?? '')}`)\n }\n return FsVersion('unversioned')\n}\n\nfunction typeOf(stat: FileStat): FsInfo['type'] {\n switch (stat.type) {\n case FileType.DIRECTORY:\n return 'directory'\n case FileType.SYMLINK:\n return 'other'\n default:\n return 'file'\n }\n}\n\nasync function* singleChunk(text: string): AsyncGenerator<string, void, void> {\n yield await Promise.resolve(text)\n}\n\nfunction tooLarge(displayPath: string, maxBytes: number, size?: number): FsError {\n const found = size === undefined ? '' : `${String(size)} bytes `\n return new FsError(\n `cannot read \"${displayPath}\": ${found}exceeds the ${String(maxBytes)} byte cap`,\n 'FS_TOO_LARGE',\n )\n}\n\n/**\n * Mirage-backed implementation of `ctx.fs`. Targets are canonical virtual\n * paths (namespace symlinks followed), every operation walks the workspace\n * op door — session grants, admission policies, cache read-through and\n * post-write invalidation all fire exactly as they do for a shell command —\n * and `processPath` answers in the same virtual path space the mirage shell\n * executes in, so the two providers share one execution world.\n *\n * One limit worth stating: mirage's op facade takes no `AbortSignal`, so\n * cancellation is honored at this adapter's own boundaries (before a\n * dispatch, between listing entries) and not inside a single op. A long\n * read from a remote backend therefore runs to completion after the\n * signal fires, and the caller learns of the abort when it returns.\n */\nexport class MirageFileSystem extends FileSystem {\n static readonly inject = ['mirage']\n\n private fsOps: Ops | null = null\n private readonly cwd: string\n private readonly diffBasisMaxBytes: number\n // Per-targetKey tail promise: serializes mutating ops so the\n // read -> guard -> write window cannot interleave (one concurrent writer\n // wins, the rest observe the new version and reject as stale).\n private readonly locks = new Map<string, Promise<void>>()\n\n constructor(ctx: Context, config: MirageFsConfig = {}) {\n super(ctx)\n this.cwd = config.cwd ?? '/'\n this.diffBasisMaxBytes = config.diffBasisMaxBytes ?? DEFAULT_DIFF_BASIS_MAX_BYTES\n }\n\n // The workspace may still be building (declarative mounts resolve\n // asynchronously), so every entry point awaits the service's `ready`\n // once and caches the op door. The caller's signal can fire during\n // that wait, after its entry assertion passed, so it is asserted\n // again here, before the op it guards dispatches.\n private async ops(signal?: AbortSignal, operation = 'ready'): Promise<Ops> {\n this.fsOps ??= (await this.ctx.mirage.ready).fs\n assertNotAborted(signal, operation)\n return this.fsOps\n }\n\n private get links(): LinksSeam | null {\n if (this.fsOps === null) {\n throw new Error('mirage: filesystem used before the workspace is ready')\n }\n return this.fsOps.links\n }\n\n /**\n * The same confinement claim `MirageShellExecutor` makes, off the same\n * fact and for the same reason: with every runtime reaching only the\n * vfs, a mutation cannot land anywhere but a mount, under its mode.\n *\n * The two seams sit over one world, so answering differently here\n * would let dsh fence a bash write and wave an identical `ctx.fs`\n * write straight through.\n */\n override get sandboxMode(): FileSystem['sandboxMode'] {\n return this.ctx.mirage.vfsOnly ? 'workspace-write' : undefined\n }\n\n /**\n * Refuse a mutation the call's sandbox policy does not allow.\n *\n * `workspaceRoot` is deliberately not consulted: it is a directory on\n * the harness's own machine, so containment against it says nothing\n * about this world. The mounts and their modes are the boundary, and\n * `read-only` is the one mode that narrows them further. Wording and\n * code mirror `dsh-fs-sandbox`, so the tool layer renders one denial\n * marker whichever backend refused.\n *\n * @param policy the per-call policy, absent for an unguarded mutation.\n * @param displayPath the path to name in the refusal.\n */\n private assertMutable(policy: SandboxPolicy, displayPath: string): void {\n if (policy?.mode !== 'read-only') return\n throw new FsError(\n `cannot write \"${displayPath}\": file access denied under read-only mode`,\n 'FS_SANDBOX_DENIED',\n )\n }\n\n /**\n * The base a relative path resolves against.\n *\n * dsh hands `ctx.fs` either the calling session's cwd or the sandbox\n * policy's workspace root, and both are directories on the harness's\n * own machine that name nothing here. Resolving `notes.txt` against\n * one yields `/Users/.../notes.txt`, which every read then reports as\n * absent. So a base that is not a directory in this world falls back\n * to the configured one, the same rule the shell executor applies to\n * a workdir.\n *\n * An absolute path ignores its base, so it never pays for the probe.\n *\n * @param path the path being resolved.\n * @param cwd the caller's base, if any.\n * @returns the base to resolve against.\n */\n private async resolveBase(path: string, cwd: string | undefined): Promise<string> {\n if (cwd === undefined || posix.isAbsolute(path)) return this.cwd\n const ws = await this.ctx.mirage.ready\n return (await ws.fs.isDir(cwd)) ? cwd : this.cwd\n }\n\n private normalize(path: string, base: string): string {\n return posix.resolve(base, path)\n }\n\n private follow(path: string): string {\n const links = this.links\n if (links === null) return path\n try {\n return links.follow(path)\n } catch (err) {\n throw mapMirageError(err, 'resolve', path)\n }\n }\n\n private withLock<T>(key: FsTargetKey, body: () => Promise<T>): Promise<T> {\n const mapKey = String(key)\n const previous = this.locks.get(mapKey) ?? Promise.resolve()\n const next = previous.then(body, body)\n // The stored tail is void (an outcome would pin whole file contents in\n // the map) and drops on settlement unless a newer op queued behind it.\n const tail = next.then(\n () => undefined,\n () => undefined,\n )\n this.locks.set(mapKey, tail)\n void tail.then(() => {\n if (this.locks.get(mapKey) === tail) this.locks.delete(mapKey)\n })\n return next\n }\n\n async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {\n assertNotAborted(opts?.signal, 'resolve')\n await this.ops(opts?.signal, 'resolve')\n const followed = this.follow(this.normalize(path, await this.resolveBase(path, opts?.cwd)))\n return { targetKey: FsTargetKey(followed), displayPath: followed }\n }\n\n processPath(target: FsTarget): string {\n return String(target.targetKey)\n }\n\n fileUrl(target: FsTarget): string {\n const encoded = String(target.targetKey).split('/').map(encodeURIComponent).join('/')\n return `file://${encoded}`\n }\n\n contains(parent: FsTarget, child: FsTarget): boolean {\n const parentKey = String(parent.targetKey)\n const childKey = String(child.targetKey)\n if (parentKey === childKey) return true\n const prefix = parentKey === '/' ? '/' : `${parentKey}/`\n return childKey.startsWith(prefix)\n }\n\n async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {\n assertNotAborted(signal, 'stat')\n const key = String(target.targetKey)\n let stat: FileStat\n try {\n stat = await (await this.ops(signal, 'stat')).stat(key)\n } catch (err) {\n if (isMissingPath(err)) return undefined\n throw mapMirageError(err, 'stat', target.displayPath)\n }\n return {\n version: versionOf(stat),\n type: typeOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async lstat(\n path: string,\n opts?: { cwd?: string },\n signal?: AbortSignal,\n ): Promise<FsPathInfo | undefined> {\n assertNotAborted(signal, 'lstat')\n await this.ops(signal, 'lstat')\n const normalized = this.normalize(path, await this.resolveBase(path, opts?.cwd))\n // Follow every component except the last: the probe is about the path\n // entry itself, so a link at the leaf must report as one.\n const parentFollowed =\n normalized === '/'\n ? '/'\n : posix.join(this.follow(posix.dirname(normalized)), posix.basename(normalized))\n const links = this.links\n if (links?.isLink(parentFollowed) === true) {\n const linkTarget = links.readlink(parentFollowed) ?? ''\n return {\n version: FsVersion(`link:${linkTarget}`),\n type: 'symlink',\n size: new TextEncoder().encode(linkTarget).byteLength,\n }\n }\n let stat: FileStat\n try {\n stat = await (await this.ops(signal, 'lstat')).stat(parentFollowed)\n } catch (err) {\n if (isMissingPath(err)) return undefined\n throw mapMirageError(err, 'lstat', normalized)\n }\n return {\n version: versionOf(stat),\n type: typeOf(stat) === 'directory' ? 'directory' : typeOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async readText(target: FsTarget, signal?: AbortSignal): Promise<string> {\n assertNotAborted(signal, 'read')\n const key = String(target.targetKey)\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops(signal, 'read')).readFile(key)\n } catch (err) {\n throw mapMirageError(err, 'read', target.displayPath)\n }\n return decodeStrictText(bytes, target.displayPath)\n }\n\n async streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> {\n // mirage reads land as one rendered buffer, so streaming is a single\n // chunk with readText's exact text semantics.\n const text = await this.readText(target, signal)\n return singleChunk(text)\n }\n\n async readBytes(\n target: FsTarget,\n signal: AbortSignal | undefined,\n maxBytes: number,\n ): Promise<Uint8Array> {\n assertNotAborted(signal, 'read')\n const info = await this.stat(target, signal)\n if (info !== undefined && info.type !== 'file') {\n // A backend read of a directory key surfaces as a missing path, which\n // would report a path that plainly exists as absent.\n throw new FsError(\n `cannot read \"${target.displayPath}\": not a regular file`,\n 'FS_NOT_REGULAR_FILE',\n )\n }\n if (info?.size !== undefined && info.size > maxBytes) {\n throw tooLarge(target.displayPath, maxBytes, info.size)\n }\n // Ask for one byte past the cap rather than the whole object: a backend\n // with a native range fetches only that window, and a full-length answer\n // is itself the proof the file is over the cap, which is what a mount\n // reporting no size (most API mounts) has no other way to establish.\n const key = String(target.targetKey)\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops(signal, 'read')).readFile(key, { size: maxBytes + 1 })\n } catch (err) {\n throw mapMirageError(err, 'read', target.displayPath)\n }\n if (bytes.byteLength > maxBytes) {\n throw tooLarge(target.displayPath, maxBytes)\n }\n return bytes\n }\n\n async listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]> {\n assertNotAborted(signal, 'list')\n const key = String(target.targetKey)\n const info = await this.stat(target, signal)\n if (info === undefined) {\n throw new FsError(\n `cannot list \"${target.displayPath}\": no such file or directory`,\n 'FS_NOT_FOUND',\n )\n }\n if (info.type !== 'directory') {\n throw new FsError(`cannot list \"${target.displayPath}\": not a directory`, 'FS_NOT_DIRECTORY')\n }\n let children: string[]\n try {\n children = await (await this.ops(signal, 'list')).readdir(key)\n } catch (err) {\n throw mapMirageError(err, 'list', target.displayPath)\n }\n const names = new Set<string>()\n for (const childPath of children) {\n names.add(posix.basename(childPath))\n }\n // Namespace symlinks live above every backend, so a listing merges\n // them in here exactly as mirage's own ls does.\n const links = this.links\n if (links !== null) {\n const base = key === '/' ? '/' : `${key}/`\n for (const linkPath of links.symlinkTargets().keys()) {\n if (linkPath.startsWith(base) && !linkPath.slice(base.length).includes('/')) {\n names.add(linkPath.slice(base.length))\n }\n }\n }\n const entries: FsDirEntry[] = []\n for (const name of [...names].sort(compareCodePoints)) {\n // Per entry, not just at the door: a listing is one classification\n // round trip per child on a backend the readdir did not warm, and\n // a caller that gave up should stop paying for them.\n assertNotAborted(signal, 'list')\n entries.push(await this.dirEntry(key, name, signal))\n }\n return entries\n }\n\n private async dirEntry(\n parentKey: string,\n name: string,\n signal?: AbortSignal,\n ): Promise<FsDirEntry> {\n const childPath = parentKey === '/' ? `/${name}` : `${parentKey}/${name}`\n let followed: string\n try {\n followed = this.follow(childPath)\n } catch {\n // A cyclic link must not reject the whole listing; it lists as an\n // entry of unknown kind keyed on its own path.\n return {\n name,\n type: 'other',\n target: { targetKey: FsTargetKey(childPath), displayPath: childPath },\n }\n }\n const target: FsTarget = { targetKey: FsTargetKey(followed), displayPath: childPath }\n let stat: FileStat\n try {\n stat = await (await this.ops(signal, 'list')).stat(followed)\n } catch (err) {\n // An abort is the caller withdrawing, not a child this listing\n // failed to classify, so it ends the walk instead of landing as\n // one more entry of unknown kind.\n if (err instanceof FsError && err.code === 'FS_ABORTED') throw err\n // A child the listing named but stat cannot classify (a broken link,\n // a race with a delete) still lists, as an entry of unknown kind.\n return { name, type: 'other', target }\n }\n return {\n name,\n type: typeOf(stat),\n target,\n version: versionOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async writeText(\n target: FsTarget,\n content: string,\n expected?: FsWriteIntent,\n signal?: AbortSignal,\n sandboxPolicy?: SandboxPolicy,\n ): Promise<FsWriteOutcome> {\n return this.withLock(target.targetKey, async () => {\n assertNotAborted(signal, 'write')\n this.assertMutable(sandboxPolicy, target.displayPath)\n const key = String(target.targetKey)\n const existing = await this.stat(target, signal)\n if (existing !== undefined && existing.type !== 'file') {\n throw new FsError(\n `cannot write \"${target.displayPath}\": not a regular file`,\n 'FS_NOT_REGULAR_FILE',\n )\n }\n if (expected?.kind === 'createIfAbsent' && existing !== undefined) {\n throw new FsError(\n `cannot write \"${target.displayPath}\": file already exists`,\n 'FS_NOT_OBSERVED',\n )\n }\n if (expected?.kind === 'replaceIfVersion') {\n if (existing?.version !== expected.version) {\n throw new FsError(\n `cannot write \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n }\n const before = existing === undefined ? null : await this.diffBasis(target, content)\n const crlf = before !== null && detectsCrlf(before)\n try {\n await (\n await this.ops(signal, 'write')\n ).writeFile(key, restoreLineEndings(normalizeLineEndings(content), crlf))\n } catch (err) {\n throw mapMirageError(err, 'write', target.displayPath)\n }\n return {\n operation: existing === undefined ? ('create' as const) : ('update' as const),\n version: await this.versionAfterWrite(target),\n before: before === null ? null : normalizeLineEndings(before),\n after: normalizeLineEndings(content),\n }\n })\n }\n\n // The overwrite-diff basis: the prior content when it is text and both\n // sides fit the exclusive limit, else null (dsh renders a whole-file\n // diff then). Never fails the write itself.\n private async diffBasis(target: FsTarget, content: string): Promise<string | null> {\n if (Buffer.byteLength(content, 'utf8') >= this.diffBasisMaxBytes) return null\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops()).readFile(String(target.targetKey))\n } catch {\n return null\n }\n if (bytes.byteLength >= this.diffBasisMaxBytes) return null\n try {\n return decodeStrictText(bytes, target.displayPath)\n } catch {\n return null\n }\n }\n\n private async versionAfterWrite(target: FsTarget): Promise<FsVersion> {\n const after = await this.stat(target)\n return after?.version ?? FsVersion(`missing:${String(target.targetKey)}`)\n }\n\n async editText(\n target: FsTarget,\n edit: FsEditRequest,\n expected?: { version: FsVersion },\n signal?: AbortSignal,\n sandboxPolicy?: SandboxPolicy,\n ): Promise<FsEditOutcome> {\n return this.withLock(target.targetKey, async () => {\n assertNotAborted(signal, 'edit')\n this.assertMutable(sandboxPolicy, target.displayPath)\n const key = String(target.targetKey)\n const existing = await this.stat(target, signal)\n // Stale guard before literal matching: an edit based on an old read\n // reports FS_STALE_VERSION, not a match failure against newer content.\n if (existing === undefined) {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n if (existing.type !== 'file') {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": not a regular file`,\n 'FS_NOT_REGULAR_FILE',\n )\n }\n if (expected !== undefined && existing.version !== expected.version) {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops(signal, 'edit')).readFile(key)\n } catch (err) {\n throw mapMirageError(err, 'edit', target.displayPath)\n }\n const raw = decodeStrictText(bytes, target.displayPath)\n const original = normalizeLineEndings(raw)\n const edited = applyLiteralEdit(\n original,\n normalizeLineEndings(edit.oldString),\n normalizeLineEndings(edit.newString),\n edit.replaceAll,\n target.displayPath,\n )\n try {\n await (\n await this.ops(signal, 'edit')\n ).writeFile(key, restoreLineEndings(edited, detectsCrlf(raw)))\n } catch (err) {\n throw mapMirageError(err, 'edit', target.displayPath)\n }\n return {\n version: await this.versionAfterWrite(target),\n before: original,\n after: edited,\n }\n })\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 { FsError } from '@deepseek-ai/dsh-fs'\nimport type { FsErrorCode } from '@deepseek-ai/dsh-fs'\nimport { isMissingPath } from '@struktoai/mirage-core/utils/errors'\n\nexport function assertNotAborted(signal: AbortSignal | undefined, operation: string): void {\n if (signal?.aborted === true) {\n throw new FsError(`${operation} aborted`, 'FS_ABORTED')\n }\n}\n\nfunction codeOf(err: unknown): string | null {\n if (err === null || typeof err !== 'object') return null\n const stamped = err as { code?: unknown }\n return typeof stamped.code === 'string' ? stamped.code : null\n}\n\n// mirage stamps POSIX codes on its errors (ENOENT, EISDIR, ENOTDIR, EACCES,\n// EROFS, EXDEV; PolicyDenied carries EACCES too), so the dsh taxonomy is\n// derived from the stamp rather than from message sniffing or class checks\n// that would couple this adapter to mirage internals.\nfunction codeFor(err: unknown): FsErrorCode {\n if (isMissingPath(err)) return 'FS_NOT_FOUND'\n // An admission policy refused the op at the door: the session's\n // permission document, or a code policy registered beside it. EACCES is\n // too coarse to say so alone, since a plain mount-mode refusal carries\n // it too, and the two want different answers — a mode refusal is the\n // shape of this world, while a policy refusal is a confinement the call\n // may be entitled to escalate past. So it maps to the sandbox code,\n // which is what makes dsh's tool layer attach the escalation hint\n // (`FsSandboxController.mapError` keys on exactly this code) and render\n // the same denial marker bash gets for a refused command.\n //\n // Matched on `name`, like the AbortError case below and for the same\n // reason: core's PolicyDenied documents that name as the discriminator\n // for \"handlers that special-case mount-mode refusals\", and matching it\n // needs no import of a class this adapter never constructs.\n if (err instanceof Error && err.name === 'PolicyDenied') return 'FS_SANDBOX_DENIED'\n switch (codeOf(err)) {\n case 'EISDIR':\n return 'FS_NOT_REGULAR_FILE'\n case 'ENOTDIR':\n return 'FS_NOT_DIRECTORY'\n case 'EACCES':\n case 'EPERM':\n case 'EROFS':\n return 'FS_PERMISSION_DENIED'\n default:\n return 'FS_IO_ERROR'\n }\n}\n\nexport function mapMirageError(err: unknown, operation: string, displayPath: string): FsError {\n if (err instanceof FsError) return err\n // Matched by name, not by class: mirage's own executor throws a\n // `DOMException`, but an aborted fetch or a backend SDK may throw a\n // plain `Error` named the same, and reporting that as FS_IO_ERROR\n // would tell the caller its own cancellation was a backend failure.\n if (err instanceof Error && err.name === 'AbortError') {\n return new FsError(`${operation} aborted`, 'FS_ABORTED', { cause: err })\n }\n const message = err instanceof Error ? err.message : String(err)\n return new FsError(`cannot ${operation} \"${displayPath}\": ${message}`, codeFor(err), {\n cause: err,\n })\n}\n"],"mappings":"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","names":["FsError","isMissingPath","FsError","isMissingPath"]}
|
|
@@ -1 +0,0 @@
|
|
|
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":"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","names":["Channel","Channel"]}
|