@agimon-ai/doompi-sandbox 0.0.1-alpha.91 → 0.0.1-alpha.92
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/dist/{brokerHost-BSb-qpbK.mjs → brokerHost-BeGtmnK8.mjs} +2 -2
- package/dist/{brokerHost-BSb-qpbK.mjs.map → brokerHost-BeGtmnK8.mjs.map} +1 -1
- package/dist/{brokerHost-D-hLs18x.cjs → brokerHost-BzffXeb2.cjs} +3 -3
- package/dist/{brokerHost-D-hLs18x.cjs.map → brokerHost-BzffXeb2.cjs.map} +1 -1
- package/dist/cockpitHarness.cjs +1 -1
- package/dist/cockpitHarness.mjs +1 -1
- package/dist/extensions/pi.cjs +1 -1
- package/dist/extensions/pi.mjs +1 -1
- package/dist/extensions/server.cjs +2 -2
- package/dist/extensions/server.mjs +2 -2
- package/dist/{harness-BWrlo9bu.mjs → harness-CEuhOTr2.mjs} +5 -4
- package/dist/harness-CEuhOTr2.mjs.map +1 -0
- package/dist/{harness-BMkPGehU.cjs → harness-fK8CSaU1.cjs} +6 -5
- package/dist/harness-fK8CSaU1.cjs.map +1 -0
- package/dist/sandboxHarness.cjs +1 -1
- package/dist/sandboxHarness.mjs +1 -1
- package/dist/{sandboxResources-CWIT6svf.mjs → sandboxResources-5pvXyBW9.mjs} +8 -1
- package/dist/{sandboxResources-CWIT6svf.mjs.map → sandboxResources-5pvXyBW9.mjs.map} +1 -1
- package/dist/{sandboxResources-CuJ5sBk1.cjs → sandboxResources-Djxj5l1V.cjs} +8 -1
- package/dist/{sandboxResources-CuJ5sBk1.cjs.map → sandboxResources-Djxj5l1V.cjs.map} +1 -1
- package/package.json +4 -4
- package/dist/harness-BMkPGehU.cjs.map +0 -1
- package/dist/harness-BWrlo9bu.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import https from "node:https";
|
|
7
7
|
//#region src/services/brokerRoutes/index.ts
|
|
@@ -299,4 +299,4 @@ async function stopBroker(server, dispose) {
|
|
|
299
299
|
//#endregion
|
|
300
300
|
export { startBroker as t };
|
|
301
301
|
|
|
302
|
-
//# sourceMappingURL=brokerHost-
|
|
302
|
+
//# sourceMappingURL=brokerHost-BeGtmnK8.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brokerHost-BSb-qpbK.mjs","names":[],"sources":["../src/services/brokerRoutes/index.ts","../src/services/brokerServer/index.ts","../src/services/brokerHost/index.ts"],"sourcesContent":["export interface BrokerRoute {\n /** Pi provider name, which is also the broker's first path segment. */\n provider: string;\n /** Upstream origin plus any base path Pi's own provider data carries. */\n upstream: string;\n /** Host environment variables that may hold the real credential, in priority order. */\n hostKeyEnv: readonly string[];\n}\n\n/**\n * Providers the broker can terminate, mirroring Pi's own provider data.\n *\n * Curated rather than derived: Pi keeps base URLs in package-internal JSON that\n * carries no stability promise. A provider missing here is simply not brokered,\n * and its credential stays on the host.\n */\nconst ROUTES: readonly BrokerRoute[] = [\n { provider: 'anthropic', upstream: 'https://api.anthropic.com', hostKeyEnv: ['ANTHROPIC_API_KEY'] },\n { provider: 'openai', upstream: 'https://api.openai.com/v1', hostKeyEnv: ['OPENAI_API_KEY'] },\n { provider: 'google', upstream: 'https://generativelanguage.googleapis.com/v1beta', hostKeyEnv: ['GEMINI_API_KEY'] },\n { provider: 'groq', upstream: 'https://api.groq.com/openai/v1', hostKeyEnv: ['GROQ_API_KEY'] },\n { provider: 'xai', upstream: 'https://api.x.ai/v1', hostKeyEnv: ['XAI_API_KEY'] },\n { provider: 'openrouter', upstream: 'https://openrouter.ai/api/v1', hostKeyEnv: ['OPENROUTER_API_KEY'] },\n { provider: 'deepseek', upstream: 'https://api.deepseek.com', hostKeyEnv: ['DEEPSEEK_API_KEY'] },\n { provider: 'mistral', upstream: 'https://api.mistral.ai', hostKeyEnv: ['MISTRAL_API_KEY'] },\n { provider: 'together', upstream: 'https://api.together.ai/v1', hostKeyEnv: ['TOGETHER_API_KEY'] },\n { provider: 'cerebras', upstream: 'https://api.cerebras.ai/v1', hostKeyEnv: ['CEREBRAS_API_KEY'] },\n { provider: 'moonshotai', upstream: 'https://api.moonshot.ai/v1', hostKeyEnv: ['MOONSHOT_API_KEY'] },\n { provider: 'zai', upstream: 'https://api.z.ai/api/coding/paas/v4', hostKeyEnv: ['ZAI_API_KEY'] },\n];\n\nexport function brokerRoutes(): readonly BrokerRoute[] {\n return ROUTES;\n}\n\nexport function findBrokerRoute(provider: string): BrokerRoute | undefined {\n return ROUTES.find((route) => route.provider === provider);\n}\n\nexport interface ResolvedCredential {\n route: BrokerRoute;\n /** Environment variable the container would otherwise have received. */\n envName: string;\n value: string;\n}\n\n/** Selects the providers this host can actually broker for a session. */\nexport function resolveBrokeredCredentials(\n environment: Readonly<Record<string, string | undefined>>,\n): ResolvedCredential[] {\n const resolved: ResolvedCredential[] = [];\n for (const route of ROUTES) {\n for (const envName of route.hostKeyEnv) {\n const value = environment[envName]?.trim();\n if (value) {\n resolved.push({ route, envName, value });\n break;\n }\n }\n }\n return resolved;\n}\n","import { timingSafeEqual } from 'node:crypto';\nimport http from 'node:http';\nimport https from 'node:https';\n\nimport type { ResolvedCredential } from '../brokerRoutes';\n\n/** Headers a provider SDK may carry its credential in. */\nconst CREDENTIAL_HEADERS = ['x-api-key', 'authorization', 'x-goog-api-key'] as const;\n/** Query parameter some Google clients use instead of a header. */\nconst CREDENTIAL_QUERY = 'key';\nconst BEARER_PREFIX = 'bearer ';\n/** Connection-scoped headers that must not cross to the upstream request. */\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n 'content-length',\n]);\nconst PATH_PATTERN = /^\\/([^/?]+)(.*)$/;\n\nexport interface BrokerServerOptions {\n /** Providers this session may reach, keyed by Pi provider name. */\n credentials: ReadonlyMap<string, ResolvedCredential>;\n /** Secret the container presents in place of a real credential. */\n token: string;\n /** Receives one line per rejected request, for host-side visibility. */\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to the real https transport. */\n requestUpstream?: typeof https.request;\n}\n\nfunction matchesToken(candidate: string, token: string): boolean {\n const left = Buffer.from(candidate);\n const right = Buffer.from(token);\n return left.length === right.length && timingSafeEqual(left, right);\n}\n\nfunction credentialValue(headerValue: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? headerValue.slice(BEARER_PREFIX.length) : headerValue;\n}\n\nfunction withCredential(headerValue: string, realKey: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? `Bearer ${realKey}` : realKey;\n}\n\n/**\n * Rewrites the request's credential, proving the caller holds the session token.\n *\n * Which header carries the key differs per provider SDK, so every candidate is\n * checked rather than assumed. Returns undefined when nothing presented the\n * token, which is what makes an unauthenticated caller indistinguishable from a\n * misrouted one.\n */\nfunction swapCredential(\n headers: http.IncomingHttpHeaders,\n search: URLSearchParams,\n token: string,\n realKey: string,\n): http.OutgoingHttpHeaders | undefined {\n const forwarded: http.OutgoingHttpHeaders = {};\n let authenticated = false;\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || HOP_BY_HOP.has(name)) continue;\n const single = Array.isArray(value) ? value[0] : value;\n if ((CREDENTIAL_HEADERS as readonly string[]).includes(name) && single !== undefined) {\n if (!matchesToken(credentialValue(single), token)) return undefined;\n forwarded[name] = withCredential(single, realKey);\n authenticated = true;\n continue;\n }\n forwarded[name] = value;\n }\n\n const queryKey = search.get(CREDENTIAL_QUERY);\n if (queryKey !== null) {\n if (!matchesToken(queryKey, token)) return undefined;\n search.set(CREDENTIAL_QUERY, realKey);\n authenticated = true;\n }\n\n return authenticated ? forwarded : undefined;\n}\n\nfunction reject(response: http.ServerResponse, status: number, message: string): void {\n response.writeHead(status, { 'content-type': 'application/json' });\n response.end(JSON.stringify({ error: { type: 'doompi_broker', message } }));\n}\n\n/**\n * Terminates provider calls from a sandboxed session on the host.\n *\n * The container never holds a real credential: it presents the session token,\n * and only a request that proves possession of it is forwarded upstream with\n * the host's key attached. Bodies stream in both directions so token-by-token\n * responses are not buffered.\n */\nexport function createBrokerServer(options: BrokerServerOptions): http.Server {\n const requestUpstream = options.requestUpstream ?? https.request;\n\n return http.createServer((request, response) => {\n const match = PATH_PATTERN.exec(request.url ?? '');\n const provider = match?.[1];\n const credential = provider ? options.credentials.get(provider) : undefined;\n if (!match || !credential) {\n options.onDenied?.(`unroutable provider path ${request.url ?? ''}`);\n reject(response, 404, 'Unknown provider for this sandbox session.');\n return;\n }\n\n const target = new URL(`${credential.route.upstream}${match[2] || ''}`);\n const headers = swapCredential(request.headers, target.searchParams, options.token, credential.value);\n if (!headers) {\n options.onDenied?.(`missing or invalid session token for ${credential.route.provider}`);\n reject(response, 401, 'This sandbox session did not present its broker token.');\n return;\n }\n\n const upstream = requestUpstream(\n target,\n { method: request.method, headers: { ...headers, host: target.host } },\n (upstreamResponse) => {\n response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);\n upstreamResponse.pipe(response);\n },\n );\n upstream.on('error', (error: Error) => {\n options.onDenied?.(`upstream ${credential.route.provider} failed: ${error.message}`);\n if (!response.headersSent) reject(response, 502, 'The provider could not be reached from the host.');\n else response.destroy();\n });\n request.on('aborted', () => upstream.destroy());\n request.pipe(upstream);\n });\n}\n","import { randomBytes } from 'node:crypto';\nimport fs from 'node:fs';\nimport type http from 'node:http';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { BrokerEndpoint } from '../../types/sandboxHarness';\nimport { resolveBrokeredCredentials } from '../brokerRoutes';\nimport { createBrokerServer } from '../brokerServer';\n\nconst SOCKET_DIRECTORY_PREFIX = 'doompi-broker-';\nconst SOCKET_FILE_NAME = 'broker.sock';\nconst OWNER_ONLY_DIRECTORY = 0o700;\nconst TOKEN_BYTES = 32;\nconst LOOPBACK = '127.0.0.1';\nconst LINUX_PLATFORM = 'linux';\nconst EPHEMERAL_PORT = 0;\n\nexport interface RunningBroker {\n /** How a container reaches this broker. */\n endpoint: BrokerEndpoint;\n /** Secret the container presents instead of any real credential. */\n token: string;\n /** Pi provider names the session may reach. */\n providers: string[];\n /** Host environment variables whose value the container receives as the token. */\n withheldEnv: string[];\n stop(): Promise<void>;\n}\n\nexport interface StartBrokerOptions {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to this host's platform. */\n platform?: string;\n /**\n * Fixed directory to hold the socket instead of a fresh temporary one.\n *\n * A reused container keeps the mounts it was created with, so a dev\n * container has to find the socket at the same path on every launch.\n */\n socketDirectory?: string;\n /** Forces the loopback transport even where a socket would work. */\n forceLoopback?: boolean;\n}\n\n/**\n * Starts the host-side provider broker for one sandboxed session.\n *\n * Answers undefined when the host holds no brokerable credential, which keeps\n * a session that authenticates some other way on the unbrokered path instead\n * of failing it.\n */\nexport async function startBroker(options: StartBrokerOptions): Promise<RunningBroker | undefined> {\n const resolved = resolveBrokeredCredentials(options.environment);\n if (resolved.length === 0) return undefined;\n\n const token = randomBytes(TOKEN_BYTES).toString('base64url');\n const credentials = new Map(resolved.map((credential) => [credential.route.provider, credential]));\n const server = createBrokerServer({ credentials, token, onDenied: options.onDenied });\n const platform = options.platform ?? process.platform;\n\n const useSocket = platform === LINUX_PLATFORM && options.forceLoopback !== true;\n const { endpoint, dispose } = useSocket\n ? await listenOnSocket(server, options.socketDirectory)\n : await listenOnLoopback(server);\n\n return {\n endpoint,\n token,\n providers: resolved.map((credential) => credential.route.provider),\n withheldEnv: resolved.map((credential) => credential.envName),\n stop: () => stopBroker(server, dispose),\n };\n}\n\n/**\n * Native Linux shares a kernel with the container, so a bind-mounted socket\n * needs no port and is reachable as an ordinary file.\n */\nasync function listenOnSocket(\n server: http.Server,\n fixedDirectory?: string,\n): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n const socketDirectory = fixedDirectory ?? fs.mkdtempSync(path.join(os.tmpdir(), SOCKET_DIRECTORY_PREFIX));\n fs.mkdirSync(socketDirectory, { recursive: true });\n fs.chmodSync(socketDirectory, OWNER_ONLY_DIRECTORY);\n const socketPath = path.join(socketDirectory, SOCKET_FILE_NAME);\n // A previous session that died without closing leaves the node behind, and\n // bind fails on an existing path.\n fs.rmSync(socketPath, { force: true });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(socketPath, () => resolve());\n });\n return {\n endpoint: { transport: 'unix', socketDirectory },\n // A fixed directory outlives the session that borrowed it.\n dispose: () =>\n fixedDirectory\n ? fs.rmSync(socketPath, { force: true })\n : fs.rmSync(socketDirectory, { recursive: true, force: true }),\n };\n}\n\n/**\n * Everywhere else the container runs in its own virtual machine, which cannot\n * connect to a host unix socket even when the file is shared through: the\n * connect fails with ENOTSUP. Loopback keeps the broker off the network while\n * still being reachable through the engine's host gateway, and the session\n * token is what stops another local process from using it.\n */\nasync function listenOnLoopback(server: http.Server): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(EPHEMERAL_PORT, LOOPBACK, () => resolve());\n });\n const address = server.address();\n if (address === null || typeof address === 'string') throw new Error('The broker did not report a listening port.');\n return { endpoint: { transport: 'tcp', port: address.port }, dispose: () => undefined };\n}\n\nasync function stopBroker(server: http.Server, dispose: () => void): Promise<void> {\n await new Promise<void>((resolve) => {\n server.close(() => resolve());\n // An in-flight provider stream would otherwise hold the listener open past\n // the session that owns it.\n server.closeAllConnections();\n });\n dispose();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"brokerHost-BeGtmnK8.mjs","names":[],"sources":["../src/services/brokerRoutes/index.ts","../src/services/brokerServer/index.ts","../src/services/brokerHost/index.ts"],"sourcesContent":["export interface BrokerRoute {\n /** Pi provider name, which is also the broker's first path segment. */\n provider: string;\n /** Upstream origin plus any base path Pi's own provider data carries. */\n upstream: string;\n /** Host environment variables that may hold the real credential, in priority order. */\n hostKeyEnv: readonly string[];\n}\n\n/**\n * Providers the broker can terminate, mirroring Pi's own provider data.\n *\n * Curated rather than derived: Pi keeps base URLs in package-internal JSON that\n * carries no stability promise. A provider missing here is simply not brokered,\n * and its credential stays on the host.\n */\nconst ROUTES: readonly BrokerRoute[] = [\n { provider: 'anthropic', upstream: 'https://api.anthropic.com', hostKeyEnv: ['ANTHROPIC_API_KEY'] },\n { provider: 'openai', upstream: 'https://api.openai.com/v1', hostKeyEnv: ['OPENAI_API_KEY'] },\n { provider: 'google', upstream: 'https://generativelanguage.googleapis.com/v1beta', hostKeyEnv: ['GEMINI_API_KEY'] },\n { provider: 'groq', upstream: 'https://api.groq.com/openai/v1', hostKeyEnv: ['GROQ_API_KEY'] },\n { provider: 'xai', upstream: 'https://api.x.ai/v1', hostKeyEnv: ['XAI_API_KEY'] },\n { provider: 'openrouter', upstream: 'https://openrouter.ai/api/v1', hostKeyEnv: ['OPENROUTER_API_KEY'] },\n { provider: 'deepseek', upstream: 'https://api.deepseek.com', hostKeyEnv: ['DEEPSEEK_API_KEY'] },\n { provider: 'mistral', upstream: 'https://api.mistral.ai', hostKeyEnv: ['MISTRAL_API_KEY'] },\n { provider: 'together', upstream: 'https://api.together.ai/v1', hostKeyEnv: ['TOGETHER_API_KEY'] },\n { provider: 'cerebras', upstream: 'https://api.cerebras.ai/v1', hostKeyEnv: ['CEREBRAS_API_KEY'] },\n { provider: 'moonshotai', upstream: 'https://api.moonshot.ai/v1', hostKeyEnv: ['MOONSHOT_API_KEY'] },\n { provider: 'zai', upstream: 'https://api.z.ai/api/coding/paas/v4', hostKeyEnv: ['ZAI_API_KEY'] },\n];\n\nexport function brokerRoutes(): readonly BrokerRoute[] {\n return ROUTES;\n}\n\nexport function findBrokerRoute(provider: string): BrokerRoute | undefined {\n return ROUTES.find((route) => route.provider === provider);\n}\n\nexport interface ResolvedCredential {\n route: BrokerRoute;\n /** Environment variable the container would otherwise have received. */\n envName: string;\n value: string;\n}\n\n/** Selects the providers this host can actually broker for a session. */\nexport function resolveBrokeredCredentials(\n environment: Readonly<Record<string, string | undefined>>,\n): ResolvedCredential[] {\n const resolved: ResolvedCredential[] = [];\n for (const route of ROUTES) {\n for (const envName of route.hostKeyEnv) {\n const value = environment[envName]?.trim();\n if (value) {\n resolved.push({ route, envName, value });\n break;\n }\n }\n }\n return resolved;\n}\n","import { timingSafeEqual } from 'node:crypto';\nimport http from 'node:http';\nimport https from 'node:https';\n\nimport type { ResolvedCredential } from '../brokerRoutes';\n\n/** Headers a provider SDK may carry its credential in. */\nconst CREDENTIAL_HEADERS = ['x-api-key', 'authorization', 'x-goog-api-key'] as const;\n/** Query parameter some Google clients use instead of a header. */\nconst CREDENTIAL_QUERY = 'key';\nconst BEARER_PREFIX = 'bearer ';\n/** Connection-scoped headers that must not cross to the upstream request. */\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n 'content-length',\n]);\nconst PATH_PATTERN = /^\\/([^/?]+)(.*)$/;\n\nexport interface BrokerServerOptions {\n /** Providers this session may reach, keyed by Pi provider name. */\n credentials: ReadonlyMap<string, ResolvedCredential>;\n /** Secret the container presents in place of a real credential. */\n token: string;\n /** Receives one line per rejected request, for host-side visibility. */\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to the real https transport. */\n requestUpstream?: typeof https.request;\n}\n\nfunction matchesToken(candidate: string, token: string): boolean {\n const left = Buffer.from(candidate);\n const right = Buffer.from(token);\n return left.length === right.length && timingSafeEqual(left, right);\n}\n\nfunction credentialValue(headerValue: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? headerValue.slice(BEARER_PREFIX.length) : headerValue;\n}\n\nfunction withCredential(headerValue: string, realKey: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? `Bearer ${realKey}` : realKey;\n}\n\n/**\n * Rewrites the request's credential, proving the caller holds the session token.\n *\n * Which header carries the key differs per provider SDK, so every candidate is\n * checked rather than assumed. Returns undefined when nothing presented the\n * token, which is what makes an unauthenticated caller indistinguishable from a\n * misrouted one.\n */\nfunction swapCredential(\n headers: http.IncomingHttpHeaders,\n search: URLSearchParams,\n token: string,\n realKey: string,\n): http.OutgoingHttpHeaders | undefined {\n const forwarded: http.OutgoingHttpHeaders = {};\n let authenticated = false;\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || HOP_BY_HOP.has(name)) continue;\n const single = Array.isArray(value) ? value[0] : value;\n if ((CREDENTIAL_HEADERS as readonly string[]).includes(name) && single !== undefined) {\n if (!matchesToken(credentialValue(single), token)) return undefined;\n forwarded[name] = withCredential(single, realKey);\n authenticated = true;\n continue;\n }\n forwarded[name] = value;\n }\n\n const queryKey = search.get(CREDENTIAL_QUERY);\n if (queryKey !== null) {\n if (!matchesToken(queryKey, token)) return undefined;\n search.set(CREDENTIAL_QUERY, realKey);\n authenticated = true;\n }\n\n return authenticated ? forwarded : undefined;\n}\n\nfunction reject(response: http.ServerResponse, status: number, message: string): void {\n response.writeHead(status, { 'content-type': 'application/json' });\n response.end(JSON.stringify({ error: { type: 'doompi_broker', message } }));\n}\n\n/**\n * Terminates provider calls from a sandboxed session on the host.\n *\n * The container never holds a real credential: it presents the session token,\n * and only a request that proves possession of it is forwarded upstream with\n * the host's key attached. Bodies stream in both directions so token-by-token\n * responses are not buffered.\n */\nexport function createBrokerServer(options: BrokerServerOptions): http.Server {\n const requestUpstream = options.requestUpstream ?? https.request;\n\n return http.createServer((request, response) => {\n const match = PATH_PATTERN.exec(request.url ?? '');\n const provider = match?.[1];\n const credential = provider ? options.credentials.get(provider) : undefined;\n if (!match || !credential) {\n options.onDenied?.(`unroutable provider path ${request.url ?? ''}`);\n reject(response, 404, 'Unknown provider for this sandbox session.');\n return;\n }\n\n const target = new URL(`${credential.route.upstream}${match[2] || ''}`);\n const headers = swapCredential(request.headers, target.searchParams, options.token, credential.value);\n if (!headers) {\n options.onDenied?.(`missing or invalid session token for ${credential.route.provider}`);\n reject(response, 401, 'This sandbox session did not present its broker token.');\n return;\n }\n\n const upstream = requestUpstream(\n target,\n { method: request.method, headers: { ...headers, host: target.host } },\n (upstreamResponse) => {\n response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);\n upstreamResponse.pipe(response);\n },\n );\n upstream.on('error', (error: Error) => {\n options.onDenied?.(`upstream ${credential.route.provider} failed: ${error.message}`);\n if (!response.headersSent) reject(response, 502, 'The provider could not be reached from the host.');\n else response.destroy();\n });\n request.on('aborted', () => upstream.destroy());\n request.pipe(upstream);\n });\n}\n","import { randomBytes } from 'node:crypto';\nimport fs from 'node:fs';\nimport type http from 'node:http';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { BrokerEndpoint } from '../../types/sandboxHarness';\nimport { resolveBrokeredCredentials } from '../brokerRoutes';\nimport { createBrokerServer } from '../brokerServer';\n\nconst SOCKET_DIRECTORY_PREFIX = 'doompi-broker-';\nconst SOCKET_FILE_NAME = 'broker.sock';\nconst OWNER_ONLY_DIRECTORY = 0o700;\nconst TOKEN_BYTES = 32;\nconst LOOPBACK = '127.0.0.1';\nconst LINUX_PLATFORM = 'linux';\nconst EPHEMERAL_PORT = 0;\n\nexport interface RunningBroker {\n /** How a container reaches this broker. */\n endpoint: BrokerEndpoint;\n /** Secret the container presents instead of any real credential. */\n token: string;\n /** Pi provider names the session may reach. */\n providers: string[];\n /** Host environment variables whose value the container receives as the token. */\n withheldEnv: string[];\n stop(): Promise<void>;\n}\n\nexport interface StartBrokerOptions {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to this host's platform. */\n platform?: string;\n /**\n * Fixed directory to hold the socket instead of a fresh temporary one.\n *\n * A reused container keeps the mounts it was created with, so a dev\n * container has to find the socket at the same path on every launch.\n */\n socketDirectory?: string;\n /** Forces the loopback transport even where a socket would work. */\n forceLoopback?: boolean;\n}\n\n/**\n * Starts the host-side provider broker for one sandboxed session.\n *\n * Answers undefined when the host holds no brokerable credential, which keeps\n * a session that authenticates some other way on the unbrokered path instead\n * of failing it.\n */\nexport async function startBroker(options: StartBrokerOptions): Promise<RunningBroker | undefined> {\n const resolved = resolveBrokeredCredentials(options.environment);\n if (resolved.length === 0) return undefined;\n\n const token = randomBytes(TOKEN_BYTES).toString('base64url');\n const credentials = new Map(resolved.map((credential) => [credential.route.provider, credential]));\n const server = createBrokerServer({ credentials, token, onDenied: options.onDenied });\n const platform = options.platform ?? process.platform;\n\n const useSocket = platform === LINUX_PLATFORM && options.forceLoopback !== true;\n const { endpoint, dispose } = useSocket\n ? await listenOnSocket(server, options.socketDirectory)\n : await listenOnLoopback(server);\n\n return {\n endpoint,\n token,\n providers: resolved.map((credential) => credential.route.provider),\n withheldEnv: resolved.map((credential) => credential.envName),\n stop: () => stopBroker(server, dispose),\n };\n}\n\n/**\n * Native Linux shares a kernel with the container, so a bind-mounted socket\n * needs no port and is reachable as an ordinary file.\n */\nasync function listenOnSocket(\n server: http.Server,\n fixedDirectory?: string,\n): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n const socketDirectory = fixedDirectory ?? fs.mkdtempSync(path.join(os.tmpdir(), SOCKET_DIRECTORY_PREFIX));\n fs.mkdirSync(socketDirectory, { recursive: true });\n fs.chmodSync(socketDirectory, OWNER_ONLY_DIRECTORY);\n const socketPath = path.join(socketDirectory, SOCKET_FILE_NAME);\n // A previous session that died without closing leaves the node behind, and\n // bind fails on an existing path.\n fs.rmSync(socketPath, { force: true });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(socketPath, () => resolve());\n });\n return {\n endpoint: { transport: 'unix', socketDirectory },\n // A fixed directory outlives the session that borrowed it.\n dispose: () =>\n fixedDirectory\n ? fs.rmSync(socketPath, { force: true })\n : fs.rmSync(socketDirectory, { recursive: true, force: true }),\n };\n}\n\n/**\n * Everywhere else the container runs in its own virtual machine, which cannot\n * connect to a host unix socket even when the file is shared through: the\n * connect fails with ENOTSUP. Loopback keeps the broker off the network while\n * still being reachable through the engine's host gateway, and the session\n * token is what stops another local process from using it.\n */\nasync function listenOnLoopback(server: http.Server): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(EPHEMERAL_PORT, LOOPBACK, () => resolve());\n });\n const address = server.address();\n if (address === null || typeof address === 'string') throw new Error('The broker did not report a listening port.');\n return { endpoint: { transport: 'tcp', port: address.port }, dispose: () => undefined };\n}\n\nasync function stopBroker(server: http.Server, dispose: () => void): Promise<void> {\n await new Promise<void>((resolve) => {\n server.close(() => resolve());\n // An in-flight provider stream would otherwise hold the listener open past\n // the session that owns it.\n server.closeAllConnections();\n });\n dispose();\n}\n"],"mappings":"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"}
|
|
@@ -20,13 +20,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
20
20
|
enumerable: true
|
|
21
21
|
}) : target, mod));
|
|
22
22
|
//#endregion
|
|
23
|
+
let node_path = require("node:path");
|
|
24
|
+
node_path = __toESM(node_path, 1);
|
|
23
25
|
let node_crypto = require("node:crypto");
|
|
24
26
|
let node_fs = require("node:fs");
|
|
25
27
|
node_fs = __toESM(node_fs, 1);
|
|
26
28
|
let node_os = require("node:os");
|
|
27
29
|
node_os = __toESM(node_os, 1);
|
|
28
|
-
let node_path = require("node:path");
|
|
29
|
-
node_path = __toESM(node_path, 1);
|
|
30
30
|
let node_http = require("node:http");
|
|
31
31
|
node_http = __toESM(node_http, 1);
|
|
32
32
|
let node_https = require("node:https");
|
|
@@ -337,4 +337,4 @@ Object.defineProperty(exports, "startBroker", {
|
|
|
337
337
|
}
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
-
//# sourceMappingURL=brokerHost-
|
|
340
|
+
//# sourceMappingURL=brokerHost-BzffXeb2.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brokerHost-D-hLs18x.cjs","names":["timingSafeEqual","https","http","randomBytes","fs","path","os"],"sources":["../src/services/brokerRoutes/index.ts","../src/services/brokerServer/index.ts","../src/services/brokerHost/index.ts"],"sourcesContent":["export interface BrokerRoute {\n /** Pi provider name, which is also the broker's first path segment. */\n provider: string;\n /** Upstream origin plus any base path Pi's own provider data carries. */\n upstream: string;\n /** Host environment variables that may hold the real credential, in priority order. */\n hostKeyEnv: readonly string[];\n}\n\n/**\n * Providers the broker can terminate, mirroring Pi's own provider data.\n *\n * Curated rather than derived: Pi keeps base URLs in package-internal JSON that\n * carries no stability promise. A provider missing here is simply not brokered,\n * and its credential stays on the host.\n */\nconst ROUTES: readonly BrokerRoute[] = [\n { provider: 'anthropic', upstream: 'https://api.anthropic.com', hostKeyEnv: ['ANTHROPIC_API_KEY'] },\n { provider: 'openai', upstream: 'https://api.openai.com/v1', hostKeyEnv: ['OPENAI_API_KEY'] },\n { provider: 'google', upstream: 'https://generativelanguage.googleapis.com/v1beta', hostKeyEnv: ['GEMINI_API_KEY'] },\n { provider: 'groq', upstream: 'https://api.groq.com/openai/v1', hostKeyEnv: ['GROQ_API_KEY'] },\n { provider: 'xai', upstream: 'https://api.x.ai/v1', hostKeyEnv: ['XAI_API_KEY'] },\n { provider: 'openrouter', upstream: 'https://openrouter.ai/api/v1', hostKeyEnv: ['OPENROUTER_API_KEY'] },\n { provider: 'deepseek', upstream: 'https://api.deepseek.com', hostKeyEnv: ['DEEPSEEK_API_KEY'] },\n { provider: 'mistral', upstream: 'https://api.mistral.ai', hostKeyEnv: ['MISTRAL_API_KEY'] },\n { provider: 'together', upstream: 'https://api.together.ai/v1', hostKeyEnv: ['TOGETHER_API_KEY'] },\n { provider: 'cerebras', upstream: 'https://api.cerebras.ai/v1', hostKeyEnv: ['CEREBRAS_API_KEY'] },\n { provider: 'moonshotai', upstream: 'https://api.moonshot.ai/v1', hostKeyEnv: ['MOONSHOT_API_KEY'] },\n { provider: 'zai', upstream: 'https://api.z.ai/api/coding/paas/v4', hostKeyEnv: ['ZAI_API_KEY'] },\n];\n\nexport function brokerRoutes(): readonly BrokerRoute[] {\n return ROUTES;\n}\n\nexport function findBrokerRoute(provider: string): BrokerRoute | undefined {\n return ROUTES.find((route) => route.provider === provider);\n}\n\nexport interface ResolvedCredential {\n route: BrokerRoute;\n /** Environment variable the container would otherwise have received. */\n envName: string;\n value: string;\n}\n\n/** Selects the providers this host can actually broker for a session. */\nexport function resolveBrokeredCredentials(\n environment: Readonly<Record<string, string | undefined>>,\n): ResolvedCredential[] {\n const resolved: ResolvedCredential[] = [];\n for (const route of ROUTES) {\n for (const envName of route.hostKeyEnv) {\n const value = environment[envName]?.trim();\n if (value) {\n resolved.push({ route, envName, value });\n break;\n }\n }\n }\n return resolved;\n}\n","import { timingSafeEqual } from 'node:crypto';\nimport http from 'node:http';\nimport https from 'node:https';\n\nimport type { ResolvedCredential } from '../brokerRoutes';\n\n/** Headers a provider SDK may carry its credential in. */\nconst CREDENTIAL_HEADERS = ['x-api-key', 'authorization', 'x-goog-api-key'] as const;\n/** Query parameter some Google clients use instead of a header. */\nconst CREDENTIAL_QUERY = 'key';\nconst BEARER_PREFIX = 'bearer ';\n/** Connection-scoped headers that must not cross to the upstream request. */\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n 'content-length',\n]);\nconst PATH_PATTERN = /^\\/([^/?]+)(.*)$/;\n\nexport interface BrokerServerOptions {\n /** Providers this session may reach, keyed by Pi provider name. */\n credentials: ReadonlyMap<string, ResolvedCredential>;\n /** Secret the container presents in place of a real credential. */\n token: string;\n /** Receives one line per rejected request, for host-side visibility. */\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to the real https transport. */\n requestUpstream?: typeof https.request;\n}\n\nfunction matchesToken(candidate: string, token: string): boolean {\n const left = Buffer.from(candidate);\n const right = Buffer.from(token);\n return left.length === right.length && timingSafeEqual(left, right);\n}\n\nfunction credentialValue(headerValue: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? headerValue.slice(BEARER_PREFIX.length) : headerValue;\n}\n\nfunction withCredential(headerValue: string, realKey: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? `Bearer ${realKey}` : realKey;\n}\n\n/**\n * Rewrites the request's credential, proving the caller holds the session token.\n *\n * Which header carries the key differs per provider SDK, so every candidate is\n * checked rather than assumed. Returns undefined when nothing presented the\n * token, which is what makes an unauthenticated caller indistinguishable from a\n * misrouted one.\n */\nfunction swapCredential(\n headers: http.IncomingHttpHeaders,\n search: URLSearchParams,\n token: string,\n realKey: string,\n): http.OutgoingHttpHeaders | undefined {\n const forwarded: http.OutgoingHttpHeaders = {};\n let authenticated = false;\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || HOP_BY_HOP.has(name)) continue;\n const single = Array.isArray(value) ? value[0] : value;\n if ((CREDENTIAL_HEADERS as readonly string[]).includes(name) && single !== undefined) {\n if (!matchesToken(credentialValue(single), token)) return undefined;\n forwarded[name] = withCredential(single, realKey);\n authenticated = true;\n continue;\n }\n forwarded[name] = value;\n }\n\n const queryKey = search.get(CREDENTIAL_QUERY);\n if (queryKey !== null) {\n if (!matchesToken(queryKey, token)) return undefined;\n search.set(CREDENTIAL_QUERY, realKey);\n authenticated = true;\n }\n\n return authenticated ? forwarded : undefined;\n}\n\nfunction reject(response: http.ServerResponse, status: number, message: string): void {\n response.writeHead(status, { 'content-type': 'application/json' });\n response.end(JSON.stringify({ error: { type: 'doompi_broker', message } }));\n}\n\n/**\n * Terminates provider calls from a sandboxed session on the host.\n *\n * The container never holds a real credential: it presents the session token,\n * and only a request that proves possession of it is forwarded upstream with\n * the host's key attached. Bodies stream in both directions so token-by-token\n * responses are not buffered.\n */\nexport function createBrokerServer(options: BrokerServerOptions): http.Server {\n const requestUpstream = options.requestUpstream ?? https.request;\n\n return http.createServer((request, response) => {\n const match = PATH_PATTERN.exec(request.url ?? '');\n const provider = match?.[1];\n const credential = provider ? options.credentials.get(provider) : undefined;\n if (!match || !credential) {\n options.onDenied?.(`unroutable provider path ${request.url ?? ''}`);\n reject(response, 404, 'Unknown provider for this sandbox session.');\n return;\n }\n\n const target = new URL(`${credential.route.upstream}${match[2] || ''}`);\n const headers = swapCredential(request.headers, target.searchParams, options.token, credential.value);\n if (!headers) {\n options.onDenied?.(`missing or invalid session token for ${credential.route.provider}`);\n reject(response, 401, 'This sandbox session did not present its broker token.');\n return;\n }\n\n const upstream = requestUpstream(\n target,\n { method: request.method, headers: { ...headers, host: target.host } },\n (upstreamResponse) => {\n response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);\n upstreamResponse.pipe(response);\n },\n );\n upstream.on('error', (error: Error) => {\n options.onDenied?.(`upstream ${credential.route.provider} failed: ${error.message}`);\n if (!response.headersSent) reject(response, 502, 'The provider could not be reached from the host.');\n else response.destroy();\n });\n request.on('aborted', () => upstream.destroy());\n request.pipe(upstream);\n });\n}\n","import { randomBytes } from 'node:crypto';\nimport fs from 'node:fs';\nimport type http from 'node:http';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { BrokerEndpoint } from '../../types/sandboxHarness';\nimport { resolveBrokeredCredentials } from '../brokerRoutes';\nimport { createBrokerServer } from '../brokerServer';\n\nconst SOCKET_DIRECTORY_PREFIX = 'doompi-broker-';\nconst SOCKET_FILE_NAME = 'broker.sock';\nconst OWNER_ONLY_DIRECTORY = 0o700;\nconst TOKEN_BYTES = 32;\nconst LOOPBACK = '127.0.0.1';\nconst LINUX_PLATFORM = 'linux';\nconst EPHEMERAL_PORT = 0;\n\nexport interface RunningBroker {\n /** How a container reaches this broker. */\n endpoint: BrokerEndpoint;\n /** Secret the container presents instead of any real credential. */\n token: string;\n /** Pi provider names the session may reach. */\n providers: string[];\n /** Host environment variables whose value the container receives as the token. */\n withheldEnv: string[];\n stop(): Promise<void>;\n}\n\nexport interface StartBrokerOptions {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to this host's platform. */\n platform?: string;\n /**\n * Fixed directory to hold the socket instead of a fresh temporary one.\n *\n * A reused container keeps the mounts it was created with, so a dev\n * container has to find the socket at the same path on every launch.\n */\n socketDirectory?: string;\n /** Forces the loopback transport even where a socket would work. */\n forceLoopback?: boolean;\n}\n\n/**\n * Starts the host-side provider broker for one sandboxed session.\n *\n * Answers undefined when the host holds no brokerable credential, which keeps\n * a session that authenticates some other way on the unbrokered path instead\n * of failing it.\n */\nexport async function startBroker(options: StartBrokerOptions): Promise<RunningBroker | undefined> {\n const resolved = resolveBrokeredCredentials(options.environment);\n if (resolved.length === 0) return undefined;\n\n const token = randomBytes(TOKEN_BYTES).toString('base64url');\n const credentials = new Map(resolved.map((credential) => [credential.route.provider, credential]));\n const server = createBrokerServer({ credentials, token, onDenied: options.onDenied });\n const platform = options.platform ?? process.platform;\n\n const useSocket = platform === LINUX_PLATFORM && options.forceLoopback !== true;\n const { endpoint, dispose } = useSocket\n ? await listenOnSocket(server, options.socketDirectory)\n : await listenOnLoopback(server);\n\n return {\n endpoint,\n token,\n providers: resolved.map((credential) => credential.route.provider),\n withheldEnv: resolved.map((credential) => credential.envName),\n stop: () => stopBroker(server, dispose),\n };\n}\n\n/**\n * Native Linux shares a kernel with the container, so a bind-mounted socket\n * needs no port and is reachable as an ordinary file.\n */\nasync function listenOnSocket(\n server: http.Server,\n fixedDirectory?: string,\n): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n const socketDirectory = fixedDirectory ?? fs.mkdtempSync(path.join(os.tmpdir(), SOCKET_DIRECTORY_PREFIX));\n fs.mkdirSync(socketDirectory, { recursive: true });\n fs.chmodSync(socketDirectory, OWNER_ONLY_DIRECTORY);\n const socketPath = path.join(socketDirectory, SOCKET_FILE_NAME);\n // A previous session that died without closing leaves the node behind, and\n // bind fails on an existing path.\n fs.rmSync(socketPath, { force: true });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(socketPath, () => resolve());\n });\n return {\n endpoint: { transport: 'unix', socketDirectory },\n // A fixed directory outlives the session that borrowed it.\n dispose: () =>\n fixedDirectory\n ? fs.rmSync(socketPath, { force: true })\n : fs.rmSync(socketDirectory, { recursive: true, force: true }),\n };\n}\n\n/**\n * Everywhere else the container runs in its own virtual machine, which cannot\n * connect to a host unix socket even when the file is shared through: the\n * connect fails with ENOTSUP. Loopback keeps the broker off the network while\n * still being reachable through the engine's host gateway, and the session\n * token is what stops another local process from using it.\n */\nasync function listenOnLoopback(server: http.Server): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(EPHEMERAL_PORT, LOOPBACK, () => resolve());\n });\n const address = server.address();\n if (address === null || typeof address === 'string') throw new Error('The broker did not report a listening port.');\n return { endpoint: { transport: 'tcp', port: address.port }, dispose: () => undefined };\n}\n\nasync function stopBroker(server: http.Server, dispose: () => void): Promise<void> {\n await new Promise<void>((resolve) => {\n server.close(() => resolve());\n // An in-flight provider stream would otherwise hold the listener open past\n // the session that owns it.\n server.closeAllConnections();\n });\n dispose();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA,MAAM,SAAiC;CACrC;EAAE,UAAU;EAAa,UAAU;EAA6B,YAAY,CAAC,mBAAmB;CAAE;CAClG;EAAE,UAAU;EAAU,UAAU;EAA6B,YAAY,CAAC,gBAAgB;CAAE;CAC5F;EAAE,UAAU;EAAU,UAAU;EAAoD,YAAY,CAAC,gBAAgB;CAAE;CACnH;EAAE,UAAU;EAAQ,UAAU;EAAkC,YAAY,CAAC,cAAc;CAAE;CAC7F;EAAE,UAAU;EAAO,UAAU;EAAuB,YAAY,CAAC,aAAa;CAAE;CAChF;EAAE,UAAU;EAAc,UAAU;EAAgC,YAAY,CAAC,oBAAoB;CAAE;CACvG;EAAE,UAAU;EAAY,UAAU;EAA4B,YAAY,CAAC,kBAAkB;CAAE;CAC/F;EAAE,UAAU;EAAW,UAAU;EAA0B,YAAY,CAAC,iBAAiB;CAAE;CAC3F;EAAE,UAAU;EAAY,UAAU;EAA8B,YAAY,CAAC,kBAAkB;CAAE;CACjG;EAAE,UAAU;EAAY,UAAU;EAA8B,YAAY,CAAC,kBAAkB;CAAE;CACjG;EAAE,UAAU;EAAc,UAAU;EAA8B,YAAY,CAAC,kBAAkB;CAAE;CACnG;EAAE,UAAU;EAAO,UAAU;EAAuC,YAAY,CAAC,aAAa;CAAE;AAClG;;AAkBA,SAAgB,2BACd,aACsB;CACtB,MAAM,WAAiC,CAAC;CACxC,KAAK,MAAM,SAAS,QAClB,KAAK,MAAM,WAAW,MAAM,YAAY;EACtC,MAAM,QAAQ,YAAY,QAAQ,EAAE,KAAK;EACzC,IAAI,OAAO;GACT,SAAS,KAAK;IAAE;IAAO;IAAS;GAAM,CAAC;GACvC;EACF;CACF;CAEF,OAAO;AACT;;;;ACtDA,MAAM,qBAAqB;CAAC;CAAa;CAAiB;AAAgB;;AAE1E,MAAM,mBAAmB;AACzB,MAAM,gBAAgB;;AAEtB,MAAM,6BAAa,IAAI,IAAI;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,eAAe;AAarB,SAAS,aAAa,WAAmB,OAAwB;CAC/D,MAAM,OAAO,OAAO,KAAK,SAAS;CAClC,MAAM,QAAQ,OAAO,KAAK,KAAK;CAC/B,OAAO,KAAK,WAAW,MAAM,WAAA,GAAUA,YAAAA,gBAAAA,CAAgB,MAAM,KAAK;AACpE;AAEA,SAAS,gBAAgB,aAA6B;CACpD,OAAO,YAAY,YAAY,CAAC,CAAC,WAAW,aAAa,IAAI,YAAY,MAAM,CAAoB,IAAI;AACzG;AAEA,SAAS,eAAe,aAAqB,SAAyB;CACpE,OAAO,YAAY,YAAY,CAAC,CAAC,WAAW,aAAa,IAAI,UAAU,YAAY;AACrF;;;;;;;;;AAUA,SAAS,eACP,SACA,QACA,OACA,SACsC;CACtC,MAAM,YAAsC,CAAC;CAC7C,IAAI,gBAAgB;CAEpB,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG;EACnD,IAAI,UAAU,KAAA,KAAa,WAAW,IAAI,IAAI,GAAG;EACjD,MAAM,SAAS,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;EACjD,IAAK,mBAAyC,SAAS,IAAI,KAAK,WAAW,KAAA,GAAW;GACpF,IAAI,CAAC,aAAa,gBAAgB,MAAM,GAAG,KAAK,GAAG,OAAO,KAAA;GAC1D,UAAU,QAAQ,eAAe,QAAQ,OAAO;GAChD,gBAAgB;GAChB;EACF;EACA,UAAU,QAAQ;CACpB;CAEA,MAAM,WAAW,OAAO,IAAI,gBAAgB;CAC5C,IAAI,aAAa,MAAM;EACrB,IAAI,CAAC,aAAa,UAAU,KAAK,GAAG,OAAO,KAAA;EAC3C,OAAO,IAAI,kBAAkB,OAAO;EACpC,gBAAgB;CAClB;CAEA,OAAO,gBAAgB,YAAY,KAAA;AACrC;AAEA,SAAS,OAAO,UAA+B,QAAgB,SAAuB;CACpF,SAAS,UAAU,QAAQ,EAAE,gBAAgB,mBAAmB,CAAC;CACjE,SAAS,IAAI,KAAK,UAAU,EAAE,OAAO;EAAE,MAAM;EAAiB;CAAQ,EAAE,CAAC,CAAC;AAC5E;;;;;;;;;AAUA,SAAgB,mBAAmB,SAA2C;CAC5E,MAAM,kBAAkB,QAAQ,mBAAmBC,WAAAA,QAAM;CAEzD,OAAOC,UAAAA,QAAK,cAAc,SAAS,aAAa;EAC9C,MAAM,QAAQ,aAAa,KAAK,QAAQ,OAAO,EAAE;EACjD,MAAM,WAAW,QAAQ;EACzB,MAAM,aAAa,WAAW,QAAQ,YAAY,IAAI,QAAQ,IAAI,KAAA;EAClE,IAAI,CAAC,SAAS,CAAC,YAAY;GACzB,QAAQ,WAAW,4BAA4B,QAAQ,OAAO,IAAI;GAClE,OAAO,UAAU,KAAK,4CAA4C;GAClE;EACF;EAEA,MAAM,SAAS,IAAI,IAAI,GAAG,WAAW,MAAM,WAAW,MAAM,MAAM,IAAI;EACtE,MAAM,UAAU,eAAe,QAAQ,SAAS,OAAO,cAAc,QAAQ,OAAO,WAAW,KAAK;EACpG,IAAI,CAAC,SAAS;GACZ,QAAQ,WAAW,wCAAwC,WAAW,MAAM,UAAU;GACtF,OAAO,UAAU,KAAK,wDAAwD;GAC9E;EACF;EAEA,MAAM,WAAW,gBACf,QACA;GAAE,QAAQ,QAAQ;GAAQ,SAAS;IAAE,GAAG;IAAS,MAAM,OAAO;GAAK;EAAE,IACpE,qBAAqB;GACpB,SAAS,UAAU,iBAAiB,cAAc,KAAK,iBAAiB,OAAO;GAC/E,iBAAiB,KAAK,QAAQ;EAChC,CACF;EACA,SAAS,GAAG,UAAU,UAAiB;GACrC,QAAQ,WAAW,YAAY,WAAW,MAAM,SAAS,WAAW,MAAM,SAAS;GACnF,IAAI,CAAC,SAAS,aAAa,OAAO,UAAU,KAAK,kDAAkD;QAC9F,SAAS,QAAQ;EACxB,CAAC;EACD,QAAQ,GAAG,iBAAiB,SAAS,QAAQ,CAAC;EAC9C,QAAQ,KAAK,QAAQ;CACvB,CAAC;AACH;;;AClIA,MAAM,0BAA0B;AAChC,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;AAC7B,MAAM,cAAc;AACpB,MAAM,WAAW;AACjB,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;;;;;;;;AAqCvB,eAAsB,YAAY,SAAiE;CACjG,MAAM,WAAW,2BAA2B,QAAQ,WAAW;CAC/D,IAAI,SAAS,WAAW,GAAG,OAAO,KAAA;CAElC,MAAM,SAAA,GAAQC,YAAAA,YAAAA,CAAY,WAAW,CAAC,CAAC,SAAS,WAAW;CAE3D,MAAM,SAAS,mBAAmB;EAAE,iBADZ,IAAI,SAAS,KAAK,eAAe,CAAC,WAAW,MAAM,UAAU,UAAU,CAAC,CAC5D;EAAa;EAAO,UAAU,QAAQ;CAAS,CAAC;CAIpF,MAAM,EAAE,UAAU,aAHD,QAAQ,YAAY,QAAQ,cAEd,kBAAkB,QAAQ,kBAAkB,OAEvE,MAAM,eAAe,QAAQ,QAAQ,eAAe,IACpD,MAAM,iBAAiB,MAAM;CAEjC,OAAO;EACL;EACA;EACA,WAAW,SAAS,KAAK,eAAe,WAAW,MAAM,QAAQ;EACjE,aAAa,SAAS,KAAK,eAAe,WAAW,OAAO;EAC5D,YAAY,WAAW,QAAQ,OAAO;CACxC;AACF;;;;;AAMA,eAAe,eACb,QACA,gBAC4D;CAC5D,MAAM,kBAAkB,kBAAkBC,QAAAA,QAAG,YAAYC,UAAAA,QAAK,KAAKC,QAAAA,QAAG,OAAO,GAAG,uBAAuB,CAAC;CACxG,QAAA,QAAG,UAAU,iBAAiB,EAAE,WAAW,KAAK,CAAC;CACjD,QAAA,QAAG,UAAU,iBAAiB,oBAAoB;CAClD,MAAM,aAAaD,UAAAA,QAAK,KAAK,iBAAiB,gBAAgB;CAG9D,QAAA,QAAG,OAAO,YAAY,EAAE,OAAO,KAAK,CAAC;CACrC,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,kBAAkB,QAAQ,CAAC;CAC3C,CAAC;CACD,OAAO;EACL,UAAU;GAAE,WAAW;GAAQ;EAAgB;EAE/C,eACE,iBACID,QAAAA,QAAG,OAAO,YAAY,EAAE,OAAO,KAAK,CAAC,IACrCA,QAAAA,QAAG,OAAO,iBAAiB;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACnE;AACF;;;;;;;;AASA,eAAe,iBAAiB,QAAiF;CAC/G,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,gBAAgB,gBAAgB,QAAQ,CAAC;CACzD,CAAC;CACD,MAAM,UAAU,OAAO,QAAQ;CAC/B,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU,MAAM,IAAI,MAAM,6CAA6C;CAClH,OAAO;EAAE,UAAU;GAAE,WAAW;GAAO,MAAM,QAAQ;EAAK;EAAG,eAAe,KAAA;CAAU;AACxF;AAEA,eAAe,WAAW,QAAqB,SAAoC;CACjF,MAAM,IAAI,SAAe,YAAY;EACnC,OAAO,YAAY,QAAQ,CAAC;EAG5B,OAAO,oBAAoB;CAC7B,CAAC;CACD,QAAQ;AACV"}
|
|
1
|
+
{"version":3,"file":"brokerHost-BzffXeb2.cjs","names":["timingSafeEqual","https","http","randomBytes","fs","path","os"],"sources":["../src/services/brokerRoutes/index.ts","../src/services/brokerServer/index.ts","../src/services/brokerHost/index.ts"],"sourcesContent":["export interface BrokerRoute {\n /** Pi provider name, which is also the broker's first path segment. */\n provider: string;\n /** Upstream origin plus any base path Pi's own provider data carries. */\n upstream: string;\n /** Host environment variables that may hold the real credential, in priority order. */\n hostKeyEnv: readonly string[];\n}\n\n/**\n * Providers the broker can terminate, mirroring Pi's own provider data.\n *\n * Curated rather than derived: Pi keeps base URLs in package-internal JSON that\n * carries no stability promise. A provider missing here is simply not brokered,\n * and its credential stays on the host.\n */\nconst ROUTES: readonly BrokerRoute[] = [\n { provider: 'anthropic', upstream: 'https://api.anthropic.com', hostKeyEnv: ['ANTHROPIC_API_KEY'] },\n { provider: 'openai', upstream: 'https://api.openai.com/v1', hostKeyEnv: ['OPENAI_API_KEY'] },\n { provider: 'google', upstream: 'https://generativelanguage.googleapis.com/v1beta', hostKeyEnv: ['GEMINI_API_KEY'] },\n { provider: 'groq', upstream: 'https://api.groq.com/openai/v1', hostKeyEnv: ['GROQ_API_KEY'] },\n { provider: 'xai', upstream: 'https://api.x.ai/v1', hostKeyEnv: ['XAI_API_KEY'] },\n { provider: 'openrouter', upstream: 'https://openrouter.ai/api/v1', hostKeyEnv: ['OPENROUTER_API_KEY'] },\n { provider: 'deepseek', upstream: 'https://api.deepseek.com', hostKeyEnv: ['DEEPSEEK_API_KEY'] },\n { provider: 'mistral', upstream: 'https://api.mistral.ai', hostKeyEnv: ['MISTRAL_API_KEY'] },\n { provider: 'together', upstream: 'https://api.together.ai/v1', hostKeyEnv: ['TOGETHER_API_KEY'] },\n { provider: 'cerebras', upstream: 'https://api.cerebras.ai/v1', hostKeyEnv: ['CEREBRAS_API_KEY'] },\n { provider: 'moonshotai', upstream: 'https://api.moonshot.ai/v1', hostKeyEnv: ['MOONSHOT_API_KEY'] },\n { provider: 'zai', upstream: 'https://api.z.ai/api/coding/paas/v4', hostKeyEnv: ['ZAI_API_KEY'] },\n];\n\nexport function brokerRoutes(): readonly BrokerRoute[] {\n return ROUTES;\n}\n\nexport function findBrokerRoute(provider: string): BrokerRoute | undefined {\n return ROUTES.find((route) => route.provider === provider);\n}\n\nexport interface ResolvedCredential {\n route: BrokerRoute;\n /** Environment variable the container would otherwise have received. */\n envName: string;\n value: string;\n}\n\n/** Selects the providers this host can actually broker for a session. */\nexport function resolveBrokeredCredentials(\n environment: Readonly<Record<string, string | undefined>>,\n): ResolvedCredential[] {\n const resolved: ResolvedCredential[] = [];\n for (const route of ROUTES) {\n for (const envName of route.hostKeyEnv) {\n const value = environment[envName]?.trim();\n if (value) {\n resolved.push({ route, envName, value });\n break;\n }\n }\n }\n return resolved;\n}\n","import { timingSafeEqual } from 'node:crypto';\nimport http from 'node:http';\nimport https from 'node:https';\n\nimport type { ResolvedCredential } from '../brokerRoutes';\n\n/** Headers a provider SDK may carry its credential in. */\nconst CREDENTIAL_HEADERS = ['x-api-key', 'authorization', 'x-goog-api-key'] as const;\n/** Query parameter some Google clients use instead of a header. */\nconst CREDENTIAL_QUERY = 'key';\nconst BEARER_PREFIX = 'bearer ';\n/** Connection-scoped headers that must not cross to the upstream request. */\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n 'content-length',\n]);\nconst PATH_PATTERN = /^\\/([^/?]+)(.*)$/;\n\nexport interface BrokerServerOptions {\n /** Providers this session may reach, keyed by Pi provider name. */\n credentials: ReadonlyMap<string, ResolvedCredential>;\n /** Secret the container presents in place of a real credential. */\n token: string;\n /** Receives one line per rejected request, for host-side visibility. */\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to the real https transport. */\n requestUpstream?: typeof https.request;\n}\n\nfunction matchesToken(candidate: string, token: string): boolean {\n const left = Buffer.from(candidate);\n const right = Buffer.from(token);\n return left.length === right.length && timingSafeEqual(left, right);\n}\n\nfunction credentialValue(headerValue: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? headerValue.slice(BEARER_PREFIX.length) : headerValue;\n}\n\nfunction withCredential(headerValue: string, realKey: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? `Bearer ${realKey}` : realKey;\n}\n\n/**\n * Rewrites the request's credential, proving the caller holds the session token.\n *\n * Which header carries the key differs per provider SDK, so every candidate is\n * checked rather than assumed. Returns undefined when nothing presented the\n * token, which is what makes an unauthenticated caller indistinguishable from a\n * misrouted one.\n */\nfunction swapCredential(\n headers: http.IncomingHttpHeaders,\n search: URLSearchParams,\n token: string,\n realKey: string,\n): http.OutgoingHttpHeaders | undefined {\n const forwarded: http.OutgoingHttpHeaders = {};\n let authenticated = false;\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || HOP_BY_HOP.has(name)) continue;\n const single = Array.isArray(value) ? value[0] : value;\n if ((CREDENTIAL_HEADERS as readonly string[]).includes(name) && single !== undefined) {\n if (!matchesToken(credentialValue(single), token)) return undefined;\n forwarded[name] = withCredential(single, realKey);\n authenticated = true;\n continue;\n }\n forwarded[name] = value;\n }\n\n const queryKey = search.get(CREDENTIAL_QUERY);\n if (queryKey !== null) {\n if (!matchesToken(queryKey, token)) return undefined;\n search.set(CREDENTIAL_QUERY, realKey);\n authenticated = true;\n }\n\n return authenticated ? forwarded : undefined;\n}\n\nfunction reject(response: http.ServerResponse, status: number, message: string): void {\n response.writeHead(status, { 'content-type': 'application/json' });\n response.end(JSON.stringify({ error: { type: 'doompi_broker', message } }));\n}\n\n/**\n * Terminates provider calls from a sandboxed session on the host.\n *\n * The container never holds a real credential: it presents the session token,\n * and only a request that proves possession of it is forwarded upstream with\n * the host's key attached. Bodies stream in both directions so token-by-token\n * responses are not buffered.\n */\nexport function createBrokerServer(options: BrokerServerOptions): http.Server {\n const requestUpstream = options.requestUpstream ?? https.request;\n\n return http.createServer((request, response) => {\n const match = PATH_PATTERN.exec(request.url ?? '');\n const provider = match?.[1];\n const credential = provider ? options.credentials.get(provider) : undefined;\n if (!match || !credential) {\n options.onDenied?.(`unroutable provider path ${request.url ?? ''}`);\n reject(response, 404, 'Unknown provider for this sandbox session.');\n return;\n }\n\n const target = new URL(`${credential.route.upstream}${match[2] || ''}`);\n const headers = swapCredential(request.headers, target.searchParams, options.token, credential.value);\n if (!headers) {\n options.onDenied?.(`missing or invalid session token for ${credential.route.provider}`);\n reject(response, 401, 'This sandbox session did not present its broker token.');\n return;\n }\n\n const upstream = requestUpstream(\n target,\n { method: request.method, headers: { ...headers, host: target.host } },\n (upstreamResponse) => {\n response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);\n upstreamResponse.pipe(response);\n },\n );\n upstream.on('error', (error: Error) => {\n options.onDenied?.(`upstream ${credential.route.provider} failed: ${error.message}`);\n if (!response.headersSent) reject(response, 502, 'The provider could not be reached from the host.');\n else response.destroy();\n });\n request.on('aborted', () => upstream.destroy());\n request.pipe(upstream);\n });\n}\n","import { randomBytes } from 'node:crypto';\nimport fs from 'node:fs';\nimport type http from 'node:http';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { BrokerEndpoint } from '../../types/sandboxHarness';\nimport { resolveBrokeredCredentials } from '../brokerRoutes';\nimport { createBrokerServer } from '../brokerServer';\n\nconst SOCKET_DIRECTORY_PREFIX = 'doompi-broker-';\nconst SOCKET_FILE_NAME = 'broker.sock';\nconst OWNER_ONLY_DIRECTORY = 0o700;\nconst TOKEN_BYTES = 32;\nconst LOOPBACK = '127.0.0.1';\nconst LINUX_PLATFORM = 'linux';\nconst EPHEMERAL_PORT = 0;\n\nexport interface RunningBroker {\n /** How a container reaches this broker. */\n endpoint: BrokerEndpoint;\n /** Secret the container presents instead of any real credential. */\n token: string;\n /** Pi provider names the session may reach. */\n providers: string[];\n /** Host environment variables whose value the container receives as the token. */\n withheldEnv: string[];\n stop(): Promise<void>;\n}\n\nexport interface StartBrokerOptions {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to this host's platform. */\n platform?: string;\n /**\n * Fixed directory to hold the socket instead of a fresh temporary one.\n *\n * A reused container keeps the mounts it was created with, so a dev\n * container has to find the socket at the same path on every launch.\n */\n socketDirectory?: string;\n /** Forces the loopback transport even where a socket would work. */\n forceLoopback?: boolean;\n}\n\n/**\n * Starts the host-side provider broker for one sandboxed session.\n *\n * Answers undefined when the host holds no brokerable credential, which keeps\n * a session that authenticates some other way on the unbrokered path instead\n * of failing it.\n */\nexport async function startBroker(options: StartBrokerOptions): Promise<RunningBroker | undefined> {\n const resolved = resolveBrokeredCredentials(options.environment);\n if (resolved.length === 0) return undefined;\n\n const token = randomBytes(TOKEN_BYTES).toString('base64url');\n const credentials = new Map(resolved.map((credential) => [credential.route.provider, credential]));\n const server = createBrokerServer({ credentials, token, onDenied: options.onDenied });\n const platform = options.platform ?? process.platform;\n\n const useSocket = platform === LINUX_PLATFORM && options.forceLoopback !== true;\n const { endpoint, dispose } = useSocket\n ? await listenOnSocket(server, options.socketDirectory)\n : await listenOnLoopback(server);\n\n return {\n endpoint,\n token,\n providers: resolved.map((credential) => credential.route.provider),\n withheldEnv: resolved.map((credential) => credential.envName),\n stop: () => stopBroker(server, dispose),\n };\n}\n\n/**\n * Native Linux shares a kernel with the container, so a bind-mounted socket\n * needs no port and is reachable as an ordinary file.\n */\nasync function listenOnSocket(\n server: http.Server,\n fixedDirectory?: string,\n): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n const socketDirectory = fixedDirectory ?? fs.mkdtempSync(path.join(os.tmpdir(), SOCKET_DIRECTORY_PREFIX));\n fs.mkdirSync(socketDirectory, { recursive: true });\n fs.chmodSync(socketDirectory, OWNER_ONLY_DIRECTORY);\n const socketPath = path.join(socketDirectory, SOCKET_FILE_NAME);\n // A previous session that died without closing leaves the node behind, and\n // bind fails on an existing path.\n fs.rmSync(socketPath, { force: true });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(socketPath, () => resolve());\n });\n return {\n endpoint: { transport: 'unix', socketDirectory },\n // A fixed directory outlives the session that borrowed it.\n dispose: () =>\n fixedDirectory\n ? fs.rmSync(socketPath, { force: true })\n : fs.rmSync(socketDirectory, { recursive: true, force: true }),\n };\n}\n\n/**\n * Everywhere else the container runs in its own virtual machine, which cannot\n * connect to a host unix socket even when the file is shared through: the\n * connect fails with ENOTSUP. Loopback keeps the broker off the network while\n * still being reachable through the engine's host gateway, and the session\n * token is what stops another local process from using it.\n */\nasync function listenOnLoopback(server: http.Server): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(EPHEMERAL_PORT, LOOPBACK, () => resolve());\n });\n const address = server.address();\n if (address === null || typeof address === 'string') throw new Error('The broker did not report a listening port.');\n return { endpoint: { transport: 'tcp', port: address.port }, dispose: () => undefined };\n}\n\nasync function stopBroker(server: http.Server, dispose: () => void): Promise<void> {\n await new Promise<void>((resolve) => {\n server.close(() => resolve());\n // An in-flight provider stream would otherwise hold the listener open past\n // the session that owns it.\n server.closeAllConnections();\n });\n dispose();\n}\n"],"mappings":"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"}
|
package/dist/cockpitHarness.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_harness = require("./harness-
|
|
2
|
+
const require_harness = require("./harness-fK8CSaU1.cjs");
|
|
3
3
|
let node_child_process = require("node:child_process");
|
|
4
4
|
let _agimon_ai_doompi_core_sandboxHarness = require("@agimon-ai/doompi-core/sandboxHarness");
|
|
5
5
|
//#region src/services/cockpitPlan/index.ts
|
package/dist/cockpitHarness.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as cockpitImageTag, c as parseRunFlags, d as DEVCONTAINER_DISABLED_ENV, l as SpawnEngineProcessRunner, n as detectEngine, o as cockpitDockerfile, r as ensureImage, s as assertRunFlags, u as containerEnvironment } from "./harness-
|
|
1
|
+
import { a as cockpitImageTag, c as parseRunFlags, d as DEVCONTAINER_DISABLED_ENV, l as SpawnEngineProcessRunner, n as detectEngine, o as cockpitDockerfile, r as ensureImage, s as assertRunFlags, u as containerEnvironment } from "./harness-CEuhOTr2.mjs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { DOOMPI_SANDBOX_ENV } from "@agimon-ai/doompi-core/sandboxHarness";
|
|
4
4
|
//#region src/services/cockpitPlan/index.ts
|
package/dist/extensions/pi.cjs
CHANGED
|
@@ -4,7 +4,7 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
const require_sandboxBridge = require("../sandboxBridge-B3FRZkC2.cjs");
|
|
6
6
|
const require_extensionService = require("../extensionService-BnIZjdJN.cjs");
|
|
7
|
-
const require_sandboxResources = require("../sandboxResources-
|
|
7
|
+
const require_sandboxResources = require("../sandboxResources-Djxj5l1V.cjs");
|
|
8
8
|
let _agimon_ai_doompi_core_piExtension = require("@agimon-ai/doompi-core/piExtension");
|
|
9
9
|
let _agimon_ai_doompi_core_extensionFile = require("@agimon-ai/doompi-core/extensionFile");
|
|
10
10
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/piRoot.ts
|
package/dist/extensions/pi.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as BROKER_CONTAINER_PORT, r as BROKER_ADDRESS_ENV, s as BROKER_PROVIDERS_ENV } from "../sandboxBridge-D6RAncmy.mjs";
|
|
2
2
|
import { t as DefaultSandboxExtensionService } from "../extensionService-DiVpVLn9.mjs";
|
|
3
|
-
import { i as createSandboxCliCommand, n as createSandboxHelpResource } from "../sandboxResources-
|
|
3
|
+
import { i as createSandboxCliCommand, n as createSandboxHelpResource } from "../sandboxResources-5pvXyBW9.mjs";
|
|
4
4
|
import { definePiExtension } from "@agimon-ai/doompi-core/piExtension";
|
|
5
5
|
import { composeRootHooks, defineCliCommand, defineProvider, defineResource, defineRoot, defineRoutedContribution } from "@agimon-ai/doompi-core/extensionFile";
|
|
6
6
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/piRoot.ts
|
|
@@ -2,9 +2,9 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_brokerHost = require("../brokerHost-
|
|
5
|
+
const require_brokerHost = require("../brokerHost-BzffXeb2.cjs");
|
|
6
6
|
const require_extensionService = require("../extensionService-BnIZjdJN.cjs");
|
|
7
|
-
const require_sandboxResources = require("../sandboxResources-
|
|
7
|
+
const require_sandboxResources = require("../sandboxResources-Djxj5l1V.cjs");
|
|
8
8
|
let _agimon_ai_doompi_core_extensionFile = require("@agimon-ai/doompi-core/extensionFile");
|
|
9
9
|
let _agimon_ai_doompi_core_serverFacet = require("@agimon-ai/doompi-core/serverFacet");
|
|
10
10
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as startBroker } from "../brokerHost-
|
|
1
|
+
import { t as startBroker } from "../brokerHost-BeGtmnK8.mjs";
|
|
2
2
|
import { t as DefaultSandboxExtensionService } from "../extensionService-DiVpVLn9.mjs";
|
|
3
|
-
import { a as createSandboxServerCommands, r as createSandboxSkillResource, s as BROKER_STATUS_SOURCE, t as createSandboxContextResource } from "../sandboxResources-
|
|
3
|
+
import { a as createSandboxServerCommands, r as createSandboxSkillResource, s as BROKER_STATUS_SOURCE, t as createSandboxContextResource } from "../sandboxResources-5pvXyBW9.mjs";
|
|
4
4
|
import { composeRootHooks, defineResource, defineRoot, defineRoutedContribution } from "@agimon-ai/doompi-core/extensionFile";
|
|
5
5
|
import { defineServerPlugin } from "@agimon-ai/doompi-core/serverFacet";
|
|
6
6
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { a as BROKER_HOST_GATEWAY, c as BROKER_SOCKET_CONTAINER_PATH, i as BROKER_CONTAINER_PORT, l as BROKER_SOCKET_ENV, n as BRIDGE_FILE_NAME, o as BROKER_PORT_ENV, r as BROKER_ADDRESS_ENV, s as BROKER_PROVIDERS_ENV, t as BRIDGE_CONTAINER_PATH, u as sandboxBridgeSource } from "./sandboxBridge-D6RAncmy.mjs";
|
|
2
|
-
import { t as startBroker } from "./brokerHost-
|
|
2
|
+
import { t as startBroker } from "./brokerHost-BeGtmnK8.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { DOOMPI_SANDBOX_ENV } from "@agimon-ai/doompi-core/sandboxHarness";
|
|
6
|
+
import path from "node:path";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
import os from "node:os";
|
|
9
|
-
import path from "node:path";
|
|
10
10
|
import net from "node:net";
|
|
11
11
|
//#region src/services/devcontainer/index.ts
|
|
12
12
|
/**
|
|
@@ -63,7 +63,8 @@ function parseDevcontainerUp(stdout) {
|
|
|
63
63
|
* inside and would fail the exec before the agent ever starts.
|
|
64
64
|
*/
|
|
65
65
|
function containerWorkspacePath(repoRoot, cwd, remoteWorkspaceFolder) {
|
|
66
|
-
const
|
|
66
|
+
const inside = path.relative(repoRoot, cwd);
|
|
67
|
+
const relative = inside === "" || inside.startsWith("..") || path.isAbsolute(inside) ? "" : inside;
|
|
67
68
|
return relative ? `${remoteWorkspaceFolder}/${relative.split("\\").join("/")}` : remoteWorkspaceFolder;
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
@@ -712,4 +713,4 @@ function launchSandbox(request) {
|
|
|
712
713
|
//#endregion
|
|
713
714
|
export { cockpitImageTag as a, parseRunFlags as c, DEVCONTAINER_DISABLED_ENV as d, launchSandbox as i, SpawnEngineProcessRunner as l, detectEngine as n, cockpitDockerfile as o, ensureImage as r, assertRunFlags as s, createSandboxLauncher as t, containerEnvironment as u };
|
|
714
715
|
|
|
715
|
-
//# sourceMappingURL=harness-
|
|
716
|
+
//# sourceMappingURL=harness-CEuhOTr2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-CEuhOTr2.mjs","names":["LAUNCHER_BINARY","BROKER_MOUNT_DIRECTORY"],"sources":["../src/services/devcontainer/index.ts","../src/services/oauthCallback/index.ts","../src/services/sandboxEnvironment/index.ts","../src/services/sandboxPlan/index.ts","../src/services/engineProcess/index.ts","../src/services/devcontainer/runtime.ts","../src/services/loginPorts/index.ts","../src/services/runFlags/index.ts","../src/services/sandboxImage/index.ts","../src/services/sandboxImageTag/index.ts","../src/services/harness/index.ts"],"sourcesContent":["import path from 'node:path';\n\n/**\n * Relative locations the Dev Containers spec looks for a configuration.\n *\n * Ordered as the spec resolves them, so the first hit on disk wins.\n */\nexport const DEVCONTAINER_CONFIG_PATHS: readonly string[] = ['.devcontainer/devcontainer.json', '.devcontainer.json'];\n\nexport const DEVCONTAINER_DISABLED_ENV = 'DOOMPI_SANDBOX_DEVCONTAINER';\nexport const DEVCONTAINER_CLI_PACKAGE = '@devcontainers/cli@0.80.0';\n/** Marks containers this layer brought up, so a stale one can be told apart. */\nexport const DEVCONTAINER_ID_LABEL = 'doompi.sandbox.devcontainer';\n\nexport interface DevcontainerUpResult {\n containerId?: string;\n /** Where the CLI mounted the workspace inside the container. */\n remoteWorkspaceFolder?: string;\n error?: string;\n}\n\nexport interface DevcontainerUpOptions {\n repoRoot: string;\n /** Host directory holding the broker socket, bind-mounted when one is used. */\n socketDirectory?: string;\n /** Container path the socket directory is mounted at. */\n socketTarget?: string;\n}\n\n/** Arguments for bringing the workspace's own dev container up. */\nexport function devcontainerUpArgs(options: DevcontainerUpOptions): string[] {\n return [\n 'up',\n '--workspace-folder',\n options.repoRoot,\n '--log-format',\n 'json',\n ...(options.socketDirectory && options.socketTarget\n ? ['--mount', `type=bind,source=${options.socketDirectory},target=${options.socketTarget}`]\n : []),\n ];\n}\n\n/**\n * Reads the container id out of the CLI's JSON log stream.\n *\n * The stream carries progress records too, so the result is the last record\n * that reports an outcome rather than the first line parsed.\n */\nexport function parseDevcontainerUp(stdout: string): DevcontainerUpResult {\n let outcome:\n | { outcome?: string; containerId?: string; remoteWorkspaceFolder?: string; message?: string; description?: string }\n | undefined;\n for (const line of stdout.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('{')) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (typeof parsed === 'object' && parsed !== null && 'outcome' in parsed) {\n outcome = parsed as typeof outcome;\n }\n }\n if (!outcome) return { error: 'The dev container CLI reported no outcome.' };\n if (outcome.outcome !== 'success') {\n return { error: outcome.description || outcome.message || `Dev container start reported ${outcome.outcome}.` };\n }\n if (!outcome.containerId) return { error: 'The dev container started but reported no container id.' };\n return { containerId: outcome.containerId, remoteWorkspaceFolder: outcome.remoteWorkspaceFolder };\n}\n\n/**\n * Maps a host path into the dev container's own workspace.\n *\n * A dev container mounts the workspace where its configuration says, commonly\n * /workspaces/<name>, so the host path the launch was given does not exist\n * inside and would fail the exec before the agent ever starts.\n */\nexport function containerWorkspacePath(repoRoot: string, cwd: string, remoteWorkspaceFolder: string): string {\n // A plain prefix test would also map a sibling such as <repo>-other into the container.\n const inside = path.relative(repoRoot, cwd);\n const relative = inside === '' || inside.startsWith('..') || path.isAbsolute(inside) ? '' : inside;\n return relative ? `${remoteWorkspaceFolder}/${relative.split('\\\\').join('/')}` : remoteWorkspaceFolder;\n}\n\n/**\n * Installs the distribution inside a dev container that lacks it.\n *\n * The container is the workspace's own, so it carries the project toolchain\n * rather than this layer's, and nothing guarantees DoomPi is present. `up`\n * reuses an existing container, so this is paid once for its lifetime.\n */\nexport function bootstrapCommand(version: string): string[] {\n return [\n 'sh',\n '-c',\n `command -v doompi >/dev/null 2>&1 && exit 0; ` +\n `command -v npm >/dev/null 2>&1 || { echo \"doompi-sandbox: this dev container has no npm to install DoomPi with\" >&2; exit 127; }; ` +\n `npm install -g @agimon-ai/doompi@${version} >/dev/null`,\n ];\n}\n\n/**\n * Rewrites forwarded host paths into the dev container's workspace.\n *\n * The launcher replays its resolved options, and several carry absolute host\n * paths: --cwd always, plugin and additional directories when set. Those\n * resolve in the built-in image, which mounts the repository at the path it\n * has on the host, but a dev container mounts it wherever its configuration\n * says, and the session would fail looking for a repository that is not there.\n */\nexport function mapForwardArgs(args: readonly string[], repoRoot: string, remoteWorkspaceFolder: string): string[] {\n return args.map((argument) =>\n argument === repoRoot || argument.startsWith(`${repoRoot}/`)\n ? containerWorkspacePath(repoRoot, argument, remoteWorkspaceFolder)\n : argument,\n );\n}\n\nexport interface DevcontainerExecOptions {\n containerId: string;\n cwd: string;\n environment: Readonly<Record<string, string>>;\n hasTty: boolean;\n command: readonly string[];\n}\n\n/**\n * Runs a command in the dev container through the engine rather than the CLI.\n *\n * `devcontainer exec` allocates no terminal, which a full screen TUI needs, so\n * the session is attached to the container the CLI already started.\n */\nexport function devcontainerExecArgs(options: DevcontainerExecOptions): string[] {\n const environmentPairs = Object.entries(options.environment).sort(([left], [right]) => left.localeCompare(right));\n return [\n 'exec',\n '-i',\n ...(options.hasTty ? ['-t'] : []),\n '-w',\n options.cwd,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n options.containerId,\n ...options.command,\n ];\n}\n","/**\n * Fixed loopback ports Pi's OAuth flows listen on.\n *\n * A provider redirects the browser to `http://localhost:<port>`, so the host\n * port has to match exactly and cannot be remapped. Providers that bind an\n * ephemeral port instead, such as OpenRouter, cannot be published ahead of the\n * flow and are not covered.\n */\nexport const OAUTH_CALLBACK_PORTS: readonly number[] = [\n 1455, // openai-codex\n 1456, // radius\n 53692, // anthropic\n];\n\nexport const OAUTH_CALLBACK_HOST_ENV = 'PI_OAUTH_CALLBACK_HOST';\n\n/**\n * Address the in-container callback server binds.\n *\n * Pi binds loopback by default, which a published port cannot reach: the\n * engine forwards to the container's external interface, not its loopback.\n * Only the bind address changes; the redirect the provider sees stays\n * `localhost`.\n */\nexport const OAUTH_CONTAINER_BIND = '0.0.0.0';\n\n/** Publishes each reachable callback port back onto the host's loopback. */\nexport function oauthPublishArgs(ports: readonly number[]): string[] {\n return ports.flatMap((port) => ['-p', `127.0.0.1:${port}:${port}`]);\n}\n","const EXACT_NAMES = new Set([\n 'TERM',\n 'COLORTERM',\n 'LANG',\n 'TZ',\n 'DOOMPI_PRESET',\n 'ELICITATION_SESSION_ID',\n 'HTTP_PROXY',\n 'HTTPS_PROXY',\n 'NO_PROXY',\n 'http_proxy',\n 'https_proxy',\n 'no_proxy',\n]);\nconst NAME_PREFIXES = ['LC_'];\nconst NAME_SUFFIXES = ['_API_KEY', '_AUTH_TOKEN', '_BASE_URL'];\n\n/** True for variables shaped like a provider credential or endpoint override. */\nexport function isCredentialEnvName(name: string): boolean {\n return NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));\n}\n\nfunction allowed(name: string): boolean {\n return (\n EXACT_NAMES.has(name) ||\n NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) ||\n NAME_SUFFIXES.some((suffix) => name.endsWith(suffix))\n );\n}\n\n/**\n * Projects the host environment down to what a sandboxed session may see.\n *\n * The list is an allowlist on purpose: everything a terminal accumulates\n * (agents, sockets, tokens, paths) stays on the host unless a rule names it.\n * Provider credentials pass through for now; the credential broker planned for\n * the client-server phase removes even those from the container.\n */\nexport function filterSandboxEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n): Record<string, string> {\n const filtered: Record<string, string> = {};\n for (const [name, value] of Object.entries(environment)) {\n if (value !== undefined && allowed(name)) filtered[name] = value;\n }\n return filtered;\n}\n","import { DOOMPI_SANDBOX_ENV } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { BrokerEndpoint, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { OAUTH_CALLBACK_HOST_ENV, OAUTH_CONTAINER_BIND, oauthPublishArgs } from '../oauthCallback';\nimport {\n BRIDGE_CONTAINER_PATH,\n BROKER_ADDRESS_ENV,\n BROKER_CONTAINER_PORT,\n BROKER_HOST_GATEWAY,\n BROKER_PORT_ENV,\n BROKER_PROVIDERS_ENV,\n BROKER_SOCKET_CONTAINER_PATH,\n BROKER_SOCKET_ENV,\n} from '../sandboxBridge';\nimport { filterSandboxEnvironment, isCredentialEnvName } from '../sandboxEnvironment';\n\nconst CONTAINER_HOME = '/doompi-home';\nconst REPOSITORY_LABEL = 'doompi.sandbox.repo';\n// The harness binary, which accepts every forwarded flag; dpi hands its\n// arguments straight to Pi and would reject harness options like --preset.\nconst LAUNCHER_BINARY = 'doompi';\nconst NODE_BINARY = 'node';\nconst LINUX_PLATFORM = 'linux';\nconst ENABLED = '1';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Host broker facts the plan projects into mounts and environment. */\nexport interface SandboxPlanBroker {\n endpoint: BrokerEndpoint;\n token: string;\n providers: readonly string[];\n /** Credential variables whose value the container receives as the token. */\n withheldEnv: readonly string[];\n}\n\nexport interface SandboxPlanInput {\n repoRoot: string;\n cwd: string;\n forwardArgs: string[];\n environment: Readonly<Record<string, string | undefined>>;\n engine: SandboxEngine;\n host: SandboxHostFacts;\n /** Image the launch resolved, which also pins the image definition. */\n imageTag: string;\n /** Engine options the operator configured, such as an alternate runtime. */\n runFlags?: readonly string[];\n /** OAuth callback ports free on the host, published so a browser can reach them. */\n loginPorts?: readonly number[];\n broker?: SandboxPlanBroker;\n}\n\nexport interface SandboxPlan {\n imageTag: string;\n /** Full engine argv after the engine binary itself. */\n runArgs: string[];\n}\n\n/**\n * Replaces every credential in the container with the broker's session token.\n *\n * Dropping all credential-shaped variables first is what makes the promise\n * hold for providers the broker does not carry: an unbrokered key would\n * otherwise stay readable inside the container.\n */\nfunction brokeredEnvironment(filtered: Record<string, string>, broker: SandboxPlanBroker): Record<string, string> {\n const environment: Record<string, string> = {};\n for (const [name, value] of Object.entries(filtered)) {\n if (!isCredentialEnvName(name)) environment[name] = value;\n }\n for (const name of broker.withheldEnv) environment[name] = broker.token;\n if (broker.endpoint.transport === 'unix') {\n environment[BROKER_SOCKET_ENV] = BROKER_SOCKET_CONTAINER_PATH;\n // Only the bridge reads this, and only a socket needs the bridge.\n environment[BROKER_PORT_ENV] = String(BROKER_CONTAINER_PORT);\n } else {\n environment[BROKER_ADDRESS_ENV] = `${BROKER_HOST_GATEWAY}:${broker.endpoint.port}`;\n }\n environment[BROKER_PROVIDERS_ENV] = broker.providers.join(',');\n return environment;\n}\n\n/**\n * Grants the container the one route it needs to reach the broker.\n *\n * A mounted socket needs no network at all. TCP needs the engine's host\n * gateway named explicitly, because it is not resolvable by default on every\n * engine.\n */\nfunction brokerAccessArgs(broker: SandboxPlanBroker | undefined): string[] {\n if (!broker) return [];\n if (broker.endpoint.transport === 'unix') {\n return ['-v', `${broker.endpoint.socketDirectory}:${BROKER_MOUNT_DIRECTORY}`];\n }\n return ['--add-host', `${BROKER_HOST_GATEWAY}:host-gateway`];\n}\n\n/**\n * Projects one launch into a disposable engine run.\n *\n * The container is ephemeral; persistence lives in two named volumes: the\n * container home (Pi sessions, sync cache) and a shadow over the repository's\n * `.pi` store, which keeps Linux installs from corrupting the host's own\n * platform-specific packages on the shared workspace mount.\n */\n/**\n * The environment a sandboxed session runs with, whichever container hosts it.\n *\n * Shared with the dev container path, which builds no run arguments of its own\n * but must apply the same allowlist and the same credential substitution.\n */\nexport function containerEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n broker?: SandboxPlanBroker,\n loginPorts: readonly number[] = [],\n): Record<string, string> {\n const filtered = filterSandboxEnvironment(environment);\n const resolved = broker ? brokeredEnvironment(filtered, broker) : filtered;\n resolved[DOOMPI_SANDBOX_ENV] = ENABLED;\n if (loginPorts.length > 0) resolved[OAUTH_CALLBACK_HOST_ENV] = OAUTH_CONTAINER_BIND;\n return resolved;\n}\n\nexport function buildSandboxPlan(input: SandboxPlanInput): SandboxPlan {\n const { repoRoot, cwd, engine, host, broker, imageTag } = input;\n const loginPorts = input.loginPorts ?? [];\n const environmentPairs = Object.entries(containerEnvironment(input.environment, broker, loginPorts)).sort(\n ([left], [right]) => left.localeCompare(right),\n );\n\n const runArgs = [\n 'run',\n '--rm',\n '-i',\n ...(host.hasTty ? ['-t'] : []),\n '--label',\n `${REPOSITORY_LABEL}=${repoRoot}`,\n '-v',\n `${repoRoot}:${repoRoot}`,\n '-v',\n `${host.repoKey}-home:${CONTAINER_HOME}`,\n '-v',\n `${host.repoKey}-pi:${repoRoot}/.pi`,\n ...brokerAccessArgs(broker),\n ...oauthPublishArgs(loginPorts),\n '-w',\n cwd,\n '-e',\n `HOME=${CONTAINER_HOME}`,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n ...(host.platform === LINUX_PLATFORM && host.userId !== undefined\n ? [\n '--user',\n `${host.userId}:${host.groupId ?? host.userId}`,\n ...(engine === 'podman' ? ['--userns=keep-id'] : []),\n ]\n : []),\n // Last before the image so a configured option wins over the defaults\n // above it, and cannot be mistaken for the image or its command.\n ...(input.runFlags ?? []),\n imageTag,\n // Only a mounted socket needs the bridge: a broker on a host port is\n // addressed directly, so the launcher runs unwrapped.\n ...(broker?.endpoint.transport === 'unix' ? [NODE_BINARY, BRIDGE_CONTAINER_PATH] : []),\n LAUNCHER_BINARY,\n ...input.forwardArgs,\n ];\n\n return { imageTag, runArgs };\n}\n","import { spawn } from 'node:child_process';\n\nimport type { EngineCaptureResult, EngineProcessRunner, EngineRunOptions } from '../../types/sandboxHarness';\n\nconst INHERIT = 'inherit';\nconst PIPE = 'pipe';\nconst IGNORE = 'ignore';\nconst FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];\n\n/** Spawns the container engine, keeping the session's terminal attached. */\nexport class SpawnEngineProcessRunner implements EngineProcessRunner {\n run(command: string, args: string[], options: EngineRunOptions = {}): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options.input === undefined ? INHERIT : [PIPE, INHERIT, INHERIT],\n });\n if (options.input !== undefined) child.stdin?.end(options.input);\n\n // With a shared terminal the engine sees Ctrl-C itself; forwarding covers\n // detached automation runs, and a duplicate signal is harmless.\n const forwarders = FORWARDED_SIGNALS.map((signal) => {\n const forward = (): void => {\n child.kill(signal);\n };\n process.on(signal, forward);\n return { signal, forward };\n });\n const cleanup = (): void => {\n for (const { signal, forward } of forwarders) process.off(signal, forward);\n };\n\n child.once('error', (error) => {\n cleanup();\n reject(error);\n });\n child.once('exit', (code, signal) => {\n cleanup();\n resolve(code ?? (signal ? 1 : 0));\n });\n });\n }\n\n capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined> {\n return new Promise((resolve) => {\n const child = spawn(command, args, { stdio: [IGNORE, PIPE, IGNORE] });\n let stdout = '';\n child.stdout?.on('data', (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.once('error', () => {\n resolve(undefined);\n });\n child.once('exit', (code) => {\n resolve({ exitCode: code ?? 1, stdout });\n });\n });\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n bootstrapCommand,\n containerWorkspacePath,\n mapForwardArgs,\n DEVCONTAINER_CLI_PACKAGE,\n DEVCONTAINER_CONFIG_PATHS,\n devcontainerExecArgs,\n devcontainerUpArgs,\n parseDevcontainerUp,\n} from '.';\nimport type { EngineProcessRunner, SandboxEngine } from '../../types/sandboxHarness';\n\nconst DEVCONTAINER_BINARY = 'devcontainer';\nconst NPX_BINARY = 'npx';\nconst LAUNCHER_BINARY = 'doompi';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Locates the workspace's own dev container configuration, if it has one. */\nexport function findDevcontainerConfig(repoRoot: string): string | undefined {\n for (const relative of DEVCONTAINER_CONFIG_PATHS) {\n const candidate = path.join(repoRoot, relative);\n if (fs.existsSync(candidate)) return candidate;\n }\n return undefined;\n}\n\n/**\n * Resolves how to invoke the Dev Containers CLI.\n *\n * A local install is preferred so a launch does not depend on the network;\n * npx is the fallback, pinned so the tool cannot change underneath a session.\n */\nexport async function resolveDevcontainerCli(\n runner: EngineProcessRunner,\n): Promise<{ command: string; prefix: string[] }> {\n const probe = await runner.capture(DEVCONTAINER_BINARY, ['--version']);\n if (probe?.exitCode === 0) return { command: DEVCONTAINER_BINARY, prefix: [] };\n return { command: NPX_BINARY, prefix: ['-y', DEVCONTAINER_CLI_PACKAGE] };\n}\n\nexport interface DevcontainerSessionOptions {\n repoRoot: string;\n cwd: string;\n forwardArgs: readonly string[];\n environment: Readonly<Record<string, string>>;\n engine: SandboxEngine;\n runner: EngineProcessRunner;\n version: string;\n hasTty: boolean;\n /** Host directory holding the broker socket, when the broker uses one. */\n socketDirectory?: string;\n onProgress?: (message: string) => void;\n}\n\n/**\n * Runs the session inside the workspace's own dev container.\n *\n * The container is brought up by the Dev Containers CLI, so the file decides\n * the image, features, mounts and lifecycle hooks: the session gets the\n * project's real toolchain, and this layer's isolation guarantees do not\n * apply beyond the environment it passes in.\n */\nexport async function runDevcontainerSession(options: DevcontainerSessionOptions): Promise<number> {\n const cli = await resolveDevcontainerCli(options.runner);\n options.onProgress?.('starting the workspace dev container');\n\n const upArgs = devcontainerUpArgs({\n repoRoot: options.repoRoot,\n socketDirectory: options.socketDirectory,\n socketTarget: options.socketDirectory ? BROKER_MOUNT_DIRECTORY : undefined,\n });\n const up = await options.runner.capture(cli.command, [...cli.prefix, ...upArgs]);\n if (!up) {\n throw new Error('The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.');\n }\n const { containerId, remoteWorkspaceFolder, error } = parseDevcontainerUp(up.stdout);\n if (!containerId) throw new Error(`The dev container did not start: ${error ?? `exit ${up.exitCode}`}`);\n\n const bootstrap = await options.runner.run(options.engine, [\n 'exec',\n containerId,\n ...bootstrapCommand(options.version),\n ]);\n if (bootstrap !== 0) {\n throw new Error(\n `DoomPi could not be installed in the dev container (exit ${bootstrap}). ` +\n 'Add it to the dev container, or unset the dev container preference to use the built-in image.',\n );\n }\n\n options.onProgress?.(`attaching to dev container ${containerId.slice(0, 12)}`);\n return options.runner.run(\n options.engine,\n devcontainerExecArgs({\n containerId,\n cwd: remoteWorkspaceFolder\n ? containerWorkspacePath(options.repoRoot, options.cwd, remoteWorkspaceFolder)\n : options.cwd,\n environment: options.environment,\n hasTty: options.hasTty,\n command: [\n LAUNCHER_BINARY,\n ...(remoteWorkspaceFolder\n ? mapForwardArgs(options.forwardArgs, options.repoRoot, remoteWorkspaceFolder)\n : options.forwardArgs),\n ],\n }),\n );\n}\n","import net from 'node:net';\n\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\n\nconst LOOPBACK = '127.0.0.1';\n\nfunction portIsFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = net.createServer();\n probe.once('error', () => resolve(false));\n probe.listen(port, LOOPBACK, () => {\n probe.close(() => resolve(true));\n });\n });\n}\n\n/**\n * Reports which OAuth callback ports this launch may publish.\n *\n * A taken port means another sandbox already holds it, so publishing would\n * fail the whole launch over a login the session may never attempt. Skipping\n * it costs only the ability to log in from inside this particular container.\n */\nexport async function availableLoginPorts(ports: readonly number[] = OAUTH_CALLBACK_PORTS): Promise<number[]> {\n const free: number[] = [];\n for (const port of ports) {\n if (await portIsFree(port)) free.push(port);\n }\n return free;\n}\n","/**\n * Splits configured engine options into argv entries.\n *\n * Whitespace separated rather than shell parsed: the engine is spawned without\n * a shell, so quoting rules would only invent a syntax the caller does not\n * actually get.\n */\nexport function parseRunFlags(configured: string | undefined): string[] {\n return (configured ?? '').split(/\\s+/).filter((flag) => flag.length > 0);\n}\n\nconst OPTION_FORM = /^--?[^\\s=]+(=.*)?$/s;\n\n/**\n * Requires every configured entry to be a self-contained option.\n *\n * Separated values cannot be told from positional arguments without knowing\n * each option's arity, and a stray positional would silently replace the image\n * or its command. Demanding `--flag=value` removes the ambiguity instead of\n * guessing at it.\n */\nexport function assertRunFlags(flags: readonly string[], optionName: string): void {\n const invalid = flags.find((flag) => !OPTION_FORM.test(flag));\n if (invalid !== undefined) {\n throw new Error(\n `${optionName} accepts engine options in --flag or --flag=value form; \"${invalid}\" is neither. ` +\n 'Write a value as part of its option, for example --runtime=runsc.',\n );\n }\n}\n","import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from '../sandboxBridge';\n\nconst IMAGE_REPOSITORY = 'doompi-sandbox';\nconst TAG_SAFE = /[^A-Za-z0-9_.-]/g;\n\n/**\n * Names one image build: a distribution version plus its image definition.\n *\n * The digest matters as much as the version. Editing the Dockerfile or the\n * bridge without it would silently reuse a cached image that predates the\n * change.\n */\nexport function formatImageTag(version: string, digest: string): string {\n return `${IMAGE_REPOSITORY}:v${version.replaceAll(TAG_SAFE, '-')}-${digest}`;\n}\n\n/**\n * Self-contained Linux image for sandboxed sessions.\n *\n * nano is present because Pi falls back to it for the external editor when\n * VISUAL and EDITOR are unset, which is always the case here: a host editor\n * variable naming a desktop application would resolve to a binary the\n * container does not have, so it is deliberately not forwarded.\n *\n * The distribution installs from the registry rather than mounting host\n * modules: host installs carry platform-specific binaries a Linux container\n * cannot run. DOOMPI_SANDBOX is baked in so nothing that reaches a shell in\n * the container can present itself as an unsandboxed session.\n */\nexport function sandboxDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi@${DOOMPI_VERSION}',\n `COPY ${BRIDGE_FILE_NAME} ${BRIDGE_CONTAINER_PATH}`,\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n\n/**\n * Image for the cockpit container, which holds the hub rather than one session.\n *\n * Kept separate from the sandbox image rather than folded into it. The cockpit\n * needs the web package and a tunnel client, and making every interactive\n * `--sandbox` launch pay for an extra apt repository and a larger download to\n * get them would be a poor trade.\n *\n * Installing `@agimon-ai/doompi-web` is enough for all three programs: its\n * `@agimon-ai/doompi` dependency owns both the session server executable and\n * the agent runtime.\n */\nexport function cockpitDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n // The tunnel runs beside the hub rather than on the host, so the cockpit\n // owns its own remote access and all of that state stays in one place.\n 'RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\\\',\n ' -o /usr/share/keyrings/cloudflare-main.gpg \\\\',\n ' && echo \"deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main\" \\\\',\n ' > /etc/apt/sources.list.d/cloudflared.list \\\\',\n ' && apt-get update && apt-get install -y --no-install-recommends cloudflared \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi-web@${DOOMPI_VERSION}',\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n","import { createHash } from 'node:crypto';\n\nimport { sandboxBridgeSource } from '../sandboxBridge';\nimport { cockpitDockerfile, formatImageTag, sandboxDockerfile } from '../sandboxImage';\n\nconst DIGEST_LENGTH = 8;\n\n/** Tags the image with the distribution version and a digest of its definition. */\nexport function sandboxImageTag(version: string): string {\n const digest = createHash('sha256')\n .update(sandboxDockerfile())\n .update(sandboxBridgeSource())\n .digest('hex')\n .slice(0, DIGEST_LENGTH);\n return formatImageTag(version, digest);\n}\n\n/**\n * Tags the cockpit image separately from the sandbox one.\n *\n * Same digest scheme, different definition, so editing either Dockerfile\n * invalidates only its own image.\n */\nexport function cockpitImageTag(version: string): string {\n const digest = createHash('sha256').update(cockpitDockerfile()).digest('hex').slice(0, DIGEST_LENGTH);\n return formatImageTag(`${version}-cockpit`, digest);\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { startBroker, type RunningBroker } from '../brokerHost';\nimport { DEVCONTAINER_DISABLED_ENV } from '../devcontainer';\nimport { findDevcontainerConfig, runDevcontainerSession } from '../devcontainer/runtime';\nimport { SpawnEngineProcessRunner } from '../engineProcess';\nimport { availableLoginPorts } from '../loginPorts';\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\nimport { assertRunFlags, parseRunFlags } from '../runFlags';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../sandboxBridge';\nimport { sandboxDockerfile } from '../sandboxImage';\nimport { sandboxImageTag } from '../sandboxImageTag';\nimport { buildSandboxPlan, containerEnvironment } from '../sandboxPlan';\n\nconst ENGINE_ENV = 'DOOMPI_SANDBOX_ENGINE';\nconst BROKER_DISABLED_ENV = 'DOOMPI_SANDBOX_BROKER';\nconst RUN_FLAGS_ENV = 'DOOMPI_SANDBOX_RUN_FLAGS';\nconst DISABLED_VALUE = '0';\n// Every entry takes docker's run syntax, which is what the plan emits.\nconst ENGINES: SandboxEngine[] = ['docker', 'podman', 'nerdctl', 'finch'];\nconst DOCKERFILE_NAME = 'Dockerfile';\nconst MODES_FILE = path.join('.doom', 'modes.yaml');\nconst LOCAL_PACKAGE_PATTERN = /^\\s*(?:-|name:)\\s*[\"']?\\.{1,2}\\//m;\nconst REPO_KEY_LENGTH = 12;\n\n// Self-reference instead of a relative path: the file depth differs between\n// running from src and from the built dist tree.\nconst require = createRequire(import.meta.url);\nconst SELF_MANIFEST = '@agimon-ai/doompi-sandbox/package.json';\n\nexport interface SandboxLauncherDependencies {\n runner?: EngineProcessRunner;\n /** Seam for tests; defaults to probing the real OAuth callback ports. */\n loginPorts?: () => Promise<number[]>;\n /** Seam for tests; defaults to the real host broker. */\n startBroker?: (options: {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n }) => Promise<RunningBroker | undefined>;\n /** Distribution version override; defaults to this package's own version. */\n version?: string;\n hostFacts?: Partial<SandboxHostFacts>;\n}\n\nfunction distributionVersion(): string {\n return (require(SELF_MANIFEST) as { version: string }).version;\n}\n\n/** Fixed per-repository directory a reused dev container can keep mounted. */\nfunction brokerDirectory(repoRoot: string): string {\n return path.join(os.tmpdir(), `${repoKey(repoRoot)}-broker`);\n}\n\nfunction repoKey(repoRoot: string): string {\n return `doompi-sandbox-${createHash('sha256').update(repoRoot).digest('hex').slice(0, REPO_KEY_LENGTH)}`;\n}\n\nexport async function detectEngine(\n runner: EngineProcessRunner,\n environment: Readonly<Record<string, string | undefined>>,\n): Promise<SandboxEngine> {\n const configured = environment[ENGINE_ENV]?.trim();\n if (configured) {\n if ((ENGINES as string[]).includes(configured)) return configured as SandboxEngine;\n throw new Error(`${ENGINE_ENV} must be one of: ${ENGINES.join(', ')}.`);\n }\n for (const engine of ENGINES) {\n const probe = await runner.capture(engine, ['--version']);\n if (probe?.exitCode === 0) return engine;\n }\n throw new Error(`No container engine found. Install one of ${ENGINES.join(', ')}, or set ${ENGINE_ENV}.`);\n}\n\n/**\n * Builds the image if the engine does not already have it.\n *\n * Takes its definition rather than assuming one, because the interactive\n * sandbox and the cockpit container are two different images built the same\n * way. The tag already carries a digest of the definition, so a changed\n * Dockerfile misses the inspect and rebuilds.\n */\nexport async function ensureImage(\n runner: EngineProcessRunner,\n engine: SandboxEngine,\n version: string,\n onProgress: ((message: string) => void) | undefined,\n definition: { tag: string; dockerfile: string; files?: Readonly<Record<string, string>> },\n): Promise<string> {\n const tag = definition.tag;\n const inspected = await runner.capture(engine, ['image', 'inspect', tag]);\n if (inspected?.exitCode === 0) return tag;\n\n onProgress?.(`building ${tag} (first run for this DoomPi version)`);\n const context = fs.mkdtempSync(path.join(os.tmpdir(), 'doompi-sandbox-build-'));\n try {\n fs.writeFileSync(path.join(context, DOCKERFILE_NAME), definition.dockerfile);\n for (const [name, contents] of Object.entries(definition.files ?? {})) {\n fs.writeFileSync(path.join(context, name), contents);\n }\n const exitCode = await runner.run(engine, [\n 'build',\n '-t',\n tag,\n '--build-arg',\n `DOOMPI_VERSION=${version}`,\n context,\n ]);\n if (exitCode !== 0) throw new Error(`Building the ${tag} image failed with exit code ${exitCode}.`);\n } finally {\n fs.rmSync(context, { recursive: true, force: true });\n }\n return tag;\n}\n\nfunction warnAboutLocalPackages(repoRoot: string, onProgress: ((message: string) => void) | undefined): void {\n let modes: string;\n try {\n modes = fs.readFileSync(path.join(repoRoot, MODES_FILE), 'utf8');\n } catch {\n return;\n }\n if (LOCAL_PACKAGE_PATTERN.test(modes)) {\n onProgress?.(\n 'modes.yaml declares local workspace packages; their platform-specific dependencies ' +\n 'must be installed inside the container before they can load',\n );\n }\n}\n\n/** Builds the launcher with injectable seams for tests. */\nexport function createSandboxLauncher(dependencies: SandboxLauncherDependencies = {}) {\n const runner = dependencies.runner ?? new SpawnEngineProcessRunner();\n const beginBroker = dependencies.startBroker ?? startBroker;\n const resolveLoginPorts = dependencies.loginPorts ?? availableLoginPorts;\n return {\n async launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n const engine = await detectEngine(runner, request.environment);\n const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);\n assertRunFlags(runFlags, RUN_FLAGS_ENV);\n request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(' ')}` : ''}`);\n\n const version = dependencies.version ?? distributionVersion();\n // A workspace that describes its own container is describing the\n // toolchain its agent needs, so that container wins over the built-in\n // image. Nothing here is built or pulled until that choice is made.\n const devcontainer =\n request.environment[DEVCONTAINER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : findDevcontainerConfig(request.repoRoot);\n const imageTag = devcontainer\n ? undefined\n : await ensureImage(runner, engine, version, request.onProgress, {\n tag: sandboxImageTag(version),\n dockerfile: sandboxDockerfile(),\n files: { [BRIDGE_FILE_NAME]: sandboxBridgeSource() },\n });\n if (devcontainer) {\n request.onProgress?.(`using the workspace dev container from ${path.relative(request.repoRoot, devcontainer)}`);\n request.onProgress?.(\n 'its configuration owns the mounts and run arguments, so this is not an isolation boundary',\n );\n } else {\n warnAboutLocalPackages(request.repoRoot, request.onProgress);\n }\n\n const broker =\n request.environment[BROKER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : await beginBroker({\n environment: request.environment,\n onDenied: (reason) => request.onProgress?.(`broker refused a call: ${reason}`),\n // A reused dev container keeps the mounts it was created with,\n // so the socket has to be at the same path on every launch.\n ...(devcontainer ? { socketDirectory: brokerDirectory(request.repoRoot) } : {}),\n });\n if (broker) {\n request.onProgress?.(`brokering ${broker.providers.join(', ')}; provider keys stay on the host`);\n }\n\n const loginPorts = devcontainer ? [] : await resolveLoginPorts();\n if (!devcontainer && loginPorts.length < OAUTH_CALLBACK_PORTS.length) {\n request.onProgress?.(\n 'some OAuth callback ports are already taken; /login inside this sandbox may not complete',\n );\n }\n\n try {\n const hasTty = process.stdin.isTTY === true && process.stdout.isTTY === true;\n if (devcontainer) {\n return await runDevcontainerSession({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: containerEnvironment(request.environment, broker),\n engine,\n runner,\n version,\n hasTty,\n ...(broker?.endpoint.transport === 'unix' ? { socketDirectory: broker.endpoint.socketDirectory } : {}),\n ...(request.onProgress ? { onProgress: request.onProgress } : {}),\n });\n }\n\n const host: SandboxHostFacts = {\n hasTty,\n platform: process.platform,\n userId: process.getuid?.(),\n groupId: process.getgid?.(),\n repoKey: repoKey(request.repoRoot),\n version,\n ...dependencies.hostFacts,\n };\n const plan = buildSandboxPlan({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: request.environment,\n engine,\n host,\n imageTag: imageTag as string,\n runFlags,\n loginPorts,\n broker,\n });\n request.onProgress?.(`starting contained session in ${imageTag}`);\n return await runner.run(engine, plan.runArgs);\n } finally {\n await broker?.stop();\n }\n },\n };\n}\n\n/** Contract entry the DoomPi harness resolves for `--sandbox`. */\nexport function launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n return createSandboxLauncher().launchSandbox(request);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAOA,MAAa,4BAA+C,CAAC,mCAAmC,oBAAoB;AAEpH,MAAa,4BAA4B;AACzC,MAAa,2BAA2B;;AAoBxC,SAAgB,mBAAmB,SAA0C;CAC3E,OAAO;EACL;EACA;EACA,QAAQ;EACR;EACA;EACA,GAAI,QAAQ,mBAAmB,QAAQ,eACnC,CAAC,WAAW,oBAAoB,QAAQ,gBAAgB,UAAU,QAAQ,cAAc,IACxF,CAAC;CACP;AACF;;;;;;;AAQA,SAAgB,oBAAoB,QAAsC;CACxE,IAAI;CAGJ,KAAK,MAAM,QAAQ,OAAO,MAAM,IAAI,GAAG;EACrC,MAAM,UAAU,KAAK,KAAK;EAC1B,IAAI,CAAC,QAAQ,WAAW,GAAG,GAAG;EAC9B,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,OAAO;EAC7B,QAAQ;GACN;EACF;EACA,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,aAAa,QAChE,UAAU;CAEd;CACA,IAAI,CAAC,SAAS,OAAO,EAAE,OAAO,6CAA6C;CAC3E,IAAI,QAAQ,YAAY,WACtB,OAAO,EAAE,OAAO,QAAQ,eAAe,QAAQ,WAAW,gCAAgC,QAAQ,QAAQ,GAAG;CAE/G,IAAI,CAAC,QAAQ,aAAa,OAAO,EAAE,OAAO,0DAA0D;CACpG,OAAO;EAAE,aAAa,QAAQ;EAAa,uBAAuB,QAAQ;CAAsB;AAClG;;;;;;;;AASA,SAAgB,uBAAuB,UAAkB,KAAa,uBAAuC;CAE3G,MAAM,SAAS,KAAK,SAAS,UAAU,GAAG;CAC1C,MAAM,WAAW,WAAW,MAAM,OAAO,WAAW,IAAI,KAAK,KAAK,WAAW,MAAM,IAAI,KAAK;CAC5F,OAAO,WAAW,GAAG,sBAAsB,GAAG,SAAS,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AACnF;;;;;;;;AASA,SAAgB,iBAAiB,SAA2B;CAC1D,OAAO;EACL;EACA;EACA,mNAEsC,QAAQ;CAChD;AACF;;;;;;;;;;AAWA,SAAgB,eAAe,MAAyB,UAAkB,uBAAyC;CACjH,OAAO,KAAK,KAAK,aACf,aAAa,YAAY,SAAS,WAAW,GAAG,SAAS,EAAE,IACvD,uBAAuB,UAAU,UAAU,qBAAqB,IAChE,QACN;AACF;;;;;;;AAgBA,SAAgB,qBAAqB,SAA4C;CAC/E,MAAM,mBAAmB,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,KAAK,cAAc,KAAK,CAAC;CAChH,OAAO;EACL;EACA;EACA,GAAI,QAAQ,SAAS,CAAC,IAAI,IAAI,CAAC;EAC/B;EACA,QAAQ;EACR,GAAG,iBAAiB,SAAS,CAAC,MAAM,WAAW,CAAC,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;EACzE,QAAQ;EACR,GAAG,QAAQ;CACb;AACF;;;;;;;;;;;AC5IA,MAAa,uBAA0C;CACrD;CACA;CACA;AACF;AAEA,MAAa,0BAA0B;;;;;;;;;AAUvC,MAAa,uBAAuB;;AAGpC,SAAgB,iBAAiB,OAAoC;CACnE,OAAO,MAAM,SAAS,SAAS,CAAC,MAAM,aAAa,KAAK,GAAG,MAAM,CAAC;AACpE;;;AC7BA,MAAM,8BAAc,IAAI,IAAI;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,gBAAgB,CAAC,KAAK;AAC5B,MAAM,gBAAgB;CAAC;CAAY;CAAe;AAAW;;AAG7D,SAAgB,oBAAoB,MAAuB;CACzD,OAAO,cAAc,MAAM,WAAW,KAAK,SAAS,MAAM,CAAC;AAC7D;AAEA,SAAS,QAAQ,MAAuB;CACtC,OACE,YAAY,IAAI,IAAI,KACpB,cAAc,MAAM,WAAW,KAAK,WAAW,MAAM,CAAC,KACtD,cAAc,MAAM,WAAW,KAAK,SAAS,MAAM,CAAC;AAExD;;;;;;;;;AAUA,SAAgB,yBACd,aACwB;CACxB,MAAM,WAAmC,CAAC;CAC1C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,WAAW,GACpD,IAAI,UAAU,KAAA,KAAa,QAAQ,IAAI,GAAG,SAAS,QAAQ;CAE7D,OAAO;AACT;;;AC9BA,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AAGzB,MAAMA,oBAAkB;AACxB,MAAM,cAAc;AACpB,MAAM,iBAAiB;AACvB,MAAM,UAAU;AAChB,MAAMC,2BAAyB;;;;;;;;AAwC/B,SAAS,oBAAoB,UAAkC,QAAmD;CAChH,MAAM,cAAsC,CAAC;CAC7C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,GACjD,IAAI,CAAC,oBAAoB,IAAI,GAAG,YAAY,QAAQ;CAEtD,KAAK,MAAM,QAAQ,OAAO,aAAa,YAAY,QAAQ,OAAO;CAClE,IAAI,OAAO,SAAS,cAAc,QAAQ;EACxC,YAAY,qBAAqB;EAEjC,YAAY,mBAAmB,OAAO,qBAAqB;CAC7D,OACE,YAAY,sBAAsB,GAAG,oBAAoB,GAAG,OAAO,SAAS;CAE9E,YAAY,wBAAwB,OAAO,UAAU,KAAK,GAAG;CAC7D,OAAO;AACT;;;;;;;;AASA,SAAS,iBAAiB,QAAiD;CACzE,IAAI,CAAC,QAAQ,OAAO,CAAC;CACrB,IAAI,OAAO,SAAS,cAAc,QAChC,OAAO,CAAC,MAAM,GAAG,OAAO,SAAS,gBAAgB,GAAGA,0BAAwB;CAE9E,OAAO,CAAC,cAAc,GAAG,oBAAoB,cAAc;AAC7D;;;;;;;;;;;;;;;AAgBA,SAAgB,qBACd,aACA,QACA,aAAgC,CAAC,GACT;CACxB,MAAM,WAAW,yBAAyB,WAAW;CACrD,MAAM,WAAW,SAAS,oBAAoB,UAAU,MAAM,IAAI;CAClE,SAAS,sBAAsB;CAC/B,IAAI,WAAW,SAAS,GAAG,SAAS,2BAA2B;CAC/D,OAAO;AACT;AAEA,SAAgB,iBAAiB,OAAsC;CACrE,MAAM,EAAE,UAAU,KAAK,QAAQ,MAAM,QAAQ,aAAa;CAC1D,MAAM,aAAa,MAAM,cAAc,CAAC;CACxC,MAAM,mBAAmB,OAAO,QAAQ,qBAAqB,MAAM,aAAa,QAAQ,UAAU,CAAC,CAAC,CAAC,MAClG,CAAC,OAAO,CAAC,WAAW,KAAK,cAAc,KAAK,CAC/C;CAwCA,OAAO;EAAE;EAAU,SAAA;GArCjB;GACA;GACA;GACA,GAAI,KAAK,SAAS,CAAC,IAAI,IAAI,CAAC;GAC5B;GACA,GAAG,iBAAiB,GAAG;GACvB;GACA,GAAG,SAAS,GAAG;GACf;GACA,GAAG,KAAK,QAAQ,QAAQ;GACxB;GACA,GAAG,KAAK,QAAQ,MAAM,SAAS;GAC/B,GAAG,iBAAiB,MAAM;GAC1B,GAAG,iBAAiB,UAAU;GAC9B;GACA;GACA;GACA,QAAQ;GACR,GAAG,iBAAiB,SAAS,CAAC,MAAM,WAAW,CAAC,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;GACzE,GAAI,KAAK,aAAa,kBAAkB,KAAK,WAAW,KAAA,IACpD;IACE;IACA,GAAG,KAAK,OAAO,GAAG,KAAK,WAAW,KAAK;IACvC,GAAI,WAAW,WAAW,CAAC,kBAAkB,IAAI,CAAC;GACpD,IACA,CAAC;GAGL,GAAI,MAAM,YAAY,CAAC;GACvB;GAGA,GAAI,QAAQ,SAAS,cAAc,SAAS,CAAC,aAAa,qBAAqB,IAAI,CAAC;GACpFD;GACA,GAAG,MAAM;EAGc;CAAE;AAC7B;;;ACpKA,MAAM,UAAU;AAChB,MAAM,OAAO;AACb,MAAM,SAAS;AACf,MAAM,oBAAsC;CAAC;CAAU;CAAW;AAAQ;;AAG1E,IAAa,2BAAb,MAAqE;CACnE,IAAI,SAAiB,MAAgB,UAA4B,CAAC,GAAoB;EACpF,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,QAAQ,MAAM,SAAS,MAAM,EACjC,OAAO,QAAQ,UAAU,KAAA,IAAY,UAAU;IAAC;IAAM;IAAS;GAAO,EACxE,CAAC;GACD,IAAI,QAAQ,UAAU,KAAA,GAAW,MAAM,OAAO,IAAI,QAAQ,KAAK;GAI/D,MAAM,aAAa,kBAAkB,KAAK,WAAW;IACnD,MAAM,gBAAsB;KAC1B,MAAM,KAAK,MAAM;IACnB;IACA,QAAQ,GAAG,QAAQ,OAAO;IAC1B,OAAO;KAAE;KAAQ;IAAQ;GAC3B,CAAC;GACD,MAAM,gBAAsB;IAC1B,KAAK,MAAM,EAAE,QAAQ,aAAa,YAAY,QAAQ,IAAI,QAAQ,OAAO;GAC3E;GAEA,MAAM,KAAK,UAAU,UAAU;IAC7B,QAAQ;IACR,OAAO,KAAK;GACd,CAAC;GACD,MAAM,KAAK,SAAS,MAAM,WAAW;IACnC,QAAQ;IACR,QAAQ,SAAS,SAAS,IAAI,EAAE;GAClC,CAAC;EACH,CAAC;CACH;CAEA,QAAQ,SAAiB,MAA0D;EACjF,OAAO,IAAI,SAAS,YAAY;GAC9B,MAAM,QAAQ,MAAM,SAAS,MAAM,EAAE,OAAO;IAAC;IAAQ;IAAM;GAAM,EAAE,CAAC;GACpE,IAAI,SAAS;GACb,MAAM,QAAQ,GAAG,SAAS,UAA2B;IACnD,UAAU,MAAM,SAAS;GAC3B,CAAC;GACD,MAAM,KAAK,eAAe;IACxB,QAAQ,KAAA,CAAS;GACnB,CAAC;GACD,MAAM,KAAK,SAAS,SAAS;IAC3B,QAAQ;KAAE,UAAU,QAAQ;KAAG;IAAO,CAAC;GACzC,CAAC;EACH,CAAC;CACH;AACF;;;AC1CA,MAAM,sBAAsB;AAC5B,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,yBAAyB;;AAG/B,SAAgB,uBAAuB,UAAsC;CAC3E,KAAK,MAAM,YAAY,2BAA2B;EAChD,MAAM,YAAY,KAAK,KAAK,UAAU,QAAQ;EAC9C,IAAI,GAAG,WAAW,SAAS,GAAG,OAAO;CACvC;AAEF;;;;;;;AAQA,eAAsB,uBACpB,QACgD;CAEhD,KAAI,MADgB,OAAO,QAAQ,qBAAqB,CAAC,WAAW,CAAC,EAAA,EAC1D,aAAa,GAAG,OAAO;EAAE,SAAS;EAAqB,QAAQ,CAAC;CAAE;CAC7E,OAAO;EAAE,SAAS;EAAY,QAAQ,CAAC,MAAM,wBAAwB;CAAE;AACzE;;;;;;;;;AAwBA,eAAsB,uBAAuB,SAAsD;CACjG,MAAM,MAAM,MAAM,uBAAuB,QAAQ,MAAM;CACvD,QAAQ,aAAa,sCAAsC;CAE3D,MAAM,SAAS,mBAAmB;EAChC,UAAU,QAAQ;EAClB,iBAAiB,QAAQ;EACzB,cAAc,QAAQ,kBAAkB,yBAAyB,KAAA;CACnE,CAAC;CACD,MAAM,KAAK,MAAM,QAAQ,OAAO,QAAQ,IAAI,SAAS,CAAC,GAAG,IAAI,QAAQ,GAAG,MAAM,CAAC;CAC/E,IAAI,CAAC,IACH,MAAM,IAAI,MAAM,iGAAiG;CAEnH,MAAM,EAAE,aAAa,uBAAuB,UAAU,oBAAoB,GAAG,MAAM;CACnF,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,oCAAoC,SAAS,QAAQ,GAAG,YAAY;CAEtG,MAAM,YAAY,MAAM,QAAQ,OAAO,IAAI,QAAQ,QAAQ;EACzD;EACA;EACA,GAAG,iBAAiB,QAAQ,OAAO;CACrC,CAAC;CACD,IAAI,cAAc,GAChB,MAAM,IAAI,MACR,4DAA4D,UAAU,iGAExE;CAGF,QAAQ,aAAa,8BAA8B,YAAY,MAAM,GAAG,EAAE,GAAG;CAC7E,OAAO,QAAQ,OAAO,IACpB,QAAQ,QACR,qBAAqB;EACnB;EACA,KAAK,wBACD,uBAAuB,QAAQ,UAAU,QAAQ,KAAK,qBAAqB,IAC3E,QAAQ;EACZ,aAAa,QAAQ;EACrB,QAAQ,QAAQ;EAChB,SAAS,CACP,iBACA,GAAI,wBACA,eAAe,QAAQ,aAAa,QAAQ,UAAU,qBAAqB,IAC3E,QAAQ,WACd;CACF,CAAC,CACH;AACF;;;AC3GA,MAAM,WAAW;AAEjB,SAAS,WAAW,MAAgC;CAClD,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,IAAI,aAAa;EAC/B,MAAM,KAAK,eAAe,QAAQ,KAAK,CAAC;EACxC,MAAM,OAAO,MAAM,gBAAgB;GACjC,MAAM,YAAY,QAAQ,IAAI,CAAC;EACjC,CAAC;CACH,CAAC;AACH;;;;;;;;AASA,eAAsB,oBAAoB,QAA2B,sBAAyC;CAC5G,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,QAAQ,OACjB,IAAI,MAAM,WAAW,IAAI,GAAG,KAAK,KAAK,IAAI;CAE5C,OAAO;AACT;;;;;;;;;;ACtBA,SAAgB,cAAc,YAA0C;CACtE,QAAQ,cAAc,GAAA,CAAI,MAAM,KAAK,CAAC,CAAC,QAAQ,SAAS,KAAK,SAAS,CAAC;AACzE;AAEA,MAAM,cAAc;;;;;;;;;AAUpB,SAAgB,eAAe,OAA0B,YAA0B;CACjF,MAAM,UAAU,MAAM,MAAM,SAAS,CAAC,YAAY,KAAK,IAAI,CAAC;CAC5D,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MACR,GAAG,WAAW,2DAA2D,QAAQ,gFAEnF;AAEJ;;;AC3BA,MAAM,mBAAmB;AACzB,MAAM,WAAW;;;;;;;;AASjB,SAAgB,eAAe,SAAiB,QAAwB;CACtE,OAAO,GAAG,iBAAiB,IAAI,QAAQ,WAAW,UAAU,GAAG,EAAE,GAAG;AACtE;;;;;;;;;;;;;;AAeA,SAAgB,oBAA4B;CAC1C,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ,iBAAiB,GAAG;EAC5B;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI;AACb;;;;;;;;;;;;;AAcA,SAAgB,oBAA4B;CAC1C,OAAO;EACL;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI;AACb;;;ACzEA,MAAM,gBAAgB;;AAGtB,SAAgB,gBAAgB,SAAyB;CAMvD,OAAO,eAAe,SALP,WAAW,QAAQ,CAAC,CAChC,OAAO,kBAAkB,CAAC,CAAC,CAC3B,OAAO,oBAAoB,CAAC,CAAC,CAC7B,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,aACmB,CAAM;AACvC;;;;;;;AAQA,SAAgB,gBAAgB,SAAyB;CACvD,MAAM,SAAS,WAAW,QAAQ,CAAC,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,aAAa;CACpG,OAAO,eAAe,GAAG,QAAQ,WAAW,MAAM;AACpD;;;ACLA,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AAEvB,MAAM,UAA2B;CAAC;CAAU;CAAU;CAAW;AAAO;AACxE,MAAM,kBAAkB;AACxB,MAAM,aAAa,KAAK,KAAK,SAAS,YAAY;AAClD,MAAM,wBAAwB;AAC9B,MAAM,kBAAkB;AAIxB,MAAM,UAAU,cAAc,YAAY,GAAG;AAC7C,MAAM,gBAAgB;AAgBtB,SAAS,sBAA8B;CACrC,OAAQ,QAAQ,aAAa,CAAC,CAAyB;AACzD;;AAGA,SAAS,gBAAgB,UAA0B;CACjD,OAAO,KAAK,KAAK,GAAG,OAAO,GAAG,GAAG,QAAQ,QAAQ,EAAE,QAAQ;AAC7D;AAEA,SAAS,QAAQ,UAA0B;CACzC,OAAO,kBAAkB,WAAW,QAAQ,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,eAAe;AACvG;AAEA,eAAsB,aACpB,QACA,aACwB;CACxB,MAAM,aAAa,YAAY,WAAW,EAAE,KAAK;CACjD,IAAI,YAAY;EACd,IAAK,QAAqB,SAAS,UAAU,GAAG,OAAO;EACvD,MAAM,IAAI,MAAM,GAAG,WAAW,mBAAmB,QAAQ,KAAK,IAAI,EAAE,EAAE;CACxE;CACA,KAAK,MAAM,UAAU,SAEnB,KAAI,MADgB,OAAO,QAAQ,QAAQ,CAAC,WAAW,CAAC,EAAA,EAC7C,aAAa,GAAG,OAAO;CAEpC,MAAM,IAAI,MAAM,6CAA6C,QAAQ,KAAK,IAAI,EAAE,WAAW,WAAW,EAAE;AAC1G;;;;;;;;;AAUA,eAAsB,YACpB,QACA,QACA,SACA,YACA,YACiB;CACjB,MAAM,MAAM,WAAW;CAEvB,KAAI,MADoB,OAAO,QAAQ,QAAQ;EAAC;EAAS;EAAW;CAAG,CAAC,EAAA,EACzD,aAAa,GAAG,OAAO;CAEtC,aAAa,YAAY,IAAI,qCAAqC;CAClE,MAAM,UAAU,GAAG,YAAY,KAAK,KAAK,GAAG,OAAO,GAAG,uBAAuB,CAAC;CAC9E,IAAI;EACF,GAAG,cAAc,KAAK,KAAK,SAAS,eAAe,GAAG,WAAW,UAAU;EAC3E,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,WAAW,SAAS,CAAC,CAAC,GAClE,GAAG,cAAc,KAAK,KAAK,SAAS,IAAI,GAAG,QAAQ;EAErD,MAAM,WAAW,MAAM,OAAO,IAAI,QAAQ;GACxC;GACA;GACA;GACA;GACA,kBAAkB;GAClB;EACF,CAAC;EACD,IAAI,aAAa,GAAG,MAAM,IAAI,MAAM,gBAAgB,IAAI,+BAA+B,SAAS,EAAE;CACpG,UAAU;EACR,GAAG,OAAO,SAAS;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACrD;CACA,OAAO;AACT;AAEA,SAAS,uBAAuB,UAAkB,YAA2D;CAC3G,IAAI;CACJ,IAAI;EACF,QAAQ,GAAG,aAAa,KAAK,KAAK,UAAU,UAAU,GAAG,MAAM;CACjE,QAAQ;EACN;CACF;CACA,IAAI,sBAAsB,KAAK,KAAK,GAClC,aACE,gJAEF;AAEJ;;AAGA,SAAgB,sBAAsB,eAA4C,CAAC,GAAG;CACpF,MAAM,SAAS,aAAa,UAAU,IAAI,yBAAyB;CACnE,MAAM,cAAc,aAAa,eAAe;CAChD,MAAM,oBAAoB,aAAa,cAAc;CACrD,OAAO,EACL,MAAM,cAAc,SAAgD;EAClE,MAAM,SAAS,MAAM,aAAa,QAAQ,QAAQ,WAAW;EAC7D,MAAM,WAAW,cAAc,QAAQ,YAAY,cAAc;EACjE,eAAe,UAAU,aAAa;EACtC,QAAQ,aAAa,SAAS,SAAS,SAAS,SAAS,IAAI,SAAS,SAAS,KAAK,GAAG,MAAM,IAAI;EAEjG,MAAM,UAAU,aAAa,WAAW,oBAAoB;EAI5D,MAAM,eACJ,QAAQ,YAAA,mCAA2C,iBAC/C,KAAA,IACA,uBAAuB,QAAQ,QAAQ;EAC7C,MAAM,WAAW,eACb,KAAA,IACA,MAAM,YAAY,QAAQ,QAAQ,SAAS,QAAQ,YAAY;GAC7D,KAAK,gBAAgB,OAAO;GAC5B,YAAY,kBAAkB;GAC9B,OAAO,GAAG,mBAAmB,oBAAoB,EAAE;EACrD,CAAC;EACL,IAAI,cAAc;GAChB,QAAQ,aAAa,0CAA0C,KAAK,SAAS,QAAQ,UAAU,YAAY,GAAG;GAC9G,QAAQ,aACN,2FACF;EACF,OACE,uBAAuB,QAAQ,UAAU,QAAQ,UAAU;EAG7D,MAAM,SACJ,QAAQ,YAAY,yBAAyB,iBACzC,KAAA,IACA,MAAM,YAAY;GAChB,aAAa,QAAQ;GACrB,WAAW,WAAW,QAAQ,aAAa,0BAA0B,QAAQ;GAG7E,GAAI,eAAe,EAAE,iBAAiB,gBAAgB,QAAQ,QAAQ,EAAE,IAAI,CAAC;EAC/E,CAAC;EACP,IAAI,QACF,QAAQ,aAAa,aAAa,OAAO,UAAU,KAAK,IAAI,EAAE,iCAAiC;EAGjG,MAAM,aAAa,eAAe,CAAC,IAAI,MAAM,kBAAkB;EAC/D,IAAI,CAAC,gBAAgB,WAAW,SAAS,qBAAqB,QAC5D,QAAQ,aACN,0FACF;EAGF,IAAI;GACF,MAAM,SAAS,QAAQ,MAAM,UAAU,QAAQ,QAAQ,OAAO,UAAU;GACxE,IAAI,cACF,OAAO,MAAM,uBAAuB;IAClC,UAAU,QAAQ;IAClB,KAAK,QAAQ;IACb,aAAa,QAAQ;IACrB,aAAa,qBAAqB,QAAQ,aAAa,MAAM;IAC7D;IACA;IACA;IACA;IACA,GAAI,QAAQ,SAAS,cAAc,SAAS,EAAE,iBAAiB,OAAO,SAAS,gBAAgB,IAAI,CAAC;IACpG,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;GACjE,CAAC;GAGH,MAAM,OAAyB;IAC7B;IACA,UAAU,QAAQ;IAClB,QAAQ,QAAQ,SAAS;IACzB,SAAS,QAAQ,SAAS;IAC1B,SAAS,QAAQ,QAAQ,QAAQ;IACjC;IACA,GAAG,aAAa;GAClB;GACA,MAAM,OAAO,iBAAiB;IAC5B,UAAU,QAAQ;IAClB,KAAK,QAAQ;IACb,aAAa,QAAQ;IACrB,aAAa,QAAQ;IACrB;IACA;IACU;IACV;IACA;IACA;GACF,CAAC;GACD,QAAQ,aAAa,iCAAiC,UAAU;GAChE,OAAO,MAAM,OAAO,IAAI,QAAQ,KAAK,OAAO;EAC9C,UAAU;GACR,MAAM,QAAQ,KAAK;EACrB;CACF,EACF;AACF;;AAGA,SAAgB,cAAc,SAAgD;CAC5E,OAAO,sBAAsB,CAAC,CAAC,cAAc,OAAO;AACtD"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const require_brokerHost = require("./brokerHost-
|
|
1
|
+
const require_brokerHost = require("./brokerHost-BzffXeb2.cjs");
|
|
2
2
|
const require_sandboxBridge = require("./sandboxBridge-B3FRZkC2.cjs");
|
|
3
3
|
let node_child_process = require("node:child_process");
|
|
4
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);
|
|
5
7
|
let node_crypto = require("node:crypto");
|
|
6
8
|
let node_fs = require("node:fs");
|
|
7
9
|
node_fs = require_brokerHost.__toESM(node_fs, 1);
|
|
8
10
|
let node_module = require("node:module");
|
|
9
11
|
let node_os = require("node:os");
|
|
10
12
|
node_os = require_brokerHost.__toESM(node_os, 1);
|
|
11
|
-
let node_path = require("node:path");
|
|
12
|
-
node_path = require_brokerHost.__toESM(node_path, 1);
|
|
13
13
|
let node_net = require("node:net");
|
|
14
14
|
node_net = require_brokerHost.__toESM(node_net, 1);
|
|
15
15
|
//#region src/services/devcontainer/index.ts
|
|
@@ -67,7 +67,8 @@ function parseDevcontainerUp(stdout) {
|
|
|
67
67
|
* inside and would fail the exec before the agent ever starts.
|
|
68
68
|
*/
|
|
69
69
|
function containerWorkspacePath(repoRoot, cwd, remoteWorkspaceFolder) {
|
|
70
|
-
const
|
|
70
|
+
const inside = node_path.default.relative(repoRoot, cwd);
|
|
71
|
+
const relative = inside === "" || inside.startsWith("..") || node_path.default.isAbsolute(inside) ? "" : inside;
|
|
71
72
|
return relative ? `${remoteWorkspaceFolder}/${relative.split("\\").join("/")}` : remoteWorkspaceFolder;
|
|
72
73
|
}
|
|
73
74
|
/**
|
|
@@ -781,4 +782,4 @@ Object.defineProperty(exports, "parseRunFlags", {
|
|
|
781
782
|
}
|
|
782
783
|
});
|
|
783
784
|
|
|
784
|
-
//# sourceMappingURL=harness-
|
|
785
|
+
//# sourceMappingURL=harness-fK8CSaU1.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-fK8CSaU1.cjs","names":["path","LAUNCHER_BINARY","BROKER_MOUNT_DIRECTORY","BROKER_SOCKET_ENV","BROKER_SOCKET_CONTAINER_PATH","BROKER_PORT_ENV","BROKER_CONTAINER_PORT","BROKER_ADDRESS_ENV","BROKER_HOST_GATEWAY","BROKER_PROVIDERS_ENV","DOOMPI_SANDBOX_ENV","BRIDGE_CONTAINER_PATH","spawn","path","fs","net","BRIDGE_FILE_NAME","BRIDGE_CONTAINER_PATH","createHash","sandboxBridgeSource","path","require","createRequire","os","createHash","fs","startBroker","BRIDGE_FILE_NAME","sandboxBridgeSource"],"sources":["../src/services/devcontainer/index.ts","../src/services/oauthCallback/index.ts","../src/services/sandboxEnvironment/index.ts","../src/services/sandboxPlan/index.ts","../src/services/engineProcess/index.ts","../src/services/devcontainer/runtime.ts","../src/services/loginPorts/index.ts","../src/services/runFlags/index.ts","../src/services/sandboxImage/index.ts","../src/services/sandboxImageTag/index.ts","../src/services/harness/index.ts"],"sourcesContent":["import path from 'node:path';\n\n/**\n * Relative locations the Dev Containers spec looks for a configuration.\n *\n * Ordered as the spec resolves them, so the first hit on disk wins.\n */\nexport const DEVCONTAINER_CONFIG_PATHS: readonly string[] = ['.devcontainer/devcontainer.json', '.devcontainer.json'];\n\nexport const DEVCONTAINER_DISABLED_ENV = 'DOOMPI_SANDBOX_DEVCONTAINER';\nexport const DEVCONTAINER_CLI_PACKAGE = '@devcontainers/cli@0.80.0';\n/** Marks containers this layer brought up, so a stale one can be told apart. */\nexport const DEVCONTAINER_ID_LABEL = 'doompi.sandbox.devcontainer';\n\nexport interface DevcontainerUpResult {\n containerId?: string;\n /** Where the CLI mounted the workspace inside the container. */\n remoteWorkspaceFolder?: string;\n error?: string;\n}\n\nexport interface DevcontainerUpOptions {\n repoRoot: string;\n /** Host directory holding the broker socket, bind-mounted when one is used. */\n socketDirectory?: string;\n /** Container path the socket directory is mounted at. */\n socketTarget?: string;\n}\n\n/** Arguments for bringing the workspace's own dev container up. */\nexport function devcontainerUpArgs(options: DevcontainerUpOptions): string[] {\n return [\n 'up',\n '--workspace-folder',\n options.repoRoot,\n '--log-format',\n 'json',\n ...(options.socketDirectory && options.socketTarget\n ? ['--mount', `type=bind,source=${options.socketDirectory},target=${options.socketTarget}`]\n : []),\n ];\n}\n\n/**\n * Reads the container id out of the CLI's JSON log stream.\n *\n * The stream carries progress records too, so the result is the last record\n * that reports an outcome rather than the first line parsed.\n */\nexport function parseDevcontainerUp(stdout: string): DevcontainerUpResult {\n let outcome:\n | { outcome?: string; containerId?: string; remoteWorkspaceFolder?: string; message?: string; description?: string }\n | undefined;\n for (const line of stdout.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('{')) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (typeof parsed === 'object' && parsed !== null && 'outcome' in parsed) {\n outcome = parsed as typeof outcome;\n }\n }\n if (!outcome) return { error: 'The dev container CLI reported no outcome.' };\n if (outcome.outcome !== 'success') {\n return { error: outcome.description || outcome.message || `Dev container start reported ${outcome.outcome}.` };\n }\n if (!outcome.containerId) return { error: 'The dev container started but reported no container id.' };\n return { containerId: outcome.containerId, remoteWorkspaceFolder: outcome.remoteWorkspaceFolder };\n}\n\n/**\n * Maps a host path into the dev container's own workspace.\n *\n * A dev container mounts the workspace where its configuration says, commonly\n * /workspaces/<name>, so the host path the launch was given does not exist\n * inside and would fail the exec before the agent ever starts.\n */\nexport function containerWorkspacePath(repoRoot: string, cwd: string, remoteWorkspaceFolder: string): string {\n // A plain prefix test would also map a sibling such as <repo>-other into the container.\n const inside = path.relative(repoRoot, cwd);\n const relative = inside === '' || inside.startsWith('..') || path.isAbsolute(inside) ? '' : inside;\n return relative ? `${remoteWorkspaceFolder}/${relative.split('\\\\').join('/')}` : remoteWorkspaceFolder;\n}\n\n/**\n * Installs the distribution inside a dev container that lacks it.\n *\n * The container is the workspace's own, so it carries the project toolchain\n * rather than this layer's, and nothing guarantees DoomPi is present. `up`\n * reuses an existing container, so this is paid once for its lifetime.\n */\nexport function bootstrapCommand(version: string): string[] {\n return [\n 'sh',\n '-c',\n `command -v doompi >/dev/null 2>&1 && exit 0; ` +\n `command -v npm >/dev/null 2>&1 || { echo \"doompi-sandbox: this dev container has no npm to install DoomPi with\" >&2; exit 127; }; ` +\n `npm install -g @agimon-ai/doompi@${version} >/dev/null`,\n ];\n}\n\n/**\n * Rewrites forwarded host paths into the dev container's workspace.\n *\n * The launcher replays its resolved options, and several carry absolute host\n * paths: --cwd always, plugin and additional directories when set. Those\n * resolve in the built-in image, which mounts the repository at the path it\n * has on the host, but a dev container mounts it wherever its configuration\n * says, and the session would fail looking for a repository that is not there.\n */\nexport function mapForwardArgs(args: readonly string[], repoRoot: string, remoteWorkspaceFolder: string): string[] {\n return args.map((argument) =>\n argument === repoRoot || argument.startsWith(`${repoRoot}/`)\n ? containerWorkspacePath(repoRoot, argument, remoteWorkspaceFolder)\n : argument,\n );\n}\n\nexport interface DevcontainerExecOptions {\n containerId: string;\n cwd: string;\n environment: Readonly<Record<string, string>>;\n hasTty: boolean;\n command: readonly string[];\n}\n\n/**\n * Runs a command in the dev container through the engine rather than the CLI.\n *\n * `devcontainer exec` allocates no terminal, which a full screen TUI needs, so\n * the session is attached to the container the CLI already started.\n */\nexport function devcontainerExecArgs(options: DevcontainerExecOptions): string[] {\n const environmentPairs = Object.entries(options.environment).sort(([left], [right]) => left.localeCompare(right));\n return [\n 'exec',\n '-i',\n ...(options.hasTty ? ['-t'] : []),\n '-w',\n options.cwd,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n options.containerId,\n ...options.command,\n ];\n}\n","/**\n * Fixed loopback ports Pi's OAuth flows listen on.\n *\n * A provider redirects the browser to `http://localhost:<port>`, so the host\n * port has to match exactly and cannot be remapped. Providers that bind an\n * ephemeral port instead, such as OpenRouter, cannot be published ahead of the\n * flow and are not covered.\n */\nexport const OAUTH_CALLBACK_PORTS: readonly number[] = [\n 1455, // openai-codex\n 1456, // radius\n 53692, // anthropic\n];\n\nexport const OAUTH_CALLBACK_HOST_ENV = 'PI_OAUTH_CALLBACK_HOST';\n\n/**\n * Address the in-container callback server binds.\n *\n * Pi binds loopback by default, which a published port cannot reach: the\n * engine forwards to the container's external interface, not its loopback.\n * Only the bind address changes; the redirect the provider sees stays\n * `localhost`.\n */\nexport const OAUTH_CONTAINER_BIND = '0.0.0.0';\n\n/** Publishes each reachable callback port back onto the host's loopback. */\nexport function oauthPublishArgs(ports: readonly number[]): string[] {\n return ports.flatMap((port) => ['-p', `127.0.0.1:${port}:${port}`]);\n}\n","const EXACT_NAMES = new Set([\n 'TERM',\n 'COLORTERM',\n 'LANG',\n 'TZ',\n 'DOOMPI_PRESET',\n 'ELICITATION_SESSION_ID',\n 'HTTP_PROXY',\n 'HTTPS_PROXY',\n 'NO_PROXY',\n 'http_proxy',\n 'https_proxy',\n 'no_proxy',\n]);\nconst NAME_PREFIXES = ['LC_'];\nconst NAME_SUFFIXES = ['_API_KEY', '_AUTH_TOKEN', '_BASE_URL'];\n\n/** True for variables shaped like a provider credential or endpoint override. */\nexport function isCredentialEnvName(name: string): boolean {\n return NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));\n}\n\nfunction allowed(name: string): boolean {\n return (\n EXACT_NAMES.has(name) ||\n NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) ||\n NAME_SUFFIXES.some((suffix) => name.endsWith(suffix))\n );\n}\n\n/**\n * Projects the host environment down to what a sandboxed session may see.\n *\n * The list is an allowlist on purpose: everything a terminal accumulates\n * (agents, sockets, tokens, paths) stays on the host unless a rule names it.\n * Provider credentials pass through for now; the credential broker planned for\n * the client-server phase removes even those from the container.\n */\nexport function filterSandboxEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n): Record<string, string> {\n const filtered: Record<string, string> = {};\n for (const [name, value] of Object.entries(environment)) {\n if (value !== undefined && allowed(name)) filtered[name] = value;\n }\n return filtered;\n}\n","import { DOOMPI_SANDBOX_ENV } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { BrokerEndpoint, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { OAUTH_CALLBACK_HOST_ENV, OAUTH_CONTAINER_BIND, oauthPublishArgs } from '../oauthCallback';\nimport {\n BRIDGE_CONTAINER_PATH,\n BROKER_ADDRESS_ENV,\n BROKER_CONTAINER_PORT,\n BROKER_HOST_GATEWAY,\n BROKER_PORT_ENV,\n BROKER_PROVIDERS_ENV,\n BROKER_SOCKET_CONTAINER_PATH,\n BROKER_SOCKET_ENV,\n} from '../sandboxBridge';\nimport { filterSandboxEnvironment, isCredentialEnvName } from '../sandboxEnvironment';\n\nconst CONTAINER_HOME = '/doompi-home';\nconst REPOSITORY_LABEL = 'doompi.sandbox.repo';\n// The harness binary, which accepts every forwarded flag; dpi hands its\n// arguments straight to Pi and would reject harness options like --preset.\nconst LAUNCHER_BINARY = 'doompi';\nconst NODE_BINARY = 'node';\nconst LINUX_PLATFORM = 'linux';\nconst ENABLED = '1';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Host broker facts the plan projects into mounts and environment. */\nexport interface SandboxPlanBroker {\n endpoint: BrokerEndpoint;\n token: string;\n providers: readonly string[];\n /** Credential variables whose value the container receives as the token. */\n withheldEnv: readonly string[];\n}\n\nexport interface SandboxPlanInput {\n repoRoot: string;\n cwd: string;\n forwardArgs: string[];\n environment: Readonly<Record<string, string | undefined>>;\n engine: SandboxEngine;\n host: SandboxHostFacts;\n /** Image the launch resolved, which also pins the image definition. */\n imageTag: string;\n /** Engine options the operator configured, such as an alternate runtime. */\n runFlags?: readonly string[];\n /** OAuth callback ports free on the host, published so a browser can reach them. */\n loginPorts?: readonly number[];\n broker?: SandboxPlanBroker;\n}\n\nexport interface SandboxPlan {\n imageTag: string;\n /** Full engine argv after the engine binary itself. */\n runArgs: string[];\n}\n\n/**\n * Replaces every credential in the container with the broker's session token.\n *\n * Dropping all credential-shaped variables first is what makes the promise\n * hold for providers the broker does not carry: an unbrokered key would\n * otherwise stay readable inside the container.\n */\nfunction brokeredEnvironment(filtered: Record<string, string>, broker: SandboxPlanBroker): Record<string, string> {\n const environment: Record<string, string> = {};\n for (const [name, value] of Object.entries(filtered)) {\n if (!isCredentialEnvName(name)) environment[name] = value;\n }\n for (const name of broker.withheldEnv) environment[name] = broker.token;\n if (broker.endpoint.transport === 'unix') {\n environment[BROKER_SOCKET_ENV] = BROKER_SOCKET_CONTAINER_PATH;\n // Only the bridge reads this, and only a socket needs the bridge.\n environment[BROKER_PORT_ENV] = String(BROKER_CONTAINER_PORT);\n } else {\n environment[BROKER_ADDRESS_ENV] = `${BROKER_HOST_GATEWAY}:${broker.endpoint.port}`;\n }\n environment[BROKER_PROVIDERS_ENV] = broker.providers.join(',');\n return environment;\n}\n\n/**\n * Grants the container the one route it needs to reach the broker.\n *\n * A mounted socket needs no network at all. TCP needs the engine's host\n * gateway named explicitly, because it is not resolvable by default on every\n * engine.\n */\nfunction brokerAccessArgs(broker: SandboxPlanBroker | undefined): string[] {\n if (!broker) return [];\n if (broker.endpoint.transport === 'unix') {\n return ['-v', `${broker.endpoint.socketDirectory}:${BROKER_MOUNT_DIRECTORY}`];\n }\n return ['--add-host', `${BROKER_HOST_GATEWAY}:host-gateway`];\n}\n\n/**\n * Projects one launch into a disposable engine run.\n *\n * The container is ephemeral; persistence lives in two named volumes: the\n * container home (Pi sessions, sync cache) and a shadow over the repository's\n * `.pi` store, which keeps Linux installs from corrupting the host's own\n * platform-specific packages on the shared workspace mount.\n */\n/**\n * The environment a sandboxed session runs with, whichever container hosts it.\n *\n * Shared with the dev container path, which builds no run arguments of its own\n * but must apply the same allowlist and the same credential substitution.\n */\nexport function containerEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n broker?: SandboxPlanBroker,\n loginPorts: readonly number[] = [],\n): Record<string, string> {\n const filtered = filterSandboxEnvironment(environment);\n const resolved = broker ? brokeredEnvironment(filtered, broker) : filtered;\n resolved[DOOMPI_SANDBOX_ENV] = ENABLED;\n if (loginPorts.length > 0) resolved[OAUTH_CALLBACK_HOST_ENV] = OAUTH_CONTAINER_BIND;\n return resolved;\n}\n\nexport function buildSandboxPlan(input: SandboxPlanInput): SandboxPlan {\n const { repoRoot, cwd, engine, host, broker, imageTag } = input;\n const loginPorts = input.loginPorts ?? [];\n const environmentPairs = Object.entries(containerEnvironment(input.environment, broker, loginPorts)).sort(\n ([left], [right]) => left.localeCompare(right),\n );\n\n const runArgs = [\n 'run',\n '--rm',\n '-i',\n ...(host.hasTty ? ['-t'] : []),\n '--label',\n `${REPOSITORY_LABEL}=${repoRoot}`,\n '-v',\n `${repoRoot}:${repoRoot}`,\n '-v',\n `${host.repoKey}-home:${CONTAINER_HOME}`,\n '-v',\n `${host.repoKey}-pi:${repoRoot}/.pi`,\n ...brokerAccessArgs(broker),\n ...oauthPublishArgs(loginPorts),\n '-w',\n cwd,\n '-e',\n `HOME=${CONTAINER_HOME}`,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n ...(host.platform === LINUX_PLATFORM && host.userId !== undefined\n ? [\n '--user',\n `${host.userId}:${host.groupId ?? host.userId}`,\n ...(engine === 'podman' ? ['--userns=keep-id'] : []),\n ]\n : []),\n // Last before the image so a configured option wins over the defaults\n // above it, and cannot be mistaken for the image or its command.\n ...(input.runFlags ?? []),\n imageTag,\n // Only a mounted socket needs the bridge: a broker on a host port is\n // addressed directly, so the launcher runs unwrapped.\n ...(broker?.endpoint.transport === 'unix' ? [NODE_BINARY, BRIDGE_CONTAINER_PATH] : []),\n LAUNCHER_BINARY,\n ...input.forwardArgs,\n ];\n\n return { imageTag, runArgs };\n}\n","import { spawn } from 'node:child_process';\n\nimport type { EngineCaptureResult, EngineProcessRunner, EngineRunOptions } from '../../types/sandboxHarness';\n\nconst INHERIT = 'inherit';\nconst PIPE = 'pipe';\nconst IGNORE = 'ignore';\nconst FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];\n\n/** Spawns the container engine, keeping the session's terminal attached. */\nexport class SpawnEngineProcessRunner implements EngineProcessRunner {\n run(command: string, args: string[], options: EngineRunOptions = {}): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options.input === undefined ? INHERIT : [PIPE, INHERIT, INHERIT],\n });\n if (options.input !== undefined) child.stdin?.end(options.input);\n\n // With a shared terminal the engine sees Ctrl-C itself; forwarding covers\n // detached automation runs, and a duplicate signal is harmless.\n const forwarders = FORWARDED_SIGNALS.map((signal) => {\n const forward = (): void => {\n child.kill(signal);\n };\n process.on(signal, forward);\n return { signal, forward };\n });\n const cleanup = (): void => {\n for (const { signal, forward } of forwarders) process.off(signal, forward);\n };\n\n child.once('error', (error) => {\n cleanup();\n reject(error);\n });\n child.once('exit', (code, signal) => {\n cleanup();\n resolve(code ?? (signal ? 1 : 0));\n });\n });\n }\n\n capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined> {\n return new Promise((resolve) => {\n const child = spawn(command, args, { stdio: [IGNORE, PIPE, IGNORE] });\n let stdout = '';\n child.stdout?.on('data', (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.once('error', () => {\n resolve(undefined);\n });\n child.once('exit', (code) => {\n resolve({ exitCode: code ?? 1, stdout });\n });\n });\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n bootstrapCommand,\n containerWorkspacePath,\n mapForwardArgs,\n DEVCONTAINER_CLI_PACKAGE,\n DEVCONTAINER_CONFIG_PATHS,\n devcontainerExecArgs,\n devcontainerUpArgs,\n parseDevcontainerUp,\n} from '.';\nimport type { EngineProcessRunner, SandboxEngine } from '../../types/sandboxHarness';\n\nconst DEVCONTAINER_BINARY = 'devcontainer';\nconst NPX_BINARY = 'npx';\nconst LAUNCHER_BINARY = 'doompi';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Locates the workspace's own dev container configuration, if it has one. */\nexport function findDevcontainerConfig(repoRoot: string): string | undefined {\n for (const relative of DEVCONTAINER_CONFIG_PATHS) {\n const candidate = path.join(repoRoot, relative);\n if (fs.existsSync(candidate)) return candidate;\n }\n return undefined;\n}\n\n/**\n * Resolves how to invoke the Dev Containers CLI.\n *\n * A local install is preferred so a launch does not depend on the network;\n * npx is the fallback, pinned so the tool cannot change underneath a session.\n */\nexport async function resolveDevcontainerCli(\n runner: EngineProcessRunner,\n): Promise<{ command: string; prefix: string[] }> {\n const probe = await runner.capture(DEVCONTAINER_BINARY, ['--version']);\n if (probe?.exitCode === 0) return { command: DEVCONTAINER_BINARY, prefix: [] };\n return { command: NPX_BINARY, prefix: ['-y', DEVCONTAINER_CLI_PACKAGE] };\n}\n\nexport interface DevcontainerSessionOptions {\n repoRoot: string;\n cwd: string;\n forwardArgs: readonly string[];\n environment: Readonly<Record<string, string>>;\n engine: SandboxEngine;\n runner: EngineProcessRunner;\n version: string;\n hasTty: boolean;\n /** Host directory holding the broker socket, when the broker uses one. */\n socketDirectory?: string;\n onProgress?: (message: string) => void;\n}\n\n/**\n * Runs the session inside the workspace's own dev container.\n *\n * The container is brought up by the Dev Containers CLI, so the file decides\n * the image, features, mounts and lifecycle hooks: the session gets the\n * project's real toolchain, and this layer's isolation guarantees do not\n * apply beyond the environment it passes in.\n */\nexport async function runDevcontainerSession(options: DevcontainerSessionOptions): Promise<number> {\n const cli = await resolveDevcontainerCli(options.runner);\n options.onProgress?.('starting the workspace dev container');\n\n const upArgs = devcontainerUpArgs({\n repoRoot: options.repoRoot,\n socketDirectory: options.socketDirectory,\n socketTarget: options.socketDirectory ? BROKER_MOUNT_DIRECTORY : undefined,\n });\n const up = await options.runner.capture(cli.command, [...cli.prefix, ...upArgs]);\n if (!up) {\n throw new Error('The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.');\n }\n const { containerId, remoteWorkspaceFolder, error } = parseDevcontainerUp(up.stdout);\n if (!containerId) throw new Error(`The dev container did not start: ${error ?? `exit ${up.exitCode}`}`);\n\n const bootstrap = await options.runner.run(options.engine, [\n 'exec',\n containerId,\n ...bootstrapCommand(options.version),\n ]);\n if (bootstrap !== 0) {\n throw new Error(\n `DoomPi could not be installed in the dev container (exit ${bootstrap}). ` +\n 'Add it to the dev container, or unset the dev container preference to use the built-in image.',\n );\n }\n\n options.onProgress?.(`attaching to dev container ${containerId.slice(0, 12)}`);\n return options.runner.run(\n options.engine,\n devcontainerExecArgs({\n containerId,\n cwd: remoteWorkspaceFolder\n ? containerWorkspacePath(options.repoRoot, options.cwd, remoteWorkspaceFolder)\n : options.cwd,\n environment: options.environment,\n hasTty: options.hasTty,\n command: [\n LAUNCHER_BINARY,\n ...(remoteWorkspaceFolder\n ? mapForwardArgs(options.forwardArgs, options.repoRoot, remoteWorkspaceFolder)\n : options.forwardArgs),\n ],\n }),\n );\n}\n","import net from 'node:net';\n\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\n\nconst LOOPBACK = '127.0.0.1';\n\nfunction portIsFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = net.createServer();\n probe.once('error', () => resolve(false));\n probe.listen(port, LOOPBACK, () => {\n probe.close(() => resolve(true));\n });\n });\n}\n\n/**\n * Reports which OAuth callback ports this launch may publish.\n *\n * A taken port means another sandbox already holds it, so publishing would\n * fail the whole launch over a login the session may never attempt. Skipping\n * it costs only the ability to log in from inside this particular container.\n */\nexport async function availableLoginPorts(ports: readonly number[] = OAUTH_CALLBACK_PORTS): Promise<number[]> {\n const free: number[] = [];\n for (const port of ports) {\n if (await portIsFree(port)) free.push(port);\n }\n return free;\n}\n","/**\n * Splits configured engine options into argv entries.\n *\n * Whitespace separated rather than shell parsed: the engine is spawned without\n * a shell, so quoting rules would only invent a syntax the caller does not\n * actually get.\n */\nexport function parseRunFlags(configured: string | undefined): string[] {\n return (configured ?? '').split(/\\s+/).filter((flag) => flag.length > 0);\n}\n\nconst OPTION_FORM = /^--?[^\\s=]+(=.*)?$/s;\n\n/**\n * Requires every configured entry to be a self-contained option.\n *\n * Separated values cannot be told from positional arguments without knowing\n * each option's arity, and a stray positional would silently replace the image\n * or its command. Demanding `--flag=value` removes the ambiguity instead of\n * guessing at it.\n */\nexport function assertRunFlags(flags: readonly string[], optionName: string): void {\n const invalid = flags.find((flag) => !OPTION_FORM.test(flag));\n if (invalid !== undefined) {\n throw new Error(\n `${optionName} accepts engine options in --flag or --flag=value form; \"${invalid}\" is neither. ` +\n 'Write a value as part of its option, for example --runtime=runsc.',\n );\n }\n}\n","import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from '../sandboxBridge';\n\nconst IMAGE_REPOSITORY = 'doompi-sandbox';\nconst TAG_SAFE = /[^A-Za-z0-9_.-]/g;\n\n/**\n * Names one image build: a distribution version plus its image definition.\n *\n * The digest matters as much as the version. Editing the Dockerfile or the\n * bridge without it would silently reuse a cached image that predates the\n * change.\n */\nexport function formatImageTag(version: string, digest: string): string {\n return `${IMAGE_REPOSITORY}:v${version.replaceAll(TAG_SAFE, '-')}-${digest}`;\n}\n\n/**\n * Self-contained Linux image for sandboxed sessions.\n *\n * nano is present because Pi falls back to it for the external editor when\n * VISUAL and EDITOR are unset, which is always the case here: a host editor\n * variable naming a desktop application would resolve to a binary the\n * container does not have, so it is deliberately not forwarded.\n *\n * The distribution installs from the registry rather than mounting host\n * modules: host installs carry platform-specific binaries a Linux container\n * cannot run. DOOMPI_SANDBOX is baked in so nothing that reaches a shell in\n * the container can present itself as an unsandboxed session.\n */\nexport function sandboxDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi@${DOOMPI_VERSION}',\n `COPY ${BRIDGE_FILE_NAME} ${BRIDGE_CONTAINER_PATH}`,\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n\n/**\n * Image for the cockpit container, which holds the hub rather than one session.\n *\n * Kept separate from the sandbox image rather than folded into it. The cockpit\n * needs the web package and a tunnel client, and making every interactive\n * `--sandbox` launch pay for an extra apt repository and a larger download to\n * get them would be a poor trade.\n *\n * Installing `@agimon-ai/doompi-web` is enough for all three programs: its\n * `@agimon-ai/doompi` dependency owns both the session server executable and\n * the agent runtime.\n */\nexport function cockpitDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n // The tunnel runs beside the hub rather than on the host, so the cockpit\n // owns its own remote access and all of that state stays in one place.\n 'RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\\\',\n ' -o /usr/share/keyrings/cloudflare-main.gpg \\\\',\n ' && echo \"deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main\" \\\\',\n ' > /etc/apt/sources.list.d/cloudflared.list \\\\',\n ' && apt-get update && apt-get install -y --no-install-recommends cloudflared \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi-web@${DOOMPI_VERSION}',\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n","import { createHash } from 'node:crypto';\n\nimport { sandboxBridgeSource } from '../sandboxBridge';\nimport { cockpitDockerfile, formatImageTag, sandboxDockerfile } from '../sandboxImage';\n\nconst DIGEST_LENGTH = 8;\n\n/** Tags the image with the distribution version and a digest of its definition. */\nexport function sandboxImageTag(version: string): string {\n const digest = createHash('sha256')\n .update(sandboxDockerfile())\n .update(sandboxBridgeSource())\n .digest('hex')\n .slice(0, DIGEST_LENGTH);\n return formatImageTag(version, digest);\n}\n\n/**\n * Tags the cockpit image separately from the sandbox one.\n *\n * Same digest scheme, different definition, so editing either Dockerfile\n * invalidates only its own image.\n */\nexport function cockpitImageTag(version: string): string {\n const digest = createHash('sha256').update(cockpitDockerfile()).digest('hex').slice(0, DIGEST_LENGTH);\n return formatImageTag(`${version}-cockpit`, digest);\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { startBroker, type RunningBroker } from '../brokerHost';\nimport { DEVCONTAINER_DISABLED_ENV } from '../devcontainer';\nimport { findDevcontainerConfig, runDevcontainerSession } from '../devcontainer/runtime';\nimport { SpawnEngineProcessRunner } from '../engineProcess';\nimport { availableLoginPorts } from '../loginPorts';\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\nimport { assertRunFlags, parseRunFlags } from '../runFlags';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../sandboxBridge';\nimport { sandboxDockerfile } from '../sandboxImage';\nimport { sandboxImageTag } from '../sandboxImageTag';\nimport { buildSandboxPlan, containerEnvironment } from '../sandboxPlan';\n\nconst ENGINE_ENV = 'DOOMPI_SANDBOX_ENGINE';\nconst BROKER_DISABLED_ENV = 'DOOMPI_SANDBOX_BROKER';\nconst RUN_FLAGS_ENV = 'DOOMPI_SANDBOX_RUN_FLAGS';\nconst DISABLED_VALUE = '0';\n// Every entry takes docker's run syntax, which is what the plan emits.\nconst ENGINES: SandboxEngine[] = ['docker', 'podman', 'nerdctl', 'finch'];\nconst DOCKERFILE_NAME = 'Dockerfile';\nconst MODES_FILE = path.join('.doom', 'modes.yaml');\nconst LOCAL_PACKAGE_PATTERN = /^\\s*(?:-|name:)\\s*[\"']?\\.{1,2}\\//m;\nconst REPO_KEY_LENGTH = 12;\n\n// Self-reference instead of a relative path: the file depth differs between\n// running from src and from the built dist tree.\nconst require = createRequire(import.meta.url);\nconst SELF_MANIFEST = '@agimon-ai/doompi-sandbox/package.json';\n\nexport interface SandboxLauncherDependencies {\n runner?: EngineProcessRunner;\n /** Seam for tests; defaults to probing the real OAuth callback ports. */\n loginPorts?: () => Promise<number[]>;\n /** Seam for tests; defaults to the real host broker. */\n startBroker?: (options: {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n }) => Promise<RunningBroker | undefined>;\n /** Distribution version override; defaults to this package's own version. */\n version?: string;\n hostFacts?: Partial<SandboxHostFacts>;\n}\n\nfunction distributionVersion(): string {\n return (require(SELF_MANIFEST) as { version: string }).version;\n}\n\n/** Fixed per-repository directory a reused dev container can keep mounted. */\nfunction brokerDirectory(repoRoot: string): string {\n return path.join(os.tmpdir(), `${repoKey(repoRoot)}-broker`);\n}\n\nfunction repoKey(repoRoot: string): string {\n return `doompi-sandbox-${createHash('sha256').update(repoRoot).digest('hex').slice(0, REPO_KEY_LENGTH)}`;\n}\n\nexport async function detectEngine(\n runner: EngineProcessRunner,\n environment: Readonly<Record<string, string | undefined>>,\n): Promise<SandboxEngine> {\n const configured = environment[ENGINE_ENV]?.trim();\n if (configured) {\n if ((ENGINES as string[]).includes(configured)) return configured as SandboxEngine;\n throw new Error(`${ENGINE_ENV} must be one of: ${ENGINES.join(', ')}.`);\n }\n for (const engine of ENGINES) {\n const probe = await runner.capture(engine, ['--version']);\n if (probe?.exitCode === 0) return engine;\n }\n throw new Error(`No container engine found. Install one of ${ENGINES.join(', ')}, or set ${ENGINE_ENV}.`);\n}\n\n/**\n * Builds the image if the engine does not already have it.\n *\n * Takes its definition rather than assuming one, because the interactive\n * sandbox and the cockpit container are two different images built the same\n * way. The tag already carries a digest of the definition, so a changed\n * Dockerfile misses the inspect and rebuilds.\n */\nexport async function ensureImage(\n runner: EngineProcessRunner,\n engine: SandboxEngine,\n version: string,\n onProgress: ((message: string) => void) | undefined,\n definition: { tag: string; dockerfile: string; files?: Readonly<Record<string, string>> },\n): Promise<string> {\n const tag = definition.tag;\n const inspected = await runner.capture(engine, ['image', 'inspect', tag]);\n if (inspected?.exitCode === 0) return tag;\n\n onProgress?.(`building ${tag} (first run for this DoomPi version)`);\n const context = fs.mkdtempSync(path.join(os.tmpdir(), 'doompi-sandbox-build-'));\n try {\n fs.writeFileSync(path.join(context, DOCKERFILE_NAME), definition.dockerfile);\n for (const [name, contents] of Object.entries(definition.files ?? {})) {\n fs.writeFileSync(path.join(context, name), contents);\n }\n const exitCode = await runner.run(engine, [\n 'build',\n '-t',\n tag,\n '--build-arg',\n `DOOMPI_VERSION=${version}`,\n context,\n ]);\n if (exitCode !== 0) throw new Error(`Building the ${tag} image failed with exit code ${exitCode}.`);\n } finally {\n fs.rmSync(context, { recursive: true, force: true });\n }\n return tag;\n}\n\nfunction warnAboutLocalPackages(repoRoot: string, onProgress: ((message: string) => void) | undefined): void {\n let modes: string;\n try {\n modes = fs.readFileSync(path.join(repoRoot, MODES_FILE), 'utf8');\n } catch {\n return;\n }\n if (LOCAL_PACKAGE_PATTERN.test(modes)) {\n onProgress?.(\n 'modes.yaml declares local workspace packages; their platform-specific dependencies ' +\n 'must be installed inside the container before they can load',\n );\n }\n}\n\n/** Builds the launcher with injectable seams for tests. */\nexport function createSandboxLauncher(dependencies: SandboxLauncherDependencies = {}) {\n const runner = dependencies.runner ?? new SpawnEngineProcessRunner();\n const beginBroker = dependencies.startBroker ?? startBroker;\n const resolveLoginPorts = dependencies.loginPorts ?? availableLoginPorts;\n return {\n async launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n const engine = await detectEngine(runner, request.environment);\n const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);\n assertRunFlags(runFlags, RUN_FLAGS_ENV);\n request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(' ')}` : ''}`);\n\n const version = dependencies.version ?? distributionVersion();\n // A workspace that describes its own container is describing the\n // toolchain its agent needs, so that container wins over the built-in\n // image. Nothing here is built or pulled until that choice is made.\n const devcontainer =\n request.environment[DEVCONTAINER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : findDevcontainerConfig(request.repoRoot);\n const imageTag = devcontainer\n ? undefined\n : await ensureImage(runner, engine, version, request.onProgress, {\n tag: sandboxImageTag(version),\n dockerfile: sandboxDockerfile(),\n files: { [BRIDGE_FILE_NAME]: sandboxBridgeSource() },\n });\n if (devcontainer) {\n request.onProgress?.(`using the workspace dev container from ${path.relative(request.repoRoot, devcontainer)}`);\n request.onProgress?.(\n 'its configuration owns the mounts and run arguments, so this is not an isolation boundary',\n );\n } else {\n warnAboutLocalPackages(request.repoRoot, request.onProgress);\n }\n\n const broker =\n request.environment[BROKER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : await beginBroker({\n environment: request.environment,\n onDenied: (reason) => request.onProgress?.(`broker refused a call: ${reason}`),\n // A reused dev container keeps the mounts it was created with,\n // so the socket has to be at the same path on every launch.\n ...(devcontainer ? { socketDirectory: brokerDirectory(request.repoRoot) } : {}),\n });\n if (broker) {\n request.onProgress?.(`brokering ${broker.providers.join(', ')}; provider keys stay on the host`);\n }\n\n const loginPorts = devcontainer ? [] : await resolveLoginPorts();\n if (!devcontainer && loginPorts.length < OAUTH_CALLBACK_PORTS.length) {\n request.onProgress?.(\n 'some OAuth callback ports are already taken; /login inside this sandbox may not complete',\n );\n }\n\n try {\n const hasTty = process.stdin.isTTY === true && process.stdout.isTTY === true;\n if (devcontainer) {\n return await runDevcontainerSession({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: containerEnvironment(request.environment, broker),\n engine,\n runner,\n version,\n hasTty,\n ...(broker?.endpoint.transport === 'unix' ? { socketDirectory: broker.endpoint.socketDirectory } : {}),\n ...(request.onProgress ? { onProgress: request.onProgress } : {}),\n });\n }\n\n const host: SandboxHostFacts = {\n hasTty,\n platform: process.platform,\n userId: process.getuid?.(),\n groupId: process.getgid?.(),\n repoKey: repoKey(request.repoRoot),\n version,\n ...dependencies.hostFacts,\n };\n const plan = buildSandboxPlan({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: request.environment,\n engine,\n host,\n imageTag: imageTag as string,\n runFlags,\n loginPorts,\n broker,\n });\n request.onProgress?.(`starting contained session in ${imageTag}`);\n return await runner.run(engine, plan.runArgs);\n } finally {\n await broker?.stop();\n }\n },\n };\n}\n\n/** Contract entry the DoomPi harness resolves for `--sandbox`. */\nexport function launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n return createSandboxLauncher().launchSandbox(request);\n}\n"],"mappings":"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"}
|
package/dist/sandboxHarness.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_harness = require("./harness-
|
|
2
|
+
const require_harness = require("./harness-fK8CSaU1.cjs");
|
|
3
3
|
exports.createSandboxLauncher = require_harness.createSandboxLauncher;
|
|
4
4
|
exports.launchSandbox = require_harness.launchSandbox;
|
package/dist/sandboxHarness.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as launchSandbox, t as createSandboxLauncher } from "./harness-
|
|
1
|
+
import { i as launchSandbox, t as createSandboxLauncher } from "./harness-CEuhOTr2.mjs";
|
|
2
2
|
export { createSandboxLauncher, launchSandbox };
|
|
@@ -52,6 +52,13 @@ function createSandboxSkillResource(root) {
|
|
|
52
52
|
if (!root.service) return void 0;
|
|
53
53
|
return {
|
|
54
54
|
name: "doompi-use-sandbox",
|
|
55
|
+
when: {
|
|
56
|
+
state: { "minor-mode": "help" },
|
|
57
|
+
attribution: {
|
|
58
|
+
kind: "minor",
|
|
59
|
+
mode: "help"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
55
62
|
kind: "skill",
|
|
56
63
|
read: () => readPackageResource(import.meta.url, "src/prompts/doompi-use-sandbox/SKILL.md")
|
|
57
64
|
};
|
|
@@ -69,4 +76,4 @@ function createSandboxHelpResource(moduleUrl) {
|
|
|
69
76
|
//#endregion
|
|
70
77
|
export { createSandboxServerCommands as a, createSandboxCliCommand as i, createSandboxHelpResource as n, BROKER_DISABLED_ENV as o, createSandboxSkillResource as r, BROKER_STATUS_SOURCE as s, createSandboxContextResource as t };
|
|
71
78
|
|
|
72
|
-
//# sourceMappingURL=sandboxResources-
|
|
79
|
+
//# sourceMappingURL=sandboxResources-5pvXyBW9.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxResources-
|
|
1
|
+
{"version":3,"file":"sandboxResources-5pvXyBW9.mjs","names":[],"sources":["../src/constants/sandbox.ts","../src/extensions/workspaces/sessions/(backend)/_lib/sandboxCommands.ts","../src/extensions/workspaces/sessions/(backend)/_lib/sandboxResources.ts"],"sourcesContent":["export const COMMAND_NAME = 'doom-sandbox';\nexport const COMMAND_DESCRIPTION = 'Show sandbox container status for this session';\n\nexport const BROKER_DISABLED_ENV = 'DOOMPI_SANDBOX_BROKER';\nexport const DISABLED_VALUE = '0';\nexport const BROKER_STATUS_SOURCE = 'doompi-sandbox-broker';\n","import type { WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport { defineCommand } from '@agimon-ai/doompi-core/piExtension';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\n\nimport { COMMAND_DESCRIPTION, COMMAND_NAME } from '../../../../../constants/sandbox';\nimport type { SandboxExtensionService } from '../../../../../types/extension';\nimport type { SandboxServerScope } from './sandboxScope';\n\nexport function createSandboxCliCommand(\n service: SandboxExtensionService,\n): readonly [string, Parameters<ExtensionAPI['registerCommand']>[1]] {\n return [\n COMMAND_NAME,\n {\n description: COMMAND_DESCRIPTION,\n async handler(_args, context) {\n const result = await service.execute();\n if (context.hasUI) context.ui.notify(result.message, result.level);\n },\n },\n ];\n}\n\nexport function createSandboxServerCommands(context: WithRoot<DoomServerPluginContext, SandboxServerScope>) {\n const service = context.root.service;\n if (!service) return [];\n return [\n defineCommand({\n name: COMMAND_NAME,\n description: COMMAND_DESCRIPTION,\n async execute() {\n const result = await service.execute();\n await context.agent?.context.client.notify({ body: result.message, level: result.level });\n },\n }),\n ];\n}\n","import type { DoomHeadlessResource } from '@agimon-ai/doompi-core/headless';\nimport { readPackageResource } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { SandboxServerScope } from './sandboxScope';\n\nexport function createSandboxContextResource(root: SandboxServerScope): DoomHeadlessResource | undefined {\n if (!root.service) return undefined;\n return {\n // The package index is Help-catalog material, not standing instruction. The Pi\n // facet has always routed it through the Help service; this is the server's gate.\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n name: 'doompi-sandbox',\n kind: 'context',\n read: () => readPackageResource(import.meta.url, 'llms.txt'),\n };\n}\n\nexport function createSandboxSkillResource(root: SandboxServerScope): DoomHeadlessResource | undefined {\n if (!root.service) return undefined;\n return {\n name: 'doompi-use-sandbox',\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n kind: 'skill',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-sandbox/SKILL.md'),\n };\n}\n\nexport function createSandboxHelpResource(moduleUrl: string) {\n return {\n source: '@agimon-ai/doompi-sandbox',\n moduleUrl,\n skills: [\n {\n name: 'doompi-use-sandbox',\n description:\n 'Use @agimon-ai/doompi-sandbox: Container sandbox for DoomPi launches: the agent, extensions, and tools run inside Docker or Podman while the terminal stays on the host',\n },\n ],\n };\n}\n"],"mappings":";;;AAAA,MAAa,eAAe;AAC5B,MAAa,sBAAsB;AAEnC,MAAa,sBAAsB;AAEnC,MAAa,uBAAuB;;;ACIpC,SAAgB,wBACd,SACmE;CACnE,OAAO,CACL,cACA;EACE,aAAa;EACb,MAAM,QAAQ,OAAO,SAAS;GAC5B,MAAM,SAAS,MAAM,QAAQ,QAAQ;GACrC,IAAI,QAAQ,OAAO,QAAQ,GAAG,OAAO,OAAO,SAAS,OAAO,KAAK;EACnE;CACF,CACF;AACF;AAEA,SAAgB,4BAA4B,SAAgE;CAC1G,MAAM,UAAU,QAAQ,KAAK;CAC7B,IAAI,CAAC,SAAS,OAAO,CAAC;CACtB,OAAO,CACL,cAAc;EACZ,MAAM;EACN,aAAa;EACb,MAAM,UAAU;GACd,MAAM,SAAS,MAAM,QAAQ,QAAQ;GACrC,MAAM,QAAQ,OAAO,QAAQ,OAAO,OAAO;IAAE,MAAM,OAAO;IAAS,OAAO,OAAO;GAAM,CAAC;EAC1F;CACF,CAAC,CACH;AACF;;;AChCA,SAAgB,6BAA6B,MAA4D;CACvG,IAAI,CAAC,KAAK,SAAS,OAAO,KAAA;CAC1B,OAAO;EAGL,MAAM;GAAE,OAAO,EAAE,cAAc,OAAO;GAAG,aAAa;IAAE,MAAM;IAAS,MAAM;GAAO;EAAE;EACtF,MAAM;EACN,MAAM;EACN,YAAY,oBAAoB,YAAY,KAAK,UAAU;CAC7D;AACF;AAEA,SAAgB,2BAA2B,MAA4D;CACrG,IAAI,CAAC,KAAK,SAAS,OAAO,KAAA;CAC1B,OAAO;EACL,MAAM;EACN,MAAM;GAAE,OAAO,EAAE,cAAc,OAAO;GAAG,aAAa;IAAE,MAAM;IAAS,MAAM;GAAO;EAAE;EACtF,MAAM;EACN,YAAY,oBAAoB,YAAY,KAAK,yCAAyC;CAC5F;AACF;AAEA,SAAgB,0BAA0B,WAAmB;CAC3D,OAAO;EACL,QAAQ;EACR;EACA,QAAQ,CACN;GACE,MAAM;GACN,aACE;EACJ,CACF;CACF;AACF"}
|
|
@@ -52,6 +52,13 @@ function createSandboxSkillResource(root) {
|
|
|
52
52
|
if (!root.service) return void 0;
|
|
53
53
|
return {
|
|
54
54
|
name: "doompi-use-sandbox",
|
|
55
|
+
when: {
|
|
56
|
+
state: { "minor-mode": "help" },
|
|
57
|
+
attribution: {
|
|
58
|
+
kind: "minor",
|
|
59
|
+
mode: "help"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
55
62
|
kind: "skill",
|
|
56
63
|
read: () => (0, _agimon_ai_doompi_core_serverFacet.readPackageResource)(require("url").pathToFileURL(__filename).href, "src/prompts/doompi-use-sandbox/SKILL.md")
|
|
57
64
|
};
|
|
@@ -110,4 +117,4 @@ Object.defineProperty(exports, "createSandboxSkillResource", {
|
|
|
110
117
|
}
|
|
111
118
|
});
|
|
112
119
|
|
|
113
|
-
//# sourceMappingURL=sandboxResources-
|
|
120
|
+
//# sourceMappingURL=sandboxResources-Djxj5l1V.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxResources-
|
|
1
|
+
{"version":3,"file":"sandboxResources-Djxj5l1V.cjs","names":["defineCommand","readPackageResource"],"sources":["../src/constants/sandbox.ts","../src/extensions/workspaces/sessions/(backend)/_lib/sandboxCommands.ts","../src/extensions/workspaces/sessions/(backend)/_lib/sandboxResources.ts"],"sourcesContent":["export const COMMAND_NAME = 'doom-sandbox';\nexport const COMMAND_DESCRIPTION = 'Show sandbox container status for this session';\n\nexport const BROKER_DISABLED_ENV = 'DOOMPI_SANDBOX_BROKER';\nexport const DISABLED_VALUE = '0';\nexport const BROKER_STATUS_SOURCE = 'doompi-sandbox-broker';\n","import type { WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport { defineCommand } from '@agimon-ai/doompi-core/piExtension';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\n\nimport { COMMAND_DESCRIPTION, COMMAND_NAME } from '../../../../../constants/sandbox';\nimport type { SandboxExtensionService } from '../../../../../types/extension';\nimport type { SandboxServerScope } from './sandboxScope';\n\nexport function createSandboxCliCommand(\n service: SandboxExtensionService,\n): readonly [string, Parameters<ExtensionAPI['registerCommand']>[1]] {\n return [\n COMMAND_NAME,\n {\n description: COMMAND_DESCRIPTION,\n async handler(_args, context) {\n const result = await service.execute();\n if (context.hasUI) context.ui.notify(result.message, result.level);\n },\n },\n ];\n}\n\nexport function createSandboxServerCommands(context: WithRoot<DoomServerPluginContext, SandboxServerScope>) {\n const service = context.root.service;\n if (!service) return [];\n return [\n defineCommand({\n name: COMMAND_NAME,\n description: COMMAND_DESCRIPTION,\n async execute() {\n const result = await service.execute();\n await context.agent?.context.client.notify({ body: result.message, level: result.level });\n },\n }),\n ];\n}\n","import type { DoomHeadlessResource } from '@agimon-ai/doompi-core/headless';\nimport { readPackageResource } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { SandboxServerScope } from './sandboxScope';\n\nexport function createSandboxContextResource(root: SandboxServerScope): DoomHeadlessResource | undefined {\n if (!root.service) return undefined;\n return {\n // The package index is Help-catalog material, not standing instruction. The Pi\n // facet has always routed it through the Help service; this is the server's gate.\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n name: 'doompi-sandbox',\n kind: 'context',\n read: () => readPackageResource(import.meta.url, 'llms.txt'),\n };\n}\n\nexport function createSandboxSkillResource(root: SandboxServerScope): DoomHeadlessResource | undefined {\n if (!root.service) return undefined;\n return {\n name: 'doompi-use-sandbox',\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n kind: 'skill',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-sandbox/SKILL.md'),\n };\n}\n\nexport function createSandboxHelpResource(moduleUrl: string) {\n return {\n source: '@agimon-ai/doompi-sandbox',\n moduleUrl,\n skills: [\n {\n name: 'doompi-use-sandbox',\n description:\n 'Use @agimon-ai/doompi-sandbox: Container sandbox for DoomPi launches: the agent, extensions, and tools run inside Docker or Podman while the terminal stays on the host',\n },\n ],\n };\n}\n"],"mappings":";;;AAAA,MAAa,eAAe;AAC5B,MAAa,sBAAsB;AAEnC,MAAa,sBAAsB;AAEnC,MAAa,uBAAuB;;;ACIpC,SAAgB,wBACd,SACmE;CACnE,OAAO,CACL,cACA;EACE,aAAa;EACb,MAAM,QAAQ,OAAO,SAAS;GAC5B,MAAM,SAAS,MAAM,QAAQ,QAAQ;GACrC,IAAI,QAAQ,OAAO,QAAQ,GAAG,OAAO,OAAO,SAAS,OAAO,KAAK;EACnE;CACF,CACF;AACF;AAEA,SAAgB,4BAA4B,SAAgE;CAC1G,MAAM,UAAU,QAAQ,KAAK;CAC7B,IAAI,CAAC,SAAS,OAAO,CAAC;CACtB,OAAO,EAAA,GACLA,mCAAAA,cAAAA,CAAc;EACZ,MAAM;EACN,aAAa;EACb,MAAM,UAAU;GACd,MAAM,SAAS,MAAM,QAAQ,QAAQ;GACrC,MAAM,QAAQ,OAAO,QAAQ,OAAO,OAAO;IAAE,MAAM,OAAO;IAAS,OAAO,OAAO;GAAM,CAAC;EAC1F;CACF,CAAC,CACH;AACF;;;AChCA,SAAgB,6BAA6B,MAA4D;CACvG,IAAI,CAAC,KAAK,SAAS,OAAO,KAAA;CAC1B,OAAO;EAGL,MAAM;GAAE,OAAO,EAAE,cAAc,OAAO;GAAG,aAAa;IAAE,MAAM;IAAS,MAAM;GAAO;EAAE;EACtF,MAAM;EACN,MAAM;EACN,aAAA,GAAYC,mCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,UAAU;CAC7D;AACF;AAEA,SAAgB,2BAA2B,MAA4D;CACrG,IAAI,CAAC,KAAK,SAAS,OAAO,KAAA;CAC1B,OAAO;EACL,MAAM;EACN,MAAM;GAAE,OAAO,EAAE,cAAc,OAAO;GAAG,aAAa;IAAE,MAAM;IAAS,MAAM;GAAO;EAAE;EACtF,MAAM;EACN,aAAA,GAAYA,mCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,yCAAyC;CAC5F;AACF;AAEA,SAAgB,0BAA0B,WAAmB;CAC3D,OAAO;EACL,QAAQ;EACR;EACA,QAAQ,CACN;GACE,MAAM;GACN,aACE;EACJ,CACF;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-sandbox",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.92",
|
|
4
4
|
"description": "Container sandbox for DoomPi launches: the agent, extensions, and tools run inside Docker or Podman while the terminal stays on the host",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
"access": "public"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@agimon-ai/doompi-core": "0.0.1-alpha.
|
|
77
|
+
"@agimon-ai/doompi-core": "0.0.1-alpha.93",
|
|
78
78
|
"@deepseek-ai/cordis": "4.0.2"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@agimon-ai/doompi-build": "0.0.1-alpha.
|
|
82
|
-
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.
|
|
81
|
+
"@agimon-ai/doompi-build": "0.0.1-alpha.16",
|
|
82
|
+
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.82",
|
|
83
83
|
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
84
84
|
"@types/node": "26.6.1",
|
|
85
85
|
"@vitest/coverage-v8": "5.0.1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"harness-BMkPGehU.cjs","names":["LAUNCHER_BINARY","BROKER_MOUNT_DIRECTORY","BROKER_SOCKET_ENV","BROKER_SOCKET_CONTAINER_PATH","BROKER_PORT_ENV","BROKER_CONTAINER_PORT","BROKER_ADDRESS_ENV","BROKER_HOST_GATEWAY","BROKER_PROVIDERS_ENV","DOOMPI_SANDBOX_ENV","BRIDGE_CONTAINER_PATH","spawn","path","fs","net","BRIDGE_FILE_NAME","BRIDGE_CONTAINER_PATH","createHash","sandboxBridgeSource","path","require","createRequire","os","createHash","fs","startBroker","BRIDGE_FILE_NAME","sandboxBridgeSource"],"sources":["../src/services/devcontainer/index.ts","../src/services/oauthCallback/index.ts","../src/services/sandboxEnvironment/index.ts","../src/services/sandboxPlan/index.ts","../src/services/engineProcess/index.ts","../src/services/devcontainer/runtime.ts","../src/services/loginPorts/index.ts","../src/services/runFlags/index.ts","../src/services/sandboxImage/index.ts","../src/services/sandboxImageTag/index.ts","../src/services/harness/index.ts"],"sourcesContent":["/**\n * Relative locations the Dev Containers spec looks for a configuration.\n *\n * Ordered as the spec resolves them, so the first hit on disk wins.\n */\nexport const DEVCONTAINER_CONFIG_PATHS: readonly string[] = ['.devcontainer/devcontainer.json', '.devcontainer.json'];\n\nexport const DEVCONTAINER_DISABLED_ENV = 'DOOMPI_SANDBOX_DEVCONTAINER';\nexport const DEVCONTAINER_CLI_PACKAGE = '@devcontainers/cli@0.80.0';\n/** Marks containers this layer brought up, so a stale one can be told apart. */\nexport const DEVCONTAINER_ID_LABEL = 'doompi.sandbox.devcontainer';\n\nexport interface DevcontainerUpResult {\n containerId?: string;\n /** Where the CLI mounted the workspace inside the container. */\n remoteWorkspaceFolder?: string;\n error?: string;\n}\n\nexport interface DevcontainerUpOptions {\n repoRoot: string;\n /** Host directory holding the broker socket, bind-mounted when one is used. */\n socketDirectory?: string;\n /** Container path the socket directory is mounted at. */\n socketTarget?: string;\n}\n\n/** Arguments for bringing the workspace's own dev container up. */\nexport function devcontainerUpArgs(options: DevcontainerUpOptions): string[] {\n return [\n 'up',\n '--workspace-folder',\n options.repoRoot,\n '--log-format',\n 'json',\n ...(options.socketDirectory && options.socketTarget\n ? ['--mount', `type=bind,source=${options.socketDirectory},target=${options.socketTarget}`]\n : []),\n ];\n}\n\n/**\n * Reads the container id out of the CLI's JSON log stream.\n *\n * The stream carries progress records too, so the result is the last record\n * that reports an outcome rather than the first line parsed.\n */\nexport function parseDevcontainerUp(stdout: string): DevcontainerUpResult {\n let outcome:\n | { outcome?: string; containerId?: string; remoteWorkspaceFolder?: string; message?: string; description?: string }\n | undefined;\n for (const line of stdout.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('{')) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (typeof parsed === 'object' && parsed !== null && 'outcome' in parsed) {\n outcome = parsed as typeof outcome;\n }\n }\n if (!outcome) return { error: 'The dev container CLI reported no outcome.' };\n if (outcome.outcome !== 'success') {\n return { error: outcome.description || outcome.message || `Dev container start reported ${outcome.outcome}.` };\n }\n if (!outcome.containerId) return { error: 'The dev container started but reported no container id.' };\n return { containerId: outcome.containerId, remoteWorkspaceFolder: outcome.remoteWorkspaceFolder };\n}\n\n/**\n * Maps a host path into the dev container's own workspace.\n *\n * A dev container mounts the workspace where its configuration says, commonly\n * /workspaces/<name>, so the host path the launch was given does not exist\n * inside and would fail the exec before the agent ever starts.\n */\nexport function containerWorkspacePath(repoRoot: string, cwd: string, remoteWorkspaceFolder: string): string {\n const relative = cwd.startsWith(repoRoot) ? cwd.slice(repoRoot.length).replace(/^[\\\\/]+/, '') : '';\n return relative ? `${remoteWorkspaceFolder}/${relative.split('\\\\').join('/')}` : remoteWorkspaceFolder;\n}\n\n/**\n * Installs the distribution inside a dev container that lacks it.\n *\n * The container is the workspace's own, so it carries the project toolchain\n * rather than this layer's, and nothing guarantees DoomPi is present. `up`\n * reuses an existing container, so this is paid once for its lifetime.\n */\nexport function bootstrapCommand(version: string): string[] {\n return [\n 'sh',\n '-c',\n `command -v doompi >/dev/null 2>&1 && exit 0; ` +\n `command -v npm >/dev/null 2>&1 || { echo \"doompi-sandbox: this dev container has no npm to install DoomPi with\" >&2; exit 127; }; ` +\n `npm install -g @agimon-ai/doompi@${version} >/dev/null`,\n ];\n}\n\n/**\n * Rewrites forwarded host paths into the dev container's workspace.\n *\n * The launcher replays its resolved options, and several carry absolute host\n * paths: --cwd always, plugin and additional directories when set. Those\n * resolve in the built-in image, which mounts the repository at the path it\n * has on the host, but a dev container mounts it wherever its configuration\n * says, and the session would fail looking for a repository that is not there.\n */\nexport function mapForwardArgs(args: readonly string[], repoRoot: string, remoteWorkspaceFolder: string): string[] {\n return args.map((argument) =>\n argument === repoRoot || argument.startsWith(`${repoRoot}/`)\n ? containerWorkspacePath(repoRoot, argument, remoteWorkspaceFolder)\n : argument,\n );\n}\n\nexport interface DevcontainerExecOptions {\n containerId: string;\n cwd: string;\n environment: Readonly<Record<string, string>>;\n hasTty: boolean;\n command: readonly string[];\n}\n\n/**\n * Runs a command in the dev container through the engine rather than the CLI.\n *\n * `devcontainer exec` allocates no terminal, which a full screen TUI needs, so\n * the session is attached to the container the CLI already started.\n */\nexport function devcontainerExecArgs(options: DevcontainerExecOptions): string[] {\n const environmentPairs = Object.entries(options.environment).sort(([left], [right]) => left.localeCompare(right));\n return [\n 'exec',\n '-i',\n ...(options.hasTty ? ['-t'] : []),\n '-w',\n options.cwd,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n options.containerId,\n ...options.command,\n ];\n}\n","/**\n * Fixed loopback ports Pi's OAuth flows listen on.\n *\n * A provider redirects the browser to `http://localhost:<port>`, so the host\n * port has to match exactly and cannot be remapped. Providers that bind an\n * ephemeral port instead, such as OpenRouter, cannot be published ahead of the\n * flow and are not covered.\n */\nexport const OAUTH_CALLBACK_PORTS: readonly number[] = [\n 1455, // openai-codex\n 1456, // radius\n 53692, // anthropic\n];\n\nexport const OAUTH_CALLBACK_HOST_ENV = 'PI_OAUTH_CALLBACK_HOST';\n\n/**\n * Address the in-container callback server binds.\n *\n * Pi binds loopback by default, which a published port cannot reach: the\n * engine forwards to the container's external interface, not its loopback.\n * Only the bind address changes; the redirect the provider sees stays\n * `localhost`.\n */\nexport const OAUTH_CONTAINER_BIND = '0.0.0.0';\n\n/** Publishes each reachable callback port back onto the host's loopback. */\nexport function oauthPublishArgs(ports: readonly number[]): string[] {\n return ports.flatMap((port) => ['-p', `127.0.0.1:${port}:${port}`]);\n}\n","const EXACT_NAMES = new Set([\n 'TERM',\n 'COLORTERM',\n 'LANG',\n 'TZ',\n 'DOOMPI_PRESET',\n 'ELICITATION_SESSION_ID',\n 'HTTP_PROXY',\n 'HTTPS_PROXY',\n 'NO_PROXY',\n 'http_proxy',\n 'https_proxy',\n 'no_proxy',\n]);\nconst NAME_PREFIXES = ['LC_'];\nconst NAME_SUFFIXES = ['_API_KEY', '_AUTH_TOKEN', '_BASE_URL'];\n\n/** True for variables shaped like a provider credential or endpoint override. */\nexport function isCredentialEnvName(name: string): boolean {\n return NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));\n}\n\nfunction allowed(name: string): boolean {\n return (\n EXACT_NAMES.has(name) ||\n NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) ||\n NAME_SUFFIXES.some((suffix) => name.endsWith(suffix))\n );\n}\n\n/**\n * Projects the host environment down to what a sandboxed session may see.\n *\n * The list is an allowlist on purpose: everything a terminal accumulates\n * (agents, sockets, tokens, paths) stays on the host unless a rule names it.\n * Provider credentials pass through for now; the credential broker planned for\n * the client-server phase removes even those from the container.\n */\nexport function filterSandboxEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n): Record<string, string> {\n const filtered: Record<string, string> = {};\n for (const [name, value] of Object.entries(environment)) {\n if (value !== undefined && allowed(name)) filtered[name] = value;\n }\n return filtered;\n}\n","import { DOOMPI_SANDBOX_ENV } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { BrokerEndpoint, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { OAUTH_CALLBACK_HOST_ENV, OAUTH_CONTAINER_BIND, oauthPublishArgs } from '../oauthCallback';\nimport {\n BRIDGE_CONTAINER_PATH,\n BROKER_ADDRESS_ENV,\n BROKER_CONTAINER_PORT,\n BROKER_HOST_GATEWAY,\n BROKER_PORT_ENV,\n BROKER_PROVIDERS_ENV,\n BROKER_SOCKET_CONTAINER_PATH,\n BROKER_SOCKET_ENV,\n} from '../sandboxBridge';\nimport { filterSandboxEnvironment, isCredentialEnvName } from '../sandboxEnvironment';\n\nconst CONTAINER_HOME = '/doompi-home';\nconst REPOSITORY_LABEL = 'doompi.sandbox.repo';\n// The harness binary, which accepts every forwarded flag; dpi hands its\n// arguments straight to Pi and would reject harness options like --preset.\nconst LAUNCHER_BINARY = 'doompi';\nconst NODE_BINARY = 'node';\nconst LINUX_PLATFORM = 'linux';\nconst ENABLED = '1';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Host broker facts the plan projects into mounts and environment. */\nexport interface SandboxPlanBroker {\n endpoint: BrokerEndpoint;\n token: string;\n providers: readonly string[];\n /** Credential variables whose value the container receives as the token. */\n withheldEnv: readonly string[];\n}\n\nexport interface SandboxPlanInput {\n repoRoot: string;\n cwd: string;\n forwardArgs: string[];\n environment: Readonly<Record<string, string | undefined>>;\n engine: SandboxEngine;\n host: SandboxHostFacts;\n /** Image the launch resolved, which also pins the image definition. */\n imageTag: string;\n /** Engine options the operator configured, such as an alternate runtime. */\n runFlags?: readonly string[];\n /** OAuth callback ports free on the host, published so a browser can reach them. */\n loginPorts?: readonly number[];\n broker?: SandboxPlanBroker;\n}\n\nexport interface SandboxPlan {\n imageTag: string;\n /** Full engine argv after the engine binary itself. */\n runArgs: string[];\n}\n\n/**\n * Replaces every credential in the container with the broker's session token.\n *\n * Dropping all credential-shaped variables first is what makes the promise\n * hold for providers the broker does not carry: an unbrokered key would\n * otherwise stay readable inside the container.\n */\nfunction brokeredEnvironment(filtered: Record<string, string>, broker: SandboxPlanBroker): Record<string, string> {\n const environment: Record<string, string> = {};\n for (const [name, value] of Object.entries(filtered)) {\n if (!isCredentialEnvName(name)) environment[name] = value;\n }\n for (const name of broker.withheldEnv) environment[name] = broker.token;\n if (broker.endpoint.transport === 'unix') {\n environment[BROKER_SOCKET_ENV] = BROKER_SOCKET_CONTAINER_PATH;\n // Only the bridge reads this, and only a socket needs the bridge.\n environment[BROKER_PORT_ENV] = String(BROKER_CONTAINER_PORT);\n } else {\n environment[BROKER_ADDRESS_ENV] = `${BROKER_HOST_GATEWAY}:${broker.endpoint.port}`;\n }\n environment[BROKER_PROVIDERS_ENV] = broker.providers.join(',');\n return environment;\n}\n\n/**\n * Grants the container the one route it needs to reach the broker.\n *\n * A mounted socket needs no network at all. TCP needs the engine's host\n * gateway named explicitly, because it is not resolvable by default on every\n * engine.\n */\nfunction brokerAccessArgs(broker: SandboxPlanBroker | undefined): string[] {\n if (!broker) return [];\n if (broker.endpoint.transport === 'unix') {\n return ['-v', `${broker.endpoint.socketDirectory}:${BROKER_MOUNT_DIRECTORY}`];\n }\n return ['--add-host', `${BROKER_HOST_GATEWAY}:host-gateway`];\n}\n\n/**\n * Projects one launch into a disposable engine run.\n *\n * The container is ephemeral; persistence lives in two named volumes: the\n * container home (Pi sessions, sync cache) and a shadow over the repository's\n * `.pi` store, which keeps Linux installs from corrupting the host's own\n * platform-specific packages on the shared workspace mount.\n */\n/**\n * The environment a sandboxed session runs with, whichever container hosts it.\n *\n * Shared with the dev container path, which builds no run arguments of its own\n * but must apply the same allowlist and the same credential substitution.\n */\nexport function containerEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n broker?: SandboxPlanBroker,\n loginPorts: readonly number[] = [],\n): Record<string, string> {\n const filtered = filterSandboxEnvironment(environment);\n const resolved = broker ? brokeredEnvironment(filtered, broker) : filtered;\n resolved[DOOMPI_SANDBOX_ENV] = ENABLED;\n if (loginPorts.length > 0) resolved[OAUTH_CALLBACK_HOST_ENV] = OAUTH_CONTAINER_BIND;\n return resolved;\n}\n\nexport function buildSandboxPlan(input: SandboxPlanInput): SandboxPlan {\n const { repoRoot, cwd, engine, host, broker, imageTag } = input;\n const loginPorts = input.loginPorts ?? [];\n const environmentPairs = Object.entries(containerEnvironment(input.environment, broker, loginPorts)).sort(\n ([left], [right]) => left.localeCompare(right),\n );\n\n const runArgs = [\n 'run',\n '--rm',\n '-i',\n ...(host.hasTty ? ['-t'] : []),\n '--label',\n `${REPOSITORY_LABEL}=${repoRoot}`,\n '-v',\n `${repoRoot}:${repoRoot}`,\n '-v',\n `${host.repoKey}-home:${CONTAINER_HOME}`,\n '-v',\n `${host.repoKey}-pi:${repoRoot}/.pi`,\n ...brokerAccessArgs(broker),\n ...oauthPublishArgs(loginPorts),\n '-w',\n cwd,\n '-e',\n `HOME=${CONTAINER_HOME}`,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n ...(host.platform === LINUX_PLATFORM && host.userId !== undefined\n ? [\n '--user',\n `${host.userId}:${host.groupId ?? host.userId}`,\n ...(engine === 'podman' ? ['--userns=keep-id'] : []),\n ]\n : []),\n // Last before the image so a configured option wins over the defaults\n // above it, and cannot be mistaken for the image or its command.\n ...(input.runFlags ?? []),\n imageTag,\n // Only a mounted socket needs the bridge: a broker on a host port is\n // addressed directly, so the launcher runs unwrapped.\n ...(broker?.endpoint.transport === 'unix' ? [NODE_BINARY, BRIDGE_CONTAINER_PATH] : []),\n LAUNCHER_BINARY,\n ...input.forwardArgs,\n ];\n\n return { imageTag, runArgs };\n}\n","import { spawn } from 'node:child_process';\n\nimport type { EngineCaptureResult, EngineProcessRunner, EngineRunOptions } from '../../types/sandboxHarness';\n\nconst INHERIT = 'inherit';\nconst PIPE = 'pipe';\nconst IGNORE = 'ignore';\nconst FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];\n\n/** Spawns the container engine, keeping the session's terminal attached. */\nexport class SpawnEngineProcessRunner implements EngineProcessRunner {\n run(command: string, args: string[], options: EngineRunOptions = {}): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options.input === undefined ? INHERIT : [PIPE, INHERIT, INHERIT],\n });\n if (options.input !== undefined) child.stdin?.end(options.input);\n\n // With a shared terminal the engine sees Ctrl-C itself; forwarding covers\n // detached automation runs, and a duplicate signal is harmless.\n const forwarders = FORWARDED_SIGNALS.map((signal) => {\n const forward = (): void => {\n child.kill(signal);\n };\n process.on(signal, forward);\n return { signal, forward };\n });\n const cleanup = (): void => {\n for (const { signal, forward } of forwarders) process.off(signal, forward);\n };\n\n child.once('error', (error) => {\n cleanup();\n reject(error);\n });\n child.once('exit', (code, signal) => {\n cleanup();\n resolve(code ?? (signal ? 1 : 0));\n });\n });\n }\n\n capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined> {\n return new Promise((resolve) => {\n const child = spawn(command, args, { stdio: [IGNORE, PIPE, IGNORE] });\n let stdout = '';\n child.stdout?.on('data', (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.once('error', () => {\n resolve(undefined);\n });\n child.once('exit', (code) => {\n resolve({ exitCode: code ?? 1, stdout });\n });\n });\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n bootstrapCommand,\n containerWorkspacePath,\n mapForwardArgs,\n DEVCONTAINER_CLI_PACKAGE,\n DEVCONTAINER_CONFIG_PATHS,\n devcontainerExecArgs,\n devcontainerUpArgs,\n parseDevcontainerUp,\n} from '.';\nimport type { EngineProcessRunner, SandboxEngine } from '../../types/sandboxHarness';\n\nconst DEVCONTAINER_BINARY = 'devcontainer';\nconst NPX_BINARY = 'npx';\nconst LAUNCHER_BINARY = 'doompi';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Locates the workspace's own dev container configuration, if it has one. */\nexport function findDevcontainerConfig(repoRoot: string): string | undefined {\n for (const relative of DEVCONTAINER_CONFIG_PATHS) {\n const candidate = path.join(repoRoot, relative);\n if (fs.existsSync(candidate)) return candidate;\n }\n return undefined;\n}\n\n/**\n * Resolves how to invoke the Dev Containers CLI.\n *\n * A local install is preferred so a launch does not depend on the network;\n * npx is the fallback, pinned so the tool cannot change underneath a session.\n */\nexport async function resolveDevcontainerCli(\n runner: EngineProcessRunner,\n): Promise<{ command: string; prefix: string[] }> {\n const probe = await runner.capture(DEVCONTAINER_BINARY, ['--version']);\n if (probe?.exitCode === 0) return { command: DEVCONTAINER_BINARY, prefix: [] };\n return { command: NPX_BINARY, prefix: ['-y', DEVCONTAINER_CLI_PACKAGE] };\n}\n\nexport interface DevcontainerSessionOptions {\n repoRoot: string;\n cwd: string;\n forwardArgs: readonly string[];\n environment: Readonly<Record<string, string>>;\n engine: SandboxEngine;\n runner: EngineProcessRunner;\n version: string;\n hasTty: boolean;\n /** Host directory holding the broker socket, when the broker uses one. */\n socketDirectory?: string;\n onProgress?: (message: string) => void;\n}\n\n/**\n * Runs the session inside the workspace's own dev container.\n *\n * The container is brought up by the Dev Containers CLI, so the file decides\n * the image, features, mounts and lifecycle hooks: the session gets the\n * project's real toolchain, and this layer's isolation guarantees do not\n * apply beyond the environment it passes in.\n */\nexport async function runDevcontainerSession(options: DevcontainerSessionOptions): Promise<number> {\n const cli = await resolveDevcontainerCli(options.runner);\n options.onProgress?.('starting the workspace dev container');\n\n const upArgs = devcontainerUpArgs({\n repoRoot: options.repoRoot,\n socketDirectory: options.socketDirectory,\n socketTarget: options.socketDirectory ? BROKER_MOUNT_DIRECTORY : undefined,\n });\n const up = await options.runner.capture(cli.command, [...cli.prefix, ...upArgs]);\n if (!up) {\n throw new Error('The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.');\n }\n const { containerId, remoteWorkspaceFolder, error } = parseDevcontainerUp(up.stdout);\n if (!containerId) throw new Error(`The dev container did not start: ${error ?? `exit ${up.exitCode}`}`);\n\n const bootstrap = await options.runner.run(options.engine, [\n 'exec',\n containerId,\n ...bootstrapCommand(options.version),\n ]);\n if (bootstrap !== 0) {\n throw new Error(\n `DoomPi could not be installed in the dev container (exit ${bootstrap}). ` +\n 'Add it to the dev container, or unset the dev container preference to use the built-in image.',\n );\n }\n\n options.onProgress?.(`attaching to dev container ${containerId.slice(0, 12)}`);\n return options.runner.run(\n options.engine,\n devcontainerExecArgs({\n containerId,\n cwd: remoteWorkspaceFolder\n ? containerWorkspacePath(options.repoRoot, options.cwd, remoteWorkspaceFolder)\n : options.cwd,\n environment: options.environment,\n hasTty: options.hasTty,\n command: [\n LAUNCHER_BINARY,\n ...(remoteWorkspaceFolder\n ? mapForwardArgs(options.forwardArgs, options.repoRoot, remoteWorkspaceFolder)\n : options.forwardArgs),\n ],\n }),\n );\n}\n","import net from 'node:net';\n\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\n\nconst LOOPBACK = '127.0.0.1';\n\nfunction portIsFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = net.createServer();\n probe.once('error', () => resolve(false));\n probe.listen(port, LOOPBACK, () => {\n probe.close(() => resolve(true));\n });\n });\n}\n\n/**\n * Reports which OAuth callback ports this launch may publish.\n *\n * A taken port means another sandbox already holds it, so publishing would\n * fail the whole launch over a login the session may never attempt. Skipping\n * it costs only the ability to log in from inside this particular container.\n */\nexport async function availableLoginPorts(ports: readonly number[] = OAUTH_CALLBACK_PORTS): Promise<number[]> {\n const free: number[] = [];\n for (const port of ports) {\n if (await portIsFree(port)) free.push(port);\n }\n return free;\n}\n","/**\n * Splits configured engine options into argv entries.\n *\n * Whitespace separated rather than shell parsed: the engine is spawned without\n * a shell, so quoting rules would only invent a syntax the caller does not\n * actually get.\n */\nexport function parseRunFlags(configured: string | undefined): string[] {\n return (configured ?? '').split(/\\s+/).filter((flag) => flag.length > 0);\n}\n\nconst OPTION_FORM = /^--?[^\\s=]+(=.*)?$/s;\n\n/**\n * Requires every configured entry to be a self-contained option.\n *\n * Separated values cannot be told from positional arguments without knowing\n * each option's arity, and a stray positional would silently replace the image\n * or its command. Demanding `--flag=value` removes the ambiguity instead of\n * guessing at it.\n */\nexport function assertRunFlags(flags: readonly string[], optionName: string): void {\n const invalid = flags.find((flag) => !OPTION_FORM.test(flag));\n if (invalid !== undefined) {\n throw new Error(\n `${optionName} accepts engine options in --flag or --flag=value form; \"${invalid}\" is neither. ` +\n 'Write a value as part of its option, for example --runtime=runsc.',\n );\n }\n}\n","import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from '../sandboxBridge';\n\nconst IMAGE_REPOSITORY = 'doompi-sandbox';\nconst TAG_SAFE = /[^A-Za-z0-9_.-]/g;\n\n/**\n * Names one image build: a distribution version plus its image definition.\n *\n * The digest matters as much as the version. Editing the Dockerfile or the\n * bridge without it would silently reuse a cached image that predates the\n * change.\n */\nexport function formatImageTag(version: string, digest: string): string {\n return `${IMAGE_REPOSITORY}:v${version.replaceAll(TAG_SAFE, '-')}-${digest}`;\n}\n\n/**\n * Self-contained Linux image for sandboxed sessions.\n *\n * nano is present because Pi falls back to it for the external editor when\n * VISUAL and EDITOR are unset, which is always the case here: a host editor\n * variable naming a desktop application would resolve to a binary the\n * container does not have, so it is deliberately not forwarded.\n *\n * The distribution installs from the registry rather than mounting host\n * modules: host installs carry platform-specific binaries a Linux container\n * cannot run. DOOMPI_SANDBOX is baked in so nothing that reaches a shell in\n * the container can present itself as an unsandboxed session.\n */\nexport function sandboxDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi@${DOOMPI_VERSION}',\n `COPY ${BRIDGE_FILE_NAME} ${BRIDGE_CONTAINER_PATH}`,\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n\n/**\n * Image for the cockpit container, which holds the hub rather than one session.\n *\n * Kept separate from the sandbox image rather than folded into it. The cockpit\n * needs the web package and a tunnel client, and making every interactive\n * `--sandbox` launch pay for an extra apt repository and a larger download to\n * get them would be a poor trade.\n *\n * Installing `@agimon-ai/doompi-web` is enough for all three programs: its\n * `@agimon-ai/doompi` dependency owns both the session server executable and\n * the agent runtime.\n */\nexport function cockpitDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n // The tunnel runs beside the hub rather than on the host, so the cockpit\n // owns its own remote access and all of that state stays in one place.\n 'RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\\\',\n ' -o /usr/share/keyrings/cloudflare-main.gpg \\\\',\n ' && echo \"deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main\" \\\\',\n ' > /etc/apt/sources.list.d/cloudflared.list \\\\',\n ' && apt-get update && apt-get install -y --no-install-recommends cloudflared \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi-web@${DOOMPI_VERSION}',\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n","import { createHash } from 'node:crypto';\n\nimport { sandboxBridgeSource } from '../sandboxBridge';\nimport { cockpitDockerfile, formatImageTag, sandboxDockerfile } from '../sandboxImage';\n\nconst DIGEST_LENGTH = 8;\n\n/** Tags the image with the distribution version and a digest of its definition. */\nexport function sandboxImageTag(version: string): string {\n const digest = createHash('sha256')\n .update(sandboxDockerfile())\n .update(sandboxBridgeSource())\n .digest('hex')\n .slice(0, DIGEST_LENGTH);\n return formatImageTag(version, digest);\n}\n\n/**\n * Tags the cockpit image separately from the sandbox one.\n *\n * Same digest scheme, different definition, so editing either Dockerfile\n * invalidates only its own image.\n */\nexport function cockpitImageTag(version: string): string {\n const digest = createHash('sha256').update(cockpitDockerfile()).digest('hex').slice(0, DIGEST_LENGTH);\n return formatImageTag(`${version}-cockpit`, digest);\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { startBroker, type RunningBroker } from '../brokerHost';\nimport { DEVCONTAINER_DISABLED_ENV } from '../devcontainer';\nimport { findDevcontainerConfig, runDevcontainerSession } from '../devcontainer/runtime';\nimport { SpawnEngineProcessRunner } from '../engineProcess';\nimport { availableLoginPorts } from '../loginPorts';\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\nimport { assertRunFlags, parseRunFlags } from '../runFlags';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../sandboxBridge';\nimport { sandboxDockerfile } from '../sandboxImage';\nimport { sandboxImageTag } from '../sandboxImageTag';\nimport { buildSandboxPlan, containerEnvironment } from '../sandboxPlan';\n\nconst ENGINE_ENV = 'DOOMPI_SANDBOX_ENGINE';\nconst BROKER_DISABLED_ENV = 'DOOMPI_SANDBOX_BROKER';\nconst RUN_FLAGS_ENV = 'DOOMPI_SANDBOX_RUN_FLAGS';\nconst DISABLED_VALUE = '0';\n// Every entry takes docker's run syntax, which is what the plan emits.\nconst ENGINES: SandboxEngine[] = ['docker', 'podman', 'nerdctl', 'finch'];\nconst DOCKERFILE_NAME = 'Dockerfile';\nconst MODES_FILE = path.join('.doom', 'modes.yaml');\nconst LOCAL_PACKAGE_PATTERN = /^\\s*(?:-|name:)\\s*[\"']?\\.{1,2}\\//m;\nconst REPO_KEY_LENGTH = 12;\n\n// Self-reference instead of a relative path: the file depth differs between\n// running from src and from the built dist tree.\nconst require = createRequire(import.meta.url);\nconst SELF_MANIFEST = '@agimon-ai/doompi-sandbox/package.json';\n\nexport interface SandboxLauncherDependencies {\n runner?: EngineProcessRunner;\n /** Seam for tests; defaults to probing the real OAuth callback ports. */\n loginPorts?: () => Promise<number[]>;\n /** Seam for tests; defaults to the real host broker. */\n startBroker?: (options: {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n }) => Promise<RunningBroker | undefined>;\n /** Distribution version override; defaults to this package's own version. */\n version?: string;\n hostFacts?: Partial<SandboxHostFacts>;\n}\n\nfunction distributionVersion(): string {\n return (require(SELF_MANIFEST) as { version: string }).version;\n}\n\n/** Fixed per-repository directory a reused dev container can keep mounted. */\nfunction brokerDirectory(repoRoot: string): string {\n return path.join(os.tmpdir(), `${repoKey(repoRoot)}-broker`);\n}\n\nfunction repoKey(repoRoot: string): string {\n return `doompi-sandbox-${createHash('sha256').update(repoRoot).digest('hex').slice(0, REPO_KEY_LENGTH)}`;\n}\n\nexport async function detectEngine(\n runner: EngineProcessRunner,\n environment: Readonly<Record<string, string | undefined>>,\n): Promise<SandboxEngine> {\n const configured = environment[ENGINE_ENV]?.trim();\n if (configured) {\n if ((ENGINES as string[]).includes(configured)) return configured as SandboxEngine;\n throw new Error(`${ENGINE_ENV} must be one of: ${ENGINES.join(', ')}.`);\n }\n for (const engine of ENGINES) {\n const probe = await runner.capture(engine, ['--version']);\n if (probe?.exitCode === 0) return engine;\n }\n throw new Error(`No container engine found. Install one of ${ENGINES.join(', ')}, or set ${ENGINE_ENV}.`);\n}\n\n/**\n * Builds the image if the engine does not already have it.\n *\n * Takes its definition rather than assuming one, because the interactive\n * sandbox and the cockpit container are two different images built the same\n * way. The tag already carries a digest of the definition, so a changed\n * Dockerfile misses the inspect and rebuilds.\n */\nexport async function ensureImage(\n runner: EngineProcessRunner,\n engine: SandboxEngine,\n version: string,\n onProgress: ((message: string) => void) | undefined,\n definition: { tag: string; dockerfile: string; files?: Readonly<Record<string, string>> },\n): Promise<string> {\n const tag = definition.tag;\n const inspected = await runner.capture(engine, ['image', 'inspect', tag]);\n if (inspected?.exitCode === 0) return tag;\n\n onProgress?.(`building ${tag} (first run for this DoomPi version)`);\n const context = fs.mkdtempSync(path.join(os.tmpdir(), 'doompi-sandbox-build-'));\n try {\n fs.writeFileSync(path.join(context, DOCKERFILE_NAME), definition.dockerfile);\n for (const [name, contents] of Object.entries(definition.files ?? {})) {\n fs.writeFileSync(path.join(context, name), contents);\n }\n const exitCode = await runner.run(engine, [\n 'build',\n '-t',\n tag,\n '--build-arg',\n `DOOMPI_VERSION=${version}`,\n context,\n ]);\n if (exitCode !== 0) throw new Error(`Building the ${tag} image failed with exit code ${exitCode}.`);\n } finally {\n fs.rmSync(context, { recursive: true, force: true });\n }\n return tag;\n}\n\nfunction warnAboutLocalPackages(repoRoot: string, onProgress: ((message: string) => void) | undefined): void {\n let modes: string;\n try {\n modes = fs.readFileSync(path.join(repoRoot, MODES_FILE), 'utf8');\n } catch {\n return;\n }\n if (LOCAL_PACKAGE_PATTERN.test(modes)) {\n onProgress?.(\n 'modes.yaml declares local workspace packages; their platform-specific dependencies ' +\n 'must be installed inside the container before they can load',\n );\n }\n}\n\n/** Builds the launcher with injectable seams for tests. */\nexport function createSandboxLauncher(dependencies: SandboxLauncherDependencies = {}) {\n const runner = dependencies.runner ?? new SpawnEngineProcessRunner();\n const beginBroker = dependencies.startBroker ?? startBroker;\n const resolveLoginPorts = dependencies.loginPorts ?? availableLoginPorts;\n return {\n async launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n const engine = await detectEngine(runner, request.environment);\n const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);\n assertRunFlags(runFlags, RUN_FLAGS_ENV);\n request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(' ')}` : ''}`);\n\n const version = dependencies.version ?? distributionVersion();\n // A workspace that describes its own container is describing the\n // toolchain its agent needs, so that container wins over the built-in\n // image. Nothing here is built or pulled until that choice is made.\n const devcontainer =\n request.environment[DEVCONTAINER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : findDevcontainerConfig(request.repoRoot);\n const imageTag = devcontainer\n ? undefined\n : await ensureImage(runner, engine, version, request.onProgress, {\n tag: sandboxImageTag(version),\n dockerfile: sandboxDockerfile(),\n files: { [BRIDGE_FILE_NAME]: sandboxBridgeSource() },\n });\n if (devcontainer) {\n request.onProgress?.(`using the workspace dev container from ${path.relative(request.repoRoot, devcontainer)}`);\n request.onProgress?.(\n 'its configuration owns the mounts and run arguments, so this is not an isolation boundary',\n );\n } else {\n warnAboutLocalPackages(request.repoRoot, request.onProgress);\n }\n\n const broker =\n request.environment[BROKER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : await beginBroker({\n environment: request.environment,\n onDenied: (reason) => request.onProgress?.(`broker refused a call: ${reason}`),\n // A reused dev container keeps the mounts it was created with,\n // so the socket has to be at the same path on every launch.\n ...(devcontainer ? { socketDirectory: brokerDirectory(request.repoRoot) } : {}),\n });\n if (broker) {\n request.onProgress?.(`brokering ${broker.providers.join(', ')}; provider keys stay on the host`);\n }\n\n const loginPorts = devcontainer ? [] : await resolveLoginPorts();\n if (!devcontainer && loginPorts.length < OAUTH_CALLBACK_PORTS.length) {\n request.onProgress?.(\n 'some OAuth callback ports are already taken; /login inside this sandbox may not complete',\n );\n }\n\n try {\n const hasTty = process.stdin.isTTY === true && process.stdout.isTTY === true;\n if (devcontainer) {\n return await runDevcontainerSession({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: containerEnvironment(request.environment, broker),\n engine,\n runner,\n version,\n hasTty,\n ...(broker?.endpoint.transport === 'unix' ? { socketDirectory: broker.endpoint.socketDirectory } : {}),\n ...(request.onProgress ? { onProgress: request.onProgress } : {}),\n });\n }\n\n const host: SandboxHostFacts = {\n hasTty,\n platform: process.platform,\n userId: process.getuid?.(),\n groupId: process.getgid?.(),\n repoKey: repoKey(request.repoRoot),\n version,\n ...dependencies.hostFacts,\n };\n const plan = buildSandboxPlan({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: request.environment,\n engine,\n host,\n imageTag: imageTag as string,\n runFlags,\n loginPorts,\n broker,\n });\n request.onProgress?.(`starting contained session in ${imageTag}`);\n return await runner.run(engine, plan.runArgs);\n } finally {\n await broker?.stop();\n }\n },\n };\n}\n\n/** Contract entry the DoomPi harness resolves for `--sandbox`. */\nexport function launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n return createSandboxLauncher().launchSandbox(request);\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"harness-BWrlo9bu.mjs","names":["LAUNCHER_BINARY","BROKER_MOUNT_DIRECTORY"],"sources":["../src/services/devcontainer/index.ts","../src/services/oauthCallback/index.ts","../src/services/sandboxEnvironment/index.ts","../src/services/sandboxPlan/index.ts","../src/services/engineProcess/index.ts","../src/services/devcontainer/runtime.ts","../src/services/loginPorts/index.ts","../src/services/runFlags/index.ts","../src/services/sandboxImage/index.ts","../src/services/sandboxImageTag/index.ts","../src/services/harness/index.ts"],"sourcesContent":["/**\n * Relative locations the Dev Containers spec looks for a configuration.\n *\n * Ordered as the spec resolves them, so the first hit on disk wins.\n */\nexport const DEVCONTAINER_CONFIG_PATHS: readonly string[] = ['.devcontainer/devcontainer.json', '.devcontainer.json'];\n\nexport const DEVCONTAINER_DISABLED_ENV = 'DOOMPI_SANDBOX_DEVCONTAINER';\nexport const DEVCONTAINER_CLI_PACKAGE = '@devcontainers/cli@0.80.0';\n/** Marks containers this layer brought up, so a stale one can be told apart. */\nexport const DEVCONTAINER_ID_LABEL = 'doompi.sandbox.devcontainer';\n\nexport interface DevcontainerUpResult {\n containerId?: string;\n /** Where the CLI mounted the workspace inside the container. */\n remoteWorkspaceFolder?: string;\n error?: string;\n}\n\nexport interface DevcontainerUpOptions {\n repoRoot: string;\n /** Host directory holding the broker socket, bind-mounted when one is used. */\n socketDirectory?: string;\n /** Container path the socket directory is mounted at. */\n socketTarget?: string;\n}\n\n/** Arguments for bringing the workspace's own dev container up. */\nexport function devcontainerUpArgs(options: DevcontainerUpOptions): string[] {\n return [\n 'up',\n '--workspace-folder',\n options.repoRoot,\n '--log-format',\n 'json',\n ...(options.socketDirectory && options.socketTarget\n ? ['--mount', `type=bind,source=${options.socketDirectory},target=${options.socketTarget}`]\n : []),\n ];\n}\n\n/**\n * Reads the container id out of the CLI's JSON log stream.\n *\n * The stream carries progress records too, so the result is the last record\n * that reports an outcome rather than the first line parsed.\n */\nexport function parseDevcontainerUp(stdout: string): DevcontainerUpResult {\n let outcome:\n | { outcome?: string; containerId?: string; remoteWorkspaceFolder?: string; message?: string; description?: string }\n | undefined;\n for (const line of stdout.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('{')) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (typeof parsed === 'object' && parsed !== null && 'outcome' in parsed) {\n outcome = parsed as typeof outcome;\n }\n }\n if (!outcome) return { error: 'The dev container CLI reported no outcome.' };\n if (outcome.outcome !== 'success') {\n return { error: outcome.description || outcome.message || `Dev container start reported ${outcome.outcome}.` };\n }\n if (!outcome.containerId) return { error: 'The dev container started but reported no container id.' };\n return { containerId: outcome.containerId, remoteWorkspaceFolder: outcome.remoteWorkspaceFolder };\n}\n\n/**\n * Maps a host path into the dev container's own workspace.\n *\n * A dev container mounts the workspace where its configuration says, commonly\n * /workspaces/<name>, so the host path the launch was given does not exist\n * inside and would fail the exec before the agent ever starts.\n */\nexport function containerWorkspacePath(repoRoot: string, cwd: string, remoteWorkspaceFolder: string): string {\n const relative = cwd.startsWith(repoRoot) ? cwd.slice(repoRoot.length).replace(/^[\\\\/]+/, '') : '';\n return relative ? `${remoteWorkspaceFolder}/${relative.split('\\\\').join('/')}` : remoteWorkspaceFolder;\n}\n\n/**\n * Installs the distribution inside a dev container that lacks it.\n *\n * The container is the workspace's own, so it carries the project toolchain\n * rather than this layer's, and nothing guarantees DoomPi is present. `up`\n * reuses an existing container, so this is paid once for its lifetime.\n */\nexport function bootstrapCommand(version: string): string[] {\n return [\n 'sh',\n '-c',\n `command -v doompi >/dev/null 2>&1 && exit 0; ` +\n `command -v npm >/dev/null 2>&1 || { echo \"doompi-sandbox: this dev container has no npm to install DoomPi with\" >&2; exit 127; }; ` +\n `npm install -g @agimon-ai/doompi@${version} >/dev/null`,\n ];\n}\n\n/**\n * Rewrites forwarded host paths into the dev container's workspace.\n *\n * The launcher replays its resolved options, and several carry absolute host\n * paths: --cwd always, plugin and additional directories when set. Those\n * resolve in the built-in image, which mounts the repository at the path it\n * has on the host, but a dev container mounts it wherever its configuration\n * says, and the session would fail looking for a repository that is not there.\n */\nexport function mapForwardArgs(args: readonly string[], repoRoot: string, remoteWorkspaceFolder: string): string[] {\n return args.map((argument) =>\n argument === repoRoot || argument.startsWith(`${repoRoot}/`)\n ? containerWorkspacePath(repoRoot, argument, remoteWorkspaceFolder)\n : argument,\n );\n}\n\nexport interface DevcontainerExecOptions {\n containerId: string;\n cwd: string;\n environment: Readonly<Record<string, string>>;\n hasTty: boolean;\n command: readonly string[];\n}\n\n/**\n * Runs a command in the dev container through the engine rather than the CLI.\n *\n * `devcontainer exec` allocates no terminal, which a full screen TUI needs, so\n * the session is attached to the container the CLI already started.\n */\nexport function devcontainerExecArgs(options: DevcontainerExecOptions): string[] {\n const environmentPairs = Object.entries(options.environment).sort(([left], [right]) => left.localeCompare(right));\n return [\n 'exec',\n '-i',\n ...(options.hasTty ? ['-t'] : []),\n '-w',\n options.cwd,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n options.containerId,\n ...options.command,\n ];\n}\n","/**\n * Fixed loopback ports Pi's OAuth flows listen on.\n *\n * A provider redirects the browser to `http://localhost:<port>`, so the host\n * port has to match exactly and cannot be remapped. Providers that bind an\n * ephemeral port instead, such as OpenRouter, cannot be published ahead of the\n * flow and are not covered.\n */\nexport const OAUTH_CALLBACK_PORTS: readonly number[] = [\n 1455, // openai-codex\n 1456, // radius\n 53692, // anthropic\n];\n\nexport const OAUTH_CALLBACK_HOST_ENV = 'PI_OAUTH_CALLBACK_HOST';\n\n/**\n * Address the in-container callback server binds.\n *\n * Pi binds loopback by default, which a published port cannot reach: the\n * engine forwards to the container's external interface, not its loopback.\n * Only the bind address changes; the redirect the provider sees stays\n * `localhost`.\n */\nexport const OAUTH_CONTAINER_BIND = '0.0.0.0';\n\n/** Publishes each reachable callback port back onto the host's loopback. */\nexport function oauthPublishArgs(ports: readonly number[]): string[] {\n return ports.flatMap((port) => ['-p', `127.0.0.1:${port}:${port}`]);\n}\n","const EXACT_NAMES = new Set([\n 'TERM',\n 'COLORTERM',\n 'LANG',\n 'TZ',\n 'DOOMPI_PRESET',\n 'ELICITATION_SESSION_ID',\n 'HTTP_PROXY',\n 'HTTPS_PROXY',\n 'NO_PROXY',\n 'http_proxy',\n 'https_proxy',\n 'no_proxy',\n]);\nconst NAME_PREFIXES = ['LC_'];\nconst NAME_SUFFIXES = ['_API_KEY', '_AUTH_TOKEN', '_BASE_URL'];\n\n/** True for variables shaped like a provider credential or endpoint override. */\nexport function isCredentialEnvName(name: string): boolean {\n return NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));\n}\n\nfunction allowed(name: string): boolean {\n return (\n EXACT_NAMES.has(name) ||\n NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) ||\n NAME_SUFFIXES.some((suffix) => name.endsWith(suffix))\n );\n}\n\n/**\n * Projects the host environment down to what a sandboxed session may see.\n *\n * The list is an allowlist on purpose: everything a terminal accumulates\n * (agents, sockets, tokens, paths) stays on the host unless a rule names it.\n * Provider credentials pass through for now; the credential broker planned for\n * the client-server phase removes even those from the container.\n */\nexport function filterSandboxEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n): Record<string, string> {\n const filtered: Record<string, string> = {};\n for (const [name, value] of Object.entries(environment)) {\n if (value !== undefined && allowed(name)) filtered[name] = value;\n }\n return filtered;\n}\n","import { DOOMPI_SANDBOX_ENV } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { BrokerEndpoint, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { OAUTH_CALLBACK_HOST_ENV, OAUTH_CONTAINER_BIND, oauthPublishArgs } from '../oauthCallback';\nimport {\n BRIDGE_CONTAINER_PATH,\n BROKER_ADDRESS_ENV,\n BROKER_CONTAINER_PORT,\n BROKER_HOST_GATEWAY,\n BROKER_PORT_ENV,\n BROKER_PROVIDERS_ENV,\n BROKER_SOCKET_CONTAINER_PATH,\n BROKER_SOCKET_ENV,\n} from '../sandboxBridge';\nimport { filterSandboxEnvironment, isCredentialEnvName } from '../sandboxEnvironment';\n\nconst CONTAINER_HOME = '/doompi-home';\nconst REPOSITORY_LABEL = 'doompi.sandbox.repo';\n// The harness binary, which accepts every forwarded flag; dpi hands its\n// arguments straight to Pi and would reject harness options like --preset.\nconst LAUNCHER_BINARY = 'doompi';\nconst NODE_BINARY = 'node';\nconst LINUX_PLATFORM = 'linux';\nconst ENABLED = '1';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Host broker facts the plan projects into mounts and environment. */\nexport interface SandboxPlanBroker {\n endpoint: BrokerEndpoint;\n token: string;\n providers: readonly string[];\n /** Credential variables whose value the container receives as the token. */\n withheldEnv: readonly string[];\n}\n\nexport interface SandboxPlanInput {\n repoRoot: string;\n cwd: string;\n forwardArgs: string[];\n environment: Readonly<Record<string, string | undefined>>;\n engine: SandboxEngine;\n host: SandboxHostFacts;\n /** Image the launch resolved, which also pins the image definition. */\n imageTag: string;\n /** Engine options the operator configured, such as an alternate runtime. */\n runFlags?: readonly string[];\n /** OAuth callback ports free on the host, published so a browser can reach them. */\n loginPorts?: readonly number[];\n broker?: SandboxPlanBroker;\n}\n\nexport interface SandboxPlan {\n imageTag: string;\n /** Full engine argv after the engine binary itself. */\n runArgs: string[];\n}\n\n/**\n * Replaces every credential in the container with the broker's session token.\n *\n * Dropping all credential-shaped variables first is what makes the promise\n * hold for providers the broker does not carry: an unbrokered key would\n * otherwise stay readable inside the container.\n */\nfunction brokeredEnvironment(filtered: Record<string, string>, broker: SandboxPlanBroker): Record<string, string> {\n const environment: Record<string, string> = {};\n for (const [name, value] of Object.entries(filtered)) {\n if (!isCredentialEnvName(name)) environment[name] = value;\n }\n for (const name of broker.withheldEnv) environment[name] = broker.token;\n if (broker.endpoint.transport === 'unix') {\n environment[BROKER_SOCKET_ENV] = BROKER_SOCKET_CONTAINER_PATH;\n // Only the bridge reads this, and only a socket needs the bridge.\n environment[BROKER_PORT_ENV] = String(BROKER_CONTAINER_PORT);\n } else {\n environment[BROKER_ADDRESS_ENV] = `${BROKER_HOST_GATEWAY}:${broker.endpoint.port}`;\n }\n environment[BROKER_PROVIDERS_ENV] = broker.providers.join(',');\n return environment;\n}\n\n/**\n * Grants the container the one route it needs to reach the broker.\n *\n * A mounted socket needs no network at all. TCP needs the engine's host\n * gateway named explicitly, because it is not resolvable by default on every\n * engine.\n */\nfunction brokerAccessArgs(broker: SandboxPlanBroker | undefined): string[] {\n if (!broker) return [];\n if (broker.endpoint.transport === 'unix') {\n return ['-v', `${broker.endpoint.socketDirectory}:${BROKER_MOUNT_DIRECTORY}`];\n }\n return ['--add-host', `${BROKER_HOST_GATEWAY}:host-gateway`];\n}\n\n/**\n * Projects one launch into a disposable engine run.\n *\n * The container is ephemeral; persistence lives in two named volumes: the\n * container home (Pi sessions, sync cache) and a shadow over the repository's\n * `.pi` store, which keeps Linux installs from corrupting the host's own\n * platform-specific packages on the shared workspace mount.\n */\n/**\n * The environment a sandboxed session runs with, whichever container hosts it.\n *\n * Shared with the dev container path, which builds no run arguments of its own\n * but must apply the same allowlist and the same credential substitution.\n */\nexport function containerEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n broker?: SandboxPlanBroker,\n loginPorts: readonly number[] = [],\n): Record<string, string> {\n const filtered = filterSandboxEnvironment(environment);\n const resolved = broker ? brokeredEnvironment(filtered, broker) : filtered;\n resolved[DOOMPI_SANDBOX_ENV] = ENABLED;\n if (loginPorts.length > 0) resolved[OAUTH_CALLBACK_HOST_ENV] = OAUTH_CONTAINER_BIND;\n return resolved;\n}\n\nexport function buildSandboxPlan(input: SandboxPlanInput): SandboxPlan {\n const { repoRoot, cwd, engine, host, broker, imageTag } = input;\n const loginPorts = input.loginPorts ?? [];\n const environmentPairs = Object.entries(containerEnvironment(input.environment, broker, loginPorts)).sort(\n ([left], [right]) => left.localeCompare(right),\n );\n\n const runArgs = [\n 'run',\n '--rm',\n '-i',\n ...(host.hasTty ? ['-t'] : []),\n '--label',\n `${REPOSITORY_LABEL}=${repoRoot}`,\n '-v',\n `${repoRoot}:${repoRoot}`,\n '-v',\n `${host.repoKey}-home:${CONTAINER_HOME}`,\n '-v',\n `${host.repoKey}-pi:${repoRoot}/.pi`,\n ...brokerAccessArgs(broker),\n ...oauthPublishArgs(loginPorts),\n '-w',\n cwd,\n '-e',\n `HOME=${CONTAINER_HOME}`,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n ...(host.platform === LINUX_PLATFORM && host.userId !== undefined\n ? [\n '--user',\n `${host.userId}:${host.groupId ?? host.userId}`,\n ...(engine === 'podman' ? ['--userns=keep-id'] : []),\n ]\n : []),\n // Last before the image so a configured option wins over the defaults\n // above it, and cannot be mistaken for the image or its command.\n ...(input.runFlags ?? []),\n imageTag,\n // Only a mounted socket needs the bridge: a broker on a host port is\n // addressed directly, so the launcher runs unwrapped.\n ...(broker?.endpoint.transport === 'unix' ? [NODE_BINARY, BRIDGE_CONTAINER_PATH] : []),\n LAUNCHER_BINARY,\n ...input.forwardArgs,\n ];\n\n return { imageTag, runArgs };\n}\n","import { spawn } from 'node:child_process';\n\nimport type { EngineCaptureResult, EngineProcessRunner, EngineRunOptions } from '../../types/sandboxHarness';\n\nconst INHERIT = 'inherit';\nconst PIPE = 'pipe';\nconst IGNORE = 'ignore';\nconst FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];\n\n/** Spawns the container engine, keeping the session's terminal attached. */\nexport class SpawnEngineProcessRunner implements EngineProcessRunner {\n run(command: string, args: string[], options: EngineRunOptions = {}): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options.input === undefined ? INHERIT : [PIPE, INHERIT, INHERIT],\n });\n if (options.input !== undefined) child.stdin?.end(options.input);\n\n // With a shared terminal the engine sees Ctrl-C itself; forwarding covers\n // detached automation runs, and a duplicate signal is harmless.\n const forwarders = FORWARDED_SIGNALS.map((signal) => {\n const forward = (): void => {\n child.kill(signal);\n };\n process.on(signal, forward);\n return { signal, forward };\n });\n const cleanup = (): void => {\n for (const { signal, forward } of forwarders) process.off(signal, forward);\n };\n\n child.once('error', (error) => {\n cleanup();\n reject(error);\n });\n child.once('exit', (code, signal) => {\n cleanup();\n resolve(code ?? (signal ? 1 : 0));\n });\n });\n }\n\n capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined> {\n return new Promise((resolve) => {\n const child = spawn(command, args, { stdio: [IGNORE, PIPE, IGNORE] });\n let stdout = '';\n child.stdout?.on('data', (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.once('error', () => {\n resolve(undefined);\n });\n child.once('exit', (code) => {\n resolve({ exitCode: code ?? 1, stdout });\n });\n });\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n bootstrapCommand,\n containerWorkspacePath,\n mapForwardArgs,\n DEVCONTAINER_CLI_PACKAGE,\n DEVCONTAINER_CONFIG_PATHS,\n devcontainerExecArgs,\n devcontainerUpArgs,\n parseDevcontainerUp,\n} from '.';\nimport type { EngineProcessRunner, SandboxEngine } from '../../types/sandboxHarness';\n\nconst DEVCONTAINER_BINARY = 'devcontainer';\nconst NPX_BINARY = 'npx';\nconst LAUNCHER_BINARY = 'doompi';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Locates the workspace's own dev container configuration, if it has one. */\nexport function findDevcontainerConfig(repoRoot: string): string | undefined {\n for (const relative of DEVCONTAINER_CONFIG_PATHS) {\n const candidate = path.join(repoRoot, relative);\n if (fs.existsSync(candidate)) return candidate;\n }\n return undefined;\n}\n\n/**\n * Resolves how to invoke the Dev Containers CLI.\n *\n * A local install is preferred so a launch does not depend on the network;\n * npx is the fallback, pinned so the tool cannot change underneath a session.\n */\nexport async function resolveDevcontainerCli(\n runner: EngineProcessRunner,\n): Promise<{ command: string; prefix: string[] }> {\n const probe = await runner.capture(DEVCONTAINER_BINARY, ['--version']);\n if (probe?.exitCode === 0) return { command: DEVCONTAINER_BINARY, prefix: [] };\n return { command: NPX_BINARY, prefix: ['-y', DEVCONTAINER_CLI_PACKAGE] };\n}\n\nexport interface DevcontainerSessionOptions {\n repoRoot: string;\n cwd: string;\n forwardArgs: readonly string[];\n environment: Readonly<Record<string, string>>;\n engine: SandboxEngine;\n runner: EngineProcessRunner;\n version: string;\n hasTty: boolean;\n /** Host directory holding the broker socket, when the broker uses one. */\n socketDirectory?: string;\n onProgress?: (message: string) => void;\n}\n\n/**\n * Runs the session inside the workspace's own dev container.\n *\n * The container is brought up by the Dev Containers CLI, so the file decides\n * the image, features, mounts and lifecycle hooks: the session gets the\n * project's real toolchain, and this layer's isolation guarantees do not\n * apply beyond the environment it passes in.\n */\nexport async function runDevcontainerSession(options: DevcontainerSessionOptions): Promise<number> {\n const cli = await resolveDevcontainerCli(options.runner);\n options.onProgress?.('starting the workspace dev container');\n\n const upArgs = devcontainerUpArgs({\n repoRoot: options.repoRoot,\n socketDirectory: options.socketDirectory,\n socketTarget: options.socketDirectory ? BROKER_MOUNT_DIRECTORY : undefined,\n });\n const up = await options.runner.capture(cli.command, [...cli.prefix, ...upArgs]);\n if (!up) {\n throw new Error('The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.');\n }\n const { containerId, remoteWorkspaceFolder, error } = parseDevcontainerUp(up.stdout);\n if (!containerId) throw new Error(`The dev container did not start: ${error ?? `exit ${up.exitCode}`}`);\n\n const bootstrap = await options.runner.run(options.engine, [\n 'exec',\n containerId,\n ...bootstrapCommand(options.version),\n ]);\n if (bootstrap !== 0) {\n throw new Error(\n `DoomPi could not be installed in the dev container (exit ${bootstrap}). ` +\n 'Add it to the dev container, or unset the dev container preference to use the built-in image.',\n );\n }\n\n options.onProgress?.(`attaching to dev container ${containerId.slice(0, 12)}`);\n return options.runner.run(\n options.engine,\n devcontainerExecArgs({\n containerId,\n cwd: remoteWorkspaceFolder\n ? containerWorkspacePath(options.repoRoot, options.cwd, remoteWorkspaceFolder)\n : options.cwd,\n environment: options.environment,\n hasTty: options.hasTty,\n command: [\n LAUNCHER_BINARY,\n ...(remoteWorkspaceFolder\n ? mapForwardArgs(options.forwardArgs, options.repoRoot, remoteWorkspaceFolder)\n : options.forwardArgs),\n ],\n }),\n );\n}\n","import net from 'node:net';\n\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\n\nconst LOOPBACK = '127.0.0.1';\n\nfunction portIsFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = net.createServer();\n probe.once('error', () => resolve(false));\n probe.listen(port, LOOPBACK, () => {\n probe.close(() => resolve(true));\n });\n });\n}\n\n/**\n * Reports which OAuth callback ports this launch may publish.\n *\n * A taken port means another sandbox already holds it, so publishing would\n * fail the whole launch over a login the session may never attempt. Skipping\n * it costs only the ability to log in from inside this particular container.\n */\nexport async function availableLoginPorts(ports: readonly number[] = OAUTH_CALLBACK_PORTS): Promise<number[]> {\n const free: number[] = [];\n for (const port of ports) {\n if (await portIsFree(port)) free.push(port);\n }\n return free;\n}\n","/**\n * Splits configured engine options into argv entries.\n *\n * Whitespace separated rather than shell parsed: the engine is spawned without\n * a shell, so quoting rules would only invent a syntax the caller does not\n * actually get.\n */\nexport function parseRunFlags(configured: string | undefined): string[] {\n return (configured ?? '').split(/\\s+/).filter((flag) => flag.length > 0);\n}\n\nconst OPTION_FORM = /^--?[^\\s=]+(=.*)?$/s;\n\n/**\n * Requires every configured entry to be a self-contained option.\n *\n * Separated values cannot be told from positional arguments without knowing\n * each option's arity, and a stray positional would silently replace the image\n * or its command. Demanding `--flag=value` removes the ambiguity instead of\n * guessing at it.\n */\nexport function assertRunFlags(flags: readonly string[], optionName: string): void {\n const invalid = flags.find((flag) => !OPTION_FORM.test(flag));\n if (invalid !== undefined) {\n throw new Error(\n `${optionName} accepts engine options in --flag or --flag=value form; \"${invalid}\" is neither. ` +\n 'Write a value as part of its option, for example --runtime=runsc.',\n );\n }\n}\n","import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from '../sandboxBridge';\n\nconst IMAGE_REPOSITORY = 'doompi-sandbox';\nconst TAG_SAFE = /[^A-Za-z0-9_.-]/g;\n\n/**\n * Names one image build: a distribution version plus its image definition.\n *\n * The digest matters as much as the version. Editing the Dockerfile or the\n * bridge without it would silently reuse a cached image that predates the\n * change.\n */\nexport function formatImageTag(version: string, digest: string): string {\n return `${IMAGE_REPOSITORY}:v${version.replaceAll(TAG_SAFE, '-')}-${digest}`;\n}\n\n/**\n * Self-contained Linux image for sandboxed sessions.\n *\n * nano is present because Pi falls back to it for the external editor when\n * VISUAL and EDITOR are unset, which is always the case here: a host editor\n * variable naming a desktop application would resolve to a binary the\n * container does not have, so it is deliberately not forwarded.\n *\n * The distribution installs from the registry rather than mounting host\n * modules: host installs carry platform-specific binaries a Linux container\n * cannot run. DOOMPI_SANDBOX is baked in so nothing that reaches a shell in\n * the container can present itself as an unsandboxed session.\n */\nexport function sandboxDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi@${DOOMPI_VERSION}',\n `COPY ${BRIDGE_FILE_NAME} ${BRIDGE_CONTAINER_PATH}`,\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n\n/**\n * Image for the cockpit container, which holds the hub rather than one session.\n *\n * Kept separate from the sandbox image rather than folded into it. The cockpit\n * needs the web package and a tunnel client, and making every interactive\n * `--sandbox` launch pay for an extra apt repository and a larger download to\n * get them would be a poor trade.\n *\n * Installing `@agimon-ai/doompi-web` is enough for all three programs: its\n * `@agimon-ai/doompi` dependency owns both the session server executable and\n * the agent runtime.\n */\nexport function cockpitDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n // The tunnel runs beside the hub rather than on the host, so the cockpit\n // owns its own remote access and all of that state stays in one place.\n 'RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\\\',\n ' -o /usr/share/keyrings/cloudflare-main.gpg \\\\',\n ' && echo \"deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main\" \\\\',\n ' > /etc/apt/sources.list.d/cloudflared.list \\\\',\n ' && apt-get update && apt-get install -y --no-install-recommends cloudflared \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG DOOMPI_VERSION',\n 'RUN npm install -g @agimon-ai/doompi-web@${DOOMPI_VERSION}',\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /doompi-home',\n 'ENV DOOMPI_SANDBOX=1 HOME=/doompi-home',\n '',\n ].join('\\n');\n}\n","import { createHash } from 'node:crypto';\n\nimport { sandboxBridgeSource } from '../sandboxBridge';\nimport { cockpitDockerfile, formatImageTag, sandboxDockerfile } from '../sandboxImage';\n\nconst DIGEST_LENGTH = 8;\n\n/** Tags the image with the distribution version and a digest of its definition. */\nexport function sandboxImageTag(version: string): string {\n const digest = createHash('sha256')\n .update(sandboxDockerfile())\n .update(sandboxBridgeSource())\n .digest('hex')\n .slice(0, DIGEST_LENGTH);\n return formatImageTag(version, digest);\n}\n\n/**\n * Tags the cockpit image separately from the sandbox one.\n *\n * Same digest scheme, different definition, so editing either Dockerfile\n * invalidates only its own image.\n */\nexport function cockpitImageTag(version: string): string {\n const digest = createHash('sha256').update(cockpitDockerfile()).digest('hex').slice(0, DIGEST_LENGTH);\n return formatImageTag(`${version}-cockpit`, digest);\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { startBroker, type RunningBroker } from '../brokerHost';\nimport { DEVCONTAINER_DISABLED_ENV } from '../devcontainer';\nimport { findDevcontainerConfig, runDevcontainerSession } from '../devcontainer/runtime';\nimport { SpawnEngineProcessRunner } from '../engineProcess';\nimport { availableLoginPorts } from '../loginPorts';\nimport { OAUTH_CALLBACK_PORTS } from '../oauthCallback';\nimport { assertRunFlags, parseRunFlags } from '../runFlags';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../sandboxBridge';\nimport { sandboxDockerfile } from '../sandboxImage';\nimport { sandboxImageTag } from '../sandboxImageTag';\nimport { buildSandboxPlan, containerEnvironment } from '../sandboxPlan';\n\nconst ENGINE_ENV = 'DOOMPI_SANDBOX_ENGINE';\nconst BROKER_DISABLED_ENV = 'DOOMPI_SANDBOX_BROKER';\nconst RUN_FLAGS_ENV = 'DOOMPI_SANDBOX_RUN_FLAGS';\nconst DISABLED_VALUE = '0';\n// Every entry takes docker's run syntax, which is what the plan emits.\nconst ENGINES: SandboxEngine[] = ['docker', 'podman', 'nerdctl', 'finch'];\nconst DOCKERFILE_NAME = 'Dockerfile';\nconst MODES_FILE = path.join('.doom', 'modes.yaml');\nconst LOCAL_PACKAGE_PATTERN = /^\\s*(?:-|name:)\\s*[\"']?\\.{1,2}\\//m;\nconst REPO_KEY_LENGTH = 12;\n\n// Self-reference instead of a relative path: the file depth differs between\n// running from src and from the built dist tree.\nconst require = createRequire(import.meta.url);\nconst SELF_MANIFEST = '@agimon-ai/doompi-sandbox/package.json';\n\nexport interface SandboxLauncherDependencies {\n runner?: EngineProcessRunner;\n /** Seam for tests; defaults to probing the real OAuth callback ports. */\n loginPorts?: () => Promise<number[]>;\n /** Seam for tests; defaults to the real host broker. */\n startBroker?: (options: {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n }) => Promise<RunningBroker | undefined>;\n /** Distribution version override; defaults to this package's own version. */\n version?: string;\n hostFacts?: Partial<SandboxHostFacts>;\n}\n\nfunction distributionVersion(): string {\n return (require(SELF_MANIFEST) as { version: string }).version;\n}\n\n/** Fixed per-repository directory a reused dev container can keep mounted. */\nfunction brokerDirectory(repoRoot: string): string {\n return path.join(os.tmpdir(), `${repoKey(repoRoot)}-broker`);\n}\n\nfunction repoKey(repoRoot: string): string {\n return `doompi-sandbox-${createHash('sha256').update(repoRoot).digest('hex').slice(0, REPO_KEY_LENGTH)}`;\n}\n\nexport async function detectEngine(\n runner: EngineProcessRunner,\n environment: Readonly<Record<string, string | undefined>>,\n): Promise<SandboxEngine> {\n const configured = environment[ENGINE_ENV]?.trim();\n if (configured) {\n if ((ENGINES as string[]).includes(configured)) return configured as SandboxEngine;\n throw new Error(`${ENGINE_ENV} must be one of: ${ENGINES.join(', ')}.`);\n }\n for (const engine of ENGINES) {\n const probe = await runner.capture(engine, ['--version']);\n if (probe?.exitCode === 0) return engine;\n }\n throw new Error(`No container engine found. Install one of ${ENGINES.join(', ')}, or set ${ENGINE_ENV}.`);\n}\n\n/**\n * Builds the image if the engine does not already have it.\n *\n * Takes its definition rather than assuming one, because the interactive\n * sandbox and the cockpit container are two different images built the same\n * way. The tag already carries a digest of the definition, so a changed\n * Dockerfile misses the inspect and rebuilds.\n */\nexport async function ensureImage(\n runner: EngineProcessRunner,\n engine: SandboxEngine,\n version: string,\n onProgress: ((message: string) => void) | undefined,\n definition: { tag: string; dockerfile: string; files?: Readonly<Record<string, string>> },\n): Promise<string> {\n const tag = definition.tag;\n const inspected = await runner.capture(engine, ['image', 'inspect', tag]);\n if (inspected?.exitCode === 0) return tag;\n\n onProgress?.(`building ${tag} (first run for this DoomPi version)`);\n const context = fs.mkdtempSync(path.join(os.tmpdir(), 'doompi-sandbox-build-'));\n try {\n fs.writeFileSync(path.join(context, DOCKERFILE_NAME), definition.dockerfile);\n for (const [name, contents] of Object.entries(definition.files ?? {})) {\n fs.writeFileSync(path.join(context, name), contents);\n }\n const exitCode = await runner.run(engine, [\n 'build',\n '-t',\n tag,\n '--build-arg',\n `DOOMPI_VERSION=${version}`,\n context,\n ]);\n if (exitCode !== 0) throw new Error(`Building the ${tag} image failed with exit code ${exitCode}.`);\n } finally {\n fs.rmSync(context, { recursive: true, force: true });\n }\n return tag;\n}\n\nfunction warnAboutLocalPackages(repoRoot: string, onProgress: ((message: string) => void) | undefined): void {\n let modes: string;\n try {\n modes = fs.readFileSync(path.join(repoRoot, MODES_FILE), 'utf8');\n } catch {\n return;\n }\n if (LOCAL_PACKAGE_PATTERN.test(modes)) {\n onProgress?.(\n 'modes.yaml declares local workspace packages; their platform-specific dependencies ' +\n 'must be installed inside the container before they can load',\n );\n }\n}\n\n/** Builds the launcher with injectable seams for tests. */\nexport function createSandboxLauncher(dependencies: SandboxLauncherDependencies = {}) {\n const runner = dependencies.runner ?? new SpawnEngineProcessRunner();\n const beginBroker = dependencies.startBroker ?? startBroker;\n const resolveLoginPorts = dependencies.loginPorts ?? availableLoginPorts;\n return {\n async launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n const engine = await detectEngine(runner, request.environment);\n const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);\n assertRunFlags(runFlags, RUN_FLAGS_ENV);\n request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(' ')}` : ''}`);\n\n const version = dependencies.version ?? distributionVersion();\n // A workspace that describes its own container is describing the\n // toolchain its agent needs, so that container wins over the built-in\n // image. Nothing here is built or pulled until that choice is made.\n const devcontainer =\n request.environment[DEVCONTAINER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : findDevcontainerConfig(request.repoRoot);\n const imageTag = devcontainer\n ? undefined\n : await ensureImage(runner, engine, version, request.onProgress, {\n tag: sandboxImageTag(version),\n dockerfile: sandboxDockerfile(),\n files: { [BRIDGE_FILE_NAME]: sandboxBridgeSource() },\n });\n if (devcontainer) {\n request.onProgress?.(`using the workspace dev container from ${path.relative(request.repoRoot, devcontainer)}`);\n request.onProgress?.(\n 'its configuration owns the mounts and run arguments, so this is not an isolation boundary',\n );\n } else {\n warnAboutLocalPackages(request.repoRoot, request.onProgress);\n }\n\n const broker =\n request.environment[BROKER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : await beginBroker({\n environment: request.environment,\n onDenied: (reason) => request.onProgress?.(`broker refused a call: ${reason}`),\n // A reused dev container keeps the mounts it was created with,\n // so the socket has to be at the same path on every launch.\n ...(devcontainer ? { socketDirectory: brokerDirectory(request.repoRoot) } : {}),\n });\n if (broker) {\n request.onProgress?.(`brokering ${broker.providers.join(', ')}; provider keys stay on the host`);\n }\n\n const loginPorts = devcontainer ? [] : await resolveLoginPorts();\n if (!devcontainer && loginPorts.length < OAUTH_CALLBACK_PORTS.length) {\n request.onProgress?.(\n 'some OAuth callback ports are already taken; /login inside this sandbox may not complete',\n );\n }\n\n try {\n const hasTty = process.stdin.isTTY === true && process.stdout.isTTY === true;\n if (devcontainer) {\n return await runDevcontainerSession({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: containerEnvironment(request.environment, broker),\n engine,\n runner,\n version,\n hasTty,\n ...(broker?.endpoint.transport === 'unix' ? { socketDirectory: broker.endpoint.socketDirectory } : {}),\n ...(request.onProgress ? { onProgress: request.onProgress } : {}),\n });\n }\n\n const host: SandboxHostFacts = {\n hasTty,\n platform: process.platform,\n userId: process.getuid?.(),\n groupId: process.getgid?.(),\n repoKey: repoKey(request.repoRoot),\n version,\n ...dependencies.hostFacts,\n };\n const plan = buildSandboxPlan({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: request.environment,\n engine,\n host,\n imageTag: imageTag as string,\n runFlags,\n loginPorts,\n broker,\n });\n request.onProgress?.(`starting contained session in ${imageTag}`);\n return await runner.run(engine, plan.runArgs);\n } finally {\n await broker?.stop();\n }\n },\n };\n}\n\n/** Contract entry the DoomPi harness resolves for `--sandbox`. */\nexport function launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n return createSandboxLauncher().launchSandbox(request);\n}\n"],"mappings":"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"}
|