@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,271 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants, existsSync, lstatSync, openSync, closeSync, mkdirSync, readFileSync, realpathSync, writeFileSync, fsyncSync, renameSync, unlinkSync } from "node:fs";
4
+ import { join, relative, dirname } from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { acquireLock } from "../../infrastructure/lock.js";
7
+ import { checkDataDirectory, readPrivateText } from "../../infrastructure/private-files.js";
8
+ import { FileCommitError } from "../../shared/errors.js";
9
+ import { checkExecutable, findExecutable } from "./runtime.js";
10
+ const types = ["pi", "codex", "claude", "opencode"];
11
+ const adapters = {
12
+ pi: { package: "pi-acp", bin: "pi-acp" },
13
+ codex: { package: "@agentclientprotocol/codex-acp", bin: "codex-acp" },
14
+ claude: { package: "@agentclientprotocol/claude-agent-acp", bin: "claude-agent-acp" },
15
+ };
16
+ const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
17
+ const directoryPattern = /^installation-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/;
18
+ /** Owns managed adapter files. Discovery never installs a native CLI or starts an agent. */
19
+ export class LocalExecutors {
20
+ adaptersRoot;
21
+ environment;
22
+ installing = new Map();
23
+ constructor(adaptersRoot, nativeEnvironment) {
24
+ this.adaptersRoot = adaptersRoot;
25
+ checkDataDirectory(adaptersRoot);
26
+ this.environment = { ...nativeEnvironment };
27
+ }
28
+ discover() {
29
+ const executors = [], states = [];
30
+ for (const type of types) {
31
+ const nativePath = this.nativePath(type);
32
+ const state = { id: type, type, nativePath, state: "missing", adapterVersion: null, error: null };
33
+ try {
34
+ const executor = nativePath ? this.installed(type, nativePath) : null;
35
+ if (executor) {
36
+ executors.push(executor);
37
+ state.state = "ready";
38
+ state.adapterVersion = executor.adapterVersion;
39
+ }
40
+ }
41
+ catch {
42
+ state.state = "error";
43
+ state.error = "Managed ACP adapter is invalid; explicitly upgrade this executor to replace it";
44
+ }
45
+ if (this.installing.has(type))
46
+ state.state = "installing";
47
+ states.push(state);
48
+ }
49
+ return { executors, states };
50
+ }
51
+ install(type, upgrade, signal) {
52
+ const pending = this.installing.get(type);
53
+ if (pending)
54
+ return pending;
55
+ const installation = this.prepare(type, upgrade, signal).finally(() => { this.installing.delete(type); });
56
+ this.installing.set(type, installation);
57
+ return installation;
58
+ }
59
+ check(executor) {
60
+ if (executor.id !== executor.type || !types.includes(executor.type) || this.nativePath(executor.type) !== executor.nativePath) {
61
+ throw new Error("Native executor is missing or changed");
62
+ }
63
+ checkExecutable(executor.nativePath);
64
+ if (executor.type === "opencode") {
65
+ if (executor.command !== executor.nativePath || executor.adapterVersion !== null ||
66
+ executor.args.length !== 1 || executor.args[0] !== "acp")
67
+ throw new Error("Invalid OpenCode launch");
68
+ }
69
+ else {
70
+ const typeRoot = join(this.adaptersRoot, executor.type);
71
+ const directory = relative(typeRoot, executor.command).split("/")[0];
72
+ if (!directory || !directoryPattern.test(directory) || !executor.adapterVersion || executor.args.length) {
73
+ throw new Error("Invalid managed ACP launch");
74
+ }
75
+ const expected = this.adapter(executor.type, executor.nativePath, { directory, version: executor.adapterVersion });
76
+ if (expected.command !== executor.command)
77
+ throw new Error("Managed ACP executable changed");
78
+ }
79
+ checkExecutable(executor.command);
80
+ }
81
+ nativePath(type) {
82
+ return findExecutable(type, this.environment, this.adaptersRoot);
83
+ }
84
+ installed(type, nativePath) {
85
+ if (type === "opencode")
86
+ return { id: type, type, nativePath, command: nativePath, args: ["acp"], adapterVersion: null };
87
+ const path = join(this.adaptersRoot, type, "current.json");
88
+ if (!lstatSync(path, { throwIfNoEntry: false }))
89
+ return null;
90
+ let value;
91
+ try {
92
+ value = JSON.parse(readPrivateText(path));
93
+ }
94
+ catch {
95
+ throw new Error("Invalid adapter installation record");
96
+ }
97
+ if (!value || typeof value !== "object" || Array.isArray(value))
98
+ throw new Error("Invalid adapter installation record");
99
+ const record = value;
100
+ if (Object.keys(record).length !== 2 || typeof record.version !== "string" || typeof record.directory !== "string" ||
101
+ !versionPattern.test(record.version) || !directoryPattern.test(record.directory))
102
+ throw new Error("Invalid adapter installation record");
103
+ return this.adapter(type, nativePath, { version: record.version, directory: record.directory });
104
+ }
105
+ adapter(type, nativePath, installation) {
106
+ const root = join(this.adaptersRoot, type, installation.directory);
107
+ checkDataDirectory(root);
108
+ const entry = adapters[type];
109
+ const packageRoot = join(root, "node_modules", entry.package);
110
+ if (realpathSync(packageRoot) !== packageRoot)
111
+ throw new Error("Invalid managed ACP package directory");
112
+ const metadata = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
113
+ if (metadata.name !== entry.package || metadata.version !== installation.version)
114
+ throw new Error("Managed ACP package version changed");
115
+ const command = realpathSync(join(root, "node_modules/.bin", entry.bin));
116
+ if (!command.startsWith(packageRoot + "/"))
117
+ throw new Error("Managed ACP executable is outside its package");
118
+ checkExecutable(command);
119
+ return { id: type, type, nativePath, command, args: [], adapterVersion: installation.version };
120
+ }
121
+ async prepare(type, upgrade, requestedSignal) {
122
+ const signal = AbortSignal.any([requestedSignal, AbortSignal.timeout(300_000)]);
123
+ signal.throwIfAborted();
124
+ const nativePath = this.nativePath(type);
125
+ if (!nativePath)
126
+ throw new Error("Native CLI is not installed: " + type);
127
+ if (type === "opencode")
128
+ return this.installed(type, nativePath);
129
+ if (!upgrade) {
130
+ const existing = this.installed(type, nativePath);
131
+ if (existing)
132
+ return existing;
133
+ }
134
+ checkDataDirectory(this.adaptersRoot);
135
+ mkdirSync(this.adaptersRoot, { recursive: true, mode: 0o700 });
136
+ const root = join(this.adaptersRoot, type);
137
+ checkDataDirectory(root);
138
+ mkdirSync(root, { mode: 0o700, recursive: true });
139
+ const unlock = await this.lock(root, signal);
140
+ try {
141
+ if (!upgrade) {
142
+ const existing = this.installed(type, nativePath);
143
+ if (existing)
144
+ return existing;
145
+ }
146
+ const npm = findExecutable("npm", this.environment);
147
+ if (!npm)
148
+ throw new Error("npm is required to install ACP adapters");
149
+ const entry = adapters[type];
150
+ const versionText = await this.npm(npm, ["view", entry.package + "@latest", "version", "--json"], root, signal, true);
151
+ let version;
152
+ try {
153
+ version = JSON.parse(versionText);
154
+ }
155
+ catch {
156
+ throw new Error("Registry did not return an ACP adapter version");
157
+ }
158
+ if (typeof version !== "string" || version.length > 100 || !versionPattern.test(version))
159
+ throw new Error("Registry returned an invalid ACP adapter version");
160
+ const installation = { version, directory: "installation-" + randomUUID() };
161
+ const directory = join(root, installation.directory);
162
+ mkdirSync(directory, { mode: 0o700 });
163
+ writeFileSync(join(directory, "package.json"), JSON.stringify({ private: true }) + "\n", { flag: "wx", mode: 0o600 });
164
+ await this.npm(npm, ["install", "--global=false", "--prefix", directory, "--save-prod", "--save-exact",
165
+ "--package-lock=true", "--ignore-scripts", "--no-audit", "--no-fund", entry.package + "@" + version], directory, signal);
166
+ signal.throwIfAborted();
167
+ const executor = this.adapter(type, nativePath, installation);
168
+ this.check(executor);
169
+ signal.throwIfAborted();
170
+ this.publish(join(root, "current.json"), installation);
171
+ return executor;
172
+ }
173
+ catch (error) {
174
+ if (signal.aborted)
175
+ throw new Error("ACP adapter installation was cancelled or timed out");
176
+ if (error instanceof FileCommitError && error.uncertain) {
177
+ throw new Error("ACP adapter publication could not be confirmed; inspect executor status before retrying");
178
+ }
179
+ // Never include npm diagnostics or parser excerpts in an API response.
180
+ if (error instanceof Error && /^(npm is required|Registry (did not return|returned)|Native executor is missing)/.test(error.message))
181
+ throw error;
182
+ throw new Error("ACP adapter installation failed; previous installed adapter was preserved");
183
+ }
184
+ finally {
185
+ await unlock();
186
+ }
187
+ }
188
+ async lock(root, signal) {
189
+ for (;;) {
190
+ signal.throwIfAborted();
191
+ try {
192
+ return await acquireLock(root);
193
+ }
194
+ catch (error) {
195
+ const code = error.cause?.errcode;
196
+ if (code !== 5 && code !== 6)
197
+ throw new Error("Could not acquire ACP adapter installation ownership");
198
+ await delay(100, undefined, { signal });
199
+ }
200
+ }
201
+ }
202
+ npm(command, args, cwd, signal, capture = false) {
203
+ signal.throwIfAborted();
204
+ const env = Object.fromEntries(["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR"].flatMap(key => this.environment[key] === undefined ? [] : [[key, this.environment[key]]]));
205
+ return new Promise((resolve, reject) => {
206
+ const child = spawn(command, args, { cwd, env, detached: true, stdio: ["ignore", "pipe", "pipe"] });
207
+ let output = "", bytes = 0, failed = false;
208
+ const stop = () => {
209
+ failed = true;
210
+ if (child.pid) {
211
+ try {
212
+ process.kill(-child.pid, "SIGKILL");
213
+ }
214
+ catch { /* Wait for process close before releasing installation ownership. */ }
215
+ }
216
+ };
217
+ const receive = (chunk, stdout) => {
218
+ bytes += chunk.length;
219
+ if (bytes > 1_048_576)
220
+ stop();
221
+ else if (capture && stdout)
222
+ output += chunk.toString("utf8");
223
+ };
224
+ child.stdout.on("data", (chunk) => receive(chunk, true));
225
+ child.stderr.on("data", (chunk) => receive(chunk, false));
226
+ child.once("error", () => { failed = true; });
227
+ signal.addEventListener("abort", stop, { once: true });
228
+ child.once("close", code => {
229
+ signal.removeEventListener("abort", stop);
230
+ if (failed || signal.aborted || code !== 0)
231
+ reject(new Error("npm operation failed"));
232
+ else
233
+ resolve(output.trim());
234
+ });
235
+ if (signal.aborted)
236
+ stop();
237
+ });
238
+ }
239
+ publish(path, installation) {
240
+ const temporary = join(dirname(path), ".current-" + randomUUID() + ".tmp");
241
+ let fd, published = false;
242
+ try {
243
+ if (lstatSync(path, { throwIfNoEntry: false }))
244
+ readPrivateText(path);
245
+ fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
246
+ writeFileSync(fd, JSON.stringify(installation) + "\n");
247
+ fsyncSync(fd);
248
+ closeSync(fd);
249
+ fd = undefined;
250
+ renameSync(temporary, path);
251
+ published = true;
252
+ const directory = openSync(dirname(path), constants.O_RDONLY);
253
+ try {
254
+ fsyncSync(directory);
255
+ }
256
+ finally {
257
+ closeSync(directory);
258
+ }
259
+ }
260
+ catch {
261
+ throw new FileCommitError(published);
262
+ }
263
+ finally {
264
+ if (fd !== undefined)
265
+ closeSync(fd);
266
+ if (existsSync(temporary))
267
+ unlinkSync(temporary);
268
+ }
269
+ }
270
+ }
271
+ //# sourceMappingURL=discovery.js.map
@@ -99,4 +99,4 @@ export const createExecution = (environment) => (input, event, beforeStart) => {
99
99
  },
