@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.
- package/RUNTIME_IMAGE_VERSION +1 -1
- package/dist/commands/doctor.js +44 -0
- package/dist/commands/register-commands.js +2 -0
- package/dist/commands/root.js +172 -247
- 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 +120 -0
- package/dist/provisioning/runtime_provisioning/system_prompt.js +44 -0
- package/dist/provisioning/template_renderer.js +29 -0
- package/dist/service/companyhelm_api_client.js +0 -48
- 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 +30 -41
- package/dist/service/thread_turn_state.js +1 -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 +37 -0
- package/dist/templates/system_prompts/dedicated_workspace.md.j2 +5 -0
- package/dist/templates/system_prompts/shared_workspace.md.j2 +6 -0
- package/dist/testing/vitest_reporter.js +23 -0
- package/dist/utils/daemon_startup_watchdog.js +27 -0
- package/package.json +2 -2
- package/dist/service/workspace_agents.js +0 -82
- 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
|
-
|
|
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 =
|
|
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 =
|
|
624
|
-
this.runDockerExecScript(["exec", "-u", user.agentUser, name, "bash", "-lc", script], `Failed to validate runtime tooling (nvm/codex/
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,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,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
|
+
```
|