@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.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +28 -29
  3. package/cordis.patch.yml +38 -6
  4. package/dist/{chunk-M4JOF35U.js → chunk-FRF6WPLN.js} +133 -44
  5. package/dist/chunk-FRF6WPLN.js.map +1 -0
  6. package/dist/chunk-G2GHA2F6.js +211 -0
  7. package/dist/chunk-G2GHA2F6.js.map +1 -0
  8. package/dist/{chunk-QTBMLMKT.js → chunk-KUIV2Z3M.js} +243 -326
  9. package/dist/chunk-KUIV2Z3M.js.map +1 -0
  10. package/dist/{chunk-4CZV24HW.js → chunk-OHZACFWO.js} +36 -14
  11. package/dist/chunk-OHZACFWO.js.map +1 -0
  12. package/dist/{chunk-2F2PJ7D7.js → chunk-OZ2F4R6T.js} +74 -17
  13. package/dist/chunk-OZ2F4R6T.js.map +1 -0
  14. package/dist/chunk-QH5H3VND.js +110 -0
  15. package/dist/chunk-QH5H3VND.js.map +1 -0
  16. package/dist/chunk-T4BSJAGN.js +64 -0
  17. package/dist/chunk-T4BSJAGN.js.map +1 -0
  18. package/dist/{fs-B3Zi9oo6.d.ts → fs-BNOIFi9o.d.ts} +48 -4
  19. package/dist/index.d.ts +8 -3
  20. package/dist/index.js +15 -6
  21. package/dist/plugin/fs.d.ts +1 -1
  22. package/dist/plugin/fs.js +2 -2
  23. package/dist/plugin/service.d.ts +2 -1
  24. package/dist/plugin/service.js +1 -1
  25. package/dist/plugin/shell.d.ts +1 -1
  26. package/dist/plugin/shell.js +3 -2
  27. package/dist/plugin/spill-store.d.ts +3 -0
  28. package/dist/plugin/spill-store.js +8 -0
  29. package/dist/plugin/spill-store.js.map +1 -0
  30. package/dist/plugin/subprocess.d.ts +8 -0
  31. package/dist/plugin/subprocess.js +9 -0
  32. package/dist/plugin/subprocess.js.map +1 -0
  33. package/dist/{service-BK6NR_RX.d.ts → service-vCjLPYMI.d.ts} +29 -17
  34. package/dist/{shell-BTf2wsDy.d.ts → shell-BAyBocjm.d.ts} +25 -35
  35. package/dist/spill-store-BfljgRi0.d.ts +38 -0
  36. package/dist/subprocess-CInesMpR.d.ts +23 -0
  37. package/package.json +24 -12
  38. package/dist/chunk-2F2PJ7D7.js.map +0 -1
  39. package/dist/chunk-4CZV24HW.js.map +0 -1
  40. package/dist/chunk-M4JOF35U.js.map +0 -1
  41. package/dist/chunk-QTBMLMKT.js.map +0 -1
@@ -1,147 +1,32 @@
1
1
  import {
2
- TailBuffer,
3
- tailCap
4
- } from "./chunk-2F2PJ7D7.js";
2
+ SpillSink,
3
+ ensureDirPath
4
+ } from "./chunk-QH5H3VND.js";
5
+ import {
6
+ StreamTail,
7
+ TailBuffer
8
+ } from "./chunk-OZ2F4R6T.js";
5
9
 
6
10
  // src/shell.ts
7
11
  import { DSH_ENV_PREFIX, ShellExecutor } from "@deepseek-ai/dsh-shell";
8
12
  import {
9
- Channel as Channel2,
13
+ Channel,
10
14
  JobConsole,
11
15
  KILLED_OUTCOME,
12
- RAMConsoleStore,
13
16
  exitOutcome
14
17
  } from "@struktoai/mirage-core/shell/console/index";
15
18
  import { setCwd } from "@struktoai/mirage-core/workspace/session/shell_dirs";
16
19
  import { sessionView } from "@struktoai/mirage-core/workspace/session/state";