100
100
  };
101
101
  };
102
- //# sourceMappingURL=node-execution.js.map
102
+ //# sourceMappingURL=execution.js.map
@@ -0,0 +1,43 @@
1
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
+ import { delimiter, isAbsolute, join } from "node:path";
3
+ export function checkExecutable(path) {
4
+ if (!isAbsolute(path) || /[\x00-\x1f\x7f]/.test(path) || realpathSync(path) !== path || !statSync(path).isFile()) {
5
+ throw new Error("Expected a canonical executable path");
6
+ }
7
+ accessSync(path, constants.X_OK);
8
+ }
9
+ /** Resolve native executables without a shell or service-managed dependencies. */
10
+ export function findExecutable(name, environment, excludedRoot) {
11
+ for (const directory of (environment.PATH ?? "").split(delimiter).filter(isAbsolute)) {
12
+ try {
13
+ const path = realpathSync(join(directory, name));
14
+ if (excludedRoot && (path === excludedRoot || path.startsWith(excludedRoot + "/")))
15
+ continue;
16
+ checkExecutable(path);
17
+ return path;
18
+ }
19
+ catch { /* Try the next native PATH entry. */ }
20
+ }
21
+ return null;
22
+ }
23
+ /** A stored launch binds the adapter to the native executable discovered for this Task. */
24
+ export function executorLaunch(executor, environment) {
25
+ checkExecutable(executor.nativePath);
26
+ checkExecutable(executor.command);
27
+ if (executor.type === "opencode"
28
+ ? executor.command !== executor.nativePath || executor.args.length !== 1 || executor.args[0] !== "acp"
29
+ : executor.args.length !== 0)
30
+ throw new Error("Invalid ACP launch arguments");
31
+ const env = { ...environment };
32
+ delete env.PI_ACP_PI_COMMAND;
33
+ delete env.CODEX_PATH;
34
+ delete env.CLAUDE_CODE_EXECUTABLE;
35
+ if (executor.type === "pi")
36
+ env.PI_ACP_PI_COMMAND = executor.nativePath;
37
+ if (executor.type === "codex")
38
+ env.CODEX_PATH = executor.nativePath;
39
+ if (executor.type === "claude")
40
+ env.CLAUDE_CODE_EXECUTABLE = executor.nativePath;
41
+ return { command: executor.command, args: [...executor.args], env };
42
+ }
43
+ //# sourceMappingURL=runtime.js.map
@@ -1,7 +1,10 @@
1
- import { openAcp } from "./acp.js";
1
+ import { openAcp } from "./connection.js";
2
2
  import { z } from "zod";
