@companyhelm/runner 0.1.1 → 0.1.3

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.
Files changed (37) hide show
  1. package/RUNTIME_IMAGE_VERSION +1 -1
  2. package/dist/commands/doctor.js +44 -0
  3. package/dist/commands/register-commands.js +2 -0
  4. package/dist/commands/root.js +132 -40
  5. package/dist/commands/runner/common.js +3 -1
  6. package/dist/commands/runner/start.js +3 -0
  7. package/dist/config.js +8 -2
  8. package/dist/preflight/check.js +2 -0
  9. package/dist/preflight/checks/linux/apparmor_restrict_unprivileged_userns_check.js +96 -0
  10. package/dist/preflight/entrypoints.js +53 -0
  11. package/dist/preflight/runner_preflight.js +56 -0
  12. package/dist/provisioning/host_provisioning/thread_metadata_store.js +249 -0
  13. package/dist/provisioning/host_provisioning/thread_metadata_types.js +2 -0
  14. package/dist/provisioning/host_provisioning/thread_workspace_provisioner.js +57 -0
  15. package/dist/provisioning/runtime_provisioning/script_renderer.js +130 -0
  16. package/dist/provisioning/runtime_provisioning/system_prompt.js +43 -0
  17. package/dist/provisioning/template_renderer.js +29 -0
  18. package/dist/service/docker/app_server_container.js +16 -1
  19. package/dist/service/sdk/refresh_models.js +8 -0
  20. package/dist/service/thread_lifecycle.js +46 -24
  21. package/dist/service/thread_turn_state.js +1 -0
  22. package/dist/templates/provisioning/runtime_agent_cli_config.sh.j2 +8 -0
  23. package/dist/templates/provisioning/runtime_agent_metadata.sh.j2 +8 -0
  24. package/dist/templates/provisioning/runtime_bashrc.sh.j2 +7 -0
  25. package/dist/templates/provisioning/runtime_codex_config.sh.j2 +7 -0
  26. package/dist/templates/provisioning/runtime_git_config.sh.j2 +28 -0
  27. package/dist/templates/provisioning/runtime_identity.sh.j2 +65 -0
  28. package/dist/templates/provisioning/runtime_thread_git_skills_clone.sh.j2 +11 -0
  29. package/dist/templates/provisioning/runtime_thread_git_skills_link.sh.j2 +7 -0
  30. package/dist/templates/provisioning/runtime_tooling_validation.sh.j2 +32 -0
  31. package/dist/templates/system_prompts/common.md.j2 +46 -0
  32. package/dist/templates/system_prompts/dedicated_workspace.md.j2 +5 -0
  33. package/dist/templates/system_prompts/shared_workspace.md.j2 +5 -0
  34. package/dist/utils/daemon_startup_watchdog.js +27 -0
  35. package/package.json +1 -1
  36. package/dist/service/workspace_agents.js +0 -82
  37. package/dist/templates/runtime_agents.md.j2 +0 -50
