@companyhelm/runner 0.0.14 → 0.0.17

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.
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCommands = registerCommands;
4
+ const common_js_1 = require("./runner/common.js");
4
5
  const register_runner_commands_js_1 = require("./runner/register-runner-commands.js");
6
+ const start_js_1 = require("./runner/start.js");
5
7
  const shell_js_1 = require("./shell.js");
6
8
  const register_sdk_commands_js_1 = require("./sdk/register-sdk-commands.js");
7
9
  const status_js_1 = require("./status.js");
8
10
  const register_thread_commands_js_1 = require("./thread/register-thread-commands.js");
9
11
  function registerCommands(program) {
12
+ (0, common_js_1.addRunnerStartOptions)(program
13
+ .command("companyhelm-runner")
14
+ .description("Alias for starting the local CompanyHelm runner.")).action(start_js_1.runRunnerStartCommand);
10
15
  (0, register_runner_commands_js_1.registerRunnerCommands)(program);
11
16
  (0, status_js_1.registerStatusCommand)(program);
12
17
  (0, register_thread_commands_js_1.registerThreadCommands)(program);
@@ -1661,15 +1661,17 @@ async function countSdkModels(cfg, sdkName) {
1661
1661
  client.close();
1662
1662
  }
1663
1663
  }
1664
- async function refreshCodexModelsForRegistration(cfg, logger) {
1664
+ async function loadCodexSdkState(cfg) {
1665
1665
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1666
- let codexSdk;
1667
1666
  try {
1668
- codexSdk = await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get() ?? undefined;
1667
+ return await db.select().from(schema_js_1.agentSdks).where((0, drizzle_orm_1.eq)(schema_js_1.agentSdks.name, "codex")).get() ?? undefined;
1669
1668
  }
1670
1669
  finally {
1671
1670
  client.close();
1672
1671
  }
1672
+ }
1673
+ async function refreshCodexModelsForRegistration(cfg, logger) {
1674
+ const codexSdk = await loadCodexSdkState(cfg);
1673
1675
  if (!codexSdk || codexSdk.status !== "configured" || codexSdk.authentication === "unauthenticated") {
1674
1676
  logger.info("Codex is not configured; registering runner with unconfigured Codex SDK state.");
1675
1677
  return null;
@@ -2394,6 +2396,12 @@ async function handleCodexConfigurationRequest(cfg, commandChannel, request, req
2394
2396
  await sendRequestError(commandChannel, "Unsupported Codex auth type.", requestId);
2395
2397
  return;
2396
2398
  }
2399
+ const codexSdk = await loadCodexSdkState(cfg);
2400
+ if (!codexSdk || codexSdk.status !== "configured" || codexSdk.authentication === "unauthenticated") {
2401
+ const sdkUpdate = await buildCodexAgentSdkUpdate(cfg, logger, protos_1.AgentSdkStatus.UNCONFIGURED);
2402
+ await sendAgentSdkUpdate(commandChannel, sdkUpdate, requestId);
2403
+ return;
2404
+ }
2397
2405
  const codexRefreshErrorMessage = await refreshCodexModelsForRegistration(cfg, logger);
2398
2406
  const sdkUpdate = await buildCodexAgentSdkUpdate(cfg, logger, codexRefreshErrorMessage ? protos_1.AgentSdkStatus.ERROR : protos_1.AgentSdkStatus.READY, codexRefreshErrorMessage ?? undefined);
2399
2407
  await sendAgentSdkUpdate(commandChannel, sdkUpdate, requestId);
@@ -1,30 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runRunnerStartCommand = runRunnerStartCommand;
3
4
  exports.registerRunnerStartCommand = registerRunnerStartCommand;
4
5
  const root_js_1 = require("../root.js");
5
6
  const common_js_1 = require("./common.js");
7
+ async function runRunnerStartCommand(options) {
8
+ if (options.daemon && !(0, root_js_1.isInternalDaemonChildProcess)()) {
9
+ await (0, root_js_1.runDetachedDaemonProcess)(options);
10
+ return;
11
+ }
12
+ try {
13
+ await (0, root_js_1.runRootCommand)(options, (0, root_js_1.isInternalDaemonChildProcess)()
14
+ ? {
15
+ onDaemonReady: () => {
16
+ (0, root_js_1.sendDaemonParentMessage)({ type: "daemon-ready" });
17
+ },
18
+ }
19
+ : undefined);
20
+ }
21
+ catch (error) {
22
+ if ((0, root_js_1.isInternalDaemonChildProcess)()) {
23
+ (0, root_js_1.sendDaemonParentMessage)({ type: "daemon-error", message: (0, root_js_1.toErrorMessage)(error) });
24
+ }
25
+ throw error;
26
+ }
27
+ }
6
28
  function registerRunnerStartCommand(runnerCommand) {
7
29
  (0, common_js_1.addRunnerStartOptions)(runnerCommand
8
30
  .command("start")
9
- .description("Start the local CompanyHelm runner daemon.")).action(async (options) => {
10
- if (options.daemon && !(0, root_js_1.isInternalDaemonChildProcess)()) {
11
- await (0, root_js_1.runDetachedDaemonProcess)(options);
12
- return;
13
- }
14
- try {
15
- await (0, root_js_1.runRootCommand)(options, (0, root_js_1.isInternalDaemonChildProcess)()
16
- ? {
17
- onDaemonReady: () => {
18
- (0, root_js_1.sendDaemonParentMessage)({ type: "daemon-ready" });
19
- },
20
- }
21
- : undefined);
22
- }
23
- catch (error) {
24
- if ((0, root_js_1.isInternalDaemonChildProcess)()) {
25
- (0, root_js_1.sendDaemonParentMessage)({ type: "daemon-error", message: (0, root_js_1.toErrorMessage)(error) });
26
- }
27
- throw error;
28
- }
29
- });
31
+ .description("Start the local CompanyHelm runner daemon.")).action(runRunnerStartCommand);
30
32
  }
@@ -40,12 +40,90 @@ exports.defaultUseDedicatedCodexAuthDependencies = {
40
40
  spawnCommand: node_child_process_1.spawn,
41
41
  spawnSyncCommand: node_child_process_1.spawnSync,
42
42
  };
43
+ function resolveContainerPath(pathValue, containerHome) {
44
+ if (pathValue === "~") {
45
+ return containerHome;
46
+ }
47
+ if (pathValue.startsWith("~/")) {
48
+ return `${containerHome}${pathValue.slice(1)}`;
49
+ }
50
+ return pathValue;
51
+ }
52
+ function shellQuote(value) {
53
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
54
+ }
55
+ function buildCodexLoginShellCommand(cfg, loginCommand) {
56
+ const hostInfo = (0, host_js_1.getHostInfo)(cfg.codex.codex_auth_path);
57
+ const containerAuthPath = resolveContainerPath(cfg.codex.codex_auth_path, cfg.agent_home_directory);
58
+ return [
59
+ `AGENT_USER=${shellQuote(cfg.agent_user)}`,
60
+ `AGENT_HOME=${shellQuote(cfg.agent_home_directory)}`,
61
+ `AGENT_UID=${shellQuote(String(hostInfo.uid))}`,
62
+ `AGENT_GID=${shellQuote(String(hostInfo.gid))}`,
63
+ `CODEX_AUTH_PATH=${shellQuote(containerAuthPath)}`,
64
+ `CODEX_LOGIN_COMMAND=${shellQuote(`source "$NVM_DIR/nvm.sh"; ${loginCommand}`)}`,
65
+ 'EXISTING_UID_USER=""',
66
+ 'if getent passwd "$AGENT_UID" >/dev/null 2>&1; then',
67
+ ' EXISTING_UID_USER="$(getent passwd "$AGENT_UID" | cut -d: -f1)"',
68
+ ' AGENT_USER="$EXISTING_UID_USER"',
69
+ 'fi',
70
+ 'AGENT_GROUP="$AGENT_USER"',
71
+ 'if getent group "$AGENT_GID" >/dev/null 2>&1; then',
72
+ ' AGENT_GROUP="$(getent group "$AGENT_GID" | cut -d: -f1)"',
73
+ 'elif getent group "$AGENT_USER" >/dev/null 2>&1; then',
74
+ ' groupmod -g "$AGENT_GID" "$AGENT_USER"',
75
+ ' AGENT_GROUP="$AGENT_USER"',
76
+ 'else',
77
+ ' groupadd -g "$AGENT_GID" "$AGENT_USER"',
78
+ ' AGENT_GROUP="$AGENT_USER"',
79
+ 'fi',
80
+ 'if id -u "$AGENT_USER" >/dev/null 2>&1; then',
81
+ ' usermod -u "$AGENT_UID" -g "$AGENT_GROUP" -d "$AGENT_HOME" -s /bin/bash "$AGENT_USER" || true',
82
+ 'else',
83
+ ' useradd -m -d "$AGENT_HOME" -u "$AGENT_UID" -g "$AGENT_GROUP" -s /bin/bash "$AGENT_USER"',
84
+ 'fi',
85
+ 'mkdir -p "$AGENT_HOME" "$(dirname "$CODEX_AUTH_PATH")"',
86
+ 'chown -R "$AGENT_UID:$AGENT_GID" "$AGENT_HOME" "$(dirname "$CODEX_AUTH_PATH")" || true',
87
+ 'export HOME="$AGENT_HOME"',
88
+ 'exec sudo -n -E -H -u "$AGENT_USER" bash -lc "$CODEX_LOGIN_COMMAND"',
89
+ ].join("\n");
90
+ }
43
91
  function isErrnoException(error) {
44
92
  return error instanceof Error && "code" in error;
45
93
  }
46
94
  function buildDockerMissingError() {
47
95
  return new Error("Docker is not installed or not available on PATH. Install Docker and retry.");
48
96
  }
97
+ function readSpawnSyncText(output) {
98
+ if (typeof output === "string") {
99
+ return output.trim();
100
+ }
101
+ if (Buffer.isBuffer(output)) {
102
+ return output.toString("utf8").trim();
103
+ }
104
+ return "";
105
+ }
106
+ function describeSpawnSyncFailure(command, args, result) {
107
+ if (isErrnoException(result.error)) {
108
+ return `${command} ${args.join(" ")} failed to start: ${result.error.message}`;
109
+ }
110
+ const parts = [`${command} ${args.join(" ")}`];
111
+ if (typeof result.status === "number") {
112
+ parts.push(`exit code ${result.status}`);
113
+ }
114
+ if (result.signal) {
115
+ parts.push(`signal ${result.signal}`);
116
+ }
117
+ const stderr = readSpawnSyncText(result.stderr);
118
+ if (stderr.length > 0) {
119
+ parts.push(`stderr: ${stderr}`);
120
+ }
121
+ const stdout = readSpawnSyncText(result.stdout);
122
+ if (stdout.length > 0) {
123
+ parts.push(`stdout: ${stdout}`);
124
+ }
125
+ return parts.join(", ");
126
+ }
49
127
  function ensureDockerAvailable(spawnSyncCommand) {
50
128
  const result = spawnSyncCommand("docker", ["--version"], { stdio: "ignore" });
51
129
  if (isErrnoException(result.error) && result.error.code === "ENOENT") {
@@ -64,9 +142,10 @@ async function runContainerizedCodexLogin(cfg, deps, options) {
64
142
  (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
65
143
  }
66
144
  const destPath = (0, node_path_1.join)(configDir, cfg.codex.codex_auth_file_path);
67
- const containerAuthPath = cfg.codex.codex_auth_path;
145
+ const containerAuthPath = resolveContainerPath(cfg.codex.codex_auth_path, cfg.agent_home_directory);
68
146
  let authCopied = false;
69
147
  let combinedOutput = "";
148
+ let lastCopyFailure = null;
70
149
  await new Promise((resolve, reject) => {
71
150
  let settled = false;
72
151
  let poll;
@@ -77,19 +156,17 @@ async function runContainerizedCodexLogin(cfg, deps, options) {
77
156
  deps.spawnSyncCommand("docker", ["rm", "-f", options.containerName], { stdio: "ignore" });
78
157
  };
79
158
  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",
159
+ const cpArgs = ["cp", `${options.containerName}:${containerAuthPath}`, destPath];
160
+ const cpResult = deps.spawnSyncCommand("docker", cpArgs, {
161
+ stdio: ["ignore", "pipe", "pipe"],
162
+ encoding: "utf8",
88
163
  });
89
164
  if (cpResult.status !== 0) {
165
+ lastCopyFailure = describeSpawnSyncFailure("docker", cpArgs, cpResult);
90
166
  return false;
91
167
  }
92
168
  authCopied = true;
169
+ lastCopyFailure = null;
93
170
  cleanup();
94
171
  return true;
95
172
  };
@@ -141,7 +218,14 @@ async function runContainerizedCodexLogin(cfg, deps, options) {
141
218
  resolveOnce();
142
219
  return;
143
220
  }
144
- rejectOnce(new Error(`Codex login failed or was cancelled.${combinedOutput.trim().length > 0 ? ` Output: ${combinedOutput.trim()}` : ""}`));
221
+ const details = [];
222
+ if (lastCopyFailure) {
223
+ details.push(`Auth file copy failed from ${containerAuthPath}: ${lastCopyFailure}`);
224
+ }
225
+ if (combinedOutput.trim().length > 0) {
226
+ details.push(`Output: ${combinedOutput.trim()}`);
227
+ }
228
+ rejectOnce(new Error(`Codex login failed or was cancelled.${details.length > 0 ? ` ${details.join(" ")}` : ""}`));
145
229
  });
146
230
  });
147
231
  return destPath;
@@ -232,6 +316,8 @@ async function runSetCodexHostAuth(cfg, overrides = {}) {
232
316
  }
233
317
  async function runDedicatedCodexAuth(cfg, db, deps) {
234
318
  const containerName = `companyhelm-codex-auth-${Date.now()}`;
319
+ ensureDockerAvailable(deps.spawnSyncCommand);
320
+ const loginCommand = buildCodexLoginShellCommand(cfg, "codex login --device-auth");
235
321
  deps.logInfo("Starting Codex login inside a container...");
236
322
  deps.logInfo("A browser URL and device code will appear -- open it to complete authentication.");
237
323
  const destPath = await runContainerizedCodexLogin(cfg, deps, {
@@ -244,7 +330,7 @@ async function runDedicatedCodexAuth(cfg, db, deps) {
244
330
  "bash",
245
331
  cfg.runtime_image,
246
332
  "-lc",
247
- 'source "$NVM_DIR/nvm.sh"; codex login --device-auth',
333
+ loginCommand,
248
334
  ],
249
335
  });
250
336
  await setCodexDedicatedAuthInDb(db);
@@ -257,6 +343,8 @@ async function runCodexApiKeyAuth(cfg, apiKey, overrides = {}) {
257
343
  try {
258
344
  deps.logInfo("Starting Codex API key login inside a container...");
259
345
  const containerName = `companyhelm-codex-auth-${Date.now()}`;
346
+ ensureDockerAvailable(deps.spawnSyncCommand);
347
+ const loginCommand = buildCodexLoginShellCommand(cfg, 'printf \'%s\\n\' "$CODEX_API_KEY" | codex login --with-api-key');
260
348
  const destPath = await runContainerizedCodexLogin(cfg, deps, {
261
349
  containerName,
262
350
  dockerArgs: [
@@ -269,7 +357,7 @@ async function runCodexApiKeyAuth(cfg, apiKey, overrides = {}) {
269
357
  "bash",
270
358
  cfg.runtime_image,
271
359
  "-lc",
272
- 'source "$NVM_DIR/nvm.sh"; printf \'%s\\n\' "$CODEX_API_KEY" | codex login --with-api-key',
360
+ loginCommand,
273
361
  ],
274
362
  });
275
363
  await setCodexApiKeyAuthInDb(db);
@@ -288,6 +376,8 @@ async function runCodexDeviceCodeAuth(cfg, onDeviceCode, overrides = {}) {
288
376
  let onDeviceCodePromise = Promise.resolve();
289
377
  deps.logInfo("Starting Codex device login inside a container...");
290
378
  const containerName = `companyhelm-codex-auth-${Date.now()}`;
379
+ ensureDockerAvailable(deps.spawnSyncCommand);
380
+ const loginCommand = buildCodexLoginShellCommand(cfg, "codex login --device-auth");
291
381
  const destPath = await runContainerizedCodexLogin(cfg, deps, {
292
382
  containerName,
293
383
  dockerArgs: [
@@ -298,7 +388,7 @@ async function runCodexDeviceCodeAuth(cfg, onDeviceCode, overrides = {}) {
298
388
  "bash",
299
389
  cfg.runtime_image,
300
390
  "-lc",
301
- 'source "$NVM_DIR/nvm.sh"; codex login --device-auth',
391
+ loginCommand,
302
392
  ],
303
393
  onOutput: (output) => {
304
394
  const deviceCode = extractCodexDeviceCodeFromOutput(output);
@@ -5,6 +5,7 @@ exports.registerSdkCodexUseDedicatedAuthCommand = registerSdkCodexUseDedicatedAu
5
5
  const config_js_1 = require("../../../config.js");
6
6
  const auth_js_1 = require("./auth.js");
7
7
  async function runSdkCodexUseDedicatedAuthCommand(cfg = config_js_1.config.parse({}), overrides = {}) {
8
+ cfg = config_js_1.config.parse(cfg);
8
9
  const deps = { ...auth_js_1.defaultUseDedicatedCodexAuthDependencies, ...overrides };
9
10
  await (0, auth_js_1.runUseDedicatedCodexAuth)(cfg, deps);
10
11
  }
@@ -5,6 +5,7 @@ exports.registerSdkCodexUseHostAuthCommand = registerSdkCodexUseHostAuthCommand;
5
5
  const config_js_1 = require("../../../config.js");
6
6
  const auth_js_1 = require("./auth.js");
7
7
  async function runSdkCodexUseHostAuthCommand(cfg = config_js_1.config.parse({}), overrides = {}) {
8
+ cfg = config_js_1.config.parse(cfg);
8
9
  const deps = { ...auth_js_1.defaultSetCodexHostAuthDependencies, ...overrides };
9
10
  const authPath = await (0, auth_js_1.runSetCodexHostAuth)(cfg, deps);
10
11
  console.log(`Codex SDK configured with host authentication using ${authPath}.`);
@@ -96,6 +96,7 @@ async function selectStartupAuthMode(options, deps) {
96
96
  return authMode;
97
97
  }
98
98
  async function startup(cfg = config_js_1.config.parse({}), overrides = {}) {
99
+ cfg = config_js_1.config.parse(cfg);
99
100
  const deps = { ...defaultStartupDependencies, ...overrides };
100
101
  banner();
101
102
  const s = deps.promptApi.spinner();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companyhelm/runner",
3
- "version": "0.0.14",
3
+ "version": "0.0.17",
4
4
  "description": "Run the CompanyHelm runner in fully isolated Docker sandboxes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  "test": "npm run build && vitest run",
25
25
  "test:unit": "npm run build && vitest run tests/unit",
26
26
  "test:integration": "npm run build && vitest run tests/integration",
27
+ "test:system:device-auth": "npm run build && COMPANYHELM_RUN_MANUAL_DEVICE_AUTH_TEST=1 vitest run --reporter=verbose tests/system/codex-device-auth.system.test.ts",
27
28
  "db:generate": "drizzle-kit generate",
28
29
  "db:migrate": "drizzle-kit migrate",
29
30
  "generate:codex-app-server": "docker run --rm --user \"$(id -u):$(id -g)\" -e CODEX_HOME=/workspace/node_modules/.cache/codex -v \"$PWD:/workspace\" -w /workspace companyhelm/runner:$(tr -d '[:space:]' < RUNTIME_IMAGE_VERSION) bash -lc 'mkdir -p /workspace/node_modules/.cache/codex; source \"$NVM_DIR/nvm.sh\"; codex app-server generate-ts --out src/generated/codex-app-server'"