@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,147 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
242
|
-
...
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
436
|
+
* @param managed the call's managed snapshot.
|
|
506
437
|
*/
|
|
507
|
-
async applyManagedEnv(ws, sessionId,
|
|
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
|
|
492
|
+
grants[entry.prefix] = rstripSlash(entry.prefix) === SINK_PREFIX ? "exec" : "read";
|
|
564
493
|
}
|
|
565
|
-
const
|
|
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.
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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?.
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
(
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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-
|
|
615
|
+
//# sourceMappingURL=chunk-KUIV2Z3M.js.map
|