@realqiyan/agent-task 0.0.1-rc.1 → 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 +8 -5
- package/bin/agent-task.mjs +23 -41
- package/dist/application/launcher.js +215 -0
- package/dist/application/main.js +122 -0
- package/dist/application/startup.js +51 -0
- package/dist/application/task-runtime.js +42 -0
- package/dist/configuration/contract.js +28 -0
- package/dist/configuration/environment.js +32 -0
- 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} +97 -42
- package/dist/executors/environment.js +10 -0
- package/dist/executors/model.js +2 -0
- package/dist/{permissions.js → executors/permissions.js} +3 -4
- package/dist/executors/registry.js +29 -0
- package/dist/executors/validation.js +10 -0
- package/dist/{lock.js → infrastructure/lock.js} +17 -1
- package/dist/{private-files.js → infrastructure/private-files.js} +17 -9
- package/dist/infrastructure/sqlite.js +19 -0
- package/dist/infrastructure/version.js +3 -0
- package/dist/{acp.js → integrations/acp/connection.js} +14 -11
- package/dist/{node-execution.js → integrations/acp/execution.js} +3 -4
- package/dist/integrations/acp/runtime.js +28 -0
- package/dist/{worker.js → integrations/acp/worker.js} +5 -4
- 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} +31 -18
- 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/api.js +1 -1
- package/dist/web/app.js +7 -0
- package/dist/web/settings-view.js +40 -28
- package/docs/architecture-guidelines.md +8 -6
- package/docs/cli.md +49 -0
- package/docs/deployment.md +32 -70
- package/docs/design-proposal.md +3 -3
- package/docs/release.md +17 -18
- package/docs/technical-design.md +47 -26
- package/docs/validation.md +57 -17
- package/docs/web-console.md +15 -19
- package/package.json +8 -8
- package/scripts/install-lib.mjs +7 -105
- package/scripts/mcp-call.mjs +1 -1
- package/scripts/probe-agent.mjs +11 -5
- package/scripts/service-runner.mjs +2 -7
- package/scripts/setup-wizard.mjs +3 -8
- package/scripts/setup.mjs +31 -40
- package/deploy/agent-task.service.example +0 -17
- package/dist/admin.js +0 -31
- package/dist/config.js +0 -113
- package/dist/environment.js +0 -23
- package/dist/main.js +0 -131
- package/dist/model.js +0 -28
- package/dist/results.js +0 -85
- 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 -509
- package/dist/version.js +0 -3
- package/scripts/uninstall.mjs +0 -53
- /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { checkDataDirectory, readPrivateText, replacePrivateText } from "../infrastructure/private-files.js";
|
|
4
|
+
import { resolveWorkspace } from "../infrastructure/workspace.js";
|
|
5
|
+
import { configSchema } from "./contract.js";
|
|
6
|
+
import { parseEnvironment } from "./environment.js";
|
|
7
|
+
import { validateConfig } from "./policy.js";
|
|
8
|
+
export const configurationPaths = { resolveWorkspace, checkDataDirectory };
|
|
9
|
+
export function loadConfig(path) {
|
|
10
|
+
let value;
|
|
11
|
+
try {
|
|
12
|
+
value = JSON.parse(readPrivateText(path));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error("Cannot read configuration JSON");
|
|
16
|
+
}
|
|
17
|
+
return validateConfig(configSchema.parse(value), resolveWorkspace);
|
|
18
|
+
}
|
|
19
|
+
export function readEnvironment(path) { return parseEnvironment(readPrivateText(path)); }
|
|
20
|
+
export class LocalSettingsFiles {
|
|
21
|
+
configPath;
|
|
22
|
+
environmentPath;
|
|
23
|
+
constructor(configPath, environmentPath) {
|
|
24
|
+
this.configPath = configPath;
|
|
25
|
+
this.environmentPath = environmentPath;
|
|
26
|
+
if (configPath === environmentPath)
|
|
27
|
+
throw new Error("Configuration and environment must be separate files");
|
|
28
|
+
}
|
|
29
|
+
read() {
|
|
30
|
+
const configText = readPrivateText(this.configPath), environmentText = readPrivateText(this.environmentPath);
|
|
31
|
+
return { configText, environmentText, config: configSchema.parse(JSON.parse(configText)), environment: parseEnvironment(environmentText) };
|
|
32
|
+
}
|
|
33
|
+
publish(target, value) {
|
|
34
|
+
replacePrivateText(target === "config" ? this.configPath : this.environmentPath, JSON.stringify(value, null, 2) + "\n");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** A stable launch locator is the only runtime input outside the selected dataDir. */
|
|
38
|
+
export function startupPaths(home) {
|
|
39
|
+
const { dataDir } = z.object({ dataDir: z.string() }).strict().parse(JSON.parse(readPrivateText(join(home, "launch.json"))));
|
|
40
|
+
checkDataDirectory(dataDir);
|
|
41
|
+
return { dataDir, configPath: join(dataDir, "config.json"), environmentPath: join(dataDir, "environment.json") };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Configuration preview and task impact are one management operation for all clients. */
|
|
2
|
+
export function previewSettings(settings, tasks, input) {
|
|
3
|
+
const preview = settings.preview(input);
|
|
4
|
+
return { ...preview, affectedTasks: tasks.executorImpact(preview.config.executors) };
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=management.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const ADMIN_TOKEN_ENV = "AGENT_TASK_ADMIN_TOKEN";
|
|
2
|
+
export const baseEnvKeys = ["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "NODE_OPTIONS", "NODE_PATH", "__proto__", "constructor", "prototype"];
|
|
3
|
+
export function coordinatorCredentialEnv(config) {
|
|
4
|
+
switch (config.provider) {
|
|
5
|
+
case "openai": return "OPENAI_API_KEY";
|
|
6
|
+
case "anthropic": return "ANTHROPIC_API_KEY";
|
|
7
|
+
case "deepseek": return "DEEPSEEK_API_KEY";
|
|
8
|
+
case "openai-compatible": return config.apiKeyEnv;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function validateConfig(value, resolveWorkspace) {
|
|
12
|
+
const config = structuredClone(value);
|
|
13
|
+
if (!config.dataDir.startsWith("/") || /[\x00-\x1f\x7f]/.test(config.dataDir))
|
|
14
|
+
throw new Error("dataDir must be absolute without control characters");
|
|
15
|
+
const coordinatorKey = config.coordinator ? coordinatorCredentialEnv(config.coordinator) : undefined;
|
|
16
|
+
if (/^(NODE_|LD_|DYLD_)/.test(config.tokenEnv) || [...baseEnvKeys, ADMIN_TOKEN_ENV, "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY", coordinatorKey].includes(config.tokenEnv))
|
|
17
|
+
throw new Error("tokenEnv must be separate from base environment, administrator and model credentials");
|
|
18
|
+
if (coordinatorKey === ADMIN_TOKEN_ENV)
|
|
19
|
+
throw new Error("Administrator token must not be used as a model credential");
|
|
20
|
+
const ids = new Set();
|
|
21
|
+
for (const executor of config.executors) {
|
|
22
|
+
if (ids.has(executor.id))
|
|
23
|
+
throw new Error("Duplicate executor ID");
|
|
24
|
+
ids.add(executor.id);
|
|
25
|
+
executor.cwd = resolveWorkspace(executor.cwd);
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { ADMIN_TOKEN_ENV,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv, validateConfig } from "./policy.js";
|
|
4
|
+
import { sameExecutor } from "../executors/registry.js";
|
|
5
|
+
import { parseEnvironment, validateServiceEnvironment } from "./environment.js";
|
|
6
|
+
import { FileCommitError } from "../shared/errors.js";
|
|
7
7
|
export class SettingsError extends Error {
|
|
8
8
|
code;
|
|
9
9
|
constructor(code, message) {
|
|
@@ -14,27 +14,22 @@ 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
|
-
return new Set(
|
|
28
|
-
...(config.coordinator ? [coordinatorCredentialEnv(config.coordinator)] : [])]);
|
|
18
|
+
return new Set(config.coordinator ? [coordinatorCredentialEnv(config.coordinator)] : []);
|
|
29
19
|
}
|
|
30
20
|
function editableKey(key, config) {
|
|
31
21
|
return !protectedKeys.has(key) && key !== config.tokenEnv && !/^(NODE_|LD_|DYLD_)/.test(key) &&
|
|
32
22
|
(/(?:_KEY|_TOKEN|_SECRET|_PASSWORD)$/.test(key) || referenced(config).has(key));
|
|
33
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
|
+
}
|
|
34
29
|
/** Disk settings are next-start inputs only. This service never mutates live objects or process.env. */
|
|
35
30
|
export class Settings {
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
files;
|
|
32
|
+
paths;
|
|
38
33
|
edits = new Map();
|
|
39
34
|
initial;
|
|
40
35
|
running;
|
|
@@ -43,11 +38,9 @@ export class Settings {
|
|
|
43
38
|
closed = false;
|
|
44
39
|
uncertain = false;
|
|
45
40
|
startedAt = new Date().toISOString();
|
|
46
|
-
constructor(
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
if (configPath === environmentPath)
|
|
50
|
-
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;
|
|
51
44
|
this.running = structuredClone(running);
|
|
52
45
|
this.base = { ...base };
|
|
53
46
|
this.runningEnvironment = { ...environment };
|
|
@@ -57,8 +50,7 @@ export class Settings {
|
|
|
57
50
|
if (this.closed)
|
|
58
51
|
throw new SettingsError("unavailable", "Configuration management is closed");
|
|
59
52
|
try {
|
|
60
|
-
|
|
61
|
-
return { configText, environmentText, config: configSchema.parse(JSON.parse(configText)), environment: parseEnvironment(environmentText) };
|
|
53
|
+
return this.files.read();
|
|
62
54
|
}
|
|
63
55
|
catch {
|
|
64
56
|
throw new SettingsError("unavailable", "Cannot read private configuration files; inspect their format, ownership and permissions offline");
|
|
@@ -80,7 +72,7 @@ export class Settings {
|
|
|
80
72
|
const editToken = randomUUID();
|
|
81
73
|
this.edits.set(editToken, { configText: snapshot.configText, environmentText: snapshot.environmentText, expires: now + 15 * 60_000 });
|
|
82
74
|
const keys = new Set([...modelKeys, ...Object.keys(snapshot.environment), ...referenced(snapshot.config), ...referenced(this.running)]);
|
|
83
|
-
const credentials = [...keys].filter(key => editableKey(key, snapshot.config)).sort().map(key => ({
|
|
75
|
+
const credentials = [...keys].filter(key => editableKey(key, snapshot.config) && editableKey(key, this.running) && ![snapshot.environment[snapshot.config.tokenEnv], snapshot.environment[ADMIN_TOKEN_ENV]].includes(snapshot.environment[key])).sort().map(key => ({
|
|
84
76
|
key, saved: Boolean(snapshot.environment[key]), running: Boolean(this.runningEnvironment[key]),
|
|
85
77
|
pendingRestart: snapshot.environment[key] !== this.runningEnvironment[key],
|
|
86
78
|
referenced: referenced(snapshot.config).has(key),
|
|
@@ -97,17 +89,23 @@ export class Settings {
|
|
|
97
89
|
}
|
|
98
90
|
return snapshot;
|
|
99
91
|
}
|
|
100
|
-
candidate(
|
|
101
|
-
const
|
|
92
|
+
candidate(input) {
|
|
93
|
+
const snapshot = this.editing(input.editToken);
|
|
102
94
|
const environment = { ...this.base, ...snapshot.environment };
|
|
103
95
|
let config;
|
|
104
96
|
try {
|
|
105
|
-
config = validateConfig(
|
|
97
|
+
config = validateConfig(input.config, this.paths.resolveWorkspace);
|
|
98
|
+
this.paths.checkDataDirectory(config.dataDir);
|
|
106
99
|
}
|
|
107
|
-
catch
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
catch {
|
|
101
|
+
throw new SettingsError("invalid", "Local validation failed: check data directory, workspaces and credential isolation");
|
|
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
|
+
}
|
|
111
109
|
}
|
|
112
110
|
const previous = snapshot.config.coordinator, next = config.coordinator;
|
|
113
111
|
const endpoint = (model) => model.provider === "openai-compatible" ? model.baseUrl : model.provider;
|
|
@@ -118,17 +116,74 @@ export class Settings {
|
|
|
118
116
|
if (!environment[key])
|
|
119
117
|
throw new SettingsError("invalid", "A referenced environment value is missing; prepare credentials or native configuration first");
|
|
120
118
|
}
|
|
119
|
+
const token = environment[config.tokenEnv] ?? environment[snapshot.config.tokenEnv];
|
|
120
|
+
try {
|
|
121
|
+
validateServiceEnvironment(config, { ...environment, [config.tokenEnv]: token });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new SettingsError("invalid", "Service, administrator and model credentials must be valid and distinct");
|
|
125
|
+
}
|
|
126
|
+
if (config.tokenEnv !== snapshot.config.tokenEnv && environment[config.tokenEnv] && environment[config.tokenEnv] !== environment[snapshot.config.tokenEnv]) {
|
|
127
|
+
throw new SettingsError("invalid", "tokenEnv already contains another value; use a new environment name");
|
|
128
|
+
}
|
|
121
129
|
return config;
|
|
122
130
|
}
|
|
123
131
|
validate(value) { return this.candidate(value); }
|
|
124
|
-
|
|
125
|
-
const config = this.candidate(
|
|
126
|
-
this.
|
|
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);
|
|
175
|
+
if (config.tokenEnv !== snapshot.config.tokenEnv) {
|
|
176
|
+
snapshot.environment[config.tokenEnv] = snapshot.environment[snapshot.config.tokenEnv];
|
|
177
|
+
// Publish an alias first. Either configuration still uses the same service token.
|
|
178
|
+
this.publish("environment", snapshot.environment);
|
|
179
|
+
}
|
|
180
|
+
this.publish("config", config);
|
|
127
181
|
return this.get();
|
|
128
182
|
}
|
|
129
|
-
saveCredential(
|
|
130
|
-
const
|
|
131
|
-
if (!editableKey(input.key, snapshot.config))
|
|
183
|
+
saveCredential(input) {
|
|
184
|
+
const snapshot = this.editing(input.editToken);
|
|
185
|
+
if (!editableKey(input.key, snapshot.config) || !editableKey(input.key, this.running) ||
|
|
186
|
+
[snapshot.environment[snapshot.config.tokenEnv], snapshot.environment[ADMIN_TOKEN_ENV]].includes(snapshot.environment[input.key]))
|
|
132
187
|
throw new SettingsError("invalid", "This environment key is not console-managed; new credential names must end in _KEY, _TOKEN, _SECRET or _PASSWORD");
|
|
133
188
|
if (input.action === "delete") {
|
|
134
189
|
if (referenced(snapshot.config).has(input.key))
|
|
@@ -144,12 +199,12 @@ export class Settings {
|
|
|
144
199
|
}
|
|
145
200
|
// Recheck the complete environment format, not only the edited value.
|
|
146
201
|
parseEnvironment(JSON.stringify(snapshot.environment));
|
|
147
|
-
this.publish(
|
|
202
|
+
this.publish("environment", snapshot.environment);
|
|
148
203
|
return this.get();
|
|
149
204
|
}
|
|
150
|
-
publish(
|
|
205
|
+
publish(target, value) {
|
|
151
206
|
try {
|
|
152
|
-
|
|
207
|
+
this.files.publish(target, value);
|
|
153
208
|
}
|
|
154
209
|
catch (error) {
|
|
155
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
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/** Applies only to ACP requests that actually ask for approval; this is not a sandbox. */
|
|
2
|
-
export function permissionDecision(request,
|
|
2
|
+
export function permissionDecision(request, cancelled = false) {
|
|
3
3
|
if (cancelled)
|
|
4
4
|
return { outcome: { outcome: "cancelled" } };
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const option = request.options.find(value => value.kind === (allowed ? "allow_once" : "reject_once"));
|
|
5
|
+
// Native authorization must suffice. A new ACP approval would widen it.
|
|
6
|
+
const option = request.options.find(value => value.kind === "reject_once");
|
|
8
7
|
return option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
|
|
9
8
|
}
|
|
10
9
|
//# sourceMappingURL=permissions.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,6 +1,22 @@
|
|
|
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 during a directory switch. */
|
|
5
|
+
export function acquireLauncherLock(home) {
|
|
6
|
+
const db = new DatabaseSync(join(home, "launcher-lock.sqlite"));
|
|
7
|
+
try {
|
|
8
|
+
db.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
db.close();
|
|
12
|
+
throw new Error("Another launcher owns this installation");
|
|
13
|
+
}
|
|
14
|
+
let released = false;
|
|
15
|
+
return () => { if (!released) {
|
|
16
|
+
db.close();
|
|
17
|
+
released = true;
|
|
18
|
+
} };
|
|
19
|
+
}
|
|
4
20
|
/** A dedicated SQLite transaction owns an OS lock until close or process death.
|
|
5
21
|
* Local filesystems only. Never unlink this file: that would create a second lock identity.
|
|
6
22
|
*/
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { FileCommitError } from "../shared/errors.js";
|
|
2
|
+
import { existsSync, constants, openSync, closeSync, fstatSync, lstatSync, realpathSync, readFileSync, writeFileSync, fsyncSync, renameSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { dirname, basename, join, isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
export function checkDataDirectory(path) {
|
|
6
|
+
if (!isAbsolute(path) || resolve(path) !== path || /[\x00-\x1f\x7f]/.test(path))
|
|
7
|
+
throw new Error("Expected a canonical absolute dataDir");
|
|
8
|
+
let parent = path;
|
|
9
|
+
while (!existsSync(parent))
|
|
10
|
+
parent = dirname(parent);
|
|
11
|
+
if (realpathSync(parent) !== parent || !lstatSync(parent).isDirectory())
|
|
12
|
+
throw new Error("dataDir must not traverse symlinks");
|
|
13
|
+
if (parent === path) {
|
|
14
|
+
const stat = lstatSync(path);
|
|
15
|
+
if (stat.uid !== process.getuid() || (stat.mode & 0o077))
|
|
16
|
+
throw new Error("dataDir must be owned and private");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
4
19
|
function privateDirectory(path) {
|
|
5
20
|
const directory = dirname(path), stat = lstatSync(directory);
|
|
6
21
|
if (realpathSync(directory) !== directory || !stat.isDirectory() || stat.uid !== process.getuid() || (stat.mode & 0o077)) {
|
|
@@ -30,13 +45,6 @@ export function readPrivateText(path) {
|
|
|
30
45
|
closeSync(fd);
|
|
31
46
|
}
|
|
32
47
|
}
|
|
33
|
-
export class FileCommitError extends Error {
|
|
34
|
-
uncertain;
|
|
35
|
-
constructor(uncertain) {
|
|
36
|
-
super(uncertain ? "Configuration publication could not be confirmed; stop and inspect the files" : "Configuration was not published");
|
|
37
|
-
this.uncertain = uncertain;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
48
|
/** Caller owns the configuration. No await between comparison, validation and publication. */
|
|
41
49
|
export function replacePrivateText(path, text) {
|
|
42
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 {
|
|
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);
|
|
@@ -47,10 +48,12 @@ export async function terminateGroup(child) {
|
|
|
47
48
|
child.stdout?.destroy();
|
|
48
49
|
child.stderr?.destroy();
|
|
49
50
|
}
|
|
50
|
-
export async function openAcp(executor, signal, emit, inputFiles = []) {
|
|
51
|
+
export async function openAcp(executor, signal, emit, resultsRoot, inputFiles = []) {
|
|
51
52
|
signal.throwIfAborted();
|
|
52
|
-
const
|
|
53
|
-
|
|
53
|
+
const results = new Results(new LocalResultFiles(resultsRoot));
|
|
54
|
+
const launch = executorCommand(executor.type, process.env);
|
|
55
|
+
const child = spawn(launch.command, launch.args, {
|
|
56
|
+
cwd: executor.cwd, env: process.env, detached: true, stdio: ["pipe", "pipe", "pipe"],
|
|
54
57
|
});
|
|
55
58
|
const controller = new AbortController();
|
|
56
59
|
const cancel = AbortSignal.any([signal, controller.signal]);
|
|
@@ -102,7 +105,7 @@ export async function openAcp(executor, signal, emit, inputFiles = []) {
|
|
|
102
105
|
cancel.throwIfAborted();
|
|
103
106
|
if (ctx.params.sessionId !== sessionId)
|
|
104
107
|
throw new Error("Wrong input-file session");
|
|
105
|
-
return { content: readInputFile(
|
|
108
|
+
return { content: results.readInputFile(inputFiles, ctx.params.path, ctx.params.line ?? 1, ctx.params.limit ?? undefined) };
|
|
106
109
|
}
|
|
107
110
|
catch (error) {
|
|
108
111
|
controller.abort(error);
|
|
@@ -111,7 +114,7 @@ export async function openAcp(executor, signal, emit, inputFiles = []) {
|
|
|
111
114
|
})
|
|
112
115
|
.onRequest(acp.methods.client.session.requestPermission, async (ctx) => {
|
|
113
116
|
const request = ctx.params;
|
|
114
|
-
const response = permissionDecision(request,
|
|
117
|
+
const response = permissionDecision(request, cancel.aborted || request.sessionId !== sessionId);
|
|
115
118
|
await report("permission", { tool: request.toolCall.name ?? null, outcome: response.outcome });
|
|
116
119
|
return response;
|
|
117
120
|
})
|
|
@@ -165,4 +168,4 @@ export async function openAcp(executor, signal, emit, inputFiles = []) {
|
|
|
165
168
|
throw error;
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
|
-
//# sourceMappingURL=
|
|
171
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { executorEnv } from "./config.js";
|
|
4
3
|
/** One supervised worker and ACP session per node, retained across turns. */
|
|
5
|
-
export const
|
|
4
|
+
export const createExecution = (environment) => (input, event, beforeStart) => {
|
|
6
5
|
const worker = fork(fileURLToPath(new URL("./worker.js", import.meta.url)), [], {
|
|
7
|
-
env:
|
|
6
|
+
env: { ...environment }, stdio: ["ignore", "ignore", "pipe", "ipc"], execArgv: [],
|
|
8
7
|
});
|
|
9
8
|
worker.stderr?.resume();
|
|
10
9
|
let stopped = false, exited = false, busy = false;
|
|
@@ -100,4 +99,4 @@ export const openExecution = (input, event, beforeStart) => {
|
|
|
100
99
|
},
|
|
101
100
|
};
|
|
102
101
|
};
|
|
103
|
-
//# sourceMappingURL=
|
|
102
|
+
//# sourceMappingURL=execution.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
+
const commands = {
|
|
4
|
+
pi: { command: "pi-acp", args: [] },
|
|
5
|
+
codex: { command: "codex-acp", args: [] },
|
|
6
|
+
claude: { command: "claude-agent-acp", args: [] },
|
|
7
|
+
opencode: { command: "opencode", args: ["acp"] },
|
|
8
|
+
};
|
|
9
|
+
/** Fixed ACP adapters, resolved in the OS user's native PATH. Never a shell command. */
|
|
10
|
+
export function executorCommand(type, environment) {
|
|
11
|
+
const find = (name) => {
|
|
12
|
+
for (const directory of (environment.PATH ?? "").split(delimiter).filter(isAbsolute)) {
|
|
13
|
+
try {
|
|
14
|
+
const path = realpathSync(join(directory, name));
|
|
15
|
+
accessSync(path, constants.X_OK);
|
|
16
|
+
if (statSync(path).isFile())
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
catch { /* Try the next native PATH entry. */ }
|
|
20
|
+
}
|
|
21
|
+
throw new Error("Missing ACP dependency on native PATH: " + name);
|
|
22
|
+
};
|
|
23
|
+
const entry = commands[type];
|
|
24
|
+
if (type === "pi")
|
|
25
|
+
find("pi");
|
|
26
|
+
return { command: find(entry.command), args: [...entry.args] };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { openAcp } from "./
|
|
1
|
+
import { openAcp } from "./connection.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { errorText
|
|
4
|
-
|
|
3
|
+
import { errorText } from "../../shared/errors.js";
|
|
4
|
+
import { executorSchema } from "../../executors/validation.js";
|
|
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();
|
|
7
8
|
let sequence = 0, started = false, busy = false;
|
|
@@ -64,7 +65,7 @@ process.on("message", (message) => {
|
|
|
64
65
|
started = true;
|
|
65
66
|
opening = (async () => {
|
|
66
67
|
const input = inputSchema.parse(message.input);
|
|
67
|
-
session = await openAcp(input.executor, controller.signal, emit, input.inputFiles);
|
|
68
|
+
session = await openAcp(input.executor, controller.signal, emit, input.resultsRoot, input.inputFiles);
|
|
68
69
|
void session.closed.then(() => { if (!closing)
|
|
69
70
|
void fail(new Error("ACP process exited")); });
|
|
70
71
|
if (!controller.signal.aborted)
|