@realqiyan/agent-task 0.0.1-rc.3 → 0.1.0-rc.2
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/CONTEXT.md +17 -0
- package/README.md +28 -6
- package/bin/agent-task.mjs +18 -6
- package/config.example.json +1 -1
- package/dist/application/launcher.js +30 -16
- package/dist/application/main.js +5 -2
- package/dist/application/mcp-entry.js +71 -0
- package/dist/application/task-runtime.js +3 -3
- package/dist/configuration/contract.js +2 -2
- package/dist/configuration/files.js +2 -3
- package/dist/configuration/management.js +1 -1
- package/dist/configuration/policy.js +3 -8
- package/dist/configuration/settings.js +10 -33
- package/dist/executors/ports.js +2 -0
- package/dist/executors/registry.js +117 -15
- package/dist/executors/validation.js +4 -5
- package/dist/infrastructure/lock.js +25 -13
- package/dist/infrastructure/workspace.js +4 -3
- package/dist/integrations/acp/connection.js +5 -5
- package/dist/integrations/acp/discovery.js +271 -0
- package/dist/integrations/acp/runtime.js +38 -23
- package/dist/integrations/acp/worker.js +5 -3
- package/dist/integrations/local-service/control.js +36 -11
- package/dist/protocol/cli/commands.js +41 -46
- package/dist/protocol/http/server.js +8 -0
- package/dist/protocol/http/web.js +7 -1
- package/dist/protocol/mcp-contract.js +194 -0
- package/dist/protocol/mcp.js +99 -32
- package/dist/protocol/settings-contract.js +4 -7
- package/dist/protocol/stdio.js +72 -0
- package/dist/protocol/task-contract.js +45 -18
- package/dist/shared/errors.js +15 -0
- package/dist/shared/limits.js +3 -0
- package/dist/tasks/definitions.js +19 -14
- package/dist/tasks/graph.js +5 -4
- package/dist/tasks/human-input.js +10 -9
- package/dist/tasks/runs.js +31 -30
- package/dist/tasks/schedule.js +2 -1
- package/dist/tasks/storage/records.js +6 -6
- package/dist/tasks/storage/store.js +8 -7
- package/dist/tasks/supervisor.js +10 -7
- package/dist/tasks/validation.js +3 -0
- package/dist/web/settings-view.js +43 -37
- package/docs/adr/0001-task-request-scope.md +3 -0
- package/docs/adr/0002-mcp-workspace-source.md +3 -0
- package/docs/adr/0003-adapter-installation.md +3 -0
- package/docs/architecture-guidelines.md +2 -2
- package/docs/cli.md +12 -4
- package/docs/deployment.md +52 -36
- package/docs/design-proposal.md +3 -0
- package/docs/release.md +27 -8
- package/docs/technical-design.md +5 -3
- package/docs/validation.md +59 -9
- package/docs/web-console.md +7 -4
- package/package.json +3 -1
- package/scripts/install-lib.mjs +5 -23
- package/scripts/probe-agent.mjs +10 -9
- package/scripts/service-runner.mjs +8 -3
- package/scripts/setup-wizard.mjs +38 -11
- package/scripts/setup.mjs +69 -95
package/CONTEXT.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Agent Task
|
|
2
|
+
|
|
3
|
+
本词汇表定义任务协调与执行中的项目术语。
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**协调模型服务(Coordinator Provider)**:
|
|
8
|
+
为内置协调员提供模型推理的服务。它的模型选择和凭据独立于各执行端的原生模型配置。
|
|
9
|
+
_Avoid_: 全局模型服务、执行端模型服务
|
|
10
|
+
|
|
11
|
+
**任务工作目录(Task Workspace)**:
|
|
12
|
+
任务各节点开展工作的项目目录。它在提交任务时确定,后续各次运行使用同一个目录。
|
|
13
|
+
_Avoid_: 执行端默认目录、服务工作目录
|
|
14
|
+
|
|
15
|
+
**任务请求键(Task Request Key)**:
|
|
16
|
+
由调用方提供、用于识别同一任务工作目录中一次任务创建请求的标识。不同工作目录中的相同请求键表示不同请求。
|
|
17
|
+
_Avoid_: 全局任务 ID
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
| 本机控制台、配置保存与 Web/CLI 重启 | [控制台说明](docs/web-console.md) |
|
|
13
13
|
| 命令行配置、凭据和任务管理 | [CLI 说明](docs/cli.md) |
|
|
14
14
|
| 理解产品目标和架构选择 | [总体方案](docs/design-proposal.md) |
|
|
15
|
+
| 查询项目术语 | [词汇表](CONTEXT.md) |
|
|
15
16
|
| 查找实现与对应测试 | [代码导航](docs/technical-design.md) |
|
|
16
17
|
| 查询测试命令、真实验收及缺口 | [验证说明](docs/validation.md) |
|
|
17
18
|
| npm 候选验收、GitHub Actions 与受控发布 | [发布说明](docs/release.md) |
|
|
@@ -21,28 +22,49 @@
|
|
|
21
22
|
|
|
22
23
|
## 使用入口
|
|
23
24
|
|
|
24
|
-
Linux/macOS,Node.js **22.19
|
|
25
|
+
Linux/macOS,Node.js **22.19+**。以下为当前源码的使用入口;安装包自带运行产物。实际发布状态及平台验收范围见[发布证据](docs/validation.md#npm-发布候选验证)。
|
|
25
26
|
|
|
26
27
|
```bash
|
|
27
28
|
npm install -g @realqiyan/agent-task
|
|
28
29
|
agent-task setup
|
|
29
|
-
agent-task
|
|
30
|
-
#
|
|
30
|
+
# 在 MCP 宿主中配置 command: agent-task,args: [mcp]
|
|
31
|
+
# 连接后可查询后台状态:
|
|
31
32
|
agent-task status
|
|
32
33
|
# 应用已保存配置并确认新实例可用:
|
|
33
34
|
agent-task restart
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
npm 包名为 `@realqiyan/agent-task`,安装后的命令仍为 `agent-task`。setup
|
|
37
|
+
npm 包名为 `@realqiyan/agent-task`,安装后的命令仍为 `agent-task`。setup 要求确认目录、端口、令牌与协调模型。MCP 连接自动启动或复用独立后台,检测本机执行 CLI,并在缺少 ACP 适配器时安装当时最新版;已有适配器复用,原生登录仍由其官方工具管理。宿主退出后后台继续运行。接入配置与前台管理方式见[部署说明](docs/deployment.md)。
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
stdio 入口从私有文件读取内部服务 token;直接 HTTP 接入需配置独立服务 token。源码开发辅助命令(凭据须安全注入环境,不写命令行):
|
|
39
40
|
|
|
40
41
|
```bash
|
|
41
42
|
node scripts/mcp-call.mjs --list
|
|
42
43
|
node scripts/mcp-call.mjs service_status
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
任务由 `task_create` 提交固定 DAG
|
|
46
|
+
任务由 `task_create` 提交固定 DAG,每个节点选择已就绪执行端。显式工作目录优先,否则 stdio 入口补入自己的启动目录;直接 HTTP 调用必须提供目录。具体契约见 [task-contract.ts](src/protocol/task-contract.ts),管理工具见 [mcp.ts](src/protocol/mcp.ts)。Prompt、人工回复和工作空间选择都不授予工具权限。
|
|
47
|
+
|
|
48
|
+
## CLI 命令索引
|
|
49
|
+
|
|
50
|
+
下表列出全部命令及子命令。参数见 `agent-task <命令> --help`,操作流程见 [CLI 说明](docs/cli.md)和[部署说明](docs/deployment.md)。
|
|
51
|
+
|
|
52
|
+
| 命令 | 子命令 | 用途 |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `agent-task setup` | — | 初始化、检查或接续安装配置 |
|
|
55
|
+
| `agent-task mcp` | — | stdio 接入,自动启动或连接共用后台 |
|
|
56
|
+
| `agent-task start` | — | 前台启动服务并调度任务 |
|
|
57
|
+
| `agent-task stop` | — | 停止共用服务 |
|
|
58
|
+
| `agent-task restart` | — | 应用已保存配置并重启、检查可用性 |
|
|
59
|
+
| `agent-task status` | — | 查看服务健康、活动任务及待生效配置 |
|
|
60
|
+
| `agent-task config` | `show`、`diff`、`edit`、`export`、`validate`、`apply` | 查看、编辑、导出、校验及保存配置 |
|
|
61
|
+
| `agent-task executor` | `list`、`show`、`refresh`、`upgrade`、`enable`、`disable` | 查看与刷新执行端、升级适配器、设置启停 |
|
|
62
|
+
| `agent-task coordinator` | `show`、`set`、`clear` | 查看、设置或清除协调模型配置 |
|
|
63
|
+
| `agent-task credential` | `list`、`set`、`delete` | 查看凭据状态、设置或删除凭据 |
|
|
64
|
+
| `agent-task task` | `list`、`show`、`pause`、`resume`、`cancel` | 查看任务,控制未来触发 |
|
|
65
|
+
| `agent-task run` | `list`、`show` | 查看运行历史与交付详情 |
|
|
66
|
+
| `agent-task help` / `agent-task --help` | — | 查看总帮助 |
|
|
67
|
+
| `agent-task --version` | — | 查看版本 |
|
|
46
68
|
|
|
47
69
|
## 许可证
|
|
48
70
|
|
package/bin/agent-task.mjs
CHANGED
|
@@ -3,11 +3,13 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
|
|
4
4
|
const help = `Usage: agent-task <command>
|
|
5
5
|
setup [options] Interactive configuration (setup --help for flags)
|
|
6
|
+
mcp [--home PATH] MCP stdio connection; start or reuse the shared background service
|
|
6
7
|
start [--home PATH] Foreground service using the private installation
|
|
8
|
+
stop [--home PATH] Stop the shared service
|
|
7
9
|
restart [--home PATH] Restart runtime; verify readiness
|
|
8
10
|
status [--home PATH] Health, active work and pending configuration
|
|
9
11
|
config Show, edit, validate and apply configuration
|
|
10
|
-
executor
|
|
12
|
+
executor Discover executors, upgrade adapters and configure enable/disable
|
|
11
13
|
coordinator Configure the coordination model
|
|
12
14
|
credential List, replace or delete private credentials
|
|
13
15
|
task Inspect tasks and control future triggers
|
|
@@ -28,14 +30,24 @@ try {
|
|
|
28
30
|
await import(`../scripts/${command}.mjs`);
|
|
29
31
|
} else {
|
|
30
32
|
const lib = await import("../scripts/install-lib.mjs");
|
|
31
|
-
if (
|
|
32
|
-
if (args.includes("--help")) { console.log("Usage: agent-task
|
|
33
|
+
if (["start", "mcp", "stop"].includes(command)) {
|
|
34
|
+
if (args.includes("--help")) { console.log("Usage: agent-task " + command + " [--home ABSOLUTE_DIR]" + (command === "stop" ? " [--json]" : "")); }
|
|
33
35
|
else {
|
|
34
|
-
const
|
|
36
|
+
const json = command === "stop" && args.includes("--json");
|
|
37
|
+
if (args.filter(arg => arg === "--json").length > 1) throw new Error("Repeated option: --json");
|
|
38
|
+
const options = lib.argumentsFor(json ? args.filter(arg => arg !== "--json") : args);
|
|
35
39
|
if (Object.keys(options).some(key => key !== "home")) throw new Error("Only --home is accepted here");
|
|
36
40
|
const root = lib.installRoot(options);
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
if (command === "mcp") {
|
|
42
|
+
const { runMcp } = await import("../dist/application/mcp-entry.js");
|
|
43
|
+
await runMcp(root);
|
|
44
|
+
} else if (command === "stop") {
|
|
45
|
+
const { controlService } = await import("../dist/integrations/local-service/control.js");
|
|
46
|
+
console.log(JSON.stringify({ event: "stopped", ...await controlService(root, "stop") }, null, json ? undefined : 2));
|
|
47
|
+
} else {
|
|
48
|
+
process.argv = [process.execPath, "service-runner", root];
|
|
49
|
+
await import("../scripts/service-runner.mjs");
|
|
50
|
+
}
|
|
39
51
|
}
|
|
40
52
|
} else {
|
|
41
53
|
const { runCli } = await import("../dist/protocol/cli/commands.js");
|
package/config.example.json
CHANGED
|
@@ -66,17 +66,25 @@ function logFile(path) {
|
|
|
66
66
|
export async function launch(home, nativeEnvironment) {
|
|
67
67
|
process.umask(0o077);
|
|
68
68
|
startupPaths(home); // Also checks private ownership of the locator.
|
|
69
|
-
const unlock = acquireLauncherLock(home);
|
|
69
|
+
const unlock = await acquireLauncherLock(home);
|
|
70
70
|
const socket = join(home, "control.sock"), children = new Set();
|
|
71
71
|
let current, ready, restarting = true, closing = false;
|
|
72
|
+
let shutting;
|
|
72
73
|
let release;
|
|
73
74
|
const finished = new Promise(resolve => { release = resolve; });
|
|
74
75
|
const control = createServer(async (req, res) => {
|
|
75
76
|
try {
|
|
76
|
-
if (req.method !== "POST" || !["/status", "/restart"].includes(req.url ?? "")) {
|
|
77
|
+
if (req.method !== "POST" || !["/status", "/restart", "/stop"].includes(req.url ?? "")) {
|
|
77
78
|
res.writeHead(404).end();
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
if (req.url === "/stop") {
|
|
82
|
+
await shutdown(true);
|
|
83
|
+
res.setHeader("Content-Type", "application/json");
|
|
84
|
+
res.setHeader("Connection", "close");
|
|
85
|
+
res.end(JSON.stringify({ stopped: true }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
80
88
|
const result = req.url === "/restart" ? await restart() : await status();
|
|
81
89
|
res.setHeader("Content-Type", "application/json");
|
|
82
90
|
res.end(JSON.stringify(result));
|
|
@@ -85,31 +93,36 @@ export async function launch(home, nativeEnvironment) {
|
|
|
85
93
|
res.writeHead(503).end(JSON.stringify({ error: "Service unavailable; inspect private logs and startup files" }));
|
|
86
94
|
}
|
|
87
95
|
});
|
|
88
|
-
const shutdown =
|
|
89
|
-
if (
|
|
90
|
-
return;
|
|
96
|
+
const shutdown = (drain = false) => {
|
|
97
|
+
if (shutting)
|
|
98
|
+
return shutting;
|
|
91
99
|
closing = true;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
return shutting = (async () => {
|
|
101
|
+
control.close();
|
|
102
|
+
if (!drain)
|
|
103
|
+
control.closeAllConnections();
|
|
104
|
+
await Promise.all([...children].map(stop));
|
|
105
|
+
try {
|
|
106
|
+
removeSocket(socket);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
unlock();
|
|
110
|
+
release();
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
102
113
|
};
|
|
103
114
|
const signal = () => { void shutdown(); };
|
|
115
|
+
const hangup = () => { }; // A detached launcher survives the originating terminal closing.
|
|
104
116
|
process.on("SIGINT", signal);
|
|
105
117
|
process.on("SIGTERM", signal);
|
|
118
|
+
process.on("SIGHUP", hangup);
|
|
106
119
|
const start = async () => {
|
|
107
120
|
const inputs = await prepareStartup(home);
|
|
108
121
|
if (closing)
|
|
109
122
|
throw new Error("Launcher is closing");
|
|
110
123
|
const log = logFile(join(inputs.dataDir, "service.log"));
|
|
111
124
|
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"],
|
|
125
|
+
env: { ...nativeEnvironment, AGENT_TASK_INSTANCE_ID: randomUUID(), AGENT_TASK_INSTALL_HOME: home }, execArgv: [], stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
113
126
|
});
|
|
114
127
|
children.add(child);
|
|
115
128
|
const output = (chunk) => { appendFileSync(log, chunk); process.stdout.write(chunk); };
|
|
@@ -210,6 +223,7 @@ export async function launch(home, nativeEnvironment) {
|
|
|
210
223
|
await shutdown();
|
|
211
224
|
process.off("SIGINT", signal);
|
|
212
225
|
process.off("SIGTERM", signal);
|
|
226
|
+
process.off("SIGHUP", hangup);
|
|
213
227
|
}
|
|
214
228
|
}
|
|
215
229
|
//# sourceMappingURL=launcher.js.map
|
package/dist/application/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpServer } from "../protocol/http/server.js";
|
|
2
2
|
import { createTaskRuntime } from "./task-runtime.js";
|
|
3
3
|
import { LocalSettingsFiles, configurationPaths } from "../configuration/files.js";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { loadConfig } from "../configuration/files.js";
|
|
7
7
|
import { baseEnvironment, validateServiceEnvironment } from "../configuration/environment.js";
|
|
@@ -12,6 +12,7 @@ import { errorText } from "../shared/errors.js";
|
|
|
12
12
|
import { createExecution } from "../integrations/acp/execution.js";
|
|
13
13
|
import { executorEnvironment } from "../executors/environment.js";
|
|
14
14
|
import { createCoordinator } from "../integrations/coordinator/coordinator.js";
|
|
15
|
+
import { LocalExecutors } from "../integrations/acp/discovery.js";
|
|
15
16
|
process.umask(0o077);
|
|
16
17
|
const args = process.argv.slice(2);
|
|
17
18
|
if (args[0] !== "--config" || args.length !== 4 || args[2] !== "--environment") {
|
|
@@ -104,7 +105,9 @@ process.once("SIGTERM", stop);
|
|
|
104
105
|
process.once("disconnect", stop);
|
|
105
106
|
try {
|
|
106
107
|
release = await acquireLock(config.dataDir);
|
|
107
|
-
|
|
108
|
+
const executorEnv = executorEnvironment(native, environment, config);
|
|
109
|
+
const discovery = new LocalExecutors(join(native.AGENT_TASK_INSTALL_HOME ?? dirname(configPath), "adapters"), executorEnv);
|
|
110
|
+
tasks = createTaskRuntime(config.dataDir, [], createExecution(executorEnv), config.coordinator ? createCoordinator(config.dataDir, config.coordinator) : undefined, fatal, discovery, config.disabledExecutors);
|
|
108
111
|
if (adminToken)
|
|
109
112
|
settings = new Settings(new LocalSettingsFiles(configPath, environmentPath), configurationPaths, config, base, process.env);
|
|
110
113
|
http = new HttpServer({ tasks, settings, token, adminToken, fatal, restart, instanceId });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, openSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
|
+
import { configSchema } from "../configuration/contract.js";
|
|
7
|
+
import { readEnvironment, startupPaths } from "../configuration/files.js";
|
|
8
|
+
import { readPrivateText } from "../infrastructure/private-files.js";
|
|
9
|
+
import { controlService, ControlError } from "../integrations/local-service/control.js";
|
|
10
|
+
import { serveStdio } from "../protocol/stdio.js";
|
|
11
|
+
async function startBackground(home) {
|
|
12
|
+
const fd = openSync(join(home, "launcher.log"), constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
13
|
+
try {
|
|
14
|
+
const stat = fstatSync(fd);
|
|
15
|
+
if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 0o077))
|
|
16
|
+
throw new Error("Expected a private launcher log");
|
|
17
|
+
const child = spawn(process.execPath, [fileURLToPath(new URL("../../scripts/service-runner.mjs", import.meta.url)), home], {
|
|
18
|
+
cwd: home, env: { ...process.env }, detached: true, stdio: ["ignore", fd, fd],
|
|
19
|
+
});
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
child.once("error", () => reject(new Error("Cannot start background service; inspect the private launcher log")));
|
|
22
|
+
child.once("spawn", () => { child.unref(); resolve(); });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function connectBackground(home) {
|
|
30
|
+
const deadline = Date.now() + 40_000;
|
|
31
|
+
let mayStart = true;
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
try {
|
|
34
|
+
return await controlService(home, "status", 2000);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!(error instanceof ControlError) || error.kind === "invalid")
|
|
38
|
+
throw error;
|
|
39
|
+
if (error.kind === "unreachable" && mayStart) {
|
|
40
|
+
mayStart = false;
|
|
41
|
+
await startBackground(home);
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
mayStart = false;
|
|
45
|
+
}
|
|
46
|
+
// Concurrent clients may launch together. The existing launcher lock selects one owner.
|
|
47
|
+
await setTimeout(200);
|
|
48
|
+
}
|
|
49
|
+
throw new Error("Background service did not become ready; inspect the private launcher log and use agent-task status. No restart was attempted");
|
|
50
|
+
}
|
|
51
|
+
/** The stdio connection owns neither the shared launcher nor persisted work. */
|
|
52
|
+
export async function runMcp(home) {
|
|
53
|
+
const workspace = process.cwd();
|
|
54
|
+
if (!existsSync(join(home, "launch.json")))
|
|
55
|
+
throw new Error("Agent Task is not initialized; run agent-task setup first");
|
|
56
|
+
startupPaths(home);
|
|
57
|
+
const ready = await connectBackground(home);
|
|
58
|
+
const paths = startupPaths(home);
|
|
59
|
+
let config;
|
|
60
|
+
try {
|
|
61
|
+
config = configSchema.parse(JSON.parse(readPrivateText(paths.configPath)));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error("Cannot read service configuration; inspect the private startup files");
|
|
65
|
+
}
|
|
66
|
+
const token = readEnvironment(paths.environmentPath)[config.tokenEnv];
|
|
67
|
+
if (!token)
|
|
68
|
+
throw new Error("Service credential is unavailable; inspect the private startup files");
|
|
69
|
+
await serveStdio({ url: ready.url, token, workspace });
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=mcp-entry.js.map
|
|
@@ -8,10 +8,10 @@ import { TaskRuns } from "../tasks/runs.js";
|
|
|
8
8
|
import { RunSupervisor } from "../tasks/supervisor.js";
|
|
9
9
|
import { TaskStore } from "../tasks/storage/store.js";
|
|
10
10
|
/** Creates and owns the business store and services; callers must stop ingress before closing. */
|
|
11
|
-
export function createTaskRuntime(directory, profiles, openExecution, coordinate, fatal) {
|
|
11
|
+
export function createTaskRuntime(directory, profiles, openExecution, coordinate, fatal, discovery, disabled = []) {
|
|
12
12
|
const store = new TaskStore(directory);
|
|
13
13
|
try {
|
|
14
|
-
const executors = new ExecutorRegistry(profiles);
|
|
14
|
+
const executors = new ExecutorRegistry(profiles, discovery, disabled);
|
|
15
15
|
const workspaces = { resolve: resolveWorkspace, check: checkWorkspace };
|
|
16
16
|
const results = new Results(new LocalResultFiles(join(directory, "results"), true));
|
|
17
17
|
const definitions = new TaskDefinitions(store, executors, workspaces, Boolean(coordinate));
|
|
@@ -25,7 +25,7 @@ export function createTaskRuntime(directory, profiles, openExecution, coordinate
|
|
|
25
25
|
const stopped = supervisor.close();
|
|
26
26
|
runs.close();
|
|
27
27
|
try {
|
|
28
|
-
await stopped;
|
|
28
|
+
await Promise.all([stopped, executors.close()]);
|
|
29
29
|
}
|
|
30
30
|
finally {
|
|
31
31
|
store.close();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { executorTypeSchema } from "../executors/validation.js";
|
|
3
3
|
import { baseEnvKeys } from "./policy.js";
|
|
4
4
|
const envKey = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
5
5
|
const modelId = z.string().min(1).max(200);
|
|
@@ -23,6 +23,6 @@ export const configSchema = z.object({
|
|
|
23
23
|
port: z.number().int().min(0).max(65535).default(8765),
|
|
24
24
|
tokenEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).default("AGENT_TASK_TOKEN"),
|
|
25
25
|
coordinator: coordinatorSchema.optional(),
|
|
26
|
-
|
|
26
|
+
disabledExecutors: z.array(executorTypeSchema).max(4).refine(types => new Set(types).size === types.length, "Disabled executor types must be unique").default([]),
|
|
27
27
|
}).strict();
|
|
28
28
|
//# sourceMappingURL=contract.js.map
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { checkDataDirectory, readPrivateText, replacePrivateText } from "../infrastructure/private-files.js";
|
|
4
|
-
import { resolveWorkspace } from "../infrastructure/workspace.js";
|
|
5
4
|
import { configSchema } from "./contract.js";
|
|
6
5
|
import { parseEnvironment } from "./environment.js";
|
|
7
6
|
import { validateConfig } from "./policy.js";
|
|
8
|
-
export const configurationPaths = {
|
|
7
|
+
export const configurationPaths = { checkDataDirectory };
|
|
9
8
|
export function loadConfig(path) {
|
|
10
9
|
let value;
|
|
11
10
|
try {
|
|
@@ -14,7 +13,7 @@ export function loadConfig(path) {
|
|
|
14
13
|
catch {
|
|
15
14
|
throw new Error("Cannot read configuration JSON");
|
|
16
15
|
}
|
|
17
|
-
return validateConfig(configSchema.parse(value)
|
|
16
|
+
return validateConfig(configSchema.parse(value));
|
|
18
17
|
}
|
|
19
18
|
export function readEnvironment(path) { return parseEnvironment(readPrivateText(path)); }
|
|
20
19
|
export class LocalSettingsFiles {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Configuration preview and task impact are one management operation for all clients. */
|
|
2
2
|
export function previewSettings(settings, tasks, input) {
|
|
3
3
|
const preview = settings.preview(input);
|
|
4
|
-
return { ...preview, affectedTasks: tasks.executorImpact(preview.config.
|
|
4
|
+
return { ...preview, affectedTasks: tasks.executorImpact(preview.config.disabledExecutors) };
|
|
5
5
|
}
|
|
6
6
|
//# sourceMappingURL=management.js.map
|
|
@@ -8,7 +8,7 @@ export function coordinatorCredentialEnv(config) {
|
|
|
8
8
|
case "openai-compatible": return config.apiKeyEnv;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
export function validateConfig(value
|
|
11
|
+
export function validateConfig(value) {
|
|
12
12
|
const config = structuredClone(value);
|
|
13
13
|
if (!config.dataDir.startsWith("/") || /[\x00-\x1f\x7f]/.test(config.dataDir))
|
|
14
14
|
throw new Error("dataDir must be absolute without control characters");
|
|
@@ -17,13 +17,8 @@ export function validateConfig(value, resolveWorkspace) {
|
|
|
17
17
|
throw new Error("tokenEnv must be separate from base environment, administrator and model credentials");
|
|
18
18
|
if (coordinatorKey === ADMIN_TOKEN_ENV)
|
|
19
19
|
throw new Error("Administrator token must not be used as a model credential");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (ids.has(executor.id))
|
|
23
|
-
throw new Error("Duplicate executor ID");
|
|
24
|
-
ids.add(executor.id);
|
|
25
|
-
executor.cwd = resolveWorkspace(executor.cwd);
|
|
26
|
-
}
|
|
20
|
+
if (new Set(config.disabledExecutors).size !== config.disabledExecutors.length)
|
|
21
|
+
throw new Error("Duplicate disabled executor type");
|
|
27
22
|
return config;
|
|
28
23
|
}
|
|
29
24
|
//# sourceMappingURL=policy.js.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv, validateConfig } from "./policy.js";
|
|
4
|
-
import { sameExecutor } from "../executors/registry.js";
|
|
5
4
|
import { parseEnvironment, validateServiceEnvironment } from "./environment.js";
|
|
6
5
|
import { FileCommitError } from "../shared/errors.js";
|
|
7
6
|
export class SettingsError extends Error {
|
|
@@ -94,18 +93,11 @@ export class Settings {
|
|
|
94
93
|
const environment = { ...this.base, ...snapshot.environment };
|
|
95
94
|
let config;
|
|
96
95
|
try {
|
|
97
|
-
config = validateConfig(input.config
|
|
96
|
+
config = validateConfig(input.config);
|
|
98
97
|
this.paths.checkDataDirectory(config.dataDir);
|
|
99
98
|
}
|
|
100
99
|
catch {
|
|
101
|
-
throw new SettingsError("invalid", "Local validation failed: check data directory,
|
|
102
|
-
}
|
|
103
|
-
for (const executor of config.executors) {
|
|
104
|
-
const previous = snapshot.config.executors.find(item => item.id === executor.id);
|
|
105
|
-
if (executor.unattendedApproved && (!previous?.unattendedApproved || !sameExecutor(previous, executor)) &&
|
|
106
|
-
!input.approveExecutors.includes(executor.id)) {
|
|
107
|
-
throw new SettingsError("invalid", "Executor changes require explicit approval of the new configuration: " + executor.id);
|
|
108
|
-
}
|
|
100
|
+
throw new SettingsError("invalid", "Local validation failed: check data directory, executor selection and credential isolation");
|
|
109
101
|
}
|
|
110
102
|
const previous = snapshot.config.coordinator, next = config.coordinator;
|
|
111
103
|
const endpoint = (model) => model.provider === "openai-compatible" ? model.baseUrl : model.provider;
|
|
@@ -140,33 +132,18 @@ export class Settings {
|
|
|
140
132
|
/** Convenience commands create a draft; all publication still uses saveConfig. */
|
|
141
133
|
prepareChange(value) {
|
|
142
134
|
const { editToken, change } = value;
|
|
143
|
-
const config = structuredClone(this.editing(editToken).config)
|
|
144
|
-
if (change.action === "
|
|
145
|
-
config.executors.push({ ...change.executor, unattendedApproved: false });
|
|
146
|
-
}
|
|
147
|
-
else if (change.action === "coordinator-set")
|
|
135
|
+
const config = structuredClone(this.editing(editToken).config);
|
|
136
|
+
if (change.action === "coordinator-set")
|
|
148
137
|
config.coordinator = change.coordinator;
|
|
149
138
|
else if (change.action === "coordinator-clear")
|
|
150
139
|
delete config.coordinator;
|
|
151
|
-
else {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
config.executors = config.executors.filter(item => item !== executor);
|
|
157
|
-
else if (change.action === "executor-update") {
|
|
158
|
-
const updated = { ...executor, ...change.patch };
|
|
159
|
-
if (!sameExecutor(executor, updated))
|
|
160
|
-
updated.unattendedApproved = false;
|
|
161
|
-
config.executors[config.executors.indexOf(executor)] = updated;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
executor.unattendedApproved = change.action === "executor-approve";
|
|
165
|
-
if (executor.unattendedApproved)
|
|
166
|
-
approveExecutors.push(executor.id);
|
|
167
|
-
}
|
|
140
|
+
else if (change.action === "executor-enable") {
|
|
141
|
+
config.disabledExecutors = config.disabledExecutors.filter(type => type !== change.id);
|
|
142
|
+
}
|
|
143
|
+
else if (!config.disabledExecutors.includes(change.id)) {
|
|
144
|
+
config.disabledExecutors.push(change.id);
|
|
168
145
|
}
|
|
169
|
-
const input = { editToken, config
|
|
146
|
+
const input = { editToken, config };
|
|
170
147
|
return { ...input, config: this.candidate(input) };
|
|
171
148
|
}
|
|
172
149
|
saveConfig(input) {
|
|
@@ -1,29 +1,131 @@
|
|
|
1
|
+
import { errorText, TaskError } from "../shared/errors.js";
|
|
1
2
|
export function sameExecutor(a, b) {
|
|
2
|
-
return a.id === b.id && a.type === b.type && a.
|
|
3
|
+
return a.id === b.id && a.type === b.type && a.nativePath === b.nativePath;
|
|
3
4
|
}
|
|
4
5
|
export function authorizedProfile(snapshot, current) {
|
|
5
|
-
return Boolean(snapshot
|
|
6
|
+
return Boolean(snapshot && current && sameExecutor(snapshot, current));
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
+
function status(executor, state = "ready") {
|
|
9
|
+
return { id: executor.id, type: executor.type, nativePath: executor.nativePath,
|
|
10
|
+
state, adapterVersion: executor.adapterVersion, error: null };
|
|
11
|
+
}
|
|
12
|
+
/** Owns the ready executor set and adapter installation lifetime, not native tool permissions. */
|
|
8
13
|
export class ExecutorRegistry {
|
|
14
|
+
discovery;
|
|
9
15
|
profiles;
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
states;
|
|
17
|
+
disabled;
|
|
18
|
+
installing = new Map();
|
|
19
|
+
closed = false;
|
|
20
|
+
closing;
|
|
21
|
+
constructor(profiles, discovery, disabled = []) {
|
|
22
|
+
this.discovery = discovery;
|
|
23
|
+
this.disabled = new Set(disabled);
|
|
24
|
+
const found = discovery?.discover();
|
|
25
|
+
this.profiles = structuredClone((found?.executors ?? profiles).filter(profile => !this.disabled.has(profile.type)));
|
|
26
|
+
this.states = found ? found.states.map(entry => this.disabled.has(entry.type)
|
|
27
|
+
? { ...entry, state: "disabled", error: null } : { ...entry })
|
|
28
|
+
: profiles.map(profile => status(profile, this.disabled.has(profile.type) ? "disabled" : "ready"));
|
|
29
|
+
}
|
|
30
|
+
checkOpen() { if (this.closed)
|
|
31
|
+
throw new TaskError("unavailable", "Executor registry closed"); }
|
|
32
|
+
list() { this.checkOpen(); return { executors: structuredClone(this.states) }; }
|
|
33
|
+
refresh() {
|
|
34
|
+
this.checkOpen();
|
|
35
|
+
if (!this.discovery)
|
|
36
|
+
return this.list();
|
|
37
|
+
const found = this.discovery.discover();
|
|
38
|
+
this.profiles = structuredClone(found.executors.filter(profile => !this.disabled.has(profile.type) && !this.installing.has(profile.type)));
|
|
39
|
+
this.states = found.states.map(entry => {
|
|
40
|
+
if (this.disabled.has(entry.type))
|
|
41
|
+
return { ...entry, state: "disabled", error: null };
|
|
42
|
+
if (this.installing.has(entry.type))
|
|
43
|
+
return { ...entry, state: "installing", error: null };
|
|
44
|
+
return { ...entry };
|
|
45
|
+
});
|
|
46
|
+
for (const entry of this.states) {
|
|
47
|
+
if (entry.state === "missing" && entry.nativePath !== null)
|
|
48
|
+
this.install(entry, false);
|
|
49
|
+
}
|
|
50
|
+
return this.list();
|
|
51
|
+
}
|
|
52
|
+
upgrade(id) {
|
|
53
|
+
this.checkOpen();
|
|
54
|
+
const entry = this.states.find(item => item.id === id);
|
|
55
|
+
if (!this.discovery || !entry || !entry.nativePath)
|
|
56
|
+
throw new TaskError("not_found", "Executor is not installed: " + id);
|
|
57
|
+
if (this.disabled.has(entry.type))
|
|
58
|
+
throw new TaskError("precondition", "Executor is disabled: " + id);
|
|
59
|
+
if (entry.type === "opencode")
|
|
60
|
+
throw new TaskError("precondition", "OpenCode uses its native ACP command; update the native CLI directly");
|
|
61
|
+
this.install(entry, true);
|
|
62
|
+
return this.list();
|
|
63
|
+
}
|
|
64
|
+
install(entry, upgrade) {
|
|
65
|
+
const discovery = this.discovery;
|
|
66
|
+
if (!discovery || this.installing.has(entry.type))
|
|
67
|
+
return;
|
|
68
|
+
const controller = new AbortController(), expectedPath = entry.nativePath;
|
|
69
|
+
this.profiles = this.profiles.filter(profile => profile.type !== entry.type);
|
|
70
|
+
Object.assign(entry, { state: "installing", error: null });
|
|
71
|
+
const done = Promise.resolve().then(async () => {
|
|
72
|
+
try {
|
|
73
|
+
if (this.closed || controller.signal.aborted)
|
|
74
|
+
return;
|
|
75
|
+
const executor = await discovery.install(entry.type, upgrade, controller.signal);
|
|
76
|
+
if (this.closed)
|
|
77
|
+
return;
|
|
78
|
+
if (executor.type !== entry.type || executor.id !== entry.id || executor.nativePath !== expectedPath) {
|
|
79
|
+
throw new TaskError("conflict", "Native executor changed during adapter installation; refresh discovery");
|
|
80
|
+
}
|
|
81
|
+
discovery.check(executor);
|
|
82
|
+
this.profiles = [...this.profiles.filter(profile => profile.type !== entry.type), structuredClone(executor)];
|
|
83
|
+
this.states = this.states.map(item => item.type === entry.type ? status(executor) : item);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!this.closed)
|
|
87
|
+
this.states = this.states.map(item => item.type === entry.type ?
|
|
88
|
+
{ ...item, state: "error", error: errorText(error) } : item);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.installing.delete(entry.type);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
this.installing.set(entry.type, { controller, done });
|
|
95
|
+
}
|
|
96
|
+
ready(id) {
|
|
97
|
+
const profile = this.profiles.find(item => item.id === id);
|
|
98
|
+
if (!profile) {
|
|
99
|
+
const entry = this.states.find(item => item.id === id);
|
|
100
|
+
throw new TaskError("precondition", "Executor is not ready: " + id + (entry ? " (" + entry.state + ")" : ""));
|
|
101
|
+
}
|
|
102
|
+
this.discovery?.check(profile);
|
|
103
|
+
return profile;
|
|
104
|
+
}
|
|
12
105
|
snapshot(ids) {
|
|
106
|
+
this.checkOpen();
|
|
13
107
|
const result = Object.create(null);
|
|
14
|
-
for (const id of ids)
|
|
15
|
-
|
|
16
|
-
if (!profile?.unattendedApproved)
|
|
17
|
-
throw new Error("Executor not found or not approved: " + id);
|
|
18
|
-
result[id] = structuredClone(profile);
|
|
19
|
-
}
|
|
108
|
+
for (const id of ids)
|
|
109
|
+
result[id] = structuredClone(this.ready(id));
|
|
20
110
|
return result;
|
|
21
111
|
}
|
|
22
112
|
authorize(snapshot) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
113
|
+
this.checkOpen();
|
|
114
|
+
if (!snapshot)
|
|
115
|
+
throw new TaskError("precondition", "Executor snapshot is missing");
|
|
116
|
+
const current = this.ready(snapshot.id);
|
|
117
|
+
if (!authorizedProfile(snapshot, current))
|
|
118
|
+
throw new TaskError("conflict", "Native executor changed; existing task assignment cannot be replaced");
|
|
119
|
+
return structuredClone(current);
|
|
120
|
+
}
|
|
121
|
+
close() {
|
|
122
|
+
if (this.closing)
|
|
123
|
+
return this.closing;
|
|
124
|
+
this.closed = true;
|
|
125
|
+
for (const entry of this.installing.values())
|
|
126
|
+
entry.controller.abort();
|
|
127
|
+
this.closing = Promise.all([...this.installing.values()].map(entry => entry.done)).then(() => undefined);
|
|
128
|
+
return this.closing;
|
|
27
129
|
}
|
|
28
130
|
}
|
|
29
131
|
//# sourceMappingURL=registry.js.map
|