@struktoai/mirage-dsh 0.0.1-alpha.1
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 +54 -0
- package/dist/chunk-C67TSSYB.js +392 -0
- package/dist/chunk-C67TSSYB.js.map +1 -0
- package/dist/chunk-CATVBGGR.js +70 -0
- package/dist/chunk-CATVBGGR.js.map +1 -0
- package/dist/chunk-J6F4OX6Z.js +102 -0
- package/dist/chunk-J6F4OX6Z.js.map +1 -0
- package/dist/chunk-ZSZVHBSL.js +455 -0
- package/dist/chunk-ZSZVHBSL.js.map +1 -0
- package/dist/fs-CuiUEA7A.d.ts +54 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/fs.d.ts +3 -0
- package/dist/plugin/fs.js +8 -0
- package/dist/plugin/fs.js.map +1 -0
- package/dist/plugin/service.d.ts +4 -0
- package/dist/plugin/service.js +7 -0
- package/dist/plugin/service.js.map +1 -0
- package/dist/plugin/shell.d.ts +3 -0
- package/dist/plugin/shell.js +8 -0
- package/dist/plugin/shell.js.map +1 -0
- package/dist/service-CxFYAvoV.d.ts +91 -0
- package/dist/shell-B0N_jppN.d.ts +108 -0
- package/package.json +71 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/service.ts
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { buildRuntime, parseMountMode } from "@struktoai/mirage-core";
|
|
4
|
+
import { buildResource, Workspace } from "@struktoai/mirage-node";
|
|
5
|
+
function isMountBlock(entry) {
|
|
6
|
+
if (Array.isArray(entry)) return false;
|
|
7
|
+
return typeof entry.resource === "string";
|
|
8
|
+
}
|
|
9
|
+
async function resolveMount(entry) {
|
|
10
|
+
const resource = await buildResource(entry.resource, entry.config ?? {});
|
|
11
|
+
if (entry.mode === void 0) return resource;
|
|
12
|
+
return [resource, parseMountMode(entry.mode)];
|
|
13
|
+
}
|
|
14
|
+
function toRuntimeEntry(entry) {
|
|
15
|
+
if (typeof entry === "string") return entry;
|
|
16
|
+
return buildRuntime(entry.name, entry.captures === void 0 ? {} : { captures: entry.captures });
|
|
17
|
+
}
|
|
18
|
+
function builtEntry(entry) {
|
|
19
|
+
return typeof entry === "string" ? buildRuntime(entry) : entry;
|
|
20
|
+
}
|
|
21
|
+
var MirageService = class extends Service {
|
|
22
|
+
static provide = "mirage";
|
|
23
|
+
/** Resolves to the shared workspace once every mount is built. */
|
|
24
|
+
ready;
|
|
25
|
+
built = null;
|
|
26
|
+
plannedRuntimes;
|
|
27
|
+
constructor(ctx, config = {}) {
|
|
28
|
+
super(ctx, "mirage");
|
|
29
|
+
if (config.workspace !== void 0 && config.mounts !== void 0) {
|
|
30
|
+
throw new Error("mirage: pass either workspace or mounts, not both");
|
|
31
|
+
}
|
|
32
|
+
if (config.workspace !== void 0) {
|
|
33
|
+
this.built = config.workspace;
|
|
34
|
+
this.ready = Promise.resolve(config.workspace);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (config.mounts === void 0) {
|
|
38
|
+
throw new Error("mirage: one of workspace or mounts is required");
|
|
39
|
+
}
|
|
40
|
+
const options = { ...config.workspaceOptions ?? {} };
|
|
41
|
+
if (config.runtimes !== void 0) {
|
|
42
|
+
if (options.runtimes !== void 0) {
|
|
43
|
+
throw new Error("mirage: pass runtimes or workspaceOptions.runtimes, not both");
|
|
44
|
+
}
|
|
45
|
+
options.runtimes = config.runtimes.map(toRuntimeEntry);
|
|
46
|
+
}
|
|
47
|
+
this.plannedRuntimes = options.runtimes?.map(builtEntry);
|
|
48
|
+
if (this.plannedRuntimes !== void 0) options.runtimes = [...this.plannedRuntimes];
|
|
49
|
+
const blocks = Object.values(config.mounts).some(isMountBlock);
|
|
50
|
+
if (blocks) {
|
|
51
|
+
this.ready = this.open(config.mounts, options);
|
|
52
|
+
} else {
|
|
53
|
+
const owned = new Workspace(config.mounts, options);
|
|
54
|
+
this.built = owned;
|
|
55
|
+
this.ready = Promise.resolve(owned);
|
|
56
|
+
}
|
|
57
|
+
ctx.effect(
|
|
58
|
+
() => async () => {
|
|
59
|
+
const ws = await this.ready.catch(() => null);
|
|
60
|
+
if (ws !== null) await ws.close();
|
|
61
|
+
},
|
|
62
|
+
"mirage workspace"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
/** The live workspace; throws until `ready` resolves. */
|
|
66
|
+
get workspace() {
|
|
67
|
+
if (this.built === null) {
|
|
68
|
+
throw new Error("mirage: workspace is not ready yet; await ctx.mirage.ready");
|
|
69
|
+
}
|
|
70
|
+
return this.built;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* True while every runtime in the world reaches only the vfs
|
|
74
|
+
* (`Runtime.reach`), meaning the workspace dispatch is the single
|
|
75
|
+
* gate for anything the agent can execute: every effect passes
|
|
76
|
+
* mount modes, session grants, and policy before touching the real
|
|
77
|
+
* backend behind a mount. The default world qualifies, and so do
|
|
78
|
+
* the bridged engines, while the host `local` python or a remote
|
|
79
|
+
* sandbox can act around the gate. Answers before `ready` from the
|
|
80
|
+
* constructor-resolved entries (mounts resolve asynchronously,
|
|
81
|
+
* runtimes never do) and from the live workspace afterwards, so a
|
|
82
|
+
* later `addRuntime` is seen.
|
|
83
|
+
*/
|
|
84
|
+
get vfsOnly() {
|
|
85
|
+
const entries = this.built === null ? this.plannedRuntimes ?? [] : this.built.runtimeEntries;
|
|
86
|
+
return entries.every((entry) => entry.reach === "vfs");
|
|
87
|
+
}
|
|
88
|
+
async open(mounts, options) {
|
|
89
|
+
const resolved = {};
|
|
90
|
+
for (const [prefix, entry] of Object.entries(mounts)) {
|
|
91
|
+
resolved[prefix] = isMountBlock(entry) ? await resolveMount(entry) : entry;
|
|
92
|
+
}
|
|
93
|
+
const ws = new Workspace(resolved, options);
|
|
94
|
+
this.built = ws;
|
|
95
|
+
return ws;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
MirageService
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=chunk-J6F4OX6Z.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../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 { Service } from '@deepseek-ai/cordis'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { MountSpec, Runtime, RuntimeEntry } from '@struktoai/mirage-core'\nimport { buildRuntime, parseMountMode } from '@struktoai/mirage-core'\nimport { buildResource, Workspace } from '@struktoai/mirage-node'\nimport type { Mount, NodeWorkspaceOptions } from '@struktoai/mirage-node'\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 /** 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\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 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 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 } 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 * 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":[]}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import {
|
|
2
|
+
tailCap
|
|
3
|
+
} from "./chunk-CATVBGGR.js";
|
|
4
|
+
|
|
5
|
+
// src/shell.ts
|
|
6
|
+
import { ShellExecutor } from "@deepseek-ai/dsh-shell";
|
|
7
|
+
import {
|
|
8
|
+
Channel as Channel2,
|
|
9
|
+
JobConsole,
|
|
10
|
+
RAMConsoleStore,
|
|
11
|
+
exitOutcome,
|
|
12
|
+
KILLED_OUTCOME
|
|
13
|
+
} from "@struktoai/mirage-core";
|
|
14
|
+
|
|
15
|
+
// src/spill.ts
|
|
16
|
+
import { Channel } from "@struktoai/mirage-core";
|
|
17
|
+
async function ensureDirPath(dirs, dir) {
|
|
18
|
+
let path = "";
|
|
19
|
+
for (const part of dir.split("/").filter((p) => p !== "")) {
|
|
20
|
+
path += `/${part}`;
|
|
21
|
+
if (await dirs.exists(path)) continue;
|
|
22
|
+
try {
|
|
23
|
+
await dirs.mkdir(path);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (!await dirs.exists(path)) throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function totalLength(parts) {
|
|
30
|
+
return parts.reduce((sum, p) => sum + p.byteLength, 0);
|
|
31
|
+
}
|
|
32
|
+
function concat(parts) {
|
|
33
|
+
const out = new Uint8Array(totalLength(parts));
|
|
34
|
+
let at = 0;
|
|
35
|
+
for (const p of parts) {
|
|
36
|
+
out.set(p, at);
|
|
37
|
+
at += p.byteLength;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
var SpillSink = class {
|
|
42
|
+
stdoutPath;
|
|
43
|
+
stderrPath;
|
|
44
|
+
target;
|
|
45
|
+
dir;
|
|
46
|
+
base;
|
|
47
|
+
started = false;
|
|
48
|
+
failed = false;
|
|
49
|
+
stdoutParts = [];
|
|
50
|
+
stderrParts = [];
|
|
51
|
+
constructor(target, dir, base) {
|
|
52
|
+
this.target = target;
|
|
53
|
+
this.dir = dir;
|
|
54
|
+
this.base = base;
|
|
55
|
+
}
|
|
56
|
+
/** Buffer a chunk before spill starts, or append it to the file after. */
|
|
57
|
+
async ingest(channel, data) {
|
|
58
|
+
if (this.failed) return;
|
|
59
|
+
if (!this.started) {
|
|
60
|
+
if (channel === Channel.STDERR) this.stderrParts.push(data);
|
|
61
|
+
else this.stdoutParts.push(data);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await this.appendFile(channel, data);
|
|
65
|
+
}
|
|
66
|
+
/** Flush the buffered streams to files; called once, on the first overrun. */
|
|
67
|
+
async begin() {
|
|
68
|
+
if (this.started || this.failed) return;
|
|
69
|
+
try {
|
|
70
|
+
await this.target.ensureDir(this.dir);
|
|
71
|
+
if (totalLength(this.stdoutParts) > 0) {
|
|
72
|
+
this.stdoutPath = `${this.dir}/${this.base}.stdout`;
|
|
73
|
+
await this.target.write(this.stdoutPath, concat(this.stdoutParts));
|
|
74
|
+
}
|
|
75
|
+
if (totalLength(this.stderrParts) > 0) {
|
|
76
|
+
this.stderrPath = `${this.dir}/${this.base}.stderr`;
|
|
77
|
+
await this.target.write(this.stderrPath, concat(this.stderrParts));
|
|
78
|
+
}
|
|
79
|
+
this.started = true;
|
|
80
|
+
this.stdoutParts = [];
|
|
81
|
+
this.stderrParts = [];
|
|
82
|
+
} catch {
|
|
83
|
+
this.disable();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async appendFile(channel, data) {
|
|
87
|
+
try {
|
|
88
|
+
if (channel === Channel.STDERR) {
|
|
89
|
+
if (this.stderrPath === void 0) {
|
|
90
|
+
this.stderrPath = `${this.dir}/${this.base}.stderr`;
|
|
91
|
+
await this.target.write(this.stderrPath, data);
|
|
92
|
+
} else {
|
|
93
|
+
await this.target.append(this.stderrPath, data);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (this.stdoutPath === void 0) {
|
|
97
|
+
this.stdoutPath = `${this.dir}/${this.base}.stdout`;
|
|
98
|
+
await this.target.write(this.stdoutPath, data);
|
|
99
|
+
} else {
|
|
100
|
+
await this.target.append(this.stdoutPath, data);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
this.disable();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Give up on spilling and report no path.
|
|
109
|
+
*
|
|
110
|
+
* Called on a write failure, and by a reader that discovers it lost
|
|
111
|
+
* bytes before they ever reached here: a file missing the middle of
|
|
112
|
+
* the stream is worse than no file, because the path presents it as
|
|
113
|
+
* the whole one.
|
|
114
|
+
*/
|
|
115
|
+
disable() {
|
|
116
|
+
this.failed = true;
|
|
117
|
+
this.stdoutPath = void 0;
|
|
118
|
+
this.stderrPath = void 0;
|
|
119
|
+
this.stdoutParts = [];
|
|
120
|
+
this.stderrParts = [];
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/shell.ts
|
|
125
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
126
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
127
|
+
var DEFAULT_STDOUT_MAX_BYTES = 2e5;
|
|
128
|
+
var DEFAULT_STDERR_MAX_BYTES = 64e3;
|
|
129
|
+
var STDERR_MARKER = "\n--- stderr ---\n";
|
|
130
|
+
var CONSOLE_RETENTION_BYTES = 5 * DEFAULT_STDOUT_MAX_BYTES;
|
|
131
|
+
var spillCounter = 0;
|
|
132
|
+
function collect(text, maxBytes) {
|
|
133
|
+
const capped = tailCap(text, maxBytes);
|
|
134
|
+
return { text: capped.text, truncated: capped.truncated };
|
|
135
|
+
}
|
|
136
|
+
function executeOptions(spec, signal, sessionId, fallbackWorkdir, sink) {
|
|
137
|
+
const env = {
|
|
138
|
+
...spec.env ?? {},
|
|
139
|
+
...spec.dshEnv ?? {}
|
|
140
|
+
};
|
|
141
|
+
const cwd = spec.workdir !== "" ? spec.workdir : sessionId === void 0 ? fallbackWorkdir : void 0;
|
|
142
|
+
return {
|
|
143
|
+
signal,
|
|
144
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
145
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
146
|
+
...Object.keys(env).length > 0 ? { env } : {},
|
|
147
|
+
...spec.stdin !== void 0 ? { stdin: new TextEncoder().encode(spec.stdin) } : {},
|
|
148
|
+
...sink !== void 0 ? { sink } : {}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
var MirageShellProcess = class {
|
|
152
|
+
status = "running";
|
|
153
|
+
exitCode = null;
|
|
154
|
+
signal = null;
|
|
155
|
+
sandbox;
|
|
156
|
+
done;
|
|
157
|
+
controller;
|
|
158
|
+
console;
|
|
159
|
+
budget;
|
|
160
|
+
spill;
|
|
161
|
+
sandboxInfo;
|
|
162
|
+
consumed;
|
|
163
|
+
pending = "";
|
|
164
|
+
lossy = false;
|
|
165
|
+
inStderr = false;
|
|
166
|
+
settled = false;
|
|
167
|
+
expectSeq = 0;
|
|
168
|
+
constructor(run, controller, console_, budget, spill, sandboxInfo) {
|
|
169
|
+
this.controller = controller;
|
|
170
|
+
this.console = console_;
|
|
171
|
+
this.budget = budget;
|
|
172
|
+
this.spill = spill;
|
|
173
|
+
this.sandboxInfo = sandboxInfo;
|
|
174
|
+
this.consumed = this.consume();
|
|
175
|
+
this.done = run.then(
|
|
176
|
+
(result) => this.settle(result, null),
|
|
177
|
+
(err) => this.settle(null, err)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
async consume() {
|
|
181
|
+
for await (const chunk of this.console.follow(0)) {
|
|
182
|
+
if (chunk.channel === Channel2.CONTROL) return;
|
|
183
|
+
if (chunk.seq !== this.expectSeq) {
|
|
184
|
+
this.lossy = true;
|
|
185
|
+
this.spill?.disable();
|
|
186
|
+
}
|
|
187
|
+
this.expectSeq = chunk.seq + 1;
|
|
188
|
+
await this.appendChunk(chunk);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async appendChunk(chunk) {
|
|
192
|
+
if (this.spill !== null) await this.spill.ingest(chunk.channel, chunk.data);
|
|
193
|
+
const text = new TextDecoder().decode(chunk.data);
|
|
194
|
+
if (chunk.channel === Channel2.STDERR) {
|
|
195
|
+
if (!this.inStderr) {
|
|
196
|
+
this.pending += STDERR_MARKER;
|
|
197
|
+
this.inStderr = true;
|
|
198
|
+
}
|
|
199
|
+
this.pending += text;
|
|
200
|
+
} else {
|
|
201
|
+
this.inStderr = false;
|
|
202
|
+
this.pending += text;
|
|
203
|
+
}
|
|
204
|
+
const capped = tailCap(this.pending, this.budget);
|
|
205
|
+
if (capped.truncated) {
|
|
206
|
+
this.pending = capped.text;
|
|
207
|
+
this.lossy = true;
|
|
208
|
+
if (this.spill !== null) await this.spill.begin();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async settle(result, err) {
|
|
212
|
+
this.settled = true;
|
|
213
|
+
if (this.sandboxInfo !== void 0) this.sandbox = this.sandboxInfo;
|
|
214
|
+
let outcome;
|
|
215
|
+
if (result !== null) {
|
|
216
|
+
this.status = "completed";
|
|
217
|
+
this.exitCode = result.exitCode;
|
|
218
|
+
outcome = exitOutcome(result.exitCode);
|
|
219
|
+
} else {
|
|
220
|
+
this.status = "killed";
|
|
221
|
+
this.signal = "SIGTERM";
|
|
222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
223
|
+
await this.console.emit(Channel2.STDERR, new TextEncoder().encode(message));
|
|
224
|
+
outcome = KILLED_OUTCOME;
|
|
225
|
+
}
|
|
226
|
+
await this.console.finish(outcome);
|
|
227
|
+
await this.consumed;
|
|
228
|
+
}
|
|
229
|
+
readOutput() {
|
|
230
|
+
const delta = this.pending;
|
|
231
|
+
this.pending = "";
|
|
232
|
+
const lossy = this.lossy;
|
|
233
|
+
this.lossy = false;
|
|
234
|
+
return {
|
|
235
|
+
delta,
|
|
236
|
+
lossy,
|
|
237
|
+
...this.spill?.stdoutPath !== void 0 ? { stdoutSpillPath: this.spill.stdoutPath } : {},
|
|
238
|
+
...this.spill?.stderrPath !== void 0 ? { stderrSpillPath: this.spill.stderrPath } : {}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
kill() {
|
|
242
|
+
if (this.settled) return false;
|
|
243
|
+
this.controller.abort();
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var MirageShellExecutor = class extends ShellExecutor {
|
|
248
|
+
static inject = ["mirage"];
|
|
249
|
+
workdir;
|
|
250
|
+
defaultTimeoutMs;
|
|
251
|
+
maxTimeoutMs;
|
|
252
|
+
stdoutMaxBytes;
|
|
253
|
+
stderrMaxBytes;
|
|
254
|
+
sessionId;
|
|
255
|
+
spillDir;
|
|
256
|
+
sessionReady = null;
|
|
257
|
+
constructor(ctx, config = {}) {
|
|
258
|
+
super(ctx);
|
|
259
|
+
this.workdir = config.workdir ?? "/";
|
|
260
|
+
this.defaultTimeoutMs = config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
261
|
+
this.maxTimeoutMs = config.maxTimeoutMs ?? MAX_TIMEOUT_MS;
|
|
262
|
+
this.stdoutMaxBytes = config.stdoutMaxBytes ?? DEFAULT_STDOUT_MAX_BYTES;
|
|
263
|
+
this.stderrMaxBytes = config.stderrMaxBytes ?? DEFAULT_STDERR_MAX_BYTES;
|
|
264
|
+
this.sessionId = config.sessionId;
|
|
265
|
+
this.spillDir = config.spillDir;
|
|
266
|
+
}
|
|
267
|
+
// The workspace may still be building (declarative mounts resolve
|
|
268
|
+
// asynchronously), so every execution awaits the service's `ready`.
|
|
269
|
+
workspace() {
|
|
270
|
+
return this.ctx.mirage.ready;
|
|
271
|
+
}
|
|
272
|
+
// A spill sink for one background command, or null when no spill
|
|
273
|
+
// directory is configured. The target reaches the live workspace so
|
|
274
|
+
// the full stream lands on a mount the agent can read back.
|
|
275
|
+
newSpill() {
|
|
276
|
+
const dir = this.spillDir;
|
|
277
|
+
if (dir === void 0) return null;
|
|
278
|
+
const target = {
|
|
279
|
+
ensureDir: async (d) => {
|
|
280
|
+
const ws = await this.workspace();
|
|
281
|
+
await ensureDirPath({ exists: (p) => ws.fs.exists(p), mkdir: (p) => ws.fs.mkdir(p) }, d);
|
|
282
|
+
},
|
|
283
|
+
write: async (p, bytes) => {
|
|
284
|
+
const ws = await this.workspace();
|
|
285
|
+
await ws.fs.writeFile(p, bytes);
|
|
286
|
+
},
|
|
287
|
+
append: async (p, bytes) => {
|
|
288
|
+
const ws = await this.workspace();
|
|
289
|
+
await ws.fs.append(p, bytes);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
spillCounter += 1;
|
|
293
|
+
return new SpillSink(target, dir, `mirage-shell-${spillCounter.toString()}`);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* With every runtime in the world reaching only the vfs
|
|
297
|
+
* (`ctx.mirage.vfsOnly`), the workspace dispatch is the single gate
|
|
298
|
+
* for anything a command can do, so this executor behaves like a
|
|
299
|
+
* workspace-write sandbox: reads and writes land only where mounts
|
|
300
|
+
* (and their modes) allow. Declaring it lets sandbox-aware plugins
|
|
301
|
+
* (dsh's permission presets) compose over this executor. A world
|
|
302
|
+
* holding a runtime with doors around the gate (the host `local`
|
|
303
|
+
* python, a remote sandbox) voids that claim, so this answers
|
|
304
|
+
* undefined then (the base contract's "does not sandbox") and those
|
|
305
|
+
* plugins refuse to compose instead of trusting a lie.
|
|
306
|
+
*/
|
|
307
|
+
get sandboxMode() {
|
|
308
|
+
return this.ctx.mirage.vfsOnly ? "workspace-write" : void 0;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The sandbox facts to stamp on this run's result and process handle,
|
|
312
|
+
* or undefined when the world is not fully workspace-bound (no claim).
|
|
313
|
+
*
|
|
314
|
+
* `enforcement` is 'full': when every runtime reaches only the vfs, the
|
|
315
|
+
* workspace gate cannot be bypassed, so unlike an OS sandbox on an older
|
|
316
|
+
* kernel there is no promised effect it fails to govern. `denied` is
|
|
317
|
+
* false because mirage has no out-of-band denial channel: a refused write
|
|
318
|
+
* (a read-only mount) fails in-band as an ordinary command error with a
|
|
319
|
+
* nonzero exit, the way EROFS would, not as a separate sandbox verdict,
|
|
320
|
+
* so there is nothing here to distinguish from the command's own failure.
|
|
321
|
+
* `runnerFailed` is false because the workspace executor is the runner
|
|
322
|
+
* and a failure to run surfaces as a rejected/aborted execution, not a
|
|
323
|
+
* runner that never started.
|
|
324
|
+
*/
|
|
325
|
+
sandboxInfo() {
|
|
326
|
+
const mode = this.sandboxMode;
|
|
327
|
+
if (mode === void 0) return void 0;
|
|
328
|
+
return { mode, denied: false, enforcement: "full", runnerFailed: false };
|
|
329
|
+
}
|
|
330
|
+
resolve(request) {
|
|
331
|
+
const workdir = request.workdir ?? (this.sessionId === void 0 ? this.workdir : "");
|
|
332
|
+
return {
|
|
333
|
+
command: request.command,
|
|
334
|
+
workdir,
|
|
335
|
+
timeoutMs: Math.min(request.timeoutMs ?? this.defaultTimeoutMs, this.maxTimeoutMs),
|
|
336
|
+
stdoutMaxBytes: request.stdoutMaxBytes ?? this.stdoutMaxBytes,
|
|
337
|
+
signal: request.signal,
|
|
338
|
+
stdin: request.stdin,
|
|
339
|
+
env: request.env,
|
|
340
|
+
dshEnv: request.dshEnv,
|
|
341
|
+
sandboxPolicy: request.sandboxPolicy
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
ensureSession() {
|
|
345
|
+
if (this.sessionId === void 0) return Promise.resolve();
|
|
346
|
+
this.sessionReady ??= this.provisionSession(this.sessionId).catch((err) => {
|
|
347
|
+
this.sessionReady = null;
|
|
348
|
+
throw err;
|
|
349
|
+
});
|
|
350
|
+
return this.sessionReady;
|
|
351
|
+
}
|
|
352
|
+
async provisionSession(sessionId) {
|
|
353
|
+
const ws = await this.workspace();
|
|
354
|
+
await ws.ensureSessionsLoaded();
|
|
355
|
+
if (ws.listSessions().some((s) => s.sessionId === sessionId)) return;
|
|
356
|
+
const session = ws.createSession(sessionId);
|
|
357
|
+
session.cwd = this.workdir;
|
|
358
|
+
session.env.PWD = this.workdir;
|
|
359
|
+
}
|
|
360
|
+
async run(spec) {
|
|
361
|
+
const sandbox = this.sandboxInfo();
|
|
362
|
+
if (spec.signal?.aborted === true) {
|
|
363
|
+
return {
|
|
364
|
+
exitCode: null,
|
|
365
|
+
signal: "SIGTERM",
|
|
366
|
+
timedOut: false,
|
|
367
|
+
aborted: true,
|
|
368
|
+
timeoutMs: spec.timeoutMs,
|
|
369
|
+
stdout: { text: "", truncated: false },
|
|
370
|
+
stderr: { text: "", truncated: false },
|
|
371
|
+
...sandbox !== void 0 ? { sandbox } : {}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
let timedOut = false;
|
|
376
|
+
let aborted = false;
|
|
377
|
+
const timer = setTimeout(() => {
|
|
378
|
+
timedOut = true;
|
|
379
|
+
controller.abort();
|
|
380
|
+
}, spec.timeoutMs);
|
|
381
|
+
const onAbort = () => {
|
|
382
|
+
if (!timedOut && !aborted) {
|
|
383
|
+
aborted = true;
|
|
384
|
+
controller.abort();
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
spec.signal?.addEventListener("abort", onAbort, { once: true });
|
|
388
|
+
try {
|
|
389
|
+
await this.ensureSession();
|
|
390
|
+
const ws = await this.workspace();
|
|
391
|
+
const result = await ws.execute(
|
|
392
|
+
spec.command,
|
|
393
|
+
executeOptions(spec, controller.signal, this.sessionId, this.workdir)
|
|
394
|
+
);
|
|
395
|
+
return {
|
|
396
|
+
exitCode: result.exitCode,
|
|
397
|
+
signal: null,
|
|
398
|
+
timedOut: false,
|
|
399
|
+
aborted: false,
|
|
400
|
+
timeoutMs: spec.timeoutMs,
|
|
401
|
+
stdout: collect(result.stdoutText, spec.stdoutMaxBytes),
|
|
402
|
+
stderr: collect(result.stderrText, this.stderrMaxBytes),
|
|
403
|
+
...sandbox !== void 0 ? { sandbox } : {}
|
|
404
|
+
};
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (!controller.signal.aborted) throw err;
|
|
407
|
+
return {
|
|
408
|
+
exitCode: null,
|
|
409
|
+
signal: "SIGTERM",
|
|
410
|
+
timedOut,
|
|
411
|
+
aborted,
|
|
412
|
+
timeoutMs: spec.timeoutMs,
|
|
413
|
+
stdout: { text: "", truncated: false },
|
|
414
|
+
stderr: { text: "", truncated: false },
|
|
415
|
+
...sandbox !== void 0 ? { sandbox } : {}
|
|
416
|
+
};
|
|
417
|
+
} finally {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
spec.signal?.removeEventListener("abort", onAbort);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
start(spec) {
|
|
423
|
+
const sandbox = this.sandboxInfo();
|
|
424
|
+
const controller = new AbortController();
|
|
425
|
+
const console_ = new JobConsole(new RAMConsoleStore(CONSOLE_RETENTION_BYTES));
|
|
426
|
+
const spill = this.newSpill();
|
|
427
|
+
if (spec.signal?.aborted === true) {
|
|
428
|
+
controller.abort();
|
|
429
|
+
return new MirageShellProcess(
|
|
430
|
+
Promise.reject(new Error("command aborted before start")),
|
|
431
|
+
controller,
|
|
432
|
+
console_,
|
|
433
|
+
spec.stdoutMaxBytes,
|
|
434
|
+
spill,
|
|
435
|
+
sandbox
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const onAbort = () => {
|
|
439
|
+
controller.abort();
|
|
440
|
+
};
|
|
441
|
+
spec.signal?.addEventListener("abort", onAbort, { once: true });
|
|
442
|
+
const run = this.ensureSession().then(() => this.workspace()).then(
|
|
443
|
+
(ws) => ws.execute(
|
|
444
|
+
spec.command,
|
|
445
|
+
executeOptions(spec, controller.signal, this.sessionId, this.workdir, console_)
|
|
446
|
+
)
|
|
447
|
+
).finally(() => spec.signal?.removeEventListener("abort", onAbort));
|
|
448
|
+
return new MirageShellProcess(run, controller, console_, spec.stdoutMaxBytes, spill, sandbox);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
export {
|
|
453
|
+
MirageShellExecutor
|
|
454
|
+
};
|
|
455
|
+
//# sourceMappingURL=chunk-ZSZVHBSL.js.map
|
|
@@ -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 { 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 RAMConsoleStore,\n exitOutcome,\n KILLED_OUTCOME,\n} from '@struktoai/mirage-core'\nimport type { ConsoleChunk, ExecuteOptions, ExecuteResult } from '@struktoai/mirage-core'\nimport type { Workspace } from '@struktoai/mirage-node'\nimport { tailCap } from './text.ts'\nimport { SpillSink, ensureDirPath, type SpillTarget } from './spill.ts'\nimport type {} from './service.ts'\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 = '\\n--- stderr ---\\n'\n// What the console may hold that the drain loop has not consumed yet.\n// Capping the delta does not bound this: a reader's cursor advances but\n// frees nothing, so an uncapped store keeps every chunk of a noisy\n// command for the life of the process. Five deltas' worth, because the\n// drain awaits the spill's own writes and has to be free to fall\n// briefly behind, and bounded, so a command that outruns it forever\n// cannot grow the heap.\nconst CONSOLE_RETENTION_BYTES = 5 * DEFAULT_STDOUT_MAX_BYTES\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/** 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. A spec carrying an explicit\n * `workdir` or `env` still runs as a one-call subshell of the bound\n * session, per mirage's `ExecuteOptions` semantics.\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 signal: AbortSignal,\n sessionId: string | undefined,\n fallbackWorkdir: string,\n sink?: JobConsole,\n): ExecuteOptions & { provision?: false } {\n const env = {\n ...(spec.env ?? {}),\n ...((spec.dshEnv as Record<string, string> | undefined) ?? {}),\n }\n // Unbound, `cwd` is always present so every command runs in an ephemeral\n // fork of the default session: isolation must not hinge on a spec\n // happening to carry a workdir. Bound, an absent workdir runs in the\n // session itself, which is what lets its state persist.\n const cwd =\n spec.workdir !== '' ? spec.workdir : sessionId === undefined ? fallbackWorkdir : undefined\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 pending = ''\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.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 const text = new TextDecoder().decode(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 if (chunk.channel === Channel.STDERR) {\n if (!this.inStderr) {\n this.pending += STDERR_MARKER\n this.inStderr = true\n }\n this.pending += text\n } else {\n this.inStderr = false\n this.pending += text\n }\n // Bound the unread backlog: a reader that never drains cannot grow\n // `pending` without limit. The tail is kept (the freshest output),\n // matching what the buffered path did at completion.\n const capped = tailCap(this.pending, this.budget)\n if (capped.truncated) {\n this.pending = capped.text\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\n this.pending = ''\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\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 // 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 return new SpillSink(target, dir, `mirage-shell-${spillCounter.toString()}`)\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 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. `denied` is\n * false because mirage has no out-of-band denial channel: a refused write\n * (a read-only mount) fails in-band as an ordinary command error with a\n * nonzero exit, the way EROFS would, not as a separate sandbox verdict,\n * so there is nothing here to distinguish from the command's own failure.\n * `runnerFailed` is false because the workspace executor is the runner\n * and a failure to run surfaces as a rejected/aborted execution, not a\n * runner that never started.\n */\n private sandboxInfo(): ShellSandboxInfo | undefined {\n const mode = this.sandboxMode\n if (mode === undefined) return undefined\n return { mode, denied: false, enforcement: 'full', runnerFailed: false }\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 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 const session = ws.createSession(sessionId)\n session.cwd = this.workdir\n session.env.PWD = this.workdir\n }\n\n async run(spec: ShellExecSpec): Promise<ShellRunResult> {\n const sandbox = this.sandboxInfo()\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 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 result = await ws.execute(\n spec.command,\n executeOptions(spec, controller.signal, this.sessionId, this.workdir),\n )\n return {\n exitCode: result.exitCode,\n signal: null,\n timedOut: false,\n aborted: false,\n timeoutMs: spec.timeoutMs,\n stdout: collect(result.stdoutText, spec.stdoutMaxBytes),\n stderr: collect(result.stderrText, this.stderrMaxBytes),\n ...(sandbox !== undefined ? { sandbox } : {}),\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 stdout: { text: '', truncated: false },\n stderr: { text: '', truncated: false },\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 const sandbox = this.sandboxInfo()\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(CONSOLE_RETENTION_BYTES))\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(() => this.workspace())\n .then((ws) =>\n ws.execute(\n spec.command,\n executeOptions(spec, controller.signal, this.sessionId, this.workdir, console_),\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'\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 started = false\n private failed = false\n private stdoutParts: Uint8Array[] = []\n private stderrParts: Uint8Array[] = []\n\n constructor(target: SpillTarget, dir: string, base: string) {\n this.target = target\n this.dir = dir\n this.base = base\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 {\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 {\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"]}
|