@realqiyan/agent-task 0.0.1-rc.2 → 0.1.0-rc.1

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 (99) hide show
  1. package/CONTEXT.md +17 -0
  2. package/README.md +30 -7
  3. package/bin/agent-task.mjs +35 -36
  4. package/config.example.json +1 -1
  5. package/dist/{launcher.js → application/launcher.js} +49 -58
  6. package/dist/application/main.js +125 -0
  7. package/dist/application/mcp-entry.js +71 -0
  8. package/dist/{startup.js → application/startup.js} +6 -11
  9. package/dist/application/task-runtime.js +42 -0
  10. package/dist/configuration/contract.js +28 -0
  11. package/dist/{environment.js → configuration/environment.js} +1 -5
  12. package/dist/configuration/files.js +42 -0
  13. package/dist/configuration/management.js +6 -0
  14. package/dist/configuration/model.js +2 -0
  15. package/dist/configuration/policy.js +24 -0
  16. package/dist/configuration/ports.js +2 -0
  17. package/dist/{settings.js → configuration/settings.js} +54 -39
  18. package/dist/executors/environment.js +10 -0
  19. package/dist/executors/model.js +2 -0
  20. package/dist/executors/ports.js +2 -0
  21. package/dist/executors/registry.js +131 -0
  22. package/dist/executors/validation.js +9 -0
  23. package/dist/infrastructure/lock.js +55 -0
  24. package/dist/{private-files.js → infrastructure/private-files.js} +1 -7
  25. package/dist/infrastructure/sqlite.js +19 -0
  26. package/dist/infrastructure/version.js +3 -0
  27. package/dist/{acp.js → integrations/acp/connection.js} +13 -11
  28. package/dist/integrations/acp/discovery.js +271 -0
  29. package/dist/{node-execution.js → integrations/acp/execution.js} +1 -1
  30. package/dist/integrations/acp/runtime.js +43 -0
  31. package/dist/{worker.js → integrations/acp/worker.js} +7 -4
  32. package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
  33. package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
  34. package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
  35. package/dist/integrations/local-service/control.js +58 -0
  36. package/dist/protocol/cli/admin-client.js +45 -0
  37. package/dist/protocol/cli/commands.js +389 -0
  38. package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
  39. package/dist/protocol/http/server.js +119 -0
  40. package/dist/{web.js → protocol/http/web.js} +24 -17
  41. package/dist/{mcp.js → protocol/mcp.js} +13 -13
  42. package/dist/protocol/services.js +2 -0
  43. package/dist/protocol/settings-contract.js +37 -0
  44. package/dist/protocol/stdio.js +71 -0
  45. package/dist/protocol/task-contract.js +28 -0
  46. package/dist/results/files.js +70 -0
  47. package/dist/results/ports.js +2 -0
  48. package/dist/results/service.js +42 -0
  49. package/dist/shared/errors.js +13 -0
  50. package/dist/shared/limits.js +3 -0
  51. package/dist/shared/time.js +2 -0
  52. package/dist/shared/validation.js +8 -0
  53. package/dist/tasks/definitions.js +86 -0
  54. package/dist/tasks/graph.js +23 -0
  55. package/dist/{human-input.js → tasks/human-input.js} +8 -7
  56. package/dist/tasks/model.js +2 -0
  57. package/dist/tasks/ports.js +2 -0
  58. package/dist/tasks/runs.js +263 -0
  59. package/dist/tasks/schedule.js +36 -0
  60. package/dist/tasks/sessions.js +62 -0
  61. package/dist/tasks/storage/records.js +45 -0
  62. package/dist/{task-storage.js → tasks/storage/store.js} +28 -40
  63. package/dist/tasks/supervisor.js +227 -0
  64. package/dist/tasks/validation.js +24 -0
  65. package/dist/web/settings-view.js +46 -33
  66. package/docs/adr/0001-task-request-scope.md +3 -0
  67. package/docs/adr/0002-mcp-workspace-source.md +3 -0
  68. package/docs/adr/0003-adapter-installation.md +3 -0
  69. package/docs/architecture-guidelines.md +8 -6
  70. package/docs/cli.md +57 -0
  71. package/docs/deployment.md +46 -70
  72. package/docs/design-proposal.md +5 -2
  73. package/docs/release.md +14 -15
  74. package/docs/technical-design.md +49 -26
  75. package/docs/validation.md +92 -18
  76. package/docs/web-console.md +16 -11
  77. package/package.json +9 -7
  78. package/scripts/install-lib.mjs +7 -116
  79. package/scripts/mcp-call.mjs +1 -1
  80. package/scripts/probe-agent.mjs +14 -12
  81. package/scripts/service-runner.mjs +9 -4
  82. package/scripts/setup-wizard.mjs +38 -16
  83. package/scripts/setup.mjs +69 -108
  84. package/deploy/agent-task.service.example +0 -17
  85. package/dist/admin.js +0 -31
  86. package/dist/config.js +0 -72
  87. package/dist/executor-runtime.js +0 -37
  88. package/dist/lock.js +0 -43
  89. package/dist/main.js +0 -175
  90. package/dist/model.js +0 -25
  91. package/dist/results.js +0 -79
  92. package/dist/settings-contract.js +0 -16
  93. package/dist/task-contract.js +0 -37
  94. package/dist/task-model.js +0 -2
  95. package/dist/tasks.js +0 -508
  96. package/dist/version.js +0 -3
  97. package/scripts/uninstall.mjs +0 -57
  98. /package/dist/{permissions.js → executors/permissions.js} +0 -0
  99. /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
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
@@ -8,9 +8,11 @@
8
8
 
