@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.
Files changed (60) hide show
  1. package/CONTEXT.md +17 -0
  2. package/README.md +28 -6
  3. package/bin/agent-task.mjs +18 -6
  4. package/config.example.json +1 -1
  5. package/dist/application/launcher.js +30 -16
  6. package/dist/application/main.js +5 -2
  7. package/dist/application/mcp-entry.js +71 -0
  8. package/dist/application/task-runtime.js +3 -3
  9. package/dist/configuration/contract.js +2 -2
  10. package/dist/configuration/files.js +2 -3
  11. package/dist/configuration/management.js +1 -1
  12. package/dist/configuration/policy.js +3 -8
  13. package/dist/configuration/settings.js +10 -33
  14. package/dist/executors/ports.js +2 -0
  15. package/dist/executors/registry.js +117 -15
  16. package/dist/executors/validation.js +4 -5
  17. package/dist/infrastructure/lock.js +25 -13
  18. package/dist/infrastructure/workspace.js +4 -3
  19. package/dist/integrations/acp/connection.js +5 -5
  20. package/dist/integrations/acp/discovery.js +271 -0
  21. package/dist/integrations/acp/runtime.js +38 -23
  22. package/dist/integrations/acp/worker.js +5 -3
  23. package/dist/integrations/local-service/control.js +36 -11
  24. package/dist/protocol/cli/commands.js +41 -46
  25. package/dist/protocol/http/server.js +8 -0
  26. package/dist/protocol/http/web.js +7 -1
  27. package/dist/protocol/mcp-contract.js +194 -0
  28. package/dist/protocol/mcp.js +99 -32
  29. package/dist/protocol/settings-contract.js +4 -7
  30. package/dist/protocol/stdio.js +72 -0
  31. package/dist/protocol/task-contract.js +45 -18
  32. package/dist/shared/errors.js +15 -0
  33. package/dist/shared/limits.js +3 -0
  34. package/dist/tasks/definitions.js +19 -14
  35. package/dist/tasks/graph.js +5 -4
  36. package/dist/tasks/human-input.js +10 -9
  37. package/dist/tasks/runs.js +31 -30
  38. package/dist/tasks/schedule.js +2 -1
  39. package/dist/tasks/storage/records.js +6 -6
  40. package/dist/tasks/storage/store.js +8 -7
  41. package/dist/tasks/supervisor.js +10 -7
  42. package/dist/tasks/validation.js +3 -0
  43. package/dist/web/settings-view.js +43 -37
  44. package/docs/adr/0001-task-request-scope.md +3 -0
  45. package/docs/adr/0002-mcp-workspace-source.md +3 -0
  46. package/docs/adr/0003-adapter-installation.md +3 -0
  47. package/docs/architecture-guidelines.md +2 -2
  48. package/docs/cli.md +12 -4
  49. package/docs/deployment.md +52 -36
  50. package/docs/design-proposal.md +3 -0
  51. package/docs/release.md +27 -8
  52. package/docs/technical-design.md +5 -3
  53. package/docs/validation.md +59 -9
  54. package/docs/web-console.md +7 -4
  55. package/package.json +3 -1
  56. package/scripts/install-lib.mjs +5 -23
  57. package/scripts/probe-agent.mjs +10 -9
  58. package/scripts/service-runner.mjs +8 -3
  59. package/scripts/setup-wizard.mjs +38 -11
  60. 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+**。安装包自带运行产物,无需克隆或编译。`latest` 指向的已发布版本及平台验收范围见[发布证据](docs/validation.md#npm-发布候选验证)。
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 start
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 提供交互配置;启动后可使用本机 Web 控制台或 [CLI](docs/cli.md),两端共用管理服务。安装器不会替用户登录或批准执行端;详细操作见[部署说明](docs/deployment.md)。
37
+ npm 包名为 `@realqiyan/agent-task`,安装后的命令仍为 `agent-task`。setup 要求确认目录、端口、令牌与协调模型。MCP 连接自动启动或复用独立后台,检测本机执行 CLI,并在缺少 ACP 适配器时安装当时最新版;已有适配器复用,原生登录仍由其官方工具管理。宿主退出后后台继续运行。接入配置与前台管理方式见[部署说明](docs/deployment.md)。
37
38
 
38
- MCP 客户端使用独立服务 token。源码开发辅助命令(凭据须安全注入环境,不写命令行):
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,每个节点选择已注册执行端。具体契约见 [task-contract.ts](src/protocol/task-contract.ts),管理工具见 [mcp.ts](src/protocol/mcp.ts)。Prompt、人工回复和工作空间选择都不授予工具权限。
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
 
@@ -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 Manage registrations and unattended authorization
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 (command === "start") {
32
- if (args.includes("--help")) { console.log("Usage: agent-task start [--home ABSOLUTE_DIR]"); }
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 options = lib.argumentsFor(args);
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
- process.argv = [process.execPath, "service-runner", root];
38
- await import("../scripts/service-runner.mjs");
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");
@@ -3,5 +3,5 @@
3
3
  "port": 8765,
4
4
  "tokenEnv": "AGENT_TASK_TOKEN",
5
5
  "coordinator": { "provider": "openai", "model": "YOUR_AVAILABLE_MODEL_ID" },
6
- "executors": []
6
+ "disabledExecutors": []
7
7
  }
@@ -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 = async () => {
89
- if (closing)
90
- return;
96
+ const shutdown = (drain = false) => {
97
+ if (shutting)
98
+ return shutting;
91
99
  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
- }
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
@@ -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
- tasks = createTaskRuntime(config.dataDir, config.executors, createExecution(executorEnvironment(native, environment, config)), config.coordinator ? createCoordinator(config.dataDir, config.coordinator) : undefined, fatal);
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 { executorSchema } from "../executors/validation.js";
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
- executors: z.array(executorSchema).max(100).default([]),
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 = { resolveWorkspace, checkDataDirectory };
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), resolveWorkspace);
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.executors) };
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, resolveWorkspace) {
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
- const ids = new Set();
21
- for (const executor of config.executors) {
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, this.paths.resolveWorkspace);
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, workspaces and credential isolation");
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), approveExecutors = [];
144
- if (change.action === "executor-add") {
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
- const executor = config.executors.find(item => item.id === change.id);
153
- if (!executor)
154
- throw new SettingsError("invalid", "Executor not found");
155
- if (change.action === "executor-remove")
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, approveExecutors };
146
+ const input = { editToken, config };
170
147
  return { ...input, config: this.candidate(input) };
171
148
  }
172
149
  saveConfig(input) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -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.cwd === b.cwd;
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?.unattendedApproved && current?.unattendedApproved && sameExecutor(snapshot, current));
6
+ return Boolean(snapshot && current && sameExecutor(snapshot, current));
6
7
  }
7
- /** Immutable registrations for one service lifetime. Settings apply to the next instance. */
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
- constructor(profiles) { this.profiles = structuredClone(profiles); }
11
- list() { return { executors: this.profiles.map(profile => ({ ...profile, transport: "stdio" })) }; }
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
- const profile = this.profiles.find(item => item.id === id);
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
- if (!snapshot || !authorizedProfile(snapshot, this.profiles.find(profile => profile.id === snapshot.id))) {
24
- throw new Error("Executor authorization revoked or changed");
25
- }
26
- return structuredClone(snapshot);
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