@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.
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/apiContracts.cjs +17 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +13 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/brokerHost-BeGtmnK8.mjs +302 -0
- package/dist/brokerHost-BeGtmnK8.mjs.map +1 -0
- package/dist/brokerHost-BzffXeb2.cjs +340 -0
- package/dist/brokerHost-BzffXeb2.cjs.map +1 -0
- package/dist/cockpitHarness.cjs +298 -0
- package/dist/cockpitHarness.cjs.map +1 -0
- package/dist/cockpitHarness.d.mts +26 -0
- package/dist/cockpitHarness.d.mts.map +1 -0
- package/dist/cockpitHarness.mjs +295 -0
- package/dist/cockpitHarness.mjs.map +1 -0
- package/dist/extensionService-BnIZjdJN.cjs +25 -0
- package/dist/extensionService-BnIZjdJN.cjs.map +1 -0
- package/dist/extensionService-DiVpVLn9.mjs +20 -0
- package/dist/extensionService-DiVpVLn9.mjs.map +1 -0
- package/dist/extensions/mcp.cjs +26 -0
- package/dist/extensions/mcp.cjs.map +1 -0
- package/dist/extensions/mcp.d.mts +5 -0
- package/dist/extensions/mcp.d.mts.map +1 -0
- package/dist/extensions/mcp.mjs +21 -0
- package/dist/extensions/mcp.mjs.map +1 -0
- package/dist/extensions/pi.cjs +85 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.mts +6 -0
- package/dist/extensions/pi.d.mts.map +1 -0
- package/dist/extensions/pi.mjs +80 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/extensions/server.cjs +92 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +87 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/harness-CEuhOTr2.mjs +716 -0
- package/dist/harness-CEuhOTr2.mjs.map +1 -0
- package/dist/harness-fK8CSaU1.cjs +785 -0
- package/dist/harness-fK8CSaU1.cjs.map +1 -0
- package/dist/index-CBt-PlVG.d.mts +22 -0
- package/dist/index-CBt-PlVG.d.mts.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/sandboxBridge-B3FRZkC2.cjs +131 -0
- package/dist/sandboxBridge-B3FRZkC2.cjs.map +1 -0
- package/dist/sandboxBridge-D6RAncmy.mjs +72 -0
- package/dist/sandboxBridge-D6RAncmy.mjs.map +1 -0
- package/dist/sandboxHarness-BIvuRMio.d.mts +38 -0
- package/dist/sandboxHarness-BIvuRMio.d.mts.map +1 -0
- package/dist/sandboxHarness.cjs +4 -0
- package/dist/sandboxHarness.d.mts +38 -0
- package/dist/sandboxHarness.d.mts.map +1 -0
- package/dist/sandboxHarness.mjs +2 -0
- package/dist/sandboxResources-5pvXyBW9.mjs +79 -0
- package/dist/sandboxResources-5pvXyBW9.mjs.map +1 -0
- package/dist/sandboxResources-Djxj5l1V.cjs +120 -0
- package/dist/sandboxResources-Djxj5l1V.cjs.map +1 -0
- package/llms.txt +11 -0
- package/package.json +132 -0
- 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
|