@agimon-ai/doompi-sandbox 0.0.1-alpha.39 → 0.0.1-alpha.40
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/README.md +4 -4
- package/dist/adapters/cockpitHarness.cjs +3 -0
- package/dist/adapters/cockpitHarness.cjs.map +1 -0
- package/dist/adapters/cockpitHarness.d.cts +20 -0
- package/dist/adapters/cockpitHarness.d.cts.map +1 -0
- package/dist/adapters/cockpitHarness.d.mts +20 -0
- package/dist/adapters/cockpitHarness.d.mts.map +1 -0
- package/dist/adapters/cockpitHarness.mjs +3 -0
- package/dist/adapters/cockpitHarness.mjs.map +1 -0
- package/dist/adapters/harness.cjs +1 -1
- package/dist/adapters/harness.cjs.map +1 -1
- package/dist/adapters/harness.d.cts.map +1 -1
- package/dist/adapters/harness.d.mts.map +1 -1
- package/dist/adapters/harness.mjs +1 -1
- package/dist/adapters/harness.mjs.map +1 -1
- package/dist/adapters/sandboxImageTag.cjs +1 -1
- package/dist/adapters/sandboxImageTag.cjs.map +1 -1
- package/dist/adapters/sandboxImageTag.mjs +1 -1
- package/dist/adapters/sandboxImageTag.mjs.map +1 -1
- package/dist/cockpitHarness.cjs +1 -0
- package/dist/cockpitHarness.d.cts +2 -0
- package/dist/cockpitHarness.d.mts +2 -0
- package/dist/cockpitHarness.mjs +1 -0
- package/dist/services/cockpitPlan.cjs +2 -0
- package/dist/services/cockpitPlan.cjs.map +1 -0
- package/dist/services/cockpitPlan.d.cts +10 -0
- package/dist/services/cockpitPlan.d.cts.map +1 -0
- package/dist/services/cockpitPlan.d.mts +10 -0
- package/dist/services/cockpitPlan.d.mts.map +1 -0
- package/dist/services/cockpitPlan.mjs +2 -0
- package/dist/services/cockpitPlan.mjs.map +1 -0
- package/dist/services/sandboxImage.cjs +2 -1
- package/dist/services/sandboxImage.cjs.map +1 -1
- package/dist/services/sandboxImage.mjs +2 -1
- package/dist/services/sandboxImage.mjs.map +1 -1
- package/dist/services/sandboxPlan.cjs +1 -1
- package/dist/services/sandboxPlan.mjs +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @agimon-ai/doompi-sandbox
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run the DoomPi agent, extensions, and tools in a container while keeping the terminal on the host.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi). You can also install it directly in [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent).
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@ pi install npm:@agimon-ai/doompi-sandbox
|
|
|
18
18
|
|
|
19
19
|
The package declares its Pi extension entry, so Pi loads it after installation. DoomPi users can include the package through their normal profile and domain composition instead.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Enable the sandbox
|
|
22
22
|
|
|
23
23
|
This layer is still in development, so `doompi init` does not add it to any mode. Opt in by hand:
|
|
24
24
|
name it as a layer in `.doom/modes.yaml`, then list that layer on the mode you want it in.
|
|
@@ -123,7 +123,7 @@ so. A second concurrent sandbox therefore starts normally but cannot complete a
|
|
|
123
123
|
that bind an ephemeral callback port instead of a fixed one, OpenRouter among them, cannot be
|
|
124
124
|
published ahead of the flow and are not covered.
|
|
125
125
|
|
|
126
|
-
## Terminal
|
|
126
|
+
## Terminal behavior
|
|
127
127
|
|
|
128
128
|
The session is Pi's own TUI running inside the container, attached straight to your terminal: the
|
|
129
129
|
launch passes `-i`, adds `-t` when the host session has one, and inherits stdio. Nothing proxies or
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const e=require("../services/cockpitPlan.cjs"),t=require("../services/runFlags.cjs"),n=require("../services/sandboxImage.cjs"),r=require("./sandboxImageTag.cjs"),i=require("./engineProcess.cjs"),a=require("./harness.cjs");let o=require("node:child_process");const s=`DOOMPI_SANDBOX_RUN_FLAGS`;function c(){let e=e=>{try{let t=(0,o.execFileSync)(`git`,[`config`,`--global`,`--get`,e],{encoding:`utf8`}).trim();return t===``?void 0:t}catch{return}},t=e(`user.name`),n=e(`user.email`);return t!==void 0&&n!==void 0?{name:t,email:n}:void 0}async function l(e){try{let t=await fetch(`http://127.0.0.1:${String(e)}/api/health`);return t.ok?(await t.json()).role===`hub`:!1}catch{return!1}}async function u(e,t,n){let r=await e.capture(t,[`stop`,n]);return await e.capture(t,[`rm`,`-f`,n]),r?.exitCode===0}async function d(e,t,n){let r=await e.capture(t,[`inspect`,`-f`,`{{.State.Running}}`,n]);return r?.exitCode===0&&r.stdout.trim()===`true`}function f(o={}){let f=o.runner??new i.SpawnEngineProcessRunner,p=o.probe??l,m=o.gitIdentity??c,h=o.now??(()=>Date.now());return{async startCockpitContainer(i){if(i.workspaces.length===0)return{ok:!1,error:`A cockpit container needs at least one workspace to mount.`};let c;try{c=await a.detectEngine(f,i.environment)}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}let l=t.parseRunFlags(i.environment[s]);try{t.assertRunFlags(l,s)}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}i.onProgress?.(`using ${c}${l.length>0?` with ${l.join(` `)}`:``}`);let g=o.version??`0.0.0`,_;try{_=await a.ensureImage(f,c,g,i.onProgress,{tag:r.cockpitImageTag(g),dockerfile:n.cockpitDockerfile()})}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}let v=e.buildCockpitPlan({workspaces:i.workspaces,port:i.port,environment:i.environment,engine:c,host:{hasTty:!1,platform:process.platform,repoKey:``,version:g},imageTag:_,runFlags:l,...m()===void 0?{}:{gitIdentity:m()}}),y=await f.capture(c,v.runArgs),b=y?.stdout.trim().split(`
|
|
2
|
+
`).pop()??``;if(y?.exitCode!==0||b===``)return{ok:!1,error:`The cockpit container did not start: ${y?.stdout.trim()??`no output`}`};i.onProgress?.(`waiting for the cockpit to answer`);let x=h()+6e4;for(;!await p(i.port);){if(h()>=x){let e=await f.capture(c,[`logs`,`--tail`,`20`,b]);return await u(f,c,b),{ok:!1,error:`The cockpit did not answer within 60s.\n${e?.stdout??``}`.trim()}}await new Promise(e=>setTimeout(e,250))}return{ok:!0,handle:{containerId:b,stop:async()=>{await u(f,c,b)},alive:async()=>await d(f,c,b)}}},async reapCockpitContainer(e){for(let t of[`docker`,`podman`,`nerdctl`,`finch`])if((await f.capture(t,[`--version`]))?.exitCode===0)return await u(f,t,e);return!1}}}async function p(e){return await f().startCockpitContainer(e)}async function m(e){return await f().reapCockpitContainer(e)}exports.createCockpitHarness=f,exports.reapCockpitContainer=m,exports.startCockpitContainer=p;
|
|
3
|
+
//# sourceMappingURL=cockpitHarness.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitHarness.cjs","names":["execFileSync","SpawnEngineProcessRunner","detectEngine","parseRunFlags","ensureImage","cockpitImageTag","cockpitDockerfile","buildCockpitPlan"],"sources":["../../src/adapters/cockpitHarness.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport type {\n CockpitContainerHandle,\n CockpitContainerHarnessModule,\n CockpitContainerRequest,\n CockpitContainerStart,\n} from '@agimon-ai/doompi-extension-contracts/cockpit-container';\nimport { buildCockpitPlan, type CockpitGitIdentity } from '../services/cockpitPlan.ts';\nimport { parseRunFlags, assertRunFlags } from '../services/runFlags.ts';\nimport { cockpitDockerfile } from '../services/sandboxImage.ts';\nimport type { EngineProcessRunner, SandboxEngine } from '../types/sandboxHarness.ts';\nimport { detectEngine, ensureImage } from './harness.ts';\nimport { cockpitImageTag } from './sandboxImageTag.ts';\nimport { SpawnEngineProcessRunner } from './engineProcess.ts';\n\n/**\n * Runs the cockpit itself in a container, detached, holding the hub and every\n * session it goes on to spawn.\n *\n * The interactive harness next door wraps one session and blocks until it\n * exits. This one returns a handle while the container keeps running, which is\n * why it is a separate entry rather than a flag on the other.\n */\n\nconst RUN_FLAGS_ENV = 'DOOMPI_SANDBOX_RUN_FLAGS';\nconst READY_TIMEOUT_MS = 60_000;\nconst READY_POLL_MS = 250;\nconst HUB_ROLE = 'hub';\n\nexport interface CockpitHarnessDependencies {\n runner?: EngineProcessRunner;\n version?: string;\n /** Test seam: reports whether the cockpit inside is answering yet. */\n probe?: (port: number) => Promise<boolean>;\n /** Test seam: the host's git identity, which is read from the real config by default. */\n gitIdentity?: () => CockpitGitIdentity | undefined;\n now?: () => number;\n}\n\n/**\n * Reads the host's committer identity.\n *\n * Passed into the container as environment rather than by mounting the config,\n * so the agent can commit without any credential crossing the boundary. It\n * cannot push, and that is the intended limit.\n */\nfunction readGitIdentity(): CockpitGitIdentity | undefined {\n const read = (key: string): string | undefined => {\n try {\n const value = execFileSync('git', ['config', '--global', '--get', key], { encoding: 'utf8' }).trim();\n return value === '' ? undefined : value;\n } catch {\n // No git, or no such key. An agent that cannot commit is a smaller\n // problem than a launch that refuses to start.\n return undefined;\n }\n };\n const name = read('user.name');\n const email = read('user.email');\n return name !== undefined && email !== undefined ? { name, email } : undefined;\n}\n\n/** Answers whether the hub inside the container is serving yet. */\nasync function defaultProbe(port: number): Promise<boolean> {\n try {\n const response = await fetch(`http://127.0.0.1:${String(port)}/api/health`);\n if (!response.ok) return false;\n return ((await response.json()) as { role?: unknown }).role === HUB_ROLE;\n } catch {\n return false;\n }\n}\n\nasync function stopContainer(runner: EngineProcessRunner, engine: SandboxEngine, id: string): Promise<boolean> {\n const stopped = await runner.capture(engine, ['stop', id]);\n // Removed explicitly because the run is not --rm: a container that dies on\n // startup has to survive long enough for its logs to be read.\n await runner.capture(engine, ['rm', '-f', id]);\n return stopped?.exitCode === 0;\n}\n\n/**\n * Whether the engine still reports this container as running.\n *\n * A removed container makes `inspect` fail rather than print `false`, so a\n * non-zero exit is read the same way: not running.\n */\nasync function isRunning(runner: EngineProcessRunner, engine: SandboxEngine, id: string): Promise<boolean> {\n const probed = await runner.capture(engine, ['inspect', '-f', '{{.State.Running}}', id]);\n return probed?.exitCode === 0 && probed.stdout.trim() === 'true';\n}\n\nexport function createCockpitHarness(dependencies: CockpitHarnessDependencies = {}): CockpitContainerHarnessModule {\n const runner = dependencies.runner ?? new SpawnEngineProcessRunner();\n const probe = dependencies.probe ?? defaultProbe;\n const readIdentity = dependencies.gitIdentity ?? readGitIdentity;\n const now = dependencies.now ?? ((): number => Date.now());\n\n return {\n async startCockpitContainer(request: CockpitContainerRequest): Promise<CockpitContainerStart> {\n if (request.workspaces.length === 0) {\n return { ok: false, error: 'A cockpit container needs at least one workspace to mount.' };\n }\n let engine: SandboxEngine;\n try {\n engine = await detectEngine(runner, request.environment);\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);\n try {\n assertRunFlags(runFlags, RUN_FLAGS_ENV);\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(' ')}` : ''}`);\n\n const version = dependencies.version ?? '0.0.0';\n let imageTag: string;\n try {\n // Before the tunnel is reported ready, never inside a session spawn:\n // a cold build is minutes and the hub gives a spawning session ten\n // seconds to register.\n imageTag = await ensureImage(runner, engine, version, request.onProgress, {\n tag: cockpitImageTag(version),\n dockerfile: cockpitDockerfile(),\n });\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n\n const plan = buildCockpitPlan({\n workspaces: request.workspaces,\n port: request.port,\n environment: request.environment,\n engine,\n host: { hasTty: false, platform: process.platform, repoKey: '', version },\n imageTag,\n runFlags,\n ...(readIdentity() === undefined ? {} : { gitIdentity: readIdentity() as CockpitGitIdentity }),\n });\n\n const started = await runner.capture(engine, plan.runArgs);\n const containerId = started?.stdout.trim().split('\\n').pop() ?? '';\n if (started?.exitCode !== 0 || containerId === '') {\n return { ok: false, error: `The cockpit container did not start: ${started?.stdout.trim() ?? 'no output'}` };\n }\n request.onProgress?.('waiting for the cockpit to answer');\n\n const deadline = now() + READY_TIMEOUT_MS;\n for (;;) {\n if (await probe(request.port)) break;\n if (now() >= deadline) {\n const logs = await runner.capture(engine, ['logs', '--tail', '20', containerId]);\n await stopContainer(runner, engine, containerId);\n return {\n ok: false,\n error:\n `The cockpit did not answer within ${String(READY_TIMEOUT_MS / 1000)}s.\\n${logs?.stdout ?? ''}`.trim(),\n };\n }\n await new Promise((resolve) => setTimeout(resolve, READY_POLL_MS));\n }\n\n const handle: CockpitContainerHandle = {\n containerId,\n stop: async () => {\n await stopContainer(runner, engine, containerId);\n },\n alive: async () => await isRunning(runner, engine, containerId),\n };\n return { ok: true, handle };\n },\n\n async reapCockpitContainer(containerId: string): Promise<boolean> {\n // Named by id rather than searched for, because the caller recorded it\n // before the process that owned it went away.\n for (const engine of ['docker', 'podman', 'nerdctl', 'finch'] as const) {\n const probed = await runner.capture(engine, ['--version']);\n if (probed?.exitCode !== 0) continue;\n return await stopContainer(runner, engine, containerId);\n }\n return false;\n },\n };\n}\n\n/** The module-shaped entry a host resolves from the composition. */\nexport async function startCockpitContainer(request: CockpitContainerRequest): Promise<CockpitContainerStart> {\n return await createCockpitHarness().startCockpitContainer(request);\n}\n\nexport async function reapCockpitContainer(containerId: string): Promise<boolean> {\n return await createCockpitHarness().reapCockpitContainer(containerId);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EngineProcessRunner } from "../types/sandboxHarness.cjs";
|
|
2
|
+
import { CockpitGitIdentity } from "../services/cockpitPlan.cjs";
|
|
3
|
+
import { CockpitContainerHarnessModule, CockpitContainerRequest, CockpitContainerStart } from "@agimon-ai/doompi-extension-contracts/cockpit-container";
|
|
4
|
+
//#region src/adapters/cockpitHarness.d.ts
|
|
5
|
+
interface CockpitHarnessDependencies {
|
|
6
|
+
runner?: EngineProcessRunner;
|
|
7
|
+
version?: string;
|
|
8
|
+
/** Test seam: reports whether the cockpit inside is answering yet. */
|
|
9
|
+
probe?: (port: number) => Promise<boolean>;
|
|
10
|
+
/** Test seam: the host's git identity, which is read from the real config by default. */
|
|
11
|
+
gitIdentity?: () => CockpitGitIdentity | undefined;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
}
|
|
14
|
+
declare function createCockpitHarness(dependencies?: CockpitHarnessDependencies): CockpitContainerHarnessModule;
|
|
15
|
+
/** The module-shaped entry a host resolves from the composition. */
|
|
16
|
+
declare function startCockpitContainer(request: CockpitContainerRequest): Promise<CockpitContainerStart>;
|
|
17
|
+
declare function reapCockpitContainer(containerId: string): Promise<boolean>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { CockpitHarnessDependencies, createCockpitHarness, reapCockpitContainer, startCockpitContainer };
|
|
20
|
+
//# sourceMappingURL=cockpitHarness.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitHarness.d.cts","names":[],"sources":["../../src/adapters/cockpitHarness.ts"],"mappings":";;;;UA6BiB;EACf,SAAS;EACT;;EAEA,SAAS,iBAAiB;;EAE1B,oBAAoB;EACpB;;iBAwDc,qBAAqB,eAAc,6BAAkC;;iBAgG/D,sBAAsB,SAAS,0BAA0B,QAAQ;iBAIjE,qBAAqB,sBAAsB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EngineProcessRunner } from "../types/sandboxHarness.mjs";
|
|
2
|
+
import { CockpitGitIdentity } from "../services/cockpitPlan.mjs";
|
|
3
|
+
import { CockpitContainerHarnessModule, CockpitContainerRequest, CockpitContainerStart } from "@agimon-ai/doompi-extension-contracts/cockpit-container";
|
|
4
|
+
//#region src/adapters/cockpitHarness.d.ts
|
|
5
|
+
interface CockpitHarnessDependencies {
|
|
6
|
+
runner?: EngineProcessRunner;
|
|
7
|
+
version?: string;
|
|
8
|
+
/** Test seam: reports whether the cockpit inside is answering yet. */
|
|
9
|
+
probe?: (port: number) => Promise<boolean>;
|
|
10
|
+
/** Test seam: the host's git identity, which is read from the real config by default. */
|
|
11
|
+
gitIdentity?: () => CockpitGitIdentity | undefined;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
}
|
|
14
|
+
declare function createCockpitHarness(dependencies?: CockpitHarnessDependencies): CockpitContainerHarnessModule;
|
|
15
|
+
/** The module-shaped entry a host resolves from the composition. */
|
|
16
|
+
declare function startCockpitContainer(request: CockpitContainerRequest): Promise<CockpitContainerStart>;
|
|
17
|
+
declare function reapCockpitContainer(containerId: string): Promise<boolean>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { CockpitHarnessDependencies, createCockpitHarness, reapCockpitContainer, startCockpitContainer };
|
|
20
|
+
//# sourceMappingURL=cockpitHarness.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitHarness.d.mts","names":[],"sources":["../../src/adapters/cockpitHarness.ts"],"mappings":";;;;UA6BiB;EACf,SAAS;EACT;;EAEA,SAAS,iBAAiB;;EAE1B,oBAAoB;EACpB;;iBAwDc,qBAAqB,eAAc,6BAAkC;;iBAgG/D,sBAAsB,SAAS,0BAA0B,QAAQ;iBAIjE,qBAAqB,sBAAsB"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{buildCockpitPlan as e}from"../services/cockpitPlan.mjs";import{assertRunFlags as t,parseRunFlags as n}from"../services/runFlags.mjs";import{cockpitDockerfile as r}from"../services/sandboxImage.mjs";import{cockpitImageTag as i}from"./sandboxImageTag.mjs";import{SpawnEngineProcessRunner as a}from"./engineProcess.mjs";import{detectEngine as o,ensureImage as s}from"./harness.mjs";import{execFileSync as c}from"node:child_process";const l=`DOOMPI_SANDBOX_RUN_FLAGS`;function u(){let e=e=>{try{let t=c(`git`,[`config`,`--global`,`--get`,e],{encoding:`utf8`}).trim();return t===``?void 0:t}catch{return}},t=e(`user.name`),n=e(`user.email`);return t!==void 0&&n!==void 0?{name:t,email:n}:void 0}async function d(e){try{let t=await fetch(`http://127.0.0.1:${String(e)}/api/health`);return t.ok?(await t.json()).role===`hub`:!1}catch{return!1}}async function f(e,t,n){let r=await e.capture(t,[`stop`,n]);return await e.capture(t,[`rm`,`-f`,n]),r?.exitCode===0}async function p(e,t,n){let r=await e.capture(t,[`inspect`,`-f`,`{{.State.Running}}`,n]);return r?.exitCode===0&&r.stdout.trim()===`true`}function m(c={}){let m=c.runner??new a,h=c.probe??d,g=c.gitIdentity??u,_=c.now??(()=>Date.now());return{async startCockpitContainer(a){if(a.workspaces.length===0)return{ok:!1,error:`A cockpit container needs at least one workspace to mount.`};let u;try{u=await o(m,a.environment)}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}let d=n(a.environment[l]);try{t(d,l)}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}a.onProgress?.(`using ${u}${d.length>0?` with ${d.join(` `)}`:``}`);let v=c.version??`0.0.0`,y;try{y=await s(m,u,v,a.onProgress,{tag:i(v),dockerfile:r()})}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}let b=e({workspaces:a.workspaces,port:a.port,environment:a.environment,engine:u,host:{hasTty:!1,platform:process.platform,repoKey:``,version:v},imageTag:y,runFlags:d,...g()===void 0?{}:{gitIdentity:g()}}),x=await m.capture(u,b.runArgs),S=x?.stdout.trim().split(`
|
|
2
|
+
`).pop()??``;if(x?.exitCode!==0||S===``)return{ok:!1,error:`The cockpit container did not start: ${x?.stdout.trim()??`no output`}`};a.onProgress?.(`waiting for the cockpit to answer`);let C=_()+6e4;for(;!await h(a.port);){if(_()>=C){let e=await m.capture(u,[`logs`,`--tail`,`20`,S]);return await f(m,u,S),{ok:!1,error:`The cockpit did not answer within 60s.\n${e?.stdout??``}`.trim()}}await new Promise(e=>setTimeout(e,250))}return{ok:!0,handle:{containerId:S,stop:async()=>{await f(m,u,S)},alive:async()=>await p(m,u,S)}}},async reapCockpitContainer(e){for(let t of[`docker`,`podman`,`nerdctl`,`finch`])if((await m.capture(t,[`--version`]))?.exitCode===0)return await f(m,t,e);return!1}}}async function h(e){return await m().startCockpitContainer(e)}async function g(e){return await m().reapCockpitContainer(e)}export{m as createCockpitHarness,g as reapCockpitContainer,h as startCockpitContainer};
|
|
3
|
+
//# sourceMappingURL=cockpitHarness.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitHarness.mjs","names":[],"sources":["../../src/adapters/cockpitHarness.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport type {\n CockpitContainerHandle,\n CockpitContainerHarnessModule,\n CockpitContainerRequest,\n CockpitContainerStart,\n} from '@agimon-ai/doompi-extension-contracts/cockpit-container';\nimport { buildCockpitPlan, type CockpitGitIdentity } from '../services/cockpitPlan.ts';\nimport { parseRunFlags, assertRunFlags } from '../services/runFlags.ts';\nimport { cockpitDockerfile } from '../services/sandboxImage.ts';\nimport type { EngineProcessRunner, SandboxEngine } from '../types/sandboxHarness.ts';\nimport { detectEngine, ensureImage } from './harness.ts';\nimport { cockpitImageTag } from './sandboxImageTag.ts';\nimport { SpawnEngineProcessRunner } from './engineProcess.ts';\n\n/**\n * Runs the cockpit itself in a container, detached, holding the hub and every\n * session it goes on to spawn.\n *\n * The interactive harness next door wraps one session and blocks until it\n * exits. This one returns a handle while the container keeps running, which is\n * why it is a separate entry rather than a flag on the other.\n */\n\nconst RUN_FLAGS_ENV = 'DOOMPI_SANDBOX_RUN_FLAGS';\nconst READY_TIMEOUT_MS = 60_000;\nconst READY_POLL_MS = 250;\nconst HUB_ROLE = 'hub';\n\nexport interface CockpitHarnessDependencies {\n runner?: EngineProcessRunner;\n version?: string;\n /** Test seam: reports whether the cockpit inside is answering yet. */\n probe?: (port: number) => Promise<boolean>;\n /** Test seam: the host's git identity, which is read from the real config by default. */\n gitIdentity?: () => CockpitGitIdentity | undefined;\n now?: () => number;\n}\n\n/**\n * Reads the host's committer identity.\n *\n * Passed into the container as environment rather than by mounting the config,\n * so the agent can commit without any credential crossing the boundary. It\n * cannot push, and that is the intended limit.\n */\nfunction readGitIdentity(): CockpitGitIdentity | undefined {\n const read = (key: string): string | undefined => {\n try {\n const value = execFileSync('git', ['config', '--global', '--get', key], { encoding: 'utf8' }).trim();\n return value === '' ? undefined : value;\n } catch {\n // No git, or no such key. An agent that cannot commit is a smaller\n // problem than a launch that refuses to start.\n return undefined;\n }\n };\n const name = read('user.name');\n const email = read('user.email');\n return name !== undefined && email !== undefined ? { name, email } : undefined;\n}\n\n/** Answers whether the hub inside the container is serving yet. */\nasync function defaultProbe(port: number): Promise<boolean> {\n try {\n const response = await fetch(`http://127.0.0.1:${String(port)}/api/health`);\n if (!response.ok) return false;\n return ((await response.json()) as { role?: unknown }).role === HUB_ROLE;\n } catch {\n return false;\n }\n}\n\nasync function stopContainer(runner: EngineProcessRunner, engine: SandboxEngine, id: string): Promise<boolean> {\n const stopped = await runner.capture(engine, ['stop', id]);\n // Removed explicitly because the run is not --rm: a container that dies on\n // startup has to survive long enough for its logs to be read.\n await runner.capture(engine, ['rm', '-f', id]);\n return stopped?.exitCode === 0;\n}\n\n/**\n * Whether the engine still reports this container as running.\n *\n * A removed container makes `inspect` fail rather than print `false`, so a\n * non-zero exit is read the same way: not running.\n */\nasync function isRunning(runner: EngineProcessRunner, engine: SandboxEngine, id: string): Promise<boolean> {\n const probed = await runner.capture(engine, ['inspect', '-f', '{{.State.Running}}', id]);\n return probed?.exitCode === 0 && probed.stdout.trim() === 'true';\n}\n\nexport function createCockpitHarness(dependencies: CockpitHarnessDependencies = {}): CockpitContainerHarnessModule {\n const runner = dependencies.runner ?? new SpawnEngineProcessRunner();\n const probe = dependencies.probe ?? defaultProbe;\n const readIdentity = dependencies.gitIdentity ?? readGitIdentity;\n const now = dependencies.now ?? ((): number => Date.now());\n\n return {\n async startCockpitContainer(request: CockpitContainerRequest): Promise<CockpitContainerStart> {\n if (request.workspaces.length === 0) {\n return { ok: false, error: 'A cockpit container needs at least one workspace to mount.' };\n }\n let engine: SandboxEngine;\n try {\n engine = await detectEngine(runner, request.environment);\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);\n try {\n assertRunFlags(runFlags, RUN_FLAGS_ENV);\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(' ')}` : ''}`);\n\n const version = dependencies.version ?? '0.0.0';\n let imageTag: string;\n try {\n // Before the tunnel is reported ready, never inside a session spawn:\n // a cold build is minutes and the hub gives a spawning session ten\n // seconds to register.\n imageTag = await ensureImage(runner, engine, version, request.onProgress, {\n tag: cockpitImageTag(version),\n dockerfile: cockpitDockerfile(),\n });\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n\n const plan = buildCockpitPlan({\n workspaces: request.workspaces,\n port: request.port,\n environment: request.environment,\n engine,\n host: { hasTty: false, platform: process.platform, repoKey: '', version },\n imageTag,\n runFlags,\n ...(readIdentity() === undefined ? {} : { gitIdentity: readIdentity() as CockpitGitIdentity }),\n });\n\n const started = await runner.capture(engine, plan.runArgs);\n const containerId = started?.stdout.trim().split('\\n').pop() ?? '';\n if (started?.exitCode !== 0 || containerId === '') {\n return { ok: false, error: `The cockpit container did not start: ${started?.stdout.trim() ?? 'no output'}` };\n }\n request.onProgress?.('waiting for the cockpit to answer');\n\n const deadline = now() + READY_TIMEOUT_MS;\n for (;;) {\n if (await probe(request.port)) break;\n if (now() >= deadline) {\n const logs = await runner.capture(engine, ['logs', '--tail', '20', containerId]);\n await stopContainer(runner, engine, containerId);\n return {\n ok: false,\n error:\n `The cockpit did not answer within ${String(READY_TIMEOUT_MS / 1000)}s.\\n${logs?.stdout ?? ''}`.trim(),\n };\n }\n await new Promise((resolve) => setTimeout(resolve, READY_POLL_MS));\n }\n\n const handle: CockpitContainerHandle = {\n containerId,\n stop: async () => {\n await stopContainer(runner, engine, containerId);\n },\n alive: async () => await isRunning(runner, engine, containerId),\n };\n return { ok: true, handle };\n },\n\n async reapCockpitContainer(containerId: string): Promise<boolean> {\n // Named by id rather than searched for, because the caller recorded it\n // before the process that owned it went away.\n for (const engine of ['docker', 'podman', 'nerdctl', 'finch'] as const) {\n const probed = await runner.capture(engine, ['--version']);\n if (probed?.exitCode !== 0) continue;\n return await stopContainer(runner, engine, containerId);\n }\n return false;\n },\n };\n}\n\n/** The module-shaped entry a host resolves from the composition. */\nexport async function startCockpitContainer(request: CockpitContainerRequest): Promise<CockpitContainerStart> {\n return await createCockpitHarness().startCockpitContainer(request);\n}\n\nexport async function reapCockpitContainer(containerId: string): Promise<boolean> {\n return await createCockpitHarness().reapCockpitContainer(containerId);\n}\n"],"mappings":"obAwBA,MAAM,EAAgB,2BAsBtB,SAAS,GAAkD,CACzD,IAAM,EAAQ,GAAoC,CAChD,GAAI,CACF,IAAM,EAAQ,EAAa,MAAO,CAAC,SAAU,WAAY,QAAS,CAAG,EAAG,CAAE,SAAU,MAAO,CAAC,CAAC,CAAC,KAAK,EACnG,OAAO,IAAU,GAAK,IAAA,GAAY,CACpC,MAAQ,CAGN,MACF,CACF,EACM,EAAO,EAAK,WAAW,EACvB,EAAQ,EAAK,YAAY,EAC/B,OAAO,IAAS,IAAA,IAAa,IAAU,IAAA,GAAY,CAAE,OAAM,OAAM,EAAI,IAAA,EACvE,CAGA,eAAe,EAAa,EAAgC,CAC1D,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,oBAAoB,OAAO,CAAI,EAAE,YAAY,EAE1E,OADK,EAAS,IACL,MAAM,EAAS,KAAK,EAAA,CAA0B,OAAS,MADvC,EAE3B,MAAQ,CACN,MAAO,EACT,CACF,CAEA,eAAe,EAAc,EAA6B,EAAuB,EAA8B,CAC7G,IAAM,EAAU,MAAM,EAAO,QAAQ,EAAQ,CAAC,OAAQ,CAAE,CAAC,EAIzD,OADA,MAAM,EAAO,QAAQ,EAAQ,CAAC,KAAM,KAAM,CAAE,CAAC,EACtC,GAAS,WAAa,CAC/B,CAQA,eAAe,EAAU,EAA6B,EAAuB,EAA8B,CACzG,IAAM,EAAS,MAAM,EAAO,QAAQ,EAAQ,CAAC,UAAW,KAAM,qBAAsB,CAAE,CAAC,EACvF,OAAO,GAAQ,WAAa,GAAK,EAAO,OAAO,KAAK,IAAM,MAC5D,CAEA,SAAgB,EAAqB,EAA2C,CAAC,EAAkC,CACjH,IAAM,EAAS,EAAa,QAAU,IAAI,EACpC,EAAQ,EAAa,OAAS,EAC9B,EAAe,EAAa,aAAe,EAC3C,EAAM,EAAa,UAAsB,KAAK,IAAI,GAExD,MAAO,CACL,MAAM,sBAAsB,EAAkE,CAC5F,GAAI,EAAQ,WAAW,SAAW,EAChC,MAAO,CAAE,GAAI,GAAO,MAAO,4DAA6D,EAE1F,IAAI,EACJ,GAAI,CACF,EAAS,MAAM,EAAa,EAAQ,EAAQ,WAAW,CACzD,OAAS,EAAO,CACd,MAAO,CAAE,GAAI,GAAO,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAE,CACpF,CACA,IAAM,EAAW,EAAc,EAAQ,YAAY,EAAc,EACjE,GAAI,CACF,EAAe,EAAU,CAAa,CACxC,OAAS,EAAO,CACd,MAAO,CAAE,GAAI,GAAO,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAE,CACpF,CACA,EAAQ,aAAa,SAAS,IAAS,EAAS,OAAS,EAAI,SAAS,EAAS,KAAK,GAAG,IAAM,IAAI,EAEjG,IAAM,EAAU,EAAa,SAAW,QACpC,EACJ,GAAI,CAIF,EAAW,MAAM,EAAY,EAAQ,EAAQ,EAAS,EAAQ,WAAY,CACxE,IAAK,EAAgB,CAAO,EAC5B,WAAY,EAAkB,CAChC,CAAC,CACH,OAAS,EAAO,CACd,MAAO,CAAE,GAAI,GAAO,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAE,CACpF,CAEA,IAAM,EAAO,EAAiB,CAC5B,WAAY,EAAQ,WACpB,KAAM,EAAQ,KACd,YAAa,EAAQ,YACrB,SACA,KAAM,CAAE,OAAQ,GAAO,SAAU,QAAQ,SAAU,QAAS,GAAI,SAAQ,EACxE,WACA,WACA,GAAI,EAAa,IAAM,IAAA,GAAY,CAAC,EAAI,CAAE,YAAa,EAAa,CAAwB,CAC9F,CAAC,EAEK,EAAU,MAAM,EAAO,QAAQ,EAAQ,EAAK,OAAO,EACnD,EAAc,GAAS,OAAO,KAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,GAAK,GAChE,GAAI,GAAS,WAAa,GAAK,IAAgB,GAC7C,MAAO,CAAE,GAAI,GAAO,MAAO,wCAAwC,GAAS,OAAO,KAAK,GAAK,aAAc,EAE7G,EAAQ,aAAa,mCAAmC,EAExD,IAAM,EAAW,EAAI,EAAI,IACzB,KACM,OAAM,EAAM,EAAQ,IAAI,GADrB,CAEP,GAAI,EAAI,GAAK,EAAU,CACrB,IAAM,EAAO,MAAM,EAAO,QAAQ,EAAQ,CAAC,OAAQ,SAAU,KAAM,CAAW,CAAC,EAE/E,OADA,MAAM,EAAc,EAAQ,EAAQ,CAAW,EACxC,CACL,GAAI,GACJ,MACE,2CAA2E,GAAM,QAAU,KAAK,KAAK,CACzG,CACF,CACA,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,GAAa,CAAC,CACnE,CASA,MAAO,CAAE,GAAI,GAAM,OAAA,CANjB,cACA,KAAM,SAAY,CAChB,MAAM,EAAc,EAAQ,EAAQ,CAAW,CACjD,EACA,MAAO,SAAY,MAAM,EAAU,EAAQ,EAAQ,CAAW,CAExC,CAAE,CAC5B,EAEA,MAAM,qBAAqB,EAAuC,CAGhE,IAAK,IAAM,IAAU,CAAC,SAAU,SAAU,UAAW,OAAO,EAE1D,IAAI,MADiB,EAAO,QAAQ,EAAQ,CAAC,WAAW,CAAC,EAAA,EAC7C,WAAa,EACzB,OAAO,MAAM,EAAc,EAAQ,EAAQ,CAAW,EAExD,MAAO,EACT,CACF,CACF,CAGA,eAAsB,EAAsB,EAAkE,CAC5G,OAAO,MAAM,EAAqB,CAAC,CAAC,sBAAsB,CAAO,CACnE,CAEA,eAAsB,EAAqB,EAAuC,CAChF,OAAO,MAAM,EAAqB,CAAC,CAAC,qBAAqB,CAAW,CACtE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../_virtual/_rolldown/runtime.cjs")
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs");require("../services/devcontainer.cjs");const t=require("../services/oauthCallback.cjs"),n=require("../services/sandboxBridge.cjs"),r=require("../services/sandboxPlan.cjs"),i=require("../services/runFlags.cjs"),a=require("../services/sandboxImage.cjs"),o=require("./devcontainer.cjs"),s=require("./loginPorts.cjs"),c=require("./brokerHost.cjs"),l=require("./sandboxImageTag.cjs"),u=require("./engineProcess.cjs");let d=require("node:crypto"),f=require("node:fs");f=e.__toESM(f,1);let p=require("node:module"),m=require("node:os");m=e.__toESM(m,1);let h=require("node:path");h=e.__toESM(h,1);const g=`DOOMPI_SANDBOX_ENGINE`,_=`DOOMPI_SANDBOX_RUN_FLAGS`,v=[`docker`,`podman`,`nerdctl`,`finch`],y=h.default.join(`.doom`,`modes.yaml`),b=/^\s*(?:-|name:)\s*["']?\.{1,2}\//m,x=(0,p.createRequire)(require("url").pathToFileURL(__filename).href);function S(){return x(`@agimon-ai/doompi-sandbox/package.json`).version}function C(e){return h.default.join(m.default.tmpdir(),`${w(e)}-broker`)}function w(e){return`doompi-sandbox-${(0,d.createHash)(`sha256`).update(e).digest(`hex`).slice(0,12)}`}async function T(e,t){let n=t[g]?.trim();if(n){if(v.includes(n))return n;throw Error(`${g} must be one of: ${v.join(`, `)}.`)}for(let t of v)if((await e.capture(t,[`--version`]))?.exitCode===0)return t;throw Error(`No container engine found. Install one of ${v.join(`, `)}, or set ${g}.`)}async function E(e,t,n,r,i){let a=i.tag;if((await e.capture(t,[`image`,`inspect`,a]))?.exitCode===0)return a;r?.(`building ${a} (first run for this DoomPi version)`);let o=f.default.mkdtempSync(h.default.join(m.default.tmpdir(),`doompi-sandbox-build-`));try{f.default.writeFileSync(h.default.join(o,`Dockerfile`),i.dockerfile);for(let[e,t]of Object.entries(i.files??{}))f.default.writeFileSync(h.default.join(o,e),t);let r=await e.run(t,[`build`,`-t`,a,`--build-arg`,`DOOMPI_VERSION=${n}`,o]);if(r!==0)throw Error(`Building the ${a} image failed with exit code ${r}.`)}finally{f.default.rmSync(o,{recursive:!0,force:!0})}return a}function D(e,t){let n;try{n=f.default.readFileSync(h.default.join(e,y),`utf8`)}catch{return}b.test(n)&&t?.(`modes.yaml declares local workspace packages; their platform-specific dependencies must be installed inside the container before they can load`)}function O(e={}){let d=e.runner??new u.SpawnEngineProcessRunner,f=e.startBroker??c.startBroker,p=e.loginPorts??s.availableLoginPorts;return{async launchSandbox(s){let c=await T(d,s.environment),u=i.parseRunFlags(s.environment[_]);i.assertRunFlags(u,_),s.onProgress?.(`using ${c}${u.length>0?` with ${u.join(` `)}`:``}`);let m=e.version??S(),g=s.environment.DOOMPI_SANDBOX_DEVCONTAINER===`0`?void 0:o.findDevcontainerConfig(s.repoRoot),v=g?void 0:await E(d,c,m,s.onProgress,{tag:l.sandboxImageTag(m),dockerfile:a.sandboxDockerfile(),files:{[n.BRIDGE_FILE_NAME]:n.sandboxBridgeSource()}});g?(s.onProgress?.(`using the workspace dev container from ${h.default.relative(s.repoRoot,g)}`),s.onProgress?.(`its configuration owns the mounts and run arguments, so this is not an isolation boundary`)):D(s.repoRoot,s.onProgress);let y=s.environment.DOOMPI_SANDBOX_BROKER===`0`?void 0:await f({environment:s.environment,onDenied:e=>s.onProgress?.(`broker refused a call: ${e}`),...g?{socketDirectory:C(s.repoRoot)}:{}});y&&s.onProgress?.(`brokering ${y.providers.join(`, `)}; provider keys stay on the host`);let b=g?[]:await p();!g&&b.length<t.OAUTH_CALLBACK_PORTS.length&&s.onProgress?.(`some OAuth callback ports are already taken; /login inside this sandbox may not complete`);try{let t=process.stdin.isTTY===!0&&process.stdout.isTTY===!0;if(g)return await o.runDevcontainerSession({repoRoot:s.repoRoot,cwd:s.cwd,forwardArgs:s.forwardArgs,environment:r.containerEnvironment(s.environment,y),engine:c,runner:d,version:m,hasTty:t,...y?.endpoint.transport===`unix`?{socketDirectory:y.endpoint.socketDirectory}:{},...s.onProgress?{onProgress:s.onProgress}:{}});let n={hasTty:t,platform:process.platform,userId:process.getuid?.(),groupId:process.getgid?.(),repoKey:w(s.repoRoot),version:m,...e.hostFacts},i=r.buildSandboxPlan({repoRoot:s.repoRoot,cwd:s.cwd,forwardArgs:s.forwardArgs,environment:s.environment,engine:c,host:n,imageTag:v,runFlags:u,loginPorts:b,broker:y});return s.onProgress?.(`starting contained session in ${v}`),await d.run(c,i.runArgs)}finally{await y?.stop()}}}}function k(e){return O().launchSandbox(e)}exports.createSandboxLauncher=O,exports.detectEngine=T,exports.ensureImage=E,exports.launchSandbox=k;
|
|
2
2
|
//# sourceMappingURL=harness.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.cjs","names":["path","require","createRequire","os","createHash","sandboxImageTag","fs","sandboxDockerfile","BRIDGE_FILE_NAME","sandboxBridgeSource","SpawnEngineProcessRunner","startBroker","availableLoginPorts","parseRunFlags","findDevcontainerConfig","OAUTH_CALLBACK_PORTS","runDevcontainerSession","containerEnvironment","buildSandboxPlan"],"sources":["../../src/adapters/harness.ts"],"sourcesContent":["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';\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport { findDevcontainerConfig, runDevcontainerSession } from './devcontainer.ts';\nimport { DEVCONTAINER_DISABLED_ENV } from '../services/devcontainer.ts';\nimport { availableLoginPorts } from './loginPorts.ts';\nimport { startBroker, type RunningBroker } from './brokerHost.ts';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { buildSandboxPlan, containerEnvironment } from '../services/sandboxPlan.ts';\nimport { OAUTH_CALLBACK_PORTS } from '../services/oauthCallback.ts';\nimport { assertRunFlags, parseRunFlags } from '../services/runFlags.ts';\nimport { sandboxDockerfile } from '../services/sandboxImage.ts';\nimport { sandboxImageTag } from './sandboxImageTag.ts';\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../types/sandboxHarness.ts';\nimport { SpawnEngineProcessRunner } from './engineProcess.ts';\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\nasync 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\nasync function ensureImage(\n runner: EngineProcessRunner,\n engine: SandboxEngine,\n version: string,\n onProgress: ((message: string) => void) | undefined,\n): Promise<string> {\n const tag = sandboxImageTag(version);\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), sandboxDockerfile());\n fs.writeFileSync(path.join(context, BRIDGE_FILE_NAME), sandboxBridgeSource());\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 sandbox 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 ? undefined : await ensureImage(runner, engine, version, request.onProgress);\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
|
+
{"version":3,"file":"harness.cjs","names":["path","require","createRequire","os","createHash","fs","SpawnEngineProcessRunner","startBroker","availableLoginPorts","parseRunFlags","findDevcontainerConfig","sandboxImageTag","sandboxDockerfile","BRIDGE_FILE_NAME","sandboxBridgeSource","OAUTH_CALLBACK_PORTS","runDevcontainerSession","containerEnvironment","buildSandboxPlan"],"sources":["../../src/adapters/harness.ts"],"sourcesContent":["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';\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport { findDevcontainerConfig, runDevcontainerSession } from './devcontainer.ts';\nimport { DEVCONTAINER_DISABLED_ENV } from '../services/devcontainer.ts';\nimport { availableLoginPorts } from './loginPorts.ts';\nimport { startBroker, type RunningBroker } from './brokerHost.ts';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { buildSandboxPlan, containerEnvironment } from '../services/sandboxPlan.ts';\nimport { OAUTH_CALLBACK_PORTS } from '../services/oauthCallback.ts';\nimport { assertRunFlags, parseRunFlags } from '../services/runFlags.ts';\nimport { sandboxDockerfile } from '../services/sandboxImage.ts';\nimport { sandboxImageTag } from './sandboxImageTag.ts';\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../types/sandboxHarness.ts';\nimport { SpawnEngineProcessRunner } from './engineProcess.ts';\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":"ooBAmBA,MAAM,EAAa,wBAEb,EAAgB,2BAGhB,EAA2B,CAAC,SAAU,SAAU,UAAW,OAAO,EAElE,EAAaA,EAAAA,QAAK,KAAK,QAAS,YAAY,EAC5C,EAAwB,oCAKxBC,GAAAA,EAAUC,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,EAiB7C,SAAS,GAA8B,CACrC,OAAQD,EAAQ,wCAAa,CAAC,CAAyB,OACzD,CAGA,SAAS,EAAgB,EAA0B,CACjD,OAAOD,EAAAA,QAAK,KAAKG,EAAAA,QAAG,OAAO,EAAG,GAAG,EAAQ,CAAQ,EAAE,QAAQ,CAC7D,CAEA,SAAS,EAAQ,EAA0B,CACzC,MAAO,mBAAA,EAAkBC,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,CAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAe,GACvG,CAEA,eAAsB,EACpB,EACA,EACwB,CACxB,IAAM,EAAa,EAAY,EAAW,EAAE,KAAK,EACjD,GAAI,EAAY,CACd,GAAK,EAAqB,SAAS,CAAU,EAAG,OAAO,EACvD,MAAU,MAAM,GAAG,EAAW,mBAAmB,EAAQ,KAAK,IAAI,EAAE,EAAE,CACxE,CACA,IAAK,IAAM,KAAU,EAEnB,IAAI,MADgB,EAAO,QAAQ,EAAQ,CAAC,WAAW,CAAC,EAAA,EAC7C,WAAa,EAAG,OAAO,EAEpC,MAAU,MAAM,6CAA6C,EAAQ,KAAK,IAAI,EAAE,WAAW,EAAW,EAAE,CAC1G,CAUA,eAAsB,EACpB,EACA,EACA,EACA,EACA,EACiB,CACjB,IAAM,EAAM,EAAW,IAEvB,IAAI,MADoB,EAAO,QAAQ,EAAQ,CAAC,QAAS,UAAW,CAAG,CAAC,EAAA,EACzD,WAAa,EAAG,OAAO,EAEtC,IAAa,YAAY,EAAI,qCAAqC,EAClE,IAAM,EAAUC,EAAAA,QAAG,YAAYL,EAAAA,QAAK,KAAKG,EAAAA,QAAG,OAAO,EAAG,uBAAuB,CAAC,EAC9E,GAAI,CACF,EAAA,QAAG,cAAcH,EAAAA,QAAK,KAAK,EAAS,YAAe,EAAG,EAAW,UAAU,EAC3E,IAAK,GAAM,CAAC,EAAM,KAAa,OAAO,QAAQ,EAAW,OAAS,CAAC,CAAC,EAClE,EAAA,QAAG,cAAcA,EAAAA,QAAK,KAAK,EAAS,CAAI,EAAG,CAAQ,EAErD,IAAM,EAAW,MAAM,EAAO,IAAI,EAAQ,CACxC,QACA,KACA,EACA,cACA,kBAAkB,IAClB,CACF,CAAC,EACD,GAAI,IAAa,EAAG,MAAU,MAAM,gBAAgB,EAAI,+BAA+B,EAAS,EAAE,CACpG,QAAU,CACR,EAAA,QAAG,OAAO,EAAS,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CACrD,CACA,OAAO,CACT,CAEA,SAAS,EAAuB,EAAkB,EAA2D,CAC3G,IAAI,EACJ,GAAI,CACF,EAAQK,EAAAA,QAAG,aAAaL,EAAAA,QAAK,KAAK,EAAU,CAAU,EAAG,MAAM,CACjE,MAAQ,CACN,MACF,CACI,EAAsB,KAAK,CAAK,GAClC,IACE,gJAEF,CAEJ,CAGA,SAAgB,EAAsB,EAA4C,CAAC,EAAG,CACpF,IAAM,EAAS,EAAa,QAAU,IAAIM,EAAAA,yBACpC,EAAc,EAAa,aAAeC,EAAAA,YAC1C,EAAoB,EAAa,YAAcC,EAAAA,oBACrD,MAAO,CACL,MAAM,cAAc,EAAgD,CAClE,IAAM,EAAS,MAAM,EAAa,EAAQ,EAAQ,WAAW,EACvD,EAAWC,EAAAA,cAAc,EAAQ,YAAY,EAAc,EACjE,EAAA,eAAe,EAAU,CAAa,EACtC,EAAQ,aAAa,SAAS,IAAS,EAAS,OAAS,EAAI,SAAS,EAAS,KAAK,GAAG,IAAM,IAAI,EAEjG,IAAM,EAAU,EAAa,SAAW,EAAoB,EAItD,EACJ,EAAQ,YAAA,8BAA2C,IAC/C,IAAA,GACAC,EAAAA,uBAAuB,EAAQ,QAAQ,EACvC,EAAW,EACb,IAAA,GACA,MAAM,EAAY,EAAQ,EAAQ,EAAS,EAAQ,WAAY,CAC7D,IAAKC,EAAAA,gBAAgB,CAAO,EAC5B,WAAYC,EAAAA,kBAAkB,EAC9B,MAAO,EAAGC,EAAAA,kBAAmBC,EAAAA,oBAAoB,CAAE,CACrD,CAAC,EACD,GACF,EAAQ,aAAa,0CAA0Cd,EAAAA,QAAK,SAAS,EAAQ,SAAU,CAAY,GAAG,EAC9G,EAAQ,aACN,2FACF,GAEA,EAAuB,EAAQ,SAAU,EAAQ,UAAU,EAG7D,IAAM,EACJ,EAAQ,YAAY,wBAAyB,IACzC,IAAA,GACA,MAAM,EAAY,CAChB,YAAa,EAAQ,YACrB,SAAW,GAAW,EAAQ,aAAa,0BAA0B,GAAQ,EAG7E,GAAI,EAAe,CAAE,gBAAiB,EAAgB,EAAQ,QAAQ,CAAE,EAAI,CAAC,CAC/E,CAAC,EACH,GACF,EAAQ,aAAa,aAAa,EAAO,UAAU,KAAK,IAAI,EAAE,iCAAiC,EAGjG,IAAM,EAAa,EAAe,CAAC,EAAI,MAAM,EAAkB,EAC3D,CAAC,GAAgB,EAAW,OAASe,EAAAA,qBAAqB,QAC5D,EAAQ,aACN,0FACF,EAGF,GAAI,CACF,IAAM,EAAS,QAAQ,MAAM,QAAU,IAAQ,QAAQ,OAAO,QAAU,GACxE,GAAI,EACF,OAAO,MAAMC,EAAAA,uBAAuB,CAClC,SAAU,EAAQ,SAClB,IAAK,EAAQ,IACb,YAAa,EAAQ,YACrB,YAAaC,EAAAA,qBAAqB,EAAQ,YAAa,CAAM,EAC7D,SACA,SACA,UACA,SACA,GAAI,GAAQ,SAAS,YAAc,OAAS,CAAE,gBAAiB,EAAO,SAAS,eAAgB,EAAI,CAAC,EACpG,GAAI,EAAQ,WAAa,CAAE,WAAY,EAAQ,UAAW,EAAI,CAAC,CACjE,CAAC,EAGH,IAAM,EAAyB,CAC7B,SACA,SAAU,QAAQ,SAClB,OAAQ,QAAQ,SAAS,EACzB,QAAS,QAAQ,SAAS,EAC1B,QAAS,EAAQ,EAAQ,QAAQ,EACjC,UACA,GAAG,EAAa,SAClB,EACM,EAAOC,EAAAA,iBAAiB,CAC5B,SAAU,EAAQ,SAClB,IAAK,EAAQ,IACb,YAAa,EAAQ,YACrB,YAAa,EAAQ,YACrB,SACA,OACU,WACV,WACA,aACA,QACF,CAAC,EAED,OADA,EAAQ,aAAa,iCAAiC,GAAU,EACzD,MAAM,EAAO,IAAI,EAAQ,EAAK,OAAO,CAC9C,QAAU,CACR,MAAM,GAAQ,KAAK,CACrB,CACF,CACF,CACF,CAGA,SAAgB,EAAc,EAAgD,CAC5E,OAAO,EAAsB,CAAC,CAAC,cAAc,CAAO,CACtD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.cts","names":[],"sources":["../../src/adapters/harness.ts"],"mappings":";;;;UAmCiB;EACf,SAAS;;EAET,mBAAmB;;EAEnB,eAAe;IACb,aAAa,SAAS;IACtB,YAAY;QACR,QAAQ;;EAEd;EACA,YAAY,QAAQ;;;
|
|
1
|
+
{"version":3,"file":"harness.d.cts","names":[],"sources":["../../src/adapters/harness.ts"],"mappings":";;;;UAmCiB;EACf,SAAS;;EAET,mBAAmB;;EAEnB,eAAe;IACb,aAAa,SAAS;IACtB,YAAY;QACR,QAAQ;;EAEd;EACA,YAAY,QAAQ;;;iBAyFN,sBAAsB,eAAc;EAK1C,cAAa,SAAU,uBAAuB;;;iBAmGxC,cAAc,SAAS,uBAAuB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.mts","names":[],"sources":["../../src/adapters/harness.ts"],"mappings":";;;;UAmCiB;EACf,SAAS;;EAET,mBAAmB;;EAEnB,eAAe;IACb,aAAa,SAAS;IACtB,YAAY;QACR,QAAQ;;EAEd;EACA,YAAY,QAAQ;;;
|
|
1
|
+
{"version":3,"file":"harness.d.mts","names":[],"sources":["../../src/adapters/harness.ts"],"mappings":";;;;UAmCiB;EACf,SAAS;;EAET,mBAAmB;;EAEnB,eAAe;IACb,aAAa,SAAS;IACtB,YAAY;QACR,QAAQ;;EAEd;EACA,YAAY,QAAQ;;;iBAyFN,sBAAsB,eAAc;EAK1C,cAAa,SAAU,uBAAuB;;;iBAmGxC,cAAc,SAAS,uBAAuB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import"../services/devcontainer.mjs";import{OAUTH_CALLBACK_PORTS as e}from"../services/oauthCallback.mjs";import{BRIDGE_FILE_NAME as t,sandboxBridgeSource as n}from"../services/sandboxBridge.mjs";import{buildSandboxPlan as r,containerEnvironment as i}from"../services/sandboxPlan.mjs";import{assertRunFlags as a,parseRunFlags as o}from"../services/runFlags.mjs";import{sandboxDockerfile as s}from"../services/sandboxImage.mjs";import{findDevcontainerConfig as c,runDevcontainerSession as l}from"./devcontainer.mjs";import{availableLoginPorts as u}from"./loginPorts.mjs";import{startBroker as d}from"./brokerHost.mjs";import{sandboxImageTag as f}from"./sandboxImageTag.mjs";import{SpawnEngineProcessRunner as p}from"./engineProcess.mjs";import{createRequire as m}from"node:module";import{createHash as h}from"node:crypto";import g from"node:fs";import _ from"node:os";import v from"node:path";const y=`DOOMPI_SANDBOX_ENGINE`,b=`DOOMPI_SANDBOX_RUN_FLAGS`,x=[`docker`,`podman`,`nerdctl`,`finch`],S=v.join(`.doom`,`modes.yaml`),C=/^\s*(?:-|name:)\s*["']?\.{1,2}\//m,w=m(import.meta.url);function T(){return w("@agimon-ai/doompi-sandbox/package.json").version}function E(e){return v.join(_.tmpdir(),`${D(e)}-broker`)}function D(e){return`doompi-sandbox-${h(`sha256`).update(e).digest(`hex`).slice(0,12)}`}async function O(e,t){let n=t[y]?.trim();if(n){if(x.includes(n))return n;throw Error(`${y} must be one of: ${x.join(`, `)}.`)}for(let t of x)if((await e.capture(t,[`--version`]))?.exitCode===0)return t;throw Error(`No container engine found. Install one of ${x.join(`, `)}, or set ${y}.`)}async function k(e,t,n,r,i){let a=i.tag;if((await e.capture(t,[`image`,`inspect`,a]))?.exitCode===0)return a;r?.(`building ${a} (first run for this DoomPi version)`);let o=g.mkdtempSync(v.join(_.tmpdir(),`doompi-sandbox-build-`));try{g.writeFileSync(v.join(o,`Dockerfile`),i.dockerfile);for(let[e,t]of Object.entries(i.files??{}))g.writeFileSync(v.join(o,e),t);let r=await e.run(t,[`build`,`-t`,a,`--build-arg`,`DOOMPI_VERSION=${n}`,o]);if(r!==0)throw Error(`Building the ${a} image failed with exit code ${r}.`)}finally{g.rmSync(o,{recursive:!0,force:!0})}return a}function A(e,t){let n;try{n=g.readFileSync(v.join(e,S),`utf8`)}catch{return}C.test(n)&&t?.(`modes.yaml declares local workspace packages; their platform-specific dependencies must be installed inside the container before they can load`)}function j(m={}){let h=m.runner??new p,g=m.startBroker??d,_=m.loginPorts??u;return{async launchSandbox(u){let d=await O(h,u.environment),p=o(u.environment[b]);a(p,b),u.onProgress?.(`using ${d}${p.length>0?` with ${p.join(` `)}`:``}`);let y=m.version??T(),x=u.environment.DOOMPI_SANDBOX_DEVCONTAINER===`0`?void 0:c(u.repoRoot),S=x?void 0:await k(h,d,y,u.onProgress,{tag:f(y),dockerfile:s(),files:{[t]:n()}});x?(u.onProgress?.(`using the workspace dev container from ${v.relative(u.repoRoot,x)}`),u.onProgress?.(`its configuration owns the mounts and run arguments, so this is not an isolation boundary`)):A(u.repoRoot,u.onProgress);let C=u.environment.DOOMPI_SANDBOX_BROKER===`0`?void 0:await g({environment:u.environment,onDenied:e=>u.onProgress?.(`broker refused a call: ${e}`),...x?{socketDirectory:E(u.repoRoot)}:{}});C&&u.onProgress?.(`brokering ${C.providers.join(`, `)}; provider keys stay on the host`);let w=x?[]:await _();!x&&w.length<e.length&&u.onProgress?.(`some OAuth callback ports are already taken; /login inside this sandbox may not complete`);try{let e=process.stdin.isTTY===!0&&process.stdout.isTTY===!0;if(x)return await l({repoRoot:u.repoRoot,cwd:u.cwd,forwardArgs:u.forwardArgs,environment:i(u.environment,C),engine:d,runner:h,version:y,hasTty:e,...C?.endpoint.transport===`unix`?{socketDirectory:C.endpoint.socketDirectory}:{},...u.onProgress?{onProgress:u.onProgress}:{}});let t={hasTty:e,platform:process.platform,userId:process.getuid?.(),groupId:process.getgid?.(),repoKey:D(u.repoRoot),version:y,...m.hostFacts},n=r({repoRoot:u.repoRoot,cwd:u.cwd,forwardArgs:u.forwardArgs,environment:u.environment,engine:d,host:t,imageTag:S,runFlags:p,loginPorts:w,broker:C});return u.onProgress?.(`starting contained session in ${S}`),await h.run(d,n.runArgs)}finally{await C?.stop()}}}}function M(e){return j().launchSandbox(e)}export{j as createSandboxLauncher,O as detectEngine,k as ensureImage,M as launchSandbox};
|
|
2
2
|
//# sourceMappingURL=harness.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.mjs","names":[],"sources":["../../src/adapters/harness.ts"],"sourcesContent":["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';\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport { findDevcontainerConfig, runDevcontainerSession } from './devcontainer.ts';\nimport { DEVCONTAINER_DISABLED_ENV } from '../services/devcontainer.ts';\nimport { availableLoginPorts } from './loginPorts.ts';\nimport { startBroker, type RunningBroker } from './brokerHost.ts';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { buildSandboxPlan, containerEnvironment } from '../services/sandboxPlan.ts';\nimport { OAUTH_CALLBACK_PORTS } from '../services/oauthCallback.ts';\nimport { assertRunFlags, parseRunFlags } from '../services/runFlags.ts';\nimport { sandboxDockerfile } from '../services/sandboxImage.ts';\nimport { sandboxImageTag } from './sandboxImageTag.ts';\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../types/sandboxHarness.ts';\nimport { SpawnEngineProcessRunner } from './engineProcess.ts';\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\nasync 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\nasync function ensureImage(\n runner: EngineProcessRunner,\n engine: SandboxEngine,\n version: string,\n onProgress: ((message: string) => void) | undefined,\n): Promise<string> {\n const tag = sandboxImageTag(version);\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), sandboxDockerfile());\n fs.writeFileSync(path.join(context, BRIDGE_FILE_NAME), sandboxBridgeSource());\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 sandbox 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 ? undefined : await ensureImage(runner, engine, version, request.onProgress);\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":"43BAmBA,MAAM,EAAa,wBAEb,EAAgB,2BAGhB,EAA2B,CAAC,SAAU,SAAU,UAAW,OAAO,EAElE,EAAa,EAAK,KAAK,QAAS,YAAY,EAC5C,EAAwB,oCAKxB,EAAU,EAAc,YAAY,GAAG,EAiB7C,SAAS,GAA8B,CACrC,OAAQ,EAAQ,wCAAa,CAAC,CAAyB,OACzD,CAGA,SAAS,EAAgB,EAA0B,CACjD,OAAO,EAAK,KAAK,EAAG,OAAO,EAAG,GAAG,EAAQ,CAAQ,EAAE,QAAQ,CAC7D,CAEA,SAAS,EAAQ,EAA0B,CACzC,MAAO,kBAAkB,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,EAAe,GACvG,CAEA,eAAe,EACb,EACA,EACwB,CACxB,IAAM,EAAa,EAAY,EAAW,EAAE,KAAK,EACjD,GAAI,EAAY,CACd,GAAK,EAAqB,SAAS,CAAU,EAAG,OAAO,EACvD,MAAU,MAAM,GAAG,EAAW,mBAAmB,EAAQ,KAAK,IAAI,EAAE,EAAE,CACxE,CACA,IAAK,IAAM,KAAU,EAEnB,IAAI,MADgB,EAAO,QAAQ,EAAQ,CAAC,WAAW,CAAC,EAAA,EAC7C,WAAa,EAAG,OAAO,EAEpC,MAAU,MAAM,6CAA6C,EAAQ,KAAK,IAAI,EAAE,WAAW,EAAW,EAAE,CAC1G,CAEA,eAAe,EACb,EACA,EACA,EACA,EACiB,CACjB,IAAM,EAAM,EAAgB,CAAO,EAEnC,IAAI,MADoB,EAAO,QAAQ,EAAQ,CAAC,QAAS,UAAW,CAAG,CAAC,EAAA,EACzD,WAAa,EAAG,OAAO,EAEtC,IAAa,YAAY,EAAI,qCAAqC,EAClE,IAAM,EAAU,EAAG,YAAY,EAAK,KAAK,EAAG,OAAO,EAAG,uBAAuB,CAAC,EAC9E,GAAI,CACF,EAAG,cAAc,EAAK,KAAK,EAAS,YAAe,EAAG,EAAkB,CAAC,EACzE,EAAG,cAAc,EAAK,KAAK,EAAS,CAAgB,EAAG,EAAoB,CAAC,EAC5E,IAAM,EAAW,MAAM,EAAO,IAAI,EAAQ,CACxC,QACA,KACA,EACA,cACA,kBAAkB,IAClB,CACF,CAAC,EACD,GAAI,IAAa,EAAG,MAAU,MAAM,oDAAoD,EAAS,EAAE,CACrG,QAAU,CACR,EAAG,OAAO,EAAS,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CACrD,CACA,OAAO,CACT,CAEA,SAAS,EAAuB,EAAkB,EAA2D,CAC3G,IAAI,EACJ,GAAI,CACF,EAAQ,EAAG,aAAa,EAAK,KAAK,EAAU,CAAU,EAAG,MAAM,CACjE,MAAQ,CACN,MACF,CACI,EAAsB,KAAK,CAAK,GAClC,IACE,gJAEF,CAEJ,CAGA,SAAgB,EAAsB,EAA4C,CAAC,EAAG,CACpF,IAAM,EAAS,EAAa,QAAU,IAAI,EACpC,EAAc,EAAa,aAAe,EAC1C,EAAoB,EAAa,YAAc,EACrD,MAAO,CACL,MAAM,cAAc,EAAgD,CAClE,IAAM,EAAS,MAAM,EAAa,EAAQ,EAAQ,WAAW,EACvD,EAAW,EAAc,EAAQ,YAAY,EAAc,EACjE,EAAe,EAAU,CAAa,EACtC,EAAQ,aAAa,SAAS,IAAS,EAAS,OAAS,EAAI,SAAS,EAAS,KAAK,GAAG,IAAM,IAAI,EAEjG,IAAM,EAAU,EAAa,SAAW,EAAoB,EAItD,EACJ,EAAQ,YAAA,8BAA2C,IAC/C,IAAA,GACA,EAAuB,EAAQ,QAAQ,EACvC,EAAW,EAAe,IAAA,GAAY,MAAM,EAAY,EAAQ,EAAQ,EAAS,EAAQ,UAAU,EACrG,GACF,EAAQ,aAAa,0CAA0C,EAAK,SAAS,EAAQ,SAAU,CAAY,GAAG,EAC9G,EAAQ,aACN,2FACF,GAEA,EAAuB,EAAQ,SAAU,EAAQ,UAAU,EAG7D,IAAM,EACJ,EAAQ,YAAY,wBAAyB,IACzC,IAAA,GACA,MAAM,EAAY,CAChB,YAAa,EAAQ,YACrB,SAAW,GAAW,EAAQ,aAAa,0BAA0B,GAAQ,EAG7E,GAAI,EAAe,CAAE,gBAAiB,EAAgB,EAAQ,QAAQ,CAAE,EAAI,CAAC,CAC/E,CAAC,EACH,GACF,EAAQ,aAAa,aAAa,EAAO,UAAU,KAAK,IAAI,EAAE,iCAAiC,EAGjG,IAAM,EAAa,EAAe,CAAC,EAAI,MAAM,EAAkB,EAC3D,CAAC,GAAgB,EAAW,OAAS,EAAqB,QAC5D,EAAQ,aACN,0FACF,EAGF,GAAI,CACF,IAAM,EAAS,QAAQ,MAAM,QAAU,IAAQ,QAAQ,OAAO,QAAU,GACxE,GAAI,EACF,OAAO,MAAM,EAAuB,CAClC,SAAU,EAAQ,SAClB,IAAK,EAAQ,IACb,YAAa,EAAQ,YACrB,YAAa,EAAqB,EAAQ,YAAa,CAAM,EAC7D,SACA,SACA,UACA,SACA,GAAI,GAAQ,SAAS,YAAc,OAAS,CAAE,gBAAiB,EAAO,SAAS,eAAgB,EAAI,CAAC,EACpG,GAAI,EAAQ,WAAa,CAAE,WAAY,EAAQ,UAAW,EAAI,CAAC,CACjE,CAAC,EAGH,IAAM,EAAyB,CAC7B,SACA,SAAU,QAAQ,SAClB,OAAQ,QAAQ,SAAS,EACzB,QAAS,QAAQ,SAAS,EAC1B,QAAS,EAAQ,EAAQ,QAAQ,EACjC,UACA,GAAG,EAAa,SAClB,EACM,EAAO,EAAiB,CAC5B,SAAU,EAAQ,SAClB,IAAK,EAAQ,IACb,YAAa,EAAQ,YACrB,YAAa,EAAQ,YACrB,SACA,OACU,WACV,WACA,aACA,QACF,CAAC,EAED,OADA,EAAQ,aAAa,iCAAiC,GAAU,EACzD,MAAM,EAAO,IAAI,EAAQ,EAAK,OAAO,CAC9C,QAAU,CACR,MAAM,GAAQ,KAAK,CACrB,CACF,CACF,CACF,CAGA,SAAgB,EAAc,EAAgD,CAC5E,OAAO,EAAsB,CAAC,CAAC,cAAc,CAAO,CACtD"}
|
|
1
|
+
{"version":3,"file":"harness.mjs","names":[],"sources":["../../src/adapters/harness.ts"],"sourcesContent":["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';\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport { findDevcontainerConfig, runDevcontainerSession } from './devcontainer.ts';\nimport { DEVCONTAINER_DISABLED_ENV } from '../services/devcontainer.ts';\nimport { availableLoginPorts } from './loginPorts.ts';\nimport { startBroker, type RunningBroker } from './brokerHost.ts';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { buildSandboxPlan, containerEnvironment } from '../services/sandboxPlan.ts';\nimport { OAUTH_CALLBACK_PORTS } from '../services/oauthCallback.ts';\nimport { assertRunFlags, parseRunFlags } from '../services/runFlags.ts';\nimport { sandboxDockerfile } from '../services/sandboxImage.ts';\nimport { sandboxImageTag } from './sandboxImageTag.ts';\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../types/sandboxHarness.ts';\nimport { SpawnEngineProcessRunner } from './engineProcess.ts';\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,2 +1,2 @@
|
|
|
1
|
-
const e=require("../services/sandboxBridge.cjs"),t=require("../services/sandboxImage.cjs");let n=require("node:crypto");function r(r){let i=(0,n.createHash)(`sha256`).update(t.sandboxDockerfile()).update(e.sandboxBridgeSource()).digest(`hex`).slice(0,8);return t.formatImageTag(r,i)}exports.sandboxImageTag=r;
|
|
1
|
+
const e=require("../services/sandboxBridge.cjs"),t=require("../services/sandboxImage.cjs");let n=require("node:crypto");function r(r){let i=(0,n.createHash)(`sha256`).update(t.sandboxDockerfile()).update(e.sandboxBridgeSource()).digest(`hex`).slice(0,8);return t.formatImageTag(r,i)}function i(e){let r=(0,n.createHash)(`sha256`).update(t.cockpitDockerfile()).digest(`hex`).slice(0,8);return t.formatImageTag(`${e}-cockpit`,r)}exports.cockpitImageTag=i,exports.sandboxImageTag=r;
|
|
2
2
|
//# sourceMappingURL=sandboxImageTag.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxImageTag.cjs","names":["createHash","sandboxDockerfile","sandboxBridgeSource","formatImageTag"],"sources":["../../src/adapters/sandboxImageTag.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { formatImageTag, sandboxDockerfile } from '../services/sandboxImage.ts';\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"],"mappings":"wHAOA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,GAAA,EAASA,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAChC,OAAOC,EAAAA,kBAAkB,CAAC,CAAC,CAC3B,OAAOC,EAAAA,oBAAoB,CAAC,CAAC,CAC7B,OAAO,KAAK,CAAC,CACb,MAAM,EAAG,CAAa,EACzB,OAAOC,EAAAA,eAAe,EAAS,CAAM,CACvC"}
|
|
1
|
+
{"version":3,"file":"sandboxImageTag.cjs","names":["createHash","sandboxDockerfile","sandboxBridgeSource","formatImageTag","cockpitDockerfile"],"sources":["../../src/adapters/sandboxImageTag.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { cockpitDockerfile, formatImageTag, sandboxDockerfile } from '../services/sandboxImage.ts';\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"],"mappings":"wHAOA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,GAAA,EAASA,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAChC,OAAOC,EAAAA,kBAAkB,CAAC,CAAC,CAC3B,OAAOC,EAAAA,oBAAoB,CAAC,CAAC,CAC7B,OAAO,KAAK,CAAC,CACb,MAAM,EAAG,CAAa,EACzB,OAAOC,EAAAA,eAAe,EAAS,CAAM,CACvC,CAQA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,GAAA,EAASH,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAOI,EAAAA,kBAAkB,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,CAAa,EACpG,OAAOD,EAAAA,eAAe,GAAG,EAAQ,UAAW,CAAM,CACpD"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{sandboxBridgeSource as e}from"../services/sandboxBridge.mjs";import{
|
|
1
|
+
import{sandboxBridgeSource as e}from"../services/sandboxBridge.mjs";import{cockpitDockerfile as t,formatImageTag as n,sandboxDockerfile as r}from"../services/sandboxImage.mjs";import{createHash as i}from"node:crypto";function a(t){let a=i(`sha256`).update(r()).update(e()).digest(`hex`).slice(0,8);return n(t,a)}function o(e){let r=i(`sha256`).update(t()).digest(`hex`).slice(0,8);return n(`${e}-cockpit`,r)}export{o as cockpitImageTag,a as sandboxImageTag};
|
|
2
2
|
//# sourceMappingURL=sandboxImageTag.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxImageTag.mjs","names":[],"sources":["../../src/adapters/sandboxImageTag.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { formatImageTag, sandboxDockerfile } from '../services/sandboxImage.ts';\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"],"mappings":"
|
|
1
|
+
{"version":3,"file":"sandboxImageTag.mjs","names":[],"sources":["../../src/adapters/sandboxImageTag.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { cockpitDockerfile, formatImageTag, sandboxDockerfile } from '../services/sandboxImage.ts';\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"],"mappings":"yNAOA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAAS,EAAW,QAAQ,CAAC,CAChC,OAAO,EAAkB,CAAC,CAAC,CAC3B,OAAO,EAAoB,CAAC,CAAC,CAC7B,OAAO,KAAK,CAAC,CACb,MAAM,EAAG,CAAa,EACzB,OAAO,EAAe,EAAS,CAAM,CACvC,CAQA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAAS,EAAW,QAAQ,CAAC,CAAC,OAAO,EAAkB,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAG,CAAa,EACpG,OAAO,EAAe,GAAG,EAAQ,UAAW,CAAM,CACpD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters/cockpitHarness.cjs");exports.createCockpitHarness=e.createCockpitHarness,exports.reapCockpitContainer=e.reapCockpitContainer,exports.startCockpitContainer=e.startCockpitContainer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createCockpitHarness as e,reapCockpitContainer as t,startCockpitContainer as n}from"./adapters/cockpitHarness.mjs";export{e as createCockpitHarness,t as reapCockpitContainer,n as startCockpitContainer};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./devcontainer.cjs"),t=require("./sandboxPlan.cjs");let n=require("@agimon-ai/doompi-extension-contracts/sandbox-harness");const r=`/doompi-home`,i=`doompi-cockpit-home`,a=`doompi.cockpit`;function o(e){return e===void 0?{}:{GIT_AUTHOR_NAME:e.name,GIT_AUTHOR_EMAIL:e.email,GIT_COMMITTER_NAME:e.name,GIT_COMMITTER_EMAIL:e.email}}function s(s){let{engine:c,host:l,broker:u,imageTag:d,port:f}=s;if(s.workspaces.length===0)throw Error(`A cockpit container needs at least one workspace to mount.`);let p={...t.containerEnvironment(s.environment,u),...o(s.gitIdentity),[e.DEVCONTAINER_DISABLED_ENV]:`0`,[n.DOOMPI_SANDBOX_ENV]:`1`},m=Object.entries(p).sort(([e],[t])=>e.localeCompare(t));return{imageTag:d,runArgs:[`run`,`-d`,`--label`,`${a}=${String(f)}`,`-p`,`127.0.0.1:${String(f)}:${String(f)}`,...s.workspaces.flatMap(e=>[`-v`,`${e.path}:${e.path}`]),`-v`,`${i}:${r}`,...u?.endpoint.transport===`tcp`?[`--add-host`,`host.docker.internal:host-gateway`]:[],...u?.endpoint.transport===`unix`?[`-v`,`${u.endpoint.socketDirectory}:/run/doompi`]:[],`-w`,s.workspaces[0]?.path??r,`-e`,`HOME=${r}`,...m.flatMap(([e,t])=>[`-e`,`${e}=${t}`]),...l.platform===`linux`&&l.userId!==void 0?[`--user`,`${String(l.userId)}:${String(l.groupId??l.userId)}`,...c===`podman`?[`--userns=keep-id`]:[]]:[],...s.runFlags??[],d,`doompi-web`,`--host`,`0.0.0.0`,`--port`,String(f)]}}exports.COCKPIT_HOME_VOLUME=i,exports.COCKPIT_LABEL=a,exports.buildCockpitPlan=s;
|
|
2
|
+
//# sourceMappingURL=cockpitPlan.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitPlan.cjs","names":["containerEnvironment","DEVCONTAINER_DISABLED_ENV","DOOMPI_SANDBOX_ENV"],"sources":["../../src/services/cockpitPlan.ts"],"sourcesContent":["import { DOOMPI_SANDBOX_ENV } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport type { CockpitWorkspace } from '@agimon-ai/doompi-extension-contracts/cockpit-container';\nimport type { SandboxEngine, SandboxHostFacts } from '../types/sandboxHarness.ts';\nimport { DEVCONTAINER_DISABLED_ENV } from './devcontainer.ts';\nimport { containerEnvironment, type SandboxPlanBroker } from './sandboxPlan.ts';\n\n/**\n * Projects the cockpit into one detached engine run.\n *\n * The sandbox plan next door describes a disposable container wrapped around a\n * single interactive session. This one describes a long-lived container holding\n * the hub and every session it goes on to spawn, which changes four things:\n * it is detached rather than attached, it publishes a port rather than only\n * consuming them, its home volume is scoped to the cockpit rather than to one\n * repository, and it mounts a set of workspaces rather than a single repo.\n */\n\nconst CONTAINER_HOME = '/doompi-home';\n/**\n * One volume for the cockpit, not one per repository.\n *\n * A cockpit spans several workspaces, so there is no repository to key on. This\n * is also where `~/.doompi/web` lands, which is what makes paired passkeys and\n * the bundle signing key survive a container restart. A rotated signing key\n * would make every paired device refuse the cockpit, so the name must be\n * stable.\n */\nexport const COCKPIT_HOME_VOLUME = 'doompi-cockpit-home';\nexport const COCKPIT_LABEL = 'doompi.cockpit';\nconst COCKPIT_BINARY = 'doompi-web';\nconst LOOPBACK = '127.0.0.1';\n/**\n * The cockpit binds every interface inside the container.\n *\n * A published port reaches the container's external interface, never its\n * loopback, which is the same reason `oauthCallback.ts` sets its own bind\n * address. The host side of the publish is pinned to loopback, so this is not\n * exposed beyond the machine.\n */\nconst CONTAINER_BIND = '0.0.0.0';\nconst LINUX_PLATFORM = 'linux';\nconst DISABLED = '0';\n\n/** Git identity the agent commits under; no key crosses the boundary, so it cannot push. */\nexport interface CockpitGitIdentity {\n name: string;\n email: string;\n}\n\nexport interface CockpitPlanInput {\n workspaces: readonly CockpitWorkspace[];\n /** Published on the host loopback and bound inside the container. */\n port: number;\n environment: Readonly<Record<string, string | undefined>>;\n engine: SandboxEngine;\n host: SandboxHostFacts;\n imageTag: string;\n runFlags?: readonly string[];\n broker?: SandboxPlanBroker;\n gitIdentity?: CockpitGitIdentity;\n}\n\nexport interface CockpitPlan {\n imageTag: string;\n /** Full engine argv after the engine binary itself. */\n runArgs: string[];\n}\n\n/**\n * Git identity, passed as environment rather than by mounting a config.\n *\n * The agent can commit; it cannot push, because no key crosses the boundary.\n * Mounting `~/.gitconfig` would drag in whatever else it configures, and\n * forwarding an ssh agent would hand the container a live credential that\n * authenticates as the user everywhere their key reaches.\n */\nfunction gitIdentityEnvironment(identity: CockpitGitIdentity | undefined): Record<string, string> {\n if (identity === undefined) return {};\n return {\n GIT_AUTHOR_NAME: identity.name,\n GIT_AUTHOR_EMAIL: identity.email,\n GIT_COMMITTER_NAME: identity.name,\n GIT_COMMITTER_EMAIL: identity.email,\n };\n}\n\nexport function buildCockpitPlan(input: CockpitPlanInput): CockpitPlan {\n const { engine, host, broker, imageTag, port } = input;\n if (input.workspaces.length === 0) throw new Error('A cockpit container needs at least one workspace to mount.');\n\n const environment: Record<string, string> = {\n ...containerEnvironment(input.environment, broker),\n ...gitIdentityEnvironment(input.gitIdentity),\n // A workspace carrying a devcontainer config would otherwise replace the\n // whole plan with an author-controlled one that can mount anything,\n // including the docker socket. That would make the containment claim false.\n [DEVCONTAINER_DISABLED_ENV]: DISABLED,\n [DOOMPI_SANDBOX_ENV]: '1',\n };\n const environmentPairs = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));\n\n const runArgs = [\n 'run',\n // Detached, and deliberately not --rm: a container that exits on startup\n // has to survive long enough for its logs to be read. The stop path and the\n // reaper both remove it.\n '-d',\n '--label',\n `${COCKPIT_LABEL}=${String(port)}`,\n '-p',\n `${LOOPBACK}:${String(port)}:${String(port)}`,\n ...input.workspaces.flatMap((workspace) => ['-v', `${workspace.path}:${workspace.path}`]),\n '-v',\n `${COCKPIT_HOME_VOLUME}:${CONTAINER_HOME}`,\n ...(broker?.endpoint.transport === 'tcp' ? ['--add-host', `host.docker.internal:host-gateway`] : []),\n ...(broker?.endpoint.transport === 'unix' ? ['-v', `${broker.endpoint.socketDirectory}:/run/doompi`] : []),\n '-w',\n input.workspaces[0]?.path ?? CONTAINER_HOME,\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 `${String(host.userId)}:${String(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 above\n // it, and cannot be mistaken for the image or its command.\n ...(input.runFlags ?? []),\n imageTag,\n COCKPIT_BINARY,\n '--host',\n CONTAINER_BIND,\n '--port',\n String(port),\n ];\n\n return { imageTag, runArgs };\n}\n"],"mappings":"4IAiBA,MAAM,EAAiB,eAUV,EAAsB,sBACtB,EAAgB,iBAgD7B,SAAS,EAAuB,EAAkE,CAEhG,OADI,IAAa,IAAA,GAAkB,CAAC,EAC7B,CACL,gBAAiB,EAAS,KAC1B,iBAAkB,EAAS,MAC3B,mBAAoB,EAAS,KAC7B,oBAAqB,EAAS,KAChC,CACF,CAEA,SAAgB,EAAiB,EAAsC,CACrE,GAAM,CAAE,SAAQ,OAAM,SAAQ,WAAU,QAAS,EACjD,GAAI,EAAM,WAAW,SAAW,EAAG,MAAU,MAAM,4DAA4D,EAE/G,IAAM,EAAsC,CAC1C,GAAGA,EAAAA,qBAAqB,EAAM,YAAa,CAAM,EACjD,GAAG,EAAuB,EAAM,WAAW,GAI1CC,EAAAA,2BAA4B,KAC5BC,EAAAA,oBAAqB,GACxB,EACM,EAAmB,OAAO,QAAQ,CAAW,CAAC,CAAC,MAAM,CAAC,GAAO,CAAC,KAAW,EAAK,cAAc,CAAK,CAAC,EAwCxG,MAAO,CAAE,WAAU,QAAA,CArCjB,MAIA,KACA,UACA,GAAG,EAAc,GAAG,OAAO,CAAI,IAC/B,KACA,aAAe,OAAO,CAAI,EAAE,GAAG,OAAO,CAAI,IAC1C,GAAG,EAAM,WAAW,QAAS,GAAc,CAAC,KAAM,GAAG,EAAU,KAAK,GAAG,EAAU,MAAM,CAAC,EACxF,KACA,GAAG,EAAoB,GAAG,IAC1B,GAAI,GAAQ,SAAS,YAAc,MAAQ,CAAC,aAAc,mCAAmC,EAAI,CAAC,EAClG,GAAI,GAAQ,SAAS,YAAc,OAAS,CAAC,KAAM,GAAG,EAAO,SAAS,gBAAgB,aAAa,EAAI,CAAC,EACxG,KACA,EAAM,WAAW,EAAE,EAAE,MAAQ,EAC7B,KACA,QAAQ,IACR,GAAG,EAAiB,SAAS,CAAC,EAAM,KAAW,CAAC,KAAM,GAAG,EAAK,GAAG,GAAO,CAAC,EACzE,GAAI,EAAK,WAAa,SAAkB,EAAK,SAAW,IAAA,GACpD,CACE,SACA,GAAG,OAAO,EAAK,MAAM,EAAE,GAAG,OAAO,EAAK,SAAW,EAAK,MAAM,IAC5D,GAAI,IAAW,SAAW,CAAC,kBAAkB,EAAI,CAAC,CACpD,EACA,CAAC,EAGL,GAAI,EAAM,UAAY,CAAC,EACvB,EACA,aACA,SACA,UACA,SACA,OAAO,CAAI,CAGY,CAAE,CAC7B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "@agimon-ai/doompi-extension-contracts/cockpit-container";
|
|
2
|
+
//#region src/services/cockpitPlan.d.ts
|
|
3
|
+
/** Git identity the agent commits under; no key crosses the boundary, so it cannot push. */
|
|
4
|
+
interface CockpitGitIdentity {
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { CockpitGitIdentity };
|
|
10
|
+
//# sourceMappingURL=cockpitPlan.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitPlan.d.cts","names":[],"sources":["../../src/services/cockpitPlan.ts"],"mappings":";;;UA4CiB;EACf;EACA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "@agimon-ai/doompi-extension-contracts/cockpit-container";
|
|
2
|
+
//#region src/services/cockpitPlan.d.ts
|
|
3
|
+
/** Git identity the agent commits under; no key crosses the boundary, so it cannot push. */
|
|
4
|
+
interface CockpitGitIdentity {
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { CockpitGitIdentity };
|
|
10
|
+
//# sourceMappingURL=cockpitPlan.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitPlan.d.mts","names":[],"sources":["../../src/services/cockpitPlan.ts"],"mappings":";;;UA4CiB;EACf;EACA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{DEVCONTAINER_DISABLED_ENV as e}from"./devcontainer.mjs";import{containerEnvironment as t}from"./sandboxPlan.mjs";import{DOOMPI_SANDBOX_ENV as n}from"@agimon-ai/doompi-extension-contracts/sandbox-harness";const r=`/doompi-home`,i=`doompi-cockpit-home`,a=`doompi.cockpit`;function o(e){return e===void 0?{}:{GIT_AUTHOR_NAME:e.name,GIT_AUTHOR_EMAIL:e.email,GIT_COMMITTER_NAME:e.name,GIT_COMMITTER_EMAIL:e.email}}function s(s){let{engine:c,host:l,broker:u,imageTag:d,port:f}=s;if(s.workspaces.length===0)throw Error(`A cockpit container needs at least one workspace to mount.`);let p={...t(s.environment,u),...o(s.gitIdentity),[e]:`0`,[n]:`1`},m=Object.entries(p).sort(([e],[t])=>e.localeCompare(t));return{imageTag:d,runArgs:[`run`,`-d`,`--label`,`${a}=${String(f)}`,`-p`,`127.0.0.1:${String(f)}:${String(f)}`,...s.workspaces.flatMap(e=>[`-v`,`${e.path}:${e.path}`]),`-v`,`${i}:${r}`,...u?.endpoint.transport===`tcp`?[`--add-host`,`host.docker.internal:host-gateway`]:[],...u?.endpoint.transport===`unix`?[`-v`,`${u.endpoint.socketDirectory}:/run/doompi`]:[],`-w`,s.workspaces[0]?.path??r,`-e`,`HOME=${r}`,...m.flatMap(([e,t])=>[`-e`,`${e}=${t}`]),...l.platform===`linux`&&l.userId!==void 0?[`--user`,`${String(l.userId)}:${String(l.groupId??l.userId)}`,...c===`podman`?[`--userns=keep-id`]:[]]:[],...s.runFlags??[],d,`doompi-web`,`--host`,`0.0.0.0`,`--port`,String(f)]}}export{i as COCKPIT_HOME_VOLUME,a as COCKPIT_LABEL,s as buildCockpitPlan};
|
|
2
|
+
//# sourceMappingURL=cockpitPlan.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cockpitPlan.mjs","names":[],"sources":["../../src/services/cockpitPlan.ts"],"sourcesContent":["import { DOOMPI_SANDBOX_ENV } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport type { CockpitWorkspace } from '@agimon-ai/doompi-extension-contracts/cockpit-container';\nimport type { SandboxEngine, SandboxHostFacts } from '../types/sandboxHarness.ts';\nimport { DEVCONTAINER_DISABLED_ENV } from './devcontainer.ts';\nimport { containerEnvironment, type SandboxPlanBroker } from './sandboxPlan.ts';\n\n/**\n * Projects the cockpit into one detached engine run.\n *\n * The sandbox plan next door describes a disposable container wrapped around a\n * single interactive session. This one describes a long-lived container holding\n * the hub and every session it goes on to spawn, which changes four things:\n * it is detached rather than attached, it publishes a port rather than only\n * consuming them, its home volume is scoped to the cockpit rather than to one\n * repository, and it mounts a set of workspaces rather than a single repo.\n */\n\nconst CONTAINER_HOME = '/doompi-home';\n/**\n * One volume for the cockpit, not one per repository.\n *\n * A cockpit spans several workspaces, so there is no repository to key on. This\n * is also where `~/.doompi/web` lands, which is what makes paired passkeys and\n * the bundle signing key survive a container restart. A rotated signing key\n * would make every paired device refuse the cockpit, so the name must be\n * stable.\n */\nexport const COCKPIT_HOME_VOLUME = 'doompi-cockpit-home';\nexport const COCKPIT_LABEL = 'doompi.cockpit';\nconst COCKPIT_BINARY = 'doompi-web';\nconst LOOPBACK = '127.0.0.1';\n/**\n * The cockpit binds every interface inside the container.\n *\n * A published port reaches the container's external interface, never its\n * loopback, which is the same reason `oauthCallback.ts` sets its own bind\n * address. The host side of the publish is pinned to loopback, so this is not\n * exposed beyond the machine.\n */\nconst CONTAINER_BIND = '0.0.0.0';\nconst LINUX_PLATFORM = 'linux';\nconst DISABLED = '0';\n\n/** Git identity the agent commits under; no key crosses the boundary, so it cannot push. */\nexport interface CockpitGitIdentity {\n name: string;\n email: string;\n}\n\nexport interface CockpitPlanInput {\n workspaces: readonly CockpitWorkspace[];\n /** Published on the host loopback and bound inside the container. */\n port: number;\n environment: Readonly<Record<string, string | undefined>>;\n engine: SandboxEngine;\n host: SandboxHostFacts;\n imageTag: string;\n runFlags?: readonly string[];\n broker?: SandboxPlanBroker;\n gitIdentity?: CockpitGitIdentity;\n}\n\nexport interface CockpitPlan {\n imageTag: string;\n /** Full engine argv after the engine binary itself. */\n runArgs: string[];\n}\n\n/**\n * Git identity, passed as environment rather than by mounting a config.\n *\n * The agent can commit; it cannot push, because no key crosses the boundary.\n * Mounting `~/.gitconfig` would drag in whatever else it configures, and\n * forwarding an ssh agent would hand the container a live credential that\n * authenticates as the user everywhere their key reaches.\n */\nfunction gitIdentityEnvironment(identity: CockpitGitIdentity | undefined): Record<string, string> {\n if (identity === undefined) return {};\n return {\n GIT_AUTHOR_NAME: identity.name,\n GIT_AUTHOR_EMAIL: identity.email,\n GIT_COMMITTER_NAME: identity.name,\n GIT_COMMITTER_EMAIL: identity.email,\n };\n}\n\nexport function buildCockpitPlan(input: CockpitPlanInput): CockpitPlan {\n const { engine, host, broker, imageTag, port } = input;\n if (input.workspaces.length === 0) throw new Error('A cockpit container needs at least one workspace to mount.');\n\n const environment: Record<string, string> = {\n ...containerEnvironment(input.environment, broker),\n ...gitIdentityEnvironment(input.gitIdentity),\n // A workspace carrying a devcontainer config would otherwise replace the\n // whole plan with an author-controlled one that can mount anything,\n // including the docker socket. That would make the containment claim false.\n [DEVCONTAINER_DISABLED_ENV]: DISABLED,\n [DOOMPI_SANDBOX_ENV]: '1',\n };\n const environmentPairs = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));\n\n const runArgs = [\n 'run',\n // Detached, and deliberately not --rm: a container that exits on startup\n // has to survive long enough for its logs to be read. The stop path and the\n // reaper both remove it.\n '-d',\n '--label',\n `${COCKPIT_LABEL}=${String(port)}`,\n '-p',\n `${LOOPBACK}:${String(port)}:${String(port)}`,\n ...input.workspaces.flatMap((workspace) => ['-v', `${workspace.path}:${workspace.path}`]),\n '-v',\n `${COCKPIT_HOME_VOLUME}:${CONTAINER_HOME}`,\n ...(broker?.endpoint.transport === 'tcp' ? ['--add-host', `host.docker.internal:host-gateway`] : []),\n ...(broker?.endpoint.transport === 'unix' ? ['-v', `${broker.endpoint.socketDirectory}:/run/doompi`] : []),\n '-w',\n input.workspaces[0]?.path ?? CONTAINER_HOME,\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 `${String(host.userId)}:${String(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 above\n // it, and cannot be mistaken for the image or its command.\n ...(input.runFlags ?? []),\n imageTag,\n COCKPIT_BINARY,\n '--host',\n CONTAINER_BIND,\n '--port',\n String(port),\n ];\n\n return { imageTag, runArgs };\n}\n"],"mappings":"mNAiBA,MAAM,EAAiB,eAUV,EAAsB,sBACtB,EAAgB,iBAgD7B,SAAS,EAAuB,EAAkE,CAEhG,OADI,IAAa,IAAA,GAAkB,CAAC,EAC7B,CACL,gBAAiB,EAAS,KAC1B,iBAAkB,EAAS,MAC3B,mBAAoB,EAAS,KAC7B,oBAAqB,EAAS,KAChC,CACF,CAEA,SAAgB,EAAiB,EAAsC,CACrE,GAAM,CAAE,SAAQ,OAAM,SAAQ,WAAU,QAAS,EACjD,GAAI,EAAM,WAAW,SAAW,EAAG,MAAU,MAAM,4DAA4D,EAE/G,IAAM,EAAsC,CAC1C,GAAG,EAAqB,EAAM,YAAa,CAAM,EACjD,GAAG,EAAuB,EAAM,WAAW,GAI1C,GAA4B,KAC5B,GAAqB,GACxB,EACM,EAAmB,OAAO,QAAQ,CAAW,CAAC,CAAC,MAAM,CAAC,GAAO,CAAC,KAAW,EAAK,cAAc,CAAK,CAAC,EAwCxG,MAAO,CAAE,WAAU,QAAA,CArCjB,MAIA,KACA,UACA,GAAG,EAAc,GAAG,OAAO,CAAI,IAC/B,KACA,aAAe,OAAO,CAAI,EAAE,GAAG,OAAO,CAAI,IAC1C,GAAG,EAAM,WAAW,QAAS,GAAc,CAAC,KAAM,GAAG,EAAU,KAAK,GAAG,EAAU,MAAM,CAAC,EACxF,KACA,GAAG,EAAoB,GAAG,IAC1B,GAAI,GAAQ,SAAS,YAAc,MAAQ,CAAC,aAAc,mCAAmC,EAAI,CAAC,EAClG,GAAI,GAAQ,SAAS,YAAc,OAAS,CAAC,KAAM,GAAG,EAAO,SAAS,gBAAgB,aAAa,EAAI,CAAC,EACxG,KACA,EAAM,WAAW,EAAE,EAAE,MAAQ,EAC7B,KACA,QAAQ,IACR,GAAG,EAAiB,SAAS,CAAC,EAAM,KAAW,CAAC,KAAM,GAAG,EAAK,GAAG,GAAO,CAAC,EACzE,GAAI,EAAK,WAAa,SAAkB,EAAK,SAAW,IAAA,GACpD,CACE,SACA,GAAG,OAAO,EAAK,MAAM,EAAE,GAAG,OAAO,EAAK,SAAW,EAAK,MAAM,IAC5D,GAAI,IAAW,SAAW,CAAC,kBAAkB,EAAI,CAAC,CACpD,EACA,CAAC,EAGL,GAAI,EAAM,UAAY,CAAC,EACvB,EACA,aACA,SACA,UACA,SACA,OAAO,CAAI,CAGY,CAAE,CAC7B"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
const e=require("./sandboxBridge.cjs"),t=/[^A-Za-z0-9_.-]/g;function n(e,n){return`doompi-sandbox:v${e.replaceAll(t,`-`)}-${n}`}function r(){return[`FROM node:22-bookworm-slim`,`RUN apt-get update \\`,` && apt-get install -y --no-install-recommends ca-certificates curl git nano openssh-client ripgrep \\`,` && rm -rf /var/lib/apt/lists/*`,`ARG DOOMPI_VERSION`,"RUN npm install -g @agimon-ai/doompi@${DOOMPI_VERSION}",`COPY ${e.BRIDGE_FILE_NAME} ${e.BRIDGE_CONTAINER_PATH}`,`# World-writable so the launch can map any host user id onto it.`,`RUN mkdir -m 0777 /doompi-home`,`ENV DOOMPI_SANDBOX=1 HOME=/doompi-home`,``].join(`
|
|
2
|
-
`)}
|
|
2
|
+
`)}function i(){return[`FROM node:22-bookworm-slim`,`RUN apt-get update \\`,` && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\`,` && rm -rf /var/lib/apt/lists/*`,`RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\`,` -o /usr/share/keyrings/cloudflare-main.gpg \\`,` && echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main" \\`,` > /etc/apt/sources.list.d/cloudflared.list \\`,` && apt-get update && apt-get install -y --no-install-recommends cloudflared \\`,` && rm -rf /var/lib/apt/lists/*`,`ARG DOOMPI_VERSION`,"RUN npm install -g @agimon-ai/doompi-web@${DOOMPI_VERSION}",`# World-writable so the launch can map any host user id onto it.`,`RUN mkdir -m 0777 /doompi-home`,`ENV DOOMPI_SANDBOX=1 HOME=/doompi-home`,``].join(`
|
|
3
|
+
`)}exports.cockpitDockerfile=i,exports.formatImageTag=n,exports.sandboxDockerfile=r;
|
|
3
4
|
//# sourceMappingURL=sandboxImage.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxImage.cjs","names":["BRIDGE_FILE_NAME","BRIDGE_CONTAINER_PATH"],"sources":["../../src/services/sandboxImage.ts"],"sourcesContent":["import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from './sandboxBridge.ts';\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"],"mappings":"uCAGM,EAAW,mBASjB,SAAgB,EAAe,EAAiB,EAAwB,CACtE,MAAO,mBAAwB,EAAQ,WAAW,EAAU,GAAG,EAAE,GAAG,GACtE,CAeA,SAAgB,GAA4B,CAC1C,MAAO,CACL,6BACA,wBACA,0GACA,mCACA,qBACA,yDACA,QAAQA,EAAAA,iBAAiB,GAAGC,EAAAA,wBAC5B,mEACA,iCACA,yCACA,EACF,CAAC,CAAC,KAAK;CAAI,CACb"}
|
|
1
|
+
{"version":3,"file":"sandboxImage.cjs","names":["BRIDGE_FILE_NAME","BRIDGE_CONTAINER_PATH"],"sources":["../../src/services/sandboxImage.ts"],"sourcesContent":["import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from './sandboxBridge.ts';\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: it\n * depends on `@agimon-ai/doompi` and `@agimon-ai/doompi-server`, so the hub can\n * spawn sessions and each session can run an agent.\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"],"mappings":"uCAGM,EAAW,mBASjB,SAAgB,EAAe,EAAiB,EAAwB,CACtE,MAAO,mBAAwB,EAAQ,WAAW,EAAU,GAAG,EAAE,GAAG,GACtE,CAeA,SAAgB,GAA4B,CAC1C,MAAO,CACL,6BACA,wBACA,0GACA,mCACA,qBACA,yDACA,QAAQA,EAAAA,iBAAiB,GAAGC,EAAAA,wBAC5B,mEACA,iCACA,yCACA,EACF,CAAC,CAAC,KAAK;CAAI,CACb,CAcA,SAAgB,GAA4B,CAC1C,MAAO,CACL,6BACA,wBACA,gHACA,mCAGA,mEACA,kDACA,6HACA,kDACA,kFACA,kCACA,qBACA,6DACA,mEACA,iCACA,yCACA,EACF,CAAC,CAAC,KAAK;CAAI,CACb"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import{BRIDGE_CONTAINER_PATH as e,BRIDGE_FILE_NAME as t}from"./sandboxBridge.mjs";const n=/[^A-Za-z0-9_.-]/g;function r(e,t){return`doompi-sandbox:v${e.replaceAll(n,`-`)}-${t}`}function i(){return[`FROM node:22-bookworm-slim`,`RUN apt-get update \\`,` && apt-get install -y --no-install-recommends ca-certificates curl git nano openssh-client ripgrep \\`,` && rm -rf /var/lib/apt/lists/*`,`ARG DOOMPI_VERSION`,"RUN npm install -g @agimon-ai/doompi@${DOOMPI_VERSION}",`COPY ${t} ${e}`,`# World-writable so the launch can map any host user id onto it.`,`RUN mkdir -m 0777 /doompi-home`,`ENV DOOMPI_SANDBOX=1 HOME=/doompi-home`,``].join(`
|
|
2
|
-
`)}
|
|
2
|
+
`)}function a(){return[`FROM node:22-bookworm-slim`,`RUN apt-get update \\`,` && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\`,` && rm -rf /var/lib/apt/lists/*`,`RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\`,` -o /usr/share/keyrings/cloudflare-main.gpg \\`,` && echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main" \\`,` > /etc/apt/sources.list.d/cloudflared.list \\`,` && apt-get update && apt-get install -y --no-install-recommends cloudflared \\`,` && rm -rf /var/lib/apt/lists/*`,`ARG DOOMPI_VERSION`,"RUN npm install -g @agimon-ai/doompi-web@${DOOMPI_VERSION}",`# World-writable so the launch can map any host user id onto it.`,`RUN mkdir -m 0777 /doompi-home`,`ENV DOOMPI_SANDBOX=1 HOME=/doompi-home`,``].join(`
|
|
3
|
+
`)}export{a as cockpitDockerfile,r as formatImageTag,i as sandboxDockerfile};
|
|
3
4
|
//# sourceMappingURL=sandboxImage.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxImage.mjs","names":[],"sources":["../../src/services/sandboxImage.ts"],"sourcesContent":["import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from './sandboxBridge.ts';\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"],"mappings":"kFAEA,MACM,EAAW,mBASjB,SAAgB,EAAe,EAAiB,EAAwB,CACtE,MAAO,mBAAwB,EAAQ,WAAW,EAAU,GAAG,EAAE,GAAG,GACtE,CAeA,SAAgB,GAA4B,CAC1C,MAAO,CACL,6BACA,wBACA,0GACA,mCACA,qBACA,yDACA,QAAQ,EAAiB,GAAG,IAC5B,mEACA,iCACA,yCACA,EACF,CAAC,CAAC,KAAK;CAAI,CACb"}
|
|
1
|
+
{"version":3,"file":"sandboxImage.mjs","names":[],"sources":["../../src/services/sandboxImage.ts"],"sourcesContent":["import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from './sandboxBridge.ts';\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: it\n * depends on `@agimon-ai/doompi` and `@agimon-ai/doompi-server`, so the hub can\n * spawn sessions and each session can run an agent.\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"],"mappings":"kFAEA,MACM,EAAW,mBASjB,SAAgB,EAAe,EAAiB,EAAwB,CACtE,MAAO,mBAAwB,EAAQ,WAAW,EAAU,GAAG,EAAE,GAAG,GACtE,CAeA,SAAgB,GAA4B,CAC1C,MAAO,CACL,6BACA,wBACA,0GACA,mCACA,qBACA,yDACA,QAAQ,EAAiB,GAAG,IAC5B,mEACA,iCACA,yCACA,EACF,CAAC,CAAC,KAAK;CAAI,CACb,CAcA,SAAgB,GAA4B,CAC1C,MAAO,CACL,6BACA,wBACA,gHACA,mCAGA,mEACA,kDACA,6HACA,kDACA,kFACA,kCACA,qBACA,6DACA,mEACA,iCACA,yCACA,EACF,CAAC,CAAC,KAAK;CAAI,CACb"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./
|
|
1
|
+
const e=require("./sandboxEnvironment.cjs"),t=require("./oauthCallback.cjs"),n=require("./sandboxBridge.cjs");let r=require("@agimon-ai/doompi-extension-contracts/sandbox-harness");const i=`/doompi-home`;function a(t,r){let i={};for(let[n,r]of Object.entries(t))e.isCredentialEnvName(n)||(i[n]=r);for(let e of r.withheldEnv)i[e]=r.token;return r.endpoint.transport===`unix`?(i[n.BROKER_SOCKET_ENV]=n.BROKER_SOCKET_CONTAINER_PATH,i[n.BROKER_PORT_ENV]=String(n.BROKER_CONTAINER_PORT)):i[n.BROKER_ADDRESS_ENV]=`${n.BROKER_HOST_GATEWAY}:${r.endpoint.port}`,i[n.BROKER_PROVIDERS_ENV]=r.providers.join(`,`),i}function o(e){return e?e.endpoint.transport===`unix`?[`-v`,`${e.endpoint.socketDirectory}:/run/doompi`]:[`--add-host`,`${n.BROKER_HOST_GATEWAY}:host-gateway`]:[]}function s(n,i,o=[]){let s=e.filterSandboxEnvironment(n),c=i?a(s,i):s;return c[r.DOOMPI_SANDBOX_ENV]=`1`,o.length>0&&(c[t.OAUTH_CALLBACK_HOST_ENV]=t.OAUTH_CONTAINER_BIND),c}function c(e){let{repoRoot:r,cwd:a,engine:c,host:l,broker:u,imageTag:d}=e,f=e.loginPorts??[],p=Object.entries(s(e.environment,u,f)).sort(([e],[t])=>e.localeCompare(t));return{imageTag:d,runArgs:[`run`,`--rm`,`-i`,...l.hasTty?[`-t`]:[],`--label`,`doompi.sandbox.repo=${r}`,`-v`,`${r}:${r}`,`-v`,`${l.repoKey}-home:${i}`,`-v`,`${l.repoKey}-pi:${r}/.pi`,...o(u),...t.oauthPublishArgs(f),`-w`,a,`-e`,`HOME=${i}`,...p.flatMap(([e,t])=>[`-e`,`${e}=${t}`]),...l.platform===`linux`&&l.userId!==void 0?[`--user`,`${l.userId}:${l.groupId??l.userId}`,...c===`podman`?[`--userns=keep-id`]:[]]:[],...e.runFlags??[],d,...u?.endpoint.transport===`unix`?[`node`,n.BRIDGE_CONTAINER_PATH]:[],`doompi`,...e.forwardArgs]}}exports.buildSandboxPlan=c,exports.containerEnvironment=s;
|
|
2
2
|
//# sourceMappingURL=sandboxPlan.cjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{filterSandboxEnvironment as e,isCredentialEnvName as t}from"./sandboxEnvironment.mjs";import{OAUTH_CALLBACK_HOST_ENV as n,OAUTH_CONTAINER_BIND as r,oauthPublishArgs as i}from"./oauthCallback.mjs";import{BRIDGE_CONTAINER_PATH as a,BROKER_ADDRESS_ENV as o,BROKER_CONTAINER_PORT as s,BROKER_HOST_GATEWAY as c,BROKER_PORT_ENV as l,BROKER_PROVIDERS_ENV as u,BROKER_SOCKET_CONTAINER_PATH as d,BROKER_SOCKET_ENV as f}from"./sandboxBridge.mjs";import{DOOMPI_SANDBOX_ENV as p}from"@agimon-ai/doompi-extension-contracts/sandbox-harness";const m=`/doompi-home`;function h(e,n){let r={};for(let[n,i]of Object.entries(e))t(n)||(r[n]=i);for(let e of n.withheldEnv)r[e]=n.token;return n.endpoint.transport===`unix`?(r[f]=d,r[l]=String(s)):r[o]=`${c}:${n.endpoint.port}`,r[u]=n.providers.join(`,`),r}function g(e){return e?e.endpoint.transport===`unix`?[`-v`,`${e.endpoint.socketDirectory}:/run/doompi`]:[`--add-host`,`${c}:host-gateway`]:[]}function _(t,i,a=[]){let o=e(t),s=i?h(o,i):o;return s[p]=`1`,a.length>0&&(s[n]=r),s}function v(e){let{repoRoot:t,cwd:n,engine:r,host:o,broker:s,imageTag:c}=e,l=e.loginPorts??[],u=Object.entries(_(e.environment,s,l)).sort(([e],[t])=>e.localeCompare(t));return{imageTag:c,runArgs:[`run`,`--rm`,`-i`,...o.hasTty?[`-t`]:[],`--label`,`doompi.sandbox.repo=${t}`,`-v`,`${t}:${t}`,`-v`,`${o.repoKey}-home:${m}`,`-v`,`${o.repoKey}-pi:${t}/.pi`,...g(s),...i(l),`-w`,n,`-e`,`HOME=${m}`,...u.flatMap(([e,t])=>[`-e`,`${e}=${t}`]),...o.platform===`linux`&&o.userId!==void 0?[`--user`,`${o.userId}:${o.groupId??o.userId}`,...r===`podman`?[`--userns=keep-id`]:[]]:[],...e.runFlags??[],c,...s?.endpoint.transport===`unix`?[`node`,a]:[],`doompi`,...e.forwardArgs]}}export{v as buildSandboxPlan,_ as containerEnvironment};
|
|
2
2
|
//# sourceMappingURL=sandboxPlan.mjs.map
|
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.40",
|
|
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
|
"coding-agent",
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
"import": "./dist/index.mjs",
|
|
41
41
|
"require": "./dist/index.cjs"
|
|
42
42
|
},
|
|
43
|
+
"./cockpit-harness": {
|
|
44
|
+
"types": "./dist/cockpitHarness.d.mts",
|
|
45
|
+
"import": "./dist/cockpitHarness.mjs",
|
|
46
|
+
"require": "./dist/cockpitHarness.cjs"
|
|
47
|
+
},
|
|
43
48
|
"./sandbox-harness": {
|
|
44
49
|
"types": "./dist/sandboxHarness.d.mts",
|
|
45
50
|
"import": "./dist/sandboxHarness.mjs",
|
|
@@ -57,7 +62,7 @@
|
|
|
57
62
|
},
|
|
58
63
|
"dependencies": {
|
|
59
64
|
"@deepseek-ai/cordis": "4.0.1",
|
|
60
|
-
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.
|
|
65
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.40"
|
|
61
66
|
},
|
|
62
67
|
"devDependencies": {
|
|
63
68
|
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
@@ -66,7 +71,7 @@
|
|
|
66
71
|
"tsdown": "0.22.14",
|
|
67
72
|
"typescript": "7.0.2",
|
|
68
73
|
"vitest": "4.1.11",
|
|
69
|
-
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.
|
|
74
|
+
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.38"
|
|
70
75
|
},
|
|
71
76
|
"peerDependencies": {
|
|
72
77
|
"@earendil-works/pi-coding-agent": "0.84.3"
|