@realqiyan/agent-task 0.0.1-rc.1 → 0.0.1-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +8 -5
  2. package/bin/agent-task.mjs +23 -41
  3. package/dist/application/launcher.js +215 -0
  4. package/dist/application/main.js +122 -0
  5. package/dist/application/startup.js +51 -0
  6. package/dist/application/task-runtime.js +42 -0
  7. package/dist/configuration/contract.js +28 -0
  8. package/dist/configuration/environment.js +32 -0
  9. package/dist/configuration/files.js +43 -0
  10. package/dist/configuration/management.js +6 -0
  11. package/dist/configuration/model.js +2 -0
  12. package/dist/configuration/policy.js +29 -0
  13. package/dist/configuration/ports.js +2 -0
  14. package/dist/{settings.js → configuration/settings.js} +97 -42
  15. package/dist/executors/environment.js +10 -0
  16. package/dist/executors/model.js +2 -0
  17. package/dist/{permissions.js → executors/permissions.js} +3 -4
  18. package/dist/executors/registry.js +29 -0
  19. package/dist/executors/validation.js +10 -0
  20. package/dist/{lock.js → infrastructure/lock.js} +17 -1
  21. package/dist/{private-files.js → infrastructure/private-files.js} +17 -9
  22. package/dist/infrastructure/sqlite.js +19 -0
  23. package/dist/infrastructure/version.js +3 -0
  24. package/dist/{acp.js → integrations/acp/connection.js} +14 -11
  25. package/dist/{node-execution.js → integrations/acp/execution.js} +3 -4
  26. package/dist/integrations/acp/runtime.js +28 -0
  27. package/dist/{worker.js → integrations/acp/worker.js} +5 -4
  28. package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
  29. package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
  30. package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
  31. package/dist/integrations/local-service/control.js +33 -0
  32. package/dist/protocol/cli/admin-client.js +45 -0
  33. package/dist/protocol/cli/commands.js +394 -0
  34. package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
  35. package/dist/protocol/http/server.js +111 -0
  36. package/dist/{web.js → protocol/http/web.js} +31 -18
  37. package/dist/{mcp.js → protocol/mcp.js} +13 -13
  38. package/dist/protocol/services.js +2 -0
  39. package/dist/protocol/settings-contract.js +40 -0
  40. package/dist/protocol/task-contract.js +28 -0
  41. package/dist/results/files.js +70 -0
  42. package/dist/results/ports.js +2 -0
  43. package/dist/results/service.js +42 -0
  44. package/dist/shared/errors.js +13 -0
  45. package/dist/shared/limits.js +3 -0
  46. package/dist/shared/time.js +2 -0
  47. package/dist/shared/validation.js +8 -0
  48. package/dist/tasks/definitions.js +83 -0
  49. package/dist/tasks/graph.js +23 -0
  50. package/dist/{human-input.js → tasks/human-input.js} +8 -7
  51. package/dist/tasks/model.js +2 -0
  52. package/dist/tasks/ports.js +2 -0
  53. package/dist/tasks/runs.js +263 -0
  54. package/dist/tasks/schedule.js +36 -0
  55. package/dist/tasks/sessions.js +62 -0
  56. package/dist/tasks/storage/records.js +45 -0
  57. package/dist/{task-storage.js → tasks/storage/store.js} +23 -36
  58. package/dist/tasks/supervisor.js +227 -0
  59. package/dist/tasks/validation.js +24 -0
  60. package/dist/web/api.js +1 -1
  61. package/dist/web/app.js +7 -0
  62. package/dist/web/settings-view.js +40 -28
  63. package/docs/architecture-guidelines.md +8 -6
  64. package/docs/cli.md +49 -0
  65. package/docs/deployment.md +32 -70
  66. package/docs/design-proposal.md +3 -3
  67. package/docs/release.md +17 -18
  68. package/docs/technical-design.md +47 -26
  69. package/docs/validation.md +57 -17
  70. package/docs/web-console.md +15 -19
  71. package/package.json +8 -8
  72. package/scripts/install-lib.mjs +7 -105
  73. package/scripts/mcp-call.mjs +1 -1
  74. package/scripts/probe-agent.mjs +11 -5
  75. package/scripts/service-runner.mjs +2 -7
  76. package/scripts/setup-wizard.mjs +3 -8
  77. package/scripts/setup.mjs +31 -40
  78. package/deploy/agent-task.service.example +0 -17
  79. package/dist/admin.js +0 -31
  80. package/dist/config.js +0 -113
  81. package/dist/environment.js +0 -23
  82. package/dist/main.js +0 -131
  83. package/dist/model.js +0 -28
  84. package/dist/results.js +0 -85
  85. package/dist/settings-contract.js +0 -16
  86. package/dist/task-contract.js +0 -37
  87. package/dist/task-model.js +0 -2
  88. package/dist/tasks.js +0 -509
  89. package/dist/version.js +0 -3
  90. package/scripts/uninstall.mjs +0 -53
  91. /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
