@realqiyan/agent-task 0.0.1-rc.2 → 0.0.1-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/bin/agent-task.mjs +23 -36
- package/dist/{launcher.js → application/launcher.js} +19 -42
- package/dist/application/main.js +122 -0
- package/dist/{startup.js → application/startup.js} +6 -11
- package/dist/application/task-runtime.js +42 -0
- package/dist/configuration/contract.js +28 -0
- package/dist/{environment.js → configuration/environment.js} +1 -5
- package/dist/configuration/files.js +43 -0
- package/dist/configuration/management.js +6 -0
- package/dist/configuration/model.js +2 -0
- package/dist/configuration/policy.js +29 -0
- package/dist/configuration/ports.js +2 -0
- package/dist/{settings.js → configuration/settings.js} +77 -39
- package/dist/executors/environment.js +10 -0
- package/dist/executors/model.js +2 -0
- package/dist/executors/registry.js +29 -0
- package/dist/executors/validation.js +10 -0
- package/dist/{lock.js → infrastructure/lock.js} +2 -2
- package/dist/{private-files.js → infrastructure/private-files.js} +1 -7
- package/dist/infrastructure/sqlite.js +19 -0
- package/dist/infrastructure/version.js +3 -0
- package/dist/{acp.js → integrations/acp/connection.js} +9 -7
- package/dist/{node-execution.js → integrations/acp/execution.js} +1 -1
- package/dist/{executor-runtime.js → integrations/acp/runtime.js} +1 -10
- package/dist/{worker.js → integrations/acp/worker.js} +3 -2
- package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
- package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
- package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
- package/dist/integrations/local-service/control.js +33 -0
- package/dist/protocol/cli/admin-client.js +45 -0
- package/dist/protocol/cli/commands.js +394 -0
- package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
- package/dist/protocol/http/server.js +111 -0
- package/dist/{web.js → protocol/http/web.js} +18 -17
- package/dist/{mcp.js → protocol/mcp.js} +13 -13
- package/dist/protocol/services.js +2 -0
- package/dist/protocol/settings-contract.js +40 -0
- package/dist/protocol/task-contract.js +28 -0
- package/dist/results/files.js +70 -0
- package/dist/results/ports.js +2 -0
- package/dist/results/service.js +42 -0
- package/dist/shared/errors.js +13 -0
- package/dist/shared/limits.js +3 -0
- package/dist/shared/time.js +2 -0
- package/dist/shared/validation.js +8 -0
- package/dist/tasks/definitions.js +83 -0
- package/dist/tasks/graph.js +23 -0
- package/dist/{human-input.js → tasks/human-input.js} +8 -7
- package/dist/tasks/model.js +2 -0
- package/dist/tasks/ports.js +2 -0
- package/dist/tasks/runs.js +263 -0
- package/dist/tasks/schedule.js +36 -0
- package/dist/tasks/sessions.js +62 -0
- package/dist/tasks/storage/records.js +45 -0
- package/dist/{task-storage.js → tasks/storage/store.js} +23 -36
- package/dist/tasks/supervisor.js +227 -0
- package/dist/tasks/validation.js +24 -0
- package/dist/web/settings-view.js +13 -6
- package/docs/architecture-guidelines.md +6 -4
- package/docs/cli.md +49 -0
- package/docs/deployment.md +16 -56
- package/docs/design-proposal.md +2 -2
- package/docs/release.md +14 -15
- package/docs/technical-design.md +47 -26
- package/docs/validation.md +49 -17
- package/docs/web-console.md +10 -8
- package/package.json +7 -7
- package/scripts/install-lib.mjs +4 -95
- package/scripts/mcp-call.mjs +1 -1
- package/scripts/probe-agent.mjs +6 -5
- package/scripts/service-runner.mjs +1 -1
- package/scripts/setup-wizard.mjs +1 -6
- package/scripts/setup.mjs +17 -30
- package/deploy/agent-task.service.example +0 -17
- package/dist/admin.js +0 -31
- package/dist/config.js +0 -72
- package/dist/main.js +0 -175
- package/dist/model.js +0 -25
- package/dist/results.js +0 -79
- package/dist/settings-contract.js +0 -16
- package/dist/task-contract.js +0 -37
- package/dist/task-model.js +0 -2
- package/dist/tasks.js +0 -508
- package/dist/version.js +0 -3
- package/scripts/uninstall.mjs +0 -57
- /package/dist/{permissions.js → executors/permissions.js} +0 -0
- /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
|
@@ -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
|
|
@@ -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
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { bearerVerifier } from "./
|
|
4
|
-
import { SettingsError } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { bearerVerifier } from "./auth.js";
|
|
4
|
+
import { SettingsError } from "../../configuration/settings.js";
|
|
5
|
+
import { previewSettings } from "../../configuration/management.js";
|
|
6
|
+
import { StorageError } from "../../shared/errors.js";
|
|
7
|
+
import { parseSettings, settingsConfigSchema, settingsChangeSchema, credentialUpdateSchema } from "../settings-contract.js";
|
|
8
|
+
import { taskIdSchema, taskListSchema, taskRunListSchema, taskUpdateSchema } from "../task-contract.js";
|
|
7
9
|
function query(schema, request) {
|
|
8
10
|
const value = { ...request.query };
|
|
9
11
|
for (const key of ["after", "limit"]) {
|
|
@@ -53,7 +55,7 @@ export function createWebRouter(tasks, settings, token, fatal, restart, instance
|
|
|
53
55
|
next();
|
|
54
56
|
});
|
|
55
57
|
const assets = new Map(["index.html", "app.js", "api.js", "dom.js", "tasks-view.js", "settings-view.js", "style.css"]
|
|
56
|
-
.map(name => [name === "index.html" ? "/" : "/" + name, fileURLToPath(new URL("
|
|
58
|
+
.map(name => [name === "index.html" ? "/" : "/" + name, fileURLToPath(new URL("../../web/" + name, import.meta.url))]));
|
|
57
59
|
router.get(["/", ...[...assets.keys()].filter(path => path !== "/")], (req, res) => {
|
|
58
60
|
res.sendFile(assets.get(req.path), { lastModified: false, cacheControl: false, acceptRanges: false }, error => { if (error && !res.headersSent)
|
|
59
61
|
res.status(404).end(); });
|
|
@@ -75,27 +77,26 @@ export function createWebRouter(tasks, settings, token, fatal, restart, instance
|
|
|
75
77
|
res.status(400).json({ error: "Request rejected; check inputs and refresh the current state" });
|
|
76
78
|
}
|
|
77
79
|
};
|
|
78
|
-
router.get("/api/status", respond(() => ({ ...tasks.status(), settings: settings.status(), instanceId })));
|
|
80
|
+
router.get("/api/status", respond(() => ({ ...tasks.supervisor.status(), settings: settings.status(), instanceId })));
|
|
79
81
|
router.get("/api/tasks", respond(req => {
|
|
80
|
-
const input = query(taskListSchema, req), page = tasks.list(input.after, input.limit);
|
|
82
|
+
const input = query(taskListSchema, req), page = tasks.definitions.list(input.after, input.limit);
|
|
81
83
|
return { ...page, items: page.items.map(task => ({ id: task.id, name: task.input.name, status: task.status, nextAt: task.nextAt, schedule: task.input.schedule })) };
|
|
82
84
|
}));
|
|
83
|
-
router.get("/api/tasks/:id", respond(req => tasks.get(taskIdSchema.parse(req.params).id)));
|
|
85
|
+
router.get("/api/tasks/:id", respond(req => tasks.definitions.get(taskIdSchema.parse(req.params).id)));
|
|
84
86
|
router.get("/api/runs", respond(req => {
|
|
85
|
-
const input = query(taskRunListSchema, req), page = tasks.runs(input.taskId, input.after, input.limit);
|
|
87
|
+
const input = query(taskRunListSchema, req), page = tasks.runs.list(input.taskId, input.after, input.limit);
|
|
86
88
|
return { ...page, items: page.items.map(run => ({ id: run.id, taskId: run.taskId, status: run.status,
|
|
87
89
|
createdAt: run.createdAt, finishedAt: run.finishedAt, error: run.error,
|
|
88
90
|
accepted: run.nodes.filter(node => node.status === "succeeded").length, total: run.nodes.length })) };
|
|
89
91
|
}));
|
|
90
|
-
router.get("/api/runs/:id", respond(req => tasks.
|
|
91
|
-
router.post("/api/task-update", respond(req => tasks.update(taskUpdateSchema.parse(req.body))));
|
|
92
|
+
router.get("/api/runs/:id", respond(req => tasks.runs.get(taskIdSchema.parse(req.params).id)));
|
|
93
|
+
router.post("/api/task-update", respond(req => tasks.definitions.update(taskUpdateSchema.parse(req.body))));
|
|
92
94
|
router.get("/api/settings", respond(() => settings.get()));
|
|
93
|
-
router.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
router.post("/api/settings/
|
|
98
|
-
router.post("/api/settings/credential", respond(req => settings.saveCredential(req.body)));
|
|
95
|
+
router.get("/api/settings/diff", respond(() => settings.diff()));
|
|
96
|
+
router.post("/api/settings/prepare", respond(req => settings.prepareChange(parseSettings(settingsChangeSchema, req.body))));
|
|
97
|
+
router.post("/api/settings/validate", respond(req => previewSettings(settings, tasks.definitions, parseSettings(settingsConfigSchema, req.body))));
|
|
98
|
+
router.post("/api/settings/config", respond(req => settings.saveConfig(parseSettings(settingsConfigSchema, req.body))));
|
|
99
|
+
router.post("/api/settings/credential", respond(req => settings.saveCredential(parseSettings(credentialUpdateSchema, req.body))));
|
|
99
100
|
router.post("/api/restart", async (req, res) => {
|
|
100
101
|
if (!req.body || Array.isArray(req.body) || typeof req.body !== "object" || Object.keys(req.body).length) {
|
|
101
102
|
res.status(400).json({ error: "Expected an empty request" });
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { errorText } from "
|
|
4
|
-
import { StorageError } from "
|
|
3
|
+
import { errorText } from "../shared/errors.js";
|
|
4
|
+
import { StorageError } from "../shared/errors.js";
|
|
5
5
|
import { replySchema, taskSchema, taskUpdateSchema, taskIdSchema, taskListSchema, taskRunListSchema } from "./task-contract.js";
|
|
6
|
-
import { version } from "
|
|
6
|
+
import { version } from "../infrastructure/version.js";
|
|
7
7
|
export function createMcpServer(service, fatal) {
|
|
8
8
|
const server = new McpServer({ name: "agent-task", version });
|
|
9
9
|
function register(name, description, inputSchema, execute, readOnly = true) {
|
|
@@ -25,16 +25,16 @@ export function createMcpServer(service, fatal) {
|
|
|
25
25
|
isError: !value.ok };
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
-
register("service_status", "Service health and active node run IDs. MCP disconnection does not cancel tasks.", z.object({}).strict(), () => service.status());
|
|
29
|
-
register("executor_list", "Registered local ACP execution profiles and authorization settings. Approval filtering is not a sandbox.", z.object({}).strict(), () => service.
|
|
30
|
-
register("task_create", "Create a task with a caller-defined DAG in nodes. Each node selects an administrator-registered ACP executorId. The coordinator advances this fixed DAG, reviews deliveries and requests missing user facts; it does not plan new nodes. Optional workspace selects an existing absolute directory; otherwise each executor uses its registered cwd. Path checks do not grant permissions; no uncertain-effect retries.", taskSchema, input => service.create(input), false);
|
|
31
|
-
register("task_list", "List persisted task definitions.", taskListSchema, input => service.list(input.after, input.limit));
|
|
32
|
-
register("task_get", "Get task goal, caller-defined DAG and assigned executor profiles, canonical workspace (null means executor cwd defaults), budgets and schedule.", taskIdSchema, input => service.get(input.id));
|
|
33
|
-
register("task_update", "Pause, resume or cancel future triggers only; resume never catches up. To change a task, cancel it and create a new task. Use task_run_cancel to stop an active run; cancellation does not roll back external effects.", taskUpdateSchema, input => service.update(input), false);
|
|
34
|
-
register("task_run_list", "List independent task runs with node progress and blocking reasons.", taskRunListSchema, input => service.runs(input.taskId, input.after, input.limit));
|
|
35
|
-
register("task_run_get", "Get the committed plan, selected node workspaces, ACP turns, accepted deliveries, blocking questions and errors for one task run.", taskIdSchema, input => service.
|
|
36
|
-
register("task_run_reply", "Answer a persisted question on the same task run. Deduplicated by requestKey; no permission escalation or uncertain-effect replay.", replySchema, input => service.reply(input), false);
|
|
37
|
-
register("task_run_cancel", "Stop this task run; external effects are not rolled back.", taskIdSchema, input => service.cancel(input.id), false);
|
|
28
|
+
register("service_status", "Service health and active node run IDs. MCP disconnection does not cancel tasks.", z.object({}).strict(), () => service.supervisor.status());
|
|
29
|
+
register("executor_list", "Registered local ACP execution profiles and authorization settings. Approval filtering is not a sandbox.", z.object({}).strict(), () => service.executors.list());
|
|
30
|
+
register("task_create", "Create a task with a caller-defined DAG in nodes. Each node selects an administrator-registered ACP executorId. The coordinator advances this fixed DAG, reviews deliveries and requests missing user facts; it does not plan new nodes. Optional workspace selects an existing absolute directory; otherwise each executor uses its registered cwd. Path checks do not grant permissions; no uncertain-effect retries.", taskSchema, input => service.definitions.create(input), false);
|
|
31
|
+
register("task_list", "List persisted task definitions.", taskListSchema, input => service.definitions.list(input.after, input.limit));
|
|
32
|
+
register("task_get", "Get task goal, caller-defined DAG and assigned executor profiles, canonical workspace (null means executor cwd defaults), budgets and schedule.", taskIdSchema, input => service.definitions.get(input.id));
|
|
33
|
+
register("task_update", "Pause, resume or cancel future triggers only; resume never catches up. To change a task, cancel it and create a new task. Use task_run_cancel to stop an active run; cancellation does not roll back external effects.", taskUpdateSchema, input => service.definitions.update(input), false);
|
|
34
|
+
register("task_run_list", "List independent task runs with node progress and blocking reasons.", taskRunListSchema, input => service.runs.list(input.taskId, input.after, input.limit));
|
|
35
|
+
register("task_run_get", "Get the committed plan, selected node workspaces, ACP turns, accepted deliveries, blocking questions and errors for one task run.", taskIdSchema, input => service.runs.get(input.id));
|
|
36
|
+
register("task_run_reply", "Answer a persisted question on the same task run. Deduplicated by requestKey; no permission escalation or uncertain-effect replay.", replySchema, input => service.supervisor.reply(input), false);
|
|
37
|
+
register("task_run_cancel", "Stop this task run; external effects are not rolled back.", taskIdSchema, input => service.supervisor.cancel(input.id), false);
|
|
38
38
|
return server;
|
|
39
39
|
}
|
|
40
40
|
//# sourceMappingURL=mcp.js.map
|