@companyhelm/runner 0.1.1 → 0.2.0

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 (38) 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 +172 -247
  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 +120 -0
  16. package/dist/provisioning/runtime_provisioning/system_prompt.js +44 -0
  17. package/dist/provisioning/template_renderer.js +29 -0
  18. package/dist/service/companyhelm_api_client.js +0 -48
  19. package/dist/service/docker/app_server_container.js +16 -1
  20. package/dist/service/sdk/refresh_models.js +8 -0
  21. package/dist/service/thread_lifecycle.js +30 -41
  22. package/dist/service/thread_turn_state.js +1 -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 +37 -0
  32. package/dist/templates/system_prompts/dedicated_workspace.md.j2 +5 -0
  33. package/dist/templates/system_prompts/shared_workspace.md.j2 +6 -0
  34. package/dist/testing/vitest_reporter.js +23 -0
  35. package/dist/utils/daemon_startup_watchdog.js +27 -0
  36. package/package.json +2 -2
  37. package/dist/service/workspace_agents.js +0 -82
  38. package/dist/templates/runtime_agents.md.j2 +0 -50
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuntimeProvisioningScriptRenderer = void 0;
4
+ const node_path_1 = require("node:path");
5
+ const runtime_bashrc_js_1 = require("../../service/runtime_bashrc.js");
6
+ const runtime_shell_js_1 = require("../../service/runtime_shell.js");
7
+ const template_renderer_js_1 = require("../template_renderer.js");
8
+ function shellQuote(value) {
9
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
10
+ }
11
+ function resolveThreadGitSkillsCloneRootDirectory(options) {
12
+ return options.cloneRootDirectory.trim().length > 0
13
+ ? options.cloneRootDirectory.trim()
14
+ : "/skills";
15
+ }
16
+ class RuntimeProvisioningScriptRenderer {
17
+ constructor(templateRenderer = new template_renderer_js_1.TemplateRenderer()) {
18
+ this.templateRenderer = templateRenderer;
19
+ }
20
+ renderIdentityScript(user) {
21
+ return this.templateRenderer.render("provisioning/runtime_identity.sh.j2", {
22
+ agent_user: shellQuote(user.agentUser),
23
+ agent_home: shellQuote(user.agentHomeDirectory),
24
+ agent_uid: shellQuote(String(user.uid)),
25
+ agent_gid: shellQuote(String(user.gid)),
26
+ });
27
+ }
28
+ renderToolingValidationScript(user) {
29
+ return this.templateRenderer.render("provisioning/runtime_tooling_validation.sh.j2", {
30
+ bootstrap: (0, runtime_shell_js_1.buildNvmCodexBootstrapScript)(user.agentHomeDirectory),
31
+ });
32
+ }
33
+ renderBashrcScript(user) {
34
+ return this.templateRenderer.render("provisioning/runtime_bashrc.sh.j2", {
35
+ agent_home: shellQuote(user.agentHomeDirectory),
36
+ bashrc_content: shellQuote((0, runtime_bashrc_js_1.renderRuntimeBashrc)(user.agentHomeDirectory)),
37
+ });
38
+ }
39
+ renderCodexConfigScript(user, configToml) {
40
+ return this.templateRenderer.render("provisioning/runtime_codex_config.sh.j2", {
41
+ agent_home: shellQuote(user.agentHomeDirectory),
42
+ config_content: shellQuote(configToml),
43
+ });
44
+ }
45
+ renderGitConfigScript(gitUserName, gitUserEmail) {
46
+ return this.templateRenderer.render("provisioning/runtime_git_config.sh.j2", {
47
+ default_git_user_name: shellQuote(gitUserName),
48
+ default_git_user_email: shellQuote(gitUserEmail),
49
+ });
50
+ }
51
+ renderThreadGitSkillsCloneScript(options) {
52
+ const cloneRootDirectory = resolveThreadGitSkillsCloneRootDirectory(options);
53
+ const packageBlocks = options.packages.map((pkg) => {
54
+ const checkoutPath = (0, node_path_1.join)(cloneRootDirectory, pkg.checkoutDirectoryName);
55
+ const sourceMarkerPath = (0, node_path_1.join)(checkoutPath, ".companyhelm-source");
56
+ return [
57
+ `PACKAGE_DIR=${shellQuote(checkoutPath)}`,
58
+ `PACKAGE_SOURCE_MARKER=${shellQuote(sourceMarkerPath)}`,
59
+ `PACKAGE_REPO_URL=${shellQuote(pkg.repositoryUrl)}`,
60
+ `PACKAGE_COMMIT_REF=${shellQuote(pkg.commitReference)}`,
61
+ 'if [ ! -d "$PACKAGE_DIR/.git" ] || [ ! -f "$PACKAGE_SOURCE_MARKER" ] || [ "$(cat "$PACKAGE_SOURCE_MARKER")" != "$PACKAGE_REPO_URL#$PACKAGE_COMMIT_REF" ]; then',
62
+ ' rm -rf "$PACKAGE_DIR"',
63
+ ' if ! git clone --depth 1 --branch "$PACKAGE_COMMIT_REF" "$PACKAGE_REPO_URL" "$PACKAGE_DIR"; then',
64
+ ' rm -rf "$PACKAGE_DIR"',
65
+ ' git clone --depth 1 "$PACKAGE_REPO_URL" "$PACKAGE_DIR"',
66
+ ' git -C "$PACKAGE_DIR" fetch --depth 1 origin "$PACKAGE_COMMIT_REF"',
67
+ ' git -C "$PACKAGE_DIR" checkout --detach FETCH_HEAD',
68
+ " fi",
69
+ ' printf \'%s\' "$PACKAGE_REPO_URL#$PACKAGE_COMMIT_REF" > "$PACKAGE_SOURCE_MARKER"',
70
+ "fi",
71
+ 'chmod -R a+rX "$PACKAGE_DIR" || true',
72
+ ].join("\n");
73
+ }).join("\n\n");
74
+ return this.templateRenderer.render("provisioning/runtime_thread_git_skills_clone.sh.j2", {
75
+ skills_root: shellQuote(cloneRootDirectory),
76
+ package_blocks: packageBlocks,
77
+ });
78
+ }
79
+ renderThreadGitSkillsLinkScript(user, options) {
80
+ const cloneRootDirectory = resolveThreadGitSkillsCloneRootDirectory(options);
81
+ const codexSkillsDirectory = (0, node_path_1.join)(user.agentHomeDirectory, ".codex", "skills");
82
+ const skillBlocks = options.packages.flatMap((pkg) => pkg.skills.map((skill) => [
83
+ `SKILL_SOURCE=${shellQuote((0, node_path_1.join)(cloneRootDirectory, pkg.checkoutDirectoryName, skill.directoryPath))}`,
84
+ `SKILL_LINK=${shellQuote((0, node_path_1.join)(codexSkillsDirectory, skill.linkName))}`,
85
+ 'if [ ! -d "$SKILL_SOURCE" ]; then',
86
+ ' echo "Thread git skill directory not found: $SKILL_SOURCE" >&2',
87
+ " exit 1",
88
+ "fi",
89
+ 'if [ ! -f "$SKILL_SOURCE/SKILL.md" ]; then',
90
+ ' echo "Thread git skill directory is missing SKILL.md: $SKILL_SOURCE" >&2',
91
+ " exit 1",
92
+ "fi",
93
+ 'rm -rf "$SKILL_LINK"',
94
+ 'ln -s "$SKILL_SOURCE" "$SKILL_LINK"',
95
+ ].join("\n"))).join("\n\n");
96
+ return this.templateRenderer.render("provisioning/runtime_thread_git_skills_link.sh.j2", {
97
+ skills_root: shellQuote(cloneRootDirectory),
98
+ codex_skills_root: shellQuote(codexSkillsDirectory),
99
+ skill_blocks: skillBlocks,
100
+ });
101
+ }
102
+ renderAgentMetadataScript(user, files) {
103
+ const metadataRoot = (0, node_path_1.join)(user.agentHomeDirectory, ".companyhelm", "agent");
104
+ const fileBlocks = files.map((file) => {
105
+ const filePath = (0, node_path_1.join)(metadataRoot, file.filename);
106
+ return [
107
+ `FILE_PATH=${shellQuote(filePath)}`,
108
+ `FILE_CONTENT=${shellQuote(file.content)}`,
109
+ 'printf \'%s\' "$FILE_CONTENT" > "$FILE_PATH"',
110
+ 'chmod 0600 "$FILE_PATH"',
111
+ ].join("\n");
112
+ }).join("\n\n");
113
+ return this.templateRenderer.render("provisioning/runtime_agent_metadata.sh.j2", {
114
+ agent_home: shellQuote(user.agentHomeDirectory),
115
+ metadata_root: shellQuote(metadataRoot),
116
+ file_blocks: fileBlocks,
117
+ });
118
+ }
119
+ }
120
+ exports.RuntimeProvisioningScriptRenderer = RuntimeProvisioningScriptRenderer;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuntimeSystemPromptRenderer = void 0;
4
+ exports.renderRuntimeSystemPrompt = renderRuntimeSystemPrompt;
5
+ exports.buildCodexDeveloperInstructions = buildCodexDeveloperInstructions;
6
+ const template_renderer_js_1 = require("../template_renderer.js");
7
+ function normalizeAdditionalInstructions(value) {
8
+ if (typeof value !== "string") {
9
+ return null;
10
+ }
11
+ const trimmed = value.trim();
12
+ return trimmed.length > 0 ? trimmed : null;
13
+ }
14
+ class RuntimeSystemPromptRenderer {
15
+ constructor(templateRenderer = new template_renderer_js_1.TemplateRenderer()) {
16
+ this.templateRenderer = templateRenderer;
17
+ }
18
+ render(options) {
19
+ const context = {
20
+ home_directory: options.homeDirectory,
21
+ agent_api_url: options.agentApiUrl,
22
+ agent_token: options.agentToken,
23
+ thread_id: options.threadId,
24
+ };
25
+ const common = this.templateRenderer.render("system_prompts/common.md.j2", context).trim();
26
+ const workspaceSpecificTemplate = options.workspaceMode === "dedicated"
27
+ ? "system_prompts/dedicated_workspace.md.j2"
28
+ : "system_prompts/shared_workspace.md.j2";
29
+ const workspaceSpecific = this.templateRenderer.render(workspaceSpecificTemplate, context).trim();
30
+ return `${common}\n\n${workspaceSpecific}\n`;
31
+ }
32
+ }
33
+ exports.RuntimeSystemPromptRenderer = RuntimeSystemPromptRenderer;
34
+ function renderRuntimeSystemPrompt(options) {
35
+ return new RuntimeSystemPromptRenderer().render(options);
36
+ }
37
+ function buildCodexDeveloperInstructions(additionalInstructions, options) {
38
+ const systemPrompt = renderRuntimeSystemPrompt(options).trimEnd();
39
+ const normalizedAdditionalInstructions = normalizeAdditionalInstructions(additionalInstructions);
40
+ if (!normalizedAdditionalInstructions) {
41
+ return `${systemPrompt}\n`;
42
+ }
43
+ return `${systemPrompt}\n\n${normalizedAdditionalInstructions}\n`;
44
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TemplateRenderer = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ class TemplateRenderer {
7
+ resolveTemplatePath(relativePath) {
8
+ const distRelativePath = (0, node_path_1.join)(__dirname, "..", "templates", relativePath);
9
+ if ((0, node_fs_1.existsSync)(distRelativePath)) {
10
+ return distRelativePath;
11
+ }
12
+ const sourceRelativePath = (0, node_path_1.join)(__dirname, "..", "..", "src", "templates", relativePath);
13
+ if ((0, node_fs_1.existsSync)(sourceRelativePath)) {
14
+ return sourceRelativePath;
15
+ }
16
+ throw new Error(`Template was not found at ${distRelativePath} or ${sourceRelativePath}`);
17
+ }
18
+ render(relativePath, context) {
19
+ const template = (0, node_fs_1.readFileSync)(this.resolveTemplatePath(relativePath), "utf8");
20
+ return template.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (_match, key) => {
21
+ const value = context[key];
22
+ if (value === undefined) {
23
+ throw new Error(`Missing template value for key '${key}' in '${relativePath}'`);
24
+ }
25
+ return value;
26
+ });
27
+ }
28
+ }
29
+ exports.TemplateRenderer = TemplateRenderer;
@@ -120,24 +120,6 @@ function createAgentRunnerControlServiceDefinition(pathPrefix = "") {
120
120
  responseSerialize: (response) => Buffer.from((0, protobuf_1.toBinary)(protos_1.ServerMessageSchema, response)),
121
121
  responseDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.ServerMessageSchema, bytes),
