@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/README.md
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
| 目的 | 入口 |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
|
|
|
12
|
-
|
|
|
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
|
|
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
|
|
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
|
|
package/bin/agent-task.mjs
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
58
|
-
|
|
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
|