{"version":3,"file":"cockpitHarness.mjs","names":[],"sources":["../src/services/cockpitPlan/index.ts","../src/services/cockpitHarness/index.ts"],"sourcesContent":["import type { CockpitWorkspace } from '@agimon-ai/doompi-core/cockpitContainer';\nimport { DOOMPI_SANDBOX_ENV } from '@agimon-ai/doompi-core/sandboxHarness';\n\nimport type { SandboxEngine, SandboxHostFacts } from '../../types/sandboxHarness';\nimport { DEVCONTAINER_DISABLED_ENV } from '../devcontainer';\nimport { containerEnvironment, type SandboxPlanBroker } from '../sandboxPlan';\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","import { execFileSync } from 'node:child_process';\n\nimport type {\n  CockpitContainerHandle,\n  CockpitContainerHarnessModule,\n  CockpitContainerRequest,\n  CockpitContainerStart,\n} from '@agimon-ai/doompi-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 = '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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"}