@@ -1 +1 @@
1
- 0.0.9
1
+ 0.0.11
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runRunnerDoctorCommand = runRunnerDoctorCommand;
4
+ exports.registerDoctorCommand = registerDoctorCommand;
5
+ const config_js_1 = require("../config.js");
6
+ const entrypoints_js_1 = require("../preflight/entrypoints.js");
7
+ class RunnerDoctorCommand {
8
+ constructor(dependencies = {}) {
9
+ this.stdout = dependencies.stdout ?? process.stdout;
10
+ this.runPreflightFn = dependencies.runPreflightFn ?? entrypoints_js_1.runRunnerPreflight;
11
+ }
12
+ async run(options) {
13
+ const cfg = config_js_1.config.parse({});
14
+ const summary = await this.runPreflightFn({
15
+ cfg,
16
+ applyFixes: options.fix === true,
17
+ });
18
+ this.stdout.write(`${(0, entrypoints_js_1.formatRunnerPreflightSummary)(summary)}\n`);
19
+ return summary;
20
+ }
21
+ }
22
+ async function runRunnerDoctorCommand(options, dependencies) {
23
+ return await new RunnerDoctorCommand(dependencies).run(options);
24
+ }
25
+ function registerDoctorCommand(program) {
26
+ const doctorCommand = program
27
+ .command("doctor")
28
+ .description("Run runner host preflight checks.");
29
+ doctorCommand.action(async () => {
30
+ const summary = await runRunnerDoctorCommand({ fix: false });
31
+ if (!summary.passed) {
32
+ process.exitCode = 1;
33
+ }
34
+ });
35
+ doctorCommand
36
+ .command("fix")
37
+ .description("Attempt to fix supported runner host preflight failures.")
38
+ .action(async () => {
39
+ const summary = await runRunnerDoctorCommand({ fix: true });
40
+ if (!summary.passed) {
41
+ process.exitCode = 1;
42
+ }
43
+ });
44
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCommands = registerCommands;
4
+ const doctor_js_1 = require("./doctor.js");
4
5
  const logs_js_1 = require("./logs.js");
5
6
  const start_js_1 = require("./runner/start.js");
6
7
  const stop_js_1 = require("./runner/stop.js");
@@ -11,6 +12,7 @@ const register_thread_commands_js_1 = require("./thread/register-thread-commands
11
12
  function registerCommands(program) {
12
13
  (0, start_js_1.registerRunnerStartCommand)(program);
13
14
  (0, stop_js_1.registerRunnerStopCommand)(program);
15
+ (0, doctor_js_1.registerDoctorCommand)(program);
14
16
  (0, status_js_1.registerStatusCommand)(program);
15
17
  (0, logs_js_1.registerLogsCommand)(program);
16
18
  (0, register_thread_commands_js_1.registerThreadCommands)(program);
@@ -74,7 +74,11 @@ const daemon_js_1 = require("../utils/daemon.js");
74
74
  const logger_js_1 = require("../utils/logger.js");
75
75
  const path_js_1 = require("../utils/path.js");
76
76
  const terminal_js_1 = require("../utils/terminal.js");
77
- const workspace_agents_js_1 = require("../service/workspace_agents.js");
77
+ const daemon_startup_watchdog_js_1 = require("../utils/daemon_startup_watchdog.js");
78
+ const thread_metadata_store_js_1 = require("../provisioning/host_provisioning/thread_metadata_store.js");
79
+ const thread_workspace_provisioner_js_1 = require("../provisioning/host_provisioning/thread_workspace_provisioner.js");
80
+ const system_prompt_js_1 = require("../provisioning/runtime_provisioning/system_prompt.js");
81
+ const entrypoints_js_1 = require("../preflight/entrypoints.js");
78
82
  const auth_js_1 = require("./sdk/codex/auth.js");
79
83
  const COMMAND_CHANNEL_CONNECT_RETRY_DELAY_MS = 1000;
80
84
  const COMMAND_CHANNEL_OPEN_TIMEOUT_MS = 5000;
@@ -95,7 +99,7 @@ const YOLO_SANDBOX_MODE = "danger-full-access";
95
99
  const YOLO_SANDBOX_POLICY = { type: "dangerFullAccess" };
96
100
  const DOCKER_INTERNAL_HOSTNAME = "host.docker.internal";
97
101
  const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
98
- const DAEMON_STARTUP_TIMEOUT_MS = 15000;
102
+ const DAEMON_STARTUP_TIMEOUT_MS = 60000;
99
103
  class RootCommandInterruptedError extends Error {
100
104
  constructor(message = "Root command interrupted.") {
101
105
  super(message);
@@ -1056,7 +1060,7 @@ function readWorkspaceThreadGitSkillsConfig(workspaceDirectory, logger) {
1056
1060
  }
1057
1061
  }
1058
1062
  async function ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger) {
1059
- const packages = readWorkspaceThreadGitSkillsConfig(threadState.workspace, logger);
1063
+ const packages = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger).readThreadGitSkillsConfig(threadState.id);
1060
1064
  if (packages.length === 0) {
1061
1065
  return;
1062
1066
  }
@@ -1104,8 +1108,11 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1104
1108
  isCurrentTurnRunning: false,
1105
1109
  };
1106
1110
  }
1107
- const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
1108
- const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
1111
+ const metadataStore = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger);
1112
+ const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
1113
+ const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
1114
+ const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
1115
+ const threadAgentCliConfig = buildThreadAgentCliConfig(threadState.cliSecret, cfg.agent_api_url);
1109
1116
  const appServerSession = await getOrCreateThreadAppServerSession(threadState.id, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
1110
1117
  const runtimeUser = buildThreadRuntimeUser(cfg, threadState);
1111
1118
  await (0, thread_runtime_js_1.ensureThreadRuntimeReady)({
@@ -1117,6 +1124,11 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1117
1124
  user: runtimeUser,
1118
1125
  });
1119
1126
  await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
1127
+ await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
1128
+ mcpServers: persistedThreadMcpServers,
1129
+ gitSkillPackages: persistedThreadGitSkillPackages,
1130
+ threadAgentCliConfig,
1131
+ });
1120
1132
  if (threadAgentCliConfig) {
1121
1133
  await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
1122
1134
  }
@@ -1151,14 +1163,22 @@ async function reconcileThreadRunningStateBeforeUserMessage(cfg, threadState, lo
1151
1163
  isCurrentTurnRunning: false,
1152
1164
  };
1153
1165
  }
