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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CONTEXT.md +17 -0
  2. package/README.md +30 -7
  3. package/bin/agent-task.mjs +35 -36
  4. package/config.example.json +1 -1
  5. package/dist/{launcher.js → application/launcher.js} +49 -58
  6. package/dist/application/main.js +125 -0
  7. package/dist/application/mcp-entry.js +71 -0
  8. package/dist/{startup.js → application/startup.js} +6 -11
  9. package/dist/application/task-runtime.js +42 -0
  10. package/dist/configuration/contract.js +28 -0
  11. package/dist/{environment.js → configuration/environment.js} +1 -5
  12. package/dist/configuration/files.js +42 -0
  13. package/dist/configuration/management.js +6 -0
  14. package/dist/configuration/model.js +2 -0
  15. package/dist/configuration/policy.js +24 -0
  16. package/dist/configuration/ports.js +2 -0
  17. package/dist/{settings.js → configuration/settings.js} +54 -39
  18. package/dist/executors/environment.js +10 -0
  19. package/dist/executors/model.js +2 -0
  20. package/dist/executors/ports.js +2 -0
  21. package/dist/executors/registry.js +131 -0
  22. package/dist/executors/validation.js +9 -0
  23. package/dist/infrastructure/lock.js +55 -0
  24. package/dist/{private-files.js → infrastructure/private-files.js} +1 -7
  25. package/dist/infrastructure/sqlite.js +19 -0
  26. package/dist/infrastructure/version.js +3 -0
  27. package/dist/{acp.js → integrations/acp/connection.js} +13 -11
  28. package/dist/integrations/acp/discovery.js +271 -0
  29. package/dist/{node-execution.js → integrations/acp/execution.js} +1 -1
  30. package/dist/integrations/acp/runtime.js +43 -0
  31. package/dist/{worker.js → integrations/acp/worker.js} +7 -4
  32. package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
  33. package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
  34. package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
  35. package/dist/integrations/local-service/control.js +58 -0
  36. package/dist/protocol/cli/admin-client.js +45 -0
  37. package/dist/protocol/cli/commands.js +389 -0
  38. package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
  39. package/dist/protocol/http/server.js +119 -0
  40. package/dist/{web.js → protocol/http/web.js} +24 -17
  41. package/dist/{mcp.js → protocol/mcp.js} +13 -13
  42. package/dist/protocol/services.js +2 -0
  43. package/dist/protocol/settings-contract.js +37 -0
  44. package/dist/protocol/stdio.js +71 -0
  45. package/dist/protocol/task-contract.js +28 -0
  46. package/dist/results/files.js +70 -0
  47. package/dist/results/ports.js +2 -0
  48. package/dist/results/service.js +42 -0
  49. package/dist/shared/errors.js +13 -0
  50. package/dist/shared/limits.js +3 -0
  51. package/dist/shared/time.js +2 -0
  52. package/dist/shared/validation.js +8 -0
  53. package/dist/tasks/definitions.js +86 -0
  54. package/dist/tasks/graph.js +23 -0
  55. package/dist/{human-input.js → tasks/human-input.js} +8 -7
  56. package/dist/tasks/model.js +2 -0
  57. package/dist/tasks/ports.js +2 -0
  58. package/dist/tasks/runs.js +263 -0
  59. package/dist/tasks/schedule.js +36 -0
  60. package/dist/tasks/sessions.js +62 -0
  61. package/dist/tasks/storage/records.js +45 -0
  62. package/dist/{task-storage.js → tasks/storage/store.js} +28 -40
  63. package/dist/tasks/supervisor.js +227 -0
  64. package/dist/tasks/validation.js +24 -0
  65. package/dist/web/settings-view.js +46 -33
  66. package/docs/adr/0001-task-request-scope.md +3 -0
  67. package/docs/adr/0002-mcp-workspace-source.md +3 -0
  68. package/docs/adr/0003-adapter-installation.md +3 -0
  69. package/docs/architecture-guidelines.md +8 -6
  70. package/docs/cli.md +57 -0
  71. package/docs/deployment.md +46 -70
  72. package/docs/design-proposal.md +5 -2
  73. package/docs/release.md +14 -15
  74. package/docs/technical-design.md +49 -26
  75. package/docs/validation.md +92 -18
  76. package/docs/web-console.md +16 -11
  77. package/package.json +9 -7
  78. package/scripts/install-lib.mjs +7 -116
  79. package/scripts/mcp-call.mjs +1 -1
  80. package/scripts/probe-agent.mjs +14 -12
  81. package/scripts/service-runner.mjs +9 -4
  82. package/scripts/setup-wizard.mjs +38 -16
  83. package/scripts/setup.mjs +69 -108
  84. package/deploy/agent-task.service.example +0 -17
  85. package/dist/admin.js +0 -31
  86. package/dist/config.js +0 -72
  87. package/dist/executor-runtime.js +0 -37
  88. package/dist/lock.js +0 -43
  89. package/dist/main.js +0 -175
  90. package/dist/model.js +0 -25
  91. package/dist/results.js +0 -79
  92. package/dist/settings-contract.js +0 -16
  93. package/dist/task-contract.js +0 -37
  94. package/dist/task-model.js +0 -2
  95. package/dist/tasks.js +0 -508
  96. package/dist/version.js +0 -3
  97. package/scripts/uninstall.mjs +0 -57
  98. /package/dist/{permissions.js → executors/permissions.js} +0 -0
  99. /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
