@realqiyan/agent-task 0.0.1-rc.3 → 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.
- package/CONTEXT.md +17 -0
- package/README.md +28 -6
- package/bin/agent-task.mjs +18 -6
- package/config.example.json +1 -1
- package/dist/application/launcher.js +30 -16
- package/dist/application/main.js +5 -2
- package/dist/application/mcp-entry.js +71 -0
- package/dist/application/task-runtime.js +3 -3
- package/dist/configuration/contract.js +2 -2
- package/dist/configuration/files.js +2 -3
- package/dist/configuration/management.js +1 -1
- package/dist/configuration/policy.js +3 -8
- package/dist/configuration/settings.js +10 -33
- package/dist/executors/ports.js +2 -0
- package/dist/executors/registry.js +117 -15
- package/dist/executors/validation.js +4 -5
- package/dist/infrastructure/lock.js +25 -13
- package/dist/integrations/acp/connection.js +5 -5
- package/dist/integrations/acp/discovery.js +271 -0
- package/dist/integrations/acp/runtime.js +38 -23
- package/dist/integrations/acp/worker.js +5 -3
- package/dist/integrations/local-service/control.js +36 -11
- package/dist/protocol/cli/commands.js +41 -46
- package/dist/protocol/http/server.js +8 -0
- package/dist/protocol/http/web.js +7 -1
- package/dist/protocol/mcp.js +3 -3
- package/dist/protocol/settings-contract.js +4 -7
- package/dist/protocol/stdio.js +71 -0
- package/dist/protocol/task-contract.js +1 -1
- package/dist/tasks/definitions.js +8 -5
- package/dist/tasks/runs.js +2 -2
- package/dist/tasks/storage/records.js +2 -2
- package/dist/tasks/storage/store.js +5 -4
- package/dist/tasks/supervisor.js +1 -1
- package/dist/web/settings-view.js +43 -37
- package/docs/adr/0001-task-request-scope.md +3 -0
- package/docs/adr/0002-mcp-workspace-source.md +3 -0
- package/docs/adr/0003-adapter-installation.md +3 -0
- package/docs/architecture-guidelines.md +2 -2
- package/docs/cli.md +12 -4
- package/docs/deployment.md +52 -36
- package/docs/design-proposal.md +3 -0
- package/docs/release.md +5 -5
- package/docs/technical-design.md +5 -3
- package/docs/validation.md +51 -9
- package/docs/web-console.md +7 -4
- package/package.json +3 -1
- package/scripts/install-lib.mjs +5 -23
- package/scripts/probe-agent.mjs +10 -9
- package/scripts/service-runner.mjs +8 -3
- package/scripts/setup-wizard.mjs +38 -11
- package/scripts/setup.mjs +69 -95
|
@@ -14,18 +14,19 @@ config edit [--editor EXECUTABLE] [--yes]
|
|
|
14
14
|
config export --output FILE
|
|
15
15
|
config validate --file FILE
|
|
16
16
|
config apply --file FILE [--yes]
|
|
17
|
-
Drafts contain {editToken, config
|
|
18
|
-
Edit tokens expire after 15 minutes or a save/restart.
|
|
17
|
+
Drafts contain {editToken, config}; no credential values.
|
|
18
|
+
Edit tokens expire after 15 minutes or a save/restart.
|
|
19
19
|
Saving applies on next start; it does not restart the service.`,
|
|
20
|
-
executor: `executor list
|
|
21
|
-
executor show
|
|
22
|
-
executor
|
|
23
|
-
executor
|
|
24
|
-
executor
|
|
25
|
-
executor
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
executor: `executor list
|
|
21
|
+
executor show pi|codex|claude|opencode
|
|
22
|
+
executor refresh
|
|
23
|
+
executor upgrade pi|codex|claude [--yes]
|
|
24
|
+
executor enable pi|codex|claude|opencode [--yes]
|
|
25
|
+
executor disable pi|codex|claude|opencode [--yes]
|
|
26
|
+
list/show report current discovery and adapter preparation; native authentication is not verified.
|
|
27
|
+
refresh discovers installed CLIs and prepares missing adapters in the background.
|
|
28
|
+
upgrade explicitly updates a managed ACP adapter; existing tasks keep their native executor assignment.
|
|
29
|
+
enable/disable apply on next start. Native CLI authentication and permissions remain native configuration.`,
|
|
29
30
|
coordinator: `coordinator show [--saved | --running]
|
|
30
31
|
coordinator set --provider openai|anthropic|deepseek|openai-compatible --model ID [--yes]
|
|
31
32
|
Compatible provider also requires: --base-url URL --api-key-env NAME
|
|
@@ -176,15 +177,11 @@ export async function runCli(command, args, resolveHome) {
|
|
|
176
177
|
throw new Error("Unknown config action");
|
|
177
178
|
break;
|
|
178
179
|
case "executor":
|
|
179
|
-
if (action === "list" || action === "
|
|
180
|
-
allowed = [
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
else if (
|
|
184
|
-
allowed = ["id", "type", "cwd", "note", "yes"];
|
|
185
|
-
count = action === "update" ? 2 : 1;
|
|
186
|
-
}
|
|
187
|
-
else if (["remove", "approve", "revoke"].includes(action)) {
|
|
180
|
+
if (action === "list" || action === "refresh")
|
|
181
|
+
allowed = [];
|
|
182
|
+
else if (action === "show")
|
|
183
|
+
count = 2;
|
|
184
|
+
else if (["enable", "disable", "upgrade"].includes(action)) {
|
|
188
185
|
count = 2;
|
|
189
186
|
allowed = ["yes"];
|
|
190
187
|
}
|
|
@@ -248,6 +245,27 @@ export async function runCli(command, args, resolveHome) {
|
|
|
248
245
|
output({ event: "healthy", ...client.ready, ...await client.request("/status") });
|
|
249
246
|
return;
|
|
250
247
|
}
|
|
248
|
+
if (command === "executor" && ["list", "show", "refresh", "upgrade"].includes(action)) {
|
|
249
|
+
if (action === "refresh") {
|
|
250
|
+
output(await client.request("/executors/refresh", {}));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (action === "upgrade") {
|
|
254
|
+
await confirm(options, "Upgrade the managed ACP adapter for " + id + "?");
|
|
255
|
+
output(await client.request("/executors/upgrade", { id }));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const result = await client.request("/executors");
|
|
259
|
+
if (action === "list")
|
|
260
|
+
output(result);
|
|
261
|
+
else {
|
|
262
|
+
const executor = result.executors.find(item => item.id === id);
|
|
263
|
+
if (!executor)
|
|
264
|
+
throw new Error("Executor not found");
|
|
265
|
+
output(executor);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
251
269
|
if (command === "task" || command === "run") {
|
|
252
270
|
if (action === "list") {
|
|
253
271
|
const query = new URLSearchParams();
|
|
@@ -294,7 +312,7 @@ export async function runCli(command, args, resolveHome) {
|
|
|
294
312
|
output(options.running ? view.running : view.saved);
|
|
295
313
|
return;
|
|
296
314
|
}
|
|
297
|
-
const draft = { editToken: view.editToken, config: view.saved
|
|
315
|
+
const draft = { editToken: view.editToken, config: view.saved };
|
|
298
316
|
if (action === "export") {
|
|
299
317
|
const path = text(options, "output", true);
|
|
300
318
|
writeDraft(path, draft);
|
|
@@ -333,36 +351,13 @@ export async function runCli(command, args, resolveHome) {
|
|
|
333
351
|
return;
|
|
334
352
|
}
|
|
335
353
|
const selected = options.running ? view.running : view.saved;
|
|
336
|
-
if (action === "list") {
|
|
337
|
-
output(selected.executors);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
354
|
if (action === "show") {
|
|
341
|
-
|
|
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
|
-
}
|
|
355
|
+
output(selected.coordinator ?? null);
|
|
349
356
|
return;
|
|
350
357
|
}
|
|
351
358
|
let change;
|
|
352
359
|
if (command === "executor") {
|
|
353
|
-
|
|
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 };
|
|
360
|
+
change = { action: "executor-" + action, id };
|
|
366
361
|
}
|
|
367
362
|
else if (action === "clear")
|
|
368
363
|
change = { action: "coordinator-clear" };
|
|
@@ -39,6 +39,14 @@ export class HttpServer {
|
|
|
39
39
|
next();
|
|
40
40
|
}, express.json({ limit: "512kb" }));
|
|
41
41
|
app.post("/mcp", async (req, res) => {
|
|
42
|
+
if (req.body?.method === "initialize") {
|
|
43
|
+
const discovered = options.tasks.executors.refresh();
|
|
44
|
+
if (!discovered.executors.some(executor => executor.nativePath !== null)) {
|
|
45
|
+
res.json({ jsonrpc: "2.0", id: req.body.id ?? null, error: { code: -32000,
|
|
46
|
+
message: "No local executor CLI found. Install and configure at least one of pi, codex, claude or opencode, then reconnect. Management remains available." } });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
42
50
|
const server = createMcpServer(options.tasks, options.fatal);
|
|
43
51
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
|
44
52
|
let closed;
|
|
@@ -4,7 +4,7 @@ import { bearerVerifier } from "./auth.js";
|
|
|
4
4
|
import { SettingsError } from "../../configuration/settings.js";
|
|
5
5
|
import { previewSettings } from "../../configuration/management.js";
|
|
6
6
|
import { StorageError } from "../../shared/errors.js";
|
|
7
|
-
import { parseSettings, settingsConfigSchema, settingsChangeSchema, credentialUpdateSchema } from "../settings-contract.js";
|
|
7
|
+
import { parseSettings, settingsConfigSchema, settingsChangeSchema, credentialUpdateSchema, executorRefreshSchema, executorUpgradeSchema } from "../settings-contract.js";
|
|
8
8
|
import { taskIdSchema, taskListSchema, taskRunListSchema, taskUpdateSchema } from "../task-contract.js";
|
|
9
9
|
function query(schema, request) {
|
|
10
10
|
const value = { ...request.query };
|
|
@@ -91,6 +91,12 @@ export function createWebRouter(tasks, settings, token, fatal, restart, instance
|
|
|
91
91
|
}));
|
|
92
92
|
router.get("/api/runs/:id", respond(req => tasks.runs.get(taskIdSchema.parse(req.params).id)));
|
|
93
93
|
router.post("/api/task-update", respond(req => tasks.definitions.update(taskUpdateSchema.parse(req.body))));
|
|
94
|
+
router.get("/api/executors", respond(() => tasks.executors.list()));
|
|
95
|
+
router.post("/api/executors/refresh", respond(req => {
|
|
96
|
+
parseSettings(executorRefreshSchema, req.body);
|
|
97
|
+
return tasks.executors.refresh();
|
|
98
|
+
}));
|
|
99
|
+
router.post("/api/executors/upgrade", respond(req => tasks.executors.upgrade(parseSettings(executorUpgradeSchema, req.body).id)));
|
|
94
100
|
router.get("/api/settings", respond(() => settings.get()));
|
|
95
101
|
router.get("/api/settings/diff", respond(() => settings.diff()));
|
|
96
102
|
router.post("/api/settings/prepare", respond(req => settings.prepareChange(parseSettings(settingsChangeSchema, req.body))));
|
package/dist/protocol/mcp.js
CHANGED
|
@@ -26,10 +26,10 @@ export function createMcpServer(service, fatal) {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
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", "
|
|
30
|
-
register("task_create", "Create a task with a caller-defined DAG
|
|
29
|
+
register("executor_list", "Discovered local ACP executors and adapter preparation status. Ready means launch dependencies are present, not verified authentication or tool permissions.", z.object({}).strict(), () => service.executors.list());
|
|
30
|
+
register("task_create", "Create a task with a caller-defined DAG. Each node selects a ready local ACP executorId. The coordinator advances the fixed DAG, reviews deliveries and requests missing user facts. Workspace is an existing absolute directory; local stdio supplies its launch directory when omitted, direct HTTP requires workspace. The resolved directory is fixed for all future runs. requestKey deduplicates within that directory. Path checks do not grant permissions; no uncertain-effect retries.", taskSchema, input => service.definitions.create(input), false);
|
|
31
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
|
|
32
|
+
register("task_get", "Get task goal, caller-defined DAG and assigned executors, fixed canonical workspace, budgets and schedule.", taskIdSchema, input => service.definitions.get(input.id));
|
|
33
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
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
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));
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import { SettingsError } from "../configuration/settings.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { configSchema } from "../configuration/contract.js";
|
|
4
|
-
import {
|
|
5
|
-
import { identifier } from "../shared/validation.js";
|
|
4
|
+
import { executorTypeSchema } from "../executors/validation.js";
|
|
6
5
|
export const settingsConfigSchema = z.object({
|
|
7
6
|
editToken: z.string().uuid(),
|
|
8
7
|
config: configSchema,
|
|
9
|
-
approveExecutors: z.array(identifier).max(100).default([]),
|
|
10
8
|
}).strict();
|
|
11
9
|
export const settingsChangeSchema = z.object({
|
|
12
10
|
editToken: z.string().uuid(),
|
|
13
11
|
change: z.discriminatedUnion("action", [
|
|
14
|
-
z.object({ action: z.
|
|
15
|
-
z.object({ action: z.literal("executor-update"), id: identifier,
|
|
16
|
-
patch: executorSchema.omit({ unattendedApproved: true }).extend({ note: executorSchema.shape.note.removeDefault() }).partial() }).strict(),
|
|
17
|
-
z.object({ action: z.enum(["executor-remove", "executor-approve", "executor-revoke"]), id: identifier }).strict(),
|
|
12
|
+
z.object({ action: z.enum(["executor-enable", "executor-disable"]), id: executorTypeSchema }).strict(),
|
|
18
13
|
z.object({ action: z.literal("coordinator-set"), coordinator: configSchema.shape.coordinator.unwrap() }).strict(),
|
|
19
14
|
z.object({ action: z.literal("coordinator-clear") }).strict(),
|
|
20
15
|
]),
|
|
21
16
|
}).strict();
|
|
17
|
+
export const executorRefreshSchema = z.object({}).strict();
|
|
18
|
+
export const executorUpgradeSchema = z.object({ id: executorTypeSchema }).strict();
|
|
22
19
|
const credential = {
|
|
23
20
|
editToken: z.string().uuid(),
|
|
24
21
|
key: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(200),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { version } from "../infrastructure/version.js";
|
|
7
|
+
const toolNames = new Set(["service_status", "executor_list", "task_create", "task_list", "task_get", "task_update",
|
|
8
|
+
"task_run_list", "task_run_get", "task_run_reply", "task_run_cancel"]);
|
|
9
|
+
/** A fixed Agent Task adapter; only task creation receives the connection's startup directory. */
|
|
10
|
+
export async function serveStdio(options) {
|
|
11
|
+
const url = new URL(options.url);
|
|
12
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.pathname !== "/mcp" || url.username || url.password || url.search || url.hash) {
|
|
13
|
+
throw new Error("Invalid local MCP address");
|
|
14
|
+
}
|
|
15
|
+
const client = new Client({ name: "agent-task-stdio", version });
|
|
16
|
+
const server = new Server({ name: "agent-task", version }, { capabilities: { tools: {} } });
|
|
17
|
+
let close, finish;
|
|
18
|
+
const finished = new Promise(resolve => { finish = resolve; });
|
|
19
|
+
const shutdown = () => close ??= Promise.resolve().then(async () => {
|
|
20
|
+
try {
|
|
21
|
+
await server.close();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
try {
|
|
25
|
+
await client.close();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
finish();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const stop = () => { void shutdown().catch(() => { }); };
|
|
33
|
+
server.onclose = stop;
|
|
34
|
+
client.onclose = stop;
|
|
35
|
+
server.onerror = stop;
|
|
36
|
+
process.stdin.once("end", stop);
|
|
37
|
+
process.stdin.once("close", stop);
|
|
38
|
+
process.stdout.once("error", stop);
|
|
39
|
+
process.once("SIGINT", stop);
|
|
40
|
+
process.once("SIGTERM", stop);
|
|
41
|
+
process.once("SIGHUP", stop);
|
|
42
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
43
|
+
const result = await client.listTools(request.params);
|
|
44
|
+
return { ...result, tools: result.tools.filter(tool => toolNames.has(tool.name)) };
|
|
45
|
+
});
|
|
46
|
+
server.setRequestHandler(CallToolRequestSchema, request => {
|
|
47
|
+
if (!toolNames.has(request.params.name))
|
|
48
|
+
throw new McpError(ErrorCode.MethodNotFound, "Unknown Agent Task tool");
|
|
49
|
+
const input = request.params.arguments ?? {};
|
|
50
|
+
return client.callTool({ ...request.params, arguments: request.params.name === "task_create" && input.workspace === undefined
|
|
51
|
+
? { ...input, workspace: options.workspace } : input });
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
await client.connect(new StreamableHTTPClientTransport(url, {
|
|
55
|
+
requestInit: { headers: { Authorization: "Bearer " + options.token }, redirect: "error" },
|
|
56
|
+
}));
|
|
57
|
+
if (!close)
|
|
58
|
+
await server.connect(new StdioServerTransport());
|
|
59
|
+
await finished;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await shutdown();
|
|
63
|
+
process.stdin.off("end", stop);
|
|
64
|
+
process.stdin.off("close", stop);
|
|
65
|
+
process.stdout.off("error", stop);
|
|
66
|
+
process.off("SIGINT", stop);
|
|
67
|
+
process.off("SIGTERM", stop);
|
|
68
|
+
process.off("SIGHUP", stop);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -15,7 +15,7 @@ export const taskUpdateSchema = taskUpdateData;
|
|
|
15
15
|
export const taskSchema = taskData.extend({
|
|
16
16
|
name: z.string().trim().min(1).max(200),
|
|
17
17
|
prompt: z.string().trim().min(1).max(64_000).describe("Overall goal; the caller supplies the fixed DAG."),
|
|
18
|
-
nodes: z.array(nodeSchema).min(1).max(20).describe("Fixed DAG with
|
|
18
|
+
nodes: z.array(nodeSchema).min(1).max(20).describe("Fixed DAG with a ready local ACP executorId on every node."),
|
|
19
19
|
schedule: z.discriminatedUnion("type", [
|
|
20
20
|
z.object({ type: z.literal("now") }).strict(),
|
|
21
21
|
z.object({ type: z.literal("once"), at: requestTimestamp }).strict(),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { authorizedProfile } from "../executors/registry.js";
|
|
3
2
|
import { iso } from "../shared/time.js";
|
|
4
3
|
import { validateGraph } from "./graph.js";
|
|
5
4
|
import { hasUncertainOutcome, initialTime, resumeSchedule } from "./schedule.js";
|
|
@@ -24,7 +23,11 @@ export class TaskDefinitions {
|
|
|
24
23
|
if (!this.coordinatorConfigured)
|
|
25
24
|
throw new Error("Coordinator is not configured");
|
|
26
25
|
const input = taskData.parse(value), all = this.store.definitions();
|
|
27
|
-
|
|
26
|
+
if (input.workspace === undefined)
|
|
27
|
+
throw new Error("workspace is required without a local MCP launch directory");
|
|
28
|
+
const workspace = this.workspaces.resolve(input.workspace);
|
|
29
|
+
input.workspace = workspace;
|
|
30
|
+
const existing = all.find(item => item.workspace === workspace && item.input.requestKey === input.requestKey);
|
|
28
31
|
if (existing) {
|
|
29
32
|
if (JSON.stringify(existing.input) !== JSON.stringify(input))
|
|
30
33
|
throw new Error("requestKey already used with different parameters");
|
|
@@ -39,7 +42,7 @@ export class TaskDefinitions {
|
|
|
39
42
|
throw new Error("Schedule must start in the future");
|
|
40
43
|
if (input.expiresAt && Date.parse(input.expiresAt) <= Math.max(now, nextAt))
|
|
41
44
|
throw new Error("Deadline must follow first trigger");
|
|
42
|
-
const task = { id: randomUUID(), input, workspace
|
|
45
|
+
const task = { id: randomUUID(), input, workspace,
|
|
43
46
|
executors, nextAt, status: "active", createdAt: iso(now) };
|
|
44
47
|
this.store.saveDefinition(task);
|
|
45
48
|
return task;
|
|
@@ -72,10 +75,10 @@ export class TaskDefinitions {
|
|
|
72
75
|
this.store.saveDefinition(task);
|
|
73
76
|
return task;
|
|
74
77
|
}
|
|
75
|
-
executorImpact(
|
|
78
|
+
executorImpact(disabled) {
|
|
76
79
|
this.check();
|
|
77
80
|
return this.store.definitions().filter(task => ((task.nextAt !== null && ["active", "paused"].includes(task.status)) || this.store.activeRounds(task.id).length > 0) &&
|
|
78
|
-
Object.
|
|
81
|
+
Object.values(task.executors).some(snapshot => disabled.includes(snapshot.type)))
|
|
79
82
|
.map(task => ({ id: task.id, name: task.input.name }));
|
|
80
83
|
}
|
|
81
84
|
close() { this.closed = true; }
|
package/dist/tasks/runs.js
CHANGED
|
@@ -123,7 +123,7 @@ export class TaskRuns {
|
|
|
123
123
|
throw new Error("Deadline reached before node start");
|
|
124
124
|
if (sessions.size >= 8)
|
|
125
125
|
throw new Error("ACP session capacity reached; accept existing results first");
|
|
126
|
-
node.workspace = task.workspace
|
|
126
|
+
node.workspace = task.workspace;
|
|
127
127
|
this.workspaces.check(node.workspace);
|
|
128
128
|
node.inputFiles = parents.map(parent => parent.resultFile);
|
|
129
129
|
}
|
|
@@ -141,7 +141,7 @@ export class TaskRuns {
|
|
|
141
141
|
// The no-replay barrier precedes worker creation, not just the ACP prompt.
|
|
142
142
|
this.store.commitRun(run);
|
|
143
143
|
return opening ? { kind: "open", nodeRunId: node.id, text, seconds: node.definition.maxRunSeconds,
|
|
144
|
-
input: { executor
|
|
144
|
+
input: { executor, workspace: node.workspace, inputFiles: node.inputFiles, resultsRoot: this.results.root } }
|
|
145
145
|
: { kind: "continue", nodeRunId: node.id, text };
|
|
146
146
|
}
|
|
147
147
|
beforeStart(id, nodeId) {
|
|
@@ -3,10 +3,10 @@ import { executorRecord } from "../../executors/validation.js";
|
|
|
3
3
|
import { absolutePath, identifier, text, timestamp } from "../../shared/validation.js";
|
|
4
4
|
import { nodeData, questionData, taskData } from "../validation.js";
|
|
5
5
|
export const definitionSchema = z.object({
|
|
6
|
-
id: z.string().uuid(), input: taskData, workspace: absolutePath
|
|
6
|
+
id: z.string().uuid(), input: taskData, workspace: absolutePath, executors: z.record(identifier, executorRecord),
|
|
7
7
|
status: z.enum(["active", "paused", "cancelled", "completed", "expired"]),
|
|
8
8
|
nextAt: z.number().finite().nullable(), createdAt: timestamp,
|
|
9
|
-
}).strict();
|
|
9
|
+
}).strict().refine(task => task.input.workspace === task.workspace, "Task workspace must match the canonical request");
|
|
10
10
|
export const roundSchema = z.object({
|
|
11
11
|
id: z.string().uuid(), taskId: z.string().uuid(), scheduledAt: z.number().finite(),
|
|
12
12
|
status: z.enum(["running", "succeeded", "failed", "interrupted", "skipped", "cancelled", "timed_out", "waiting_input"]),
|
|
@@ -14,7 +14,7 @@ export class TaskStore {
|
|
|
14
14
|
try {
|
|
15
15
|
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
16
16
|
if (tables.length) {
|
|
17
|
-
this.db.prepare("SELECT seq,id,request_key,data FROM definitions LIMIT 0").all();
|
|
17
|
+
this.db.prepare("SELECT seq,id,workspace,request_key,data FROM definitions LIMIT 0").all();
|
|
18
18
|
this.db.prepare("SELECT seq,id,task_id,scheduled_at,data FROM rounds LIMIT 0").all();
|
|
19
19
|
this.definitions();
|
|
20
20
|
this.rounds();
|
|
@@ -22,7 +22,8 @@ export class TaskStore {
|
|
|
22
22
|
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON");
|
|
23
23
|
if (!tables.length)
|
|
24
24
|
this.db.exec(`BEGIN IMMEDIATE;
|
|
25
|
-
CREATE TABLE definitions(seq INTEGER PRIMARY KEY, id TEXT UNIQUE NOT NULL,
|
|
25
|
+
CREATE TABLE definitions(seq INTEGER PRIMARY KEY, id TEXT UNIQUE NOT NULL, workspace TEXT NOT NULL,
|
|
26
|
+
request_key TEXT NOT NULL, data TEXT NOT NULL, UNIQUE(workspace,request_key));
|
|
26
27
|
CREATE TABLE rounds(seq INTEGER PRIMARY KEY, id TEXT UNIQUE NOT NULL, task_id TEXT NOT NULL REFERENCES definitions(id),
|
|
27
28
|
scheduled_at REAL NOT NULL, data TEXT NOT NULL, UNIQUE(task_id,scheduled_at));
|
|
28
29
|
COMMIT;`);
|
|
@@ -86,8 +87,8 @@ export class TaskStore {
|
|
|
86
87
|
try {
|
|
87
88
|
this.db.exec("BEGIN IMMEDIATE");
|
|
88
89
|
if (definition)
|
|
89
|
-
this.db.prepare(`INSERT INTO definitions(id,request_key,data) VALUES(
|
|
90
|
-
ON CONFLICT(id) DO UPDATE SET data=excluded.data`).run(definition.id, definition.input.requestKey, JSON.stringify(definition));
|
|
90
|
+
this.db.prepare(`INSERT INTO definitions(id,workspace,request_key,data) VALUES(?,?,?,?)
|
|
91
|
+
ON CONFLICT(id) DO UPDATE SET data=excluded.data`).run(definition.id, definition.workspace, definition.input.requestKey, JSON.stringify(definition));
|
|
91
92
|
if (round)
|
|
92
93
|
this.db.prepare(`INSERT INTO rounds(id,task_id,scheduled_at,data) VALUES(?,?,?,?)
|
|
93
94
|
ON CONFLICT(id) DO UPDATE SET data=excluded.data`).run(round.id, round.taskId, round.scheduledAt, JSON.stringify(round));
|
package/dist/tasks/supervisor.js
CHANGED
|
@@ -132,7 +132,7 @@ export class RunSupervisor {
|
|
|
132
132
|
if (this.timer)
|
|
133
133
|
throw new Error("Run supervisor already started");
|
|
134
134
|
this.recover();
|
|
135
|
-
this.timer = setInterval(() => this.tick(),
|
|
135
|
+
this.timer = setInterval(() => this.tick(), 10_000);
|
|
136
136
|
this.tick();
|
|
137
137
|
}
|
|
138
138
|
tick() {
|
|
@@ -1,31 +1,28 @@
|
|
|
1
1
|
import { request } from "./api.js";
|
|
2
|
-
import { element, field, select, button, submit, block, table, notice } from "./dom.js";
|
|
2
|
+
import { element, field, select, button, submit, block, table, notice, report } from "./dom.js";
|
|
3
3
|
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return () =>
|
|
26
|
-
id: fields.id.value, type: fields.type.value, note: fields.note.value,
|
|
27
|
-
cwd: fields.cwd.value, unattendedApproved: approved.input.checked,
|
|
28
|
-
} }] : [];
|
|
4
|
+
function executorStatus(host, initial) {
|
|
5
|
+
let state = initial, timer;
|
|
6
|
+
const labels = { missing: "未安装", disabled: "已禁用", installing: "准备中", ready: "可派发", error: "准备失败" };
|
|
7
|
+
const draw = () => {
|
|
8
|
+
clearTimeout(timer);
|
|
9
|
+
host.replaceChildren(table(["执行端", "状态", "本机路径", "ACP 适配器版本", "错误", "操作"], state.executors.map(executor => [
|
|
10
|
+
executor.id, executor.state === "missing" && executor.nativePath ? "已发现,待准备" : labels[executor.state],
|
|
11
|
+
executor.nativePath, executor.adapterVersion ?? (executor.type === "opencode" ? "原生 ACP" : "—"), executor.error,
|
|
12
|
+
executor.type === "opencode" ? "通过原生 CLI 更新" : button("升级 ACP 适配器", async () => {
|
|
13
|
+
if (!confirm("升级 " + executor.id + " 的托管 ACP 适配器?准备期间暂停向此执行端派发新工作。")) return;
|
|
14
|
+
state = await request("/executors/upgrade", { id: executor.id }); draw();
|
|
15
|
+
notice("适配器升级已开始,完成后更新状态。");
|
|
16
|
+
}, !executor.nativePath || ["disabled", "installing"].includes(executor.state)),
|
|
17
|
+
])));
|
|
18
|
+
if (state.executors.some(executor => executor.state === "installing")) timer = setTimeout(async () => {
|
|
19
|
+
if (!host.isConnected) return;
|
|
20
|
+
try { state = await request("/executors"); if (host.isConnected) draw(); }
|
|
21
|
+
catch (error) { if (host.isConnected) report(error); }
|
|
22
|
+
}, 2000);
|
|
23
|
+
};
|
|
24
|
+
draw();
|
|
25
|
+
return async () => { state = await request("/executors/refresh", {}); draw(); };
|
|
29
26
|
}
|
|
30
27
|
function coordinatorEditor(host, config) {
|
|
31
28
|
const provider = select("协调模型提供方", [["", "未配置"], ["openai", "OpenAI"], ["anthropic", "Anthropic"],
|
|
@@ -56,12 +53,18 @@ function coordinatorEditor(host, config) {
|
|
|
56
53
|
};
|
|
57
54
|
}
|
|
58
55
|
export async function renderSettings(root, navigate) {
|
|
59
|
-
|
|
56
|
+
const initial = await Promise.all([request("/settings"), request("/executors")]);
|
|
57
|
+
let state = initial[0];
|
|
60
58
|
root.append(element("h2", "配置与凭据"), element("p", "配置和密钥分别保存。保存不修改当前进程;每次重新启动(包括故障后拉起)读取已保存值。"),
|
|
61
59
|
element("p", "先保存新密钥,再保存引用它的配置。执行端原生登录和原生权限仍通过官方工具管理;这里不编辑其认证文件。"),
|
|
62
60
|
block("当前运行配置(只读,无密钥值)", state.running));
|
|
63
61
|
if (state.writeBlocked) root.append(element("p", "上次文件提交结果不确定。请停服核查;当前禁止继续保存。"));
|
|
64
|
-
const
|
|
62
|
+
const executorHost = element("div");
|
|
63
|
+
const refreshExecutors = executorStatus(executorHost, initial[1]);
|
|
64
|
+
root.append(element("h3", "本机执行端"),
|
|
65
|
+
element("p", "自动检测 pi、codex、claude 和 opencode。可派发表示启动依赖已准备,不代表原生认证或工具权限已验收。"),
|
|
66
|
+
button("重新检测并准备执行端", refreshExecutors), executorHost);
|
|
67
|
+
const form = element("form"), modelHost = element("fieldset"), executorOptions = element("fieldset");
|
|
65
68
|
modelHost.append(element("legend", "协调模型"));
|
|
66
69
|
const readModel = coordinatorEditor(modelHost, state.saved.coordinator);
|
|
67
70
|
const startup = {
|
|
@@ -69,18 +72,21 @@ export async function renderSettings(root, navigate) {
|
|
|
69
72
|
port: field("下次启动的监听端口(0 为自动分配)", state.saved.port, "number"),
|
|
70
73
|
tokenEnv: field("MCP 令牌环境变量名(改名保留令牌值)", state.saved.tokenEnv),
|
|
71
74
|
};
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
75
|
+
executorOptions.append(element("legend", "下次启动启用的执行端"),
|
|
76
|
+
element("p", "默认使用所有检测到的执行端。取消勾选后保存并重启;已有任务不会自动改派。"));
|
|
77
|
+
const enabled = ["pi", "codex", "claude", "opencode"].map(type => {
|
|
78
|
+
const item = field(type, !state.saved.disabledExecutors.includes(type), "checkbox");
|
|
79
|
+
executorOptions.append(item.row); return { type, input: item.input };
|
|
80
|
+
});
|
|
81
|
+
const advanced = element("details"); advanced.dataset.key = "advanced-settings";
|
|
82
|
+
advanced.append(element("summary", "高级设置"), element("h3", "启动配置"),
|
|
83
|
+
...Object.values(startup).map(item => item.row), executorOptions);
|
|
84
|
+
form.append(modelHost, advanced);
|
|
78
85
|
const preview = element("div");
|
|
79
86
|
const save = submit(form, "校验并预览配置变更", async () => {
|
|
80
|
-
const
|
|
81
|
-
const input = { editToken: state.editToken, approveExecutors: entries.filter(item => item.explicitlyApproved).map(item => item.executor.id),
|
|
87
|
+
const input = { editToken: state.editToken,
|
|
82
88
|
config: { dataDir: startup.dataDir.input.value, port: startup.port.input.valueAsNumber, tokenEnv: startup.tokenEnv.input.value,
|
|
83
|
-
coordinator: readModel(),
|
|
89
|
+
coordinator: readModel(), disabledExecutors: enabled.filter(item => !item.input.checked).map(item => item.type) } };
|
|
84
90
|
const checked = await request("/settings/validate", input);
|
|
85
91
|
preview.replaceChildren(block("校验后的待保存配置", checked.config),
|
|
86
92
|
block("配置变更", checked.changes),
|
|
@@ -34,8 +34,8 @@ CLI 覆盖 Web 的全部配置和管理操作,两端必须复用同一套底
|
|
|
34
34
|
## 权限与敏感数据
|
|
35
35
|
|
|
36
36
|
- 执行端声明能力、管理员授权、真实验证证据分别表达,不能相互推导。
|
|
37
|
-
-
|
|
38
|
-
- 授权须在可控制的实际操作入口检查。沿用执行端原生授权,额外 ACP
|
|
37
|
+
- 只有已发现、未禁用且 ACP 依赖就绪的本机执行端可启动。启动命令由系统控制,MCP 调用方不能提供命令、凭据或能力端点;任务目录在创建时确定,路径校验不是授权或沙箱。
|
|
38
|
+
- 授权须在可控制的实际操作入口检查。沿用执行端原生授权,额外 ACP 审批请求一律拒绝;发现与依赖就绪不能证明所有原生工具已受控。
|
|
39
39
|
- Prompt、角色、技能、工具标题、协议成功和人工事实回复都不授予权限。取消、恢复和跨 Agent 传递不能扩大授权。
|
|
40
40
|
- 无人值守执行不等待交互审批;授权不充分或元数据不足时拒绝,不退回更宽权限或自动永久允许。
|
|
41
41
|
- MCP token、管理员 token、协调模型与执行端凭据分开;执行端继承用户原生环境前过滤服务私密变量,不有意记录秘密。Agent 输出和协调日志仍可能敏感。
|
package/docs/cli.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 本机管理 CLI
|
|
2
2
|
|
|
3
|
-
CLI 覆盖 Web
|
|
3
|
+
CLI 覆盖 Web 的配置、凭据、执行端管理、任务查询、未来触发控制及重启操作。两端通过同一管理 API 使用配置、执行端和任务服务;重启由同一启动器处理。命令入口见 [命令实现](../src/protocol/cli/commands.ts),管理传输见 [admin-client.ts](../src/protocol/cli/admin-client.ts)。
|
|
4
4
|
|
|
5
5
|
## 连接与命令
|
|
6
6
|
|
|
@@ -16,9 +16,9 @@ agent-task task --help
|
|
|
16
16
|
agent-task run --help
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
逐级帮助是命令与参数的完整入口。管理命令支持 `--json`,结果写入 stdout,预览和诊断写入 stderr
|
|
19
|
+
逐级帮助是命令与参数的完整入口。管理命令支持 `--json`,结果写入 stdout,预览和诊断写入 stderr;失败退出码非零。非交互保存、适配器升级与任务变更须使用 `--yes`,它只确认本次操作,不授予执行端原生工具权限。
|
|
20
20
|
|
|
21
|
-
服务不可用时,管理命令失败,不自动离线写文件。首次安装仍使用 `setup`;启动故障按[部署说明](deployment.md
|
|
21
|
+
服务不可用时,管理命令失败,不自动离线写文件。首次安装仍使用 `setup`;启动故障按[部署说明](deployment.md#后台前台与健康检查)停服核查。CLI 不迁移数据或修复历史。
|
|
22
22
|
|
|
23
23
|
## 配置流程
|
|
24
24
|
|
|
@@ -38,10 +38,18 @@ agent-task config show --running
|
|
|
38
38
|
|
|
39
39
|
导出文件不包含密钥值,以私有权限创建,拒绝覆盖已有文件。编辑令牌过期、服务重启或任一客户端保存配置/凭据后,旧草稿不能提交;重新读取并人工核对,不自动合并。草稿契约见 [settings-contract.ts](../src/protocol/settings-contract.ts)。
|
|
40
40
|
|
|
41
|
-
`executor` 和 `coordinator`
|
|
41
|
+
`executor enable/disable` 和 `coordinator` 提供便捷配置操作。配置服务生成候选配置,再走同一预览、校验和保存流程。执行端自动发现,无须手工注册、设置默认工作目录或另行批准。启用与禁用在下次启动生效,不改写已有任务的执行端指派。
|
|
42
42
|
|
|
43
43
|
配置与凭据均在下次启动生效。影响预览不修改旧任务快照,也不证明模型认证或 ACP 执行成功。保存顺序、端点与凭据隔离、重启影响见[控制台说明](web-console.md#保存与重启)。
|
|
44
44
|
|
|
45
|
+
## 执行端发现与更新
|
|
46
|
+
|
|
47
|
+
`agent-task executor list` 查看当前发现和准备结果;`executor show codex` 查看单个执行端。安装新的原生 CLI 后,运行 `executor refresh` 重新检测并在后台准备缺少的 ACP 适配器。该操作无需重启。准备失败时先检查报告的原因,修正后显式刷新或升级。
|
|
48
|
+
|
|
49
|
+
已准备的适配器不会随普通启动或刷新自动升级。需要更新时运行 `agent-task executor upgrade codex --yes`,再用 `executor list` 检查结果。升级期间暂停向该执行端派发新工作;它不修改已有任务指派。OpenCode 使用原生 ACP 命令,通过原生 CLI 的安装渠道更新。
|
|
50
|
+
|
|
51
|
+
就绪状态只表示启动依赖已准备。原生登录和权限仍由执行端管理;首次准备、版本来源及运行边界见[部署说明](deployment.md)。
|
|
52
|
+
|
|
45
53
|
## 凭据与任务
|
|
46
54
|
|
|
47
55
|
`credential set NAME` 使用隐藏终端输入;自动化通过 `--stdin --yes` 从可信凭据工具或受保护文件传值,不把值写进命令参数。`credential list` 只显示设置状态。执行端原生登录和权限继续由其官方工具管理。
|