9
9
  | 目的 | 入口 |
10
10
  | --- | --- |
11
- | 安装、认证、后台运行、卸载 | [部署说明](docs/deployment.md) |
11
+ | 安装、配置、运行 | [部署说明](docs/deployment.md) |
12
12
  | 本机控制台、配置保存与 Web/CLI 重启 | [控制台说明](docs/web-console.md) |
13
+ | 命令行配置、凭据和任务管理 | [CLI 说明](docs/cli.md) |
13
14
  | 理解产品目标和架构选择 | [总体方案](docs/design-proposal.md) |
15
+ | 查询项目术语 | [词汇表](CONTEXT.md) |
14
16
  | 查找实现与对应测试 | [代码导航](docs/technical-design.md) |
15
17
  | 查询测试命令、真实验收及缺口 | [验证说明](docs/validation.md) |
16
18
  | npm 候选验收、GitHub Actions 与受控发布 | [发布说明](docs/release.md) |
@@ -20,28 +22,49 @@
20
22
 
21
23
  ## 使用入口
22
24
 
23
- Linux/macOS,Node.js **22.19+**。安装包自带运行产物,无需克隆或编译。`latest` 指向的已发布版本及平台验收范围见[发布证据](docs/validation.md#npm-发布候选验证)。
25
+ Linux/macOS,Node.js **22.19+**。以下描述 `0.1.0-rc.1` 的使用入口;安装包自带运行产物。实际发布状态及平台验收范围见[发布证据](docs/validation.md#npm-发布候选验证)。
24
26
 
25
27
  ```bash
26
28
  npm install -g @realqiyan/agent-task
27
29
  agent-task setup
28
- agent-task start
29
- # 另一个终端进行带认证的健康检查:
30
+ # 在 MCP 宿主中配置 command: agent-task,args: [mcp]
31
+ # 连接后可查询后台状态:
30
32
  agent-task status
31
33
  # 应用已保存配置并确认新实例可用:
32
34
  agent-task restart
33
35
  ```
34
36
 
35
- npm 包名为 `@realqiyan/agent-task`,安装后的命令仍为 `agent-task`。setup 提供交互配置;启动后可使用本机 Web 控制台。先停止前台实例,再用 `agent-task service install` 安装并启动用户后台服务。安装器不会替用户登录或批准执行端;详细操作见[部署说明](docs/deployment.md)。
37
+ npm 包名为 `@realqiyan/agent-task`,安装后的命令仍为 `agent-task`。setup 要求确认目录、端口、令牌与协调模型。MCP 连接自动启动或复用独立后台,检测本机执行 CLI,并在缺少 ACP 适配器时安装当时最新版;已有适配器复用,原生登录仍由其官方工具管理。宿主退出后后台继续运行。接入配置与前台管理方式见[部署说明](docs/deployment.md)。
36
38
 
37
- MCP 客户端使用独立服务 token。源码开发辅助命令(凭据须安全注入环境,不写命令行):
39
+ stdio 入口从私有文件读取内部服务 token;直接 HTTP 接入需配置独立服务 token。源码开发辅助命令(凭据须安全注入环境,不写命令行):
38
40
 
39
41
  ```bash
40
42
  node scripts/mcp-call.mjs --list
41
43
  node scripts/mcp-call.mjs service_status
42
44
  ```
43
45
 
44
- 任务由 `task_create` 提交固定 DAG,每个节点选择已注册执行端。具体契约见 [task-contract.ts](src/task-contract.ts),管理工具见 [mcp.ts](src/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` | — | 查看版本 |
45
68
 
46
69
  ## 许可证
47
70
 
@@ -3,17 +3,20 @@ 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
7
- restart [--home PATH] Restart foreground or managed runtime; verify readiness
8
- status [--home PATH] Authenticated MCP health check; no token printed
9
- service install [--home PATH]
10
- Install/start systemd user service or LaunchAgent
11
- service remove [--home PATH]
12
- Stop/remove only the owned service
13
- uninstall [--home PATH] Remove service and owned executor components;
14
- preserve configuration, credentials and task data
8
+ stop [--home PATH] Stop the shared service
9
+ restart [--home PATH] Restart runtime; verify readiness
10
+ status [--home PATH] Health, active work and pending configuration
11
+ config Show, edit, validate and apply configuration
12
+ executor Discover executors, upgrade adapters and configure enable/disable
13
+ coordinator Configure the coordination model
14
+ credential List, replace or delete private credentials
15
+ task Inspect tasks and control future triggers
16
+ run Inspect run history and deliveries
17
+ <command> --help Command usage; management supports --json
15
18
  --version | --help
16
- Node.js 22.19+; Linux or macOS. Setup never grants executor permissions.`;
19
+ Node.js 22.19+; Linux or macOS.`;
17
20
  try {
18
21
  const [major, minor] = process.versions.node.split(".").map(Number);
19
22
  if (major < 22 || (major === 22 && minor < 19)) throw new Error("Node.js 22.19+ is required");
@@ -22,40 +25,36 @@ try {
22
25
  console.log(help);
23
26
  } else if (command === "--version") {
24
27
  console.log(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
25
- } else if (command === "setup" || command === "uninstall") {
28
+ } else if (command === "setup") {
26
29
  process.argv = [process.execPath, command, ...args];
27
30
  await import(`../scripts/${command}.mjs`);
28
31
  } else {
29
32
  const lib = await import("../scripts/install-lib.mjs");
30
- const action = command === "service" ? args.shift() : undefined;
31
- if (!["start", "status", "restart", "service"].includes(command) || (command === "service" && !["install", "remove"].includes(action))) throw new Error(help);
32
- const options = lib.argumentsFor(args);
33
- if (Object.keys(options).some(key => key !== "home")) throw new Error("Only --home is accepted here");
34
- const root = lib.installRoot(options);
35
- if (action === "install") {
36
- process.argv = [process.execPath, "setup", "--home", root, "--service"];
37
- await import("../scripts/setup.mjs");
38
- } else if (action === "remove") {
39
- lib.readManifest(root);
40
- const unlock = lib.operationLock(root);
41
- try {
42
- const manifest = lib.readManifest(root);
43
- await lib.deactivate(manifest);
44
- manifest.serviceInstalled = false;
45
- lib.saveManifest(root, manifest);
46
- console.log("Owned service removed; configuration, credentials, components and data preserved.");
47
- } finally { unlock(); }
48
- } else if (command === "start") {
49
- if (lib.readManifest(root).serviceInstalled) throw new Error("Remove the managed service before starting a foreground instance");
50
- process.argv = [process.execPath, "service-runner", root];
51
- await import("../scripts/service-runner.mjs");
33
+ if (["start", "mcp", "stop"].includes(command)) {
34
+ if (args.includes("--help")) { console.log("Usage: agent-task " + command + " [--home ABSOLUTE_DIR]" + (command === "stop" ? " [--json]" : "")); }
35
+ else {
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);
39
+ if (Object.keys(options).some(key => key !== "home")) throw new Error("Only --home is accepted here");
40
+ const root = lib.installRoot(options);
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
+ }
51
+ }
52
52
  } else {
53
- const { controlService } = await import("../dist/launcher.js");
54
- const ready = await controlService(root, command);
55
- console.log(JSON.stringify({ event: command === "restart" ? "restarted" : "healthy", ...ready }));
53
+ const { runCli } = await import("../dist/protocol/cli/commands.js");
54
+ await runCli(command, args, home => lib.installRoot({ home }));
56
55
  }
57
56
  }
58
57
  } catch (error) {
59
- console.error(error.message);
58
+ console.error(process.argv.includes("--json") ? JSON.stringify({ error: error.message }) : error.message);
60
59
  process.exitCode = 1;
61
60
  }
@@ -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
  }
@@ -1,13 +1,12 @@
1
1
  import { fork } from "node:child_process";
2
- import { createServer, request } from "node:http";
2
+ import { createServer } from "node:http";
3
3
  import { appendFileSync, chmodSync, closeSync, constants, existsSync, fstatSync, lstatSync, openSync, unlinkSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { randomUUID } from "node:crypto";
7
- import { acquireLauncherLock } from "./lock.js";
8
- import { prepareStartup, startupPaths } from "./startup.js";
9
- import { readEnvironment } from "./environment.js";
10
- import { loadConfig } from "./config.js";
7
+ import { acquireLauncherLock } from "../infrastructure/lock.js";
8
+ import { prepareStartup } from "./startup.js";
9
+ import { startupPaths } from "../configuration/files.js";
11
10
  function message(child, type, timeout = 30_000) {
12
11
  return new Promise((resolve, reject) => {
13
12
  const cleanup = () => { clearTimeout(timer); child.off("message", receive); child.off("exit", exit); child.off("error", fail); };
@@ -67,17 +66,25 @@ function logFile(path) {
67
66
  export async function launch(home, nativeEnvironment) {
68
67
  process.umask(0o077);
69
68
  startupPaths(home); // Also checks private ownership of the locator.
70
- const unlock = acquireLauncherLock(home);
69
+ const unlock = await acquireLauncherLock(home);
71
70
  const socket = join(home, "control.sock"), children = new Set();
72
71
  let current, ready, restarting = true, closing = false;
72
+ let shutting;
73
73
  let release;
74
74
  const finished = new Promise(resolve => { release = resolve; });
75
75
  const control = createServer(async (req, res) => {
76
76
  try {
77
- if (req.method !== "POST" || !["/status", "/restart"].includes(req.url ?? "")) {
77
+ if (req.method !== "POST" || !["/status", "/restart", "/stop"].includes(req.url ?? "")) {
78
78
  res.writeHead(404).end();
79
79
  return;
80
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
+ }
81
88
  const result = req.url === "/restart" ? await restart() : await status();
82
89
  res.setHeader("Content-Type", "application/json");
83
90
  res.end(JSON.stringify(result));
@@ -86,31 +93,36 @@ export async function launch(home, nativeEnvironment) {
86
93
  res.writeHead(503).end(JSON.stringify({ error: "Service unavailable; inspect private logs and startup files" }));
87
94
  }
88
95
  });
89
- const shutdown = async () => {
90
- if (closing)
91
- return;
96
+ const shutdown = (drain = false) => {
97
+ if (shutting)
98
+ return shutting;
92
99
  closing = true;
93
- control.close();
94
- control.closeAllConnections();
95
- await Promise.all([...children].map(stop));
96
- try {
97
- removeSocket(socket);
98
- }
99
- finally {
100
- unlock();
101
- release();
102
- }
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
+ })();
103
113
  };
104
114
  const signal = () => { void shutdown(); };
115
+ const hangup = () => { }; // A detached launcher survives the originating terminal closing.
105
116
  process.on("SIGINT", signal);
106
117
  process.on("SIGTERM", signal);
118
+ process.on("SIGHUP", hangup);
107
119
  const start = async () => {
108
120
  const inputs = await prepareStartup(home);
109
121
  if (closing)
110
122
  throw new Error("Launcher is closing");
111
123
  const log = logFile(join(inputs.dataDir, "service.log"));
112
124
  const child = fork(fileURLToPath(new URL("./main.js", import.meta.url)), ["--config", inputs.configPath, "--environment", inputs.environmentPath], {
113
- 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"],
114
126
  });
115
127
  children.add(child);
116
128
  const output = (chunk) => { appendFileSync(log, chunk); process.stdout.write(chunk); };
@@ -144,7 +156,7 @@ export async function launch(home, nativeEnvironment) {
144
156
  const value = (await message(child, "ready")).ready;
145
157
  await health(value, inputs.environment[inputs.config.tokenEnv]);
146
158
  current = child;
147
- ready = value;
159
+ ready = { info: value, token: inputs.environment[inputs.config.tokenEnv] };
148
160
  return value;
149
161
  }
150
162
  catch (error) {
@@ -155,28 +167,35 @@ export async function launch(home, nativeEnvironment) {
155
167
  const status = async () => {
156
168
  if (!ready || restarting || closing)
157
169
  throw new Error("Service unavailable");
158
- const paths = startupPaths(home), config = loadConfig(paths.configPath), env = readEnvironment(paths.environmentPath);
159
- await health(ready, env[config.tokenEnv]);
160
- return ready;
170
+ // Saved inputs may need repair. Health belongs to the running instance,
171
+ // including the credential with which that instance actually started.
172
+ const running = ready;
173
+ await health(running.info, running.token);
174
+ return running.info;
161
175
  };
162
176
  const restart = async (requester) => {
163
177
  if (restarting || closing)
164
178
  throw new Error("Restart already in progress or closed");
165
179
  restarting = true;
166
180
  const old = current;
167
- current = undefined;
168
- ready = undefined;
169
181
  try {
170
182
  if (old) {
171
183
  const drained = message(old, "drained");
172
184
  old.send({ type: "drain" });
185
+ let result;
173
186
  try {
174
- await drained;
187
+ result = await drained;
175
188
  }
176
189
  catch (error) {
190
+ current = undefined;
191
+ ready = undefined;
177
192
  await stop(old);
178
193
  throw error;
179
194
  }
195
+ if (result.error)
196
+ throw new Error(result.error);
197
+ current = undefined;
198
+ ready = undefined;
180
199
  // Keep only the authenticated Web response alive until the new process is verified.
181
200
  if (old !== requester)
182
201
  await stop(old);
@@ -186,7 +205,7 @@ export async function launch(home, nativeEnvironment) {
186
205
  }
187
206
  finally {
188
207
  restarting = false;
189
- if (requester) {
208
+ if (requester && requester !== current) {
190
209
  const timer = setTimeout(() => { void stop(requester); }, 5000);
191
210
  requester.once("close", () => clearTimeout(timer));
192
211
  }
@@ -204,35 +223,7 @@ export async function launch(home, nativeEnvironment) {
204
223
  await shutdown();
205
224
  process.off("SIGINT", signal);
206
225
  process.off("SIGTERM", signal);
226
+ process.off("SIGHUP", hangup);
207
227
  }
208
228
  }
209
- /** Local administrator capability: the socket lives in the private launcher directory. */
210
- export function controlService(home, action) {
211
- startupPaths(home);
212
- return new Promise((resolve, reject) => {
213
- let timedOut = false;
214
- const req = request({ socketPath: join(home, "control.sock"), path: "/" + action, method: "POST" }, res => {
215
- let body = "";
216
- res.setEncoding("utf8");
217
- res.on("data", chunk => { body += chunk; });
218
- res.on("end", () => {
219
- if (res.statusCode !== 200) {
220
- reject(new Error("Service unavailable; inspect private logs and startup files"));
221
- return;
222
- }
223
- try {
224
- resolve(JSON.parse(body));
225
- }
226
- catch {
227
- reject(new Error("Invalid launcher response"));
228
- }
229
- });
230
- });
231
- req.setTimeout(60_000, () => { timedOut = true; req.destroy(); });
232
- req.once("error", () => reject(new Error(timedOut
233
- ? "Service control timed out; outcome is unknown, check status before another restart"
234
- : "Cannot contact launcher; start the foreground or managed service first")));
235
- req.end();
236
- });
237
- }
238
229
  //# sourceMappingURL=launcher.js.map
@@ -0,0 +1,125 @@
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 { dirname, join, 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
+ import { LocalExecutors } from "../integrations/acp/discovery.js";
16
+ process.umask(0o077);
17
+ const args = process.argv.slice(2);
18
+ if (args[0] !== "--config" || args.length !== 4 || args[2] !== "--environment") {
19
+ console.error("Internal runtime requires --config PATH --environment PATH; use agent-task start");
20
+ process.exit(2);
21
+ }
22
+ const configPath = resolve(args[1]), environmentPath = resolve(args[3]);
23
+ const native = { ...process.env }, base = baseEnvironment(native);
24
+ const environment = readEnvironment(environmentPath);
25
+ const instanceId = process.env.AGENT_TASK_INSTANCE_ID ?? randomUUID();
26
+ for (const key of Object.keys(process.env))
27
+ delete process.env[key];
28
+ Object.assign(process.env, base, environment);
29
+ const config = loadConfig(configPath);
30
+ const { token, adminToken } = validateServiceEnvironment(config, environment);
31
+ let http;
32
+ let tasks, settings;
33
+ let release;
34
+ let shutting;
35
+ function shutdown(code, drain = false) {
36
+ process.exitCode = Math.max(process.exitCode ? Number(process.exitCode) : 0, code);
37
+ return shutting ??= (async () => {
38
+ http?.stopAccepting();
39
+ settings?.close();
40
+ try {
41
+ await tasks?.close();
42
+ }
43
+ catch (error) {
44
+ console.error(errorText(error));
45
+ process.exitCode = 1;
46
+ }
47
+ await http?.close(drain);
48
+ await release?.();
49
+ })().catch(error => { console.error(errorText(error)); process.exitCode = 1; });
50
+ }
51
+ const fatal = (error) => { console.error(errorText(error)); void shutdown(1).finally(() => { if (process.connected)
52
+ process.disconnect(); }); };
53
+ let restartPending = false;
54
+ const restart = async () => {
55
+ if (!process.connected || restartPending || shutting)
56
+ throw new Error("Restart unavailable");
57
+ restartPending = true;
58
+ return new Promise((resolve, reject) => {
59
+ process.once("message", function receive(value) {
60
+ if (value.type !== "restarted") {
61
+ process.once("message", receive);
62
+ return;
63
+ }
64
+ if (value.error && !shutting) {
65
+ restartPending = false;
66
+ reject(new Error(value.error));
67
+ return;
68
+ }
69
+ // The HTTP adapter finishes the authenticated response before this child exits.
70
+ setTimeout(() => { if (process.connected)
71
+ process.disconnect(); http?.closeConnections(); }, 200);
72
+ if (value.error)
73
+ reject(new Error(value.error));
74
+ else
75
+ resolve(value.ready);
76
+ });
77
+ process.send({ type: "restart" });
78
+ });
79
+ };
80
+ process.on("message", (value) => {
81
+ if (value.type !== "drain")
82
+ return;
83
+ // Both Web and CLI restarts pass this boundary before changing runtime ownership.
84
+ try {
85
+ if (settings?.status().writeBlocked)
86
+ throw new Error("Configuration publication is uncertain");
87
+ }
88
+ catch {
89
+ process.send?.({ type: "drained", error: "Restart blocked; stop and inspect private configuration files" });
90
+ return;
91
+ }
92
+ void shutdown(0, true).then(() => {
93
+ if (process.exitCode) {
94
+ if (process.connected)
95
+ process.disconnect();
96
+ }
97
+ else
98
+ process.send?.({ type: "drained" });
99
+ });
100
+ });
101
+ const stop = () => { void shutdown(0).finally(() => { http?.closeConnections(); if (process.connected)
102
+ process.disconnect(); }); };
103
+ process.once("SIGINT", stop);
104
+ process.once("SIGTERM", stop);
105
+ process.once("disconnect", stop);
106
+ try {
107
+ release = await acquireLock(config.dataDir);
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);
111
+ if (adminToken)
112
+ settings = new Settings(new LocalSettingsFiles(configPath, environmentPath), configurationPaths, config, base, process.env);
113
+ http = new HttpServer({ tasks, settings, token, adminToken, fatal, restart, instanceId });
114
+ const origin = await http.listen(config.port);
115
+ if (shutting)
116
+ throw new Error("Startup stopped");
117
+ tasks.supervisor.start();
118
+ const ready = { instanceId, pid: process.pid, url: origin + "/mcp", consoleUrl: settings ? origin + "/admin/" : null };
119
+ console.log(JSON.stringify({ event: "ready", ...ready }));
120
+ process.send?.({ type: "ready", ready });
121
+ }
122
+ catch (error) {
123
+ fatal(error);
124
+ }
125
+ //# sourceMappingURL=main.js.map
@@ -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
@@ -1,16 +1,11 @@
1
+ import { startupPaths } from "../configuration/files.js";
1
2
  import { existsSync, mkdirSync, writeFileSync, openSync, fsyncSync, closeSync } from "node:fs";
2
3
  import { join } from "node:path";
3
- import { z } from "zod";
4
- import { loadConfig } from "./config.js";
5
- import { readEnvironment, validateServiceEnvironment } from "./environment.js";
6
- import { checkDataDirectory, readPrivateText, replacePrivateText } from "./private-files.js";
7
- import { acquireLock } from "./lock.js";
8
- /** A stable launch locator is the only runtime input outside the selected dataDir. */
9
- export function startupPaths(home) {
10
- const { dataDir } = z.object({ dataDir: z.string() }).strict().parse(JSON.parse(readPrivateText(join(home, "launch.json"))));
11
- checkDataDirectory(dataDir);
12
- return { dataDir, configPath: join(dataDir, "config.json"), environmentPath: join(dataDir, "environment.json") };
13
- }
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";
14
9
  function prepareFile(path, text) {
15
10
  if (existsSync(path)) {
16
11
  if (readPrivateText(path) !== text)
@@ -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, discovery, disabled = []) {
12
+ const store = new TaskStore(directory);
13
+ try {
14
+ const executors = new ExecutorRegistry(profiles, discovery, disabled);
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 Promise.all([stopped, executors.close()]);
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