122
122
  },
123
- listGithubInstallationsForRunner: {
124
- path: buildRpcPath(methods.listGithubInstallations.name, pathPrefix),
125
- requestStream: false,
126
- responseStream: false,
127
- requestSerialize: (request) => Buffer.from((0, protobuf_1.toBinary)(protos_1.ListGithubInstallationsRequestSchema, request)),
128
- requestDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.ListGithubInstallationsRequestSchema, bytes),
129
- responseSerialize: (response) => Buffer.from((0, protobuf_1.toBinary)(protos_1.ListGithubInstallationsResponseSchema, response)),
130
- responseDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.ListGithubInstallationsResponseSchema, bytes),
131
- },
132
- getGithubInstallationAccessTokenForRunner: {
133
- path: buildRpcPath(methods.githubInstallationAccessToken.name, pathPrefix),
134
- requestStream: false,
135
- responseStream: false,
136
- requestSerialize: (request) => Buffer.from((0, protobuf_1.toBinary)(protos_1.GithubInstallationAccessTokenRequestSchema, request)),
137
- requestDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.GithubInstallationAccessTokenRequestSchema, bytes),
138
- responseSerialize: (response) => Buffer.from((0, protobuf_1.toBinary)(protos_1.GithubInstallationAccessTokenResponseSchema, response)),
139
- responseDeserialize: (bytes) => (0, protobuf_1.fromBinary)(protos_1.GithubInstallationAccessTokenResponseSchema, bytes),
140
- },
141
123
  };
