@realqiyan/agent-task 0.0.1-rc.1 → 0.0.1-rc.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/README.md +8 -5
- package/bin/agent-task.mjs +23 -41
- package/dist/application/launcher.js +215 -0
- package/dist/application/main.js +122 -0
- package/dist/application/startup.js +51 -0
- package/dist/application/task-runtime.js +42 -0
- package/dist/configuration/contract.js +28 -0
- package/dist/configuration/environment.js +32 -0
- package/dist/configuration/files.js +43 -0
- package/dist/configuration/management.js +6 -0
- package/dist/configuration/model.js +2 -0
- package/dist/configuration/policy.js +29 -0
- package/dist/configuration/ports.js +2 -0
- package/dist/{settings.js → configuration/settings.js} +97 -42
- package/dist/executors/environment.js +10 -0
- package/dist/executors/model.js +2 -0
- package/dist/{permissions.js → executors/permissions.js} +3 -4
- package/dist/executors/registry.js +29 -0
- package/dist/executors/validation.js +10 -0
- package/dist/{lock.js → infrastructure/lock.js} +17 -1
- package/dist/{private-files.js → infrastructure/private-files.js} +17 -9
- package/dist/infrastructure/sqlite.js +19 -0
- package/dist/infrastructure/version.js +3 -0
- package/dist/{acp.js → integrations/acp/connection.js} +14 -11
- package/dist/{node-execution.js → integrations/acp/execution.js} +3 -4
- package/dist/integrations/acp/runtime.js +28 -0
- package/dist/{worker.js → integrations/acp/worker.js} +5 -4
- package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
- package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
- package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
- package/dist/integrations/local-service/control.js +33 -0
- package/dist/protocol/cli/admin-client.js +45 -0
- package/dist/protocol/cli/commands.js +394 -0
- package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
- package/dist/protocol/http/server.js +111 -0
- package/dist/{web.js → protocol/http/web.js} +31 -18
- package/dist/{mcp.js → protocol/mcp.js} +13 -13
- package/dist/protocol/services.js +2 -0
- package/dist/protocol/settings-contract.js +40 -0
- package/dist/protocol/task-contract.js +28 -0
- package/dist/results/files.js +70 -0
- package/dist/results/ports.js +2 -0
- package/dist/results/service.js +42 -0
- package/dist/shared/errors.js +13 -0
- package/dist/shared/limits.js +3 -0
- package/dist/shared/time.js +2 -0
- package/dist/shared/validation.js +8 -0
- package/dist/tasks/definitions.js +83 -0
- package/dist/tasks/graph.js +23 -0
- package/dist/{human-input.js → tasks/human-input.js} +8 -7
- package/dist/tasks/model.js +2 -0
- package/dist/tasks/ports.js +2 -0
- package/dist/tasks/runs.js +263 -0
- package/dist/tasks/schedule.js +36 -0
- package/dist/tasks/sessions.js +62 -0
- package/dist/tasks/storage/records.js +45 -0
- package/dist/{task-storage.js → tasks/storage/store.js} +23 -36
- package/dist/tasks/supervisor.js +227 -0
- package/dist/tasks/validation.js +24 -0
- package/dist/web/api.js +1 -1
- package/dist/web/app.js +7 -0
- package/dist/web/settings-view.js +40 -28
- package/docs/architecture-guidelines.md +8 -6
- package/docs/cli.md +49 -0
- package/docs/deployment.md +32 -70
- package/docs/design-proposal.md +3 -3
- package/docs/release.md +17 -18
- package/docs/technical-design.md +47 -26
- package/docs/validation.md +57 -17
- package/docs/web-console.md +15 -19
- package/package.json +8 -8
- package/scripts/install-lib.mjs +7 -105
- package/scripts/mcp-call.mjs +1 -1
- package/scripts/probe-agent.mjs +11 -5
- package/scripts/service-runner.mjs +2 -7
- package/scripts/setup-wizard.mjs +3 -8
- package/scripts/setup.mjs +31 -40
- package/deploy/agent-task.service.example +0 -17
- package/dist/admin.js +0 -31
- package/dist/config.js +0 -113
- package/dist/environment.js +0 -23
- package/dist/main.js +0 -131
- package/dist/model.js +0 -28
- package/dist/results.js +0 -85
- package/dist/settings-contract.js +0 -16
- package/dist/task-contract.js +0 -37
- package/dist/task-model.js +0 -2
- package/dist/tasks.js +0 -509
- package/dist/version.js +0 -3
- package/scripts/uninstall.mjs +0 -53
- /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@realqiyan/agent-task",
|
|
3
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"version": "0.0.1-rc.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "realqiyan",
|
|
6
6
|
"repository": {
|
|
@@ -21,11 +21,9 @@
|
|
|
21
21
|
"scripts/setup-wizard.mjs",
|
|
22
22
|
"scripts/install-lib.mjs",
|
|
23
23
|
"scripts/service-runner.mjs",
|
|
24
|
-
"scripts/uninstall.mjs",
|
|
25
24
|
"scripts/probe-agent.mjs",
|
|
26
25
|
"scripts/mcp-call.mjs",
|
|
27
26
|
"docs/*.md",
|
|
28
|
-
"deploy/*.example",
|
|
29
27
|
"config.example.json",
|
|
30
28
|
"LICENSE"
|
|
31
29
|
],
|
|
@@ -34,24 +32,26 @@
|
|
|
34
32
|
"registry": "https://registry.npmjs.org/",
|
|
35
33
|
"tag": "latest"
|
|
36
34
|
},
|
|
37
|
-
"os": [
|
|
35
|
+
"os": [
|
|
36
|
+
"linux",
|
|
37
|
+
"darwin"
|
|
38
|
+
],
|
|
38
39
|
"type": "module",
|
|
39
40
|
"description": "Durable intelligent task coordination over MCP with ACP executors",
|
|
40
41
|
"engines": {
|
|
41
42
|
"node": ">=22.19.0"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
|
-
"check": "tsc --noEmit",
|
|
45
|
+
"check": "tsc --noEmit && node scripts/check-boundaries.mjs",
|
|
45
46
|
"build": "rm -rf dist && tsc && node scripts/copy-web.mjs",
|
|
46
47
|
"test": "npm run build && node --test --test-concurrency=1 tests/*.test.mjs",
|
|
47
48
|
"test:e2e": "npm run build && node --test --test-concurrency=1 tests/e2e/deepseek-acp.test.mjs",
|
|
48
49
|
"test:e2e:human": "npm run build && node scripts/run-pi-human-e2e.mjs",
|
|
49
|
-
"start": "node
|
|
50
|
+
"start": "node bin/agent-task.mjs start",
|
|
50
51
|
"setup": "node bin/agent-task.mjs setup",
|
|
51
52
|
"prepack": "npm run build",
|
|
52
53
|
"pack:release": "node scripts/pack-release.mjs",
|
|
53
|
-
"test:install": "node scripts/verify-install.mjs"
|
|
54
|
-
"uninstall:service": "node scripts/uninstall.mjs"
|
|
54
|
+
"test:install": "node scripts/verify-install.mjs"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@agentclientprotocol/sdk": "2.0.0",
|
package/scripts/install-lib.mjs
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { DatabaseSync } from "
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync, realpathSync, readdirSync, unlinkSync,
|
|
2
|
+
import { DatabaseSync } from "../dist/infrastructure/sqlite.js";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync, realpathSync, readdirSync, unlinkSync, openSync, closeSync, fsyncSync, renameSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve, isAbsolute, basename } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
8
7
|
|
|
9
8
|
export const packageRoot = realpathSync(fileURLToPath(new URL("..", import.meta.url)));
|
|
10
9
|
|
|
11
|
-
export const label = "com.agent-task.service";
|
|
12
|
-
export const unit = "agent-task.service";
|
|
13
10
|
export const packages = {
|
|
14
11
|
pi: ["@earendil-works/pi-coding-agent@1.3.0", "pi-acp@0.0.34"],
|
|
15
12
|
codex: ["@agentclientprotocol/codex-acp@2.2.2"],
|
|
13
|
+
claude: ["@zed-industries/claude-agent-acp@0.23.1"],
|
|
16
14
|
opencode: ["opencode-ai@1.19.3"],
|
|
17
15
|
};
|
|
18
16
|
export function argumentsFor(argv) {
|
|
19
|
-
const flags = new Set(["
|
|
17
|
+
const flags = new Set(["check", "probe", "resume", "help"]), options = {};
|
|
20
18
|
for (let i = 0; i < argv.length; i++) {
|
|
21
19
|
const key = argv[i].replace(/^--/, "");
|
|
22
20
|
if (!argv[i].startsWith("--") || key in options) throw new Error("Invalid or repeated option");
|
|
@@ -59,7 +57,7 @@ export function run(command, args, options = {}) {
|
|
|
59
57
|
export function operationLock(root) {
|
|
60
58
|
const db = new DatabaseSync(join(root, "setup-lock.sqlite"));
|
|
61
59
|
try { db.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE"); }
|
|
62
|
-
catch { db.close(); throw new Error("Another setup
|
|
60
|
+
catch { db.close(); throw new Error("Another setup operation owns this installation"); }
|
|
63
61
|
return () => db.close();
|
|
64
62
|
}
|
|
65
63
|
export function readManifest(root) {
|
|
@@ -85,107 +83,11 @@ export function saveManifest(root, manifest) {
|
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
export function runtimeBaseEnv(manifest) {
|
|
88
|
-
//
|
|
89
|
-
return { HOME: manifest.home, PATH: [join(manifest.root, "components/node_modules/.bin"), dirname(manifest.node), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].join(":"), LANG: "en_US.UTF-8" };
|
|
90
|
-
}
|
|
91
|
-
export function runtimeEnv(manifest) {
|
|
92
|
-
const values = json(join(manifest.root, "environment.json"));
|
|
93
|
-
if (!values || typeof values !== "object" || Array.isArray(values) || Object.entries(values).some(([key, value]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || ["__proto__", "constructor", "prototype"].includes(key) || typeof value !== "string" || value.includes("\0"))) throw new Error("Invalid environment.json");
|
|
94
|
-
return { ...runtimeBaseEnv(manifest), ...values };
|
|
95
|
-
}
|
|
96
|
-
const xml = value => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
97
|
-
const systemdQuote = value => '"' + value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("$", "$$") + '"';
|
|
98
|
-
export function serviceDefinition(manifest) {
|
|
99
|
-
const runner = join(manifest.app, "scripts/service-runner.mjs");
|
|
100
|
-
if (manifest.platform === "linux") return {
|
|
101
|
-
path: join(manifest.home, ".config/systemd/user", unit),
|
|
102
|
-
text: `[Unit]\nDescription=Agent Task (managed by setup)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=${[manifest.node, runner, manifest.root].map(systemdQuote).join(" ")}\nRestart=on-failure\nRestartSec=5\nTimeoutStopSec=20\nKillMode=control-group\nUMask=0077\n\n[Install]\nWantedBy=default.target\n`,
|
|
103
|
-
};
|
|
104
|
-
return { path: join(manifest.home, "Library/LaunchAgents", label + ".plist"), text:
|
|
105
|
-
`<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>${label}</string>\n<key>ProgramArguments</key><array>${[manifest.node, runner, manifest.root].map(value => `<string>${xml(value)}</string>`).join("")}</array>\n<key>RunAtLoad</key><true/>\n<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n<key>ThrottleInterval</key><integer>5</integer>\n<key>ExitTimeOut</key><integer>20</integer>\n<key>AbandonProcessGroup</key><false/>\n<key>Umask</key><integer>63</integer>\n<key>StandardOutPath</key><string>${xml(join(manifest.root, "service.log"))}</string>\n<key>StandardErrorPath</key><string>${xml(join(manifest.root, "service-error.log"))}</string>\n</dict></plist>\n` };
|
|
106
|
-
}
|
|
107
|
-
export function serviceEnv() {
|
|
108
|
-
const keys = ["HOME", "PATH", "LANG", "LC_ALL", "DBUS_SESSION_BUS_ADDRESS"];
|
|
109
|
-
return { ...Object.fromEntries(keys.filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]])),
|
|
110
|
-
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${process.getuid()}` };
|
|
111
|
-
}
|
|
112
|
-
export async function checkHealth(config, env, waitMs = 5000) {
|
|
113
|
-
const token = env[config.tokenEnv ?? "AGENT_TASK_TOKEN"];
|
|
114
|
-
if (!token || token.length < 32) throw new Error("A valid service token is required for the health check");
|
|
115
|
-
const deadline = Date.now() + waitMs;
|
|
116
|
-
do {
|
|
117
|
-
try {
|
|
118
|
-
const response = await fetch(`http://127.0.0.1:${config.port}/mcp`, {
|
|
119
|
-
method: "POST", headers: { Authorization: "Bearer " + token, "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
|
|
120
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "service_status", arguments: {} } }),
|
|
121
|
-
signal: AbortSignal.timeout(1000),
|
|
122
|
-
});
|
|
123
|
-
const body = await response.json();
|
|
124
|
-
if (response.ok && body.result?.structuredContent?.ok && body.result.structuredContent.data?.running) return;
|
|
125
|
-
} catch { /* Startup can take several seconds; never log the authenticated request. */ }
|
|
126
|
-
await delay(200);
|
|
127
|
-
} while (Date.now() < deadline);
|
|
128
|
-
throw new Error("Authenticated MCP health check failed; inspect the service manager and private configuration");
|
|
129
|
-
}
|
|
130
|
-
export function verifyServiceFile(manifest) {
|
|
131
|
-
const definition = serviceDefinition(manifest);
|
|
132
|
-
if (existsSync(definition.path)) {
|
|
133
|
-
privateFile(definition.path);
|
|
134
|
-
if (readFileSync(definition.path, "utf8") !== definition.text) throw new Error("Service file differs; refusing to overwrite or remove it: " + definition.path);
|
|
135
|
-
}
|
|
136
|
-
return definition;
|
|
137
|
-
}
|
|
138
|
-
export function activate(manifest) {
|
|
139
|
-
const definition = verifyServiceFile(manifest);
|
|
140
|
-
if (!existsSync(definition.path)) createFile(definition.path, definition.text);
|
|
141
|
-
if (manifest.platform === "linux") {
|
|
142
|
-
run("systemctl", ["--user", "daemon-reload"], { env: serviceEnv() });
|
|
143
|
-
run("systemctl", ["--user", "enable", "--now", unit], { env: serviceEnv() });
|
|
144
|
-
run("systemctl", ["--user", "is-active", unit], { env: serviceEnv() });
|
|
145
|
-
} else {
|
|
146
|
-
const domain = `gui/${process.getuid()}`;
|
|
147
|
-
const active = spawnSync("launchctl", ["print", domain + "/" + label], { stdio: "ignore" });
|
|
148
|
-
if (active.status !== 0) run("launchctl", ["bootstrap", domain, definition.path]);
|
|
149
|
-
run("launchctl", ["kickstart", domain + "/" + label]);
|
|
150
|
-
run("launchctl", ["print", domain + "/" + label]);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
export function deactivate(manifest) {
|
|
154
|
-
const definition = verifyServiceFile(manifest);
|
|
155
|
-
if (!existsSync(definition.path)) {
|
|
156
|
-
if (manifest.serviceInstalled) throw new Error("Managed service file is missing; verify it is unloaded before cleanup");
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (manifest.platform === "linux") {
|
|
160
|
-
run("systemctl", ["--user", "disable", "--now", unit], { env: serviceEnv() });
|
|
161
|
-
// Do not remove code while the service is still active or deactivating.
|
|
162
|
-
const state = run("systemctl", ["--user", "show", "--property=ActiveState", "--value", unit], { env: serviceEnv() });
|
|
163
|
-
if (!["inactive", "failed"].includes(state)) throw new Error("Service has not stopped");
|
|
164
|
-
} else {
|
|
165
|
-
const target = `gui/${process.getuid()}/${label}`;
|
|
166
|
-
const active = spawnSync("launchctl", ["print", target], { stdio: "ignore" });
|
|
167
|
-
if (active.status === 0) run("launchctl", ["bootout", target]);
|
|
168
|
-
if (spawnSync("launchctl", ["print", target], { stdio: "ignore" }).status === 0) throw new Error("LaunchAgent is still loaded");
|
|
169
|
-
}
|
|
170
|
-
unlinkSync(definition.path);
|
|
171
|
-
if (manifest.platform === "linux") run("systemctl", ["--user", "daemon-reload"], { env: serviceEnv() });
|
|
86
|
+
// Capture the OS user environment; the runtime isolates private service values before worker creation.
|
|
87
|
+
return { ...process.env, HOME: manifest.home, PATH: [join(manifest.root, "components/node_modules/.bin"), dirname(manifest.node), process.env.PATH ?? "", "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].join(":"), LANG: process.env.LANG ?? "en_US.UTF-8" };
|
|
172
88
|
}
|
|
173
89
|
export function inventory(root, relative) {
|
|
174
90
|
const path = join(root, relative), stat = lstatSync(path);
|
|
175
91
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return [relative];
|
|
176
92
|
return readdirSync(path).flatMap(name => inventory(root, join(relative, name))).concat(relative);
|
|
177
93
|
}
|
|
178
|
-
export function removeInventory(root, files) {
|
|
179
|
-
for (const relative of files) {
|
|
180
|
-
if (!/^components(\/|$)/.test(relative) || relative.split("/").includes("..")) throw new Error("Invalid component inventory");
|
|
181
|
-
const path = join(root, relative);
|
|
182
|
-
// Never follow a directory replaced by a symlink outside the installation.
|
|
183
|
-
try {
|
|
184
|
-
if (realpathSync(dirname(path)) !== dirname(path)) throw new Error("Component parent path changed; cleanup stopped");
|
|
185
|
-
const stat = lstatSync(path);
|
|
186
|
-
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
187
|
-
try { rmdirSync(path); } catch (error) { if (error.code !== "ENOTEMPTY") throw error; }
|
|
188
|
-
} else unlinkSync(path);
|
|
189
|
-
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
190
|
-
}
|
|
191
|
-
}
|
package/scripts/mcp-call.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
-
import { version } from "../dist/version.js";
|
|
3
|
+
import { version } from "../dist/infrastructure/version.js";
|
|
4
4
|
const [tool, input = "{}"] = process.argv.slice(2);
|
|
5
5
|
if (!tool || !process.env.AGENT_TASK_TOKEN) {
|
|
6
6
|
console.error("Usage: AGENT_TASK_TOKEN=... node scripts/mcp-call.mjs <tool|--list> '[JSON arguments]'\nAGENT_TASK_URL defaults to http://127.0.0.1:8765/mcp");
|
package/scripts/probe-agent.mjs
CHANGED
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { Readable, Writable } from "node:stream";
|
|
4
4
|
import * as acp from "@agentclientprotocol/sdk";
|
|
5
|
-
import { loadConfig
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { loadConfig } from "../dist/configuration/files.js";
|
|
6
|
+
import { executorEnvironment } from "../dist/executors/environment.js";
|
|
7
|
+
import { executorCommand } from "../dist/integrations/acp/runtime.js";
|
|
8
|
+
import { readEnvironment } from "../dist/configuration/files.js";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { terminateGroup } from "../dist/integrations/acp/connection.js";
|
|
11
|
+
import { version } from "../dist/infrastructure/version.js";
|
|
8
12
|
const [file, id] = process.argv.slice(2);
|
|
9
13
|
if (!file || !id) { console.error("Usage: node scripts/probe-agent.mjs <config.json> <executorId>"); process.exit(2); }
|
|
10
14
|
const config = loadConfig(file);
|
|
11
15
|
const executor = config.executors.find(value => value.id === id);
|
|
12
16
|
if (!executor) throw new Error("Executor not found");
|
|
13
|
-
const
|
|
14
|
-
|
|
17
|
+
const env = executorEnvironment(process.env, readEnvironment(join(config.dataDir, "environment.json")), config);
|
|
18
|
+
const launch = executorCommand(executor.type, env);
|
|
19
|
+
const child = spawn(launch.command, launch.args, {
|
|
20
|
+
cwd: executor.cwd, env, detached: true, stdio: ["pipe", "pipe", "pipe"],
|
|
15
21
|
});
|
|
16
22
|
child.stderr.resume();
|
|
17
23
|
let connection;
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
// Same process as the service: the service manager supervises the actual HTTP owner.
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { readManifest, runtimeBaseEnv } from "./install-lib.mjs";
|
|
2
|
+
import { launch } from "../dist/application/launcher.js";
|
|
4
3
|
|
|
5
4
|
process.umask(0o077);
|
|
6
5
|
const root = process.argv[2];
|
|
7
6
|
if (!root || process.argv.length !== 3) throw new Error("Expected installation home");
|
|
8
7
|
const manifest = readManifest(root);
|
|
9
8
|
if (manifest.phase !== "ready") throw new Error("Installation is not ready");
|
|
10
|
-
|
|
11
|
-
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
12
|
-
Object.assign(process.env, env);
|
|
13
|
-
process.argv = [process.execPath, join(manifest.app, "dist/main.js"), "--config", join(root, "config.json"), "--environment", join(root, "environment.json")];
|
|
14
|
-
await import("../dist/main.js");
|
|
9
|
+
await launch(root, runtimeBaseEnv(manifest));
|
package/scripts/setup-wizard.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
|
|
3
3
|
export async function setupWizard(options, root) {
|
|
4
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive setup requires a terminal; use --executors none|pi,codex,opencode and --cwd ABSOLUTE_DIR for noninteractive setup");
|
|
4
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive setup requires a terminal; use --executors none|pi,codex,claude,opencode and --cwd ABSOLUTE_DIR for noninteractive setup");
|
|
5
5
|
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
6
6
|
const abort = new AbortController();
|
|
7
7
|
input.on("SIGINT", () => abort.abort());
|
|
8
8
|
const ask = async (question, fallback) => (await input.question(`${question}${fallback ? " [" + fallback + "]" : ""}: `, { signal: abort.signal })).trim() || fallback;
|
|
9
9
|
try {
|
|
10
|
-
console.log(`Private installation: ${root}
|
|
11
|
-
options.executors ??= await ask("Executors to install (none or comma-separated pi,codex,opencode)", "none");
|
|
10
|
+
console.log(`Private installation: ${root}`);
|
|
11
|
+
options.executors ??= await ask("Executors to install (none or comma-separated pi,codex,claude,opencode)", "none");
|
|
12
12
|
options.cwd ??= await ask("Existing absolute workspace (outside installation and npm package)", process.cwd());
|
|
13
13
|
if (!options.provider) {
|
|
14
14
|
const provider = await ask("Coordinator provider (none/openai/anthropic/deepseek; configurable later in Web)", "none");
|
|
@@ -16,11 +16,6 @@ export async function setupWizard(options, root) {
|
|
|
16
16
|
}
|
|
17
17
|
if (options.provider) options.model ??= await ask("Coordinator model ID", "");
|
|
18
18
|
options.port ??= await ask("Loopback HTTP port", "8765");
|
|
19
|
-
if (!options["prepare-only"] && !options.service) {
|
|
20
|
-
const answer = await ask("Install and start the background service now? (yes/no)", "no");
|
|
21
|
-
if (!["yes", "no"].includes(answer)) throw new Error("Expected yes or no; setup cancelled");
|
|
22
|
-
if (answer === "yes") options.service = true;
|
|
23
|
-
}
|
|
24
19
|
if (await ask("Write private configuration and install selected components? (yes/no)", "no") !== "yes") throw new Error("Setup cancelled; no configuration written");
|
|
25
20
|
} catch (error) {
|
|
26
21
|
if (abort.signal.aborted) throw new Error("Setup cancelled; no configuration written");
|
package/scripts/setup.mjs
CHANGED
|
@@ -5,18 +5,21 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
import { argumentsFor, installRoot, packages, createFile, json, run, operationLock, readManifest, saveManifest,
|
|
8
|
-
|
|
8
|
+
runtimeBaseEnv, inventory, packageRoot } from "./install-lib.mjs";
|
|
9
9
|
import { setupWizard } from "./setup-wizard.mjs";
|
|
10
|
+
import { startupPaths } from "../dist/configuration/files.js";
|
|
11
|
+
import { readEnvironment } from "../dist/configuration/files.js";
|
|
12
|
+
import { executorEnvironment } from "../dist/executors/environment.js";
|
|
13
|
+
import { executorCommand } from "../dist/integrations/acp/runtime.js";
|
|
10
14
|
|
|
11
15
|
process.umask(0o077);
|
|
12
16
|
let unlock;
|
|
13
17
|
try {
|
|
14
18
|
const options = argumentsFor(process.argv.slice(2));
|
|
15
19
|
if (options.help) {
|
|
16
|
-
console.log("Usage: agent-task setup [--home ABSOLUTE_PRIVATE_DIR]\nInteractive on a terminal for a new installation. Noninteractive: --executors pi,codex,opencode|none --cwd ABSOLUTE_DIR [--provider openai|anthropic|deepseek --model ID] [--port 8765]\
|
|
20
|
+
console.log("Usage: agent-task setup [--home ABSOLUTE_PRIVATE_DIR]\nInteractive on a terminal for a new installation. Noninteractive: --executors pi,codex,claude,opencode|none --cwd ABSOLUTE_DIR [--provider openai|anthropic|deepseek --model ID] [--port 8765]\nExisting installation: --check [--probe] | --resume. No credentials accepted on the command line. Existing configuration is never replaced.");
|
|
17
21
|
process.exit(0);
|
|
18
22
|
}
|
|
19
|
-
if (options.service && (options["prepare-only"] || options.check)) throw new Error("--service cannot be combined with --prepare-only or --check");
|
|
20
23
|
if (options.probe && !options.check) throw new Error("--probe requires --check");
|
|
21
24
|
const root = installRoot(options), manifestPath = join(root, "installation.json");
|
|
22
25
|
if (!existsSync(manifestPath) && !options.check && !options.resume && (!options.executors || !options.cwd)) await setupWizard(options, root);
|
|
@@ -27,10 +30,10 @@ try {
|
|
|
27
30
|
if (existsSync(manifestPath)) {
|
|
28
31
|
manifest = readManifest(root);
|
|
29
32
|
if (["executors", "cwd", "provider", "model", "port"].some(key => options[key] !== undefined)) throw new Error("Installation already exists; initialization options cannot replace existing configuration");
|
|
30
|
-
if (manifest.phase !== "ready" && !options.resume) throw new Error("Incomplete
|
|
33
|
+
if (manifest.phase !== "ready" && !options.resume) throw new Error("Incomplete components; use --resume explicitly (configuration will be preserved)");
|
|
31
34
|
} else {
|
|
32
35
|
if (options.check || options.probe || options.resume) throw new Error("No installation to check/resume");
|
|
33
|
-
if (!options.executors || !options.cwd) throw new Error("Select --executors pi,codex,opencode|none and --cwd ABSOLUTE_DIR");
|
|
36
|
+
if (!options.executors || !options.cwd) throw new Error("Select --executors pi,codex,claude,opencode|none and --cwd ABSOLUTE_DIR");
|
|
34
37
|
const selected = options.executors === "none" ? [] : options.executors.split(",");
|
|
35
38
|
if (selected.some(id => !Object.hasOwn(packages, id)) || new Set(selected).size !== selected.length) throw new Error("Invalid executor selection");
|
|
36
39
|
if (!options.cwd.startsWith("/") || !lstatSync(realpathSync(options.cwd)).isDirectory()) throw new Error("cwd must be an existing absolute directory");
|
|
@@ -40,31 +43,28 @@ try {
|
|
|
40
43
|
const port = Number(options.port ?? 8765);
|
|
41
44
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid port");
|
|
42
45
|
// Reject unowned artifacts. This script never adopts or clears an existing installation.
|
|
43
|
-
for (const name of ["components", "
|
|
46
|
+
for (const name of ["components", "launch.json", "data"]) {
|
|
44
47
|
if (existsSync(join(root, name))) throw new Error("Existing unowned installation artifact: " + name);
|
|
45
48
|
}
|
|
46
49
|
manifest = { project: "agent-task", root, home: homedir(), platform: process.platform, node: process.execPath, app: packageRoot,
|
|
47
50
|
selected, cwd, port, coordinator: options.provider ? { provider: options.provider, model: options.model } : undefined,
|
|
48
|
-
phase: "installing",
|
|
51
|
+
phase: "installing", files: [] };
|
|
49
52
|
createFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
50
53
|
}
|
|
51
|
-
verifyServiceFile(manifest);
|
|
52
54
|
const app = manifest.app, components = join(root, "components");
|
|
53
|
-
if (!existsSync(join(app, "dist/main.js"))) throw new Error("Prebuilt runtime is missing; reinstall the npm package (source contributors: npm run build)");
|
|
55
|
+
if (!existsSync(join(app, "dist/application/main.js"))) throw new Error("Prebuilt runtime is missing; reinstall the npm package (source contributors: npm run build)");
|
|
54
56
|
if (manifest.phase !== "ready") {
|
|
55
|
-
if (manifest.serviceInstalled) throw new Error("Stop/uninstall the existing service before rebuilding components");
|
|
56
57
|
for (const name of ["components"]) {
|
|
57
58
|
if (!existsSync(join(root, name))) continue;
|
|
58
59
|
const known = new Set(manifest.files);
|
|
59
60
|
if (inventory(root, name).some(file => !known.has(file))) throw new Error("Uninventoried component files exist; refusing to overwrite them during resume");
|
|
60
61
|
}
|
|
61
62
|
try {
|
|
62
|
-
console.log("Using the prebuilt npm runtime. No source checkout or compilation required.");
|
|
63
63
|
mkdirSync(components, { recursive: true, mode: 0o700 });
|
|
64
64
|
if (!existsSync(join(components, "package.json"))) createFile(join(components, "package.json"), JSON.stringify({ private: true }));
|
|
65
65
|
const dependencies = manifest.selected.flatMap(id => packages[id]);
|
|
66
66
|
if (dependencies.length) {
|
|
67
|
-
console.log("Installing selected ACP executors
|
|
67
|
+
console.log("Installing selected ACP executors...");
|
|
68
68
|
const installArgs = existsSync(join(components, "package-lock.json"))
|
|
69
69
|
? ["ci", "--ignore-scripts", "--no-audit", "--no-fund"]
|
|
70
70
|
: ["install", "--save-exact", "--ignore-scripts", "--no-audit", "--no-fund", ...dependencies];
|
|
@@ -75,52 +75,43 @@ try {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
mkdirSync(join(root, "data/results"), { recursive: true, mode: 0o700 });
|
|
78
|
-
|
|
79
|
-
if (!existsSync(join(root, "
|
|
78
|
+
const dataDir = join(root, "data");
|
|
79
|
+
if (!existsSync(join(root, "launch.json"))) createFile(join(root, "launch.json"), JSON.stringify({ dataDir }) + "\n");
|
|
80
|
+
if (!existsSync(join(dataDir, "environment.json"))) createFile(join(dataDir, "environment.json"), JSON.stringify({ AGENT_TASK_TOKEN: randomBytes(32).toString("hex"), AGENT_TASK_ADMIN_TOKEN: randomBytes(32).toString("hex") }, null, 2) + "\n");
|
|
81
|
+
if (!existsSync(join(dataDir, "config.json"))) createFile(join(dataDir, "config.json"), JSON.stringify({ dataDir, port: manifest.port,
|
|
80
82
|
tokenEnv: "AGENT_TASK_TOKEN", coordinator: manifest.coordinator,
|
|
81
|
-
executors: manifest.selected.map(id => ({ id,
|
|
82
|
-
args: id === "opencode" ? ["acp"] : [], cwd: manifest.cwd, envKeys: ["HOME"],
|
|
83
|
-
permissionAllowlist: [], unattendedApproved: false, resultReadRoots: [] })) }, null, 2) + "\n");
|
|
83
|
+
executors: manifest.selected.map(id => ({ id, type: id, note: "", cwd: manifest.cwd, unattendedApproved: false })) }, null, 2) + "\n");
|
|
84
84
|
manifest.phase = "ready";
|
|
85
|
+
delete manifest.port; delete manifest.cwd; delete manifest.coordinator;
|
|
85
86
|
} finally {
|
|
86
87
|
manifest.files = ["components"].filter(name => existsSync(join(root, name))).flatMap(name => inventory(root, name));
|
|
87
88
|
saveManifest(root, manifest);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
const env =
|
|
91
|
-
//
|
|
92
|
-
run(manifest.node, ["--input-type=module", "-e", `import {loadConfig} from ${JSON.stringify(pathToFileURL(join(app, "dist/
|
|
93
|
-
const config = json(
|
|
94
|
-
const { executorEnv } = await import(pathToFileURL(join(app, "dist/config.js")).href);
|
|
91
|
+
const paths = startupPaths(root), env = runtimeBaseEnv(manifest);
|
|
92
|
+
// Use the same native launch environment; private values are read separately, never passed to probes.
|
|
93
|
+
run(manifest.node, ["--input-type=module", "-e", `import {loadConfig} from ${JSON.stringify(pathToFileURL(join(app, "dist/configuration/files.js")).href)}; loadConfig(process.argv[1]);`, paths.configPath], { env });
|
|
94
|
+
const config = json(paths.configPath);
|
|
95
95
|
for (const id of manifest.selected) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
run(join(components, "node_modules/.bin", binary), ["--version"], { env: cliEnv, timeout: 20000 });
|
|
96
|
+
const cliEnv = executorEnvironment(env, readEnvironment(paths.environmentPath), config);
|
|
97
|
+
executorCommand(id, cliEnv); // No native process is launched without an explicit --probe.
|
|
99
98
|
}
|
|
100
|
-
const report = { platform: process.platform, dependencies: "ready", configuration: "valid",
|
|
99
|
+
const report = { platform: process.platform, dependencies: "ready", configuration: "valid",
|
|
101
100
|
coordinator: config.coordinator ? "configured; authentication not verified" : "not configured; task admission disabled",
|
|
102
101
|
executors: config.executors.map(executor => ({ id: executor.id, authorized: executor.unattendedApproved,
|
|
103
102
|
handshake: "not checked", execution: "not verified by setup" })) };
|
|
104
103
|
if (options.probe) {
|
|
105
104
|
for (const entry of report.executors) {
|
|
106
|
-
try { run(manifest.node, [join(app, "scripts/probe-agent.mjs"),
|
|
105
|
+
try { run(manifest.node, [join(app, "scripts/probe-agent.mjs"), paths.configPath, entry.id], { env, timeout: 20000 }); entry.handshake = "passed (initialize only)"; }
|
|
107
106
|
catch { entry.handshake = "failed; inspect native dependencies/configuration separately"; process.exitCode = 1; }
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
|
-
if (options.service) {
|
|
111
|
-
// Record intent before activating so a failed service start is still owned by uninstall.
|
|
112
|
-
manifest.serviceInstalled = true; saveManifest(root, manifest);
|
|
113
|
-
activate(manifest);
|
|
114
|
-
report.background = "service manager registered";
|
|
115
|
-
}
|
|
116
|
-
if (manifest.serviceInstalled) {
|
|
117
|
-
try {
|
|
118
|
-
await checkHealth(config, env, 15000);
|
|
119
|
-
report.background = "authenticated MCP health check passed";
|
|
120
|
-
} catch { report.background = "not ready; run --check after startup and inspect the service manager"; process.exitCode = 1; }
|
|
121
|
-
}
|
|
122
109
|
console.log(JSON.stringify(report, null, 2));
|
|
123
|
-
|
|
110
|
+
if (!options.check) {
|
|
111
|
+
console.log(`Config: ${paths.configPath}\nCredentials: ${paths.environmentPath}\nConsole: http://127.0.0.1:${config.port}/admin/\nSign in with AGENT_TASK_ADMIN_TOKEN from the credentials file.`);
|
|
112
|
+
if (config.executors.length) console.log("Prepare native executor authentication and review permissions before approving unattended execution.");
|
|
113
|
+
console.log(`Start: agent-task start --home '${root.replaceAll("'", "'\"'\"'")}'`);
|
|
114
|
+
}
|
|
124
115
|
} catch (error) {
|
|
125
116
|
console.error(error.message); process.exitCode = 1;
|
|
126
117
|
} finally { unlock?.(); }
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
[Unit]
|
|
2
|
-
Description=Agent Task local service
|
|
3
|
-
StartLimitIntervalSec=60
|
|
4
|
-
StartLimitBurst=3
|
|
5
|
-
|
|
6
|
-
[Service]
|
|
7
|
-
Type=simple
|
|
8
|
-
WorkingDirectory=/absolute/path/to/agent-task
|
|
9
|
-
ExecStart=/absolute/path/to/node /absolute/path/to/agent-task/dist/main.js --config %h/.config/agent-task/config.json --environment %h/.config/agent-task/environment.json
|
|
10
|
-
Restart=on-failure
|
|
11
|
-
RestartSec=5
|
|
12
|
-
TimeoutStopSec=15
|
|
13
|
-
KillMode=control-group
|
|
14
|
-
UMask=0077
|
|
15
|
-
|
|
16
|
-
[Install]
|
|
17
|
-
WantedBy=default.target
|
package/dist/admin.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { loadConfig, validateConfig } from "./config.js";
|
|
4
|
-
import { replacePrivateText } from "./private-files.js";
|
|
5
|
-
import { executorSchema } from "./model.js";
|
|
6
|
-
import { acquireLock } from "./lock.js";
|
|
7
|
-
process.umask(0o077);
|
|
8
|
-
const [action, configPath, profilePath] = process.argv.slice(2);
|
|
9
|
-
if (!configPath || !["register", "list"].includes(action ?? "") || (action === "register" && !profilePath)) {
|
|
10
|
-
throw new Error("Usage: node dist/admin.js list CONFIG | register CONFIG PROFILE.json (service stopped)");
|
|
11
|
-
}
|
|
12
|
-
const path = resolve(configPath), config = loadConfig(path);
|
|
13
|
-
const release = await acquireLock(config.dataDir);
|
|
14
|
-
try {
|
|
15
|
-
if (action === "list") {
|
|
16
|
-
console.log(JSON.stringify(config.executors.map(({ id, command, args, cwd, envKeys, permissionAllowlist, unattendedApproved, resultReadRoots }) => ({ id, transport: "stdio", command, args, cwd, envKeys, permissionAllowlist, unattendedApproved, resultReadRoots })), null, 2));
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
const profile = executorSchema.parse(JSON.parse(readFileSync(resolve(profilePath), "utf8")));
|
|
20
|
-
if (config.executors.some(item => item.id === profile.id))
|
|
21
|
-
throw new Error("Executor ID already registered; edit reviewed config while stopped to replace");
|
|
22
|
-
config.executors.push(profile);
|
|
23
|
-
// Validate the complete candidate before publication, using the same rules as startup and the console.
|
|
24
|
-
replacePrivateText(path, JSON.stringify(validateConfig(config), null, 2) + "\n");
|
|
25
|
-
console.log(JSON.stringify({ registered: profile.id, transport: "stdio" }));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
finally {
|
|
29
|
-
await release();
|
|
30
|
-
}
|
|
31
|
-
//# sourceMappingURL=admin.js.map
|
package/dist/config.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { readFileSync, realpathSync, statSync, accessSync, constants } from "node:fs";
|
|
2
|
-
import { basename, delimiter, isAbsolute, join } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { executorSchema } from "./model.js";
|
|
5
|
-
export const ADMIN_TOKEN_ENV = "AGENT_TASK_ADMIN_TOKEN";
|
|
6
|
-
const envKey = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
7
|
-
const baseEnvKeys = ["PATH", "LANG", "LC_ALL", "TZ"];
|
|
8
|
-
const modelId = z.string().min(1).max(200);
|
|
9
|
-
const coordinatorSchema = z.discriminatedUnion("provider", [
|
|
10
|
-
z.object({ provider: z.enum(["openai", "anthropic", "deepseek"]), model: modelId }).strict(),
|
|
11
|
-
z.object({
|
|
12
|
-
provider: z.literal("openai-compatible"), model: modelId,
|
|
13
|
-
api: z.enum(["openai-completions", "openai-responses"]).default("openai-completions"),
|
|
14
|
-
baseUrl: z.string().url().refine(value => {
|
|
15
|
-
const url = new URL(value);
|
|
16
|
-
return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
|
|
17
|
-
}, "baseUrl must be HTTP(S) without credentials, query or fragment"),
|
|
18
|
-
apiKeyEnv: envKey.refine(key => !baseEnvKeys.includes(key), "Credential must not use a base environment key"),
|
|
19
|
-
contextWindow: z.number().int().positive(),
|
|
20
|
-
maxTokens: z.number().int().positive(),
|
|
21
|
-
reasoning: z.boolean().default(false),
|
|
22
|
-
}).strict(),
|
|
23
|
-
]).refine(value => value.provider !== "openai-compatible" || value.maxTokens <= value.contextWindow, "maxTokens must not exceed contextWindow");
|
|
24
|
-
export const configSchema = z.object({
|
|
25
|
-
dataDir: z.string().min(1),
|
|
26
|
-
port: z.number().int().min(0).max(65535).default(8765),
|
|
27
|
-
tokenEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).default("AGENT_TASK_TOKEN"),
|
|
28
|
-
coordinator: coordinatorSchema.optional(),
|
|
29
|
-
executors: z.array(executorSchema).max(100).default([]),
|
|
30
|
-
}).strict();
|
|
31
|
-
export function coordinatorCredentialEnv(config) {
|
|
32
|
-
switch (config.provider) {
|
|
33
|
-
case "openai": return "OPENAI_API_KEY";
|
|
34
|
-
case "anthropic": return "ANTHROPIC_API_KEY";
|
|
35
|
-
case "deepseek": return "DEEPSEEK_API_KEY";
|
|
36
|
-
case "openai-compatible": return config.apiKeyEnv;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export function loadConfig(path, environment = process.env) {
|
|
40
|
-
let value;
|
|
41
|
-
try {
|
|
42
|
-
value = JSON.parse(readFileSync(path, "utf8"));
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
throw new Error("Cannot read configuration JSON");
|
|
46
|
-
}
|
|
47
|
-
return validateConfig(value, environment);
|
|
48
|
-
}
|
|
49
|
-
export function validateConfig(value, environment = process.env) {
|
|
50
|
-
const config = configSchema.parse(value);
|
|
51
|
-
if (!isAbsolute(config.dataDir))
|
|
52
|
-
throw new Error("dataDir must be absolute");
|
|
53
|
-
const coordinatorKey = config.coordinator ? coordinatorCredentialEnv(config.coordinator) : undefined;
|
|
54
|
-
if ([...baseEnvKeys, ADMIN_TOKEN_ENV, "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY", coordinatorKey].includes(config.tokenEnv))
|
|
55
|
-
throw new Error("tokenEnv must be separate from base environment, administrator and model credentials");
|
|
56
|
-
if (coordinatorKey === ADMIN_TOKEN_ENV)
|
|
57
|
-
throw new Error("Administrator token must not be used as a model credential");
|
|
58
|
-
const ids = new Set();
|
|
59
|
-
for (const executor of config.executors) {
|
|
60
|
-
if (ids.has(executor.id))
|
|
61
|
-
throw new Error("Duplicate executor ID");
|
|
62
|
-
ids.add(executor.id);
|
|
63
|
-
if (!isAbsolute(executor.command) || !isAbsolute(executor.cwd))
|
|
64
|
-
throw new Error("Executor command and cwd must be absolute");
|
|
65
|
-
if (basename(executor.command) === "pi") {
|
|
66
|
-
throw new Error(`Executor ${executor.id}: pi CLI is not an ACP server (including --mode rpc). Install pi-acp with npm install -g pi-acp@0.0.34 and configure its absolute executable path.`);
|
|
67
|
-
}
|
|
68
|
-
const piCommandOverride = executor.envKeys.includes("PI_ACP_PI_COMMAND") && environment.PI_ACP_PI_COMMAND;
|
|
69
|
-
if (basename(executor.command) === "pi-acp" && !piCommandOverride && !(environment.PATH ?? "").split(delimiter).some(directory => {
|
|
70
|
-
if (!isAbsolute(directory))
|
|
71
|
-
return false;
|
|
72
|
-
try {
|
|
73
|
-
const file = join(directory, "pi");
|
|
74
|
-
accessSync(file, constants.X_OK);
|
|
75
|
-
return statSync(file).isFile();
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
})) {
|
|
81
|
-
throw new Error(`Executor ${executor.id}: pi-acp requires pi on the executor PATH. Install/configure pi before starting the service.`);
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
executor.command = realpathSync(executor.command);
|
|
85
|
-
accessSync(executor.command, constants.X_OK);
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
throw new Error(`Executor ${executor.id}: ACP executable is missing or not executable. Check the administrator-configured absolute command path. For Pi, install npm install -g pi-acp@0.0.34 and use command -v pi-acp to locate it; pi must also be on the executor PATH.`);
|
|
89
|
-
}
|
|
90
|
-
executor.cwd = realpathSync(executor.cwd);
|
|
91
|
-
if (!statSync(executor.command).isFile() || !statSync(executor.cwd).isDirectory())
|
|
92
|
-
throw new Error("Invalid executor command or cwd");
|
|
93
|
-
for (const root of executor.resultReadRoots) {
|
|
94
|
-
if (!isAbsolute(root) || realpathSync(root) !== root || !statSync(root).isDirectory())
|
|
95
|
-
throw new Error("Result read roots must be canonical absolute directories");
|
|
96
|
-
}
|
|
97
|
-
if (executor.envKeys.includes(config.tokenEnv) || executor.envKeys.includes(ADMIN_TOKEN_ENV))
|
|
98
|
-
throw new Error("Service or administrator token must not be passed to executors");
|
|
99
|
-
if (coordinatorKey && executor.envKeys.includes(coordinatorKey))
|
|
100
|
-
throw new Error("Coordinator credential must not be passed to executors; use separate executor credentials");
|
|
101
|
-
}
|
|
102
|
-
return config;
|
|
103
|
-
}
|
|
104
|
-
/** No shell or wholesale inheritance; secrets are references to administrator-selected environment keys. */
|
|
105
|
-
export function executorEnv(keys, source = process.env) {
|
|
106
|
-
const env = {};
|
|
107
|
-
for (const key of new Set(["PATH", "LANG", "LC_ALL", "TZ", ...keys])) {
|
|
108
|
-
if (key !== ADMIN_TOKEN_ENV && source[key] !== undefined)
|
|
109
|
-
env[key] = source[key];
|
|
110
|
-
}
|
|
111
|
-
return env;
|
|
112
|
-
}
|
|
113
|
-
//# sourceMappingURL=config.js.map
|
package/dist/environment.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { readPrivateText } from "./private-files.js";
|
|
2
|
-
export function parseEnvironment(text) {
|
|
3
|
-
let value;
|
|
4
|
-
try {
|
|
5
|
-
value = JSON.parse(text);
|
|
6
|
-
}
|
|
7
|
-
catch {
|
|
8
|
-
throw new Error("Invalid environment file JSON");
|
|
9
|
-
}
|
|
10
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.entries(value).some(([key, item]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || ["__proto__", "constructor", "prototype"].includes(key) ||
|
|
11
|
-
typeof item !== "string" || item.includes("\0")))
|
|
12
|
-
throw new Error("Invalid environment file");
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
export function readEnvironment(path) {
|
|
16
|
-
return parseEnvironment(readPrivateText(path));
|
|
17
|
-
}
|
|
18
|
-
/** Explicit file mode never falls back to inherited shell credentials. */
|
|
19
|
-
export function baseEnvironment(source) {
|
|
20
|
-
return Object.fromEntries(["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR"]
|
|
21
|
-
.filter(key => source[key] !== undefined).map(key => [key, source[key]]));
|
|
22
|
-
}
|
|
23
|
-
//# sourceMappingURL=environment.js.map
|