@agimon-ai/doompi-sandbox 0.0.1-alpha.101

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -0
  3. package/dist/apiContracts.cjs +17 -0
  4. package/dist/apiContracts.cjs.map +1 -0
  5. package/dist/apiContracts.d.mts +53 -0
  6. package/dist/apiContracts.d.mts.map +1 -0
  7. package/dist/apiContracts.mjs +13 -0
  8. package/dist/apiContracts.mjs.map +1 -0
  9. package/dist/brokerHost-BeGtmnK8.mjs +302 -0
  10. package/dist/brokerHost-BeGtmnK8.mjs.map +1 -0
  11. package/dist/brokerHost-BzffXeb2.cjs +340 -0
  12. package/dist/brokerHost-BzffXeb2.cjs.map +1 -0
  13. package/dist/cockpitHarness.cjs +298 -0
  14. package/dist/cockpitHarness.cjs.map +1 -0
  15. package/dist/cockpitHarness.d.mts +26 -0
  16. package/dist/cockpitHarness.d.mts.map +1 -0
  17. package/dist/cockpitHarness.mjs +295 -0
  18. package/dist/cockpitHarness.mjs.map +1 -0
  19. package/dist/extensionService-BnIZjdJN.cjs +25 -0
  20. package/dist/extensionService-BnIZjdJN.cjs.map +1 -0
  21. package/dist/extensionService-DiVpVLn9.mjs +20 -0
  22. package/dist/extensionService-DiVpVLn9.mjs.map +1 -0
  23. package/dist/extensions/mcp.cjs +26 -0
  24. package/dist/extensions/mcp.cjs.map +1 -0
  25. package/dist/extensions/mcp.d.mts +5 -0
  26. package/dist/extensions/mcp.d.mts.map +1 -0
  27. package/dist/extensions/mcp.mjs +21 -0
  28. package/dist/extensions/mcp.mjs.map +1 -0
  29. package/dist/extensions/pi.cjs +85 -0
  30. package/dist/extensions/pi.cjs.map +1 -0
  31. package/dist/extensions/pi.d.mts +6 -0
  32. package/dist/extensions/pi.d.mts.map +1 -0
  33. package/dist/extensions/pi.mjs +80 -0
  34. package/dist/extensions/pi.mjs.map +1 -0
  35. package/dist/extensions/server.cjs +92 -0
  36. package/dist/extensions/server.cjs.map +1 -0
  37. package/dist/extensions/server.d.mts +5 -0
  38. package/dist/extensions/server.d.mts.map +1 -0
  39. package/dist/extensions/server.mjs +87 -0
  40. package/dist/extensions/server.mjs.map +1 -0
  41. package/dist/harness-CEuhOTr2.mjs +716 -0
  42. package/dist/harness-CEuhOTr2.mjs.map +1 -0
  43. package/dist/harness-fK8CSaU1.cjs +785 -0
  44. package/dist/harness-fK8CSaU1.cjs.map +1 -0
  45. package/dist/index-CBt-PlVG.d.mts +22 -0
  46. package/dist/index-CBt-PlVG.d.mts.map +1 -0
  47. package/dist/index.cjs +3 -0
  48. package/dist/index.d.mts +2 -0
  49. package/dist/index.mjs +2 -0
  50. package/dist/sandboxBridge-B3FRZkC2.cjs +131 -0
  51. package/dist/sandboxBridge-B3FRZkC2.cjs.map +1 -0
  52. package/dist/sandboxBridge-D6RAncmy.mjs +72 -0
  53. package/dist/sandboxBridge-D6RAncmy.mjs.map +1 -0
  54. package/dist/sandboxHarness-BIvuRMio.d.mts +38 -0
  55. package/dist/sandboxHarness-BIvuRMio.d.mts.map +1 -0
  56. package/dist/sandboxHarness.cjs +4 -0
  57. package/dist/sandboxHarness.d.mts +38 -0
  58. package/dist/sandboxHarness.d.mts.map +1 -0
  59. package/dist/sandboxHarness.mjs +2 -0
  60. package/dist/sandboxResources-5pvXyBW9.mjs +79 -0
  61. package/dist/sandboxResources-5pvXyBW9.mjs.map +1 -0
  62. package/dist/sandboxResources-Djxj5l1V.cjs +120 -0
  63. package/dist/sandboxResources-Djxj5l1V.cjs.map +1 -0
  64. package/llms.txt +11 -0
  65. package/package.json +132 -0
  66. package/src/prompts/doompi-use-sandbox/SKILL.md +14 -0