17
-
18
- // src/spill.ts
19
- import { Channel } from "@struktoai/mirage-core/shell/console/index";
20
- async function ensureDirPath(dirs, dir) {
21
- let path = "";
22
- for (const part of dir.split("/").filter((p) => p !== "")) {
23
- path += `/${part}`;
24
- if (await dirs.exists(path)) continue;
25
- try {
26
- await dirs.mkdir(path);
27
- } catch (err) {
28
- if (!await dirs.exists(path)) throw err;
29
- }
30
- }
31
- }
32
- function totalLength(parts) {
33
- return parts.reduce((sum, p) => sum + p.byteLength, 0);
34
- }
35
- function concat(parts) {
36
- const out = new Uint8Array(totalLength(parts));
37
- let at = 0;
38
- for (const p of parts) {
39
- out.set(p, at);
40
- at += p.byteLength;
41
- }
42
- return out;
43
- }
44
- var SpillSink = class {
45
- stdoutPath;
46
- stderrPath;
47
- target;
48
- dir;
49
- base;
50
- onFailure;
51
- started = false;
52
- failed = false;
53
- stdoutParts = [];
54
- stderrParts = [];
55
- constructor(target, dir, base, onFailure = () => void 0) {
56
- this.target = target;
57
- this.dir = dir;
58
- this.base = base;
59
- this.onFailure = onFailure;
60
- }
61
- /** Buffer a chunk before spill starts, or append it to the file after. */
62
- async ingest(channel, data) {
63
- if (this.failed) return;
64
- if (!this.started) {
65
- if (channel === Channel.STDERR) this.stderrParts.push(data);
66
- else this.stdoutParts.push(data);
67
- return;
68
- }
69
- await this.appendFile(channel, data);
70
- }
71
- /** Flush the buffered streams to files; called once, on the first overrun. */
72
- async begin() {
73
- if (this.started || this.failed) return;
74
- try {
75
- await this.target.ensureDir(this.dir);
76
- if (totalLength(this.stdoutParts) > 0) {
77
- this.stdoutPath = `${this.dir}/${this.base}.stdout`;
78
- await this.target.write(this.stdoutPath, concat(this.stdoutParts));
79
- }
80
- if (totalLength(this.stderrParts) > 0) {
81
- this.stderrPath = `${this.dir}/${this.base}.stderr`;
82
- await this.target.write(this.stderrPath, concat(this.stderrParts));
83
- }
84
- this.started = true;
85
- this.stdoutParts = [];
86
- this.stderrParts = [];
87
- } catch (err) {
88
- this.onFailure(err);
89
- this.disable();
90
- }
91
- }
92
- async appendFile(channel, data) {
93
- try {
94
- if (channel === Channel.STDERR) {
95
- if (this.stderrPath === void 0) {
96
- this.stderrPath = `${this.dir}/${this.base}.stderr`;
97
- await this.target.write(this.stderrPath, data);
98
- } else {
99
- await this.target.append(this.stderrPath, data);
100
- }
101
- } else {
102
- if (this.stdoutPath === void 0) {
103
- this.stdoutPath = `${this.dir}/${this.base}.stdout`;
104
- await this.target.write(this.stdoutPath, data);
105
- } else {
106
- await this.target.append(this.stdoutPath, data);
107
- }
108
- }
109
- } catch (err) {
110
- this.onFailure(err);
111
- this.disable();
112
- }
113
- }
114
- /**
115
- * Give up on spilling and report no path.
116
- *
117
- * Called on a write failure, and by a reader that discovers it lost
118
- * bytes before they ever reached here: a file missing the middle of
119
- * the stream is worse than no file, because the path presents it as
120
- * the whole one.
121
- */
122
- disable() {
123
- this.failed = true;
124
- this.stdoutPath = void 0;
125
- this.stderrPath = void 0;
126
- this.stdoutParts = [];
127
- this.stderrParts = [];
128
- }
129
- };
130
-
131
- // src/shell.ts
20
+ import { refusalLine } from "@struktoai/mirage-core/workspace/tools/io_text";
21
+ import { rstripSlash } from "@struktoai/mirage-core/utils/slash";
132
22
  var DEFAULT_TIMEOUT_MS = 12e4;
133
23
  var MAX_TIMEOUT_MS = 6e5;
134
24
  var DEFAULT_STDOUT_MAX_BYTES = 2e5;
135
25
  var DEFAULT_STDERR_MAX_BYTES = 64e3;
136
26
  var STDERR_MARKER = new TextEncoder().encode("\n--- stderr ---\n");
137
- var CONSOLE_RETENTION_DELTAS = 5;
138
27
  var spillCounter = 0;
139
28
  var SINK_PREFIX = "/dev";
140
29
  var DENIAL_SIGNATURES = ["read-only mount at ", ": Permission denied", ": Read-only file system"];
