@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
|
@@ -6,12 +6,14 @@ function addRunnerStartOptions(command) {
|
|
|
6
6
|
return command
|
|
7
7
|
.option("--config-path <path>", global_options_js_1.CONFIG_PATH_OPTION_DESCRIPTION)
|
|
8
8
|
.option("--server-url <url>", "CompanyHelm gRPC API URL override.")
|
|
9
|
-
.option("--agent-api-url <url>", "Agent
|
|
9
|
+
.option("--agent-api-url <url>", "Agent REST API base URL for runtime containers (localhost is rewritten to http://host.docker.internal).")
|
|
10
|
+
.option("--workspace-path <path>", "Shared host workspace mounted at /workspace (defaults to the current working directory).")
|
|
10
11
|
.option("--secret <secret>", "Bearer secret used as gRPC Authorization header.")
|
|
11
12
|
.option("--state-db-path <path>", "State database path override (defaults to state.db under the active config directory).")
|
|
12
13
|
.option("--log-path <path>", "Daemon log file override.")
|
|
13
14
|
.option("--use-host-docker-runtime", "Mount host Docker socket into runtime containers instead of creating DinD sidecars.")
|
|
14
15
|
.option("--use-dedicated-auth", "Preserve existing dedicated Codex auth if already configured; otherwise keep Codex unconfigured on startup.")
|
|
16
|
+
.option("--use-dedicated-workspaces", "Create per-thread dedicated workspaces under the configured workspaces directory.")
|
|
15
17
|
.option("--host-docker-path <path>", "Host Docker endpoint when --use-host-docker-runtime is enabled (unix:///<socket-path> or tcp://localhost:<port>).")
|
|
16
18
|
.option("--thread-git-skills-directory <path>", "Container path where thread git skill repositories are cloned before linking into ~/.codex/skills.")
|
|
17
19
|
.option("-d, --daemon", "Run in daemon mode and fail fast when no SDK is configured.")
|
|
@@ -15,6 +15,9 @@ async function runRunnerStartCommand(options) {
|
|
|
15
15
|
onDaemonReady: () => {
|
|
16
16
|
(0, root_js_1.sendDaemonParentMessage)({ type: "daemon-ready" });
|
|
17
17
|
},
|
|
18
|
+
onDaemonProgress: (message) => {
|
|
19
|
+
(0, root_js_1.sendDaemonParentMessage)({ type: "daemon-progress", message });
|
|
20
|
+
},
|
|
18
21
|
}
|
|
19
22
|
: undefined);
|
|
20
23
|
}
|
package/dist/config.js
CHANGED
|
@@ -53,6 +53,12 @@ exports.config = zod_1.z.object({
|
|
|
53
53
|
config_directory: zod_1.z.string()
|
|
54
54
|
.describe("The directory where the config files are stored.")
|
|
55
55
|
.default(resolveConfigDirectoryDefault),
|
|
56
|
+
workspace_path: zod_1.z.string()
|
|
57
|
+
.describe("Shared workspace directory mounted at /workspace when dedicated workspaces are disabled.")
|
|
58
|
+
.default(() => process.cwd()),
|
|
59
|
+
use_dedicated_workspaces: zod_1.z.boolean()
|
|
60
|
+
.describe("When true, create per-thread dedicated workspaces under workspaces_directory.")
|
|
61
|
+
.default(false),
|
|
56
62
|
workspaces_directory: zod_1.z.string()
|
|
57
63
|
.describe("The directory where thread workspaces are stored, relative to config_directory when not absolute.")
|
|
58
64
|
.default("workspaces"),
|
|
@@ -63,8 +69,8 @@ exports.config = zod_1.z.object({
|
|
|
63
69
|
.describe("CompanyHelm control plane gRPC endpoint URL.")
|
|
64
70
|
.default("https://api.companyhelm.com:50051"),
|
|
65
71
|
agent_api_url: zod_1.z.string()
|
|
66
|
-
.describe("CompanyHelm
|
|
67
|
-
.default("https://api.companyhelm.com
|
|
72
|
+
.describe("CompanyHelm agent REST API base URL used inside runtime threads.")
|
|
73
|
+
.default("https://api.companyhelm.com/agent/v1"),
|
|
68
74
|
// Max outbound gRPC client messages to hold while the command channel is disconnected.
|
|
69
75
|
client_message_buffer_limit: zod_1.z.number()
|
|
70
76
|
.int()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LinuxApparmorRestrictUnprivilegedUsernsCheck = void 0;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const APPARMOR_SYSCTL_KEY = "kernel.apparmor_restrict_unprivileged_userns";
|
|
7
|
+
const UNPRIVILEGED_USERNS_SYSCTL_KEY = "kernel.unprivileged_userns_clone";
|
|
8
|
+
const USER_NAMESPACE_LIMIT_SYSCTL_KEY = "user.max_user_namespaces";
|
|
9
|
+
function isRootlessDindImage(image) {
|
|
10
|
+
return image.toLowerCase().includes("rootless");
|
|
11
|
+
}
|
|
12
|
+
async function defaultReadSysctlValue(key) {
|
|
13
|
+
const path = `/proc/sys/${key.replace(/\./g, "/")}`;
|
|
14
|
+
try {
|
|
15
|
+
return (await (0, promises_1.readFile)(path, "utf8")).trim();
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function defaultRunShellCommand(command) {
|
|
25
|
+
await new Promise((resolve, reject) => {
|
|
26
|
+
const child = (0, node_child_process_1.spawn)("bash", ["-lc", command], { stdio: "inherit" });
|
|
27
|
+
child.on("error", reject);
|
|
28
|
+
child.on("exit", (code, signal) => {
|
|
29
|
+
if (code === 0) {
|
|
30
|
+
resolve();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
reject(new Error(`Command failed (${signal ?? code ?? "unknown"}): ${command}`));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
class LinuxApparmorRestrictUnprivilegedUsernsCheck {
|
|
38
|
+
constructor(cfg, dependencies = {}) {
|
|
39
|
+
this.cfg = cfg;
|
|
40
|
+
this.id = "linux.apparmor_restrict_unprivileged_userns";
|
|
41
|
+
this.description = "Verify Linux AppArmor permits unprivileged user namespaces for rootless DinD.";
|
|
42
|
+
this.platform = dependencies.platform ?? process.platform;
|
|
43
|
+
this.readSysctlValue = dependencies.readSysctlValue ?? defaultReadSysctlValue;
|
|
44
|
+
this.runShellCommand = dependencies.runShellCommand ?? defaultRunShellCommand;
|
|
45
|
+
}
|
|
46
|
+
async run() {
|
|
47
|
+
if (!this.isApplicable()) {
|
|
48
|
+
return {
|
|
49
|
+
status: "skipped",
|
|
50
|
+
summary: "Check only applies to Linux rootless DinD setups.",
|
|
51
|
+
fixAvailable: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const apparmorRestriction = await this.readSysctlValue(APPARMOR_SYSCTL_KEY);
|
|
55
|
+
if (apparmorRestriction === "1") {
|
|
56
|
+
const [userNamespaceClone, userNamespaceLimit] = await Promise.all([
|
|
57
|
+
this.readSysctlValue(UNPRIVILEGED_USERNS_SYSCTL_KEY),
|
|
58
|
+
this.readSysctlValue(USER_NAMESPACE_LIMIT_SYSCTL_KEY),
|
|
59
|
+
]);
|
|
60
|
+
return {
|
|
61
|
+
status: "failed",
|
|
62
|
+
summary: `${APPARMOR_SYSCTL_KEY}=1 blocks rootless DinD on this Linux host ` +
|
|
63
|
+
`(kernel.unprivileged_userns_clone=${userNamespaceClone ?? "unknown"}, ` +
|
|
64
|
+
`user.max_user_namespaces=${userNamespaceLimit ?? "unknown"}).`,
|
|
65
|
+
fixAvailable: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
status: "passed",
|
|
70
|
+
summary: "Linux host is compatible with rootless DinD.",
|
|
71
|
+
fixAvailable: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async fix() {
|
|
75
|
+
if (!this.isApplicable()) {
|
|
76
|
+
return {
|
|
77
|
+
status: "skipped",
|
|
78
|
+
summary: "Check only applies to Linux rootless DinD setups.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
await this.runShellCommand("sudo tee /etc/sysctl.d/99-companyhelm-rootless.conf >/dev/null <<'EOF'\n" +
|
|
82
|
+
"kernel.unprivileged_userns_clone = 1\n" +
|
|
83
|
+
"user.max_user_namespaces = 28633\n" +
|
|
84
|
+
"kernel.apparmor_restrict_unprivileged_userns = 0\n" +
|
|
85
|
+
"EOF");
|
|
86
|
+
await this.runShellCommand("sudo sysctl --system");
|
|
87
|
+
return {
|
|
88
|
+
status: "fixed",
|
|
89
|
+
summary: "Updated Linux sysctl configuration for rootless DinD.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
isApplicable() {
|
|
93
|
+
return this.platform === "linux" && !this.cfg.use_host_docker_runtime && isRootlessDindImage(this.cfg.dind_image);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.LinuxApparmorRestrictUnprivilegedUsernsCheck = LinuxApparmorRestrictUnprivilegedUsernsCheck;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RUNNER_STARTUP_PREFLIGHT_SKIP_ENV = void 0;
|
|
4
|
+
exports.createRunnerPreflight = createRunnerPreflight;
|
|
5
|
+
exports.runRunnerPreflight = runRunnerPreflight;
|
|
6
|
+
exports.formatRunnerPreflightSummary = formatRunnerPreflightSummary;
|
|
7
|
+
exports.ensureRunnerStartupPreflight = ensureRunnerStartupPreflight;
|
|
8
|
+
const apparmor_restrict_unprivileged_userns_check_js_1 = require("./checks/linux/apparmor_restrict_unprivileged_userns_check.js");
|
|
9
|
+
const runner_preflight_js_1 = require("./runner_preflight.js");
|
|
10
|
+
exports.RUNNER_STARTUP_PREFLIGHT_SKIP_ENV = "COMPANYHELM_SKIP_RUNNER_STARTUP_PREFLIGHT";
|
|
11
|
+
function renderPreflightStatusLabel(status) {
|
|
12
|
+
if (status === "passed") {
|
|
13
|
+
return "PASS";
|
|
14
|
+
}
|
|
15
|
+
if (status === "failed") {
|
|
16
|
+
return "FAIL";
|
|
17
|
+
}
|
|
18
|
+
return "SKIP";
|
|
19
|
+
}
|
|
20
|
+
function createRunnerPreflight(cfg, overrides = {}) {
|
|
21
|
+
return new runner_preflight_js_1.RunnerPreflight([
|
|
22
|
+
new apparmor_restrict_unprivileged_userns_check_js_1.LinuxApparmorRestrictUnprivilegedUsernsCheck(cfg, overrides),
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
async function runRunnerPreflight(options, overrides = {}) {
|
|
26
|
+
return await createRunnerPreflight(options.cfg, overrides).run({ applyFixes: options.applyFixes });
|
|
27
|
+
}
|
|
28
|
+
function formatRunnerPreflightSummary(summary) {
|
|
29
|
+
const lines = [`Preflight status: ${summary.passed ? "passed" : "failed"}`];
|
|
30
|
+
if (summary.results.length === 0) {
|
|
31
|
+
lines.push("No applicable preflight checks.");
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
for (const result of summary.results) {
|
|
35
|
+
lines.push(`[${renderPreflightStatusLabel(result.status)}] ${result.id}: ${result.summary}`);
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
function shouldSkipRunnerStartupPreflight() {
|
|
40
|
+
const value = process.env[exports.RUNNER_STARTUP_PREFLIGHT_SKIP_ENV]?.trim().toLowerCase();
|
|
41
|
+
return value === "1" || value === "true";
|
|
42
|
+
}
|
|
43
|
+
async function ensureRunnerStartupPreflight(cfg, overrides = {}) {
|
|
44
|
+
if (shouldSkipRunnerStartupPreflight()) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const summary = await runRunnerPreflight({ cfg }, overrides);
|
|
48
|
+
if (summary.passed) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`${formatRunnerPreflightSummary(summary)}\n` +
|
|
52
|
+
"Run `companyhelm-runner doctor` for details or `companyhelm-runner doctor fix` to try automatic fixes.");
|
|
53
|
+
}
|
|
@@ -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;
|