@@ -1,9 +1,9 @@
1
1
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
2
2
  import { Type } from "@earendil-works/pi-ai";
3
3
  import { createModels } from "@earendil-works/pi-ai/models";
4
- import { createCoordinatorProvider } from "./coordinator-provider.js";
4
+ import { createCoordinatorProvider } from "./provider.js";
5
5
  import { createRegistry, defineExtension, defineTool, Harness, section } from "@earendil-works/pi-durable";
6
- import { openCoordinatorStorage } from "./coordinator-storage.js";
6
+ import { openCoordinatorStorage } from "./storage.js";
7
7
  const instructions = `You advance one persisted task run using the DAG supplied by the caller. External ACP agents do all domain work.
8
8
  Start with inspect_run. The nodes, prompts, dependencies and assigned executorIds are already fixed. Do not decompose the task, add nodes, replace the plan or switch executors.
9
9
  Use converse to start a pending node only after all direct dependencies have accepted deliveries. Its first turn uses the caller-supplied node prompt. Independent nodes may run concurrently.
@@ -79,7 +79,8 @@ export function createCoordinator(directory, config) {
79
79
  tool({ name: "request_input", description: "Persist a question and suspend this run for human facts. Only after all ACP turns finish. Not permission approval.",
80
80
  parameters: Type.Object({ nodeId: Type.Optional(Type.String()),
81
81
  question: Type.String({ minLength: 1, maxLength: 4000 }), completedWork: Type.String({ minLength: 1, maxLength: 4000 }) }), replay: "unsafe",
82
- execute: async (args) => ({ ...output(actions.ask(args)), control: { terminate: true } }) }),
82
+ execute: async (args) => ({ ...output(actions.ask({ ...args, question: args.question.trim(), completedWork: args.completedWork.trim() })),
83
+ control: { terminate: true } }) }),
83
84
  tool({ name: "stop_run", description: "Stop without success for an unachievable task. Use request_input for missing user facts.",
84
85
  parameters: Type.Object({ status: Type.Literal("failed"),
85
86
  reason: Type.String({ minLength: 1, maxLength: 4000 }) }), replay: "unsafe",
@@ -87,10 +88,18 @@ export function createCoordinator(directory, config) {
87
88
  ],
88
89
  }));
89
90
  const storage = await openCoordinatorStorage(directory, runId);
90
- const harness = await Harness.open(storage, {
91
- models, registry,
92
- settings: { retry: { enabled: false }, stream: { maxRetries: 0, timeoutMs: 120000 } },
93
- }, context);
91
+ let harness;
92
+ try {
93
+ harness = await Harness.open(storage, {
94
+ models, registry,
95
+ settings: { retry: { enabled: false }, stream: { maxRetries: 0, timeoutMs: 120000 } },
96
+ }, context);
97
+ }
98
+ catch (error) {
99
+ // Ownership transfers to Harness only after open succeeds. Storage close is idempotent.
100
+ await storage.close(context);
101
+ throw error;
102
+ }
94
103
  // Close rather than mark durable tasks aborted. A safe run may resume its same submission after restart.
95
104
  const abort = () => { void harness.close(context).catch(() => { }); };
96
105
  signal.addEventListener("abort", abort, { once: true });
@@ -25,4 +25,4 @@ export function createCoordinatorProvider(config) {
25
25
  });
26
26
  }
27
27
  }
28
- //# sourceMappingURL=coordinator-provider.js.map
28
+ //# sourceMappingURL=provider.js.map
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
3
+ import { DatabaseSync } from "../../infrastructure/sqlite.js";
4
4
  import { SqliteStorage } from "@earendil-works/pi-durable/storage/sqlite";
5
5
  import { openNodeSqliteDatabase } from "@earendil-works/pi-durable/storage/sqlite/node";
6
6
  /** The caller holds the application directory lock. New files initialize; existing files must already match the pinned SDK. */
@@ -40,4 +40,4 @@ export async function openCoordinatorStorage(directory, runId) {
40
40
  throw error;
41
41
  }
42
42
  }
43
- //# sourceMappingURL=coordinator-storage.js.map
43
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1,33 @@
1
+ import { request } from "node:http";
2
+ import { join } from "node:path";
3
+ import { startupPaths } from "../../configuration/files.js";
4
+ /** Local administrator capability: the socket lives in the private launcher directory. */
5
+ export function controlService(home, action) {
6
+ startupPaths(home);
7
+ return new Promise((resolve, reject) => {
8
+ let timedOut = false;
9
+ const req = request({ socketPath: join(home, "control.sock"), path: "/" + action, method: "POST" }, res => {
10
+ let body = "";
11
+ res.setEncoding("utf8");
12
+ res.on("data", chunk => { body += chunk; });
13
+ res.on("end", () => {
14
+ if (res.statusCode !== 200) {
15
+ reject(new Error("Service unavailable; inspect private logs and startup files"));
16
+ return;
17
+ }
18
+ try {
19
+ resolve(JSON.parse(body));
20
+ }
21
+ catch {
22
+ reject(new Error("Invalid launcher response"));
23
+ }
24
+ });
25
+ });
26
+ req.setTimeout(60_000, () => { timedOut = true; req.destroy(); });
27
+ req.once("error", () => reject(new Error(timedOut
28
+ ? "Service control timed out; outcome is unknown, check status before another restart"
29
+ : "Cannot contact launcher; start the foreground or managed service first")));
30
+ req.end();
31
+ });
32
+ }
33
+ //# sourceMappingURL=control.js.map
@@ -0,0 +1,45 @@
1
+ import { controlService } from "../../integrations/local-service/control.js";
2
+ import { startupPaths } from "../../configuration/files.js";
3
+ import { readEnvironment } from "../../configuration/files.js";
4
+ import { ADMIN_TOKEN_ENV } from "../../configuration/policy.js";
5
+ /** Local CLI transport. Credentials never enter arguments, URLs or diagnostics. */
6
+ export async function connectAdmin(home) {
7
+ const ready = await controlService(home, "status");
8
+ if (!ready.consoleUrl)
9
+ throw new Error("Administrator API is disabled; configure an administrator token while stopped");
10
+ const url = new URL(ready.consoleUrl);
11
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.username || url.password) {
12
+ throw new Error("Invalid local administrator address");
13
+ }
14
+ const token = readEnvironment(startupPaths(home).environmentPath)[ADMIN_TOKEN_ENV];
15
+ if (!token)
16
+ throw new Error("Administrator credential is unavailable");
17
+ return {
18
+ ready,
19
+ async request(path, input) {
20
+ let response;
21
+ try {
22
+ response = await fetch(url.origin + "/admin/api" + path, {
23
+ method: input === undefined ? "GET" : "POST", redirect: "error",
24
+ headers: { Authorization: "Bearer " + token,
25
+ ...(input === undefined ? {} : { Origin: url.origin, "Content-Type": "application/json" }) },
26
+ body: input === undefined ? undefined : JSON.stringify(input), signal: AbortSignal.timeout(60_000),
27
+ });
28
+ }
29
+ catch {
30
+ throw new Error("Administrator request could not be confirmed; inspect current state before retrying. No offline write was attempted");
31
+ }
32
+ let body;
33
+ try {
34
+ body = await response.json();
35
+ }
36
+ catch {
37
+ throw new Error("Invalid administrator response");
38
+ }
39
+ if (!response.ok)
40
+ throw new Error(body.error ?? "Administrator request rejected (HTTP " + response.status + ")");
41
+ return body.data;
42
+ },
43
+ };
44
+ }
45
+ //# sourceMappingURL=admin-client.js.map
@@ -0,0 +1,394 @@
1
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { spawnSync } from "node:child_process";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { Writable } from "node:stream";
7
+ import { connectAdmin } from "./admin-client.js";
8
+ import { controlService } from "../../integrations/local-service/control.js";
9
+ const common = "Common: --home ABSOLUTE_DIR --json --help. Writes: --yes for noninteractive confirmation.";
10
+ const help = {
11
+ config: `config show [--saved | --running]
12
+ config diff
13
+ config edit [--editor EXECUTABLE] [--yes]
14
+ config export --output FILE
15
+ config validate --file FILE
16
+ config apply --file FILE [--yes]
17
+ Drafts contain {editToken, config, approveExecutors}; no credential values.
18
+ Edit tokens expire after 15 minutes or a save/restart. Explicit approvals name executor IDs in the candidate configuration.
19
+ Saving applies on next start; it does not restart the service.`,
20
+ executor: `executor list [--saved | --running]
21
+ executor show ID [--saved | --running]
22
+ executor add --id ID --type pi|codex|claude|opencode --cwd ABSOLUTE_DIR [--note TEXT] [--yes]
23
+ executor update ID [--id NEW_ID] [--type TYPE] [--cwd ABSOLUTE_DIR] [--note TEXT] [--yes]
24
+ executor remove ID [--yes]
25
+ executor approve ID [--yes]
26
+ executor revoke ID [--yes]
27
+ Changes apply on next start. New registrations are unapproved; changing ID, type or workspace clears approval.
28
+ approve records the administrator's review of native authorization; --yes alone does not grant it.`,
29
+ coordinator: `coordinator show [--saved | --running]
30
+ coordinator set --provider openai|anthropic|deepseek|openai-compatible --model ID [--yes]
31
+ Compatible provider also requires: --base-url URL --api-key-env NAME
32
+ --context-window NUMBER --max-tokens NUMBER [--api openai-completions|openai-responses] [--reasoning true|false]
33
+ coordinator clear [--yes]
34
+ Save the required credential first. Changes apply on next start.`,
35
+ credential: `credential list
36
+ credential set NAME [--stdin] [--yes]
37
+ credential delete NAME [--yes]
38
+ set reads a hidden terminal prompt or stdin (one trailing newline is removed).
39
+ Values are never accepted as command arguments or printed. Saves apply on next start.
40
+ Service/admin tokens and executor native authentication are not managed here.`,
41
+ task: `task list [--after NUMBER] [--limit NUMBER]
42
+ task show ID
43
+ task pause ID [--yes]
44
+ task resume ID [--yes]
45
+ task cancel ID [--yes]
46
+ Only future triggers change; current runs continue. Resume does not catch up. Cancel is irreversible for future triggers.`,
47
+ run: `run list [--task ID] [--after NUMBER] [--limit NUMBER]
48
+ run show ID
49
+ show includes nodes, turns, accepted deliveries, errors, questions and existing replies.`,
50
+ status: "status\nAuthenticated health, active runs/sessions and pending configuration changes.",
51
+ restart: "restart\nApply saved settings and verify the new instance. Active ACP sessions close; uncertain work is not replayed.",
52
+ };
53
+ const flags = new Set(["json", "yes", "help", "saved", "running", "stdin"]);
54
+ function parse(args) {
55
+ const options = {}, positionals = [];
56
+ for (let i = 0; i < args.length; i++) {
57
+ const arg = args[i];
58
+ if (!arg.startsWith("--")) {
59
+ positionals.push(arg);
60
+ continue;
61
+ }
62
+ const key = arg.slice(2);
63
+ if (Object.hasOwn(options, key))
64
+ throw new Error("Repeated option: --" + key);
65
+ if (flags.has(key))
66
+ options[key] = true;
67
+ else {
68
+ const value = args[++i];
69
+ if (value === undefined || value.startsWith("--"))
70
+ throw new Error("Missing option value: --" + key);
71
+ options[key] = value;
72
+ }
73
+ }
74
+ return { options, positionals };
75
+ }
76
+ function text(options, name, required = false) {
77
+ const value = options[name];
78
+ if (typeof value === "string")
79
+ return value;
80
+ if (required)
81
+ throw new Error("Required option: --" + name);
82
+ return undefined;
83
+ }
84
+ function number(options, name, required = false) {
85
+ const value = text(options, name, required);
86
+ if (value === undefined)
87
+ return undefined;
88
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
89
+ throw new Error("Expected integer: --" + name);
90
+ return Number(value);
91
+ }
92
+ function readJson(path) {
93
+ try {
94
+ const value = readFileSync(path, "utf8");
95
+ if (Buffer.byteLength(value) > 512 * 1024)
96
+ throw new Error();
97
+ return JSON.parse(value);
98
+ }
99
+ catch {
100
+ throw new Error("Cannot read JSON input (maximum 512 KiB)");
101
+ }
102
+ }
103
+ function writeDraft(path, value) {
104
+ writeFileSync(path, JSON.stringify(value, null, 2) + "\n", { flag: "wx", mode: 0o600 });
105
+ }
106
+ async function confirm(options, message) {
107
+ if (options.yes)
108
+ return;
109
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
110
+ throw new Error("Noninteractive writes require --yes");
111
+ const terminal = createInterface({ input: process.stdin, output: process.stderr });
112
+ try {
113
+ if (!/^(y|yes)$/i.test((await terminal.question(message + " [y/N] ")).trim()))
114
+ throw new Error("Cancelled; no change saved");
115
+ }
116
+ finally {
117
+ terminal.close();
118
+ }
119
+ }
120
+ async function credentialValue(options) {
121
+ if (options.stdin) {
122
+ if (process.stdin.isTTY)
123
+ throw new Error("--stdin requires piped input");
124
+ const chunks = [];
125
+ let length = 0;
126
+ for await (const chunk of process.stdin) {
127
+ const bytes = Buffer.from(chunk);
128
+ length += bytes.length;
129
+ if (length > 65538)
130
+ throw new Error("Credential input is too large");
131
+ chunks.push(bytes);
132
+ }
133
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
134
+ }
135
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
136
+ throw new Error("Use --stdin to provide a credential without a terminal");
137
+ const muted = new Writable({ write(_chunk, _encoding, done) { done(); } });
138
+ const terminal = createInterface({ input: process.stdin, output: muted, terminal: true });
139
+ process.stderr.write("New credential (hidden): ");
140
+ try {
141
+ return await terminal.question("");
142
+ }
143
+ finally {
144
+ terminal.close();
145
+ muted.end();
146
+ process.stderr.write("\n");
147
+ }
148
+ }
149
+ export async function runCli(command, args, resolveHome) {
150
+ if (!help[command])
151
+ throw new Error("Unknown command; use agent-task --help");
152
+ const { options, positionals } = parse(args);
153
+ if (options.help || (positionals.length === 0 && !["status", "restart"].includes(command))) {
154
+ console.log("Usage: agent-task " + help[command] + "\n" + common);
155
+ return;
156
+ }
157
+ const [action, id] = positionals;
158
+ let allowed = [], count = 1;
159
+ switch (command) {
160
+ case "status":
161
+ case "restart":
162
+ count = 0;
163
+ break;
164
+ case "config":
165
+ if (action === "show")
166
+ allowed = ["saved", "running"];
167
+ else if (action === "diff")
168
+ allowed = [];
169
+ else if (action === "edit")
170
+ allowed = ["editor", "yes"];
171
+ else if (action === "export")
172
+ allowed = ["output"];
173
+ else if (action === "validate" || action === "apply")
174
+ allowed = ["file", ...(action === "apply" ? ["yes"] : [])];
175
+ else
176
+ throw new Error("Unknown config action");
177
+ break;
178
+ case "executor":
179
+ if (action === "list" || action === "show") {
180
+ allowed = ["saved", "running"];
181
+ count = action === "show" ? 2 : 1;
182
+ }
183
+ else if (action === "add" || action === "update") {
184
+ allowed = ["id", "type", "cwd", "note", "yes"];
185
+ count = action === "update" ? 2 : 1;
186
+ }
187
+ else if (["remove", "approve", "revoke"].includes(action)) {
188
+ count = 2;
189
+ allowed = ["yes"];
190
+ }
191
+ else
192
+ throw new Error("Unknown executor action");
193
+ break;
194
+ case "coordinator":
195
+ if (action === "show")
196
+ allowed = ["saved", "running"];
197
+ else if (action === "set")
198
+ allowed = ["provider", "model", "base-url", "api-key-env", "api", "context-window", "max-tokens", "reasoning", "yes"];
199
+ else if (action === "clear")
200
+ allowed = ["yes"];
201
+ else
202
+ throw new Error("Unknown coordinator action");
203
+ break;
204
+ case "credential":
205
+ if (action === "list")
206
+ break;
207
+ if (action !== "set" && action !== "delete")
208
+ throw new Error("Unknown credential action");
209
+ count = 2;
210
+ allowed = action === "set" ? ["stdin", "yes"] : ["yes"];
211
+ break;
212
+ case "task":
213
+ case "run":
214
+ if (action === "list")
215
+ allowed = ["after", "limit", ...(command === "run" ? ["task"] : [])];
216
+ else if (action === "show")
217
+ count = 2;
218
+ else if (command === "task" && ["pause", "resume", "cancel"].includes(action)) {
219
+ count = 2;
220
+ allowed = ["yes"];
221
+ }
222
+ else
223
+ throw new Error("Unknown " + command + " action");
224
+ }
225
+ if (positionals.length !== count)
226
+ throw new Error("Unexpected or missing arguments; use " + command + " --help");
227
+ for (const key of Object.keys(options)) {
228
+ if (!["home", "json", ...allowed].includes(key))
229
+ throw new Error("Unsupported option: --" + key);
230
+ }
231
+ if (options.saved && options.running)
232
+ throw new Error("Choose either --saved or --running");
233
+ const home = resolveHome(text(options, "home"));
234
+ const output = (value) => console.log(JSON.stringify(value, null, options.json ? undefined : 2));
235
+ if (command === "restart") {
236
+ output({ event: "restarted", ...await controlService(home, "restart") });
237
+ return;
238
+ }
239
+ if (command === "status") {
240
+ const ready = await controlService(home, "status");
241
+ if (!ready.consoleUrl) {
242
+ output({ event: "healthy", ...ready, adminEnabled: false });
243
+ return;
244
+ }
245
+ }
246
+ const client = await connectAdmin(home);
247
+ if (command === "status") {
248
+ output({ event: "healthy", ...client.ready, ...await client.request("/status") });
249
+ return;
250
+ }
251
+ if (command === "task" || command === "run") {
252
+ if (action === "list") {
253
+ const query = new URLSearchParams();
254
+ for (const key of ["after", "limit"]) {
255
+ const value = number(options, key);
256
+ if (value !== undefined)
257
+ query.set(key, String(value));
258
+ }
259
+ if (text(options, "task"))
260
+ query.set("taskId", text(options, "task"));
261
+ output(await client.request("/" + command + "s?" + query));
262
+ }
263
+ else if (action === "show")
264
+ output(await client.request("/" + command + "s/" + encodeURIComponent(id)));
265
+ else {
266
+ const task = await client.request("/tasks/" + encodeURIComponent(id));
267
+ process.stderr.write(JSON.stringify({ action, task }) + "\nOnly future triggers change; current runs continue. Resume does not catch up; cancel cannot be undone.\n");
268
+ await confirm(options, "Change future triggers?");
269
+ output(await client.request("/task-update", { id, action }));
270
+ }
271
+ return;
272
+ }
273
+ const previewAndSave = async (input) => {
274
+ const preview = await client.request("/settings/validate", input);
275
+ process.stderr.write(JSON.stringify(preview, null, 2) + "\nLocal validation does not verify model authentication or ACP execution. Saved changes apply on next start.\n");
276
+ await confirm(options, "Save configuration?");
277
+ output(await client.request("/settings/config", input));
278
+ };
279
+ if (command === "config") {
280
+ if (action === "diff") {
281
+ output(await client.request("/settings/diff"));
282
+ return;
283
+ }
284
+ if (action === "validate" || action === "apply") {
285
+ const input = readJson(text(options, "file", true));
286
+ if (action === "validate")
287
+ output(await client.request("/settings/validate", input));
288
+ else
289
+ await previewAndSave(input);
290
+ return;
291
+ }
292
+ const view = await client.request("/settings");
293
+ if (action === "show") {
294
+ output(options.running ? view.running : view.saved);
295
+ return;
296
+ }
297
+ const draft = { editToken: view.editToken, config: view.saved, approveExecutors: [] };
298
+ if (action === "export") {
299
+ const path = text(options, "output", true);
300
+ writeDraft(path, draft);
301
+ output({ file: path });
302
+ return;
303
+ }
304
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
305
+ throw new Error("config edit requires a terminal; use export and apply for noninteractive edits");
306
+ const editor = text(options, "editor") ?? process.env.VISUAL ?? process.env.EDITOR;
307
+ if (!editor)
308
+ throw new Error("Set EDITOR/VISUAL or --editor to an executable path (no shell arguments)");
309
+ const directory = mkdtempSync(join(tmpdir(), "agent-task-edit-")), file = join(directory, "draft.json");
310
+ try {
311
+ writeDraft(file, draft);
312
+ const result = spawnSync(editor, [file], { stdio: "inherit" });
313
+ if (result.error || result.status !== 0)
314
+ throw new Error("Editor failed; no configuration saved");
315
+ await previewAndSave(readJson(file));
316
+ }
317
+ finally {
318
+ rmSync(directory, { recursive: true, force: true });
319
+ }
320
+ return;
321
+ }
322
+ const view = await client.request("/settings");
323
+ if (command === "credential") {
324
+ if (action === "list") {
325
+ output(view.credentials);
326
+ return;
327
+ }
328
+ process.stderr.write("Credential " + action + ": " + id + "; applies on next start.\n");
329
+ await confirm(options, "Save credential change?");
330
+ const input = { editToken: view.editToken, key: id, action: action === "set" ? "replace" : "delete",
331
+ ...(action === "set" ? { value: await credentialValue(options) } : {}) };
332
+ output(await client.request("/settings/credential", input));
333
+ return;
334
+ }
335
+ const selected = options.running ? view.running : view.saved;
336
+ if (action === "list") {
337
+ output(selected.executors);
338
+ return;
339
+ }
340
+ if (action === "show") {
341
+ if (command === "coordinator")
342
+ output(selected.coordinator ?? null);
343
+ else {
344
+ const executor = selected.executors.find(item => item.id === id);
345
+ if (!executor)
346
+ throw new Error("Executor not found");
347
+ output(executor);
348
+ }
349
+ return;
350
+ }
351
+ let change;
352
+ if (command === "executor") {
353
+ if (action === "add" || action === "update") {
354
+ const fields = {};
355
+ for (const key of ["id", "type", "cwd", "note"]) {
356
+ const value = text(options, key, action === "add" && key !== "note");
357
+ if (value !== undefined)
358
+ fields[key] = value;
359
+ }
360
+ if (!Object.keys(fields).length)
361
+ throw new Error("Specify at least one executor field to update");
362
+ change = action === "add" ? { action: "executor-add", executor: fields } : { action: "executor-update", id, patch: fields };
363
+ }
364
+ else
365
+ change = { action: "executor-" + action, id };
366
+ }
367
+ else if (action === "clear")
368
+ change = { action: "coordinator-clear" };
369
+ else {
370
+ const provider = text(options, "provider", true), model = text(options, "model", true);
371
+ const coordinator = { provider, model };
372
+ const compatible = provider === "openai-compatible";
373
+ for (const [flag, field] of [["base-url", "baseUrl"], ["api-key-env", "apiKeyEnv"], ["api", "api"]]) {
374
+ const value = text(options, flag, compatible && flag !== "api");
375
+ if (value !== undefined)
376
+ coordinator[field] = value;
377
+ }
378
+ for (const [flag, field] of [["context-window", "contextWindow"], ["max-tokens", "maxTokens"]]) {
379
+ const value = number(options, flag, compatible);
380
+ if (value !== undefined)
381
+ coordinator[field] = value;
382
+ }
383
+ const reasoning = text(options, "reasoning");
384
+ if (reasoning !== undefined) {
385
+ if (!["true", "false"].includes(reasoning))
386
+ throw new Error("--reasoning must be true or false");
387
+ coordinator.reasoning = reasoning === "true";
388
+ }
389
+ change = { action: "coordinator-set", coordinator };
390
+ }
391
+ const draft = await client.request("/settings/prepare", { editToken: view.editToken, change });
392
+ await previewAndSave(draft);
393
+ }
394
+ //# sourceMappingURL=commands.js.map
@@ -6,4 +6,4 @@ export function bearerVerifier(token) {
6
6
  return actual.length === expected.length && timingSafeEqual(actual, expected);
7
7
  };
