@agimon-ai/po-sandbox 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/dist/apiContracts.cjs +17 -0
  4. package/dist/apiContracts.cjs.map +1 -0
  5. package/dist/apiContracts.d.mts +53 -0
  6. package/dist/apiContracts.d.mts.map +1 -0
  7. package/dist/apiContracts.mjs +13 -0
  8. package/dist/apiContracts.mjs.map +1 -0
  9. package/dist/cockpitHarness.cjs +600 -0
  10. package/dist/cockpitHarness.cjs.map +1 -0
  11. package/dist/cockpitHarness.d.mts +41 -0
  12. package/dist/cockpitHarness.d.mts.map +1 -0
  13. package/dist/cockpitHarness.mjs +594 -0
  14. package/dist/cockpitHarness.mjs.map +1 -0
  15. package/dist/extensionService-CoqM-IE1.cjs +25 -0
  16. package/dist/extensionService-CoqM-IE1.cjs.map +1 -0
  17. package/dist/extensionService-FmuCugPK.mjs +20 -0
  18. package/dist/extensionService-FmuCugPK.mjs.map +1 -0
  19. package/dist/extensions/mcp.cjs +26 -0
  20. package/dist/extensions/mcp.cjs.map +1 -0
  21. package/dist/extensions/mcp.d.mts +5 -0
  22. package/dist/extensions/mcp.d.mts.map +1 -0
  23. package/dist/extensions/mcp.mjs +21 -0
  24. package/dist/extensions/mcp.mjs.map +1 -0
  25. package/dist/extensions/server.cjs +476 -0
  26. package/dist/extensions/server.cjs.map +1 -0
  27. package/dist/extensions/server.d.mts +5 -0
  28. package/dist/extensions/server.d.mts.map +1 -0
  29. package/dist/extensions/server.mjs +466 -0
  30. package/dist/extensions/server.mjs.map +1 -0
  31. package/dist/index.cjs +3 -0
  32. package/dist/index.d.mts +22 -0
  33. package/dist/index.d.mts.map +1 -0
  34. package/dist/index.mjs +2 -0
  35. package/dist/sandboxBridge-77zC5pQR.mjs +13 -0
  36. package/dist/sandboxBridge-77zC5pQR.mjs.map +1 -0
  37. package/dist/sandboxBridge-Db6DubLC.cjs +81 -0
  38. package/dist/sandboxBridge-Db6DubLC.cjs.map +1 -0
  39. package/llms.txt +11 -0
  40. package/package.json +117 -0
  41. package/src/prompts/po-use-sandbox/SKILL.md +14 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cockpitHarness.cjs","names":["BROKER_SOCKET_ENV","BROKER_SOCKET_CONTAINER_PATH","BROKER_PORT_ENV","BROKER_CONTAINER_PORT","BROKER_ADDRESS_ENV","BROKER_HOST_GATEWAY","BROKER_PROVIDERS_ENV","PO_SANDBOX_ENV","PO_SANDBOX_ENV","spawn","fs","path","os","createHash","execFileSync"],"sources":["../src/services/oauthCallback/index.ts","../src/services/sandboxEnvironment/index.ts","../src/services/containerEnvironment/index.ts","../src/services/devcontainer/index.ts","../src/services/cockpitPlan/index.ts","../src/services/engineProcess/index.ts","../src/services/harness/index.ts","../src/services/runFlags/index.ts","../src/services/sandboxImage/index.ts","../src/services/sandboxImageTag/index.ts","../src/services/cockpitHarness/index.ts"],"sourcesContent":["/**\n * Fixed loopback ports Pi's OAuth flows listen on.\n *\n * A provider redirects the browser to `http://localhost:<port>`, so the host\n * port has to match exactly and cannot be remapped. Providers that bind an\n * ephemeral port instead, such as OpenRouter, cannot be published ahead of the\n * flow and are not covered.\n */\nexport const OAUTH_CALLBACK_PORTS: readonly number[] = [\n 1455, // openai-codex\n 1456, // radius\n 53692, // anthropic\n];\n\nexport const OAUTH_CALLBACK_HOST_ENV = 'PI_OAUTH_CALLBACK_HOST';\n\n/**\n * Address the in-container callback server binds.\n *\n * Pi binds loopback by default, which a published port cannot reach: the\n * engine forwards to the container's external interface, not its loopback.\n * Only the bind address changes; the redirect the provider sees stays\n * `localhost`.\n */\nexport const OAUTH_CONTAINER_BIND = '0.0.0.0';\n\n/** Publishes each reachable callback port back onto the host's loopback. */\nexport function oauthPublishArgs(ports: readonly number[]): string[] {\n return ports.flatMap((port) => ['-p', `127.0.0.1:${port}:${port}`]);\n}\n","const EXACT_NAMES = new Set([\n 'TERM',\n 'COLORTERM',\n 'LANG',\n 'TZ',\n 'PO_PRESET',\n 'ELICITATION_SESSION_ID',\n 'HTTP_PROXY',\n 'HTTPS_PROXY',\n 'NO_PROXY',\n 'http_proxy',\n 'https_proxy',\n 'no_proxy',\n]);\nconst NAME_PREFIXES = ['LC_'];\nconst NAME_SUFFIXES = ['_API_KEY', '_AUTH_TOKEN', '_BASE_URL'];\n\n/** True for variables shaped like a provider credential or endpoint override. */\nexport function isCredentialEnvName(name: string): boolean {\n return NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));\n}\n\nfunction allowed(name: string): boolean {\n return (\n EXACT_NAMES.has(name) ||\n NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) ||\n NAME_SUFFIXES.some((suffix) => name.endsWith(suffix))\n );\n}\n\n/**\n * Projects the host environment down to what a sandboxed session may see.\n *\n * The list is an allowlist on purpose: everything a terminal accumulates\n * (agents, sockets, tokens, paths) stays on the host unless a rule names it.\n * Provider credentials pass through for now; the credential broker planned for\n * the client-server phase removes even those from the container.\n */\nexport function filterSandboxEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n): Record<string, string> {\n const filtered: Record<string, string> = {};\n for (const [name, value] of Object.entries(environment)) {\n if (value !== undefined && allowed(name)) filtered[name] = value;\n }\n return filtered;\n}\n","import { PO_SANDBOX_ENV } from '@agimon-ai/po-core/sandboxHarness';\n\nimport type { BrokerEndpoint } from '../../types/sandboxHarness';\nimport { OAUTH_CALLBACK_HOST_ENV, OAUTH_CONTAINER_BIND } from '../oauthCallback';\nimport {\n BROKER_ADDRESS_ENV,\n BROKER_CONTAINER_PORT,\n BROKER_HOST_GATEWAY,\n BROKER_PORT_ENV,\n BROKER_PROVIDERS_ENV,\n BROKER_SOCKET_CONTAINER_PATH,\n BROKER_SOCKET_ENV,\n} from '../sandboxBridge';\nimport { filterSandboxEnvironment, isCredentialEnvName } from '../sandboxEnvironment';\n\nconst ENABLED = '1';\n\n/** Host broker facts a container plan projects into mounts and environment. */\nexport interface SandboxPlanBroker {\n endpoint: BrokerEndpoint;\n token: string;\n providers: readonly string[];\n /** Credential variables whose value the container receives as the token. */\n withheldEnv: readonly string[];\n}\n\n/**\n * Replaces every credential in the container with the broker's session token.\n *\n * Dropping all credential-shaped variables first is what makes the promise\n * hold for providers the broker does not carry: an unbrokered key would\n * otherwise stay readable inside the container.\n */\nfunction brokeredEnvironment(filtered: Record<string, string>, broker: SandboxPlanBroker): Record<string, string> {\n const environment: Record<string, string> = {};\n for (const [name, value] of Object.entries(filtered)) {\n if (!isCredentialEnvName(name)) environment[name] = value;\n }\n for (const name of broker.withheldEnv) environment[name] = broker.token;\n if (broker.endpoint.transport === 'unix') {\n environment[BROKER_SOCKET_ENV] = BROKER_SOCKET_CONTAINER_PATH;\n // Only the bridge reads this, and only a socket needs the bridge.\n environment[BROKER_PORT_ENV] = String(BROKER_CONTAINER_PORT);\n } else {\n environment[BROKER_ADDRESS_ENV] = `${BROKER_HOST_GATEWAY}:${broker.endpoint.port}`;\n }\n environment[BROKER_PROVIDERS_ENV] = broker.providers.join(',');\n return environment;\n}\n\n/**\n * The environment a sandboxed session runs with: the allowlisted host\n * variables, with every credential substituted by the broker token.\n */\nexport function containerEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n broker?: SandboxPlanBroker,\n loginPorts: readonly number[] = [],\n): Record<string, string> {\n const filtered = filterSandboxEnvironment(environment);\n const resolved = broker ? brokeredEnvironment(filtered, broker) : filtered;\n resolved[PO_SANDBOX_ENV] = ENABLED;\n if (loginPorts.length > 0) resolved[OAUTH_CALLBACK_HOST_ENV] = OAUTH_CONTAINER_BIND;\n return resolved;\n}\n","import path from 'node:path';\n\n/**\n * Relative locations the Dev Containers spec looks for a configuration.\n *\n * Ordered as the spec resolves them, so the first hit on disk wins.\n */\nexport const DEVCONTAINER_CONFIG_PATHS: readonly string[] = ['.devcontainer/devcontainer.json', '.devcontainer.json'];\n\nexport const DEVCONTAINER_DISABLED_ENV = 'PO_SANDBOX_DEVCONTAINER';\nexport const DEVCONTAINER_CLI_PACKAGE = '@devcontainers/cli@0.80.0';\n/** Marks containers this layer brought up, so a stale one can be told apart. */\nexport const DEVCONTAINER_ID_LABEL = 'po.sandbox.devcontainer';\n\nexport interface DevcontainerUpResult {\n containerId?: string;\n /** Where the CLI mounted the workspace inside the container. */\n remoteWorkspaceFolder?: string;\n error?: string;\n}\n\nexport interface DevcontainerUpOptions {\n repoRoot: string;\n /** Host directory holding the broker socket, bind-mounted when one is used. */\n socketDirectory?: string;\n /** Container path the socket directory is mounted at. */\n socketTarget?: string;\n}\n\n/** Arguments for bringing the workspace's own dev container up. */\nexport function devcontainerUpArgs(options: DevcontainerUpOptions): string[] {\n return [\n 'up',\n '--workspace-folder',\n options.repoRoot,\n '--log-format',\n 'json',\n ...(options.socketDirectory && options.socketTarget\n ? ['--mount', `type=bind,source=${options.socketDirectory},target=${options.socketTarget}`]\n : []),\n ];\n}\n\n/**\n * Reads the container id out of the CLI's JSON log stream.\n *\n * The stream carries progress records too, so the result is the last record\n * that reports an outcome rather than the first line parsed.\n */\nexport function parseDevcontainerUp(stdout: string): DevcontainerUpResult {\n let outcome:\n | { outcome?: string; containerId?: string; remoteWorkspaceFolder?: string; message?: string; description?: string }\n | undefined;\n for (const line of stdout.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('{')) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (typeof parsed === 'object' && parsed !== null && 'outcome' in parsed) {\n outcome = parsed as typeof outcome;\n }\n }\n if (!outcome) return { error: 'The dev container CLI reported no outcome.' };\n if (outcome.outcome !== 'success') {\n return { error: outcome.description || outcome.message || `Dev container start reported ${outcome.outcome}.` };\n }\n if (!outcome.containerId) return { error: 'The dev container started but reported no container id.' };\n return { containerId: outcome.containerId, remoteWorkspaceFolder: outcome.remoteWorkspaceFolder };\n}\n\n/**\n * Maps a host path into the dev container's own workspace.\n *\n * A dev container mounts the workspace where its configuration says, commonly\n * /workspaces/<name>, so the host path the launch was given does not exist\n * inside and would fail the exec before the agent ever starts.\n */\nexport function containerWorkspacePath(repoRoot: string, cwd: string, remoteWorkspaceFolder: string): string {\n // A plain prefix test would also map a sibling such as <repo>-other into the container.\n const inside = path.relative(repoRoot, cwd);\n const relative = inside === '' || inside.startsWith('..') || path.isAbsolute(inside) ? '' : inside;\n return relative ? `${remoteWorkspaceFolder}/${relative.split('\\\\').join('/')}` : remoteWorkspaceFolder;\n}\n\n/**\n * Installs the distribution inside a dev container that lacks it.\n *\n * The container is the workspace's own, so it carries the project toolchain\n * rather than this layer's, and nothing guarantees Po is present. `up`\n * reuses an existing container, so this is paid once for its lifetime.\n */\nexport function bootstrapCommand(version: string): string[] {\n return [\n 'sh',\n '-c',\n `command -v po >/dev/null 2>&1 && exit 0; ` +\n `command -v npm >/dev/null 2>&1 || { echo \"po-sandbox: this dev container has no npm to install Po with\" >&2; exit 127; }; ` +\n `npm install -g @agimon-ai/po@${version} >/dev/null`,\n ];\n}\n\n/**\n * Rewrites forwarded host paths into the dev container's workspace.\n *\n * The launcher replays its resolved options, and several carry absolute host\n * paths: --cwd always, plugin and additional directories when set. Those\n * resolve in the built-in image, which mounts the repository at the path it\n * has on the host, but a dev container mounts it wherever its configuration\n * says, and the session would fail looking for a repository that is not there.\n */\nexport function mapForwardArgs(args: readonly string[], repoRoot: string, remoteWorkspaceFolder: string): string[] {\n return args.map((argument) =>\n argument === repoRoot || argument.startsWith(`${repoRoot}/`)\n ? containerWorkspacePath(repoRoot, argument, remoteWorkspaceFolder)\n : argument,\n );\n}\n\nexport interface DevcontainerExecOptions {\n containerId: string;\n cwd: string;\n environment: Readonly<Record<string, string>>;\n hasTty: boolean;\n command: readonly string[];\n}\n\n/**\n * Runs a command in the dev container through the engine rather than the CLI.\n *\n * `devcontainer exec` allocates no terminal, which a full screen TUI needs, so\n * the session is attached to the container the CLI already started.\n */\nexport function devcontainerExecArgs(options: DevcontainerExecOptions): string[] {\n const environmentPairs = Object.entries(options.environment).sort(([left], [right]) => left.localeCompare(right));\n return [\n 'exec',\n '-i',\n ...(options.hasTty ? ['-t'] : []),\n '-w',\n options.cwd,\n ...environmentPairs.flatMap(([name, value]) => ['-e', `${name}=${value}`]),\n options.containerId,\n ...options.command,\n ];\n}\n","import type { CockpitWorkspace } from '@agimon-ai/po-core/cockpitContainer';\nimport { PO_SANDBOX_ENV } from '@agimon-ai/po-core/sandboxHarness';\n\nimport type { SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { containerEnvironment, type SandboxPlanBroker } from '../containerEnvironment';\nimport { DEVCONTAINER_DISABLED_ENV } from '../devcontainer';\n\n/**\n * Projects the cockpit into one detached engine run.\n *\n * Unlike a disposable container wrapped around a single interactive session,\n * this plan 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 = '/po-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 `~/.po/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 = 'po-cockpit-home';\nexport const COCKPIT_LABEL = 'po.cockpit';\nconst COCKPIT_BINARY = 'po-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 [PO_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/po`] : []),\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","import { spawn } from 'node:child_process';\n\nimport type { EngineCaptureResult, EngineProcessRunner, EngineRunOptions } from '../../types/sandboxHarness';\n\nconst INHERIT = 'inherit';\nconst PIPE = 'pipe';\nconst IGNORE = 'ignore';\nconst FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];\n\n/** Spawns the container engine, keeping the session's terminal attached. */\nexport class SpawnEngineProcessRunner implements EngineProcessRunner {\n run(command: string, args: string[], options: EngineRunOptions = {}): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options.input === undefined ? INHERIT : [PIPE, INHERIT, INHERIT],\n });\n if (options.input !== undefined) child.stdin?.end(options.input);\n\n // With a shared terminal the engine sees Ctrl-C itself; forwarding covers\n // detached automation runs, and a duplicate signal is harmless.\n const forwarders = FORWARDED_SIGNALS.map((signal) => {\n const forward = (): void => {\n child.kill(signal);\n };\n process.on(signal, forward);\n return { signal, forward };\n });\n const cleanup = (): void => {\n for (const { signal, forward } of forwarders) process.off(signal, forward);\n };\n\n child.once('error', (error) => {\n cleanup();\n reject(error);\n });\n child.once('exit', (code, signal) => {\n cleanup();\n resolve(code ?? (signal ? 1 : 0));\n });\n });\n }\n\n capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined> {\n return new Promise((resolve) => {\n const child = spawn(command, args, { stdio: [IGNORE, PIPE, IGNORE] });\n let stdout = '';\n child.stdout?.on('data', (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.once('error', () => {\n resolve(undefined);\n });\n child.once('exit', (code) => {\n resolve({ exitCode: code ?? 1, stdout });\n });\n });\n }\n}\n","import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport type { EngineProcessRunner, SandboxEngine } from '../../types/sandboxHarness';\n\nconst ENGINE_ENV = 'PO_SANDBOX_ENGINE';\n// Every entry takes docker's run syntax, which is what the plans emit.\nconst ENGINES: SandboxEngine[] = ['docker', 'podman', 'nerdctl', 'finch'];\nconst DOCKERFILE_NAME = 'Dockerfile';\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 Po version)`);\n const context = fs.mkdtempSync(path.join(os.tmpdir(), 'po-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, ['build', '-t', tag, '--build-arg', `PO_VERSION=${version}`, context]);\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","/**\n * Splits configured engine options into argv entries.\n *\n * Whitespace separated rather than shell parsed: the engine is spawned without\n * a shell, so quoting rules would only invent a syntax the caller does not\n * actually get.\n */\nexport function parseRunFlags(configured: string | undefined): string[] {\n return (configured ?? '').split(/\\s+/).filter((flag) => flag.length > 0);\n}\n\nconst OPTION_FORM = /^--?[^\\s=]+(=.*)?$/s;\n\n/**\n * Requires every configured entry to be a self-contained option.\n *\n * Separated values cannot be told from positional arguments without knowing\n * each option's arity, and a stray positional would silently replace the image\n * or its command. Demanding `--flag=value` removes the ambiguity instead of\n * guessing at it.\n */\nexport function assertRunFlags(flags: readonly string[], optionName: string): void {\n const invalid = flags.find((flag) => !OPTION_FORM.test(flag));\n if (invalid !== undefined) {\n throw new Error(\n `${optionName} accepts engine options in --flag or --flag=value form; \"${invalid}\" is neither. ` +\n 'Write a value as part of its option, for example --runtime=runsc.',\n );\n }\n}\n","import { BRIDGE_CONTAINER_PATH, BRIDGE_FILE_NAME } from '../sandboxBridge';\n\nconst IMAGE_REPOSITORY = 'po-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. PO_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 PO_VERSION',\n 'RUN npm install -g @agimon-ai/po@${PO_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 /po-home',\n 'ENV PO_SANDBOX=1 HOME=/po-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/po-web` is enough for all three programs: its\n * `@agimon-ai/po` dependency owns both the session server executable and\n * the agent runtime.\n */\nexport function cockpitDockerfile(): string {\n return [\n 'FROM node:22-bookworm-slim',\n 'RUN apt-get update \\\\',\n ' && apt-get install -y --no-install-recommends ca-certificates curl git gnupg nano openssh-client ripgrep \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n // The tunnel runs beside the hub rather than on the host, so the cockpit\n // owns its own remote access and all of that state stays in one place.\n 'RUN curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \\\\',\n ' -o /usr/share/keyrings/cloudflare-main.gpg \\\\',\n ' && echo \"deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main\" \\\\',\n ' > /etc/apt/sources.list.d/cloudflared.list \\\\',\n ' && apt-get update && apt-get install -y --no-install-recommends cloudflared \\\\',\n ' && rm -rf /var/lib/apt/lists/*',\n 'ARG PO_VERSION',\n 'RUN npm install -g @agimon-ai/po-web@${PO_VERSION}',\n '# World-writable so the launch can map any host user id onto it.',\n 'RUN mkdir -m 0777 /po-home',\n 'ENV PO_SANDBOX=1 HOME=/po-home',\n '',\n ].join('\\n');\n}\n","import { createHash } from 'node:crypto';\n\nimport { sandboxBridgeSource } from '../sandboxBridge';\nimport { cockpitDockerfile, formatImageTag, sandboxDockerfile } from '../sandboxImage';\n\nconst DIGEST_LENGTH = 8;\n\n/** Tags the image with the distribution version and a digest of its definition. */\nexport function sandboxImageTag(version: string): string {\n const digest = createHash('sha256')\n .update(sandboxDockerfile())\n .update(sandboxBridgeSource())\n .digest('hex')\n .slice(0, DIGEST_LENGTH);\n return formatImageTag(version, digest);\n}\n\n/**\n * Tags the cockpit image separately from the sandbox one.\n *\n * Same digest scheme, different definition, so editing either Dockerfile\n * invalidates only its own image.\n */\nexport function cockpitImageTag(version: string): string {\n const digest = createHash('sha256').update(cockpitDockerfile()).digest('hex').slice(0, DIGEST_LENGTH);\n return formatImageTag(`${version}-cockpit`, digest);\n}\n","import { execFileSync } from 'node:child_process';\n\nimport type {\n CockpitContainerHandle,\n CockpitContainerHarnessModule,\n CockpitContainerRequest,\n CockpitContainerStart,\n} from '@agimon-ai/po-core/cockpitContainer';\n\nimport type { EngineProcessRunner, SandboxEngine } from '../../types/sandboxHarness';\nimport { buildCockpitPlan, type CockpitGitIdentity } from '../cockpitPlan';\nimport { SpawnEngineProcessRunner } from '../engineProcess';\nimport { detectEngine, ensureImage } from '../harness';\nimport { parseRunFlags, assertRunFlags } from '../runFlags';\nimport { cockpitDockerfile } from '../sandboxImage';\nimport { cockpitImageTag } from '../sandboxImageTag';\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 = 'PO_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,41 @@
1
+ import { CockpitContainerHarnessModule, CockpitContainerRequest, CockpitContainerStart } from "@agimon-ai/po-core/cockpitContainer";
2
+ //#region src/types/sandboxHarness.d.ts
3
+ interface EngineRunOptions {
4
+ /** Pipes this to stdin instead of attaching the session's stdio. */
5
+ input?: string;
6
+ }
7
+ interface EngineCaptureResult {
8
+ exitCode: number;
9
+ stdout: string;
10
+ }
11
+ /** Runs container engine commands with the session's terminal attached. */
12
+ interface EngineProcessRunner {
13
+ run(command: string, args: string[], options?: EngineRunOptions): Promise<number>;
14
+ /** Captures stdout for probes; an unspawnable command reports undefined. */
15
+ capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined>;
16
+ }
17
+ //#endregion
18
+ //#region src/services/cockpitPlan/index.d.ts
19
+ /** Git identity the agent commits under; no key crosses the boundary, so it cannot push. */
20
+ interface CockpitGitIdentity {
21
+ name: string;
22
+ email: string;
23
+ }
24
+ //#endregion
25
+ //#region src/services/cockpitHarness/index.d.ts
26
+ interface CockpitHarnessDependencies {
27
+ runner?: EngineProcessRunner;
28
+ version?: string;
29
+ /** Test seam: reports whether the cockpit inside is answering yet. */
30
+ probe?: (port: number) => Promise<boolean>;
31
+ /** Test seam: the host's git identity, which is read from the real config by default. */
32
+ gitIdentity?: () => CockpitGitIdentity | undefined;
33
+ now?: () => number;
34
+ }
35
+ export declare function createCockpitHarness(dependencies?: CockpitHarnessDependencies): CockpitContainerHarnessModule;
36
+ /** The module-shaped entry a host resolves from the composition. */
37
+ export declare function startCockpitContainer(request: CockpitContainerRequest): Promise<CockpitContainerStart>;
38
+ export declare function reapCockpitContainer(containerId: string): Promise<boolean>;
39
+ //#endregion
40
+ export type { CockpitHarnessDependencies };
41
+ //# sourceMappingURL=cockpitHarness.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cockpitHarness.d.mts","names":[],"sources":["../src/types/sandboxHarness.ts","../src/services/cockpitPlan/index.ts","../src/services/cockpitHarness/index.ts"],"mappings":";;UAEiB;;EAEf;;UAGe;EACf;EACA;;;UAIe;EACf,IAAI,iBAAiB,gBAAgB,UAAU,mBAAmB;;EAElE,QAAQ,iBAAiB,iBAAiB,QAAQ;;;;;UC6BnC;EACf;EACA;;;;UChBe;EACf,SAAS;EACT;;EAEA,SAAS,iBAAiB;;EAE1B,oBAAoB;EACpB;;wBAwDc,qBAAqB,eAAc,6BAAkC;;wBAgG/D,sBAAsB,SAAS,0BAA0B,QAAQ;wBAIjE,qBAAqB,sBAAsB"}