@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.
Files changed (52) hide show
  1. package/CONTEXT.md +17 -0
  2. package/README.md +28 -6
  3. package/bin/agent-task.mjs +18 -6
  4. package/config.example.json +1 -1
  5. package/dist/application/launcher.js +30 -16
  6. package/dist/application/main.js +5 -2
  7. package/dist/application/mcp-entry.js +71 -0
  8. package/dist/application/task-runtime.js +3 -3
  9. package/dist/configuration/contract.js +2 -2
  10. package/dist/configuration/files.js +2 -3
  11. package/dist/configuration/management.js +1 -1
  12. package/dist/configuration/policy.js +3 -8
  13. package/dist/configuration/settings.js +10 -33
  14. package/dist/executors/ports.js +2 -0
  15. package/dist/executors/registry.js +117 -15
  16. package/dist/executors/validation.js +4 -5
  17. package/dist/infrastructure/lock.js +25 -13
  18. package/dist/integrations/acp/connection.js +5 -5
  19. package/dist/integrations/acp/discovery.js +271 -0
  20. package/dist/integrations/acp/runtime.js +38 -23
  21. package/dist/integrations/acp/worker.js +5 -3
  22. package/dist/integrations/local-service/control.js +36 -11
  23. package/dist/protocol/cli/commands.js +41 -46
  24. package/dist/protocol/http/server.js +8 -0
  25. package/dist/protocol/http/web.js +7 -1
  26. package/dist/protocol/mcp.js +3 -3
  27. package/dist/protocol/settings-contract.js +4 -7
  28. package/dist/protocol/stdio.js +71 -0
  29. package/dist/protocol/task-contract.js +1 -1
  30. package/dist/tasks/definitions.js +8 -5
  31. package/dist/tasks/runs.js +2 -2
  32. package/dist/tasks/storage/records.js +2 -2
  33. package/dist/tasks/storage/store.js +5 -4
  34. package/dist/tasks/supervisor.js +1 -1
  35. package/dist/web/settings-view.js +43 -37
  36. package/docs/adr/0001-task-request-scope.md +3 -0
  37. package/docs/adr/0002-mcp-workspace-source.md +3 -0
  38. package/docs/adr/0003-adapter-installation.md +3 -0
  39. package/docs/architecture-guidelines.md +2 -2
  40. package/docs/cli.md +12 -4
  41. package/docs/deployment.md +52 -36
  42. package/docs/design-proposal.md +3 -0
  43. package/docs/release.md +5 -5
  44. package/docs/technical-design.md +5 -3
  45. package/docs/validation.md +51 -9
  46. package/docs/web-console.md +7 -4
  47. package/package.json +3 -1
  48. package/scripts/install-lib.mjs +5 -23
  49. package/scripts/probe-agent.mjs +10 -9
  50. package/scripts/service-runner.mjs +8 -3
  51. package/scripts/setup-wizard.mjs +38 -11
  52. 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, approveExecutors}; no credential values.