8
8
  }
9
- //# sourceMappingURL=http-auth.js.map
9
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,111 @@
1
+ import express from "express";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { createMcpServer } from "../mcp.js";
4
+ import { bearerVerifier } from "./auth.js";
5
+ import { createWebRouter } from "./web.js";
6
+ /** Owns the listener and request-scoped MCP transports. Disconnecting a request never cancels a task. */
7
+ export class HttpServer {
8
+ app = express();
9
+ connections = new Set();
10
+ listener;
11
+ opening;
12
+ closing;
13
+ accepting = true;
14
+ constructor(options) {
15
+ const authorized = bearerVerifier(options.token);
16
+ const app = this.app;
17
+ app.disable("x-powered-by");
18
+ app.disable("etag");
19
+ app.use((_req, res, next) => {
20
+ if (!this.accepting) {
21
+ res.status(503).end();
22
+ return;
23
+ }
24
+ next();
25
+ });
26
+ if (options.settings && options.adminToken) {
27
+ app.use("/admin", createWebRouter(options.tasks, options.settings, options.adminToken, options.fatal, options.restart, options.instanceId));
28
+ }
29
+ app.use("/mcp", (req, res, next) => {
30
+ const suffix = req.socket.localPort === 80 ? "" : ":" + req.socket.localPort;
31
+ if (req.headers.origin !== undefined || !["127.0.0.1" + suffix, "localhost" + suffix].includes(req.headers.host ?? "")) {
32
+ res.status(403).end();
33
+ return;
34
+ }
35
+ if (!authorized(req.headers.authorization)) {
36
+ res.status(401).set("WWW-Authenticate", "Bearer").end();
37
+ return;
38
+ }
39
+ next();
40
+ }, express.json({ limit: "512kb" }));
41
+ app.post("/mcp", async (req, res) => {
42
+ const server = createMcpServer(options.tasks, options.fatal);
43
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
44
+ let closed;
45
+ const close = () => closed ??= (async () => {
46
+ try {
47
+ await transport.close();
48
+ }
49
+ finally {
50
+ try {
51
+ await server.close();
52
+ }
53
+ finally {
54
+ this.connections.delete(close);
55
+ }
56
+ }
57
+ })();
58
+ this.connections.add(close);
59
+ res.once("close", () => { void close().catch(() => { }); });
60
+ try {
61
+ await server.connect(transport);
62
+ await transport.handleRequest(req, res, req.body);
63
+ }
64
+ catch {
65
+ if (!res.headersSent)
66
+ res.status(500).json({ error: "MCP request failed" });
67
+ await close();
68
+ }
69
+ });
70
+ app.all("/mcp", (_req, res) => { res.status(405).set("Allow", "POST").end(); });
71
+ app.use((_req, res) => { res.status(404).end(); });
72
+ app.use(((error, _req, res, _next) => {
73
+ res.status(error.status === 413 ? 413 : 400).json({ error: "Invalid request body" });
74
+ }));
75
+ }
76
+ async listen(port) {
77
+ if (this.closing || this.listener)
78
+ throw new Error("HTTP server closed or already listening");
79
+ const server = this.app.listen(port, "127.0.0.1");
80
+ this.listener = server;
81
+ this.opening = new Promise((done, reject) => {
82
+ server.once("listening", done);
83
+ server.once("error", reject);
84
+ });
85
+ await this.opening;
86
+ if (this.closing) {
87
+ await this.closing;
88
+ throw new Error("Startup stopped");
89
+ }
90
+ const address = this.listener.address();
91
+ return "http://127.0.0.1:" + (typeof address === "object" && address ? address.port : port);
92
+ }
93
+ stopAccepting() { this.accepting = false; }
94
+ closeConnections() { this.listener?.closeAllConnections(); }
95
+ close(drain = false) {
96
+ this.stopAccepting();
97
+ return this.closing ??= (async () => {
98
+ await this.opening?.catch(() => { });
99
+ await Promise.allSettled([...this.connections].map(close => close()));
100
+ if (this.listener) {
101
+ if (drain)
102
+ this.listener.close(); // Keep the authenticated restart response alive.
103
+ else {
104
+ this.listener.closeAllConnections();
105
+ await new Promise(done => this.listener.close(() => done()));
106
+ }
107
+ }
108
+ })();
109
+ }
110
+ }
111
+ //# sourceMappingURL=server.js.map