1154
- async function listTrackedThreadWorkspaces(cfg, logger) {
1166
+ async function listTrackedThreadRuntimeTargets(cfg, logger) {
1155
1167
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1156
1168
  try {
1157
- const rows = await db.select({ workspace: schema_js_1.threads.workspace }).from(schema_js_1.threads);
1158
- return [...new Set(rows.map((row) => row.workspace.trim()).filter((workspace) => workspace.length > 0))];
1169
+ return await db
1170
+ .select({
1171
+ threadId: schema_js_1.threads.id,
1172
+ runtimeContainer: schema_js_1.threads.runtimeContainer,
1173
+ homeDirectory: schema_js_1.threads.homeDirectory,
1174
+ uid: schema_js_1.threads.uid,
1175
+ gid: schema_js_1.threads.gid,
1176
+ })
1177
+ .from(schema_js_1.threads)
1178
+ .all();
1159
1179
  }
1160
1180
  catch (error) {
1161
- logger.warn(`Failed to list tracked thread workspaces for GitHub installation sync: ${toErrorMessage(error)}`);
1181
+ logger.warn(`Failed to list tracked thread runtimes for GitHub installation sync: ${toErrorMessage(error)}`);
1162
1182
  return [];
1163
1183
  }
1164
1184
  finally {
@@ -1179,19 +1199,35 @@ function resolveGithubInstallationsSyncDelayMs(installations) {
1179
1199
  }
1180
1200
  return Math.max(GITHUB_INSTALLATIONS_MIN_SYNC_INTERVAL_MS, Math.min(GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS, syncDelayMs));
1181
1201
  }
1182
- async function syncGithubInstallationsForWorkspaces(apiClient, options, workspaceDirectories, logger) {
1183
- const uniqueWorkspaces = [
1184
- ...new Set(workspaceDirectories.map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0)),
1202
+ async function syncGithubInstallationsForRuntimeTargets(cfg, apiClient, options, runtimeTargets, logger) {
1203
+ const uniqueTargets = [
1204
+ ...new Map(runtimeTargets
1205
+ .filter((target) => target.runtimeContainer.trim().length > 0)
1206
+ .map((target) => [target.runtimeContainer, target])).values(),
1185
1207
  ];
1186
- if (uniqueWorkspaces.length === 0) {
1208
+ if (uniqueTargets.length === 0) {
1187
1209
  return [];
1188
1210
  }
1189
1211
  const installations = await loadRuntimeGithubInstallations(apiClient, options, logger);
1190
1212
  const payload = buildWorkspaceGithubInstallationsPayload(installations);
1191
- for (const workspaceDirectory of uniqueWorkspaces) {
1192
- writeWorkspaceGithubInstallationsPayload(workspaceDirectory, payload, logger);
1213
+ const containerService = new thread_lifecycle_js_1.ThreadContainerService();
1214
+ for (const target of uniqueTargets) {
1215
+ try {
1216
+ if (!await containerService.isContainerRunning(target.runtimeContainer)) {
1217
+ continue;
1218
+ }
1219
+ await containerService.ensureRuntimeContainerGithubInstallations(target.runtimeContainer, {
1220
+ uid: target.uid,
1221
+ gid: target.gid,
1222
+ agentUser: cfg.agent_user,
1223
+ agentHomeDirectory: target.homeDirectory,
1224
+ }, payload);
1225
+ }
1226
+ catch (error) {
1227
+ logger.warn(`Failed syncing GitHub installations into runtime container '${target.runtimeContainer}': ${toErrorMessage(error)}`);
1228
+ }
1193
1229
  }
1194
- logger.debug(`Synced ${installations.length} GitHub installation token(s) to ${uniqueWorkspaces.length} workspace(s).`);
1230
+ logger.debug(`Synced ${installations.length} GitHub installation token(s) to ${uniqueTargets.length} runtime container(s).`);
1195
1231
  return installations;
1196
1232
  }
1197
1233
  async function waitForAbort(signal, delayMs) {
@@ -1215,8 +1251,8 @@ async function runGithubInstallationsSyncLoop(cfg, apiClient, options, logger, s
1215
1251
  while (!signal.aborted) {
1216
1252
  let nextDelayMs = GITHUB_INSTALLATIONS_SYNC_INTERVAL_MS;
1217
1253
  try {
1218
- const workspaces = await listTrackedThreadWorkspaces(cfg, logger);
1219
- const installations = await syncGithubInstallationsForWorkspaces(apiClient, options, workspaces, logger);
1254
+ const runtimeTargets = await listTrackedThreadRuntimeTargets(cfg, logger);
1255
+ const installations = await syncGithubInstallationsForRuntimeTargets(cfg, apiClient, options, runtimeTargets, logger);
1220
1256
  nextDelayMs = resolveGithubInstallationsSyncDelayMs(installations);
1221
1257
  }
1222
1258
  catch (error) {
@@ -1243,8 +1279,23 @@ function normalizeAdditionalModelInstructions(value) {
1243
1279
  const trimmed = value.trim();
1244
1280
  return trimmed.length > 0 ? trimmed : null;
1245
1281
  }
1246
- function buildThreadDeveloperInstructions(additionalModelInstructions) {
1247
- return normalizeAdditionalModelInstructions(additionalModelInstructions);
1282
+ function buildThreadAgentCliConfig(cliSecret, agentApiUrl) {
1283
+ const normalizedSecret = normalizeNonEmptyString(cliSecret);
1284
+ if (!normalizedSecret) {
1285
+ return null;
1286
+ }
1287
+ return {
1288
+ agent_api_url: normalizeThreadAgentApiUrlForRuntime(agentApiUrl),
1289
+ token: normalizedSecret,
1290
+ };
1291
+ }
1292
+ function buildThreadDeveloperInstructions(cfg, additionalModelInstructions, cliSecret) {
1293
+ return (0, system_prompt_js_1.buildCodexDeveloperInstructions)(additionalModelInstructions, {
1294
+ homeDirectory: cfg.agent_home_directory,
1295
+ agentApiUrl: normalizeThreadAgentApiUrlForRuntime(cfg.agent_api_url),
1296
+ agentToken: normalizeNonEmptyString(cliSecret) ?? "<thread-secret>",
1297
+ workspaceMode: cfg.use_dedicated_workspaces ? "dedicated" : "shared",
1298
+ });
1248
1299
  }
1249
1300
  function buildUserTextInput(text) {
1250
1301
  return [
@@ -1598,16 +1649,22 @@ async function loadCodexSdkState(cfg) {
1598
1649
  client.close();
1599
1650
  }
1600
1651
  }
1601
- async function refreshCodexModelsForRegistration(cfg, logger) {
1652
+ async function refreshCodexModelsForRegistration(cfg, logger, reportProgress) {
1602
1653
  const codexSdk = await loadCodexSdkState(cfg);
1603
1654
  if (!codexSdk || codexSdk.status !== "configured" || codexSdk.authentication === "unauthenticated") {
1604
1655
  logger.info("Codex is not configured; registering runner with unconfigured Codex SDK state.");
1605
1656
  return null;
1606
1657
  }
1607
1658
  try {
1608
- const results = await (0, refresh_models_js_1.refreshSdkModels)({ sdk: "codex", logger });
1659
+ reportProgress?.("Refreshing Codex models from the local app-server.");
1660
+ const results = await (0, refresh_models_js_1.refreshSdkModels)({
1661
+ sdk: "codex",
1662
+ logger,
1663
+ imageStatusReporter: reportProgress,
1664
+ });
1609
1665
  const modelCount = results[0]?.modelCount ?? 0;
1610
1666
  logger.info(`Refreshed Codex models from container app-server (${modelCount} models).`);
1667
+ reportProgress?.(`Refreshed Codex models from container app-server (${modelCount} models).`);
1611
1668
  return null;
1612
1669
  }
1613
1670
  catch (error) {
@@ -1679,7 +1736,9 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1679
1736
  return;
1680
1737
  }
1681
1738
  const { db, client } = await (0, db_js_1.initDb)(cfg.state_db_path);
1682
- const threadDirectory = (0, thread_lifecycle_js_1.resolveThreadDirectory)(cfg.config_directory, cfg.workspaces_directory, threadId);
1739
+ const workspaceProvisioner = new thread_workspace_provisioner_js_1.ThreadWorkspaceProvisioner(cfg.config_directory, cfg.workspaces_directory, cfg.workspace_path, cfg.use_dedicated_workspaces);
1740
+ const metadataStore = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger);
1741
+ const threadDirectory = workspaceProvisioner.resolveWorkspaceDirectory(threadId);
1683
1742
  const containerNames = (0, thread_lifecycle_js_1.buildThreadContainerNames)(threadId);
1684
1743
  const hostInfo = (0, host_js_1.getHostInfo)(cfg.codex.codex_auth_path);
1685
1744
  const normalizedAdditionalModelInstructions = normalizeAdditionalModelInstructions(request.additionalModelInstructions);
@@ -1741,12 +1800,9 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1741
1800
  finally {
1742
1801
  client.close();
1743
1802
  }
1744
- (0, node_fs_1.mkdirSync)(threadDirectory, { recursive: true });
1745
- (0, workspace_agents_js_1.ensureWorkspaceAgentsMd)(threadDirectory, cfg.agent_home_directory);
1746
- writeWorkspaceThreadGitSkillsConfig(threadDirectory, threadGitSkillPackages, logger);
1747
- writeWorkspaceThreadMcpConfig(threadDirectory, threadMcpServers, logger);
1748
- writeWorkspaceThreadAgentCliConfig(threadDirectory, cliSecret, cfg.agent_api_url, logger);
1749
- await syncGithubInstallationsForWorkspaces(apiClient, apiCallOptions, [threadDirectory], logger);
1803
+ workspaceProvisioner.ensureWorkspaceDirectory(threadId);
1804
+ metadataStore.writeThreadGitSkillsConfig(threadId, threadGitSkillPackages);
1805
+ metadataStore.writeThreadMcpConfig(threadId, threadMcpServers);
1750
1806
  logger.debug(`Thread '${threadId}' workspace initialized at '${threadDirectory}'.`);
1751
1807
  const containerService = new thread_lifecycle_js_1.ThreadContainerService();
1752
1808
  const mounts = (0, thread_lifecycle_js_1.buildSharedThreadMounts)({
@@ -1796,8 +1852,10 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1796
1852
  if (!threadState) {
1797
1853
  throw new Error(`Thread '${threadId}' disappeared before SDK bootstrap.`);
1798
1854
  }
1799
- const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
1800
- const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
1855
+ const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
1856
+ const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
1857
+ const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
1858
+ const threadAgentCliConfig = buildThreadAgentCliConfig(threadState.cliSecret, cfg.agent_api_url);
1801
1859
  const appServerSession = await getOrCreateThreadAppServerSession(threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
1802
1860
  const runtimeUser = {
1803
1861
  uid: threadState.uid,
@@ -1814,14 +1872,28 @@ async function handleCreateThreadRequest(cfg, commandChannel, request, requestId
1814
1872
  user: runtimeUser,
1815
1873
  });
1816
1874
  await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
1875
+ await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
1876
+ mcpServers: persistedThreadMcpServers,
1877
+ gitSkillPackages: persistedThreadGitSkillPackages,
1878
+ threadAgentCliConfig,
1879
+ });
1817
1880
  if (threadAgentCliConfig) {
1818
1881
  await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
1819
1882
  }
1883
+ await syncGithubInstallationsForRuntimeTargets(cfg, apiClient, apiCallOptions, [
1884
+ {
1885
+ threadId: threadState.id,
1886
+ runtimeContainer: threadState.runtimeContainer,
1887
+ homeDirectory: threadState.homeDirectory,
1888
+ uid: threadState.uid,
1889
+ gid: threadState.gid,
1890
+ },
1891
+ ], logger);
1820
1892
  if (!appServerSession.started) {
1821
1893
  await containerService.ensureRuntimeContainerCodexConfig(threadState.runtimeContainer, runtimeUser, threadMcpSetup.configToml);
1822
1894
  }
1823
1895
  await ensureThreadAppServerSessionStarted(appServerSession);
1824
- const developerInstructions = buildThreadDeveloperInstructions(threadState.additionalModelInstructions);
1896
+ const developerInstructions = buildThreadDeveloperInstructions(cfg, threadState.additionalModelInstructions, threadState.cliSecret);
1825
1897
  logger.debug(`Starting app-server thread '${threadId}' with developer instructions: ${JSON.stringify(developerInstructions)}.`);
1826
1898
  const threadStartResponse = await appServerSession.appServer.startThreadWithResponse({
1827
1899
  model: threadState.model,
@@ -1904,6 +1976,7 @@ async function deleteThreadWithCleanup(cfg, request) {
1904
1976
  const containerService = new thread_lifecycle_js_1.ThreadContainerService();
1905
1977
  try {
1906
1978
  const containerNames = (0, thread_lifecycle_js_1.buildThreadContainerNames)(existingThread.id);
1979
+ const workspaceProvisioner = new thread_workspace_provisioner_js_1.ThreadWorkspaceProvisioner(cfg.config_directory, cfg.workspaces_directory, cfg.workspace_path, cfg.use_dedicated_workspaces);
1907
1980
  await stopThreadAppServerSession(request.threadId);
1908
1981
  threadRolloutPaths.delete(request.threadId);
1909
1982
  await containerService.forceRemoveContainer(existingThread.runtimeContainer);
@@ -1912,7 +1985,8 @@ async function deleteThreadWithCleanup(cfg, request) {
1912
1985
  }
1913
1986
  await containerService.forceRemoveVolume(containerNames.home);
1914
1987
  await containerService.forceRemoveVolume(containerNames.tmp);
1915
- removeWorkspaceDirectory(existingThread.workspace);
1988
+ workspaceProvisioner.removeWorkspaceDirectory(existingThread.id, existingThread.workspace);
1989
+ new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, (0, logger_js_1.createLogger)("ERROR")).removeThreadMetadata(existingThread.id);
1916
1990
  }
1917
1991
  catch (error) {
1918
1992
  return {
@@ -2071,8 +2145,11 @@ async function waitForThreadTurnCompletion(stateDbPath, appServer, commandChanne
2071
2145
  }
2072
2146
  async function executeCreateUserMessageRequest(cfg, commandChannel, request, requestId, threadState, startedFromIdle, trackTurnCompletion, logger) {
2073
2147
  const containerService = new thread_lifecycle_js_1.ThreadContainerService();
2074
- const threadMcpSetup = buildThreadCodexMcpSetup(readWorkspaceThreadMcpConfig(threadState.workspace, logger));
2075
- const threadAgentCliConfig = readWorkspaceThreadAgentCliConfig(threadState.workspace, logger);
2148
+ const metadataStore = new thread_metadata_store_js_1.ThreadMetadataStore(cfg.config_directory, logger);
2149
+ const persistedThreadMcpServers = metadataStore.readThreadMcpConfig(threadState.id);
2150
+ const persistedThreadGitSkillPackages = metadataStore.readThreadGitSkillsConfig(threadState.id);
2151
+ const threadMcpSetup = buildThreadCodexMcpSetup(persistedThreadMcpServers);
2152
+ const threadAgentCliConfig = buildThreadAgentCliConfig(threadState.cliSecret, cfg.agent_api_url);
2076
2153
  const appServerSession = await getOrCreateThreadAppServerSession(request.threadId, threadState.runtimeContainer, threadMcpSetup.appServerEnv, cfg.codex.app_server_client_name, logger);
2077
2154
  const appServer = appServerSession.appServer;
2078
2155
  const runtimeUser = buildThreadRuntimeUser(cfg, threadState);
@@ -2093,6 +2170,11 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
2093
2170
  user: runtimeUser,
2094
2171
  });
2095
2172
  await ensureThreadGitSkillsInRuntime(cfg, threadState, containerService, logger);
2173
+ await containerService.ensureRuntimeContainerThreadMetadata(threadState.runtimeContainer, runtimeUser, {
2174
+ mcpServers: persistedThreadMcpServers,
2175
+ gitSkillPackages: persistedThreadGitSkillPackages,
2176
+ threadAgentCliConfig,
2177
+ });
2096
2178
  if (threadAgentCliConfig) {
2097
2179
  await containerService.ensureRuntimeContainerAgentCliConfig(threadState.runtimeContainer, runtimeUser, threadAgentCliConfig);
2098
2180
  }
@@ -2119,7 +2201,7 @@ async function executeCreateUserMessageRequest(cfg, commandChannel, request, req
2119
2201
  await updateThreadTurnState(cfg, request.threadId, { sdkThreadId });
2120
2202
  }
2121
2203
  else {
2122
- const developerInstructions = buildThreadDeveloperInstructions(threadState.additionalModelInstructions);
2204
+ const developerInstructions = buildThreadDeveloperInstructions(cfg, threadState.additionalModelInstructions, threadState.cliSecret);
2123
2205
  const threadStartParams = {
2124
2206
  model: request.model ?? threadState.model,
2125
2207
  modelProvider: null,
@@ -2498,20 +2580,20 @@ async function runDetachedDaemonProcess(options) {
2498
2580
  windowsHide: true,
2499
2581
  });
2500
2582
  let settled = false;
2501
- const timeout = setTimeout(() => {
2583
+ const startupWatchdog = new daemon_startup_watchdog_js_1.DaemonStartupWatchdog(DAEMON_STARTUP_TIMEOUT_MS, () => {
2502
2584
  if (settled) {
2503
2585
  return;
2504
2586
  }
2505
2587
  settled = true;
2506
2588
  child.kill();
2507
2589
  reject(new Error(`Timed out waiting for daemon startup confirmation. See ${logPath}.`));
2508
- }, DAEMON_STARTUP_TIMEOUT_MS);
2590
+ });
2509
2591
  const finish = (callback) => {
2510
2592
  if (settled) {
2511
2593
  return;
2512
2594
  }
2513
2595
  settled = true;
2514
- clearTimeout(timeout);
2596
+ startupWatchdog.finish();
2515
2597
  callback();
2516
2598
  };
2517
2599
  child.once("error", (error) => {
@@ -2527,6 +2609,10 @@ async function runDetachedDaemonProcess(options) {
2527
2609
  return;
2528
2610
  }
2529
2611
  const type = message.type;
2612
+ if (type === "daemon-progress") {
2613
+ startupWatchdog.bump();
2614
+ return;
2615
+ }
2530
2616
  if (type === "daemon-ready") {
2531
2617
  finish(() => {
2532
2618
  if (child.connected) {
@@ -2558,11 +2644,12 @@ function sendDaemonParentMessage(message) {
2558
2644
  async function runRootCommand(options, runtimeOptions) {
2559
2645
  const logger = (0, logger_js_1.createLogger)(options.logLevel ?? "INFO", { daemonMode: options.daemon ?? false });
2560
2646
  const cfg = buildRootConfig(options);
2647
+ await (0, entrypoints_js_1.ensureRunnerStartupPreflight)(cfg);
2561
2648
  await (0, auth_js_1.ensureCodexRunnerStartState)(cfg, {
2562
2649
  useDedicatedAuth: options.useDedicatedAuth,
2563
2650
  logInfo: (message) => logger.info(message),
2564
2651
  });
2565
- const codexRefreshErrorMessage = await refreshCodexModelsForRegistration(cfg, logger);
2652
+ const codexRefreshErrorMessage = await refreshCodexModelsForRegistration(cfg, logger, runtimeOptions?.onDaemonProgress);
2566
2653
  const registerRequest = await buildRegisterRunnerRequest(cfg, logger, codexRefreshErrorMessage);
2567
2654
  const apiCallOptions = buildGrpcAuthCallOptions(options.secret);
2568
2655
  if (options.daemon) {
@@ -2666,8 +2753,13 @@ async function runRootCommand(options, runtimeOptions) {
2666
2753
  }
2667
2754
  }
2668
2755
  function buildRootConfig(options) {
2756
+ if (options.useDedicatedWorkspaces && typeof options.workspacePath === "string" && options.workspacePath.trim().length > 0) {
2757
+ throw new Error("--workspace-path and --use-dedicated-workspaces cannot be used together.");
2758
+ }
2669
2759
  return config_js_1.config.parse({
2670
2760
  config_directory: options.configPath,
2761
+ workspace_path: options.workspacePath,
2762
+ use_dedicated_workspaces: options.useDedicatedWorkspaces,
2671
2763
  state_db_path: options.stateDbPath,
2672
2764
  companyhelm_api_url: options.serverUrl,
2673
2765
  agent_api_url: options.agentApiUrl,
@@ -6,12 +6,14 @@ function addRunnerStartOptions(command) {
6
6
  return command
7
7
  .option("--config-path <path>", global_options_js_1.CONFIG_PATH_OPTION_DESCRIPTION)
8
8
  .option("--server-url <url>", "CompanyHelm gRPC API URL override.")
9
- .option("--agent-api-url <url>", "Agent gRPC API URL for companyhelm-agent in runtime containers (localhost is rewritten to http://host.docker.internal).")
9
+ .option("--agent-api-url <url>", "Agent REST API base URL for runtime containers (localhost is rewritten to http://host.docker.internal).")
10
+ .option("--workspace-path <path>", "Shared host workspace mounted at /workspace (defaults to the current working directory).")
10
11
  .option("--secret <secret>", "Bearer secret used as gRPC Authorization header.")
11
12
  .option("--state-db-path <path>", "State database path override (defaults to state.db under the active config directory).")
12
13
  .option("--log-path <path>", "Daemon log file override.")
13
14
  .option("--use-host-docker-runtime", "Mount host Docker socket into runtime containers instead of creating DinD sidecars.")
14
15
  .option("--use-dedicated-auth", "Preserve existing dedicated Codex auth if already configured; otherwise keep Codex unconfigured on startup.")
16
+ .option("--use-dedicated-workspaces", "Create per-thread dedicated workspaces under the configured workspaces directory.")
15
17
  .option("--host-docker-path <path>", "Host Docker endpoint when --use-host-docker-runtime is enabled (unix:///<socket-path> or tcp://localhost:<port>).")
16
18
  .option("--thread-git-skills-directory <path>", "Container path where thread git skill repositories are cloned before linking into ~/.codex/skills.")
17
19
  .option("-d, --daemon", "Run in daemon mode and fail fast when no SDK is configured.")
@@ -15,6 +15,9 @@ async function runRunnerStartCommand(options) {
15
15
  onDaemonReady: () => {
16
16
  (0, root_js_1.sendDaemonParentMessage)({ type: "daemon-ready" });
17
17
  },
18
+ onDaemonProgress: (message) => {
19
+ (0, root_js_1.sendDaemonParentMessage)({ type: "daemon-progress", message });
20
+ },
18
21
  }
19
22
  : undefined);
20
23
  }
package/dist/config.js CHANGED
@@ -53,6 +53,12 @@ exports.config = zod_1.z.object({
53
53
  config_directory: zod_1.z.string()
54
54
  .describe("The directory where the config files are stored.")
55
55
  .default(resolveConfigDirectoryDefault),
56
+ workspace_path: zod_1.z.string()
57
+ .describe("Shared workspace directory mounted at /workspace when dedicated workspaces are disabled.")
58
+ .default(() => process.cwd()),
59
+ use_dedicated_workspaces: zod_1.z.boolean()
60
+ .describe("When true, create per-thread dedicated workspaces under workspaces_directory.")
61
+ .default(false),
56
62
  workspaces_directory: zod_1.z.string()
57
63
  .describe("The directory where thread workspaces are stored, relative to config_directory when not absolute.")
58
64
  .default("workspaces"),
@@ -63,8 +69,8 @@ exports.config = zod_1.z.object({
63
69
  .describe("CompanyHelm control plane gRPC endpoint URL.")
64
70
  .default("https://api.companyhelm.com:50051"),
65
71
  agent_api_url: zod_1.z.string()
66
- .describe("CompanyHelm AgentTaskService gRPC endpoint URL used by companyhelm-agent inside runtime threads.")
67
- .default("https://api.companyhelm.com:50052"),
72
+ .describe("CompanyHelm agent REST API base URL used inside runtime threads.")
73
+ .default("https://api.companyhelm.com/agent/v1"),
68
74
  // Max outbound gRPC client messages to hold while the command channel is disconnected.
69
75
  client_message_buffer_limit: zod_1.z.number()
70
76
  .int()
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LinuxApparmorRestrictUnprivilegedUsernsCheck = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_child_process_1 = require("node:child_process");
6
+ const APPARMOR_SYSCTL_KEY = "kernel.apparmor_restrict_unprivileged_userns";
7
+ const UNPRIVILEGED_USERNS_SYSCTL_KEY = "kernel.unprivileged_userns_clone";
8
+ const USER_NAMESPACE_LIMIT_SYSCTL_KEY = "user.max_user_namespaces";
9
+ function isRootlessDindImage(image) {
10
+ return image.toLowerCase().includes("rootless");
11
+ }
12
+ async function defaultReadSysctlValue(key) {
13
+ const path = `/proc/sys/${key.replace(/\./g, "/")}`;
14
+ try {
15
+ return (await (0, promises_1.readFile)(path, "utf8")).trim();
16
+ }
17
+ catch (error) {
18
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
19
+ return null;
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+ async function defaultRunShellCommand(command) {
25
+ await new Promise((resolve, reject) => {
26
+ const child = (0, node_child_process_1.spawn)("bash", ["-lc", command], { stdio: "inherit" });
27
+ child.on("error", reject);
28
+ child.on("exit", (code, signal) => {
29
+ if (code === 0) {
30
+ resolve();
31
+ return;
32
+ }
33
+ reject(new Error(`Command failed (${signal ?? code ?? "unknown"}): ${command}`));
34
+ });
35
+ });
36
+ }
37
+ class LinuxApparmorRestrictUnprivilegedUsernsCheck {
38
+ constructor(cfg, dependencies = {}) {
39
+ this.cfg = cfg;
40
+ this.id = "linux.apparmor_restrict_unprivileged_userns";
41
+ this.description = "Verify Linux AppArmor permits unprivileged user namespaces for rootless DinD.";
42
+ this.platform = dependencies.platform ?? process.platform;
43
+ this.readSysctlValue = dependencies.readSysctlValue ?? defaultReadSysctlValue;
44
+ this.runShellCommand = dependencies.runShellCommand ?? defaultRunShellCommand;
45
+ }
46
+ async run() {
47
+ if (!this.isApplicable()) {
48
+ return {
49
+ status: "skipped",
50
+ summary: "Check only applies to Linux rootless DinD setups.",
51
+ fixAvailable: false,
52
+ };
53
+ }
54
+ const apparmorRestriction = await this.readSysctlValue(APPARMOR_SYSCTL_KEY);
55
+ if (apparmorRestriction === "1") {
56
+ const [userNamespaceClone, userNamespaceLimit] = await Promise.all([
57
+ this.readSysctlValue(UNPRIVILEGED_USERNS_SYSCTL_KEY),
58
+ this.readSysctlValue(USER_NAMESPACE_LIMIT_SYSCTL_KEY),
59
+ ]);
60
+ return {
61
+ status: "failed",
62
+ summary: `${APPARMOR_SYSCTL_KEY}=1 blocks rootless DinD on this Linux host ` +
63
+ `(kernel.unprivileged_userns_clone=${userNamespaceClone ?? "unknown"}, ` +
64
+ `user.max_user_namespaces=${userNamespaceLimit ?? "unknown"}).`,
65
+ fixAvailable: true,
66
+ };
67
+ }
68
+ return {
69
+ status: "passed",
70
+ summary: "Linux host is compatible with rootless DinD.",
71
+ fixAvailable: false,
72
+ };
73
+ }
74
+ async fix() {
75
+ if (!this.isApplicable()) {
76
+ return {
77
+ status: "skipped",
78
+ summary: "Check only applies to Linux rootless DinD setups.",
79
+ };
80
+ }
81
+ await this.runShellCommand("sudo tee /etc/sysctl.d/99-companyhelm-rootless.conf >/dev/null <<'EOF'\n" +
82
+ "kernel.unprivileged_userns_clone = 1\n" +
83
+ "user.max_user_namespaces = 28633\n" +
84
+ "kernel.apparmor_restrict_unprivileged_userns = 0\n" +
85
+ "EOF");
86
+ await this.runShellCommand("sudo sysctl --system");
87
+ return {
88
+ status: "fixed",
89
+ summary: "Updated Linux sysctl configuration for rootless DinD.",
90
+ };
91
+ }
92
+ isApplicable() {
93
+ return this.platform === "linux" && !this.cfg.use_host_docker_runtime && isRootlessDindImage(this.cfg.dind_image);
94
+ }
95
+ }
96
+ exports.LinuxApparmorRestrictUnprivilegedUsernsCheck = LinuxApparmorRestrictUnprivilegedUsernsCheck;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUNNER_STARTUP_PREFLIGHT_SKIP_ENV = void 0;
4
+ exports.createRunnerPreflight = createRunnerPreflight;
5
+ exports.runRunnerPreflight = runRunnerPreflight;
6
+ exports.formatRunnerPreflightSummary = formatRunnerPreflightSummary;
7
+ exports.ensureRunnerStartupPreflight = ensureRunnerStartupPreflight;
8
+ const apparmor_restrict_unprivileged_userns_check_js_1 = require("./checks/linux/apparmor_restrict_unprivileged_userns_check.js");
9
+ const runner_preflight_js_1 = require("./runner_preflight.js");
10
+ exports.RUNNER_STARTUP_PREFLIGHT_SKIP_ENV = "COMPANYHELM_SKIP_RUNNER_STARTUP_PREFLIGHT";
11
+ function renderPreflightStatusLabel(status) {
12
+ if (status === "passed") {
13
+ return "PASS";
14
+ }
15
+ if (status === "failed") {
16
+ return "FAIL";
17
+ }
18
+ return "SKIP";
19
+ }
20
+ function createRunnerPreflight(cfg, overrides = {}) {
21
+ return new runner_preflight_js_1.RunnerPreflight([
22
+ new apparmor_restrict_unprivileged_userns_check_js_1.LinuxApparmorRestrictUnprivilegedUsernsCheck(cfg, overrides),
23
+ ]);
24
+ }
25
+ async function runRunnerPreflight(options, overrides = {}) {
26
+ return await createRunnerPreflight(options.cfg, overrides).run({ applyFixes: options.applyFixes });
27
+ }
28
+ function formatRunnerPreflightSummary(summary) {
29
+ const lines = [`Preflight status: ${summary.passed ? "passed" : "failed"}`];
30
+ if (summary.results.length === 0) {
31
+ lines.push("No applicable preflight checks.");
32
+ return lines.join("\n");
33
+ }
34
+ for (const result of summary.results) {
35
+ lines.push(`[${renderPreflightStatusLabel(result.status)}] ${result.id}: ${result.summary}`);
36
+ }
37
+ return lines.join("\n");
38
+ }
39
+ function shouldSkipRunnerStartupPreflight() {
40
+ const value = process.env[exports.RUNNER_STARTUP_PREFLIGHT_SKIP_ENV]?.trim().toLowerCase();
41
+ return value === "1" || value === "true";
42
+ }
43
+ async function ensureRunnerStartupPreflight(cfg, overrides = {}) {
44
+ if (shouldSkipRunnerStartupPreflight()) {
45
+ return;
46
+ }
47
+ const summary = await runRunnerPreflight({ cfg }, overrides);
48
+ if (summary.passed) {
49
+ return;
50
+ }
51
+ throw new Error(`${formatRunnerPreflightSummary(summary)}\n` +
52
+ "Run `companyhelm-runner doctor` for details or `companyhelm-runner doctor fix` to try automatic fixes.");
53
+ }