* Create and start a headless coding agent container. * Runs a task via the agent, commits, and merges back. Ephemeral — exits when done. * @param {object} options * @param {string} options.containerName - Docker container name * @param {string} options.repo - GitHub repo full name * @param {st
({ containerName, repo, branch, featureBranch, workspaceId, taskPrompt, mode = 'plan', codingAgent, systemPrompt, continueSession = true, injectSecrets, scope, userId })
| 415 | * @returns {Promise<{containerId: string, containerName: string}>} |
| 416 | */ |
| 417 | async function runHeadlessContainer({ containerName, repo, branch, featureBranch, workspaceId, taskPrompt, mode = 'plan', codingAgent, systemPrompt, continueSession = true, injectSecrets, scope, userId }) { |
| 418 | const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code'; |
| 419 | const version = process.env.THEPOPEBOT_VERSION; |
| 420 | const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`; |
| 421 | |
| 422 | const env = [ |
| 423 | `RUNTIME=headless`, |
| 424 | `REPO=${repo}`, |
| 425 | `BRANCH=${branch}`, |
| 426 | `PROMPT=${taskPrompt}`, |
| 427 | `USER_ID=${userId}`, |
| 428 | ]; |
| 429 | if (featureBranch) { |
| 430 | env.push(`FEATURE_BRANCH=${featureBranch}`); |
| 431 | } |
| 432 | if (mode) { |
| 433 | // Map 'dangerous' to 'code', keep 'plan' as-is |
| 434 | const permission = mode === 'dangerous' ? 'code' : mode; |
| 435 | env.push(`PERMISSION=${permission}`); |
| 436 | } |
| 437 | if (continueSession) { |
| 438 | env.push(`CONTINUE_SESSION=1`); |
| 439 | } |
| 440 | if (scope) { |
| 441 | env.push(`SCOPE=${scope}`); |
| 442 | } |
| 443 | |
| 444 | // Auth env vars based on agent type |
| 445 | const { env: authEnv, backendApi } = buildAgentAuthEnv(agent); |
| 446 | env.push(...authEnv); |
| 447 | |
| 448 | const ghToken = getConfig('GH_TOKEN'); |
| 449 | if (ghToken) { |
| 450 | env.push(`GH_TOKEN=${ghToken}`); |
| 451 | } |
| 452 | |
| 453 | // Inject agent job secrets when running in agent chat mode |
| 454 | if (injectSecrets) { |
| 455 | const { getAllAgentJobSecrets } = await import('../db/config.js'); |
| 456 | const jobSecrets = getAllAgentJobSecrets(); |
| 457 | for (const { key, value } of jobSecrets) { |
| 458 | if (value !== null && !env.some(e => e.startsWith(`${key}=`))) { |
| 459 | env.push(`${key}=${value}`); |
| 460 | } |
| 461 | } |
| 462 | // Create per-container API key for agent-secrets access |
| 463 | const { createAgentJobApiKey } = await import('../db/api-keys.js'); |
| 464 | const { key: agentJobToken } = createAgentJobApiKey(containerName); |
| 465 | env.push(`AGENT_JOB_TOKEN=${agentJobToken}`); |
| 466 | const appUrl = getConfig('APP_URL'); |
| 467 | if (appUrl) env.push(`APP_URL=${appUrl}`); |
| 468 | } |
| 469 | |
| 470 | const hostConfig = {}; |
| 471 | if (workspaceId) { |
| 472 | const dir = workspaceDir(workspaceId); |
| 473 | const wsDir = path.join(dir, 'workspace'); |
| 474 | fs.mkdirSync(wsDir, { recursive: true }); |
no test coverage detected