@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.
Files changed (91) hide show
  1. package/README.md +8 -5
  2. package/bin/agent-task.mjs +23 -41
  3. package/dist/application/launcher.js +215 -0
  4. package/dist/application/main.js +122 -0
  5. package/dist/application/startup.js +51 -0
  6. package/dist/application/task-runtime.js +42 -0
  7. package/dist/configuration/contract.js +28 -0
  8. package/dist/configuration/environment.js +32 -0
  9. package/dist/configuration/files.js +43 -0
  10. package/dist/configuration/management.js +6 -0
  11. package/dist/configuration/model.js +2 -0
  12. package/dist/configuration/policy.js +29 -0
  13. package/dist/configuration/ports.js +2 -0
  14. package/dist/{settings.js → configuration/settings.js} +97 -42
  15. package/dist/executors/environment.js +10 -0
  16. package/dist/executors/model.js +2 -0
  17. package/dist/{permissions.js → executors/permissions.js} +3 -4
  18. package/dist/executors/registry.js +29 -0
  19. package/dist/executors/validation.js +10 -0
  20. package/dist/{lock.js → infrastructure/lock.js} +17 -1
  21. package/dist/{private-files.js → infrastructure/private-files.js} +17 -9
  22. package/dist/infrastructure/sqlite.js +19 -0
  23. package/dist/infrastructure/version.js +3 -0
  24. package/dist/{acp.js → integrations/acp/connection.js} +14 -11
  25. package/dist/{node-execution.js → integrations/acp/execution.js} +3 -4
  26. package/dist/integrations/acp/runtime.js +28 -0
  27. package/dist/{worker.js → integrations/acp/worker.js} +5 -4
  28. package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
  29. package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
  30. package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
  31. package/dist/integrations/local-service/control.js +33 -0
  32. package/dist/protocol/cli/admin-client.js +45 -0
  33. package/dist/protocol/cli/commands.js +394 -0
  34. package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
  35. package/dist/protocol/http/server.js +111 -0
  36. package/dist/{web.js → protocol/http/web.js} +31 -18
  37. package/dist/{mcp.js → protocol/mcp.js} +13 -13
  38. package/dist/protocol/services.js +2 -0
  39. package/dist/protocol/settings-contract.js +40 -0
  40. package/dist/protocol/task-contract.js +28 -0
  41. package/dist/results/files.js +70 -0
  42. package/dist/results/ports.js +2 -0
  43. package/dist/results/service.js +42 -0
  44. package/dist/shared/errors.js +13 -0
  45. package/dist/shared/limits.js +3 -0
  46. package/dist/shared/time.js +2 -0
  47. package/dist/shared/validation.js +8 -0
  48. package/dist/tasks/definitions.js +83 -0
  49. package/dist/tasks/graph.js +23 -0
  50. package/dist/{human-input.js → tasks/human-input.js} +8 -7
  51. package/dist/tasks/model.js +2 -0
  52. package/dist/tasks/ports.js +2 -0
  53. package/dist/tasks/runs.js +263 -0
  54. package/dist/tasks/schedule.js +36 -0
  55. package/dist/tasks/sessions.js +62 -0
  56. package/dist/tasks/storage/records.js +45 -0
  57. package/dist/{task-storage.js → tasks/storage/store.js} +23 -36
  58. package/dist/tasks/supervisor.js +227 -0
  59. package/dist/tasks/validation.js +24 -0
  60. package/dist/web/api.js +1 -1
  61. package/dist/web/app.js +7 -0
  62. package/dist/web/settings-view.js +40 -28
  63. package/docs/architecture-guidelines.md +8 -6
  64. package/docs/cli.md +49 -0
  65. package/docs/deployment.md +32 -70
  66. package/docs/design-proposal.md +3 -3
  67. package/docs/release.md +17 -18
  68. package/docs/technical-design.md +47 -26
  69. package/docs/validation.md +57 -17
  70. package/docs/web-console.md +15 -19
  71. package/package.json +8 -8
  72. package/scripts/install-lib.mjs +7 -105
  73. package/scripts/mcp-call.mjs +1 -1
  74. package/scripts/probe-agent.mjs +11 -5
  75. package/scripts/service-runner.mjs +2 -7
  76. package/scripts/setup-wizard.mjs +3 -8
  77. package/scripts/setup.mjs +31 -40
  78. package/deploy/agent-task.service.example +0 -17
  79. package/dist/admin.js +0 -31
  80. package/dist/config.js +0 -113
  81. package/dist/environment.js +0 -23
  82. package/dist/main.js +0 -131
  83. package/dist/model.js +0 -28
  84. package/dist/results.js +0 -85
  85. package/dist/settings-contract.js +0 -16
  86. package/dist/task-contract.js +0 -37
  87. package/dist/task-model.js +0 -2
  88. package/dist/tasks.js +0 -509
  89. package/dist/version.js +0 -3
  90. package/scripts/uninstall.mjs +0 -53
  91. /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