@@ -0,0 +1,227 @@
1
+ import { errorText, StorageError } from "../shared/errors.js";
2
+ import { coordinatorInput } from "./human-input.js";
3
+ import { NodeSessions } from "./sessions.js";
4
+ /** Owns coordinator generations, the calendar clock and NodeSessions. All persisted decisions go through TaskRuns. */
5
+ export class RunSupervisor {
6
+ runs;
7
+ coordinate;
8
+ fatal;
9
+ closed = false;
10
+ closing;
11
+ timer;
12
+ active = new Map();
13
+ turns = new Set();
14
+ sessions;
15
+ constructor(runs, openExecution, coordinate, fatal) {
16
+ this.runs = runs;
17
+ this.coordinate = coordinate;
18
+ this.fatal = fatal;
19
+ this.sessions = new NodeSessions(openExecution, fatal);
20
+ }
21
+ check() { if (this.closed)
22
+ throw new Error("Run supervisor closed"); }
23
+ status() {
24
+ return { running: !this.closed && this.timer !== undefined && this.runs.healthy,
25
+ activeTaskRunIds: [...this.active.keys()], activeNodeRunIds: this.sessions.nodeIds(), storageHealthy: this.runs.healthy };
26
+ }
27
+ actions(id, requestId = coordinatorInput(this.runs.get(id)).requestId) {
28
+ this.check();
29
+ const current = () => { this.check(); return this.runs.current(id, requestId); };
30
+ return {
31
+ inspect: () => { current(); return this.runs.inspect(id, requestId); },
32
+ step: () => { current(); try {
33
+ this.runs.step(id);
34
+ }
35
+ finally {
36
+ this.reconcile(id);
37
+ } },
38
+ converse: (node, message) => { current(); return this.converse(id, node, message); },
39
+ accept: (node, turn, result, reason) => { current(); return this.accept(id, node, turn, result, reason); },
40
+ ask: request => {
41
+ current();
42
+ const question = this.runs.ask(id, request);
43
+ clearTimeout(this.active.get(id)?.timer);
44
+ return question;
45
+ },
46
+ finish: summary => { current(); this.runs.finish(id, summary); },
47
+ stop: (status, reason) => { current(); this.stopActive(id, status, reason); },
48
+ };
49
+ }
50
+ converse(id, nodeId, message) {
51
+ const pending = this.performTurn(id, nodeId, message);
52
+ this.turns.add(pending);
53
+ void pending.finally(() => this.turns.delete(pending)).catch(() => { });
54
+ return pending;
55
+ }
56
+ async performTurn(id, nodeId, message) {
57
+ this.check();
58
+ let dispatched = false;
59
+ try {
60
+ const dispatch = this.runs.prepareTurn(id, nodeId, message, this.sessions);
61
+ dispatched = true;
62
+ const handle = dispatch.kind === "continue" ? this.sessions.get(dispatch.nodeRunId)
63
+ : this.sessions.open(dispatch.nodeRunId, id, dispatch.input, dispatch.seconds, (kind, data, text) => this.runs.record(id, nodeId, kind, data, text), () => { this.check(); this.runs.beforeStart(id, nodeId); }, () => {
64
+ const waiting = this.runs.get(id).status === "waiting_input";
65
+ this.stopActive(id, waiting ? "interrupted" : "timed_out", waiting
66
+ ? "ACP session lifetime expired while waiting; no replay" : "ACP node session timed out");
67
+ }, () => { if (!this.closed && this.runs.healthy)
68
+ this.stopActive(id, "interrupted", "ACP session disconnected; external effects unknown"); });
69
+ const result = await handle.prompt(dispatch.text);
70
+ return this.runs.completeTurn(id, nodeId, result.stopReason);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof StorageError)
74
+ this.fatal(error);
75
+ else if (dispatched && !this.closed && this.runs.healthy)
76
+ this.stopActive(id, "interrupted", errorText(error));
77
+ throw error;
78
+ }
79
+ finally {
80
+ if (!this.closed)
81
+ this.reconcile(id);
82
+ }
83
+ }
84
+ async accept(id, nodeId, turn, result, reason) {
85
+ this.check();
86
+ const owned = this.runs.accept(id, nodeId, turn, result, reason);
87
+ await this.sessions.closeSession(owned);
88
+ }
89
+ reconcile(id) {
90
+ if (!this.runs.healthy)
91
+ return;
92
+ if (["running", "waiting_input"].includes(this.runs.get(id).status))
93
+ return;
94
+ this.active.get(id)?.controller.abort(new Error("Run stopped"));
95
+ void this.sessions.closeRun(id).catch(this.fatal);
96
+ }
97
+ stopActive(id, status, reason) {
98
+ try {
99
+ this.runs.end(id, status, reason);
100
+ }
101
+ catch (error) {
102
+ this.fatal(error);
103
+ }
104
+ finally {
105
+ this.active.get(id)?.controller.abort(new Error(reason));
106
+ void this.sessions.closeRun(id).catch(this.fatal);
107
+ }
108
+ }
109
+ reply(input, now = Date.now()) {
110
+ this.check();
111
+ try {
112
+ return this.runs.reply(input, id => this.sessions.has(id), now);
113
+ }
114
+ finally {
115
+ this.reconcile(input.runId);
116
+ }
117
+ }
118
+ cancel(id) {
119
+ this.check();
120
+ this.runs.get(id);
121
+ this.stopActive(id, "cancelled", "Run cancelled; external effects are not rolled back");
122
+ return this.runs.get(id);
123
+ }
124
+ recover() {
125
+ this.check();
126
+ if (this.active.size || this.sessions.size)
127
+ throw new Error("Recovery requires no active resources");
128
+ this.runs.recover();
129
+ }
130
+ start() {
131
+ this.check();
132
+ if (this.timer)
133
+ throw new Error("Run supervisor already started");
134
+ this.recover();
135
+ this.timer = setInterval(() => this.tick(), 10_000);
136
+ this.tick();
137
+ }
138
+ tick() {
139
+ this.check();
140
+ try {
141
+ this.runs.trigger();
142
+ for (const run of this.runs.active()) {
143
+ if (run.status === "waiting_input") {
144
+ this.runs.expireWaiting(run.id);
145
+ this.reconcile(run.id);
146
+ continue;
147
+ }
148
+ if (this.active.has(run.id) || this.active.size >= 4)
149
+ continue;
150
+ this.launch(run.id);
151
+ }
152
+ }
153
+ catch (error) {
154
+ if (this.timer)
155
+ clearInterval(this.timer);
156
+ this.fatal(error);
157
+ }
158
+ }
159
+ launch(id) {
160
+ const coordinate = this.coordinate;
161
+ if (!coordinate) {
162
+ this.runs.end(id, "failed", "Coordinator not configured");
163
+ return;
164
+ }
165
+ const admission = this.runs.admitCoordinator(id);
166
+ if (!admission)
167
+ return;
168
+ const { input, remaining } = admission;
169
+ const stillCurrent = () => {
170
+ const current = this.runs.get(id);
171
+ return current.status === "running" && coordinatorInput(current).requestId === input.requestId;
172
+ };
173
+ const controller = new AbortController();
174
+ const timer = setTimeout(() => this.stopActive(id, "timed_out", "Task run timed out"), remaining);
175
+ // Register the controller before the deferred external entry. Recheck generation and cancellation there.
176
+ const completion = Promise.resolve().then(() => {
177
+ this.check();
178
+ controller.signal.throwIfAborted();
179
+ this.runs.current(id, input.requestId);
180
+ return coordinate(id, input, this.actions(id, input.requestId), controller.signal);
181
+ }).then(() => {
182
+ if (!this.closed && stillCurrent())
183
+ this.runs.end(id, "failed", "Coordinator ended without an explicit completion or blocking decision");
184
+ }, error => {
185
+ if (error instanceof StorageError)
186
+ throw error;
187
+ if (!this.closed && this.runs.healthy && stillCurrent())
188
+ this.runs.end(id, "failed", errorText(error));
189
+ }).catch(this.fatal).finally(async () => {
190
+ clearTimeout(timer);
191
+ try {
192
+ if (!this.runs.healthy || !["running", "waiting_input"].includes(this.runs.get(id).status))
193
+ await this.sessions.closeRun(id);
194
+ }
195
+ finally {
196
+ this.active.delete(id);
197
+ }
198
+ }).catch(this.fatal);
199
+ this.active.set(id, { controller, completion, timer });
200
+ }
201
+ close() { return this.closing ??= this.stop(); }
202
+ async stop() {
203
+ this.closed = true;
204
+ if (this.timer)
205
+ clearInterval(this.timer);
206
+ const errors = [];
207
+ for (const id of this.sessions.runIds()) {
208
+ try {
209
+ this.runs.end(id, "interrupted", "Service stopped with an open ACP session");
210
+ }
211
+ catch (error) {
212
+ errors.push(error);
213
+ }
214
+ }
215
+ for (const active of this.active.values()) {
216
+ clearTimeout(active.timer);
217
+ active.controller.abort(new Error("Service stopping"));
218
+ }
219
+ const stopped = await Promise.allSettled([this.sessions.close(), ...[...this.active.values()].map(active => active.completion), ...this.turns]);
220
+ // Rejected prompts are expected during cancellation; resource-close failures remain fatal.
221
+ if (stopped[0]?.status === "rejected")
222
+ errors.push(stopped[0].reason);
223
+ if (errors.length)
224
+ throw new AggregateError(errors, "Run supervision could not close cleanly");
225
+ }
226
+ }
227
+ //# sourceMappingURL=supervisor.js.map
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { absolutePath, identifier, text, timestamp } from "../shared/validation.js";
3
+ export const scheduleData = z.discriminatedUnion("type", [
4
+ z.object({ type: z.literal("now") }).strict(),
5
+ z.object({ type: z.literal("once"), at: timestamp }).strict(),
6
+ z.object({ type: z.literal("interval"), startAt: timestamp, seconds: z.number().int().min(60).max(31_536_000) }).strict(),
7
+ ]);
8
+ export const nodeData = z.object({
9
+ id: identifier, prompt: text(64_000), executorId: identifier,
10
+ dependsOn: z.array(identifier).max(20), maxRunSeconds: z.number().int().min(1).max(3600),
11
+ }).strict();
12
+ export const questionData = z.object({ nodeId: identifier.optional(), question: text(4000), completedWork: text(4000) }).strict();
13
+ export const replyData = z.object({
14
+ runId: z.string().uuid(), questionId: z.string().uuid(), requestKey: identifier, answer: text(16000),
15
+ }).strict();
16
+ /** Valid internal data. No request defaults, coercion or normalization at this boundary. */
17
+ export const taskData = z.object({
18
+ requestKey: identifier, name: text(200), prompt: text(64_000), nodes: z.array(nodeData).min(1).max(20),
19
+ workspace: absolutePath.optional(), schedule: scheduleData, expiresAt: timestamp.optional(),
20
+ maxRunSeconds: z.number().int().min(1).max(86_400), maxSteps: z.number().int().min(1).max(200),
21
+ maxNodeTurns: z.number().int().min(1).max(10), inputWaitSeconds: z.number().int().min(1).max(604800),
22
+ }).strict();
23
+ export const taskUpdateData = z.object({ id: z.string().uuid(), action: z.enum(["pause", "resume", "cancel"]) }).strict();
24
+ //# sourceMappingURL=validation.js.map
@@ -1,27 +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
- for (const [key, input] of Object.entries(fields)) if (key !== "note") input.addEventListener("input", () => { approved.input.checked = false; });
18
- card.append(approved.row, element("p", "按类型从用户原生 PATH 查找 ACP 适配器。继承用户原生环境,但过滤服务私密变量;额外 ACP 审批一律拒绝。结果固定保存在 dataDir/results,只向节点提供指定上游文件,不提供整个数据目录读取。"),
19
- button("从待保存配置中移除", () => { card.remove(); host.dispatchEvent(new Event("input", { bubbles: true })); }));
20
- host.append(card);
21
- return () => card.parentNode === host ? [{
22
- id: fields.id.value, type: fields.type.value, note: fields.note.value,
23
- cwd: fields.cwd.value, unattendedApproved: approved.input.checked,
24
- }] : [];
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(); };
25
26
  }
