@companyhelm/runner 0.0.13 → 0.0.15

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 CHANGED
@@ -36,6 +36,14 @@ Check whether the daemon is running:
36
36
  companyhelm-runner status
37
37
  ```
38
38
 
39
+ Open the interactive state DB shell:
40
+
41
+ ```bash
42
+ companyhelm-runner shell
43
+ ```
44
+
45
+ From the shell you can inspect threads and daemon state, and open a Docker bash session inside a selected thread runtime container.
46
+
39
47
  The `status` command prints:
40
48
 
41
49
  - whether the daemon is running
@@ -58,7 +58,6 @@ const node_crypto_1 = require("node:crypto");
58
58
  const node_fs_1 = require("node:fs");
59
59
  const node_path_1 = require("node:path");
60
60
  const config_js_1 = require("../config.js");
61
- const startup_js_1 = require("./startup.js");
62
61
  const companyhelm_api_client_js_1 = require("../service/companyhelm_api_client.js");
63
62
  const buffered_client_message_sender_js_1 = require("../service/buffered_client_message_sender.js");
64
63
  const host_js_1 = require("../service/host.js");
@@ -77,6 +76,7 @@ const logger_js_1 = require("../utils/logger.js");
77
76
  const path_js_1 = require("../utils/path.js");
78
77
  const terminal_js_1 = require("../utils/terminal.js");
79
78
  const workspace_agents_js_1 = require("../service/workspace_agents.js");
79
+ const auth_js_1 = require("./sdk/codex/auth.js");
80
80
  const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
81
81
  const COMMAND_CHANNEL_OPEN_TIMEOUT_MS = 5000;
82
82
  const TURN_COMPLETION_TIMEOUT_MS = 2 * 60 * 60000;
@@ -1515,6 +1515,28 @@ async function sendHeartbeatResponse(commandChannel, requestId) {
1515
1515
  });
1516
1516
  await commandChannel.send(message);
1517
1517
  }
1518
+ async function sendCodexDeviceCode(commandChannel, deviceCode, requestId) {
1519
+ const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
1520
+ requestId,
1521
+ payload: {
1522
+ case: "codexDeviceCode",
1523
+ value: {
1524
+ deviceCode,
1525
+ },
1526
+ },
1527
+ });
1528
+ await commandChannel.send(message);
1529
+ }
1530
+ async function sendAgentSdkUpdate(commandChannel, sdk, requestId) {
1531
+ const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
1532
+ requestId,
1533
+ payload: {
1534
+ case: "agentSdkUpdate",
1535
+ value: sdk,
1536
+ },
1537
+ });
1538
+ await commandChannel.send(message);
1539
+ }
1518
1540
  async function sendThreadUpdate(commandChannel, threadId, status, requestId) {
1519
1541
  const message = (0, protobuf_1.create)(protos_1.ClientMessageSchema, {
1520
1542
  requestId,
@@ -1575,12 +1597,12 @@ async function sendItemExecutionUpdate(commandChannel, threadId, sdkTurnId, sdkI
1575
1597
  });
1576
1598
  await commandChannel.send(message);
1577
1599
  }
1578
- async function buildRegisterRunnerRequest(cfg) {
1600
+ async function loadRunnerRegistrationSdks(cfg, logger) {
1579
1601
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1580
1602
  try {
1581
1603
  const configuredSdks = await db.select().from(schema_js_1.agentSdks).orderBy(schema_js_1.agentSdks.name).all();
1582
1604
  if (configuredSdks.length === 0) {
1583
- throw new Error("No SDKs configured. Run startup before connecting to CompanyHelm API.");
1605
+ return [];
1584
1606
  }
1585
1607
  const models = await db.select().from(schema_js_1.llmModels).orderBy(schema_js_1.llmModels.sdkName, schema_js_1.llmModels.name).all();
1586
1608
  const modelsBySdk = new Map();
@@ -1592,52 +1614,107 @@ async function buildRegisterRunnerRequest(cfg) {
1592
1614
  });
1593
1615
  modelsBySdk.set(model.sdkName, sdkModels);
1594
1616
  }
1595
- return (0, protobuf_1.create)(protos_1.RegisterRunnerRequestSchema, {
1596
- agentSdks: configuredSdks.map((sdk) => ({
1597
- name: sdk.name,
1598
- models: modelsBySdk.get(sdk.name) ?? [],
1599
- })),
1617
+ return configuredSdks.map((sdk) => {
1618
+ if (sdk.name !== "codex") {
1619
+ return {
1620
+ name: sdk.name,
1621
+ models: sdk.status === "configured" ? (modelsBySdk.get(sdk.name) ?? []) : [],
1622
+ status: sdk.status === "configured" ? protos_1.AgentSdkStatus.READY : protos_1.AgentSdkStatus.UNCONFIGURED,
1623
+ };
1624
+ }
1625
+ if (sdk.status !== "configured" || sdk.authentication === "unauthenticated") {
1626
+ return {
1627
+ name: sdk.name,
1628
+ models: [],
1629
+ status: protos_1.AgentSdkStatus.UNCONFIGURED,
1630
+ };
1631
+ }
1632
+ try {
1633
+ logger.debug("Refreshing Codex models for runner registration.");
1634
+ return {
1635
+ name: sdk.name,
1636
+ models: modelsBySdk.get(sdk.name) ?? [],
1637
+ status: protos_1.AgentSdkStatus.READY,
1638
+ };
1639
+ }
1640
+ catch (error) {
1641
+ return {
1642
+ name: sdk.name,
1643
+ models: modelsBySdk.get(sdk.name) ?? [],
1644
+ status: protos_1.AgentSdkStatus.ERROR,
1645
+ errorMessage: (0, refresh_models_js_1.formatSdkModelRefreshFailure)("codex", error),
1646
+ };
1647
+ }
1600
1648
  });
1601
1649
  }
1602
1650
  finally {
1603
1651
  client.close();
1604
1652
  }
1605
1653
  }
1606
- async function hasConfiguredSdks(cfg) {
1654
+ async function countSdkModels(cfg, sdkName) {
1607
1655
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1608
1656
  try {
1609
- const configuredSdks = await db.select().from(schema_js_1.agentSdks).all();
1610
- return configuredSdks.length > 0;
1657
+ const models = await db.select({ name: schema_js_1.llmModels.name }).from(schema_js_1.llmModels).where((0, drizzle_orm_1.eq)(schema_js_1.llmModels.sdkName, sdkName)).all();
1658
+ return models.length;
1611
1659
  }
1612
1660
  finally {
1613
1661
  client.close();
1614
1662
  }
1615
1663
  }
1616
- async function countSdkModels(cfg, sdkName) {
1664
+ async function refreshCodexModelsForRegistration(cfg, logger) {
1617
1665
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1666
+ let codexSdk;
1618
1667
  try {
1619
- const models = await db.select({ name: schema_js_1.llmModels.name }).from(schema_js_1.llmModels).where((0, drizzle_orm_1.eq)(schema_js_1.llmModels.sdkName, sdkName)).all();
1620
- return models.length;
1668
+ codexSdk = await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get() ?? undefined;
1621
1669
  }
1622
1670
  finally {
1623
1671
  client.close();
1624
1672
  }
1625
- }
1626
- async function refreshCodexModelsForRegistration(cfg, logger) {
1673
+ if (!codexSdk || codexSdk.status !== "configured" || codexSdk.authentication === "unauthenticated") {
1674
+ logger.info("Codex is not configured; registering runner with unconfigured Codex SDK state.");
1675
+ return null;
1676
+ }
1627
1677
  try {
1628
1678
  const results = await (0, refresh_models_js_1.refreshSdkModels)({ sdk: "codex", logger });
1629
1679
  const modelCount = results[0]?.modelCount ?? 0;
1630
1680
  logger.info(`Refreshed Codex models from container app-server (${modelCount} models).`);
1681
+ return null;
1631
1682
  }
1632
1683
  catch (error) {
1633
1684
  const cachedModelCount = await countSdkModels(cfg, "codex");
1634
1685
  const failureMessage = (0, refresh_models_js_1.formatSdkModelRefreshFailure)("codex", error);
1635
- if (cachedModelCount === 0) {
1636
- throw new Error(`${failureMessage} Runner startup aborted because no cached Codex models are available; refusing to register zero models.`);
1686
+ if (cachedModelCount > 0) {
1687
+ logger.warn(`${failureMessage} Using ${cachedModelCount} cached Codex model(s) while registering Codex in error state.`);
1688
+ }
1689
+ else {
1690
+ logger.warn(`${failureMessage} Registering Codex in error state with zero models.`);
1637
1691
  }
1638
- logger.warn(`${failureMessage} Using ${cachedModelCount} cached Codex model(s) from local state instead of registering zero models.`);
1692
+ return failureMessage;
1639
1693
  }
1640
1694
  }
1695
+ async function buildRegisterRunnerRequest(cfg, logger, codexRefreshErrorMessage) {
1696
+ const sdks = await loadRunnerRegistrationSdks(cfg, logger);
1697
+ return (0, protobuf_1.create)(protos_1.RegisterRunnerRequestSchema, {
1698
+ agentSdks: sdks.map((sdk) => ({
1699
+ name: sdk.name,
1700
+ models: sdk.models,
1701
+ status: sdk.name === "codex" && codexRefreshErrorMessage
1702
+ ? protos_1.AgentSdkStatus.ERROR
1703
+ : sdk.status,
1704
+ errorMessage: sdk.name === "codex" ? (codexRefreshErrorMessage ?? sdk.errorMessage) : sdk.errorMessage,
1705
+ })),
1706
+ });
1707
+ }
1708
+ async function buildCodexAgentSdkUpdate(cfg, logger, statusOverride, errorMessage) {
1709
+ const sdks = await loadRunnerRegistrationSdks(cfg, logger);
1710
+ const codex = sdks.find((sdk) => sdk.name === "codex");
1711
+ return {
1712
+ name: "codex",
1713
+ models: codex?.models ?? [],
1714
+ status: statusOverride ?? codex?.status ?? protos_1.AgentSdkStatus.UNCONFIGURED,
1715
+ errorMessage: errorMessage ?? codex?.errorMessage,
1716
+ };
1717
+ }
1641
1718
  async function resolveThreadAuthMode(cfg) {
1642
1719
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1643
1720
  try {
@@ -2292,6 +2369,42 @@ async function handleCreateUserMessageRequest(cfg, commandChannel, request, requ
2292
2369
  const trackTurnCompletion = startedFromIdle;
2293
2370
  void executeCreateUserMessageRequest(cfg, commandChannel, request, requestId, threadState, startedFromIdle, trackTurnCompletion, logger);
2294
2371
  }
2372
+ async function handleCodexConfigurationRequest(cfg, commandChannel, request, requestId, logger) {
2373
+ try {
2374
+ if (request.authType === protos_1.CodexAuthType.API_KEY) {
2375
+ const apiKey = String(request.codexApiKey ?? "").trim();
2376
+ if (!apiKey) {
2377
+ await sendRequestError(commandChannel, "Codex API key is required.", requestId);
2378
+ return;
2379
+ }
2380
+ await (0, auth_js_1.runCodexApiKeyAuth)(cfg, apiKey, {
2381
+ logInfo: (message) => logger.info(message),
2382
+ logSuccess: (message) => logger.info(message),
2383
+ });
2384
+ }
2385
+ else if (request.authType === protos_1.CodexAuthType.DEVICE_CODE) {
2386
+ await (0, auth_js_1.runCodexDeviceCodeAuth)(cfg, async (deviceCode) => {
2387
+ await sendCodexDeviceCode(commandChannel, deviceCode, requestId);
2388
+ }, {
2389
+ logInfo: (message) => logger.info(message),
2390
+ logSuccess: (message) => logger.info(message),
2391
+ });
2392
+ }
2393
+ else {
2394
+ await sendRequestError(commandChannel, "Unsupported Codex auth type.", requestId);
2395
+ return;
2396
+ }
2397
+ const codexRefreshErrorMessage = await refreshCodexModelsForRegistration(cfg, logger);
2398
+ const sdkUpdate = await buildCodexAgentSdkUpdate(cfg, logger, codexRefreshErrorMessage ? protos_1.AgentSdkStatus.ERROR : protos_1.AgentSdkStatus.READY, codexRefreshErrorMessage ?? undefined);
2399
+ await sendAgentSdkUpdate(commandChannel, sdkUpdate, requestId);
2400
+ }
2401
+ catch (error) {
2402
+ const message = error instanceof Error ? error.message : String(error);
2403
+ const sdkUpdate = await buildCodexAgentSdkUpdate(cfg, logger, protos_1.AgentSdkStatus.ERROR, message);
2404
+ await sendAgentSdkUpdate(commandChannel, sdkUpdate, requestId);
2405
+ await sendRequestError(commandChannel, message, requestId);
2406
+ }
2407
+ }
2295
2408
  async function runCommandLoop(cfg, commandChannel, commandMessageSink, apiClient, apiCallOptions, logger) {
2296
2409
  for await (const serverMessage of commandChannel) {
2297
2410
  const requestId = extractServerMessageRequestId(serverMessage);
@@ -2313,6 +2426,9 @@ async function runCommandLoop(cfg, commandChannel, commandMessageSink, apiClient
2313
2426
  case "heartbeatRequest":
2314
2427
  await sendHeartbeatResponse(commandMessageSink, requestId);
2315
2428
  break;
2429
+ case "codexConfigurationRequest":
2430
+ await handleCodexConfigurationRequest(cfg, commandMessageSink, serverMessage.request.value, requestId, logger);
2431
+ break;
2316
2432
  default:
2317
2433
  break;
2318
2434
  }
@@ -2503,16 +2619,12 @@ function sendDaemonParentMessage(message) {
2503
2619
  async function runRootCommand(options, runtimeOptions) {
2504
2620
  const logger = (0, logger_js_1.createLogger)(options.logLevel ?? "INFO", { daemonMode: options.daemon ?? false });
2505
2621
  const cfg = buildRootConfig(options);
2506
- const configuredSdks = await hasConfiguredSdks(cfg);
2507
- if (!configuredSdks && options.daemon) {
2508
- throw new Error("No SDKs configured. Daemon mode requires at least one configured SDK.");
2509
- }
2510
- if (!configuredSdks) {
2511
- await (0, startup_js_1.startup)(cfg);
2512
- (0, terminal_js_1.restoreInteractiveTerminalState)();
2513
- }
2514
- await refreshCodexModelsForRegistration(cfg, logger);
2515
- const registerRequest = await buildRegisterRunnerRequest(cfg);
2622
+ await (0, auth_js_1.ensureCodexRunnerStartState)(cfg, {
2623
+ useDedicatedAuth: options.useDedicatedAuth,
2624
+ logInfo: (message) => logger.info(message),
2625
+ });
2626
+ const codexRefreshErrorMessage = await refreshCodexModelsForRegistration(cfg, logger);
2627
+ const registerRequest = await buildRegisterRunnerRequest(cfg, logger, codexRefreshErrorMessage);
2516
2628
  const apiCallOptions = buildGrpcAuthCallOptions(options.secret);
2517
2629
  if (options.daemon) {
2518
2630
  await (0, daemon_state_js_1.claimCurrentDaemonState)(cfg.state_db_path, process.pid, resolveEffectiveDaemonLogPath(cfg));
@@ -11,6 +11,7 @@ function addRunnerStartOptions(command) {
11
11
  .option("--state-db-path <path>", "State database path override (defaults to state.db under the active config directory).")
12
12
  .option("--log-path <path>", "Daemon log file override.")
13
13
  .option("--use-host-docker-runtime", "Mount host Docker socket into runtime containers instead of creating DinD sidecars.")
14
+ .option("--use-dedicated-auth", "Preserve existing dedicated Codex auth if already configured; otherwise keep Codex unconfigured on startup.")
14
15
  .option("--host-docker-path <path>", "Host Docker endpoint when --use-host-docker-runtime is enabled (unix:///<socket-path> or tcp://localhost:<port>).")
15
16
  .option("--thread-git-skills-directory <path>", "Container path where thread git skill repositories are cloned before linking into ~/.codex/skills.")
16
17
  .option("-d, --daemon", "Run in daemon mode and fail fast when no SDK is configured.")
@@ -1,14 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.defaultUseDedicatedCodexAuthDependencies = exports.defaultSetCodexHostAuthDependencies = void 0;
3
+ exports.defaultUseDedicatedCodexAuthDependencies = exports.defaultEnsureCodexRunnerStartStateDependencies = exports.defaultSetCodexHostAuthDependencies = void 0;
4
4
  exports.isErrnoException = isErrnoException;
5
5
  exports.buildDockerMissingError = buildDockerMissingError;
6
6
  exports.ensureDockerAvailable = ensureDockerAvailable;
7
+ exports.extractCodexDeviceCodeFromOutput = extractCodexDeviceCodeFromOutput;
7
8
  exports.listCodexStartupAuthOptions = listCodexStartupAuthOptions;
8
9
  exports.setCodexHostAuthInDb = setCodexHostAuthInDb;
9
10
  exports.setCodexDedicatedAuthInDb = setCodexDedicatedAuthInDb;
11
+ exports.setCodexApiKeyAuthInDb = setCodexApiKeyAuthInDb;
12
+ exports.setCodexUnconfiguredInDb = setCodexUnconfiguredInDb;
13
+ exports.ensureCodexRunnerStartState = ensureCodexRunnerStartState;
10
14
  exports.runSetCodexHostAuth = runSetCodexHostAuth;
11
15
  exports.runDedicatedCodexAuth = runDedicatedCodexAuth;
16
+ exports.runCodexApiKeyAuth = runCodexApiKeyAuth;
17
+ exports.runCodexDeviceCodeAuth = runCodexDeviceCodeAuth;
12
18
  exports.runUseDedicatedCodexAuth = runUseDedicatedCodexAuth;
13
19
  const node_child_process_1 = require("node:child_process");
14
20
  const node_fs_1 = require("node:fs");
@@ -22,6 +28,11 @@ exports.defaultSetCodexHostAuthDependencies = {
22
28
  getHostInfoFn: host_js_1.getHostInfo,
23
29
  initDbFn: db_js_1.initDb,
24
30
  };
31
+ exports.defaultEnsureCodexRunnerStartStateDependencies = {
32
+ ...exports.defaultSetCodexHostAuthDependencies,
33
+ logInfo: () => undefined,
34
+ useDedicatedAuth: false,
35
+ };
25
36
  exports.defaultUseDedicatedCodexAuthDependencies = {
26
37
  initDbFn: db_js_1.initDb,
27
38
  logInfo: console.log,
@@ -41,6 +52,100 @@ function ensureDockerAvailable(spawnSyncCommand) {
41
52
  throw buildDockerMissingError();
42
53
  }
43
54
  }
55
+ function extractCodexDeviceCodeFromOutput(output) {
56
+ const normalizedOutput = output.replace(/\u001b\[[0-9;]*m/g, "");
57
+ const match = normalizedOutput.match(/Enter this one-time code[\s\S]*?\n\s*([A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+)\s*(?:\n|$)/i);
58
+ return match?.[1]?.trim() ?? null;
59
+ }
60
+ async function runContainerizedCodexLogin(cfg, deps, options) {
61
+ ensureDockerAvailable(deps.spawnSyncCommand);
62
+ const configDir = (0, path_js_1.expandHome)(cfg.config_directory);
63
+ if (!(0, node_fs_1.existsSync)(configDir)) {
64
+ (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
65
+ }
66
+ const destPath = (0, node_path_1.join)(configDir, cfg.codex.codex_auth_file_path);
67
+ const containerAuthPath = cfg.codex.codex_auth_path;
68
+ let authCopied = false;
69
+ let combinedOutput = "";
70
+ await new Promise((resolve, reject) => {
71
+ let settled = false;
72
+ let poll;
73
+ const cleanup = () => {
74
+ if (poll) {
75
+ clearInterval(poll);
76
+ }
77
+ deps.spawnSyncCommand("docker", ["rm", "-f", options.containerName], { stdio: "ignore" });
78
+ };
79
+ const tryCopyAuthFile = () => {
80
+ const check = deps.spawnSyncCommand("docker", ["exec", options.containerName, "sh", "-c", `test -f ${containerAuthPath}`], {
81
+ stdio: "ignore",
82
+ });
83
+ if (check.status !== 0) {
84
+ return false;
85
+ }
86
+ const cpResult = deps.spawnSyncCommand("docker", ["cp", `${options.containerName}:${containerAuthPath}`, destPath], {
87
+ stdio: "ignore",
88
+ });
89
+ if (cpResult.status !== 0) {
90
+ return false;
91
+ }
92
+ authCopied = true;
93
+ cleanup();
94
+ return true;
95
+ };
96
+ const rejectOnce = (error) => {
97
+ if (settled) {
98
+ return;
99
+ }
100
+ settled = true;
101
+ cleanup();
102
+ reject(error);
103
+ };
104
+ const resolveOnce = () => {
105
+ if (settled) {
106
+ return;
107
+ }
108
+ settled = true;
109
+ cleanup();
110
+ resolve();
111
+ };
112
+ const child = deps.spawnCommand("docker", options.dockerArgs, { stdio: ["ignore", "pipe", "pipe"] });
113
+ const handleOutput = (chunk) => {
114
+ const text = chunk.toString("utf8");
115
+ combinedOutput += text;
116
+ options.onOutput?.(combinedOutput);
117
+ };
118
+ child.stdout.on("data", handleOutput);
119
+ child.stderr.on("data", handleOutput);
120
+ child.on("error", (error) => {
121
+ if (isErrnoException(error) && error.code === "ENOENT") {
122
+ rejectOnce(buildDockerMissingError());
123
+ return;
124
+ }
125
+ rejectOnce(new Error(`Failed to start Codex login container: ${error.message}`));
126
+ });
127
+ poll = setInterval(() => {
128
+ if (settled) {
129
+ return;
130
+ }
131
+ if (tryCopyAuthFile()) {
132
+ resolveOnce();
133
+ }
134
+ }, 1000);
135
+ child.on("exit", () => {
136
+ if (authCopied) {
137
+ resolveOnce();
138
+ return;
139
+ }
140
+ if (tryCopyAuthFile()) {
141
+ resolveOnce();
142
+ return;
143
+ }
144
+ rejectOnce(new Error(`Codex login failed or was cancelled.${combinedOutput.trim().length > 0 ? ` Output: ${combinedOutput.trim()}` : ""}`));
145
+ });
146
+ });
147
+ return destPath;
148
+ }
44
149
  function listCodexStartupAuthOptions(cfg, getHostInfoFn = host_js_1.getHostInfo) {
45
150
  const options = [
46
151
  {
@@ -59,27 +164,55 @@ function listCodexStartupAuthOptions(cfg, getHostInfoFn = host_js_1.getHostInfo)
59
164
  }
60
165
  return options;
61
166
  }
62
- async function setCodexHostAuthInDb(db) {
167
+ async function upsertCodexSdkState(db, authentication, status) {
63
168
  const existingSdk = await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get();
64
169
  if (existingSdk) {
65
170
  await db
66
171
  .update(schema_js_1.agentSdks)
67
- .set({ authentication: "host" })
172
+ .set({ authentication, status })
68
173
  .where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex"));
69
174
  return;
70
175
  }
71
- await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "host" });
176
+ await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication, status });
177
+ }
178
+ async function setCodexHostAuthInDb(db) {
179
+ await upsertCodexSdkState(db, "host", "configured");
72
180
  }
73
181
  async function setCodexDedicatedAuthInDb(db) {
74
- const existingSdk = await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get();
75
- if (existingSdk) {
76
- await db
77
- .update(schema_js_1.agentSdks)
78
- .set({ authentication: "dedicated" })
79
- .where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex"));
80
- return;
182
+ await upsertCodexSdkState(db, "dedicated", "configured");
183
+ }
184
+ async function setCodexApiKeyAuthInDb(db) {
185
+ await upsertCodexSdkState(db, "api-key", "configured");
186
+ }
187
+ async function setCodexUnconfiguredInDb(db) {
188
+ await upsertCodexSdkState(db, "unauthenticated", "unconfigured");
189
+ }
190
+ async function ensureCodexRunnerStartState(cfg, overrides = {}) {
191
+ const deps = {
192
+ ...exports.defaultEnsureCodexRunnerStartStateDependencies,
193
+ ...overrides,
194
+ };
195
+ const { db, client } = await deps.initDbFn(cfg.state_db_path);
196
+ try {
197
+ const existingSdk = await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get();
198
+ if (deps.useDedicatedAuth) {
199
+ if (existingSdk?.authentication === "dedicated" && existingSdk.status === "configured") {
200
+ return;
201
+ }
202
+ await setCodexUnconfiguredInDb(db);
203
+ return;
204
+ }
205
+ const hostInfo = deps.getHostInfoFn(cfg.codex.codex_auth_path);
206
+ if (hostInfo.codexAuthExists) {
207
+ deps.logInfo(`Detected Codex host auth at ${(0, path_js_1.expandHome)(cfg.codex.codex_auth_path)}; using host auth automatically.`);
208
+ await setCodexHostAuthInDb(db);
209
+ return;
210
+ }
211
+ await setCodexUnconfiguredInDb(db);
212
+ }
213
+ finally {
214
+ client.close();
81
215
  }
82
- await db.insert(schema_js_1.agentSdks).values({ name: "codex", authentication: "dedicated" });
83
216
  }
84
217
  async function runSetCodexHostAuth(cfg, overrides = {}) {
85
218
  const deps = { ...exports.defaultSetCodexHostAuthDependencies, ...overrides };
@@ -98,96 +231,93 @@ async function runSetCodexHostAuth(cfg, overrides = {}) {
98
231
  return authPath;
99
232
  }
100
233
  async function runDedicatedCodexAuth(cfg, db, deps) {
101
- ensureDockerAvailable(deps.spawnSyncCommand);
102
- const port = cfg.codex.codex_auth_port;
103
- const socatPort = port + 1;
104
234
  const containerName = `companyhelm-codex-auth-${Date.now()}`;
105
235
  deps.logInfo("Starting Codex login inside a container...");
106
- deps.logInfo("A browser URL will appear -- open it to complete authentication.");
107
- const configDir = (0, path_js_1.expandHome)(cfg.config_directory);
108
- if (!(0, node_fs_1.existsSync)(configDir)) {
109
- (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
110
- }
111
- const destPath = (0, node_path_1.join)(configDir, cfg.codex.codex_auth_file_path);
112
- let authCopied = false;
113
- await new Promise((resolve, reject) => {
114
- let settled = false;
115
- let poll;
116
- const rejectOnce = (error) => {
117
- if (settled) {
118
- return;
119
- }
120
- settled = true;
121
- if (poll) {
122
- clearInterval(poll);
123
- }
124
- deps.spawnSyncCommand("docker", ["rm", "-f", containerName], { stdio: "ignore" });
125
- reject(error);
126
- };
127
- const resolveOnce = () => {
128
- if (settled) {
129
- return;
130
- }
131
- settled = true;
132
- if (poll) {
133
- clearInterval(poll);
134
- }
135
- resolve();
136
- };
137
- const child = deps.spawnCommand("docker", [
236
+ deps.logInfo("A browser URL and device code will appear -- open it to complete authentication.");
237
+ const destPath = await runContainerizedCodexLogin(cfg, deps, {
238
+ containerName,
239
+ dockerArgs: [
138
240
  "run",
139
- "-it",
140
241
  "--name",
141
242
  containerName,
142
- "-p",
143
- `${port}:${socatPort}`,
144
243
  "--entrypoint",
145
244
  "bash",
146
245
  cfg.runtime_image,
147
- "-c",
148
- `source "$NVM_DIR/nvm.sh"; socat TCP-LISTEN:${socatPort},fork,bind=0.0.0.0,reuseaddr TCP:127.0.0.1:${port} 2>/dev/null & codex`,
149
- ], { stdio: "inherit" });
150
- child.on("error", (error) => {
151
- if (isErrnoException(error) && error.code === "ENOENT") {
152
- rejectOnce(buildDockerMissingError());
153
- return;
154
- }
155
- rejectOnce(new Error(`Failed to start Codex login container: ${error.message}`));
156
- });
157
- poll = setInterval(() => {
158
- if (settled) {
159
- return;
160
- }
161
- const check = deps.spawnSyncCommand("docker", ["exec", containerName, "sh", "-c", `test -f ${cfg.codex.codex_auth_path}`], {
162
- stdio: "ignore",
163
- });
164
- if (check.status === 0) {
165
- const resolveResult = deps.spawnSyncCommand("docker", ["exec", containerName, "sh", "-c", `echo ${cfg.codex.codex_auth_path}`], {
166
- encoding: "utf-8",
167
- });
168
- const containerAuthAbsPath = resolveResult.stdout.trim();
169
- const cpResult = deps.spawnSyncCommand("docker", ["cp", `${containerName}:${containerAuthAbsPath}`, destPath], {
170
- stdio: "ignore",
171
- });
172
- if (cpResult.status !== 0) {
173
- rejectOnce(new Error("Failed to extract auth file from container."));
174
- return;
175
- }
176
- authCopied = true;
177
- deps.spawnSyncCommand("docker", ["rm", "-f", containerName], { stdio: "ignore" });
178
- resolveOnce();
179
- }
180
- }, 1000);
181
- child.on("exit", () => {
182
- if (!authCopied) {
183
- rejectOnce(new Error("Codex login failed or was cancelled."));
184
- }
185
- });
246
+ "-lc",
247
+ 'source "$NVM_DIR/nvm.sh"; codex login --device-auth',
248
+ ],
186
249
  });
187
250
  await setCodexDedicatedAuthInDb(db);
188
251
  deps.logSuccess(`Codex auth saved to ${destPath}`);
189
252
  return destPath;
190
253
  }
254
+ async function runCodexApiKeyAuth(cfg, apiKey, overrides = {}) {
255
+ const deps = { ...exports.defaultUseDedicatedCodexAuthDependencies, ...overrides };
256
+ const { db, client } = await deps.initDbFn(cfg.state_db_path);
257
+ try {
258
+ deps.logInfo("Starting Codex API key login inside a container...");
259
+ const containerName = `companyhelm-codex-auth-${Date.now()}`;
260
+ const destPath = await runContainerizedCodexLogin(cfg, deps, {
261
+ containerName,
262
+ dockerArgs: [
263
+ "run",
264
+ "--name",
265
+ containerName,
266
+ "-e",
267
+ `CODEX_API_KEY=${apiKey}`,
268
+ "--entrypoint",
269
+ "bash",
270
+ cfg.runtime_image,
271
+ "-lc",
272
+ 'source "$NVM_DIR/nvm.sh"; printf \'%s\\n\' "$CODEX_API_KEY" | codex login --with-api-key',
273
+ ],
274
+ });
275
+ await setCodexApiKeyAuthInDb(db);
276
+ deps.logSuccess(`Codex auth saved to ${destPath}`);
277
+ return destPath;
278
+ }
279
+ finally {
280
+ client.close();
281
+ }
282
+ }
283
+ async function runCodexDeviceCodeAuth(cfg, onDeviceCode, overrides = {}) {
284
+ const deps = { ...exports.defaultUseDedicatedCodexAuthDependencies, ...overrides };
285
+ const { db, client } = await deps.initDbFn(cfg.state_db_path);
286
+ try {
287
+ let emittedDeviceCode = null;
288
+ let onDeviceCodePromise = Promise.resolve();
289
+ deps.logInfo("Starting Codex device login inside a container...");
290
+ const containerName = `companyhelm-codex-auth-${Date.now()}`;
291
+ const destPath = await runContainerizedCodexLogin(cfg, deps, {
292
+ containerName,
293
+ dockerArgs: [
294
+ "run",
295
+ "--name",
296
+ containerName,
297
+ "--entrypoint",
298
+ "bash",
299
+ cfg.runtime_image,
300
+ "-lc",
301
+ 'source "$NVM_DIR/nvm.sh"; codex login --device-auth',
302
+ ],
303
+ onOutput: (output) => {
304
+ const deviceCode = extractCodexDeviceCodeFromOutput(output);
305
+ if (!deviceCode || emittedDeviceCode === deviceCode) {
306
+ return;
307
+ }
308
+ emittedDeviceCode = deviceCode;
309
+ onDeviceCodePromise = Promise.resolve(onDeviceCode(deviceCode));
310
+ },
311
+ });
312
+ await onDeviceCodePromise;
313
+ await setCodexDedicatedAuthInDb(db);
314
+ deps.logSuccess(`Codex auth saved to ${destPath}`);
315
+ return destPath;
316
+ }
317
+ finally {
318
+ client.close();
319
+ }
320
+ }
191
321
  async function runUseDedicatedCodexAuth(cfg, overrides = {}) {
192
322
  const deps = { ...exports.defaultUseDedicatedCodexAuthDependencies, ...overrides };
193
323
  const { db, client } = await deps.initDbFn(cfg.state_db_path);