@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 +8 -0
- package/dist/commands/root.js +141 -29
- package/dist/commands/runner/common.js +1 -0
- package/dist/commands/sdk/codex/auth.js +219 -89
- package/dist/commands/shell.js +300 -12
- package/dist/service/app_server.js +3 -0
- package/dist/state/schema.js +1 -0
- package/drizzle/0002_kind_sue_storm.sql +45 -1
- package/drizzle/0012_magenta_chameleon.sql +19 -0
- package/drizzle/meta/0012_snapshot.json +313 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +2 -2
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
|
package/dist/commands/root.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
|
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
|
|
1610
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
"-
|
|
148
|
-
|
|
149
|
-
],
|
|
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);
|