@companyhelm/runner 0.1.0 → 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.
- package/RUNTIME_IMAGE_VERSION +1 -1
- package/dist/commands/doctor.js +44 -0
- package/dist/commands/register-commands.js +2 -5
- package/dist/commands/root.js +132 -40
- package/dist/commands/runner/common.js +3 -1
- package/dist/commands/runner/start.js +3 -0
- package/dist/config.js +8 -2
- package/dist/preflight/check.js +2 -0
- package/dist/preflight/checks/linux/apparmor_restrict_unprivileged_userns_check.js +96 -0
- package/dist/preflight/entrypoints.js +53 -0
- package/dist/preflight/runner_preflight.js +56 -0
- package/dist/provisioning/host_provisioning/thread_metadata_store.js +249 -0
- package/dist/provisioning/host_provisioning/thread_metadata_types.js +2 -0
- package/dist/provisioning/host_provisioning/thread_workspace_provisioner.js +57 -0
- package/dist/provisioning/runtime_provisioning/script_renderer.js +130 -0
- package/dist/provisioning/runtime_provisioning/system_prompt.js +43 -0
- package/dist/provisioning/template_renderer.js +29 -0
- package/dist/service/docker/app_server_container.js +16 -1
- package/dist/service/sdk/refresh_models.js +8 -0
- package/dist/service/thread_lifecycle.js +46 -24
- package/dist/service/thread_turn_state.js +1 -0
- package/dist/templates/provisioning/runtime_agent_cli_config.sh.j2 +8 -0
- package/dist/templates/provisioning/runtime_agent_metadata.sh.j2 +8 -0
- package/dist/templates/provisioning/runtime_bashrc.sh.j2 +7 -0
- package/dist/templates/provisioning/runtime_codex_config.sh.j2 +7 -0
- package/dist/templates/provisioning/runtime_git_config.sh.j2 +28 -0
- package/dist/templates/provisioning/runtime_identity.sh.j2 +65 -0
- package/dist/templates/provisioning/runtime_thread_git_skills_clone.sh.j2 +11 -0
- package/dist/templates/provisioning/runtime_thread_git_skills_link.sh.j2 +7 -0
- package/dist/templates/provisioning/runtime_tooling_validation.sh.j2 +32 -0
- package/dist/templates/system_prompts/common.md.j2 +46 -0
- package/dist/templates/system_prompts/dedicated_workspace.md.j2 +5 -0
- package/dist/templates/system_prompts/shared_workspace.md.j2 +5 -0
- package/dist/utils/daemon_startup_watchdog.js +27 -0
- package/package.json +1 -1
- package/dist/service/workspace_agents.js +0 -82
- package/dist/templates/runtime_agents.md.j2 +0 -50
package/RUNTIME_IMAGE_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
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,21 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerCommands = registerCommands;
|
|
4
|
-
const
|
|
4
|
+
const doctor_js_1 = require("./doctor.js");
|
|
5
5
|
const logs_js_1 = require("./logs.js");
|
|
6
6
|
const start_js_1 = require("./runner/start.js");
|
|
7
7
|
const stop_js_1 = require("./runner/stop.js");
|
|
8
|
-
const start_js_2 = require("./runner/start.js");
|
|
9
8
|
const shell_js_1 = require("./shell.js");
|
|
10
9
|
const register_sdk_commands_js_1 = require("./sdk/register-sdk-commands.js");
|
|
11
10
|
const status_js_1 = require("./status.js");
|
|
12
11
|
const register_thread_commands_js_1 = require("./thread/register-thread-commands.js");
|
|
13
12
|
function registerCommands(program) {
|
|
14
|
-
(0, common_js_1.addRunnerStartOptions)(program
|
|
15
|
-
.command("companyhelm-runner")
|
|
16
|
-
.description("Alias for starting the local CompanyHelm runner.")).action(start_js_2.runRunnerStartCommand);
|
|
17
13
|
(0, start_js_1.registerRunnerStartCommand)(program);
|
|
18
14
|
(0, stop_js_1.registerRunnerStopCommand)(program);
|
|
15
|
+
(0, doctor_js_1.registerDoctorCommand)(program);
|
|
19
16
|
(0, status_js_1.registerStatusCommand)(program);
|
|
20
17
|
(0, logs_js_1.registerLogsCommand)(program);
|
|
21
18
|
(0, register_thread_commands_js_1.registerThreadCommands)(program);
|
package/dist/commands/root.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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
|
|
1108
|
-
const
|
|
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
|
|
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
|
-
|
|
1158
|
-
|
|
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
|
|
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
|
|
1183
|
-
const
|
|
1184
|
-
...new
|
|
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 (
|
|
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
|
-
|
|
1192
|
-
|
|
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 ${
|
|
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
|
|
1219
|
-
const installations = await
|
|
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
|
|
1247
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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
|
|
1800
|
-
const
|
|
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
|
|
2075
|
-
const
|
|
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
|
|
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
|
-
}
|
|
2590
|
+
});
|
|
2509
2591
|
const finish = (callback) => {
|
|
2510
2592
|
if (settled) {
|
|
2511
2593
|
return;
|
|
2512
2594
|
}
|
|
2513
2595
|
settled = true;
|
|
2514
|
-
|
|
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
|
|
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
|
|
67
|
-
.default("https://api.companyhelm.com
|
|
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,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
|
+
}
|