141
- function collect(text, maxBytes) {
142
- const capped = tailCap(text, maxBytes);
143
- return { text: capped.text, truncated: capped.truncated };
144
- }
145
30
  function executeOptions(spec, workdir, signal, sessionId, bound, fallbackWorkdir, sink) {
146
31
  const managed = spec.dshEnv ?? {};
147
32
  const env = bound ? { ...spec.env ?? {} } : { ...spec.env ?? {}, ...managed };
@@ -155,51 +40,102 @@ function executeOptions(spec, workdir, signal, sessionId, bound, fallbackWorkdir
155
40
  ...sink !== void 0 ? { sink } : {}
156
41
  };
157
42
  }
158
- var MirageShellProcess = class {
43
+ function untilAborted(work, signal) {
44
+ return new Promise((resolve, reject) => {
45
+ const abort = () => {
46
+ reject(signal.reason);
47
+ };
48
+ signal.addEventListener("abort", abort, { once: true });
49
+ work.then(resolve, reject).finally(() => {
50
+ signal.removeEventListener("abort", abort);
51
+ });
52
+ });
53
+ }
54
+ var ExecutionStore = class {
55
+ constructor(deliver) {
56
+ this.deliver = deliver;
57
+ }
58
+ deliver;
59
+ nextSeq = 0;
60
+ isClosed = false;
61
+ waiters = [];
62
+ get closed() {
63
+ return this.isClosed;
64
+ }
65
+ async append(channel, data) {
66
+ const chunk = { seq: this.nextSeq, ts: Date.now() / 1e3, channel, data };
67
+ this.nextSeq += 1;
68
+ await this.deliver(chunk);
69
+ return chunk;
70
+ }
71
+ readFrom() {
72
+ return Promise.resolve([[], this.nextSeq, false]);
73
+ }
74
+ wait() {
75
+ if (this.isClosed) return Promise.resolve();
76
+ return new Promise((resolve) => {
77
+ this.waiters.push(resolve);
78
+ });
79
+ }
80
+ close() {
81
+ this.isClosed = true;
82
+ for (const resolve of this.waiters.splice(0)) resolve();
83
+ return Promise.resolve();
84
+ }
85
+ };
86
+ var MirageShellExecution = class {
159
87
  status = "running";
160
88
  exitCode = null;
161
89
  signal = null;
162
90
  sandbox;
163
91
  done;
164
- controller;
92
+ observed;
93
+ parts;
165
94
  console;
166
- budget;
167
- spill;
168
- sandboxInfo;
169
- consumed;
170
95
  pending;
96
+ stdoutTail;
97
+ stderrTail;
98
+ failure = null;
99
+ settledResult = null;
171
100
  lossy = false;
172
101
  inStderr = false;
173
102
  settled = false;
174
- expectSeq = 0;
175
- constructor(run, controller, console_, budget, spill, sandboxInfo) {
176
- this.controller = controller;
177
- this.console = console_;
178
- this.budget = budget;
179
- this.pending = new TailBuffer(budget);
180
- this.spill = spill;
181
- this.sandboxInfo = sandboxInfo;
182
- this.consumed = this.consume();
183
- this.done = run.then(
103
+ /**
104
+ * @param launch starts the command streaming into the console it is
105
+ * handed; null when the deadline expired while it was being prepared.
106
+ * @param parts what the execution needs from its executor.
107
+ */
108
+ constructor(launch, parts) {
109
+ this.parts = parts;
110
+ this.console = new JobConsole(
111
+ new ExecutionStore(
112
+ (chunk) => chunk.channel === Channel.CONTROL ? Promise.resolve() : this.appendChunk(chunk)
113
+ )
114
+ );
115
+ this.pending = new TailBuffer(parts.budget);
116
+ this.stdoutTail = new StreamTail(parts.stdoutMaxBytes);
117
+ this.stderrTail = new StreamTail(parts.stderrMaxBytes);
118
+ this.observed = {
119
+ stdout: { readFrom: (from) => this.readStream(this.stdoutTail, from, "stdout") },
120
+ stderr: { readFrom: (from) => this.readStream(this.stderrTail, from, "stderr") }
121
+ };
122
+ this.done = launch === null ? this.settleExpired() : launch(this.console).then(
184
123
  (result) => this.settle(result, null),
185
124
  (err) => this.settle(null, err)
186
125
  );
187
126
  }
188
- async consume() {
189
- for await (const chunk of this.console.follow(0)) {
190
- if (chunk.channel === Channel2.CONTROL) return;
191
- if (chunk.seq !== this.expectSeq) {
192
- this.lossy = true;
193
- this.spill?.disable();
194
- }
195
- this.expectSeq = chunk.seq + 1;
196
- await this.appendChunk(chunk);
197
- }
127
+ readStream(tail, fromByte, channel) {
128
+ const read = tail.readFrom(fromByte);
129
+ const spillPath = channel === "stdout" ? this.parts.spill?.stdoutPath : this.parts.spill?.stderrPath;
130
+ return { ...read, ...spillPath !== void 0 ? { spillPath } : {} };
198
131
  }
199
132
  async appendChunk(chunk) {
200
- if (this.spill !== null) await this.spill.ingest(chunk.channel, chunk.data);
133
+ const spill = this.parts.spill;
134
+ if (spill !== null) await spill.ingest(chunk.channel, chunk.data);
135
+ const tail = chunk.channel === Channel.STDERR ? this.stderrTail : this.stdoutTail;
136
+ tail.append(chunk.data);
201
137
  let dropped = false;
202
- if (chunk.channel === Channel2.STDERR) {
138
+ if (chunk.channel === Channel.STDERR) {
203
139
  if (!this.inStderr) {
204
140
  dropped = this.pending.append(STDERR_MARKER);
205
141
  this.inStderr = true;
@@ -208,14 +144,11 @@ var MirageShellProcess = class {
208
144
  this.inStderr = false;
209
145
  }
210
146
  dropped = this.pending.append(chunk.data) || dropped;
211
- if (dropped) {
212
- this.lossy = true;
213
- if (this.spill !== null) await this.spill.begin();
214
- }
147
+ if (dropped) this.lossy = true;
148
+ if ((dropped || tail.truncated) && spill !== null) await spill.begin();
215
149
  }
216
150
  async settle(result, err) {
217
151
  this.settled = true;
218
- if (this.sandboxInfo !== void 0) this.sandbox = this.sandboxInfo;
219
152
  let outcome;
220
153
  if (result !== null) {
221
154
  this.status = "completed";
@@ -224,29 +157,77 @@ var MirageShellProcess = class {
224
157
  } else {
225
158
  this.status = "killed";
226
159
  this.signal = "SIGTERM";
227
- const message = err instanceof Error ? err.message : String(err);
228
- await this.console.emit(Channel2.STDERR, new TextEncoder().encode(message));
229
160
  outcome = KILLED_OUTCOME;
161
+ if (!this.parts.controller.signal.aborted) {
162
+ this.failure = { error: err };
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ await this.console.emit(Channel.STDERR, new TextEncoder().encode(message));
165
+ }
166
+ }
167
+ if (result !== null) {
168
+ const before = this.stderrTail.readFrom(0).text;
169
+ const line = refusalLine(result.refusal);
170
+ if (line !== "") {
171
+ const lead = before === "" || before.endsWith("\n") ? "" : "\n";
172
+ await this.console.emit(Channel.STDERR, new TextEncoder().encode(lead + line));
173
+ }
230
174
  }
231
175
  await this.console.finish(outcome);
232
- await this.consumed;
176
+ this.stdoutTail.end();
177
+ this.stderrTail.end();
178
+ const stderr = this.stderrTail.readFrom(0).text;
179
+ const sandbox = this.parts.verdict(result, stderr);
180
+ if (sandbox !== void 0) this.sandbox = sandbox;
181
+ this.parts.disarm();
182
+ }
183
+ async settleExpired() {
184
+ this.settled = true;
185
+ this.status = "killed";
186
+ await this.console.finish(KILLED_OUTCOME);
187
+ const sandbox = this.parts.verdict(null, "");
188
+ if (sandbox !== void 0) this.sandbox = sandbox;
189
+ this.parts.disarm();
233
190
  }
234
191
  readOutput() {
235
192
  const delta = this.pending.take();
236
193
  const lossy = this.lossy;
237
194
  this.lossy = false;
195
+ const spill = this.parts.spill;
238
196
  return {
239
197
  delta,
240
198
  lossy,
241
- ...this.spill?.stdoutPath !== void 0 ? { stdoutSpillPath: this.spill.stdoutPath } : {},
242
- ...this.spill?.stderrPath !== void 0 ? { stderrSpillPath: this.spill.stderrPath } : {}
199
+ ...spill?.stdoutPath !== void 0 ? { stdoutSpillPath: spill.stdoutPath } : {},
200
+ ...spill?.stderrPath !== void 0 ? { stderrSpillPath: spill.stderrPath } : {}
243
201
  };
244
202
  }
245
203
  kill() {
246
204
  if (this.settled) return false;
247
- this.controller.abort();
205
+ this.parts.controller.abort();
248
206
  return true;
249
207
  }
208
+ result() {
209
+ this.settledResult ??= this.done.then(() => {
210
+ if (this.failure !== null) throw this.failure.error;
211
+ return {
212
+ exitCode: this.exitCode,
213
+ signal: this.signal,
214
+ ...this.parts.classify(),
215
+ timeoutMs: this.parts.timeoutMs,
216
+ stdout: this.collected(this.stdoutTail, "stdout"),
217
+ stderr: this.collected(this.stderrTail, "stderr"),
218
+ ...this.sandbox !== void 0 ? { sandbox: this.sandbox } : {}
219
+ };
220
+ });
221
+ return this.settledResult;
222
+ }
223
+ collected(tail, channel) {
224
+ const read = this.readStream(tail, 0, channel);
225
+ return {
226
+ text: read.text,
227
+ truncated: read.lossy,
228
+ ...read.spillPath !== void 0 ? { spillPath: read.spillPath } : {}
229
+ };
230
+ }
250
231
  };
251
232
  var MirageShellExecutor = class extends ShellExecutor {
252
233
  static inject = ["mirage"];
@@ -259,6 +240,9 @@ var MirageShellExecutor = class extends ShellExecutor {
259
240
  spillDir;
260
241
  sessionReady = null;
261
242
  readOnlyReady = null;
243
+ seeding = /* @__PURE__ */ new Map();
244
+ issued = 0;
245
+ seeded = /* @__PURE__ */ new Map();
262
246
  constructor(ctx, config = {}) {
263
247
  super(ctx);
264
248
  this.workdir = config.workdir ?? "/";
@@ -292,7 +276,7 @@ var MirageShellExecutor = class extends ShellExecutor {
292
276
  async worldWorkdir(spec) {
293
277
  if (spec.workdir === "") return "";
294
278
  const ws = await this.workspace();
295
- if (await ws.fs.isDir(spec.workdir)) return spec.workdir;
279
+ if (await ws.vfs.isDir(spec.workdir)) return spec.workdir;
296
280
  return this.sessionId === void 0 ? this.workdir : "";
297
281
  }
298
282
  // A spill sink for one background command, or null when no spill
@@ -304,15 +288,15 @@ var MirageShellExecutor = class extends ShellExecutor {
304
288
  const target = {
305
289
  ensureDir: async (d) => {
306
290
  const ws = await this.workspace();
307
- await ensureDirPath({ exists: (p) => ws.fs.exists(p), mkdir: (p) => ws.fs.mkdir(p) }, d);
291
+ await ensureDirPath({ exists: (p) => ws.vfs.exists(p), mkdir: (p) => ws.vfs.mkdir(p) }, d);
308
292
  },
309
293
  write: async (p, bytes) => {
310
294
  const ws = await this.workspace();
311
- await ws.fs.writeFile(p, bytes);
295
+ await ws.vfs.write(p, bytes);
312
296
  },
313
297
  append: async (p, bytes) => {
314
298
  const ws = await this.workspace();
315
- await ws.fs.append(p, bytes);
299
+ await ws.vfs.append(p, bytes);
316
300
  }
317
301
  };
318
302
  spillCounter += 1;
@@ -328,7 +312,7 @@ var MirageShellExecutor = class extends ShellExecutor {
328
312
  * workspace-write sandbox: reads and writes land only where mounts
329
313
  * (and their modes) allow. Declaring it lets sandbox-aware plugins
330
314
  * (dsh's permission presets) compose over this executor. A world
331
- * holding a runtime with doors around the gate (the host `local`
315
+ * holding a runtime with entry points around the gate (the host `local`
332
316
  * python, a remote sandbox) voids that claim, so this answers
333
317
  * undefined then (the base contract's "does not sandbox") and those
334
318
  * plugins refuse to compose instead of trusting a lie.
@@ -385,60 +369,6 @@ var MirageShellExecutor = class extends ShellExecutor {
385
369
  if (mode === void 0) return void 0;
386
370
  return { mode, denied, enforcement: "full", runnerFailed: false };
387
371
  }
388
- /**
389
- * The retention budget of a background command's console.
390
- *
391
- * Only the background path caps retention: there a follow loop drains
392
- * the console while the command still runs, so the budget bounds what
393
- * the loop has not reached yet, and a chunk lost to it is reported as
394
- * a gap in the sequence. A foreground console is read once, after the
395
- * fact, with nothing to bound.
396
- *
397
- * @param spec the resolved spec carrying the stdout budget.
398
- * @returns the retention budget in bytes.
399
- */
400
- retentionFor(spec) {
401
- return Math.max(spec.stdoutMaxBytes, this.stderrMaxBytes) * CONSOLE_RETENTION_DELTAS;
402
- }
403
- /**
404
- * Take a finished run's two streams off its console, capped, and spill
405
- * whichever one lost bytes.
406
- *
407
- * A foreground run streams into a console rather than returning its
408
- * output whole, because mirage throws on abort: bytes a killed command
409
- * had already printed are recoverable from a sink and nowhere else.
410
- * Only a truncated stream spills, since an untruncated one is already
411
- * whole in `text` and writing a file for it would put a copy of every
412
- * command's output on a mount. The console this reads is untrimmed, so
413
- * a spill is the whole stream rather than the tail of one.
414
- *
415
- * @param console_ the console this run streamed into.
416
- * @param spec the resolved spec carrying the stdout budget.
417
- * @returns the capped streams, each with a spill path when it lost bytes.
418
- */
419
- async collectFrom(console_, spec) {
420
- const decoder = new TextDecoder();
421
- const outBytes = await console_.snapshot(Channel2.STDOUT);
422
- const errBytes = await console_.snapshot(Channel2.STDERR);
423
- const stdout = collect(decoder.decode(outBytes), spec.stdoutMaxBytes);
424
- const stderr = collect(decoder.decode(errBytes), this.stderrMaxBytes);
425
- if (!stdout.truncated && !stderr.truncated) return { stdout, stderr };
426
- const spill = this.newSpill();
427
- if (spill === null) return { stdout, stderr };
428
- if (stdout.truncated) await spill.ingest(Channel2.STDOUT, outBytes);
429
- if (stderr.truncated) await spill.ingest(Channel2.STDERR, errBytes);
430
- await spill.begin();
431
- return {
432
- stdout: {
433
- ...stdout,
434
- ...spill.stdoutPath !== void 0 ? { spillPath: spill.stdoutPath } : {}
435
- },
436
- stderr: {
437
- ...stderr,
438
- ...spill.stderrPath !== void 0 ? { spillPath: spill.stderrPath } : {}
439
- }
440
- };
441
- }
442
372
  /**
443
373
  * Whether this run was refused, by the session's permission document
444
374
  * or by the read-only narrowing.
@@ -471,6 +401,7 @@ var MirageShellExecutor = class extends ShellExecutor {
471
401
  command: request.command,
472
402
  workdir,
473
403
  timeoutMs: Math.min(request.timeoutMs ?? this.defaultTimeoutMs, this.maxTimeoutMs),
404
+ onExpiry: request.onExpiry ?? "kill",
474
405
  stdoutMaxBytes: request.stdoutMaxBytes ?? this.stdoutMaxBytes,
475
406
  signal: request.signal,
476
407
  stdin: request.stdin,
@@ -502,11 +433,9 @@ var MirageShellExecutor = class extends ShellExecutor {
502
433
  *
503
434
  * @param ws the live workspace holding the session.
504
435
  * @param sessionId the session this call runs in.
505
- * @param spec the resolved spec carrying the snapshot.
436
+ * @param managed the call's managed snapshot.
506
437
  */
507
- async applyManagedEnv(ws, sessionId, spec) {
508
- const managed = spec.dshEnv;
509
- if (managed === void 0) return;
438
+ async applyManagedEnv(ws, sessionId, managed) {
510
439
  const session = ws.getSession(sessionId);
511
440
  const view = sessionView(session);
512
441
  for (const key of Object.keys(session.env)) {
@@ -560,15 +489,20 @@ var MirageShellExecutor = class extends ShellExecutor {
560
489
  const source = ws.getSession(this.sessionId ?? ws.defaultSessionId);
561
490
  const grants = {};
562
491
  for (const entry of ws.mounts()) {
563
- grants[entry.prefix] = entry.prefix.replace(/\/+$/, "") === SINK_PREFIX ? "exec" : "read";
492
+ grants[entry.prefix] = rstripSlash(entry.prefix) === SINK_PREFIX ? "exec" : "read";
564
493
  }
565
- const hide = [...source.hiddenPaths?.paths ?? [], ...source.hiddenPaths?.patterns ?? []];
494
+ const hidden = source.visibility.paths;
495
+ const hide = [...hidden?.paths ?? [], ...hidden?.patterns ?? []];
566
496
  const twin = ws.createSession(sessionId, {
567
497
  mounts: grants,
568
498
  ...hide.length > 0 ? { permissions: { paths: { hide } } } : {}
569
499
  });
570
500
  twin.commands = source.commands;
571
- twin.hiddenVars = source.hiddenVars;
501
+ twin.visibility = {
502
+ ...twin.visibility,
503
+ vars: source.visibility.vars,
504
+ commands: source.visibility.commands
505
+ };
572
506
  setCwd(twin, this.workdir);
573
507
  return sessionId;
574
508
  }
@@ -578,121 +512,104 @@ var MirageShellExecutor = class extends ShellExecutor {
578
512
  if (ws.listSessions().some((s) => s.sessionId === sessionId)) return;
579
513
  setCwd(ws.createSession(sessionId), this.workdir);
580
514
  }
581
- async run(spec) {
582
- const sandbox = this.sandboxInfo(spec);
583
- if (spec.signal?.aborted === true) {
584
- return {
585
- exitCode: null,
586
- signal: "SIGTERM",
587
- timedOut: false,
588
- aborted: true,
589
- timeoutMs: spec.timeoutMs,
590
- stdout: { text: "", truncated: false },
591
- stderr: { text: "", truncated: false },
592
- ...sandbox !== void 0 ? { sandbox } : {}
593
- };
515
+ /**
516
+ * Where and as whom one command runs, settled before it starts: the
517
+ * session binding, the workdir in this world, and its managed env.
518
+ *
519
+ * Seeding the env is the one step that writes, so it comes last and
520
+ * waits for any seed already running on the same session (the bound
521
+ * one and its read-only twin queue apart). Calls are numbered as they
522
+ * arrive, and a seed is skipped once a later call has seeded the same
523
+ * session, so the newest snapshot wins: a slow or abandoned preparation
524
+ * never lands an old one over it, and a stall before the seed holds up
525
+ * no other call. A call carrying no snapshot seeds nothing, so it
526
+ * never counts as the newest, but it still waits for the seeds already
527
+ * running, so its command never sees one half applied.
528
+ *
529
+ * @param spec the resolved spec being prepared.
530
+ * @returns the workspace, session and workdir the command runs under.
531
+ */
532
+ async prepare(spec) {
533
+ const ticket = ++this.issued;
534
+ await this.ensureSession();
535
+ const ws = await this.workspace();
536
+ const sessionId = await this.sessionFor(spec);
537
+ const bound = this.sessionId !== void 0;
538
+ const workdir = await this.worldWorkdir(spec);
539
+ const managed = spec.dshEnv;
540
+ if (bound && sessionId !== void 0) {
541
+ const running = this.seeding.get(sessionId) ?? Promise.resolve();
542
+ if (managed === void 0) {
543
+ await running;
544
+ } else {
545
+ const seed = running.then(async () => {
546
+ if (ticket < (this.seeded.get(sessionId) ?? 0)) return;
547
+ await this.applyManagedEnv(ws, sessionId, managed);
548
+ this.seeded.set(sessionId, ticket);
549
+ });
550
+ this.seeding.set(
551
+ sessionId,
552
+ seed.catch(() => void 0)
553
+ );
554
+ await seed;
555
+ }
594
556
  }
557
+ return { ws, sessionId, bound, workdir };
558
+ }
559
+ async execute(spec) {
595
560
  const controller = new AbortController();
596
- const console_ = new JobConsole(new RAMConsoleStore(null));
597
561
  let timedOut = false;
598
562
  let aborted = false;
599
- const timer = setTimeout(() => {
563
+ const timer = spec.onExpiry === "kill" ? setTimeout(() => {
600
564
  timedOut = true;
601
565
  controller.abort();
602
- }, spec.timeoutMs);
566
+ }, spec.timeoutMs) : void 0;
603
567
  const onAbort = () => {
604
568
  if (!timedOut && !aborted) {
605
569
  aborted = true;
606
570
  controller.abort();
607
571
  }
608
572
  };
609
- spec.signal?.addEventListener("abort", onAbort, { once: true });
610
- try {
611
- await this.ensureSession();
612
- const ws = await this.workspace();
613
- const sessionId = await this.sessionFor(spec);
614
- const bound = this.sessionId !== void 0;
615
- if (bound && sessionId !== void 0) await this.applyManagedEnv(ws, sessionId, spec);
616
- const workdir = await this.worldWorkdir(spec);
617
- const result = await ws.execute(
618
- spec.command,
619
- executeOptions(spec, workdir, controller.signal, sessionId, bound, this.workdir, console_)
620
- );
621
- const captured = await this.collectFrom(console_, spec);
622
- const settled = this.sandboxInfo(spec, this.wasDenied(spec, result, captured.stderr.text));
623
- return {
624
- exitCode: result.exitCode,
625
- signal: null,
626
- timedOut: false,
627
- aborted: false,
628
- timeoutMs: spec.timeoutMs,
629
- ...captured,
630
- ...settled !== void 0 ? { sandbox: settled } : {}
631
- };
632
- } catch (err) {
633
- if (!controller.signal.aborted) throw err;
634
- return {
635
- exitCode: null,
636
- signal: "SIGTERM",
637
- timedOut,
638
- aborted,
639
- timeoutMs: spec.timeoutMs,
640
- // Whatever the command printed before the kill landed. GNU's own
641
- // timeout keeps it, and a model that watched a build run for two
642
- // minutes is owed the log rather than an empty string.
643
- ...await this.collectFrom(console_, spec),
644
- ...sandbox !== void 0 ? { sandbox } : {}
645
- };
646
- } finally {
573
+ if (spec.signal?.aborted === true) onAbort();
574
+ else spec.signal?.addEventListener("abort", onAbort, { once: true });
575
+ const disarm = () => {
647
576
  clearTimeout(timer);
648
577
  spec.signal?.removeEventListener("abort", onAbort);
649
- }
650
- }
651
- start(spec) {
652
- const sandbox = this.sandboxInfo(spec);
653
- const controller = new AbortController();
654
- const console_ = new JobConsole(new RAMConsoleStore(this.retentionFor(spec)));
655
- const spill = this.newSpill();
656
- if (spec.signal?.aborted === true) {
657
- controller.abort();
658
- return new MirageShellProcess(
659
- Promise.reject(new Error("command aborted before start")),
660
- controller,
661
- console_,
662
- spec.stdoutMaxBytes,
663
- spill,
664
- sandbox
665
- );
666
- }
667
- const onAbort = () => {
668
- controller.abort();
669
578
  };
670
- spec.signal?.addEventListener("abort", onAbort, { once: true });
671
- const run = this.ensureSession().then(async () => {
672
- const sessionId = await this.sessionFor(spec);
673
- const ws = await this.workspace();
674
- const bound = this.sessionId !== void 0;
675
- if (bound && sessionId !== void 0) await this.applyManagedEnv(ws, sessionId, spec);
676
- return { ws, sessionId, bound, workdir: await this.worldWorkdir(spec) };
677
- }).then(
678
- ({ ws, sessionId, bound, workdir }) => ws.execute(
579
+ const parts = {
580
+ controller,
581
+ budget: spec.stdoutMaxBytes,
582
+ stdoutMaxBytes: spec.stdoutMaxBytes,
583
+ stderrMaxBytes: this.stderrMaxBytes,
584
+ timeoutMs: spec.timeoutMs,
585
+ spill: this.newSpill(),
586
+ classify: () => ({ timedOut, aborted }),
587
+ verdict: (result, stderr) => this.sandboxInfo(spec, result !== null && this.wasDenied(spec, result, stderr)),
588
+ disarm
589
+ };
590
+ let prepared;
591
+ try {
592
+ controller.signal.throwIfAborted();
593
+ prepared = await untilAborted(this.prepare(spec), controller.signal);
594
+ } catch (err) {
595
+ if (!parts.classify().timedOut) {
596
+ disarm();
597
+ throw err;
598
+ }
599
+ return new MirageShellExecution(null, parts);
600
+ }
601
+ const { ws, sessionId, bound, workdir } = prepared;
602
+ return new MirageShellExecution(
603
+ (sink) => ws.shell(
679
604
  spec.command,
680
- executeOptions(
681
- spec,
682
- workdir,
683
- controller.signal,
684
- sessionId,
685
- bound,
686
- this.workdir,
687
- console_
688
- )
689
- )
690
- ).finally(() => spec.signal?.removeEventListener("abort", onAbort));
691
- return new MirageShellProcess(run, controller, console_, spec.stdoutMaxBytes, spill, sandbox);
605
+ executeOptions(spec, workdir, controller.signal, sessionId, bound, this.workdir, sink)
606
+ ),
607
+ parts
608
+ );
692
609
  }
693
610
  };
694
611
 
695
612
  export {
696
613
  MirageShellExecutor
697
614
  };
698
- //# sourceMappingURL=chunk-QTBMLMKT.js.map
615
+ //# sourceMappingURL=chunk-KUIV2Z3M.js.map