@realqiyan/agent-task 0.0.1-rc.3 → 0.1.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +17 -0
- package/README.md +28 -6
- package/bin/agent-task.mjs +18 -6
- package/config.example.json +1 -1
- package/dist/application/launcher.js +30 -16
- package/dist/application/main.js +5 -2
- package/dist/application/mcp-entry.js +71 -0
- package/dist/application/task-runtime.js +3 -3
- package/dist/configuration/contract.js +2 -2
- package/dist/configuration/files.js +2 -3
- package/dist/configuration/management.js +1 -1
- package/dist/configuration/policy.js +3 -8
- package/dist/configuration/settings.js +10 -33
- package/dist/executors/ports.js +2 -0
- package/dist/executors/registry.js +117 -15
- package/dist/executors/validation.js +4 -5
- package/dist/infrastructure/lock.js +25 -13
- package/dist/infrastructure/workspace.js +4 -3
- package/dist/integrations/acp/connection.js +5 -5
- package/dist/integrations/acp/discovery.js +271 -0
- package/dist/integrations/acp/runtime.js +38 -23
- package/dist/integrations/acp/worker.js +5 -3
- package/dist/integrations/local-service/control.js +36 -11
- package/dist/protocol/cli/commands.js +41 -46
- package/dist/protocol/http/server.js +8 -0
- package/dist/protocol/http/web.js +7 -1
- package/dist/protocol/mcp-contract.js +194 -0
- package/dist/protocol/mcp.js +99 -32
- package/dist/protocol/settings-contract.js +4 -7
- package/dist/protocol/stdio.js +72 -0
- package/dist/protocol/task-contract.js +45 -18
- package/dist/shared/errors.js +15 -0
- package/dist/shared/limits.js +3 -0
- package/dist/tasks/definitions.js +19 -14
- package/dist/tasks/graph.js +5 -4
- package/dist/tasks/human-input.js +10 -9
- package/dist/tasks/runs.js +31 -30
- package/dist/tasks/schedule.js +2 -1
- package/dist/tasks/storage/records.js +6 -6
- package/dist/tasks/storage/store.js +8 -7
- package/dist/tasks/supervisor.js +10 -7
- package/dist/tasks/validation.js +3 -0
- package/dist/web/settings-view.js +43 -37
- package/docs/adr/0001-task-request-scope.md +3 -0
- package/docs/adr/0002-mcp-workspace-source.md +3 -0
- package/docs/adr/0003-adapter-installation.md +3 -0
- package/docs/architecture-guidelines.md +2 -2
- package/docs/cli.md +12 -4
- package/docs/deployment.md +52 -36
- package/docs/design-proposal.md +3 -0
- package/docs/release.md +27 -8
- package/docs/technical-design.md +5 -3
- package/docs/validation.md +59 -9
- package/docs/web-console.md +7 -4
- package/package.json +3 -1
- package/scripts/install-lib.mjs +5 -23
- package/scripts/probe-agent.mjs +10 -9
- package/scripts/service-runner.mjs +8 -3
- package/scripts/setup-wizard.mjs +38 -11
- package/scripts/setup.mjs +69 -95
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { absolutePath, identifier } from "../shared/validation.js";
|
|
3
|
+
export const executorTypeSchema = z.enum(["pi", "codex", "claude", "opencode"]);
|
|
3
4
|
export const executorRecord = z.object({
|
|
4
|
-
id: identifier, type:
|
|
5
|
-
|
|
5
|
+
id: identifier, type: executorTypeSchema, nativePath: absolutePath,
|
|
6
|
+
command: absolutePath, args: z.array(z.string().max(4000)).max(100),
|
|
7
|
+
adapterVersion: z.string().min(1).max(200).nullable(),
|
|
6
8
|
}).strict();
|
|
7
|
-
export const executorSchema = executorRecord.extend({
|
|
8
|
-
note: executorRecord.shape.note.default(""), unattendedApproved: z.boolean().default(false),
|
|
9
|
-
});
|
|
10
9
|
//# sourceMappingURL=validation.js.map
|
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
import { DatabaseSync } from "./sqlite.js";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
5
|
/** The stable installation lock prevents two launchers during a directory switch. */
|
|
5
|
-
export function acquireLauncherLock(home) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
db
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
export async function acquireLauncherLock(home) {
|
|
7
|
+
const deadline = Date.now() + 1500;
|
|
8
|
+
for (;;) {
|
|
9
|
+
const db = new DatabaseSync(join(home, "launcher-lock.sqlite"));
|
|
10
|
+
try {
|
|
11
|
+
db.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE");
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
db.close();
|
|
15
|
+
const code = error.errcode;
|
|
16
|
+
if (code !== 5 && code !== 6)
|
|
17
|
+
throw new Error("Cannot acquire launcher ownership; inspect the installation filesystem", { cause: error });
|
|
18
|
+
if (Date.now() >= deadline)
|
|
19
|
+
throw new Error("Another launcher owns this installation", { cause: error });
|
|
20
|
+
// On a new SQLite file, concurrent readers can all lose the first exclusive-lock attempt.
|
|
21
|
+
// Release each failed connection and stagger retries before any service work starts.
|
|
22
|
+
await delay(25 + Math.floor(Math.random() * 25));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
let released = false;
|
|
26
|
+
return () => { if (!released) {
|
|
27
|
+
db.close();
|
|
28
|
+
released = true;
|
|
29
|
+
} };
|
|
13
30
|
}
|
|
14
|
-
let released = false;
|
|
15
|
-
return () => { if (!released) {
|
|
16
|
-
db.close();
|
|
17
|
-
released = true;
|
|
18
|
-
} };
|
|
19
31
|
}
|
|
20
32
|
/** A dedicated SQLite transaction owns an OS lock until close or process death.
|
|
21
33
|
* Local filesystems only. Never unlink this file: that would create a second lock identity.
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
|
+
import { TaskError } from "../shared/errors.js";
|
|
3
4
|
/** Path validity only; native executor policy owns directory and tool access. */
|
|
4
5
|
export function resolveWorkspace(path) {
|
|
5
6
|
if (!path || path.includes("\0") || !isAbsolute(path))
|
|
6
|
-
throw new
|
|
7
|
+
throw new TaskError("validation", "Workspace must be an absolute directory path");
|
|
7
8
|
const canonical = realpathSync(path);
|
|
8
9
|
if (!statSync(canonical).isDirectory())
|
|
9
|
-
throw new
|
|
10
|
+
throw new TaskError("validation", "Workspace must be an existing directory");
|
|
10
11
|
return canonical;
|
|
11
12
|
}
|
|
12
13
|
export function checkWorkspace(path) {
|
|
13
14
|
if (resolveWorkspace(path) !== path)
|
|
14
|
-
throw new
|
|
15
|
+
throw new TaskError("conflict", "Workspace path changed since admission");
|
|
15
16
|
}
|
|
16
17
|
//# sourceMappingURL=workspace.js.map
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { Readable, Writable } from "node:stream";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
4
|
import * as acp from "@agentclientprotocol/sdk";
|
|
5
|
-
import {
|
|
5
|
+
import { executorLaunch } from "./runtime.js";
|
|
6
6
|
import { Results } from "../../results/service.js";
|
|
7
7
|
import { LocalResultFiles } from "../../results/files.js";
|
|
8
8
|
import { permissionDecision } from "../../executors/permissions.js";
|
|
@@ -48,12 +48,12 @@ export async function terminateGroup(child) {
|
|
|
48
48
|
child.stdout?.destroy();
|
|
49
49
|
child.stderr?.destroy();
|
|
50
50
|
}
|
|
51
|
-
export async function openAcp(executor, signal, emit, resultsRoot, inputFiles = []) {
|
|
51
|
+
export async function openAcp(executor, workspace, signal, emit, resultsRoot, inputFiles = []) {
|
|
52
52
|
signal.throwIfAborted();
|
|
53
53
|
const results = new Results(new LocalResultFiles(resultsRoot));
|
|
54
|
-
const launch =
|
|
54
|
+
const launch = executorLaunch(executor, process.env);
|
|
55
55
|
const child = spawn(launch.command, launch.args, {
|
|
56
|
-
cwd:
|
|
56
|
+
cwd: workspace, env: launch.env, detached: true, stdio: ["pipe", "pipe", "pipe"],
|
|
57
57
|
});
|
|
58
58
|
const controller = new AbortController();
|
|
59
59
|
const cancel = AbortSignal.any([signal, controller.signal]);
|
|
@@ -136,7 +136,7 @@ export async function openAcp(executor, signal, emit, resultsRoot, inputFiles =
|
|
|
136
136
|
if (init.protocolVersion !== acp.PROTOCOL_VERSION)
|
|
137
137
|
throw new Error("Incompatible ACP protocol version");
|
|
138
138
|
await report("agent", { name: init.agentInfo?.name ?? null, version: init.agentInfo?.version ?? null });
|
|
139
|
-
const session = await connection.agent.request(acp.methods.agent.session.new, { cwd:
|
|
139
|
+
const session = await connection.agent.request(acp.methods.agent.session.new, { cwd: workspace, mcpServers: [] });
|
|
140
140
|
sessionId = session.sessionId;
|
|
141
141
|
await report("session", sessionId);
|
|
142
142
|
cancel.throwIfAborted();
|
|
@@ -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
|
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
export function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
catch { /* Try the next native PATH entry. */ }
|
|
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;
|
|
20
18
|
}
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 };
|
|
27
42
|
}
|
|
28
43
|
//# sourceMappingURL=runtime.js.map
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { openAcp } from "./connection.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { errorText } from "../../shared/errors.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
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();
|
|
6
8
|
const controller = new AbortController();
|
|
7
9
|
const pending = new Map();
|
|
8
10
|
let sequence = 0, started = false, busy = false;
|
|
@@ -65,7 +67,7 @@ process.on("message", (message) => {
|
|
|
65
67
|
started = true;
|
|
66
68
|
opening = (async () => {
|
|
67
69
|
const input = inputSchema.parse(message.input);
|
|
68
|
-
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);
|
|
69
71
|
void session.closed.then(() => { if (!closing)
|
|
70
72
|
void fail(new Error("ACP process exited")); });
|
|
71
73
|
if (!controller.signal.aborted)
|
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
import { request } from "node:http";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { startupPaths } from "../../configuration/files.js";
|
|
4
|
-
|
|
5
|
-
|
|
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) {
|
|
6
12
|
startupPaths(home);
|
|
7
13
|
return new Promise((resolve, reject) => {
|
|
8
14
|
let timedOut = false;
|
|
9
|
-
const req = request({ socketPath: join(home, "control.sock"), path: "/" + action, method: "POST" }, res => {
|
|
15
|
+
const req = request({ socketPath: join(home, "control.sock"), path: "/" + action, method: "POST", agent: false }, res => {
|
|
10
16
|
let body = "";
|
|
11
17
|
res.setEncoding("utf8");
|
|
12
|
-
res.on("data", chunk => {
|
|
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
|
+
});
|
|
13
25
|
res.on("end", () => {
|
|
14
26
|
if (res.statusCode !== 200) {
|
|
15
|
-
reject(new
|
|
27
|
+
reject(new ControlError("unavailable", "Service unavailable; inspect private logs and startup files"));
|
|
16
28
|
return;
|
|
17
29
|
}
|
|
18
30
|
try {
|
|
19
|
-
|
|
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
|
+
}
|
|
20
43
|
}
|
|
21
44
|
catch {
|
|
22
|
-
reject(new
|
|
45
|
+
reject(new ControlError("invalid", "Invalid launcher response"));
|
|
23
46
|
}
|
|
24
47
|
});
|
|
48
|
+
res.once("error", () => reject(new ControlError("unavailable", "Launcher response interrupted; check status before another action")));
|
|
25
49
|
});
|
|
26
|
-
req.setTimeout(
|
|
27
|
-
req.once("error", () => reject(new
|
|
28
|
-
|
|
29
|
-
|
|
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")));
|
|
30
55
|
req.end();
|
|
31
56
|
});
|
|
32
57
|
}
|