142
124
  }
143
125
  function createAgentRunnerControlClient(endpoint, credentials, channelOptions) {
@@ -300,36 +282,6 @@ class CompanyhelmApiClient {
300
282
  const stream = this.client.controlChannel(options?.metadata, options?.callOptions);
301
283
  return new CompanyhelmCommandChannel(stream, this.logger);
302
284
  }
303
- listGithubInstallationsForRunner(options) {
304
- const metadata = options?.metadata ?? new grpc.Metadata();
305
- const callOptions = options?.callOptions ?? {};
306
- const request = (0, protobuf_1.create)(protos_1.ListGithubInstallationsRequestSchema, {});
307
- return new Promise((resolve, reject) => {
308
- this.client.listGithubInstallationsForRunner(request, metadata, callOptions, (error, response) => {
309
- if (error) {
310
- reject(error);
311
- return;
312
- }
313
- resolve(response ?? (0, protobuf_1.create)(protos_1.ListGithubInstallationsResponseSchema, {}));
314
- });
315
- });
316
- }
317
- getGithubInstallationAccessTokenForRunner(installationId, options) {
318
- const metadata = options?.metadata ?? new grpc.Metadata();
319
- const callOptions = options?.callOptions ?? {};
320
- const request = (0, protobuf_1.create)(protos_1.GithubInstallationAccessTokenRequestSchema, {
321
- installationId,
322
- });
323
- return new Promise((resolve, reject) => {
324
- this.client.getGithubInstallationAccessTokenForRunner(request, metadata, callOptions, (error, response) => {
325
- if (error) {
326
- reject(error);
327
- return;
328
- }
329
- resolve(response ?? (0, protobuf_1.create)(protos_1.GithubInstallationAccessTokenResponseSchema, {}));
330
- });
331
- });
332
- }
333
285
  close() {
334
286
  this.client.close();
335
287
  }
@@ -110,6 +110,13 @@ class AppServerContainerService {
110
110
  static isRecord(value) {
111
111
  return typeof value === "object" && value !== null;
112
112
  }
113
+ static isRemoteManifestUnknown(error) {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ return /manifest\s+for .* not found|manifest unknown/i.test(message);
116
+ }
117
+ static buildRemoteManifestUnknownError(image) {
118
+ return new Error(`Docker image '${image}' is not available remotely yet. The Docker build/push may still be running. Wait for the image publish to finish, or set runtime_image to an available tag, then retry.`);
119
+ }
113
120
  async pullImage(image) {
114
121
  let lastReportedProgressBucket = -1;
115
122
  const layerProgress = new Map();
@@ -186,7 +193,15 @@ class AppServerContainerService {
186
193
  }
187
194
  }
188
195
  this.reportImageStatus(`Docker image '${image}' not found locally. Pulling remotely.`);
189
- await this.pullImage(image);
196
+ try {
197
+ await this.pullImage(image);
198
+ }
199
+ catch (error) {
200
+ if (AppServerContainerService.isRemoteManifestUnknown(error)) {
201
+ throw AppServerContainerService.buildRemoteManifestUnknownError(image);
202
+ }
203
+ throw error;
204
+ }
190
205
  this.reportImageStatus(`Docker image '${image}' is ready.`);
191
206
  }
192
207
  async start() {
@@ -11,7 +11,15 @@ const app_server_container_js_1 = require("../docker/app_server_container.js");
11
11
  function toErrorMessage(error) {
12
12
  return error instanceof Error ? error.message : String(error);
13
13
  }
14
+ function isRuntimeImageUnavailable(error) {
15
+ const message = toErrorMessage(error);
16
+ return /manifest\s+for .* not found|manifest unknown/i.test(message);
17
+ }
14
18
  function formatSdkModelRefreshFailure(sdk, error) {
19
+ if (isRuntimeImageUnavailable(error)) {
20
+ return (`Failed to refresh ${sdk} models from the local Codex app-server: ${toErrorMessage(error)}. ` +
21
+ "The configured runner image is not available from Docker yet. The Docker build/push may still be running. Wait for the image publish to finish or set runtime_image to an available tag, then retry.");
22
+ }
15
23
  return (`Failed to refresh ${sdk} models from the local Codex app-server: ${toErrorMessage(error)}. ` +
16
24
  "Verify the runner image can start Codex app-server with valid auth, then retry.");
17
25
  }
@@ -15,6 +15,7 @@ const dockerode_1 = __importDefault(require("dockerode"));
15
15
  const node_child_process_1 = require("node:child_process");
16
16
  const node_fs_1 = require("node:fs");
17
17
  const node_path_1 = require("node:path");
18
+ const script_renderer_js_1 = require("../provisioning/runtime_provisioning/script_renderer.js");
18
19
  const path_js_1 = require("../utils/path.js");
19
20
  const runtime_bashrc_js_1 = require("./runtime_bashrc.js");
20
21
  const runtime_shell_js_1 = require("./runtime_shell.js");
@@ -193,12 +194,6 @@ function buildRuntimeToolingValidationScript(user) {
193
194
  return [
194
195
  bootstrap,
195
196
  "",
196
- 'if ! command -v companyhelm-agent >/dev/null 2>&1; then',
197
- ' echo "companyhelm-agent CLI is not available after sourcing nvm." >&2',
198
- ' echo "Fix: install @companyhelm/agent-cli in the runtime image." >&2',
199
- " exit 1",
200
- "fi",
201
- "",
202
197
  'if ! command -v aws >/dev/null 2>&1; then',
203
198
  ' echo "aws CLI is not available in runtime PATH." >&2',
204
199
  ' echo "Fix: install awscli in the runtime image." >&2',
@@ -318,21 +313,6 @@ function buildRuntimeThreadGitSkillsLinkScript(user, options) {
318
313
  }
319
314
  return scriptLines.join("\n");
320
315
  }
321
- function buildRuntimeAgentCliConfigScript(user, config) {
322
- const configDirectory = (0, node_path_1.join)(user.agentHomeDirectory, ".config", "companyhelm-agent-cli");
323
- const configPath = (0, node_path_1.join)(configDirectory, "config.json");
324
- const configContent = `${JSON.stringify(config, null, 2)}\n`;
325
- return [
326
- "set -euo pipefail",
327
- `CONFIG_DIR=${shellQuote(configDirectory)}`,
328
- `CONFIG_PATH=${shellQuote(configPath)}`,
329
- `CONFIG_CONTENT=${shellQuote(configContent)}`,
330
- "",
331
- 'install -d -m 0755 "$CONFIG_DIR"',
332
- 'printf \'%s\' "$CONFIG_CONTENT" > "$CONFIG_PATH"',
333
- 'chmod 0600 "$CONFIG_PATH"',
334
- ].join("\n");
335
- }
336
316
  function buildDindContainerOptions(options) {
337
317
  return {
338
318
  name: options.names.dind,
@@ -442,6 +422,7 @@ class ThreadContainerService {
442
422
  constructor(docker, runCommand = node_child_process_1.spawnSync) {
443
423
  this.docker = docker ?? new dockerode_1.default();
444
424
  this.runCommand = runCommand;
425
+ this.scriptRenderer = new script_renderer_js_1.RuntimeProvisioningScriptRenderer();
445
426
  }
446
427
  runDockerExecScript(args, contextMessage) {
447
428
  const result = this.runCommand("docker", args, {
@@ -616,46 +597,54 @@ class ThreadContainerService {
616
597
  }
617
598
  }
618
599
  async ensureRuntimeContainerIdentity(name, user) {
619
- const script = buildRuntimeIdentityProvisionScript(user);
600
+ const script = this.scriptRenderer.renderIdentityScript(user);
620
601
  this.runDockerExecScript(["exec", "-u", "0", name, "bash", "-lc", script], `Failed to provision runtime user '${user.agentUser}' in container '${name}'`);
621
602
  }
622
603
  async ensureRuntimeContainerTooling(name, user) {
623
- const script = buildRuntimeToolingValidationScript(user);
624
- this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to validate runtime tooling (nvm/codex/companyhelm-agent/aws/playwright) in container '${name}'`);
604
+ const script = this.scriptRenderer.renderToolingValidationScript(user);
605
+ this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to validate runtime tooling (nvm/codex/aws/playwright) in container '${name}'`);
625
606
  }
626
607
  async ensureRuntimeContainerBashrc(name, user) {
627
- const script = buildRuntimeBashrcProvisionScript(user);
608
+ const script = this.scriptRenderer.renderBashrcScript(user);
628
609
  this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to provision runtime .bashrc in container '${name}'`);
629
610
  }
630
611
  async ensureRuntimeContainerCodexConfig(name, user, configToml) {
631
- const script = [
632
- "set -euo pipefail",
633
- `AGENT_HOME=${shellQuote(user.agentHomeDirectory)}`,
634
- `CONFIG_CONTENT=${shellQuote(configToml)}`,
635
- "",
636
- 'install -d -m 0755 "$AGENT_HOME/.codex"',
637
- 'printf \'%s\' "$CONFIG_CONTENT" > "$AGENT_HOME/.codex/config.toml"',
638
- 'chmod 0644 "$AGENT_HOME/.codex/config.toml"',
639
- ].join("\n");
612
+ const script = this.scriptRenderer.renderCodexConfigScript(user, configToml);
640
613
  this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to write runtime Codex config.toml in container '${name}'`);
641
614
  }
642
- async ensureRuntimeContainerAgentCliConfig(name, user, config) {
643
- const script = buildRuntimeAgentCliConfigScript(user, config);
644
- this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to write runtime companyhelm-agent CLI config in container '${name}'`);
645
- }
646
615
  async ensureRuntimeContainerGitConfig(name, user, gitUserName, gitUserEmail) {
647
- const script = buildRuntimeGitConfigScript(gitUserName, gitUserEmail);
616
+ const script = this.scriptRenderer.renderGitConfigScript(gitUserName, gitUserEmail);
648
617
  this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to configure git author defaults in runtime container '${name}'`);
649
618
  }
650
619
  async ensureRuntimeContainerThreadGitSkills(name, user, options) {
651
620
  if (options.packages.length === 0) {
652
621
  return;
653
622
  }
654
- const cloneScript = buildRuntimeThreadGitSkillsCloneScript(options);
623
+ const cloneScript = this.scriptRenderer.renderThreadGitSkillsCloneScript(options);
655
624
  this.runDockerExecScript(["exec", "-u", "0", name, "bash", "-lc", cloneScript], `Failed to provision thread git skills in runtime container '${name}'`);
656
- const linkScript = buildRuntimeThreadGitSkillsLinkScript(user, options);
625
+ const linkScript = this.scriptRenderer.renderThreadGitSkillsLinkScript(user, options);
657
626
  this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", linkScript], `Failed to provision thread git skills in runtime container '${name}'`);
658
627
  }
628
+ async ensureRuntimeContainerAgentMetadataFiles(name, user, files, contextMessage) {
629
+ if (files.length === 0) {
630
+ return;
631
+ }
632
+ const script = this.scriptRenderer.renderAgentMetadataScript(user, files);
633
+ this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], contextMessage);
634
+ }
635
+ async ensureRuntimeContainerThreadMetadata(name, user, payload) {
636
+ const files = [
637
+ {
638
+ filename: "thread-mcp.json",
639
+ content: `${JSON.stringify({ servers: payload.mcpServers }, null, 2)}\n`,
640
+ },
641
+ {
642
+ filename: "thread-git-skills.json",
643
+ content: `${JSON.stringify({ packages: payload.gitSkillPackages }, null, 2)}\n`,
644
+ },
645
+ ];
646
+ await this.ensureRuntimeContainerAgentMetadataFiles(name, user, files, `Failed to write runtime thread metadata in container '${name}'`);
647
+ }
659
648
  async stopContainer(name) {
660
649
  try {
661
650
  await this.docker.getContainer(name).stop({ t: 10 });
@@ -12,6 +12,7 @@ async function loadThreadMessageExecutionState(stateDbPath, threadId) {
12
12
  .select({
13
13
  id: schema_js_1.threads.id,
14
14
  workspace: schema_js_1.threads.workspace,
15
+ cliSecret: schema_js_1.threads.cliSecret,
15
16
  sdkThreadId: schema_js_1.threads.sdkThreadId,
16
17
  model: schema_js_1.threads.model,
17
18
  reasoningLevel: schema_js_1.threads.reasoningLevel,
@@ -0,0 +1,8 @@
1
+ set -euo pipefail
2
+ AGENT_HOME={{agent_home}}
3
+ METADATA_ROOT={{metadata_root}}
4
+
5
+ install -d -m 0755 "$AGENT_HOME/.companyhelm"
6
+ install -d -m 0755 "$METADATA_ROOT"
7
+
8
+ {{file_blocks}}
@@ -0,0 +1,7 @@
1
+ set -euo pipefail
2
+ AGENT_HOME={{agent_home}}
3
+ BASHRC_CONTENT={{bashrc_content}}
4
+
5
+ install -d -m 0755 "$AGENT_HOME"
6
+ printf '%s' "$BASHRC_CONTENT" > "$AGENT_HOME/.bashrc"
7
+ chmod 0644 "$AGENT_HOME/.bashrc"
@@ -0,0 +1,7 @@
1
+ set -euo pipefail
2
+ AGENT_HOME={{agent_home}}
3
+ CONFIG_CONTENT={{config_content}}
4
+
5
+ install -d -m 0755 "$AGENT_HOME/.codex"
6
+ printf '%s' "$CONFIG_CONTENT" > "$AGENT_HOME/.codex/config.toml"
7
+ chmod 0644 "$AGENT_HOME/.codex/config.toml"
@@ -0,0 +1,28 @@
1
+ set -euo pipefail
2
+ DEFAULT_GIT_USER_NAME={{default_git_user_name}}
3
+ DEFAULT_GIT_USER_EMAIL={{default_git_user_email}}
4
+
5
+ if ! command -v git >/dev/null 2>&1; then
6
+ exit 0
7
+ fi
8
+
9
+ if ! git config --global --get user.name >/dev/null 2>&1; then
10
+ git config --global user.name "$DEFAULT_GIT_USER_NAME"
11
+ fi
12
+ if ! git config --global --get user.email >/dev/null 2>&1; then
13
+ git config --global user.email "$DEFAULT_GIT_USER_EMAIL"
14
+ fi
15
+
16
+ if [ ! -d /workspace ]; then
17
+ exit 0
18
+ fi
19
+
20
+ while IFS= read -r -d '' git_dir; do
21
+ repo_root="$(dirname "$git_dir")"
22
+ if ! git -C "$repo_root" config --local --get user.name >/dev/null 2>&1; then
23
+ git -C "$repo_root" config --local user.name "$DEFAULT_GIT_USER_NAME"
24
+ fi
25
+ if ! git -C "$repo_root" config --local --get user.email >/dev/null 2>&1; then
26
+ git -C "$repo_root" config --local user.email "$DEFAULT_GIT_USER_EMAIL"
27
+ fi
28
+ done < <(find /workspace -type d -name .git -print0 2>/dev/null || true)
@@ -0,0 +1,65 @@
1
+ set -euo pipefail
2
+ AGENT_USER={{agent_user}}
3
+ AGENT_HOME={{agent_home}}
4
+ AGENT_UID={{agent_uid}}
5
+ AGENT_GID={{agent_gid}}
6
+
7
+ AGENT_GROUP="$AGENT_USER"
8
+ EXISTING_GID_GROUP="$(getent group "$AGENT_GID" | cut -d: -f1 || true)"
9
+ if [ -n "$EXISTING_GID_GROUP" ] && [ "$EXISTING_GID_GROUP" != "$AGENT_USER" ] && ! getent group "$AGENT_USER" >/dev/null 2>&1; then
10
+ groupmod -n "$AGENT_USER" "$EXISTING_GID_GROUP"
11
+ fi
12
+ if getent group "$AGENT_USER" >/dev/null 2>&1; then
13
+ if [ "$(getent group "$AGENT_USER" | cut -d: -f3)" != "$AGENT_GID" ]; then
14
+ groupmod -g "$AGENT_GID" "$AGENT_USER"
15
+ fi
16
+ else
17
+ groupadd -g "$AGENT_GID" "$AGENT_USER"
18
+ fi
19
+
20
+ EXISTING_UID_USER="$(getent passwd "$AGENT_UID" | cut -d: -f1 || true)"
21
+ if [ -n "$EXISTING_UID_USER" ] && [ "$EXISTING_UID_USER" != "$AGENT_USER" ] && ! id -u "$AGENT_USER" >/dev/null 2>&1; then
22
+ usermod -l "$AGENT_USER" -d "$AGENT_HOME" -g "$AGENT_GROUP" -s /bin/bash "$EXISTING_UID_USER"
23
+ elif id -u "$AGENT_USER" >/dev/null 2>&1; then
24
+ usermod -u "$AGENT_UID" -g "$AGENT_GROUP" -d "$AGENT_HOME" -s /bin/bash "$AGENT_USER"
25
+ else
26
+ useradd -m -d "$AGENT_HOME" -u "$AGENT_UID" -g "$AGENT_GROUP" -s /bin/bash "$AGENT_USER"
27
+ fi
28
+
29
+ install -d -m 0755 -o "$AGENT_UID" -g "$AGENT_GID" "$AGENT_HOME"
30
+
31
+ SUDOERS_FILE="/etc/sudoers.d/90-companyhelm-agent"
32
+ if command -v sudo >/dev/null 2>&1; then
33
+ install -d -m 0750 /etc/sudoers.d
34
+ printf '%s\n' "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" > "$SUDOERS_FILE"
35
+ chmod 0440 "$SUDOERS_FILE"
36
+ if command -v visudo >/dev/null 2>&1; then
37
+ visudo -cf "$SUDOERS_FILE" >/dev/null
38
+ fi
39
+ fi
40
+
41
+ install -d -m 0755 -o "$AGENT_UID" -g "$AGENT_GID" "$AGENT_HOME/.codex"
42
+ install -d -m 0755 -o "$AGENT_UID" -g "$AGENT_GID" "$AGENT_HOME/.cache"
43
+ if [ -e "$AGENT_HOME/.codex/auth.json" ]; then
44
+ chown "$AGENT_UID:$AGENT_GID" "$AGENT_HOME/.codex/auth.json" || true
45
+ fi
46
+
47
+ PLAYWRIGHT_SHARED_CACHE="/ms-playwright"
48
+ PLAYWRIGHT_DEFAULT_CACHE="$AGENT_HOME/.cache/ms-playwright"
49
+ install -d -m 0755 "$PLAYWRIGHT_SHARED_CACHE"
50
+ if [ "$(stat -c '%u:%g' "$PLAYWRIGHT_SHARED_CACHE" 2>/dev/null || true)" != "$AGENT_UID:$AGENT_GID" ]; then
51
+ chown -R "$AGENT_UID:$AGENT_GID" "$PLAYWRIGHT_SHARED_CACHE" || true
52
+ fi
53
+ if [ -L "$PLAYWRIGHT_DEFAULT_CACHE" ]; then
54
+ if [ "$(readlink "$PLAYWRIGHT_DEFAULT_CACHE" 2>/dev/null || true)" != "$PLAYWRIGHT_SHARED_CACHE" ]; then
55
+ rm -f "$PLAYWRIGHT_DEFAULT_CACHE"
56
+ ln -s "$PLAYWRIGHT_SHARED_CACHE" "$PLAYWRIGHT_DEFAULT_CACHE"
57
+ fi
58
+ elif [ ! -e "$PLAYWRIGHT_DEFAULT_CACHE" ]; then
59
+ ln -s "$PLAYWRIGHT_SHARED_CACHE" "$PLAYWRIGHT_DEFAULT_CACHE"
60
+ else
61
+ chown -R "$AGENT_UID:$AGENT_GID" "$PLAYWRIGHT_DEFAULT_CACHE" || true
62
+ fi
63
+ install -d -m 0755 -o "$AGENT_UID" -g "$AGENT_GID" "$AGENT_HOME/.companyhelm"
64
+ install -d -m 0755 -o "$AGENT_UID" -g "$AGENT_GID" "$AGENT_HOME/.companyhelm/agent"
65
+ chown -R "$AGENT_UID:$AGENT_GID" "$AGENT_HOME" || true
@@ -0,0 +1,11 @@
1
+ set -euo pipefail
2
+ SKILLS_ROOT={{skills_root}}
3
+
4
+ install -d -m 0755 "$SKILLS_ROOT"
5
+
6
+ if ! command -v git >/dev/null 2>&1; then
7
+ echo "git is not available in the runtime container." >&2
8
+ exit 1
9
+ fi
10
+
11
+ {{package_blocks}}
@@ -0,0 +1,7 @@
1
+ set -euo pipefail
2
+ SKILLS_ROOT={{skills_root}}
3
+ CODEX_SKILLS_ROOT={{codex_skills_root}}
4
+
5
+ install -d -m 0755 "$CODEX_SKILLS_ROOT"
6
+
7
+ {{skill_blocks}}
@@ -0,0 +1,32 @@
1
+ {{bootstrap}}
2
+
3
+ if ! command -v aws >/dev/null 2>&1; then
4
+ echo "aws CLI is not available in runtime PATH." >&2
5
+ echo "Fix: install awscli in the runtime image." >&2
6
+ exit 1
7
+ fi
8
+
9
+ if ! command -v playwright >/dev/null 2>&1; then
10
+ echo "playwright CLI is not available after sourcing nvm." >&2
11
+ echo "Fix: install playwright in the runtime image (for example: npm install --global playwright)." >&2
12
+ exit 1
13
+ fi
14
+
15
+ PLAYWRIGHT_CACHE_DIR="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
16
+ if [ ! -d "$PLAYWRIGHT_CACHE_DIR" ]; then
17
+ echo "Playwright browser cache directory is missing: $PLAYWRIGHT_CACHE_DIR" >&2
18
+ echo "Fix: set PLAYWRIGHT_BROWSERS_PATH and run npx playwright install chromium during runtime image build." >&2
19
+ exit 1
20
+ fi
21
+
22
+ CHROMIUM_BIN="$(find "$PLAYWRIGHT_CACHE_DIR" -type f -path "*/chrome-linux/chrome" -print -quit 2>/dev/null || true)"
23
+ if [ -z "$CHROMIUM_BIN" ]; then
24
+ echo "Playwright chromium binary is missing under $PLAYWRIGHT_CACHE_DIR." >&2
25
+ echo "Fix: run npx playwright install chromium while building the runtime image." >&2
26
+ exit 1
27
+ fi
28
+
29
+ if [ ! -x "$CHROMIUM_BIN" ]; then
30
+ echo "Playwright chromium binary exists but is not executable: $CHROMIUM_BIN" >&2
31
+ exit 1
32
+ fi
@@ -0,0 +1,37 @@
1
+ # Agent Instructions
2
+
3
+ ## Identity
4
+
5
+ - Your CompanyHelm API thread id is `{{thread_id}}`.
6
+ - Use this thread id when you need to correlate your own runtime actions with CompanyHelm thread state.
7
+
8
+ ## Workspace Structure
9
+
10
+ - You are running in a thread-specific container and workspace.
11
+ - This workspace may be shared or dedicated depending on runner startup configuration.
12
+ - Repositories should live in subdirectories of `/workspace`.
13
+
14
+ ## Docker
15
+
16
+ Docker is available, the docker host runs in a separate container. The network is shared with the container.
17
+ - Only `/workspace` is shared with the docker host.
18
+ - `{{home_directory}}/.codex/auth.json` is also shared with the docker host.
19
+ - Nested DinD is not supported in this environment because the outer runtime already uses rootless DinD.
20
+ - If you need Docker in Docker, mount the configured host runtime instead of trying nested DinD.
21
+
22
+ ## Available CLI Tools
23
+
24
+ - `aws`: AWS CLI is pre-installed and available in `PATH`.
25
+ - For scripted PR creation and updates, use `gh pr create --body-file <path>` and `gh pr edit --body-file <path>`.
26
+ - Playwright CLI is already installed and available with Chromium pre-installed.
27
+
28
+ ## Agent API
29
+
30
+ - Use the CompanyHelm agent REST API directly with `Authorization: Bearer {{agent_token}}`.
31
+ - Agent API base URL: `{{agent_api_url}}`
32
+ - The bearer token above is the existing thread secret.
33
+
34
+ ```bash
35
+ curl -H "Authorization: Bearer {{agent_token}}" \
36
+ {{agent_api_url}}/
37
+ ```