@@ -0,0 +1,785 @@
1
+ const require_brokerHost = require("./brokerHost-BzffXeb2.cjs");
2
+ const require_sandboxBridge = require("./sandboxBridge-B3FRZkC2.cjs");
3
+ let node_child_process = require("node:child_process");
4
+ let _agimon_ai_doompi_core_sandboxHarness = require("@agimon-ai/doompi-core/sandboxHarness");
5
+ let node_path = require("node:path");
6
+ node_path = require_brokerHost.__toESM(node_path, 1);
7
+ let node_crypto = require("node:crypto");
8
+ let node_fs = require("node:fs");
9
+ node_fs = require_brokerHost.__toESM(node_fs, 1);
10
+ let node_module = require("node:module");
11
+ let node_os = require("node:os");
12
+ node_os = require_brokerHost.__toESM(node_os, 1);
13
+ let node_net = require("node:net");
14
+ node_net = require_brokerHost.__toESM(node_net, 1);
15
+ //#region src/services/devcontainer/index.ts
16
+ /**
17
+ * Relative locations the Dev Containers spec looks for a configuration.
18
+ *
19
+ * Ordered as the spec resolves them, so the first hit on disk wins.
20
+ */
21
+ const DEVCONTAINER_CONFIG_PATHS = [".devcontainer/devcontainer.json", ".devcontainer.json"];
22
+ const DEVCONTAINER_DISABLED_ENV = "DOOMPI_SANDBOX_DEVCONTAINER";
23
+ const DEVCONTAINER_CLI_PACKAGE = "@devcontainers/cli@0.80.0";
24
+ /** Arguments for bringing the workspace's own dev container up. */
25
+ function devcontainerUpArgs(options) {
26
+ return [
27
+ "up",
28
+ "--workspace-folder",
29
+ options.repoRoot,
30
+ "--log-format",
31
+ "json",
32
+ ...options.socketDirectory && options.socketTarget ? ["--mount", `type=bind,source=${options.socketDirectory},target=${options.socketTarget}`] : []
33
+ ];
34
+ }
35
+ /**
36
+ * Reads the container id out of the CLI's JSON log stream.
37
+ *
38
+ * The stream carries progress records too, so the result is the last record
39
+ * that reports an outcome rather than the first line parsed.
40
+ */
41
+ function parseDevcontainerUp(stdout) {
42
+ let outcome;
43
+ for (const line of stdout.split("\n")) {
44
+ const trimmed = line.trim();
45
+ if (!trimmed.startsWith("{")) continue;
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(trimmed);
49
+ } catch {
50
+ continue;
51
+ }
52
+ if (typeof parsed === "object" && parsed !== null && "outcome" in parsed) outcome = parsed;
53
+ }
54
+ if (!outcome) return { error: "The dev container CLI reported no outcome." };
55
+ if (outcome.outcome !== "success") return { error: outcome.description || outcome.message || `Dev container start reported ${outcome.outcome}.` };
56
+ if (!outcome.containerId) return { error: "The dev container started but reported no container id." };
57
+ return {
58
+ containerId: outcome.containerId,
59
+ remoteWorkspaceFolder: outcome.remoteWorkspaceFolder
60
+ };
61
+ }
62
+ /**
63
+ * Maps a host path into the dev container's own workspace.
64
+ *
65
+ * A dev container mounts the workspace where its configuration says, commonly
66
+ * /workspaces/<name>, so the host path the launch was given does not exist
67
+ * inside and would fail the exec before the agent ever starts.
68
+ */
69
+ function containerWorkspacePath(repoRoot, cwd, remoteWorkspaceFolder) {
70
+ const inside = node_path.default.relative(repoRoot, cwd);
71
+ const relative = inside === "" || inside.startsWith("..") || node_path.default.isAbsolute(inside) ? "" : inside;
72
+ return relative ? `${remoteWorkspaceFolder}/${relative.split("\\").join("/")}` : remoteWorkspaceFolder;
73
+ }
74
+ /**
75
+ * Installs the distribution inside a dev container that lacks it.
76
+ *
77
+ * The container is the workspace's own, so it carries the project toolchain
78
+ * rather than this layer's, and nothing guarantees DoomPi is present. `up`
79
+ * reuses an existing container, so this is paid once for its lifetime.
80
+ */
81
+ function bootstrapCommand(version) {
82
+ return [
83
+ "sh",
84
+ "-c",
85
+ `command -v doompi >/dev/null 2>&1 && exit 0; command -v npm >/dev/null 2>&1 || { echo "doompi-sandbox: this dev container has no npm to install DoomPi with" >&2; exit 127; }; npm install -g @agimon-ai/doompi@${version} >/dev/null`
86
+ ];
87
+ }
88
+ /**
89
+ * Rewrites forwarded host paths into the dev container's workspace.
90
+ *
91
+ * The launcher replays its resolved options, and several carry absolute host
92
+ * paths: --cwd always, plugin and additional directories when set. Those
93
+ * resolve in the built-in image, which mounts the repository at the path it
94
+ * has on the host, but a dev container mounts it wherever its configuration
95
+ * says, and the session would fail looking for a repository that is not there.
96
+ */
97
+ function mapForwardArgs(args, repoRoot, remoteWorkspaceFolder) {
98
+ return args.map((argument) => argument === repoRoot || argument.startsWith(`${repoRoot}/`) ? containerWorkspacePath(repoRoot, argument, remoteWorkspaceFolder) : argument);
99
+ }
100
+ /**
101
+ * Runs a command in the dev container through the engine rather than the CLI.
102
+ *
103
+ * `devcontainer exec` allocates no terminal, which a full screen TUI needs, so
104
+ * the session is attached to the container the CLI already started.
105
+ */
106
+ function devcontainerExecArgs(options) {
107
+ const environmentPairs = Object.entries(options.environment).sort(([left], [right]) => left.localeCompare(right));
108
+ return [
109
+ "exec",
110
+ "-i",
111
+ ...options.hasTty ? ["-t"] : [],
112
+ "-w",
113
+ options.cwd,
114
+ ...environmentPairs.flatMap(([name, value]) => ["-e", `${name}=${value}`]),
115
+ options.containerId,
116
+ ...options.command
117
+ ];
118
+ }
119
+ //#endregion
120
+ //#region src/services/oauthCallback/index.ts
121
+ /**
122
+ * Fixed loopback ports Pi's OAuth flows listen on.
123
+ *
124
+ * A provider redirects the browser to `http://localhost:<port>`, so the host
125
+ * port has to match exactly and cannot be remapped. Providers that bind an
126
+ * ephemeral port instead, such as OpenRouter, cannot be published ahead of the
127
+ * flow and are not covered.
128
+ */
129
+ const OAUTH_CALLBACK_PORTS = [
130
+ 1455,
131
+ 1456,
132
+ 53692
133
+ ];
134
+ const OAUTH_CALLBACK_HOST_ENV = "PI_OAUTH_CALLBACK_HOST";
135
+ /**
136
+ * Address the in-container callback server binds.
137
+ *
138
+ * Pi binds loopback by default, which a published port cannot reach: the
139
+ * engine forwards to the container's external interface, not its loopback.
140
+ * Only the bind address changes; the redirect the provider sees stays
141
+ * `localhost`.
142
+ */
143
+ const OAUTH_CONTAINER_BIND = "0.0.0.0";
144
+ /** Publishes each reachable callback port back onto the host's loopback. */
145
+ function oauthPublishArgs(ports) {
146
+ return ports.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]);
147
+ }
148
+ //#endregion
149
+ //#region src/services/sandboxEnvironment/index.ts
150
+ const EXACT_NAMES = /* @__PURE__ */ new Set([
151
+ "TERM",
152
+ "COLORTERM",
153
+ "LANG",
154
+ "TZ",
155
+ "DOOMPI_PRESET",
156
+ "ELICITATION_SESSION_ID",
157
+ "HTTP_PROXY",
158
+ "HTTPS_PROXY",
159
+ "NO_PROXY",
160
+ "http_proxy",
161
+ "https_proxy",
162
+ "no_proxy"
163
+ ]);
164
+ const NAME_PREFIXES = ["LC_"];
165
+ const NAME_SUFFIXES = [
166
+ "_API_KEY",
167
+ "_AUTH_TOKEN",
168
+ "_BASE_URL"
169
+ ];
170
+ /** True for variables shaped like a provider credential or endpoint override. */
171
+ function isCredentialEnvName(name) {
172
+ return NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));
173
+ }
174
+ function allowed(name) {
175
+ return EXACT_NAMES.has(name) || NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) || NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));
176
+ }
177
+ /**
178
+ * Projects the host environment down to what a sandboxed session may see.
179
+ *
180
+ * The list is an allowlist on purpose: everything a terminal accumulates
181
+ * (agents, sockets, tokens, paths) stays on the host unless a rule names it.
182
+ * Provider credentials pass through for now; the credential broker planned for
183
+ * the client-server phase removes even those from the container.
184
+ */
185
+ function filterSandboxEnvironment(environment) {
186
+ const filtered = {};
187
+ for (const [name, value] of Object.entries(environment)) if (value !== void 0 && allowed(name)) filtered[name] = value;
188
+ return filtered;
189
+ }
190
+ //#endregion
191
+ //#region src/services/sandboxPlan/index.ts
192
+ const CONTAINER_HOME = "/doompi-home";
193
+ const REPOSITORY_LABEL = "doompi.sandbox.repo";
194
+ const LAUNCHER_BINARY$1 = "doompi";
195
+ const NODE_BINARY = "node";
196
+ const LINUX_PLATFORM = "linux";
197
+ const ENABLED = "1";
198
+ const BROKER_MOUNT_DIRECTORY$1 = "/run/doompi";
199
+ /**
200
+ * Replaces every credential in the container with the broker's session token.
201
+ *
202
+ * Dropping all credential-shaped variables first is what makes the promise
203
+ * hold for providers the broker does not carry: an unbrokered key would
204
+ * otherwise stay readable inside the container.
205
+ */
206
+ function brokeredEnvironment(filtered, broker) {
207
+ const environment = {};
208
+ for (const [name, value] of Object.entries(filtered)) if (!isCredentialEnvName(name)) environment[name] = value;
209
+ for (const name of broker.withheldEnv) environment[name] = broker.token;
210
+ if (broker.endpoint.transport === "unix") {
211
+ environment[require_sandboxBridge.BROKER_SOCKET_ENV] = require_sandboxBridge.BROKER_SOCKET_CONTAINER_PATH;
212
+ environment[require_sandboxBridge.BROKER_PORT_ENV] = String(require_sandboxBridge.BROKER_CONTAINER_PORT);
213
+ } else environment[require_sandboxBridge.BROKER_ADDRESS_ENV] = `${require_sandboxBridge.BROKER_HOST_GATEWAY}:${broker.endpoint.port}`;
214
+ environment[require_sandboxBridge.BROKER_PROVIDERS_ENV] = broker.providers.join(",");
215
+ return environment;
216
+ }
217
+ /**
218
+ * Grants the container the one route it needs to reach the broker.
219
+ *
220
+ * A mounted socket needs no network at all. TCP needs the engine's host
221
+ * gateway named explicitly, because it is not resolvable by default on every
222
+ * engine.
223
+ */
224
+ function brokerAccessArgs(broker) {
225
+ if (!broker) return [];
226
+ if (broker.endpoint.transport === "unix") return ["-v", `${broker.endpoint.socketDirectory}:${BROKER_MOUNT_DIRECTORY$1}`];
227
+ return ["--add-host", `${require_sandboxBridge.BROKER_HOST_GATEWAY}:host-gateway`];
228
+ }
229
+ /**
230
+ * Projects one launch into a disposable engine run.
231
+ *
232
+ * The container is ephemeral; persistence lives in two named volumes: the
233
+ * container home (Pi sessions, sync cache) and a shadow over the repository's
234
+ * `.pi` store, which keeps Linux installs from corrupting the host's own
235
+ * platform-specific packages on the shared workspace mount.
236
+ */
237
+ /**
238
+ * The environment a sandboxed session runs with, whichever container hosts it.
239
+ *
240
+ * Shared with the dev container path, which builds no run arguments of its own
241
+ * but must apply the same allowlist and the same credential substitution.
242
+ */
243
+ function containerEnvironment(environment, broker, loginPorts = []) {
244
+ const filtered = filterSandboxEnvironment(environment);
245
+ const resolved = broker ? brokeredEnvironment(filtered, broker) : filtered;
246
+ resolved[_agimon_ai_doompi_core_sandboxHarness.DOOMPI_SANDBOX_ENV] = ENABLED;
247
+ if (loginPorts.length > 0) resolved[OAUTH_CALLBACK_HOST_ENV] = OAUTH_CONTAINER_BIND;
248
+ return resolved;
249
+ }
250
+ function buildSandboxPlan(input) {
251
+ const { repoRoot, cwd, engine, host, broker, imageTag } = input;
252
+ const loginPorts = input.loginPorts ?? [];
253
+ const environmentPairs = Object.entries(containerEnvironment(input.environment, broker, loginPorts)).sort(([left], [right]) => left.localeCompare(right));
254
+ return {
255
+ imageTag,
256
+ runArgs: [
257
+ "run",
258
+ "--rm",
259
+ "-i",
260
+ ...host.hasTty ? ["-t"] : [],
261
+ "--label",
262
+ `${REPOSITORY_LABEL}=${repoRoot}`,
263
+ "-v",
264
+ `${repoRoot}:${repoRoot}`,
265
+ "-v",
266
+ `${host.repoKey}-home:${CONTAINER_HOME}`,
267
+ "-v",
268
+ `${host.repoKey}-pi:${repoRoot}/.pi`,
269
+ ...brokerAccessArgs(broker),
270
+ ...oauthPublishArgs(loginPorts),
271
+ "-w",
272
+ cwd,
273
+ "-e",
274
+ `HOME=${CONTAINER_HOME}`,
275
+ ...environmentPairs.flatMap(([name, value]) => ["-e", `${name}=${value}`]),
276
+ ...host.platform === LINUX_PLATFORM && host.userId !== void 0 ? [
277
+ "--user",
278
+ `${host.userId}:${host.groupId ?? host.userId}`,
279
+ ...engine === "podman" ? ["--userns=keep-id"] : []
280
+ ] : [],
281
+ ...input.runFlags ?? [],
282
+ imageTag,
283
+ ...broker?.endpoint.transport === "unix" ? [NODE_BINARY, require_sandboxBridge.BRIDGE_CONTAINER_PATH] : [],
284
+ LAUNCHER_BINARY$1,
285
+ ...input.forwardArgs
286
+ ]
287
+ };
288
+ }
289
+ //#endregion
290
+ //#region src/services/engineProcess/index.ts
291
+ const INHERIT = "inherit";
292
+ const PIPE = "pipe";
293
+ const IGNORE = "ignore";
294
+ const FORWARDED_SIGNALS = [
295
+ "SIGINT",
296
+ "SIGTERM",
297
+ "SIGHUP"
298
+ ];
299
+ /** Spawns the container engine, keeping the session's terminal attached. */
300
+ var SpawnEngineProcessRunner = class {
301
+ run(command, args, options = {}) {
302
+ return new Promise((resolve, reject) => {
303
+ const child = (0, node_child_process.spawn)(command, args, { stdio: options.input === void 0 ? INHERIT : [
304
+ PIPE,
305
+ INHERIT,
306
+ INHERIT
307
+ ] });
308
+ if (options.input !== void 0) child.stdin?.end(options.input);
309
+ const forwarders = FORWARDED_SIGNALS.map((signal) => {
310
+ const forward = () => {
311
+ child.kill(signal);
312
+ };
313
+ process.on(signal, forward);
314
+ return {
315
+ signal,
316
+ forward
317
+ };
318
+ });
319
+ const cleanup = () => {
320
+ for (const { signal, forward } of forwarders) process.off(signal, forward);
321
+ };
322
+ child.once("error", (error) => {
323
+ cleanup();
324
+ reject(error);
325
+ });
326
+ child.once("exit", (code, signal) => {
327
+ cleanup();
328
+ resolve(code ?? (signal ? 1 : 0));
329
+ });
330
+ });
331
+ }
332
+ capture(command, args) {
333
+ return new Promise((resolve) => {
334
+ const child = (0, node_child_process.spawn)(command, args, { stdio: [
335
+ IGNORE,
336
+ PIPE,
337
+ IGNORE
338
+ ] });
339
+ let stdout = "";
340
+ child.stdout?.on("data", (chunk) => {
341
+ stdout += chunk.toString();
342
+ });
343
+ child.once("error", () => {
344
+ resolve(void 0);
345
+ });
346
+ child.once("exit", (code) => {
347
+ resolve({
348
+ exitCode: code ?? 1,
349
+ stdout
350
+ });
351
+ });
352
+ });
353
+ }
354
+ };
355
+ //#endregion
356
+ //#region src/services/devcontainer/runtime.ts
357
+ const DEVCONTAINER_BINARY = "devcontainer";
358
+ const NPX_BINARY = "npx";
359
+ const LAUNCHER_BINARY = "doompi";
360
+ const BROKER_MOUNT_DIRECTORY = "/run/doompi";
361
+ /** Locates the workspace's own dev container configuration, if it has one. */
362
+ function findDevcontainerConfig(repoRoot) {
363
+ for (const relative of DEVCONTAINER_CONFIG_PATHS) {
364
+ const candidate = node_path.default.join(repoRoot, relative);
365
+ if (node_fs.default.existsSync(candidate)) return candidate;
366
+ }
367
+ }
368
+ /**
369
+ * Resolves how to invoke the Dev Containers CLI.
370
+ *
371
+ * A local install is preferred so a launch does not depend on the network;
372
+ * npx is the fallback, pinned so the tool cannot change underneath a session.
373
+ */
374
+ async function resolveDevcontainerCli(runner) {
375
+ if ((await runner.capture(DEVCONTAINER_BINARY, ["--version"]))?.exitCode === 0) return {
376
+ command: DEVCONTAINER_BINARY,
377
+ prefix: []
378
+ };
379
+ return {
380
+ command: NPX_BINARY,
381
+ prefix: ["-y", DEVCONTAINER_CLI_PACKAGE]
382
+ };
383
+ }
384
+ /**
385
+ * Runs the session inside the workspace's own dev container.
386
+ *
387
+ * The container is brought up by the Dev Containers CLI, so the file decides
388
+ * the image, features, mounts and lifecycle hooks: the session gets the
389
+ * project's real toolchain, and this layer's isolation guarantees do not
390
+ * apply beyond the environment it passes in.
391
+ */
392
+ async function runDevcontainerSession(options) {
393
+ const cli = await resolveDevcontainerCli(options.runner);
394
+ options.onProgress?.("starting the workspace dev container");
395
+ const upArgs = devcontainerUpArgs({
396
+ repoRoot: options.repoRoot,
397
+ socketDirectory: options.socketDirectory,
398
+ socketTarget: options.socketDirectory ? BROKER_MOUNT_DIRECTORY : void 0
399
+ });
400
+ const up = await options.runner.capture(cli.command, [...cli.prefix, ...upArgs]);
401
+ if (!up) throw new Error("The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.");
402
+ const { containerId, remoteWorkspaceFolder, error } = parseDevcontainerUp(up.stdout);
403
+ if (!containerId) throw new Error(`The dev container did not start: ${error ?? `exit ${up.exitCode}`}`);
404
+ const bootstrap = await options.runner.run(options.engine, [
405
+ "exec",
406
+ containerId,
407
+ ...bootstrapCommand(options.version)
408
+ ]);
409
+ if (bootstrap !== 0) throw new Error(`DoomPi could not be installed in the dev container (exit ${bootstrap}). Add it to the dev container, or unset the dev container preference to use the built-in image.`);
410
+ options.onProgress?.(`attaching to dev container ${containerId.slice(0, 12)}`);
411
+ return options.runner.run(options.engine, devcontainerExecArgs({
412
+ containerId,
413
+ cwd: remoteWorkspaceFolder ? containerWorkspacePath(options.repoRoot, options.cwd, remoteWorkspaceFolder) : options.cwd,
414
+ environment: options.environment,
415
+ hasTty: options.hasTty,
416
+ command: [LAUNCHER_BINARY, ...remoteWorkspaceFolder ? mapForwardArgs(options.forwardArgs, options.repoRoot, remoteWorkspaceFolder) : options.forwardArgs]
417
+ }));
418
+ }
419
+ //#endregion
420
+ //#region src/services/loginPorts/index.ts
421
+ const LOOPBACK = "127.0.0.1";
422
+ function portIsFree(port) {
423
+ return new Promise((resolve) => {
424
+ const probe = node_net.default.createServer();
425
+ probe.once("error", () => resolve(false));
426
+ probe.listen(port, LOOPBACK, () => {
427
+ probe.close(() => resolve(true));
428
+ });
429
+ });
430
+ }
431
+ /**
432
+ * Reports which OAuth callback ports this launch may publish.
433
+ *
434
+ * A taken port means another sandbox already holds it, so publishing would
435
+ * fail the whole launch over a login the session may never attempt. Skipping
436
+ * it costs only the ability to log in from inside this particular container.
437
+ */
438
+ async function availableLoginPorts(ports = OAUTH_CALLBACK_PORTS) {
439
+ const free = [];
440
+ for (const port of ports) if (await portIsFree(port)) free.push(port);
441
+ return free;
442
+ }
443
+ //#endregion
444
+ //#region src/services/runFlags/index.ts
445
+ /**
446
+ * Splits configured engine options into argv entries.
447
+ *
448
+ * Whitespace separated rather than shell parsed: the engine is spawned without
449
+ * a shell, so quoting rules would only invent a syntax the caller does not
450
+ * actually get.
451
+ */
452
+ function parseRunFlags(configured) {
453
+ return (configured ?? "").split(/\s+/).filter((flag) => flag.length > 0);
454
+ }
455
+ const OPTION_FORM = /^--?[^\s=]+(=.*)?$/s;
456
+ /**
457
+ * Requires every configured entry to be a self-contained option.
458
+ *
459
+ * Separated values cannot be told from positional arguments without knowing
460
+ * each option's arity, and a stray positional would silently replace the image
461
+ * or its command. Demanding `--flag=value` removes the ambiguity instead of
462
+ * guessing at it.
463
+ */
464
+ function assertRunFlags(flags, optionName) {
465
+ const invalid = flags.find((flag) => !OPTION_FORM.test(flag));
466
+ if (invalid !== void 0) throw new Error(`${optionName} accepts engine options in --flag or --flag=value form; "${invalid}" is neither. Write a value as part of its option, for example --runtime=runsc.`);
467
+ }
468
+ //#endregion
469
+ //#region src/services/sandboxImage/index.ts
470
+ const IMAGE_REPOSITORY = "doompi-sandbox";
471
+ const TAG_SAFE = /[^A-Za-z0-9_.-]/g;
472
+ /**
473
+ * Names one image build: a distribution version plus its image definition.
474
+ *
475
+ * The digest matters as much as the version. Editing the Dockerfile or the
476
+ * bridge without it would silently reuse a cached image that predates the
477
+ * change.
478
+ */
479
+ function formatImageTag(version, digest) {
480
+ return `${IMAGE_REPOSITORY}:v${version.replaceAll(TAG_SAFE, "-")}-${digest}`;
481
+ }
482
+ /**
483
+ * Self-contained Linux image for sandboxed sessions.
484
+ *
485
+ * nano is present because Pi falls back to it for the external editor when
486
+ * VISUAL and EDITOR are unset, which is always the case here: a host editor
487
+ * variable naming a desktop application would resolve to a binary the
488
+ * container does not have, so it is deliberately not forwarded.
489
+ *
490
+ * The distribution installs from the registry rather than mounting host
491
+ * modules: host installs carry platform-specific binaries a Linux container
492
+ * cannot run. DOOMPI_SANDBOX is baked in so nothing that reaches a shell in
493
+ * the container can present itself as an unsandboxed session.
494
+ */
495
+ function sandboxDockerfile() {
496
+ return [
497
+ "FROM node:22-bookworm-slim",
498
+ "RUN apt-get update \\",
499
+ " && apt-get install -y --no-install-recommends ca-certificates curl git nano openssh-client ripgrep \\",
500
+ " && rm -rf /var/lib/apt/lists/*",
501
+ "ARG DOOMPI_VERSION",
502
+ "RUN npm install -g @agimon-ai/doompi@${DOOMPI_VERSION}",
503
+ `COPY ${require_sandboxBridge.BRIDGE_FILE_NAME} ${require_sandboxBridge.BRIDGE_CONTAINER_PATH}`,
504
+ "# World-writable so the launch can map any host user id onto it.",
505
+ "RUN mkdir -m 0777 /doompi-home",
506
+ "ENV DOOMPI_SANDBOX=1 HOME=/doompi-home",
507
+ ""
508
+ ].join("\n");
509
+ }
510
+ /**
511
+ * Image for the cockpit container, which holds the hub rather than one session.
512
+ *
513
+ * Kept separate from the sandbox image rather than folded into it. The cockpit
514
+ * needs the web package and a tunnel client, and making every interactive
515
+ * `--sandbox` launch pay for an extra apt repository and a larger download to
516
+ * get them would be a poor trade.
517
+ *
518
+ * Installing `@agimon-ai/doompi-web` is enough for all three programs: its
519
+ * `@agimon-ai/doompi` dependency owns both the session server executable and
520
+ * the agent runtime.
521
+ */
522
+ function cockpitDockerfile() {
523
+ return [
524
+ "FROM node:22-bookworm-slim",
525
+ "RUN apt-get update \\",
526
+ " && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\",
527
+ " && rm -rf /var/lib/apt/lists/*",
528
+ "RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\",
529
+ " -o /usr/share/keyrings/cloudflare-main.gpg \\",
530
+ " && echo \"deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main\" \\",
531
+ " > /etc/apt/sources.list.d/cloudflared.list \\",
532
+ " && apt-get update && apt-get install -y --no-install-recommends cloudflared \\",
533
+ " && rm -rf /var/lib/apt/lists/*",
534
+ "ARG DOOMPI_VERSION",
535
+ "RUN npm install -g @agimon-ai/doompi-web@${DOOMPI_VERSION}",
536
+ "# World-writable so the launch can map any host user id onto it.",
537
+ "RUN mkdir -m 0777 /doompi-home",
538
+ "ENV DOOMPI_SANDBOX=1 HOME=/doompi-home",
539
+ ""
540
+ ].join("\n");
541
+ }
542
+ //#endregion
543
+ //#region src/services/sandboxImageTag/index.ts
544
+ const DIGEST_LENGTH = 8;
545
+ /** Tags the image with the distribution version and a digest of its definition. */
546
+ function sandboxImageTag(version) {
547
+ return formatImageTag(version, (0, node_crypto.createHash)("sha256").update(sandboxDockerfile()).update(require_sandboxBridge.sandboxBridgeSource()).digest("hex").slice(0, DIGEST_LENGTH));
548
+ }
549
+ /**
550
+ * Tags the cockpit image separately from the sandbox one.
551
+ *
552
+ * Same digest scheme, different definition, so editing either Dockerfile
553
+ * invalidates only its own image.
554
+ */
555
+ function cockpitImageTag(version) {
556
+ const digest = (0, node_crypto.createHash)("sha256").update(cockpitDockerfile()).digest("hex").slice(0, DIGEST_LENGTH);
557
+ return formatImageTag(`${version}-cockpit`, digest);
558
+ }
559
+ //#endregion
560
+ //#region src/services/harness/index.ts
561
+ const ENGINE_ENV = "DOOMPI_SANDBOX_ENGINE";
562
+ const BROKER_DISABLED_ENV = "DOOMPI_SANDBOX_BROKER";
563
+ const RUN_FLAGS_ENV = "DOOMPI_SANDBOX_RUN_FLAGS";
564
+ const DISABLED_VALUE = "0";
565
+ const ENGINES = [
566
+ "docker",
567
+ "podman",
568
+ "nerdctl",
569
+ "finch"
570
+ ];
571
+ const DOCKERFILE_NAME = "Dockerfile";
572
+ const MODES_FILE = node_path.default.join(".doom", "modes.yaml");
573
+ const LOCAL_PACKAGE_PATTERN = /^\s*(?:-|name:)\s*["']?\.{1,2}\//m;
574
+ const REPO_KEY_LENGTH = 12;
575
+ const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
576
+ const SELF_MANIFEST = "@agimon-ai/doompi-sandbox/package.json";
577
+ function distributionVersion() {
578
+ return require$1(SELF_MANIFEST).version;
579
+ }
580
+ /** Fixed per-repository directory a reused dev container can keep mounted. */
581
+ function brokerDirectory(repoRoot) {
582
+ return node_path.default.join(node_os.default.tmpdir(), `${repoKey(repoRoot)}-broker`);
583
+ }
584
+ function repoKey(repoRoot) {
585
+ return `doompi-sandbox-${(0, node_crypto.createHash)("sha256").update(repoRoot).digest("hex").slice(0, REPO_KEY_LENGTH)}`;
586
+ }
587
+ async function detectEngine(runner, environment) {
588
+ const configured = environment[ENGINE_ENV]?.trim();
589
+ if (configured) {
590
+ if (ENGINES.includes(configured)) return configured;
591
+ throw new Error(`${ENGINE_ENV} must be one of: ${ENGINES.join(", ")}.`);
592
+ }
593
+ for (const engine of ENGINES) if ((await runner.capture(engine, ["--version"]))?.exitCode === 0) return engine;
594
+ throw new Error(`No container engine found. Install one of ${ENGINES.join(", ")}, or set ${ENGINE_ENV}.`);
595
+ }
596
+ /**
597
+ * Builds the image if the engine does not already have it.
598
+ *
599
+ * Takes its definition rather than assuming one, because the interactive
600
+ * sandbox and the cockpit container are two different images built the same
601
+ * way. The tag already carries a digest of the definition, so a changed
602
+ * Dockerfile misses the inspect and rebuilds.
603
+ */
604
+ async function ensureImage(runner, engine, version, onProgress, definition) {
605
+ const tag = definition.tag;
606
+ if ((await runner.capture(engine, [
607
+ "image",
608
+ "inspect",
609
+ tag
610
+ ]))?.exitCode === 0) return tag;
611
+ onProgress?.(`building ${tag} (first run for this DoomPi version)`);
612
+ const context = node_fs.default.mkdtempSync(node_path.default.join(node_os.default.tmpdir(), "doompi-sandbox-build-"));
613
+ try {
614
+ node_fs.default.writeFileSync(node_path.default.join(context, DOCKERFILE_NAME), definition.dockerfile);
615
+ for (const [name, contents] of Object.entries(definition.files ?? {})) node_fs.default.writeFileSync(node_path.default.join(context, name), contents);
616
+ const exitCode = await runner.run(engine, [
617
+ "build",
618
+ "-t",
619
+ tag,
620
+ "--build-arg",
621
+ `DOOMPI_VERSION=${version}`,
622
+ context
623
+ ]);
624
+ if (exitCode !== 0) throw new Error(`Building the ${tag} image failed with exit code ${exitCode}.`);
625
+ } finally {
626
+ node_fs.default.rmSync(context, {
627
+ recursive: true,
628
+ force: true
629
+ });
630
+ }
631
+ return tag;
632
+ }
633
+ function warnAboutLocalPackages(repoRoot, onProgress) {
634
+ let modes;
635
+ try {
636
+ modes = node_fs.default.readFileSync(node_path.default.join(repoRoot, MODES_FILE), "utf8");
637
+ } catch {
638
+ return;
639
+ }
640
+ if (LOCAL_PACKAGE_PATTERN.test(modes)) onProgress?.("modes.yaml declares local workspace packages; their platform-specific dependencies must be installed inside the container before they can load");
641
+ }
642
+ /** Builds the launcher with injectable seams for tests. */
643
+ function createSandboxLauncher(dependencies = {}) {
644
+ const runner = dependencies.runner ?? new SpawnEngineProcessRunner();
645
+ const beginBroker = dependencies.startBroker ?? require_brokerHost.startBroker;
646
+ const resolveLoginPorts = dependencies.loginPorts ?? availableLoginPorts;
647
+ return { async launchSandbox(request) {
648
+ const engine = await detectEngine(runner, request.environment);
649
+ const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);
650
+ assertRunFlags(runFlags, RUN_FLAGS_ENV);
651
+ request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(" ")}` : ""}`);
652
+ const version = dependencies.version ?? distributionVersion();
653
+ const devcontainer = request.environment["DOOMPI_SANDBOX_DEVCONTAINER"] === DISABLED_VALUE ? void 0 : findDevcontainerConfig(request.repoRoot);
654
+ const imageTag = devcontainer ? void 0 : await ensureImage(runner, engine, version, request.onProgress, {
655
+ tag: sandboxImageTag(version),
656
+ dockerfile: sandboxDockerfile(),
657
+ files: { [require_sandboxBridge.BRIDGE_FILE_NAME]: require_sandboxBridge.sandboxBridgeSource() }
658
+ });
659
+ if (devcontainer) {
660
+ request.onProgress?.(`using the workspace dev container from ${node_path.default.relative(request.repoRoot, devcontainer)}`);
661
+ request.onProgress?.("its configuration owns the mounts and run arguments, so this is not an isolation boundary");
662
+ } else warnAboutLocalPackages(request.repoRoot, request.onProgress);
663
+ const broker = request.environment[BROKER_DISABLED_ENV] === DISABLED_VALUE ? void 0 : await beginBroker({
664
+ environment: request.environment,
665
+ onDenied: (reason) => request.onProgress?.(`broker refused a call: ${reason}`),
666
+ ...devcontainer ? { socketDirectory: brokerDirectory(request.repoRoot) } : {}
667
+ });
668
+ if (broker) request.onProgress?.(`brokering ${broker.providers.join(", ")}; provider keys stay on the host`);
669
+ const loginPorts = devcontainer ? [] : await resolveLoginPorts();
670
+ if (!devcontainer && loginPorts.length < OAUTH_CALLBACK_PORTS.length) request.onProgress?.("some OAuth callback ports are already taken; /login inside this sandbox may not complete");
671
+ try {
672
+ const hasTty = process.stdin.isTTY === true && process.stdout.isTTY === true;
673
+ if (devcontainer) return await runDevcontainerSession({
674
+ repoRoot: request.repoRoot,
675
+ cwd: request.cwd,
676
+ forwardArgs: request.forwardArgs,
677
+ environment: containerEnvironment(request.environment, broker),
678
+ engine,
679
+ runner,
680
+ version,
681
+ hasTty,
682
+ ...broker?.endpoint.transport === "unix" ? { socketDirectory: broker.endpoint.socketDirectory } : {},
683
+ ...request.onProgress ? { onProgress: request.onProgress } : {}
684
+ });
685
+ const host = {
686
+ hasTty,
687
+ platform: process.platform,
688
+ userId: process.getuid?.(),
689
+ groupId: process.getgid?.(),
690
+ repoKey: repoKey(request.repoRoot),
691
+ version,
692
+ ...dependencies.hostFacts
693
+ };
694
+ const plan = buildSandboxPlan({
695
+ repoRoot: request.repoRoot,
696
+ cwd: request.cwd,
697
+ forwardArgs: request.forwardArgs,
698
+ environment: request.environment,
699
+ engine,
700
+ host,
701
+ imageTag,
702
+ runFlags,
703
+ loginPorts,
704
+ broker
705
+ });
706
+ request.onProgress?.(`starting contained session in ${imageTag}`);
707
+ return await runner.run(engine, plan.runArgs);
708
+ } finally {
709
+ await broker?.stop();
710
+ }
711
+ } };
712
+ }
713
+ /** Contract entry the DoomPi harness resolves for `--sandbox`. */
714
+ function launchSandbox(request) {
715
+ return createSandboxLauncher().launchSandbox(request);
716
+ }
717
+ //#endregion
718
+ Object.defineProperty(exports, "DEVCONTAINER_DISABLED_ENV", {
719
+ enumerable: true,
720
+ get: function() {
721
+ return DEVCONTAINER_DISABLED_ENV;
722
+ }
723
+ });
724
+ Object.defineProperty(exports, "SpawnEngineProcessRunner", {
725
+ enumerable: true,
726
+ get: function() {
727
+ return SpawnEngineProcessRunner;
728
+ }
729
+ });
730
+ Object.defineProperty(exports, "assertRunFlags", {
731
+ enumerable: true,
732
+ get: function() {
733
+ return assertRunFlags;
734
+ }
735
+ });
736
+ Object.defineProperty(exports, "cockpitDockerfile", {
737
+ enumerable: true,
738
+ get: function() {
739
+ return cockpitDockerfile;
740
+ }
741
+ });
742
+ Object.defineProperty(exports, "cockpitImageTag", {
743
+ enumerable: true,
744
+ get: function() {
745
+ return cockpitImageTag;
746
+ }
747
+ });
748
+ Object.defineProperty(exports, "containerEnvironment", {
749
+ enumerable: true,
750
+ get: function() {
751
+ return containerEnvironment;
752
+ }
753
+ });
754
+ Object.defineProperty(exports, "createSandboxLauncher", {
755
+ enumerable: true,
756
+ get: function() {
757
+ return createSandboxLauncher;
758
+ }
759
+ });
760
+ Object.defineProperty(exports, "detectEngine", {
761
+ enumerable: true,
762
+ get: function() {
763
+ return detectEngine;
764
+ }
765
+ });
766
+ Object.defineProperty(exports, "ensureImage", {
767
+ enumerable: true,
768
+ get: function() {
769
+ return ensureImage;
770
+ }
771
+ });
772
+ Object.defineProperty(exports, "launchSandbox", {
773
+ enumerable: true,
774
+ get: function() {
775
+ return launchSandbox;
776
+ }
777
+ });
778
+ Object.defineProperty(exports, "parseRunFlags", {
779
+ enumerable: true,
780
+ get: function() {
781
+ return parseRunFlags;
782
+ }
783
+ });
784
+
785
+ //# sourceMappingURL=harness-fK8CSaU1.cjs.map