26
27
  function coordinatorEditor(host, config) {
27
28
  const provider = select("协调模型提供方", [["", "未配置"], ["openai", "OpenAI"], ["anthropic", "Anthropic"],
@@ -52,12 +53,18 @@ function coordinatorEditor(host, config) {
52
53
  };
53
54
  }
54
55
  export async function renderSettings(root, navigate) {
55
- let state = await request("/settings");
56
+ const initial = await Promise.all([request("/settings"), request("/executors")]);
57
+ let state = initial[0];
56
58
  root.append(element("h2", "配置与凭据"), element("p", "配置和密钥分别保存。保存不修改当前进程;每次重新启动(包括故障后拉起)读取已保存值。"),
57
59
  element("p", "先保存新密钥,再保存引用它的配置。执行端原生登录和原生权限仍通过官方工具管理;这里不编辑其认证文件。"),
58
60
  block("当前运行配置(只读,无密钥值)", state.running));
59
61
  if (state.writeBlocked) root.append(element("p", "上次文件提交结果不确定。请停服核查;当前禁止继续保存。"));
60
- 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");
61
68
  modelHost.append(element("legend", "协调模型"));
62
69
  const readModel = coordinatorEditor(modelHost, state.saved.coordinator);
63
70
  const startup = {
@@ -65,18 +72,24 @@ export async function renderSettings(root, navigate) {
65
72
  port: field("下次启动的监听端口(0 为自动分配)", state.saved.port, "number"),
66
73
  tokenEnv: field("MCP 令牌环境变量名(改名保留令牌值)", state.saved.tokenEnv),
67
74
  };
68
- form.append(element("h3", "启动配置"), ...Object.values(startup).map(item => item.row),
69
- modelHost, element("h3", "执行端注册与权限"), executors);
70
- for (const executor of state.saved.executors) readers.push(executorEditor(executors, executor));
71
- form.append(button("添加执行端", () => {
72
- readers.push(executorEditor(executors, { id: "", type: "pi", note: "", cwd: "", unattendedApproved: false }));
73
- }, 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);
74
85
  const preview = element("div");
75
86
  const save = submit(form, "校验并预览配置变更", async () => {
76
- const input = { editToken: state.editToken, config: { dataDir: startup.dataDir.input.value, port: startup.port.input.valueAsNumber, tokenEnv: startup.tokenEnv.input.value,
77
- coordinator: readModel(), executors: readers.flatMap(read => read()) } };
87
+ const input = { editToken: state.editToken,
88
+ config: { dataDir: startup.dataDir.input.value, port: startup.port.input.valueAsNumber, tokenEnv: startup.tokenEnv.input.value,
89
+ coordinator: readModel(), disabledExecutors: enabled.filter(item => !item.input.checked).map(item => item.type) } };
78
90
  const checked = await request("/settings/validate", input);
79
91
  preview.replaceChildren(block("校验后的待保存配置", checked.config),
92
+ block("配置变更", checked.changes),
80
93
  block("重启后受执行端变更影响的任务", checked.affectedTasks));
81
94
  const affected = checked.affectedTasks.map(task => task.name + " (" + task.id + ")").join("\n") || "无";
82
95
  if (!confirm("本地校验通过,不代表模型认证或 ACP 执行已验证。\n受影响任务:\n" + affected +
@@ -121,6 +134,6 @@ export async function renderSettings(root, navigate) {
121
134
  const ready = await request("/restart", {});
122
135
  document.dispatchEvent(new CustomEvent("service-restarted", { detail: ready }));
123
136
  }, state.writeBlocked),
124
- block("前台或 systemd / launchd 托管实例", "agent-task restart [--home /absolute/installation]"),
137
+ block("重启命令", "agent-task restart [--home /absolute/installation]"),
125
138
  element("p", "只有新实例通过认证健康检查才显示成功。若请求结果不确定,请先运行 agent-task status;启动失败时检查当前 dataDir/service.log,不删除历史或重放任务来修复。"));
126
139
  }
@@ -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 适配器检测,缺失时安装当时最新版,已有安装继续复用。升级由显式操作触发,避免普通重连改变任务使用的执行环境。
@@ -1,6 +1,6 @@
1
1
  # 架构原则
2
2
 
3
- 目标是让维护者可以局部理解并安全修改系统,不为目录对称或未来假设增加机制。产品边界见[总体方案](design-proposal.md),当前差距见[代码导航](technical-design.md)。
3
+ 目标是让维护者可以局部理解并安全修改系统,不为目录对称或未来假设增加机制。产品边界见[总体方案](design-proposal.md),职责与资源所有权见[代码导航](technical-design.md)。
4
4
 
5
5
  ## 职责与依赖
6
6
 
@@ -12,11 +12,13 @@
12
12
  | MCP/ACP、进程及操作系统协议 | 接入 |
13
13
  | 依赖装配、运行实例及生命周期 | 应用入口 |
14
14
  | 普通领域数据,不含业务行为或 SDK 对象 | 模型 |
15
- | 展示与收集操作,通过服务执行 | Web 控制台 |
15
+ | 展示与收集操作,通过服务执行 | Web 控制台、CLI |
16
16
 
17
17
  服务只接收普通数据和窄能力,不依赖 SDK 上下文、HTTP 对象、UI 或完整进程对象。接入实现服务所需能力,由入口装配;存储不反向依赖服务或协议。禁止循环依赖、隐藏的全局可变状态和服务定位器。
18
18
 
19
- 当前仍是扁平 MVP,不要求立即按层建目录。修改受影响职责时连同边界收敛,并保持行为测试;不做纯目录搬迁,不加空目录、无人使用的接口、转发层或动态插件平台。依赖边界检查应在分层落实后引入,不能提前声称已有。
19
+ CLI 覆盖 Web 的全部配置和管理操作,两端必须复用同一套底层服务,不维护各自的业务规则。
20
+
21
+ 按业务能力聚合模型、规则和服务,存储实现与协议接入保留明确边界。新增职责必须指定状态修改入口和资源所有者;保持关键行为测试,不加空目录、无人使用的接口、转发层或动态插件平台。实际分层和随 `npm run check` 执行的依赖检查见[代码导航](technical-design.md#依赖与一致性边界)。
20
22
 
21
23
  ## 执行与持久性
22
24
 
@@ -27,13 +29,13 @@
27
29
  - 未收到结果不等于没有执行。中断或结果不明时阻止新派发,包括模型新发起的调用;不自动重试、切换 Agent 或创建新身份重放。
28
30
  - 截止时间限制新执行开始,执行超时是另一规则。客户端断开不取消任务;取消不回滚外部操作。
29
31
  - 资源有明确所有者,关闭幂等,关闭后拒绝操作。先停止新派发,再处理活动执行、网络与存储,最后释放所有权。
30
- - 目录锁、事务、worker 监督和服务管理器解决不同问题,不等于跨机一致性、无孤儿进程或工具沙箱。
32
+ - 目录锁、事务和 worker 监督解决不同问题,不等于跨机一致性、无孤儿进程或工具沙箱。
31
33
 
32
34
  ## 权限与敏感数据
33
35
 
34
36
  - 执行端声明能力、管理员授权、真实验证证据分别表达,不能相互推导。
35
- - 只有管理员注册的执行端可启动。命令、凭据和能力端点属于管理员;任务可选择现有绝对工作空间,但路径校验不是授权或沙箱。
36
- - 授权须在可控制的实际操作入口检查。沿用执行端原生授权,额外 ACP 审批请求一律拒绝;`unattendedApproved` 不能证明所有原生工具已受控。
37
+ - 只有已发现、未禁用且 ACP 依赖就绪的本机执行端可启动。启动命令由系统控制,MCP 调用方不能提供命令、凭据或能力端点;任务目录在创建时确定,路径校验不是授权或沙箱。
38
+ - 授权须在可控制的实际操作入口检查。沿用执行端原生授权,额外 ACP 审批请求一律拒绝;发现与依赖就绪不能证明所有原生工具已受控。
37
39
  - Prompt、角色、技能、工具标题、协议成功和人工事实回复都不授予权限。取消、恢复和跨 Agent 传递不能扩大授权。
38
40
  - 无人值守执行不等待交互审批;授权不充分或元数据不足时拒绝,不退回更宽权限或自动永久允许。
39
41
  - MCP token、管理员 token、协调模型与执行端凭据分开;执行端继承用户原生环境前过滤服务私密变量,不有意记录秘密。Agent 输出和协调日志仍可能敏感。
package/docs/cli.md ADDED
@@ -0,0 +1,57 @@
1
+ # 本机管理 CLI
2
+
3
+ CLI 覆盖 Web 的配置、凭据、执行端管理、任务查询、未来触发控制及重启操作。两端通过同一管理 API 使用配置、执行端和任务服务;重启由同一启动器处理。命令入口见 [命令实现](../src/protocol/cli/commands.ts),管理传输见 [admin-client.ts](../src/protocol/cli/admin-client.ts)。
4
+
5
+ ## 连接与命令
6
+
7
+ 先按[部署说明](deployment.md)安装并启动服务。CLI 通过安装目录定位当前实例,从私有环境文件读取管理员令牌,不需要复制令牌或登录命令。使用自定义安装目录时,每条命令都传入 `--home /absolute/installation`。
8
+
9
+ ```bash
10
+ agent-task --help
11
+ agent-task config --help
12
+ agent-task executor --help
13
+ agent-task coordinator --help
14
+ agent-task credential --help
15
+ agent-task task --help
16
+ agent-task run --help
17
+ ```
18
+
19
+ 逐级帮助是命令与参数的完整入口。管理命令支持 `--json`,结果写入 stdout,预览和诊断写入 stderr;失败退出码非零。非交互保存、适配器升级与任务变更须使用 `--yes`,它只确认本次操作,不授予执行端原生工具权限。
20
+
21
+ 服务不可用时,管理命令失败,不自动离线写文件。首次安装仍使用 `setup`;启动故障按[部署说明](deployment.md#后台前台与健康检查)停服核查。CLI 不迁移数据或修复历史。
22
+
23
+ ## 配置流程
24
+
25
+ 交互编辑可用 `agent-task config edit`。编辑器由 `--editor`、`VISUAL` 或 `EDITOR` 指定,值须是可执行文件名或路径,不包含 shell 参数。退出编辑器后先校验并显示影响,再确认保存。
26
+
27
+ 批量编辑使用配置草稿:
28
+
29
+ ```bash
30
+ agent-task config export --output /absolute/private/draft.json
31
+ # 编辑草稿中的配置,保留编辑令牌。
32
+ agent-task config validate --file /absolute/private/draft.json
33
+ agent-task config apply --file /absolute/private/draft.json --yes
34
+ agent-task config diff
35
+ agent-task restart
36
+ agent-task config show --running
37
+ ```
38
+
39
+ 导出文件不包含密钥值,以私有权限创建,拒绝覆盖已有文件。编辑令牌过期、服务重启或任一客户端保存配置/凭据后,旧草稿不能提交;重新读取并人工核对,不自动合并。草稿契约见 [settings-contract.ts](../src/protocol/settings-contract.ts)。
40
+
41
+ `executor enable/disable` 和 `coordinator` 提供便捷配置操作。配置服务生成候选配置,再走同一预览、校验和保存流程。执行端自动发现,无须手工注册、设置默认工作目录或另行批准。启用与禁用在下次启动生效,不改写已有任务的执行端指派。
42
+
43
+ 配置与凭据均在下次启动生效。影响预览不修改旧任务快照,也不证明模型认证或 ACP 执行成功。保存顺序、端点与凭据隔离、重启影响见[控制台说明](web-console.md#保存与重启)。
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
+
53
+ ## 凭据与任务
54
+
55
+ `credential set NAME` 使用隐藏终端输入;自动化通过 `--stdin --yes` 从可信凭据工具或受保护文件传值,不把值写进命令参数。`credential list` 只显示设置状态。执行端原生登录和权限继续由其官方工具管理。
56
+
57
+ `task show ID` 查看固定 DAG,`run show ID` 查看节点、原始回合、已接受交付及问题回复。`task pause/resume/cancel` 仅控制未来触发,不停止当前运行;恢复不补跑,取消后不可恢复未来触发;有不确定外部结果时也拒绝恢复。操作边界与 Web 一致,见[控制台说明](web-console.md#操作范围)。