package/README.md CHANGED
@@ -8,8 +8,9 @@
8
8
 
9
9
  | 目的 | 入口 |
10
10
  | --- | --- |
11
- | 安装、认证、后台运行、卸载 | [部署说明](docs/deployment.md) |
12
- | 本机控制台、配置保存与手动重启 | [控制台说明](docs/web-console.md) |
11
+ | 安装、配置、运行 | [部署说明](docs/deployment.md) |
12
+ | 本机控制台、配置保存与 Web/CLI 重启 | [控制台说明](docs/web-console.md) |
13
+ | 命令行配置、凭据和任务管理 | [CLI 说明](docs/cli.md) |
13
14
  | 理解产品目标和架构选择 | [总体方案](docs/design-proposal.md) |
14
15
  | 查找实现与对应测试 | [代码导航](docs/technical-design.md) |
15
16
  | 查询测试命令、真实验收及缺口 | [验证说明](docs/validation.md) |
@@ -20,7 +21,7 @@
20
21
 
21
22
  ## 使用入口
22
23
 
23
- Linux/macOS,Node.js **22.19+**。安装包自带运行产物,无需克隆或编译。发布目标为 `0.0.1-rc.1`,使用 `latest`;实际发布与平台验收状态见[发布证据](docs/validation.md#npm-发布候选验证)。
24
+ Linux/macOS,Node.js **22.19+**。安装包自带运行产物,无需克隆或编译。`latest` 指向的已发布版本及平台验收范围见[发布证据](docs/validation.md#npm-发布候选验证)。
24
25
 
25
26
  ```bash
26
27
  npm install -g @realqiyan/agent-task
@@ -28,9 +29,11 @@ agent-task setup
28
29
  agent-task start
29
30
  # 另一个终端进行带认证的健康检查:
30
31
  agent-task status
32
+ # 应用已保存配置并确认新实例可用:
33
+ agent-task restart
31
34
  ```
32
35
 
33
- npm 包名为 `@realqiyan/agent-task`,安装后的命令仍为 `agent-task`。setup 提供交互配置;启动后可使用本机 Web 控制台。先停止前台实例,再用 `agent-task service install` 安装并启动用户后台服务。安装器不会替用户登录或批准执行端;详细操作见[部署说明](docs/deployment.md)。
36
+ npm 包名为 `@realqiyan/agent-task`,安装后的命令仍为 `agent-task`。setup 提供交互配置;启动后可使用本机 Web 控制台或 [CLI](docs/cli.md),两端共用管理服务。安装器不会替用户登录或批准执行端;详细操作见[部署说明](docs/deployment.md)。
34
37
 
35
38
  MCP 客户端使用独立服务 token。源码开发辅助命令(凭据须安全注入环境,不写命令行):
36
39
 
@@ -39,7 +42,7 @@ node scripts/mcp-call.mjs --list
39
42
  node scripts/mcp-call.mjs service_status
40
43
  ```
41
44
 
42
- 任务由 `task_create` 提交固定 DAG,每个节点选择已注册执行端。具体契约见 [task-contract.ts](src/task-contract.ts),管理工具见 [mcp.ts](src/mcp.ts)。Prompt、人工回复和工作空间选择都不授予工具权限。
45
+ 任务由 `task_create` 提交固定 DAG,每个节点选择已注册执行端。具体契约见 [task-contract.ts](src/protocol/task-contract.ts),管理工具见 [mcp.ts](src/protocol/mcp.ts)。Prompt、人工回复和工作空间选择都不授予工具权限。
43
46
 
44
47
  ## 许可证
45
48
 
@@ -4,17 +4,17 @@ import { readFileSync } from "node:fs";
4
4
  const help = `Usage: agent-task <command>
5
5
  setup [options] Interactive configuration (setup --help for flags)
6
6
  start [--home PATH] Foreground service using the private installation
7
- start --config PATH --environment PATH
8
- Foreground service with explicit private files
9
- status [--home PATH] Authenticated MCP health check; no token printed
10
- service install [--home PATH]
11
- Install/start systemd user service or LaunchAgent
12
- service remove [--home PATH]
13
- Stop/remove only the owned service
14
- uninstall [--home PATH] Remove service and owned executor components;
15
- preserve configuration, credentials and task data
7
+ restart [--home PATH] Restart runtime; verify readiness
8
+ status [--home PATH] Health, active work and pending configuration
9
+ config Show, edit, validate and apply configuration
10
+ executor Manage registrations and unattended authorization
11
+ coordinator Configure the coordination model
12
+ credential List, replace or delete private credentials
13
+ task Inspect tasks and control future triggers
14
+ run Inspect run history and deliveries
15
+ <command> --help Command usage; management supports --json
16
16
  --version | --help
17
- Node.js 22.19+; Linux or macOS. Setup never grants executor permissions.`;
17
+ Node.js 22.19+; Linux or macOS.`;
18
18
  try {
19
19
  const [major, minor] = process.versions.node.split(".").map(Number);
20
20
  if (major < 22 || (major === 22 && minor < 19)) throw new Error("Node.js 22.19+ is required");
@@ -23,44 +23,26 @@ try {
23
23
  console.log(help);
24
24
  } else if (command === "--version") {
25
25
  console.log(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
26
- } else if (command === "setup" || command === "uninstall") {
26
+ } else if (command === "setup") {
27
27
  process.argv = [process.execPath, command, ...args];
28
28
  await import(`../scripts/${command}.mjs`);
29
- } else if (command === "start" && args[0] === "--config") {
30
- if (args.length !== 4 || args[2] !== "--environment") throw new Error("Use start --config PATH --environment PATH");
31
- process.argv = [process.execPath, "agent-task", ...args];
32
- await import("../dist/main.js");
33
29
  } else {
34
30
  const lib = await import("../scripts/install-lib.mjs");
35
- const action = command === "service" ? args.shift() : undefined;
36
- if (!["start", "status", "service"].includes(command) || (command === "service" && !["install", "remove"].includes(action))) throw new Error(help);
37
- const options = lib.argumentsFor(args);
38
- if (Object.keys(options).some(key => key !== "home")) throw new Error("Only --home is accepted here");
39
- const root = lib.installRoot(options);
40
- if (action === "install") {
41
- process.argv = [process.execPath, "setup", "--home", root, "--service"];
42
- await import("../scripts/setup.mjs");
43
- } else if (action === "remove") {
44
- lib.readManifest(root);
45
- const unlock = lib.operationLock(root);
46
- try {
47
- const manifest = lib.readManifest(root);
48
- lib.deactivate(manifest);
49
- manifest.serviceInstalled = false;
50
- lib.saveManifest(root, manifest);
51
- console.log("Owned service removed; configuration, credentials, components and data preserved.");
52
- } finally { unlock(); }
53
- } else if (command === "start") {
54
- process.argv = [process.execPath, "service-runner", root];
55
- await import("../scripts/service-runner.mjs");
31
+ if (command === "start") {
32
+ if (args.includes("--help")) { console.log("Usage: agent-task start [--home ABSOLUTE_DIR]"); }
33
+ else {
34
+ const options = lib.argumentsFor(args);
35
+ if (Object.keys(options).some(key => key !== "home")) throw new Error("Only --home is accepted here");
36
+ const root = lib.installRoot(options);
37
+ process.argv = [process.execPath, "service-runner", root];
38
+ await import("../scripts/service-runner.mjs");
39
+ }
56
40
  } else {
57
- const manifest = lib.readManifest(root);
58
- if (manifest.phase !== "ready") throw new Error("Installation is not ready");
59
- await lib.checkHealth(lib.json(root + "/config.json"), lib.runtimeEnv(manifest));
60
- console.log("Authenticated MCP health check passed.");
41
+ const { runCli } = await import("../dist/protocol/cli/commands.js");
42
+ await runCli(command, args, home => lib.installRoot({ home }));
61
43
  }
62
44
  }
63
45
  } catch (error) {
64
- console.error(error.message);
46
+ console.error(process.argv.includes("--json") ? JSON.stringify({ error: error.message }) : error.message);
65
47
  process.exitCode = 1;
66
48
  }
@@ -0,0 +1,215 @@
1
+ import { fork } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import { appendFileSync, chmodSync, closeSync, constants, existsSync, fstatSync, lstatSync, openSync, unlinkSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { randomUUID } from "node:crypto";
7
+ import { acquireLauncherLock } from "../infrastructure/lock.js";
8
+ import { prepareStartup } from "./startup.js";
9
+ import { startupPaths } from "../configuration/files.js";
10
+ function message(child, type, timeout = 30_000) {
11
+ return new Promise((resolve, reject) => {
12
+ const cleanup = () => { clearTimeout(timer); child.off("message", receive); child.off("exit", exit); child.off("error", fail); };
13
+ const fail = () => { cleanup(); reject(new Error("Service process failed before " + type)); };
14
+ const exit = () => fail();
15
+ const receive = (value) => { if (value.type === type) {
16
+ cleanup();
17
+ resolve(value);
18
+ } };
19
+ const timer = setTimeout(() => { cleanup(); reject(new Error("Service timed out before " + type)); }, timeout);
20
+ child.on("message", receive);
21
+ child.once("exit", exit);
22
+ child.once("error", fail);
23
+ });
24
+ }
25
+ async function stop(child) {
26
+ if (child.exitCode !== null || child.signalCode !== null)
27
+ return;
28
+ const closed = new Promise(resolve => child.once("close", () => resolve()));
29
+ child.kill("SIGTERM");
30
+ const timer = setTimeout(() => child.kill("SIGKILL"), 12_000);
31
+ try {
32
+ await closed;
33
+ }
34
+ finally {
35
+ clearTimeout(timer);
36
+ }
37
+ }
38
+ async function health(ready, token) {
39
+ const response = await fetch(ready.url, {
40
+ method: "POST", headers: { Authorization: "Bearer " + token, "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
41
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "service_status", arguments: {} } }),
42
+ signal: AbortSignal.timeout(5000),
43
+ });
44
+ const body = await response.json();
45
+ if (!response.ok || !body.result?.structuredContent?.data?.running)
46
+ throw new Error("Authenticated health check failed");
47
+ }
48
+ function removeSocket(path) {
49
+ if (!existsSync(path))
50
+ return;
51
+ const stat = lstatSync(path);
52
+ if (!stat.isSocket() || stat.uid !== process.getuid())
53
+ throw new Error("Refusing to remove an unowned control socket path");
54
+ unlinkSync(path);
55
+ }
56
+ function logFile(path) {
57
+ const fd = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
58
+ const stat = fstatSync(fd);
59
+ if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 0o077)) {
60
+ closeSync(fd);
61
+ throw new Error("Invalid private log file");
62
+ }
63
+ return fd;
64
+ }
65
+ /** Stable owner for foreground and service managers. Only explicit requests restart a child. */
66
+ export async function launch(home, nativeEnvironment) {
67
+ process.umask(0o077);
68
+ startupPaths(home); // Also checks private ownership of the locator.
69
+ const unlock = acquireLauncherLock(home);
70
+ const socket = join(home, "control.sock"), children = new Set();
71
+ let current, ready, restarting = true, closing = false;
72
+ let release;
73
+ const finished = new Promise(resolve => { release = resolve; });
74
+ const control = createServer(async (req, res) => {
75
+ try {
76
+ if (req.method !== "POST" || !["/status", "/restart"].includes(req.url ?? "")) {
77
+ res.writeHead(404).end();
78
+ return;
79
+ }
80
+ const result = req.url === "/restart" ? await restart() : await status();
81
+ res.setHeader("Content-Type", "application/json");
82
+ res.end(JSON.stringify(result));
83
+ }
84
+ catch {
85
+ res.writeHead(503).end(JSON.stringify({ error: "Service unavailable; inspect private logs and startup files" }));
86
+ }
87
+ });
88
+ const shutdown = async () => {
89
+ if (closing)
90
+ return;
91
+ closing = true;
92
+ control.close();
93
+ control.closeAllConnections();
94
+ await Promise.all([...children].map(stop));
95
+ try {
96
+ removeSocket(socket);
97
+ }
98
+ finally {
99
+ unlock();
100
+ release();
101
+ }
102
+ };
103
+ const signal = () => { void shutdown(); };
104
+ process.on("SIGINT", signal);
105
+ process.on("SIGTERM", signal);
106
+ const start = async () => {
107
+ const inputs = await prepareStartup(home);
108
+ if (closing)
109
+ throw new Error("Launcher is closing");
110
+ const log = logFile(join(inputs.dataDir, "service.log"));
111
+ const child = fork(fileURLToPath(new URL("./main.js", import.meta.url)), ["--config", inputs.configPath, "--environment", inputs.environmentPath], {
112
+ env: { ...nativeEnvironment, AGENT_TASK_INSTANCE_ID: randomUUID() }, execArgv: [], stdio: ["ignore", "pipe", "pipe", "ipc"],
113
+ });
114
+ children.add(child);
115
+ const output = (chunk) => { appendFileSync(log, chunk); process.stdout.write(chunk); };
116
+ child.stdout.on("data", output);
117
+ child.stderr.on("data", output);
118
+ child.once("close", () => {
119
+ closeSync(log);
120
+ children.delete(child);
121
+ if (current === child && !restarting && !closing) {
122
+ ready = undefined;
123
+ process.exitCode = 1;
124
+ void shutdown();
125
+ }
126
+ });
127
+ child.on("message", (value) => {
128
+ if (value.type !== "restart")
129
+ return;
130
+ if (child !== current || restarting || closing) {
131
+ child.send({ type: "restarted", error: "Restart already in progress or unavailable" }, () => { });
132
+ return;
133
+ }
134
+ void restart(child).then(value => {
135
+ if (child.connected)
136
+ child.send({ type: "restarted", ready: value }, () => { });
137
+ }, () => {
138
+ if (child.connected)
139
+ child.send({ type: "restarted", error: "Restart failed; inspect private logs and use CLI restart after correcting startup files" }, () => { });
140
+ });
141
+ });
142
+ try {
143
+ const value = (await message(child, "ready")).ready;
144
+ await health(value, inputs.environment[inputs.config.tokenEnv]);
145
+ current = child;
146
+ ready = { info: value, token: inputs.environment[inputs.config.tokenEnv] };
147
+ return value;
148
+ }
149
+ catch (error) {
150
+ await stop(child);
151
+ throw error;
152
+ }
153
+ };
154
+ const status = async () => {
155
+ if (!ready || restarting || closing)
156
+ throw new Error("Service unavailable");
157
+ // Saved inputs may need repair. Health belongs to the running instance,
158
+ // including the credential with which that instance actually started.
159
+ const running = ready;
160
+ await health(running.info, running.token);
161
+ return running.info;
162
+ };
163
+ const restart = async (requester) => {
164
+ if (restarting || closing)
165
+ throw new Error("Restart already in progress or closed");
166
+ restarting = true;
167
+ const old = current;
168
+ try {
169
+ if (old) {
170
+ const drained = message(old, "drained");
171
+ old.send({ type: "drain" });
172
+ let result;
173
+ try {
174
+ result = await drained;
175
+ }
176
+ catch (error) {
177
+ current = undefined;
178
+ ready = undefined;
179
+ await stop(old);
180
+ throw error;
181
+ }
182
+ if (result.error)
183
+ throw new Error(result.error);
184
+ current = undefined;
185
+ ready = undefined;
186
+ // Keep only the authenticated Web response alive until the new process is verified.
187
+ if (old !== requester)
188
+ await stop(old);
189
+ }
190
+ const value = await start();
191
+ return value;
192
+ }
193
+ finally {
194
+ restarting = false;
195
+ if (requester && requester !== current) {
196
+ const timer = setTimeout(() => { void stop(requester); }, 5000);
197
+ requester.once("close", () => clearTimeout(timer));
198
+ }
199
+ }
200
+ };
201
+ try {
202
+ removeSocket(socket); // Stale socket only, under exclusive launcher ownership.
203
+ await new Promise((resolve, reject) => { control.once("error", reject); control.listen(socket, resolve); });
204
+ chmodSync(socket, 0o600);
205
+ await start();
206
+ restarting = false;
207
+ await finished;
208
+ }
209
+ finally {
210
+ await shutdown();
211
+ process.off("SIGINT", signal);
212
+ process.off("SIGTERM", signal);
213
+ }
214
+ }
215
+ //# sourceMappingURL=launcher.js.map
@@ -0,0 +1,122 @@
1
+ import { HttpServer } from "../protocol/http/server.js";
2
+ import { createTaskRuntime } from "./task-runtime.js";
3
+ import { LocalSettingsFiles, configurationPaths } from "../configuration/files.js";
4
+ import { resolve } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { loadConfig } from "../configuration/files.js";
7
+ import { baseEnvironment, validateServiceEnvironment } from "../configuration/environment.js";
8
+ import { readEnvironment } from "../configuration/files.js";
9
+ import { Settings } from "../configuration/settings.js";
10
+ import { acquireLock } from "../infrastructure/lock.js";
11
+ import { errorText } from "../shared/errors.js";
12
+ import { createExecution } from "../integrations/acp/execution.js";
13
+ import { executorEnvironment } from "../executors/environment.js";
14
+ import { createCoordinator } from "../integrations/coordinator/coordinator.js";
15
+ process.umask(0o077);
16
+ const args = process.argv.slice(2);
17
+ if (args[0] !== "--config" || args.length !== 4 || args[2] !== "--environment") {
18
+ console.error("Internal runtime requires --config PATH --environment PATH; use agent-task start");
19
+ process.exit(2);
20
+ }
21
+ const configPath = resolve(args[1]), environmentPath = resolve(args[3]);
22
+ const native = { ...process.env }, base = baseEnvironment(native);
23
+ const environment = readEnvironment(environmentPath);
24
+ const instanceId = process.env.AGENT_TASK_INSTANCE_ID ?? randomUUID();
25
+ for (const key of Object.keys(process.env))
26
+ delete process.env[key];
27
+ Object.assign(process.env, base, environment);
28
+ const config = loadConfig(configPath);
29
+ const { token, adminToken } = validateServiceEnvironment(config, environment);
30
+ let http;
31
+ let tasks, settings;
32
+ let release;
33
+ let shutting;
34
+ function shutdown(code, drain = false) {
35
+ process.exitCode = Math.max(process.exitCode ? Number(process.exitCode) : 0, code);
36
+ return shutting ??= (async () => {
37
+ http?.stopAccepting();
38
+ settings?.close();
39
+ try {
40
+ await tasks?.close();
41
+ }
42
+ catch (error) {
43
+ console.error(errorText(error));
44
+ process.exitCode = 1;
45
+ }
46
+ await http?.close(drain);
47
+ await release?.();
48
+ })().catch(error => { console.error(errorText(error)); process.exitCode = 1; });
49
+ }
50
+ const fatal = (error) => { console.error(errorText(error)); void shutdown(1).finally(() => { if (process.connected)
51
+ process.disconnect(); }); };
52
+ let restartPending = false;
53
+ const restart = async () => {
54
+ if (!process.connected || restartPending || shutting)
55
+ throw new Error("Restart unavailable");
56
+ restartPending = true;
57
+ return new Promise((resolve, reject) => {
58
+ process.once("message", function receive(value) {
59
+ if (value.type !== "restarted") {
60
+ process.once("message", receive);
61
+ return;
62
+ }
63
+ if (value.error && !shutting) {
64
+ restartPending = false;
65
+ reject(new Error(value.error));
66
+ return;
67
+ }
68
+ // The HTTP adapter finishes the authenticated response before this child exits.
69
+ setTimeout(() => { if (process.connected)
70
+ process.disconnect(); http?.closeConnections(); }, 200);
71
+ if (value.error)
72
+ reject(new Error(value.error));
73
+ else
74
+ resolve(value.ready);
75
+ });
76
+ process.send({ type: "restart" });
77
+ });
78
+ };
79
+ process.on("message", (value) => {
80
+ if (value.type !== "drain")
81
+ return;
82
+ // Both Web and CLI restarts pass this boundary before changing runtime ownership.
83
+ try {
84
+ if (settings?.status().writeBlocked)
85
+ throw new Error("Configuration publication is uncertain");
86
+ }
87
+ catch {
88
+ process.send?.({ type: "drained", error: "Restart blocked; stop and inspect private configuration files" });
89
+ return;
90
+ }
91
+ void shutdown(0, true).then(() => {
92
+ if (process.exitCode) {
93
+ if (process.connected)
94
+ process.disconnect();
95
+ }
96
+ else
97
+ process.send?.({ type: "drained" });
98
+ });
99
+ });
100
+ const stop = () => { void shutdown(0).finally(() => { http?.closeConnections(); if (process.connected)
101
+ process.disconnect(); }); };
102
+ process.once("SIGINT", stop);
103
+ process.once("SIGTERM", stop);
104
+ process.once("disconnect", stop);
105
+ try {
106
+ release = await acquireLock(config.dataDir);
107
+ tasks = createTaskRuntime(config.dataDir, config.executors, createExecution(executorEnvironment(native, environment, config)), config.coordinator ? createCoordinator(config.dataDir, config.coordinator) : undefined, fatal);
108
+ if (adminToken)
109
+ settings = new Settings(new LocalSettingsFiles(configPath, environmentPath), configurationPaths, config, base, process.env);
110
+ http = new HttpServer({ tasks, settings, token, adminToken, fatal, restart, instanceId });
111
+ const origin = await http.listen(config.port);
112
+ if (shutting)
113
+ throw new Error("Startup stopped");
114
+ tasks.supervisor.start();
115
+ const ready = { instanceId, pid: process.pid, url: origin + "/mcp", consoleUrl: settings ? origin + "/admin/" : null };
116
+ console.log(JSON.stringify({ event: "ready", ...ready }));
117
+ process.send?.({ type: "ready", ready });
118
+ }
119
+ catch (error) {
120
+ fatal(error);
121
+ }
122
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,51 @@
1
+ import { startupPaths } from "../configuration/files.js";
2
+ import { existsSync, mkdirSync, writeFileSync, openSync, fsyncSync, closeSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { loadConfig } from "../configuration/files.js";
5
+ import { readEnvironment } from "../configuration/files.js";
6
+ import { validateServiceEnvironment } from "../configuration/environment.js";
7
+ import { checkDataDirectory, readPrivateText, replacePrivateText } from "../infrastructure/private-files.js";
8
+ import { acquireLock } from "../infrastructure/lock.js";
9
+ function prepareFile(path, text) {
10
+ if (existsSync(path)) {
11
+ if (readPrivateText(path) !== text)
12
+ throw new Error("Target contains different startup files; refusing to overwrite them");
13
+ return; // Explicitly resume an interrupted preparation, not a data repair.
14
+ }
15
+ const fd = openSync(path, "wx", 0o600);
16
+ try {
17
+ writeFileSync(fd, text);
18
+ fsyncSync(fd);
19
+ }
20
+ finally {
21
+ closeSync(fd);
22
+ }
23
+ }
24
+ export async function prepareStartup(home) {
25
+ const current = startupPaths(home), config = loadConfig(current.configPath), environment = readEnvironment(current.environmentPath);
26
+ validateServiceEnvironment(config, environment);
27
+ checkDataDirectory(config.dataDir);
28
+ if (config.dataDir !== current.dataDir) {
29
+ mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
30
+ checkDataDirectory(config.dataDir);
31
+ const release = await acquireLock(config.dataDir);
32
+ try {
33
+ // Only next-start inputs. Never copy tasks, sessions, results, native configuration or workspaces.
34
+ prepareFile(join(config.dataDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
35
+ prepareFile(join(config.dataDir, "environment.json"), JSON.stringify(environment, null, 2) + "\n");
36
+ const fd = openSync(config.dataDir, "r");
37
+ try {
38
+ fsyncSync(fd);
39
+ }
40
+ finally {
41
+ closeSync(fd);
42
+ }
43
+ replacePrivateText(join(home, "launch.json"), JSON.stringify({ dataDir: config.dataDir }) + "\n");
44
+ }
45
+ finally {
46
+ await release();
47
+ }
48
+ }
49
+ return { ...startupPaths(home), config, environment };
50
+ }
51
+ //# sourceMappingURL=startup.js.map
@@ -0,0 +1,42 @@
1
+ import { join } from "node:path";
2
+ import { ExecutorRegistry } from "../executors/registry.js";
3
+ import { checkWorkspace, resolveWorkspace } from "../infrastructure/workspace.js";
4
+ import { LocalResultFiles } from "../results/files.js";
5
+ import { Results } from "../results/service.js";
6
+ import { TaskDefinitions } from "../tasks/definitions.js";
7
+ import { TaskRuns } from "../tasks/runs.js";
8
+ import { RunSupervisor } from "../tasks/supervisor.js";
9
+ import { TaskStore } from "../tasks/storage/store.js";
10
+ /** Creates and owns the business store and services; callers must stop ingress before closing. */
11
+ export function createTaskRuntime(directory, profiles, openExecution, coordinate, fatal) {
12
+ const store = new TaskStore(directory);
13
+ try {
14
+ const executors = new ExecutorRegistry(profiles);
15
+ const workspaces = { resolve: resolveWorkspace, check: checkWorkspace };
16
+ const results = new Results(new LocalResultFiles(join(directory, "results"), true));
17
+ const definitions = new TaskDefinitions(store, executors, workspaces, Boolean(coordinate));
18
+ const runs = new TaskRuns(store, executors, results, workspaces);
19
+ const supervisor = new RunSupervisor(runs, openExecution, coordinate, fatal);
20
+ let closing;
21
+ return { definitions, runs, supervisor, executors,
22
+ close() {
23
+ return closing ??= (async () => {
24
+ definitions.close();
25
+ const stopped = supervisor.close();
26
+ runs.close();
27
+ try {
28
+ await stopped;
29
+ }
30
+ finally {
31
+ store.close();
32
+ }
33
+ })();
34
+ },
35
+ };
36
+ }
37
+ catch (error) {
38
+ store.close();
39
+ throw error;
40
+ }
41
+ }
42
+ //# sourceMappingURL=task-runtime.js.map
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { executorSchema } from "../executors/validation.js";
3
+ import { baseEnvKeys } from "./policy.js";
4
+ const envKey = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
5
+ const modelId = z.string().min(1).max(200);
6
+ const coordinatorSchema = z.discriminatedUnion("provider", [
7
+ z.object({ provider: z.enum(["openai", "anthropic", "deepseek"]), model: modelId }).strict(),
8
+ z.object({
9
+ provider: z.literal("openai-compatible"), model: modelId,
10
+ api: z.enum(["openai-completions", "openai-responses"]).default("openai-completions"),
11
+ baseUrl: z.string().url().refine(value => {
12
+ const url = new URL(value);
13
+ return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
14
+ }, "baseUrl must be HTTP(S) without credentials, query or fragment"),
15
+ apiKeyEnv: envKey.refine(key => !baseEnvKeys.includes(key), "Credential must not use a base environment key"),
16
+ contextWindow: z.number().int().positive(),
17
+ maxTokens: z.number().int().positive(),
18
+ reasoning: z.boolean().default(false),
19
+ }).strict(),
20
+ ]).refine(value => value.provider !== "openai-compatible" || value.maxTokens <= value.contextWindow, "maxTokens must not exceed contextWindow");
21
+ export const configSchema = z.object({
22
+ dataDir: z.string().min(1),
23
+ port: z.number().int().min(0).max(65535).default(8765),
24
+ tokenEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).default("AGENT_TASK_TOKEN"),
25
+ coordinator: coordinatorSchema.optional(),
26
+ executors: z.array(executorSchema).max(100).default([]),
27
+ }).strict();
28
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1,32 @@
1
+ import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv } from "./policy.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
+ /** Credential names and values must stay separate, including historical token aliases. */
16
+ export function validateServiceEnvironment(config, environment) {
17
+ const token = environment[config.tokenEnv], adminToken = environment[ADMIN_TOKEN_ENV];
18
+ if (!token || !/^[!-~]{32,256}$/.test(token))
19
+ throw new Error("A private service token of 32–256 visible ASCII characters is required");
20
+ if (adminToken !== undefined && (!/^[!-~]{32,256}$/.test(adminToken) || adminToken === token))
21
+ throw new Error("Invalid or reused administrator token");
22
+ const credential = config.coordinator ? environment[coordinatorCredentialEnv(config.coordinator)] : undefined;
23
+ if (credential && (credential === token || credential === adminToken))
24
+ throw new Error("Model credentials must be distinct from service and administrator tokens");
25
+ return { token, adminToken };
26
+ }
27
+ /** Explicit file mode never falls back to inherited shell credentials. */
28
+ export function baseEnvironment(source) {
29
+ return Object.fromEntries(["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR"]
30
+ .filter(key => source[key] !== undefined).map(key => [key, source[key]]));
31
+ }
32
+ //# sourceMappingURL=environment.js.map