18
- Edit tokens expire after 15 minutes or a save/restart. Explicit approvals name executor IDs in the candidate configuration.
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 [--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.`,
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 === "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)) {
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, approveExecutors: [] };
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
- 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
- }
355
+ output(selected.coordinator ?? null);
349
356
  return;
350
357
  }
351
358
  let change;
352
359
  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 };
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))));
@@ -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", "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);
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 executor profiles, canonical workspace (null means executor cwd defaults), budgets and schedule.", taskIdSchema, input => service.definitions.get(input.id));
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 { executorSchema } from "../executors/validation.js";
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.literal("executor-add"), executor: executorSchema.omit({ unattendedApproved: true }) }).strict(),
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 an administrator-registered ACP executorId on every node."),
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
- const existing = all.find(item => item.input.requestKey === input.requestKey);
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: input.workspace === undefined ? null : this.workspaces.resolve(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(profiles) {
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.entries(task.executors).some(([id, snapshot]) => !authorizedProfile(snapshot, profiles.find(profile => profile.id === id))))
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; }
@@ -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 ?? executor.cwd;
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: { ...executor, cwd: node.workspace }, inputFiles: node.inputFiles, resultsRoot: this.results.root } }
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.nullable(), executors: z.record(identifier, executorRecord),
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, request_key TEXT UNIQUE NOT NULL, data TEXT 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));
@@ -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(), 500);
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 executorEditor(host, executor) {
5
- const card = element("fieldset"), fields = {};
6
- card.append(element("legend", "ACP 执行端"));
7
- const type = select("类型", ["pi", "codex", "claude", "opencode"].map(value => [value, value]), executor.type ?? "pi");
8
- fields.type = type.input; card.append(type.row);
9
- for (const [key, label, kind, value] of [
10
- ["id", "注册 ID", "text", executor.id],
11
- ["note", "备注", "textarea", executor.note],
12
- ["cwd", "默认工作空间(绝对路径)", "text", executor.cwd],
13
- ]) {
14
- const item = field(label, value, kind); fields[key] = item.input; card.append(item.row);
15
- }
16
- const approved = field("已由管理员审核原生授权,允许无人值守执行", executor.unattendedApproved, "checkbox");
17
- let explicitlyApproved = false;
18
- approved.input.addEventListener("change", () => { explicitlyApproved = approved.input.checked; });
19
- for (const [key, input] of Object.entries(fields)) if (key !== "note") input.addEventListener("input", () => {
20
- approved.input.checked = false; explicitlyApproved = false;
21
- });
22
- card.append(approved.row, element("p", "按类型从用户原生 PATH 查找 ACP 适配器。继承用户原生环境,但过滤服务私密变量;额外 ACP 审批一律拒绝。结果固定保存在 dataDir/results,只向节点提供指定上游文件,不提供整个数据目录读取。"),
23
- button("从待保存配置中移除", () => { card.remove(); host.dispatchEvent(new Event("input", { bubbles: true })); }));
24
- host.append(card);
25
- return () => card.parentNode === host ? [{ explicitlyApproved, executor: {
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
- let state = await request("/settings");
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 form = element("form"), modelHost = element("fieldset"), executors = element("div"), readers = [];
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
- form.append(element("h3", "启动配置"), ...Object.values(startup).map(item => item.row),
73
- modelHost, element("h3", "执行端注册与权限"), executors);
74
- for (const executor of state.saved.executors) readers.push(executorEditor(executors, executor));
75
- form.append(button("添加执行端", () => {
76
- readers.push(executorEditor(executors, { id: "", type: "pi", note: "", cwd: "", unattendedApproved: false }));
77
- }, state.writeBlocked));
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 entries = readers.flatMap(read => read());
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(), executors: entries.map(item => item.executor) } };
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),
@@ -0,0 +1,3 @@
1
+ # 按任务工作目录区分创建请求
2
+
3
+ 多个项目共用本机后台时,调用方可能使用相同请求键。任务创建请求按最终规范工作目录和请求键确定身份,使不同目录的请求相互独立,同一目录中的重复提交仍能去重。
@@ -0,0 +1,3 @@
1
+ # 使用明确的任务工作目录来源
2
+
3
+ MCP 宿主不保证从当前项目目录启动接入进程,服务无法据此通用地识别宿主当前项目。任务显式提供的工作目录优先,否则使用本机 MCP 入口的启动目录;直接 HTTP 调用须显式提供目录。默认目录属于该 MCP 连接,避免多个项目误用共用后台的启动目录。
@@ -0,0 +1,3 @@
1
+ # 按需安装最新版适配器并复用已有安装
2
+
3
+ MCP 连接触发 ACP 适配器检测,缺失时安装当时最新版,已有安装继续复用。升级由显式操作触发,避免普通重连改变任务使用的执行环境。
@@ -34,8 +34,8 @@ CLI 覆盖 Web 的全部配置和管理操作,两端必须复用同一套底
34
34
  ## 权限与敏感数据
35
35
 
36
36
  - 执行端声明能力、管理员授权、真实验证证据分别表达,不能相互推导。
37
- - 只有管理员注册的执行端可启动。命令、凭据和能力端点属于管理员;任务可选择现有绝对工作空间,但路径校验不是授权或沙箱。
38
- - 授权须在可控制的实际操作入口检查。沿用执行端原生授权,额外 ACP 审批请求一律拒绝;`unattendedApproved` 不能证明所有原生工具已受控。
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 的配置、凭据、任务查询、未来触发控制及重启操作。两端通过同一管理 API 使用配置和任务服务;重启由同一启动器处理。命令入口见 [命令实现](../src/protocol/cli/commands.ts),管理传输见 [admin-client.ts](../src/protocol/cli/admin-client.ts)。
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;失败退出码非零。非交互保存与任务变更须使用 `--yes`,它只确认本次操作,不替代校验或执行端授权。
19
+ 逐级帮助是命令与参数的完整入口。管理命令支持 `--json`,结果写入 stdout,预览和诊断写入 stderr;失败退出码非零。非交互保存、适配器升级与任务变更须使用 `--yes`,它只确认本次操作,不授予执行端原生工具权限。
20
20
 
21
- 服务不可用时,管理命令失败,不自动离线写文件。首次安装仍使用 `setup`;启动故障按[部署说明](deployment.md#前台重启与健康检查)停服核查。CLI 不迁移数据或修复历史。
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` 提供便捷操作,配置服务负责生成候选配置,再走同一预览、校验和保存流程。新执行端默认未批准;修改 ID、类型或工作空间后清除批准。原生授权审核完成后,显式运行 `executor approve ID`。完整草稿新增或变更已批准的执行端时,也须明确声明对候选配置的授权。
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` 只显示设置状态。执行端原生登录和权限继续由其官方工具管理。