@companyhelm/runner 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RunnerPreflight = void 0;
|
|
4
|
+
function toErrorMessage(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
class RunnerPreflight {
|
|
8
|
+
constructor(checks) {
|
|
9
|
+
this.checks = checks;
|
|
10
|
+
}
|
|
11
|
+
async run(options = {}) {
|
|
12
|
+
const results = [];
|
|
13
|
+
for (const check of this.checks) {
|
|
14
|
+
results.push(await this.runCheck(check, options.applyFixes === true));
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
passed: results.every((result) => result.status !== "failed"),
|
|
18
|
+
results,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async runCheck(check, applyFixes) {
|
|
22
|
+
let result = await this.safeRun(check);
|
|
23
|
+
if (applyFixes && result.status === "failed" && result.fixAvailable) {
|
|
24
|
+
try {
|
|
25
|
+
await check.fix();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
...result,
|
|
30
|
+
id: check.id,
|
|
31
|
+
description: check.description,
|
|
32
|
+
summary: `${result.summary} Fix attempt failed: ${toErrorMessage(error)}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
result = await this.safeRun(check);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
id: check.id,
|
|
40
|
+
description: check.description,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async safeRun(check) {
|
|
44
|
+
try {
|
|
45
|
+
return await check.run();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
status: "failed",
|
|
50
|
+
summary: `Check execution failed: ${toErrorMessage(error)}`,
|
|
51
|
+
fixAvailable: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.RunnerPreflight = RunnerPreflight;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ThreadMetadataStore = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const thread_workspace_provisioner_js_1 = require("./thread_workspace_provisioner.js");
|
|
7
|
+
const THREAD_GIT_SKILLS_CONFIG_FILENAME = "thread-git-skills.json";
|
|
8
|
+
const THREAD_MCP_CONFIG_FILENAME = "thread-mcp.json";
|
|
9
|
+
function toErrorMessage(error) {
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
|
12
|
+
function normalizeNonEmptyString(value) {
|
|
13
|
+
if (typeof value !== "string") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
18
|
+
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null;
|
|
21
|
+
}
|
|
22
|
+
function isHttpsRepositoryUrl(value) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(value);
|
|
25
|
+
return parsed.protocol === "https:";
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function normalizeThreadGitSkillDirectoryPath(value) {
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith("/") || trimmed.includes("\\")) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const segments = trimmed.split("/").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
37
|
+
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return segments.join("/");
|
|
41
|
+
}
|
|
42
|
+
function parseThreadMcpConfig(content) {
|
|
43
|
+
if (!isRecord(content) || !Array.isArray(content.servers)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const parsedServers = [];
|
|
47
|
+
for (const rawServer of content.servers) {
|
|
48
|
+
if (!isRecord(rawServer)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const name = normalizeNonEmptyString(rawServer.name);
|
|
52
|
+
const transport = rawServer.transport;
|
|
53
|
+
const authType = rawServer.authType;
|
|
54
|
+
if (!name ||
|
|
55
|
+
(transport !== "stdio" && transport !== "streamable_http") ||
|
|
56
|
+
(authType !== "none" && authType !== "bearer_token")) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const args = Array.isArray(rawServer.args) && rawServer.args.every((arg) => typeof arg === "string")
|
|
60
|
+
? rawServer.args
|
|
61
|
+
: [];
|
|
62
|
+
const envVars = Array.isArray(rawServer.envVars)
|
|
63
|
+
? rawServer.envVars
|
|
64
|
+
.filter((entry) => isRecord(entry))
|
|
65
|
+
.map((entry) => ({
|
|
66
|
+
key: normalizeNonEmptyString(entry.key) ?? "",
|
|
67
|
+
value: typeof entry.value === "string" ? entry.value : "",
|
|
68
|
+
}))
|
|
69
|
+
.filter((entry) => entry.key.length > 0)
|
|
70
|
+
: [];
|
|
71
|
+
const headers = Array.isArray(rawServer.headers)
|
|
72
|
+
? rawServer.headers
|
|
73
|
+
.filter((entry) => isRecord(entry))
|
|
74
|
+
.map((entry) => ({
|
|
75
|
+
key: normalizeNonEmptyString(entry.key) ?? "",
|
|
76
|
+
value: typeof entry.value === "string" ? entry.value : "",
|
|
77
|
+
}))
|
|
78
|
+
.filter((entry) => entry.key.length > 0)
|
|
79
|
+
: [];
|
|
80
|
+
if (transport === "stdio") {
|
|
81
|
+
const command = normalizeNonEmptyString(rawServer.command);
|
|
82
|
+
if (!command) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
parsedServers.push({
|
|
86
|
+
name,
|
|
87
|
+
transport,
|
|
88
|
+
command,
|
|
89
|
+
args,
|
|
90
|
+
envVars,
|
|
91
|
+
authType,
|
|
92
|
+
headers: [],
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const url = normalizeNonEmptyString(rawServer.url);
|
|
97
|
+
const bearerToken = authType === "bearer_token"
|
|
98
|
+
? normalizeNonEmptyString(rawServer.bearerToken)
|
|
99
|
+
: null;
|
|
100
|
+
if (!url || (authType === "bearer_token" && !bearerToken)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
parsedServers.push({
|
|
104
|
+
name,
|
|
105
|
+
transport,
|
|
106
|
+
args: [],
|
|
107
|
+
envVars: [],
|
|
108
|
+
url,
|
|
109
|
+
authType,
|
|
110
|
+
bearerToken,
|
|
111
|
+
headers,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return parsedServers;
|
|
115
|
+
}
|
|
116
|
+
function parseThreadGitSkillsConfig(content) {
|
|
117
|
+
if (!isRecord(content) || !Array.isArray(content.packages)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const parsedPackages = [];
|
|
121
|
+
for (const rawPackage of content.packages) {
|
|
122
|
+
if (!isRecord(rawPackage)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const repositoryUrl = normalizeNonEmptyString(rawPackage.repositoryUrl);
|
|
126
|
+
const commitReference = normalizeNonEmptyString(rawPackage.commitReference);
|
|
127
|
+
const checkoutDirectoryName = normalizeNonEmptyString(rawPackage.checkoutDirectoryName);
|
|
128
|
+
const rawSkills = rawPackage.skills;
|
|
129
|
+
if (!repositoryUrl ||
|
|
130
|
+
!isHttpsRepositoryUrl(repositoryUrl) ||
|
|
131
|
+
!commitReference ||
|
|
132
|
+
!checkoutDirectoryName ||
|
|
133
|
+
checkoutDirectoryName.includes("/") ||
|
|
134
|
+
checkoutDirectoryName.includes("\\") ||
|
|
135
|
+
!Array.isArray(rawSkills)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const parsedSkills = [];
|
|
139
|
+
for (const rawSkill of rawSkills) {
|
|
140
|
+
if (!isRecord(rawSkill)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const directoryPath = normalizeThreadGitSkillDirectoryPath(normalizeNonEmptyString(rawSkill.directoryPath) ?? "");
|
|
144
|
+
const linkName = normalizeNonEmptyString(rawSkill.linkName);
|
|
145
|
+
if (!directoryPath ||
|
|
146
|
+
!linkName ||
|
|
147
|
+
linkName.includes("/") ||
|
|
148
|
+
linkName.includes("\\") ||
|
|
149
|
+
linkName.trim() === "." ||
|
|
150
|
+
linkName.trim() === "..") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
parsedSkills.push({ directoryPath, linkName });
|
|
154
|
+
}
|
|
155
|
+
if (parsedSkills.length === 0) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
parsedPackages.push({
|
|
159
|
+
repositoryUrl,
|
|
160
|
+
commitReference,
|
|
161
|
+
checkoutDirectoryName,
|
|
162
|
+
skills: parsedSkills,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return parsedPackages;
|
|
166
|
+
}
|
|
167
|
+
class ThreadMetadataStore {
|
|
168
|
+
constructor(configDirectory, logger) {
|
|
169
|
+
this.configDirectory = configDirectory;
|
|
170
|
+
this.logger = logger;
|
|
171
|
+
}
|
|
172
|
+
resolveThreadMetadataPath(threadId, filename) {
|
|
173
|
+
return (0, node_path_1.join)((0, thread_workspace_provisioner_js_1.resolveThreadMetadataDirectory)(this.configDirectory, threadId), filename);
|
|
174
|
+
}
|
|
175
|
+
writeJsonFile(threadId, filename, payload) {
|
|
176
|
+
const filePath = this.resolveThreadMetadataPath(threadId, filename);
|
|
177
|
+
const directoryPath = (0, thread_workspace_provisioner_js_1.resolveThreadMetadataDirectory)(this.configDirectory, threadId);
|
|
178
|
+
const temporaryPath = `${filePath}.tmp`;
|
|
179
|
+
try {
|
|
180
|
+
(0, node_fs_1.mkdirSync)(directoryPath, { recursive: true });
|
|
181
|
+
(0, node_fs_1.writeFileSync)(temporaryPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
182
|
+
(0, node_fs_1.renameSync)(temporaryPath, filePath);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.logger.warn(`Failed writing thread metadata at '${filePath}': ${toErrorMessage(error)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
removeFile(threadId, filename) {
|
|
189
|
+
const filePath = this.resolveThreadMetadataPath(threadId, filename);
|
|
190
|
+
(0, node_fs_1.rmSync)(filePath, { force: true });
|
|
191
|
+
(0, node_fs_1.rmSync)(`${filePath}.tmp`, { force: true });
|
|
192
|
+
}
|
|
193
|
+
writeThreadMcpConfig(threadId, mcpServers) {
|
|
194
|
+
if (mcpServers.length === 0) {
|
|
195
|
+
this.removeFile(threadId, THREAD_MCP_CONFIG_FILENAME);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.writeJsonFile(threadId, THREAD_MCP_CONFIG_FILENAME, { servers: mcpServers });
|
|
199
|
+
}
|
|
200
|
+
readThreadMcpConfig(threadId) {
|
|
201
|
+
const filePath = this.resolveThreadMetadataPath(threadId, THREAD_MCP_CONFIG_FILENAME);
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf8"));
|
|
204
|
+
const mcpServers = parseThreadMcpConfig(parsed);
|
|
205
|
+
if (!mcpServers) {
|
|
206
|
+
this.logger.warn(`Thread MCP config has invalid shape at '${filePath}'.`);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
return mcpServers;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
this.logger.warn(`Failed reading thread MCP config at '${filePath}': ${toErrorMessage(error)}`);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
writeThreadGitSkillsConfig(threadId, gitSkillPackages) {
|
|
220
|
+
if (gitSkillPackages.length === 0) {
|
|
221
|
+
this.removeFile(threadId, THREAD_GIT_SKILLS_CONFIG_FILENAME);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.writeJsonFile(threadId, THREAD_GIT_SKILLS_CONFIG_FILENAME, { packages: gitSkillPackages });
|
|
225
|
+
}
|
|
226
|
+
readThreadGitSkillsConfig(threadId) {
|
|
227
|
+
const filePath = this.resolveThreadMetadataPath(threadId, THREAD_GIT_SKILLS_CONFIG_FILENAME);
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf8"));
|
|
230
|
+
const packages = parseThreadGitSkillsConfig(parsed);
|
|
231
|
+
if (!packages) {
|
|
232
|
+
this.logger.warn(`Thread git skills config has invalid shape at '${filePath}'.`);
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
return packages;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
this.logger.warn(`Failed reading thread git skills config at '${filePath}': ${toErrorMessage(error)}`);
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
removeThreadMetadata(threadId) {
|
|
246
|
+
(0, node_fs_1.rmSync)((0, thread_workspace_provisioner_js_1.resolveThreadMetadataDirectory)(this.configDirectory, threadId), { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
exports.ThreadMetadataStore = ThreadMetadataStore;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ThreadWorkspaceProvisioner = void 0;
|
|
4
|
+
exports.resolveThreadWorkspaceDirectory = resolveThreadWorkspaceDirectory;
|
|
5
|
+
exports.resolveThreadMetadataDirectory = resolveThreadMetadataDirectory;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const path_js_1 = require("../../utils/path.js");
|
|
9
|
+
const thread_lifecycle_js_1 = require("../../service/thread_lifecycle.js");
|
|
10
|
+
function resolveSharedWorkspacePath(workspacePath) {
|
|
11
|
+
const expandedWorkspacePath = (0, path_js_1.expandHome)(workspacePath);
|
|
12
|
+
if ((0, node_path_1.isAbsolute)(expandedWorkspacePath)) {
|
|
13
|
+
return expandedWorkspacePath;
|
|
14
|
+
}
|
|
15
|
+
return (0, node_path_1.resolve)(process.cwd(), expandedWorkspacePath);
|
|
16
|
+
}
|
|
17
|
+
function resolveThreadWorkspaceDirectory(options) {
|
|
18
|
+
if (options.useDedicatedWorkspaces) {
|
|
19
|
+
return (0, thread_lifecycle_js_1.resolveThreadDirectory)(options.configDirectory, options.workspacesDirectory, options.threadId);
|
|
20
|
+
}
|
|
21
|
+
return resolveSharedWorkspacePath(options.workspacePath);
|
|
22
|
+
}
|
|
23
|
+
function resolveThreadMetadataDirectory(configDirectory, threadId) {
|
|
24
|
+
return (0, node_path_1.join)((0, path_js_1.expandHome)(configDirectory), "thread-metadata", `thread-${threadId}`);
|
|
25
|
+
}
|
|
26
|
+
class ThreadWorkspaceProvisioner {
|
|
27
|
+
constructor(configDirectory, workspacesDirectory, workspacePath, useDedicatedWorkspaces) {
|
|
28
|
+
this.configDirectory = configDirectory;
|
|
29
|
+
this.workspacesDirectory = workspacesDirectory;
|
|
30
|
+
this.workspacePath = workspacePath;
|
|
31
|
+
this.useDedicatedWorkspaces = useDedicatedWorkspaces;
|
|
32
|
+
}
|
|
33
|
+
resolveWorkspaceDirectory(threadId) {
|
|
34
|
+
return resolveThreadWorkspaceDirectory({
|
|
35
|
+
configDirectory: this.configDirectory,
|
|
36
|
+
workspacesDirectory: this.workspacesDirectory,
|
|
37
|
+
workspacePath: this.workspacePath,
|
|
38
|
+
useDedicatedWorkspaces: this.useDedicatedWorkspaces,
|
|
39
|
+
threadId,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
ensureWorkspaceDirectory(threadId) {
|
|
43
|
+
const workspaceDirectory = this.resolveWorkspaceDirectory(threadId);
|
|
44
|
+
(0, node_fs_1.mkdirSync)(workspaceDirectory, { recursive: true });
|
|
45
|
+
return workspaceDirectory;
|
|
46
|
+
}
|
|
47
|
+
removeWorkspaceDirectory(threadId, workspaceDirectory) {
|
|
48
|
+
if (!this.useDedicatedWorkspaces) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (workspaceDirectory !== this.resolveWorkspaceDirectory(threadId)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
(0, node_fs_1.rmSync)(workspaceDirectory, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.ThreadWorkspaceProvisioner = ThreadWorkspaceProvisioner;
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
renderAgentCliConfigScript(user, config) {
|
|
46
|
+
const configDirectory = (0, node_path_1.join)(user.agentHomeDirectory, ".config", "companyhelm-agent-cli");
|
|
47
|
+
const configPath = (0, node_path_1.join)(configDirectory, "config.json");
|
|
48
|
+
const configContent = `${JSON.stringify(config, null, 2)}\n`;
|
|
49
|
+
return this.templateRenderer.render("provisioning/runtime_agent_cli_config.sh.j2", {
|
|
50
|
+
config_dir: shellQuote(configDirectory),
|
|
51
|
+
config_path: shellQuote(configPath),
|
|
52
|
+
config_content: shellQuote(configContent),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
renderGitConfigScript(gitUserName, gitUserEmail) {
|
|
56
|
+
return this.templateRenderer.render("provisioning/runtime_git_config.sh.j2", {
|
|
57
|
+
default_git_user_name: shellQuote(gitUserName),
|
|
58
|
+
default_git_user_email: shellQuote(gitUserEmail),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
renderThreadGitSkillsCloneScript(options) {
|
|
62
|
+
const cloneRootDirectory = resolveThreadGitSkillsCloneRootDirectory(options);
|
|
63
|
+
const packageBlocks = options.packages.map((pkg) => {
|
|
64
|
+
const checkoutPath = (0, node_path_1.join)(cloneRootDirectory, pkg.checkoutDirectoryName);
|
|
65
|
+
const sourceMarkerPath = (0, node_path_1.join)(checkoutPath, ".companyhelm-source");
|
|
66
|
+
return [
|
|
67
|
+
`PACKAGE_DIR=${shellQuote(checkoutPath)}`,
|
|
68
|
+
`PACKAGE_SOURCE_MARKER=${shellQuote(sourceMarkerPath)}`,
|
|
69
|
+
`PACKAGE_REPO_URL=${shellQuote(pkg.repositoryUrl)}`,
|
|
70
|
+
`PACKAGE_COMMIT_REF=${shellQuote(pkg.commitReference)}`,
|
|
71
|
+
'if [ ! -d "$PACKAGE_DIR/.git" ] || [ ! -f "$PACKAGE_SOURCE_MARKER" ] || [ "$(cat "$PACKAGE_SOURCE_MARKER")" != "$PACKAGE_REPO_URL#$PACKAGE_COMMIT_REF" ]; then',
|
|
72
|
+
' rm -rf "$PACKAGE_DIR"',
|
|
73
|
+
' if ! git clone --depth 1 --branch "$PACKAGE_COMMIT_REF" "$PACKAGE_REPO_URL" "$PACKAGE_DIR"; then',
|
|
74
|
+
' rm -rf "$PACKAGE_DIR"',
|
|
75
|
+
' git clone --depth 1 "$PACKAGE_REPO_URL" "$PACKAGE_DIR"',
|
|
76
|
+
' git -C "$PACKAGE_DIR" fetch --depth 1 origin "$PACKAGE_COMMIT_REF"',
|
|
77
|
+
' git -C "$PACKAGE_DIR" checkout --detach FETCH_HEAD',
|
|
78
|
+
" fi",
|
|
79
|
+
' printf \'%s\' "$PACKAGE_REPO_URL#$PACKAGE_COMMIT_REF" > "$PACKAGE_SOURCE_MARKER"',
|
|
80
|
+
"fi",
|
|
81
|
+
'chmod -R a+rX "$PACKAGE_DIR" || true',
|
|
82
|
+
].join("\n");
|
|
83
|
+
}).join("\n\n");
|
|
84
|
+
return this.templateRenderer.render("provisioning/runtime_thread_git_skills_clone.sh.j2", {
|
|
85
|
+
skills_root: shellQuote(cloneRootDirectory),
|
|
86
|
+
package_blocks: packageBlocks,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
renderThreadGitSkillsLinkScript(user, options) {
|
|
90
|
+
const cloneRootDirectory = resolveThreadGitSkillsCloneRootDirectory(options);
|
|
91
|
+
const codexSkillsDirectory = (0, node_path_1.join)(user.agentHomeDirectory, ".codex", "skills");
|
|
92
|
+
const skillBlocks = options.packages.flatMap((pkg) => pkg.skills.map((skill) => [
|
|
93
|
+
`SKILL_SOURCE=${shellQuote((0, node_path_1.join)(cloneRootDirectory, pkg.checkoutDirectoryName, skill.directoryPath))}`,
|
|
94
|
+
`SKILL_LINK=${shellQuote((0, node_path_1.join)(codexSkillsDirectory, skill.linkName))}`,
|
|
95
|
+
'if [ ! -d "$SKILL_SOURCE" ]; then',
|
|
96
|
+
' echo "Thread git skill directory not found: $SKILL_SOURCE" >&2',
|
|
97
|
+
" exit 1",
|
|
98
|
+
"fi",
|
|
99
|
+
'if [ ! -f "$SKILL_SOURCE/SKILL.md" ]; then',
|
|
100
|
+
' echo "Thread git skill directory is missing SKILL.md: $SKILL_SOURCE" >&2',
|
|
101
|
+
" exit 1",
|
|
102
|
+
"fi",
|
|
103
|
+
'rm -rf "$SKILL_LINK"',
|
|
104
|
+
'ln -s "$SKILL_SOURCE" "$SKILL_LINK"',
|
|
105
|
+
].join("\n"))).join("\n\n");
|
|
106
|
+
return this.templateRenderer.render("provisioning/runtime_thread_git_skills_link.sh.j2", {
|
|
107
|
+
skills_root: shellQuote(cloneRootDirectory),
|
|
108
|
+
codex_skills_root: shellQuote(codexSkillsDirectory),
|
|
109
|
+
skill_blocks: skillBlocks,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
renderAgentMetadataScript(user, files) {
|
|
113
|
+
const metadataRoot = (0, node_path_1.join)(user.agentHomeDirectory, ".companyhelm", "agent");
|
|
114
|
+
const fileBlocks = files.map((file) => {
|
|
115
|
+
const filePath = (0, node_path_1.join)(metadataRoot, file.filename);
|
|
116
|
+
return [
|
|
117
|
+
`FILE_PATH=${shellQuote(filePath)}`,
|
|
118
|
+
`FILE_CONTENT=${shellQuote(file.content)}`,
|
|
119
|
+
'printf \'%s\' "$FILE_CONTENT" > "$FILE_PATH"',
|
|
120
|
+
'chmod 0600 "$FILE_PATH"',
|
|
121
|
+
].join("\n");
|
|
122
|
+
}).join("\n\n");
|
|
123
|
+
return this.templateRenderer.render("provisioning/runtime_agent_metadata.sh.j2", {
|
|
124
|
+
agent_home: shellQuote(user.agentHomeDirectory),
|
|
125
|
+
metadata_root: shellQuote(metadataRoot),
|
|
126
|
+
file_blocks: fileBlocks,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.RuntimeProvisioningScriptRenderer = RuntimeProvisioningScriptRenderer;
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
};
|
|
24
|
+
const common = this.templateRenderer.render("system_prompts/common.md.j2", context).trim();
|
|
25
|
+
const workspaceSpecificTemplate = options.workspaceMode === "dedicated"
|
|
26
|
+
? "system_prompts/dedicated_workspace.md.j2"
|
|
27
|
+
: "system_prompts/shared_workspace.md.j2";
|
|
28
|
+
const workspaceSpecific = this.templateRenderer.render(workspaceSpecificTemplate, context).trim();
|
|
29
|
+
return `${common}\n\n${workspaceSpecific}\n`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.RuntimeSystemPromptRenderer = RuntimeSystemPromptRenderer;
|
|
33
|
+
function renderRuntimeSystemPrompt(options) {
|
|
34
|
+
return new RuntimeSystemPromptRenderer().render(options);
|
|
35
|
+
}
|
|
36
|
+
function buildCodexDeveloperInstructions(additionalInstructions, options) {
|
|
37
|
+
const systemPrompt = renderRuntimeSystemPrompt(options).trimEnd();
|
|
38
|
+
const normalizedAdditionalInstructions = normalizeAdditionalInstructions(additionalInstructions);
|
|
39
|
+
if (!normalizedAdditionalInstructions) {
|
|
40
|
+
return `${systemPrompt}\n`;
|
|
41
|
+
}
|
|
42
|
+
return `${systemPrompt}\n\n${normalizedAdditionalInstructions}\n`;
|
|
43
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
}
|