@realqiyan/agent-task 0.0.1-rc.2 → 0.0.1-rc.3
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/README.md +4 -3
- package/bin/agent-task.mjs +23 -36
- package/dist/{launcher.js → application/launcher.js} +19 -42
- package/dist/application/main.js +122 -0
- package/dist/{startup.js → application/startup.js} +6 -11
- package/dist/application/task-runtime.js +42 -0
- package/dist/configuration/contract.js +28 -0
- package/dist/{environment.js → configuration/environment.js} +1 -5
- package/dist/configuration/files.js +43 -0
- package/dist/configuration/management.js +6 -0
- package/dist/configuration/model.js +2 -0
- package/dist/configuration/policy.js +29 -0
- package/dist/configuration/ports.js +2 -0
- package/dist/{settings.js → configuration/settings.js} +77 -39
- package/dist/executors/environment.js +10 -0
- package/dist/executors/model.js +2 -0
- package/dist/executors/registry.js +29 -0
- package/dist/executors/validation.js +10 -0
- package/dist/{lock.js → infrastructure/lock.js} +2 -2
- package/dist/{private-files.js → infrastructure/private-files.js} +1 -7
- package/dist/infrastructure/sqlite.js +19 -0
- package/dist/infrastructure/version.js +3 -0
- package/dist/{acp.js → integrations/acp/connection.js} +9 -7
- package/dist/{node-execution.js → integrations/acp/execution.js} +1 -1
- package/dist/{executor-runtime.js → integrations/acp/runtime.js} +1 -10
- package/dist/{worker.js → integrations/acp/worker.js} +3 -2
- package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
- package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
- package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
- package/dist/integrations/local-service/control.js +33 -0
- package/dist/protocol/cli/admin-client.js +45 -0
- package/dist/protocol/cli/commands.js +394 -0
- package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
- package/dist/protocol/http/server.js +111 -0
- package/dist/{web.js → protocol/http/web.js} +18 -17
- package/dist/{mcp.js → protocol/mcp.js} +13 -13
- package/dist/protocol/services.js +2 -0
- package/dist/protocol/settings-contract.js +40 -0
- package/dist/protocol/task-contract.js +28 -0
- package/dist/results/files.js +70 -0
- package/dist/results/ports.js +2 -0
- package/dist/results/service.js +42 -0
- package/dist/shared/errors.js +13 -0
- package/dist/shared/limits.js +3 -0
- package/dist/shared/time.js +2 -0
- package/dist/shared/validation.js +8 -0
- package/dist/tasks/definitions.js +83 -0
- package/dist/tasks/graph.js +23 -0
- package/dist/{human-input.js → tasks/human-input.js} +8 -7
- package/dist/tasks/model.js +2 -0
- package/dist/tasks/ports.js +2 -0
- package/dist/tasks/runs.js +263 -0
- package/dist/tasks/schedule.js +36 -0
- package/dist/tasks/sessions.js +62 -0
- package/dist/tasks/storage/records.js +45 -0
- package/dist/{task-storage.js → tasks/storage/store.js} +23 -36
- package/dist/tasks/supervisor.js +227 -0
- package/dist/tasks/validation.js +24 -0
- package/dist/web/settings-view.js +13 -6
- package/docs/architecture-guidelines.md +6 -4
- package/docs/cli.md +49 -0
- package/docs/deployment.md +16 -56
- package/docs/design-proposal.md +2 -2
- package/docs/release.md +14 -15
- package/docs/technical-design.md +47 -26
- package/docs/validation.md +49 -17
- package/docs/web-console.md +10 -8
- package/package.json +7 -7
- package/scripts/install-lib.mjs +4 -95
- package/scripts/mcp-call.mjs +1 -1
- package/scripts/probe-agent.mjs +6 -5
- package/scripts/service-runner.mjs +1 -1
- package/scripts/setup-wizard.mjs +1 -6
- package/scripts/setup.mjs +17 -30
- package/deploy/agent-task.service.example +0 -17
- package/dist/admin.js +0 -31
- package/dist/config.js +0 -72
- package/dist/main.js +0 -175
- package/dist/model.js +0 -25
- package/dist/results.js +0 -79
- package/dist/settings-contract.js +0 -16
- package/dist/task-contract.js +0 -37
- package/dist/task-model.js +0 -2
- package/dist/tasks.js +0 -508
- package/dist/version.js +0 -3
- package/scripts/uninstall.mjs +0 -57
- /package/dist/{permissions.js → executors/permissions.js} +0 -0
- /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { ADMIN_TOKEN_ENV,
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv, validateConfig } from "./policy.js";
|
|
4
|
+
import { sameExecutor } from "../executors/registry.js";
|
|
4
5
|
import { parseEnvironment, validateServiceEnvironment } from "./environment.js";
|
|
5
|
-
import {
|
|
6
|
-
import { credentialUpdateSchema, settingsConfigSchema } from "./settings-contract.js";
|
|
6
|
+
import { FileCommitError } from "../shared/errors.js";
|
|
7
7
|
export class SettingsError extends Error {
|
|
8
8
|
code;
|
|
9
9
|
constructor(code, message) {
|
|
@@ -14,15 +14,6 @@ export class SettingsError extends Error {
|
|
|
14
14
|
const protectedKeys = new Set(["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "NODE_OPTIONS", "NODE_PATH",
|
|
15
15
|
"PI_ACP_PI_COMMAND", "AGENT_TASK_TOKEN", ADMIN_TOKEN_ENV, "__proto__", "constructor", "prototype"]);
|
|
16
16
|
const modelKeys = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY"];
|
|
17
|
-
function parse(schema, value) {
|
|
18
|
-
const result = schema.safeParse(value);
|
|
19
|
-
if (!result.success) {
|
|
20
|
-
// Field paths only: parser diagnostics and rejected values may contain credentials.
|
|
21
|
-
const fields = result.error.issues.slice(0, 8).map(issue => issue.path.join(".") || "request");
|
|
22
|
-
throw new SettingsError("invalid", "Invalid fields: " + fields.join(", "));
|
|
23
|
-
}
|
|
24
|
-
return result.data;
|
|
25
|
-
}
|
|
26
17
|
function referenced(config) {
|
|
27
18
|
return new Set(config.coordinator ? [coordinatorCredentialEnv(config.coordinator)] : []);
|
|
28
19
|
}
|
|
@@ -30,10 +21,15 @@ function editableKey(key, config) {
|
|
|
30
21
|
return !protectedKeys.has(key) && key !== config.tokenEnv && !/^(NODE_|LD_|DYLD_)/.test(key) &&
|
|
31
22
|
(/(?:_KEY|_TOKEN|_SECRET|_PASSWORD)$/.test(key) || referenced(config).has(key));
|
|
32
23
|
}
|
|
24
|
+
export function configChanges(before, after) {
|
|
25
|
+
return Object.keys(after).concat(Object.keys(before))
|
|
26
|
+
.filter((key, index, keys) => keys.indexOf(key) === index && !isDeepStrictEqual(before[key], after[key]))
|
|
27
|
+
.map(field => ({ field, before: before[field] ?? null, after: after[field] ?? null }));
|
|
28
|
+
}
|
|
33
29
|
/** Disk settings are next-start inputs only. This service never mutates live objects or process.env. */
|
|
34
30
|
export class Settings {
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
files;
|
|
32
|
+
paths;
|
|
37
33
|
edits = new Map();
|
|
38
34
|
initial;
|
|
39
35
|
running;
|
|
@@ -42,11 +38,9 @@ export class Settings {
|
|
|
42
38
|
closed = false;
|
|
43
39
|
uncertain = false;
|
|
44
40
|
startedAt = new Date().toISOString();
|
|
45
|
-
constructor(
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
48
|
-
if (configPath === environmentPath)
|
|
49
|
-
throw new SettingsError("invalid", "Configuration and environment must be separate files");
|
|
41
|
+
constructor(files, paths, running, base, environment) {
|
|
42
|
+
this.files = files;
|
|
43
|
+
this.paths = paths;
|
|
50
44
|
this.running = structuredClone(running);
|
|
51
45
|
this.base = { ...base };
|
|
52
46
|
this.runningEnvironment = { ...environment };
|
|
@@ -56,8 +50,7 @@ export class Settings {
|
|
|
56
50
|
if (this.closed)
|
|
57
51
|
throw new SettingsError("unavailable", "Configuration management is closed");
|
|
58
52
|
try {
|
|
59
|
-
|
|
60
|
-
return { configText, environmentText, config: configSchema.parse(JSON.parse(configText)), environment: parseEnvironment(environmentText) };
|
|
53
|
+
return this.files.read();
|
|
61
54
|
}
|
|
62
55
|
catch {
|
|
63
56
|
throw new SettingsError("unavailable", "Cannot read private configuration files; inspect their format, ownership and permissions offline");
|
|
@@ -96,19 +89,24 @@ export class Settings {
|
|
|
96
89
|
}
|
|
97
90
|
return snapshot;
|
|
98
91
|
}
|
|
99
|
-
candidate(
|
|
100
|
-
const
|
|
92
|
+
candidate(input) {
|
|
93
|
+
const snapshot = this.editing(input.editToken);
|
|
101
94
|
const environment = { ...this.base, ...snapshot.environment };
|
|
102
95
|
let config;
|
|
103
96
|
try {
|
|
104
|
-
config = validateConfig(input.config);
|
|
105
|
-
checkDataDirectory(config.dataDir);
|
|
97
|
+
config = validateConfig(input.config, this.paths.resolveWorkspace);
|
|
98
|
+
this.paths.checkDataDirectory(config.dataDir);
|
|
106
99
|
}
|
|
107
|
-
catch
|
|
108
|
-
if (error instanceof z.ZodError)
|
|
109
|
-
throw new SettingsError("invalid", "Invalid configuration fields");
|
|
100
|
+
catch {
|
|
110
101
|
throw new SettingsError("invalid", "Local validation failed: check data directory, workspaces and credential isolation");
|
|
111
102
|
}
|
|
103
|
+
for (const executor of config.executors) {
|
|
104
|
+
const previous = snapshot.config.executors.find(item => item.id === executor.id);
|
|
105
|
+
if (executor.unattendedApproved && (!previous?.unattendedApproved || !sameExecutor(previous, executor)) &&
|
|
106
|
+
!input.approveExecutors.includes(executor.id)) {
|
|
107
|
+
throw new SettingsError("invalid", "Executor changes require explicit approval of the new configuration: " + executor.id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
112
110
|
const previous = snapshot.config.coordinator, next = config.coordinator;
|
|
113
111
|
const endpoint = (model) => model.provider === "openai-compatible" ? model.baseUrl : model.provider;
|
|
114
112
|
if (previous && next && endpoint(previous) !== endpoint(next) && coordinatorCredentialEnv(previous) === coordinatorCredentialEnv(next)) {
|
|
@@ -131,19 +129,59 @@ export class Settings {
|
|
|
131
129
|
return config;
|
|
132
130
|
}
|
|
133
131
|
validate(value) { return this.candidate(value); }
|
|
134
|
-
|
|
135
|
-
const config = this.candidate(
|
|
136
|
-
|
|
132
|
+
preview(input) {
|
|
133
|
+
const config = this.candidate(input);
|
|
134
|
+
return { config, changes: configChanges(this.editing(input.editToken).config, config) };
|
|
135
|
+
}
|
|
136
|
+
diff() {
|
|
137
|
+
const saved = this.snapshot().config;
|
|
138
|
+
return { changes: configChanges(this.running, saved), ...this.status() };
|
|
139
|
+
}
|
|
140
|
+
/** Convenience commands create a draft; all publication still uses saveConfig. */
|
|
141
|
+
prepareChange(value) {
|
|
142
|
+
const { editToken, change } = value;
|
|
143
|
+
const config = structuredClone(this.editing(editToken).config), approveExecutors = [];
|
|
144
|
+
if (change.action === "executor-add") {
|
|
145
|
+
config.executors.push({ ...change.executor, unattendedApproved: false });
|
|
146
|
+
}
|
|
147
|
+
else if (change.action === "coordinator-set")
|
|
148
|
+
config.coordinator = change.coordinator;
|
|
149
|
+
else if (change.action === "coordinator-clear")
|
|
150
|
+
delete config.coordinator;
|
|
151
|
+
else {
|
|
152
|
+
const executor = config.executors.find(item => item.id === change.id);
|
|
153
|
+
if (!executor)
|
|
154
|
+
throw new SettingsError("invalid", "Executor not found");
|
|
155
|
+
if (change.action === "executor-remove")
|
|
156
|
+
config.executors = config.executors.filter(item => item !== executor);
|
|
157
|
+
else if (change.action === "executor-update") {
|
|
158
|
+
const updated = { ...executor, ...change.patch };
|
|
159
|
+
if (!sameExecutor(executor, updated))
|
|
160
|
+
updated.unattendedApproved = false;
|
|
161
|
+
config.executors[config.executors.indexOf(executor)] = updated;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
executor.unattendedApproved = change.action === "executor-approve";
|
|
165
|
+
if (executor.unattendedApproved)
|
|
166
|
+
approveExecutors.push(executor.id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const input = { editToken, config, approveExecutors };
|
|
170
|
+
return { ...input, config: this.candidate(input) };
|
|
171
|
+
}
|
|
172
|
+
saveConfig(input) {
|
|
173
|
+
const config = this.candidate(input);
|
|
174
|
+
const snapshot = this.editing(input.editToken);
|
|
137
175
|
if (config.tokenEnv !== snapshot.config.tokenEnv) {
|
|
138
176
|
snapshot.environment[config.tokenEnv] = snapshot.environment[snapshot.config.tokenEnv];
|
|
139
177
|
// Publish an alias first. Either configuration still uses the same service token.
|
|
140
|
-
this.publish(
|
|
178
|
+
this.publish("environment", snapshot.environment);
|
|
141
179
|
}
|
|
142
|
-
this.publish(
|
|
180
|
+
this.publish("config", config);
|
|
143
181
|
return this.get();
|
|
144
182
|
}
|
|
145
|
-
saveCredential(
|
|
146
|
-
const
|
|
183
|
+
saveCredential(input) {
|
|
184
|
+
const snapshot = this.editing(input.editToken);
|
|
147
185
|
if (!editableKey(input.key, snapshot.config) || !editableKey(input.key, this.running) ||
|
|
148
186
|
[snapshot.environment[snapshot.config.tokenEnv], snapshot.environment[ADMIN_TOKEN_ENV]].includes(snapshot.environment[input.key]))
|
|
149
187
|
throw new SettingsError("invalid", "This environment key is not console-managed; new credential names must end in _KEY, _TOKEN, _SECRET or _PASSWORD");
|
|
@@ -161,12 +199,12 @@ export class Settings {
|
|
|
161
199
|
}
|
|
162
200
|
// Recheck the complete environment format, not only the edited value.
|
|
163
201
|
parseEnvironment(JSON.stringify(snapshot.environment));
|
|
164
|
-
this.publish(
|
|
202
|
+
this.publish("environment", snapshot.environment);
|
|
165
203
|
return this.get();
|
|
166
204
|
}
|
|
167
|
-
publish(
|
|
205
|
+
publish(target, value) {
|
|
168
206
|
try {
|
|
169
|
-
|
|
207
|
+
this.files.publish(target, value);
|
|
170
208
|
}
|
|
171
209
|
catch (error) {
|
|
172
210
|
if (error instanceof FileCommitError && error.uncertain)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv } from "../configuration/policy.js";
|
|
2
|
+
/** Private service inputs never enter workers, even through an alias with the same value. */
|
|
3
|
+
export function executorEnvironment(native, privateValues, config) {
|
|
4
|
+
const blocked = new Set([...Object.keys(privateValues), config.tokenEnv, ADMIN_TOKEN_ENV,
|
|
5
|
+
...(config.coordinator ? [coordinatorCredentialEnv(config.coordinator)] : [])]);
|
|
6
|
+
const secrets = new Set([...Object.values(privateValues), ...[...blocked].map(key => native[key])].filter(Boolean));
|
|
7
|
+
return Object.fromEntries(Object.entries(native).filter(([key, value]) => value !== undefined && !blocked.has(key) && !secrets.has(value) &&
|
|
8
|
+
!/^(AGENT_TASK_|NODE_|LD_|DYLD_)/.test(key) && key !== "PI_ACP_PI_COMMAND"));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function sameExecutor(a, b) {
|
|
2
|
+
return a.id === b.id && a.type === b.type && a.cwd === b.cwd;
|
|
3
|
+
}
|
|
4
|
+
export function authorizedProfile(snapshot, current) {
|
|
5
|
+
return Boolean(snapshot?.unattendedApproved && current?.unattendedApproved && sameExecutor(snapshot, current));
|
|
6
|
+
}
|
|
7
|
+
/** Immutable registrations for one service lifetime. Settings apply to the next instance. */
|
|
8
|
+
export class ExecutorRegistry {
|
|
9
|
+
profiles;
|
|
10
|
+
constructor(profiles) { this.profiles = structuredClone(profiles); }
|
|
11
|
+
list() { return { executors: this.profiles.map(profile => ({ ...profile, transport: "stdio" })) }; }
|
|
12
|
+
snapshot(ids) {
|
|
13
|
+
const result = Object.create(null);
|
|
14
|
+
for (const id of ids) {
|
|
15
|
+
const profile = this.profiles.find(item => item.id === id);
|
|
16
|
+
if (!profile?.unattendedApproved)
|
|
17
|
+
throw new Error("Executor not found or not approved: " + id);
|
|
18
|
+
result[id] = structuredClone(profile);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
authorize(snapshot) {
|
|
23
|
+
if (!snapshot || !authorizedProfile(snapshot, this.profiles.find(profile => profile.id === snapshot.id))) {
|
|
24
|
+
throw new Error("Executor authorization revoked or changed");
|
|
25
|
+
}
|
|
26
|
+
return structuredClone(snapshot);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { absolutePath, identifier } from "../shared/validation.js";
|
|
3
|
+
export const executorRecord = z.object({
|
|
4
|
+
id: identifier, type: z.enum(["pi", "codex", "claude", "opencode"]),
|
|
5
|
+
note: z.string().max(2000), cwd: absolutePath, unattendedApproved: z.boolean(),
|
|
6
|
+
}).strict();
|
|
7
|
+
export const executorSchema = executorRecord.extend({
|
|
8
|
+
note: executorRecord.shape.note.default(""), unattendedApproved: z.boolean().default(false),
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { DatabaseSync } from "
|
|
1
|
+
import { DatabaseSync } from "./sqlite.js";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
/** The stable installation lock prevents two launchers
|
|
4
|
+
/** The stable installation lock prevents two launchers during a directory switch. */
|
|
5
5
|
export function acquireLauncherLock(home) {
|
|
6
6
|
const db = new DatabaseSync(join(home, "launcher-lock.sqlite"));
|
|
7
7
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FileCommitError } from "../shared/errors.js";
|
|
1
2
|
import { existsSync, constants, openSync, closeSync, fstatSync, lstatSync, realpathSync, readFileSync, writeFileSync, fsyncSync, renameSync, unlinkSync } from "node:fs";
|
|
2
3
|
import { dirname, basename, join, isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -44,13 +45,6 @@ export function readPrivateText(path) {
|
|
|
44
45
|
closeSync(fd);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
|
-
export class FileCommitError extends Error {
|
|
48
|
-
uncertain;
|
|
49
|
-
constructor(uncertain) {
|
|
50
|
-
super(uncertain ? "Configuration publication could not be confirmed; stop and inspect the files" : "Configuration was not published");
|
|
51
|
-
this.uncertain = uncertain;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
48
|
/** Caller owns the configuration. No await between comparison, validation and publication. */
|
|
55
49
|
export function replacePrivateText(path, text) {
|
|
56
50
|
const temporary = join(dirname(path), "." + basename(path) + "." + randomUUID() + ".tmp");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
// Node 22 warns on loading SQLite. Suppress only that known notice during the
|
|
3
|
+
// synchronous import, then restore normal warning handling for all other code.
|
|
4
|
+
const emitWarning = process.emitWarning;
|
|
5
|
+
let sqlite;
|
|
6
|
+
try {
|
|
7
|
+
process.emitWarning = (warning, ...args) => {
|
|
8
|
+
if (warning === "SQLite is an experimental feature and might change at any time"
|
|
9
|
+
&& args[0] === "ExperimentalWarning")
|
|
10
|
+
return;
|
|
11
|
+
Reflect.apply(emitWarning, process, [warning, ...args]);
|
|
12
|
+
};
|
|
13
|
+
sqlite = createRequire(import.meta.url)("node:sqlite");
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
process.emitWarning = emitWarning;
|
|
17
|
+
}
|
|
18
|
+
export const DatabaseSync = sqlite.DatabaseSync;
|
|
19
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -2,10 +2,11 @@ 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 { executorCommand } from "./
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { executorCommand } from "./runtime.js";
|
|
6
|
+
import { Results } from "../../results/service.js";
|
|
7
|
+
import { LocalResultFiles } from "../../results/files.js";
|
|
8
|
+
import { permissionDecision } from "../../executors/permissions.js";
|
|
9
|
+
import { version } from "../../infrastructure/version.js";
|
|
9
10
|
/** Project only public progress. Queue every piece before yielding so concurrent notifications cannot reorder output. */
|
|
10
11
|
export function progressEvents(update) {
|
|
11
12
|
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
|
|
@@ -38,7 +39,7 @@ function signalGroup(child, signal) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
/** Kill the whole group even if its leader already exited
|
|
42
|
+
/** Kill the whole group even if its leader already exited. */
|
|
42
43
|
export async function terminateGroup(child) {
|
|
43
44
|
signalGroup(child, "SIGTERM");
|
|
44
45
|
await delay(300);
|
|
@@ -49,6 +50,7 @@ export async function terminateGroup(child) {
|
|
|
49
50
|
}
|
|
50
51
|
export async function openAcp(executor, signal, emit, resultsRoot, inputFiles = []) {
|
|
51
52
|
signal.throwIfAborted();
|
|
53
|
+
const results = new Results(new LocalResultFiles(resultsRoot));
|
|
52
54
|
const launch = executorCommand(executor.type, process.env);
|
|
53
55
|
const child = spawn(launch.command, launch.args, {
|
|
54
56
|
cwd: executor.cwd, env: process.env, detached: true, stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -103,7 +105,7 @@ export async function openAcp(executor, signal, emit, resultsRoot, inputFiles =
|
|
|
103
105
|
cancel.throwIfAborted();
|
|
104
106
|
if (ctx.params.sessionId !== sessionId)
|
|
105
107
|
throw new Error("Wrong input-file session");
|
|
106
|
-
return { content: readInputFile(
|
|
108
|
+
return { content: results.readInputFile(inputFiles, ctx.params.path, ctx.params.line ?? 1, ctx.params.limit ?? undefined) };
|
|
107
109
|
}
|
|
108
110
|
catch (error) {
|
|
109
111
|
controller.abort(error);
|
|
@@ -166,4 +168,4 @@ export async function openAcp(executor, signal, emit, resultsRoot, inputFiles =
|
|
|
166
168
|
throw error;
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
|
-
//# sourceMappingURL=
|
|
171
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
-
import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv } from "./config.js";
|
|
4
3
|
const commands = {
|
|
5
4
|
pi: { command: "pi-acp", args: [] },
|
|
6
5
|
codex: { command: "codex-acp", args: [] },
|
|
@@ -26,12 +25,4 @@ export function executorCommand(type, environment) {
|
|
|
26
25
|
find("pi");
|
|
27
26
|
return { command: find(entry.command), args: [...entry.args] };
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
export function executorEnvironment(native, privateValues, config) {
|
|
31
|
-
const blocked = new Set([...Object.keys(privateValues), config.tokenEnv, ADMIN_TOKEN_ENV,
|
|
32
|
-
...(config.coordinator ? [coordinatorCredentialEnv(config.coordinator)] : [])]);
|
|
33
|
-
const secrets = new Set([...Object.values(privateValues), ...[...blocked].map(key => native[key])].filter(Boolean));
|
|
34
|
-
return Object.fromEntries(Object.entries(native).filter(([key, value]) => value !== undefined && !blocked.has(key) && !secrets.has(value) &&
|
|
35
|
-
!/^(AGENT_TASK_|NODE_|LD_|DYLD_)/.test(key) && key !== "PI_ACP_PI_COMMAND"));
|
|
36
|
-
}
|
|
37
|
-
//# sourceMappingURL=executor-runtime.js.map
|
|
28
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { openAcp } from "./
|
|
1
|
+
import { openAcp } from "./connection.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { errorText
|
|
3
|
+
import { errorText } from "../../shared/errors.js";
|
|
4
|
+
import { executorSchema } from "../../executors/validation.js";
|
|
4
5
|
const inputSchema = z.object({ executor: executorSchema, inputFiles: z.array(z.string()).max(20), resultsRoot: z.string().min(1) }).strict();
|
|
5
6
|
const controller = new AbortController();
|
|
6
7
|
const pending = new Map();
|
|
@@ -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 "./
|
|
4
|
+
import { createCoordinatorProvider } from "./provider.js";
|
|
5
5
|
import { createRegistry, defineExtension, defineTool, Harness, section } from "@earendil-works/pi-durable";
|
|
6
|
-
import { openCoordinatorStorage } from "./
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { DatabaseSync } from "
|
|
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=
|
|
43
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { request } from "node:http";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { startupPaths } from "../../configuration/files.js";
|
|
4
|
+
/** Local administrator capability: the socket lives in the private launcher directory. */
|
|
5
|
+
export function controlService(home, action) {
|
|
6
|
+
startupPaths(home);
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
let timedOut = false;
|
|
9
|
+
const req = request({ socketPath: join(home, "control.sock"), path: "/" + action, method: "POST" }, res => {
|
|
10
|
+
let body = "";
|
|
11
|
+
res.setEncoding("utf8");
|
|
12
|
+
res.on("data", chunk => { body += chunk; });
|
|
13
|
+
res.on("end", () => {
|
|
14
|
+
if (res.statusCode !== 200) {
|
|
15
|
+
reject(new Error("Service unavailable; inspect private logs and startup files"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
resolve(JSON.parse(body));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
reject(new Error("Invalid launcher response"));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
req.setTimeout(60_000, () => { timedOut = true; req.destroy(); });
|
|
27
|
+
req.once("error", () => reject(new Error(timedOut
|
|
28
|
+
? "Service control timed out; outcome is unknown, check status before another restart"
|
|
29
|
+
: "Cannot contact launcher; start the foreground or managed service first")));
|
|
30
|
+
req.end();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# 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
|