3
- import { errorText, executorSchema } from "./model.js";
4
- const inputSchema = z.object({ executor: executorSchema, inputFiles: z.array(z.string()).max(20), resultsRoot: z.string().min(1) }).strict();
3
+ import { errorText } from "../../shared/errors.js";
4
+ import { executorRecord } from "../../executors/validation.js";
5
+ import { absolutePath } from "../../shared/validation.js";
6
+ const inputSchema = z.object({ executor: executorRecord, workspace: absolutePath,
7
+ inputFiles: z.array(z.string()).max(20), resultsRoot: z.string().min(1) }).strict();
5
8
  const controller = new AbortController();
6
9
  const pending = new Map();
7
10
  let sequence = 0, started = false, busy = false;
@@ -64,7 +67,7 @@ process.on("message", (message) => {
64
67
  started = true;
65
68
  opening = (async () => {
66
69
  const input = inputSchema.parse(message.input);
67
- session = await openAcp(input.executor, controller.signal, emit, input.resultsRoot, input.inputFiles);
70
+ session = await openAcp(input.executor, input.workspace, controller.signal, emit, input.resultsRoot, input.inputFiles);
68
71
  void session.closed.then(() => { if (!closing)
69
72
  void fail(new Error("ACP process exited")); });
70
73
  if (!controller.signal.aborted)
@@ -1,9 +1,9 @@
1
1
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
2
2
  import { Type } from "@earendil-works/pi-ai";
3
3
  import { createModels } from "@earendil-works/pi-ai/models";
4
- import { createCoordinatorProvider } from "./coordinator-provider.js";
4
+ import { createCoordinatorProvider } from "./provider.js";
5
5
  import { createRegistry, defineExtension, defineTool, Harness, section } from "@earendil-works/pi-durable";
6
- import { openCoordinatorStorage } from "./coordinator-storage.js";
6
+ import { openCoordinatorStorage } from "./storage.js";
7
7
  const instructions = `You advance one persisted task run using the DAG supplied by the caller. External ACP agents do all domain work.
8
8
  Start with inspect_run. The nodes, prompts, dependencies and assigned executorIds are already fixed. Do not decompose the task, add nodes, replace the plan or switch executors.
9
9
  Use converse to start a pending node only after all direct dependencies have accepted deliveries. Its first turn uses the caller-supplied node prompt. Independent nodes may run concurrently.
@@ -79,7 +79,8 @@ export function createCoordinator(directory, config) {
79
79
  tool({ name: "request_input", description: "Persist a question and suspend this run for human facts. Only after all ACP turns finish. Not permission approval.",
80
80
  parameters: Type.Object({ nodeId: Type.Optional(Type.String()),
81
81
  question: Type.String({ minLength: 1, maxLength: 4000 }), completedWork: Type.String({ minLength: 1, maxLength: 4000 }) }), replay: "unsafe",
82
- execute: async (args) => ({ ...output(actions.ask(args)), control: { terminate: true } }) }),
82
+ execute: async (args) => ({ ...output(actions.ask({ ...args, question: args.question.trim(), completedWork: args.completedWork.trim() })),
83
+ control: { terminate: true } }) }),
83
84
  tool({ name: "stop_run", description: "Stop without success for an unachievable task. Use request_input for missing user facts.",
84
85
  parameters: Type.Object({ status: Type.Literal("failed"),
85
86
  reason: Type.String({ minLength: 1, maxLength: 4000 }) }), replay: "unsafe",
@@ -87,10 +88,18 @@ export function createCoordinator(directory, config) {
87
88
  ],
88
89
  }));
89
90
  const storage = await openCoordinatorStorage(directory, runId);
90
- const harness = await Harness.open(storage, {
91
- models, registry,
92
- settings: { retry: { enabled: false }, stream: { maxRetries: 0, timeoutMs: 120000 } },
93
- }, context);
91
+ let harness;
92
+ try {
93
+ harness = await Harness.open(storage, {
94
+ models, registry,
95
+ settings: { retry: { enabled: false }, stream: { maxRetries: 0, timeoutMs: 120000 } },
96
+ }, context);
97
+ }
98
+ catch (error) {
99
+ // Ownership transfers to Harness only after open succeeds. Storage close is idempotent.
100
+ await storage.close(context);
101
+ throw error;
102
+ }
94
103
  // Close rather than mark durable tasks aborted. A safe run may resume its same submission after restart.
95
104
  const abort = () => { void harness.close(context).catch(() => { }); };
96
105
  signal.addEventListener("abort", abort, { once: true });
@@ -25,4 +25,4 @@ export function createCoordinatorProvider(config) {
25
25
  });
26
26
  }
27
27
  }
28
- //# sourceMappingURL=coordinator-provider.js.map
28
+ //# sourceMappingURL=provider.js.map
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
3
+ import { DatabaseSync } from "../../infrastructure/sqlite.js";
4
4
  import { SqliteStorage } from "@earendil-works/pi-durable/storage/sqlite";
5
5
  import { openNodeSqliteDatabase } from "@earendil-works/pi-durable/storage/sqlite/node";
6
6
  /** The caller holds the application directory lock. New files initialize; existing files must already match the pinned SDK. */
@@ -40,4 +40,4 @@ export async function openCoordinatorStorage(directory, runId) {
40
40
  throw error;
41
41
  }
42
42
  }
43
- //# sourceMappingURL=coordinator-storage.js.map
43
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1,58 @@
1
+ import { request } from "node:http";
2
+ import { join } from "node:path";
3
+ import { startupPaths } from "../../configuration/files.js";
4
+ export class ControlError extends Error {
5
+ kind;
6
+ constructor(kind, message) {
7
+ super(message);
8
+ this.kind = kind;
9
+ }
10
+ }
11
+ export function controlService(home, action, timeout = 60_000) {
12
+ startupPaths(home);
13
+ return new Promise((resolve, reject) => {
14
+ let timedOut = false;
15
+ const req = request({ socketPath: join(home, "control.sock"), path: "/" + action, method: "POST", agent: false }, res => {
16
+ let body = "";
17
+ res.setEncoding("utf8");
18
+ res.on("data", chunk => {
19
+ body += chunk;
20
+ if (body.length > 16_384) {
21
+ reject(new ControlError("invalid", "Invalid launcher response"));
22
+ req.destroy();
23
+ }
24
+ });
25
+ res.on("end", () => {
26
+ if (res.statusCode !== 200) {
27
+ reject(new ControlError("unavailable", "Service unavailable; inspect private logs and startup files"));
28
+ return;
29
+ }
30
+ try {
31
+ const value = JSON.parse(body);
32
+ if (action === "stop") {
33
+ if (value.stopped !== true)
34
+ throw new Error();
35
+ resolve({ stopped: true });
36
+ }
37
+ else {
38
+ if (typeof value.instanceId !== "string" || typeof value.url !== "string" || !Number.isInteger(value.pid) ||
39
+ !(value.consoleUrl === null || typeof value.consoleUrl === "string"))
40
+ throw new Error();
41
+ resolve(value);
42
+ }
43
+ }
44
+ catch {
45
+ reject(new ControlError("invalid", "Invalid launcher response"));
46
+ }
47
+ });
48
+ res.once("error", () => reject(new ControlError("unavailable", "Launcher response interrupted; check status before another action")));
49
+ });
50
+ req.setTimeout(timeout, () => { timedOut = true; req.destroy(); });
51
+ req.once("error", (error) => reject(new ControlError(timedOut ? "timeout"
52
+ : ["ENOENT", "ECONNREFUSED"].includes(error.code ?? "") ? "unreachable" : "unavailable", timedOut
53
+ ? "Service control timed out; check status before another action"
54
+ : "Cannot contact launcher; use agent-task mcp or start the service")));
55
+ req.end();
56
+ });
57
+ }
58
+ //# sourceMappingURL=control.js.map
@@ -0,0 +1,45 @@
1
+ import { controlService } from "../../integrations/local-service/control.js";
2
+ import { startupPaths } from "../../configuration/files.js";
3
+ import { readEnvironment } from "../../configuration/files.js";
4
+ import { ADMIN_TOKEN_ENV } from "../../configuration/policy.js";
5
+ /** Local CLI transport. Credentials never enter arguments, URLs or diagnostics. */
6
+ export async function connectAdmin(home) {
7
+ const ready = await controlService(home, "status");
8
+ if (!ready.consoleUrl)
9
+ throw new Error("Administrator API is disabled; configure an administrator token while stopped");
10
+ const url = new URL(ready.consoleUrl);
11
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.username || url.password) {
12
+ throw new Error("Invalid local administrator address");
13
+ }
14
+ const token = readEnvironment(startupPaths(home).environmentPath)[ADMIN_TOKEN_ENV];
15
+ if (!token)
16
+ throw new Error("Administrator credential is unavailable");
17
+ return {
18
+ ready,
19
+ async request(path, input) {
20
+ let response;
21
+ try {
22
+ response = await fetch(url.origin + "/admin/api" + path, {
23
+ method: input === undefined ? "GET" : "POST", redirect: "error",
24
+ headers: { Authorization: "Bearer " + token,
25
+ ...(input === undefined ? {} : { Origin: url.origin, "Content-Type": "application/json" }) },
26
+ body: input === undefined ? undefined : JSON.stringify(input), signal: AbortSignal.timeout(60_000),
27
+ });
28
+ }
29
+ catch {
30
+ throw new Error("Administrator request could not be confirmed; inspect current state before retrying. No offline write was attempted");
31
+ }
32
+ let body;
33
+ try {
34
+ body = await response.json();
35
+ }
36
+ catch {
37
+ throw new Error("Invalid administrator response");
38
+ }
39
+ if (!response.ok)
40
+ throw new Error(body.error ?? "Administrator request rejected (HTTP " + response.status + ")");
41
+ return body.data;
42
+ },
43
+ };
44
+ }
45
+ //# sourceMappingURL=admin-client.js.map