@realqiyan/agent-task 0.0.1-rc.2 → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +17 -0
- package/README.md +30 -7
- package/bin/agent-task.mjs +35 -36
- package/config.example.json +1 -1
- package/dist/{launcher.js → application/launcher.js} +49 -58
- package/dist/application/main.js +125 -0
- package/dist/application/mcp-entry.js +71 -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 +42 -0
- package/dist/configuration/management.js +6 -0
- package/dist/configuration/model.js +2 -0
- package/dist/configuration/policy.js +24 -0
- package/dist/configuration/ports.js +2 -0
- package/dist/{settings.js → configuration/settings.js} +54 -39
- package/dist/executors/environment.js +10 -0
- package/dist/executors/model.js +2 -0
- package/dist/executors/ports.js +2 -0
- package/dist/executors/registry.js +131 -0
- package/dist/executors/validation.js +9 -0
- package/dist/infrastructure/lock.js +55 -0
- 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} +13 -11
- package/dist/integrations/acp/discovery.js +271 -0
- package/dist/{node-execution.js → integrations/acp/execution.js} +1 -1
- package/dist/integrations/acp/runtime.js +43 -0
- package/dist/{worker.js → integrations/acp/worker.js} +7 -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 +58 -0
- package/dist/protocol/cli/admin-client.js +45 -0
- package/dist/protocol/cli/commands.js +389 -0
- package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
- package/dist/protocol/http/server.js +119 -0
- package/dist/{web.js → protocol/http/web.js} +24 -17
- package/dist/{mcp.js → protocol/mcp.js} +13 -13
- package/dist/protocol/services.js +2 -0
- package/dist/protocol/settings-contract.js +37 -0
- package/dist/protocol/stdio.js +71 -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 +86 -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} +28 -40
- package/dist/tasks/supervisor.js +227 -0
- package/dist/tasks/validation.js +24 -0
- package/dist/web/settings-view.js +46 -33
- 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 +8 -6
- package/docs/cli.md +57 -0
- package/docs/deployment.md +46 -70
- package/docs/design-proposal.md +5 -2
- package/docs/release.md +14 -15
- package/docs/technical-design.md +49 -26
- package/docs/validation.md +92 -18
- package/docs/web-console.md +16 -11
- package/package.json +9 -7
- package/scripts/install-lib.mjs +7 -116
- package/scripts/mcp-call.mjs +1 -1
- package/scripts/probe-agent.mjs +14 -12
- package/scripts/service-runner.mjs +9 -4
- package/scripts/setup-wizard.mjs +38 -16
- package/scripts/setup.mjs +69 -108
- package/deploy/agent-task.service.example +0 -17
- package/dist/admin.js +0 -31
- package/dist/config.js +0 -72
- package/dist/executor-runtime.js +0 -37
- package/dist/lock.js +0 -43
- 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
package/scripts/setup.mjs
CHANGED
|
@@ -1,130 +1,91 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, realpathSync, lstatSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { join, isAbsolute } from "node:path";
|
|
5
4
|
import { homedir } from "node:os";
|
|
6
5
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
import { argumentsFor, installRoot,
|
|
8
|
-
runtimeBaseEnv, activate, inventory, verifyServiceFile, packageRoot, checkHealth } from "./install-lib.mjs";
|
|
6
|
+
import { argumentsFor, installRoot, createFile, json, operationLock, readManifest, saveManifest, packageRoot } from "./install-lib.mjs";
|
|
9
7
|
import { setupWizard } from "./setup-wizard.mjs";
|
|
10
|
-
import { startupPaths } from "../dist/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
8
|
+
import { startupPaths, loadConfig, readEnvironment } from "../dist/configuration/files.js";
|
|
9
|
+
import { configSchema } from "../dist/configuration/contract.js";
|
|
10
|
+
import { validateConfig, coordinatorCredentialEnv } from "../dist/configuration/policy.js";
|
|
11
|
+
import { validateServiceEnvironment, parseEnvironment } from "../dist/configuration/environment.js";
|
|
12
|
+
import { checkDataDirectory } from "../dist/infrastructure/private-files.js";
|
|
14
13
|
|
|
15
14
|
process.umask(0o077);
|
|
16
15
|
let unlock;
|
|
17
16
|
try {
|
|
18
17
|
const options = argumentsFor(process.argv.slice(2));
|
|
19
18
|
if (options.help) {
|
|
20
|
-
console.log("Usage: agent-task setup [--home ABSOLUTE_PRIVATE_DIR]\
|
|
19
|
+
console.log("Usage: agent-task setup [--home ABSOLUTE_PRIVATE_DIR]\nNew installation: confirm directories, port, tokens and coordinator in the terminal.\nNoninteractive: --home DIR --data-dir DIR --port NUMBER --generate-tokens|--credentials-file PRIVATE_JSON --yes [--provider openai|anthropic|deepseek|openai-compatible --model ID]\nCompatible provider: --base-url URL --api openai-completions|openai-responses --api-key-env NAME --context-window NUMBER --max-tokens NUMBER\nExisting installation: --check | --resume. Secrets are read from a private JSON file or hidden terminal input, never command arguments. No executor installation during setup.");
|
|
21
20
|
process.exit(0);
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
unlock = operationLock(root);
|
|
30
|
-
let manifest;
|
|
31
|
-
if (existsSync(manifestPath)) {
|
|
22
|
+
let root = installRoot(options);
|
|
23
|
+
if (!existsSync(join(root, "installation.json")) && !options.check && !options.resume && !options.yes) {
|
|
24
|
+
await setupWizard(options, root); root = installRoot(options);
|
|
25
|
+
}
|
|
26
|
+
let manifest, initialEnvironment;
|
|
27
|
+
if (existsSync(join(root, "installation.json"))) {
|
|
32
28
|
manifest = readManifest(root);
|
|
33
|
-
if (
|
|
34
|
-
if (manifest.phase !== "ready" && !options.resume) throw new Error("Incomplete
|
|
29
|
+
if (Object.keys(options).some(key => !["home", "check", "resume"].includes(key))) throw new Error("Installation exists; use configuration commands instead of initialization flags");
|
|
30
|
+
if (manifest.phase !== "ready" && !options.resume) throw new Error("Incomplete initialization; inspect private files and use --resume explicitly");
|
|
35
31
|
} else {
|
|
36
|
-
if (options.check || options.
|
|
37
|
-
if (!options.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
phase: "installing", serviceInstalled: false, files: [] };
|
|
53
|
-
createFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
54
|
-
}
|
|
55
|
-
verifyServiceFile(manifest);
|
|
56
|
-
const app = manifest.app, components = join(root, "components");
|
|
57
|
-
if (!existsSync(join(app, "dist/main.js"))) throw new Error("Prebuilt runtime is missing; reinstall the npm package (source contributors: npm run build)");
|
|
58
|
-
if (manifest.phase !== "ready") {
|
|
59
|
-
if (manifest.serviceInstalled) throw new Error("Stop/uninstall the existing service before rebuilding components");
|
|
60
|
-
for (const name of ["components"]) {
|
|
61
|
-
if (!existsSync(join(root, name))) continue;
|
|
62
|
-
const known = new Set(manifest.files);
|
|
63
|
-
if (inventory(root, name).some(file => !known.has(file))) throw new Error("Uninventoried component files exist; refusing to overwrite them during resume");
|
|
32
|
+
if (options.check || options.resume) throw new Error("No installation to check/resume");
|
|
33
|
+
if (!options.yes || !options.home || !options["data-dir"] || options.port === undefined ||
|
|
34
|
+
(!options["generate-tokens"] && !options["credentials-file"])) throw new Error("Confirm initialization with --home, --data-dir, --port, --generate-tokens/--credentials-file and --yes");
|
|
35
|
+
const dataDir = installRoot({ home: options["data-dir"] });
|
|
36
|
+
if (dataDir === root || dataDir === packageRoot || dataDir.startsWith(packageRoot + "/")) throw new Error("Data directory must be separate from the installation root and npm package");
|
|
37
|
+
const coordinator = !options.provider || options.provider === "none" ? undefined : {
|
|
38
|
+
provider: options.provider, model: options.model,
|
|
39
|
+
...(options.provider === "openai-compatible" ? { baseUrl: options["base-url"], api: options.api,
|
|
40
|
+
apiKeyEnv: options["api-key-env"], contextWindow: Number(options["context-window"]), maxTokens: Number(options["max-tokens"]), reasoning: false } : {}),
|
|
41
|
+
};
|
|
42
|
+
const config = validateConfig(configSchema.parse({ dataDir, port: Number(options.port), tokenEnv: options["token-env"] ?? "AGENT_TASK_TOKEN", coordinator, disabledExecutors: [] }));
|
|
43
|
+
initialEnvironment = options["credentials-file"] ? parseEnvironment(JSON.stringify(json(options["credentials-file"]))) : {};
|
|
44
|
+
if (options["generate-tokens"]) {
|
|
45
|
+
if (initialEnvironment[config.tokenEnv] || initialEnvironment.AGENT_TASK_ADMIN_TOKEN) throw new Error("Choose either supplied service/admin tokens or generated tokens");
|
|
46
|
+
initialEnvironment[config.tokenEnv] = randomBytes(32).toString("hex");
|
|
47
|
+
initialEnvironment.AGENT_TASK_ADMIN_TOKEN = randomBytes(32).toString("hex");
|
|
64
48
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
console.log("Installing selected ACP executors into private components (not global executables).");
|
|
72
|
-
const installArgs = existsSync(join(components, "package-lock.json"))
|
|
73
|
-
? ["ci", "--ignore-scripts", "--no-audit", "--no-fund"]
|
|
74
|
-
: ["install", "--save-exact", "--ignore-scripts", "--no-audit", "--no-fund", ...dependencies];
|
|
75
|
-
run("npm", installArgs, { cwd: components });
|
|
76
|
-
if (manifest.selected.includes("opencode")) {
|
|
77
|
-
// This pinned package requires copying its platform binary. No other lifecycle scripts run.
|
|
78
|
-
run(manifest.node, [join(components, "node_modules/opencode-ai/postinstall.mjs")], { cwd: components });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
mkdirSync(join(root, "data/results"), { recursive: true, mode: 0o700 });
|
|
82
|
-
const dataDir = join(root, "data");
|
|
83
|
-
if (!existsSync(join(root, "launch.json"))) createFile(join(root, "launch.json"), JSON.stringify({ dataDir }) + "\n");
|
|
84
|
-
if (!existsSync(join(dataDir, "environment.json"))) createFile(join(dataDir, "environment.json"), JSON.stringify({ AGENT_TASK_TOKEN: randomBytes(32).toString("hex"), AGENT_TASK_ADMIN_TOKEN: randomBytes(32).toString("hex") }, null, 2) + "\n");
|
|
85
|
-
if (!existsSync(join(dataDir, "config.json"))) createFile(join(dataDir, "config.json"), JSON.stringify({ dataDir, port: manifest.port,
|
|
86
|
-
tokenEnv: "AGENT_TASK_TOKEN", coordinator: manifest.coordinator,
|
|
87
|
-
executors: manifest.selected.map(id => ({ id, type: id, note: "", cwd: manifest.cwd, unattendedApproved: false })) }, null, 2) + "\n");
|
|
88
|
-
manifest.phase = "ready";
|
|
89
|
-
delete manifest.port; delete manifest.cwd; delete manifest.coordinator;
|
|
90
|
-
} finally {
|
|
91
|
-
manifest.files = ["components"].filter(name => existsSync(join(root, name))).flatMap(name => inventory(root, name));
|
|
92
|
-
saveManifest(root, manifest);
|
|
49
|
+
if (coordinator && options.coordinatorKey) initialEnvironment[coordinatorCredentialEnv(config.coordinator)] = options.coordinatorKey;
|
|
50
|
+
validateServiceEnvironment(config, initialEnvironment);
|
|
51
|
+
if (!initialEnvironment.AGENT_TASK_ADMIN_TOKEN) throw new Error("An independent administrator token is required for initialization");
|
|
52
|
+
if (coordinator && !initialEnvironment[coordinatorCredentialEnv(config.coordinator)]) throw new Error("Coordinator credential is missing; supply it through the private credentials file");
|
|
53
|
+
for (const path of [join(root, "launch.json"), join(dataDir, "config.json"), join(dataDir, "environment.json")]) {
|
|
54
|
+
if (existsSync(path)) throw new Error("Existing unowned initialization file; refusing to overwrite: " + path);
|
|
93
55
|
}
|
|
56
|
+
checkDataDirectory(root); checkDataDirectory(dataDir);
|
|
57
|
+
manifest = { project: "agent-task", root, home: homedir(), platform: process.platform, node: process.execPath,
|
|
58
|
+
app: packageRoot, phase: "initializing", dataDir, config };
|
|
94
59
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (const entry of report.executors) {
|
|
109
|
-
try { run(manifest.node, [join(app, "scripts/probe-agent.mjs"), paths.configPath, entry.id], { env, timeout: 20000 }); entry.handshake = "passed (initialize only)"; }
|
|
110
|
-
catch { entry.handshake = "failed; inspect native dependencies/configuration separately"; process.exitCode = 1; }
|
|
60
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
61
|
+
if (realpathSync(root) !== root || lstatSync(root).uid !== process.getuid() || (lstatSync(root).mode & 0o077)) throw new Error("Installation home must be canonical, owned and private");
|
|
62
|
+
unlock = operationLock(root);
|
|
63
|
+
if (!existsSync(join(root, "installation.json"))) createFile(join(root, "installation.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
64
|
+
if (!existsSync(join(packageRoot, "dist/application/main.js"))) throw new Error("Prebuilt runtime missing; reinstall the package");
|
|
65
|
+
if (manifest.phase !== "ready") {
|
|
66
|
+
if (!isAbsolute(manifest.dataDir) || !manifest.config) throw new Error("Incomplete installation record; no repair performed");
|
|
67
|
+
const dataDir = manifest.dataDir;
|
|
68
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 }); checkDataDirectory(dataDir);
|
|
69
|
+
const environmentPath = join(dataDir, "environment.json"), configPath = join(dataDir, "config.json");
|
|
70
|
+
if (!existsSync(environmentPath)) {
|
|
71
|
+
if (!initialEnvironment) throw new Error("Initialization has no saved credentials; inspect the private directory before resuming");
|
|
72
|
+
createFile(environmentPath, JSON.stringify(initialEnvironment, null, 2) + "\n");
|
|
111
73
|
}
|
|
74
|
+
if (!existsSync(configPath)) createFile(configPath, JSON.stringify(manifest.config, null, 2) + "\n");
|
|
75
|
+
const config = loadConfig(configPath), environment = readEnvironment(environmentPath);
|
|
76
|
+
if (JSON.stringify(config) !== JSON.stringify(manifest.config)) throw new Error("Saved initialization configuration differs; no repair performed");
|
|
77
|
+
validateServiceEnvironment(config, environment);
|
|
78
|
+
if (!existsSync(join(root, "launch.json"))) createFile(join(root, "launch.json"), JSON.stringify({ dataDir }) + "\n");
|
|
79
|
+
if (startupPaths(root).dataDir !== dataDir) throw new Error("Launch locator differs from initialization");
|
|
80
|
+
manifest.phase = "ready"; delete manifest.config; delete manifest.dataDir;
|
|
81
|
+
saveManifest(root, manifest);
|
|
112
82
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
report.background = "service manager registered";
|
|
119
|
-
}
|
|
120
|
-
if (manifest.serviceInstalled) {
|
|
121
|
-
try {
|
|
122
|
-
await checkHealth(root, 15000);
|
|
123
|
-
report.background = "authenticated MCP health check passed";
|
|
124
|
-
} catch { report.background = "not ready; run --check after startup and inspect the service manager"; process.exitCode = 1; }
|
|
125
|
-
}
|
|
126
|
-
console.log(JSON.stringify(report, null, 2));
|
|
127
|
-
console.log(`Config: ${paths.configPath}\nSecrets (never printed): ${paths.environmentPath}\nInstalled ACP adapters: ${join(components, "node_modules/.bin")}. Prepare authentication with each executor\'s official native tools under the service OS user. No login is performed by setup.\nConsole: http://127.0.0.1:${config.port}/admin/ (use the separate AGENT_TASK_ADMIN_TOKEN from the private environment file; never place it in a URL). Configure coordinator credentials there or in environment.json, and audit native executor permissions before setting unattendedApproved=true. No permissions are granted by setup. Use Web restart or agent-task restart after changes.\nLinux: user service requires lingering for logout survival (administrator decision). macOS: LaunchAgent survives terminal close, not user logout; log in again to restart. Neither platform executes during sleep/shutdown.`);
|
|
83
|
+
const paths = startupPaths(root), config = loadConfig(paths.configPath);
|
|
84
|
+
validateServiceEnvironment(config, readEnvironment(paths.environmentPath));
|
|
85
|
+
console.log(JSON.stringify({ configuration: "valid", coordinator: config.coordinator ? "configured; authentication not verified" : "not configured",
|
|
86
|
+
configPath: paths.configPath, credentialsPath: paths.environmentPath, executorDiscovery: "on MCP connection" }, null, 2));
|
|
87
|
+
if (!options.check) console.log("Add agent-task with args [mcp] to your MCP client (include --home for a custom installation). Token values are not printed. Use agent-task start for foreground management.");
|
|
128
88
|
} catch (error) {
|
|
129
|
-
console.error(error.
|
|
89
|
+
console.error(error.name === "ZodError" ? "Invalid initialization configuration; check provider, model, directories and port" : error.message);
|
|
90
|
+
process.exitCode = 1;
|
|
130
91
|
} finally { unlock?.(); }
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
[Unit]
|
|
2
|
-
Description=Agent Task local service
|
|
3
|
-
StartLimitIntervalSec=60
|
|
4
|
-
StartLimitBurst=3
|
|
5
|
-
|
|
6
|
-
[Service]
|
|
7
|
-
Type=simple
|
|
8
|
-
WorkingDirectory=/absolute/path/to/agent-task
|
|
9
|
-
ExecStart=/absolute/path/to/node /absolute/path/to/agent-task/scripts/service-runner.mjs %h/.local/share/agent-task
|
|
10
|
-
Restart=on-failure
|
|
11
|
-
RestartSec=5
|
|
12
|
-
TimeoutStopSec=45
|
|
13
|
-
KillMode=control-group
|
|
14
|
-
UMask=0077
|
|
15
|
-
|
|
16
|
-
[Install]
|
|
17
|
-
WantedBy=default.target
|
package/dist/admin.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { loadConfig, validateConfig } from "./config.js";
|
|
4
|
-
import { replacePrivateText } from "./private-files.js";
|
|
5
|
-
import { executorSchema } from "./model.js";
|
|
6
|
-
import { acquireLock } from "./lock.js";
|
|
7
|
-
process.umask(0o077);
|
|
8
|
-
const [action, configPath, profilePath] = process.argv.slice(2);
|
|
9
|
-
if (!configPath || !["register", "list"].includes(action ?? "") || (action === "register" && !profilePath)) {
|
|
10
|
-
throw new Error("Usage: node dist/admin.js list CONFIG | register CONFIG PROFILE.json (service stopped)");
|
|
11
|
-
}
|
|
12
|
-
const path = resolve(configPath), config = loadConfig(path);
|
|
13
|
-
const release = await acquireLock(dirname(path));
|
|
14
|
-
try {
|
|
15
|
-
if (action === "list") {
|
|
16
|
-
console.log(JSON.stringify(config.executors.map(executor => ({ ...executor, transport: "stdio" })), null, 2));
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
const profile = executorSchema.parse(JSON.parse(readFileSync(resolve(profilePath), "utf8")));
|
|
20
|
-
if (config.executors.some(item => item.id === profile.id))
|
|
21
|
-
throw new Error("Executor ID already registered; edit reviewed config while stopped to replace");
|
|
22
|
-
config.executors.push(profile);
|
|
23
|
-
// Validate the complete candidate before publication, using the same rules as startup and the console.
|
|
24
|
-
replacePrivateText(path, JSON.stringify(validateConfig(config), null, 2) + "\n");
|
|
25
|
-
console.log(JSON.stringify({ registered: profile.id, transport: "stdio" }));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
finally {
|
|
29
|
-
await release();
|
|
30
|
-
}
|
|
31
|
-
//# sourceMappingURL=admin.js.map
|
package/dist/config.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { executorSchema } from "./model.js";
|
|
5
|
-
import { readPrivateText } from "./private-files.js";
|
|
6
|
-
export const ADMIN_TOKEN_ENV = "AGENT_TASK_ADMIN_TOKEN";
|
|
7
|
-
const envKey = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
8
|
-
const baseEnvKeys = ["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "NODE_OPTIONS", "NODE_PATH", "__proto__", "constructor", "prototype"];
|
|
9
|
-
const modelId = z.string().min(1).max(200);
|
|
10
|
-
const coordinatorSchema = z.discriminatedUnion("provider", [
|
|
11
|
-
z.object({ provider: z.enum(["openai", "anthropic", "deepseek"]), model: modelId }).strict(),
|
|
12
|
-
z.object({
|
|
13
|
-
provider: z.literal("openai-compatible"), model: modelId,
|
|
14
|
-
api: z.enum(["openai-completions", "openai-responses"]).default("openai-completions"),
|
|
15
|
-
baseUrl: z.string().url().refine(value => {
|
|
16
|
-
const url = new URL(value);
|
|
17
|
-
return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
|
|
18
|
-
}, "baseUrl must be HTTP(S) without credentials, query or fragment"),
|
|
19
|
-
apiKeyEnv: envKey.refine(key => !baseEnvKeys.includes(key), "Credential must not use a base environment key"),
|
|
20
|
-
contextWindow: z.number().int().positive(),
|
|
21
|
-
maxTokens: z.number().int().positive(),
|
|
22
|
-
reasoning: z.boolean().default(false),
|
|
23
|
-
}).strict(),
|
|
24
|
-
]).refine(value => value.provider !== "openai-compatible" || value.maxTokens <= value.contextWindow, "maxTokens must not exceed contextWindow");
|
|
25
|
-
export const configSchema = z.object({
|
|
26
|
-
dataDir: z.string().min(1),
|
|
27
|
-
port: z.number().int().min(0).max(65535).default(8765),
|
|
28
|
-
tokenEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).default("AGENT_TASK_TOKEN"),
|
|
29
|
-
coordinator: coordinatorSchema.optional(),
|
|
30
|
-
executors: z.array(executorSchema).max(100).default([]),
|
|
31
|
-
}).strict();
|
|
32
|
-
export function coordinatorCredentialEnv(config) {
|
|
33
|
-
switch (config.provider) {
|
|
34
|
-
case "openai": return "OPENAI_API_KEY";
|
|
35
|
-
case "anthropic": return "ANTHROPIC_API_KEY";
|
|
36
|
-
case "deepseek": return "DEEPSEEK_API_KEY";
|
|
37
|
-
case "openai-compatible": return config.apiKeyEnv;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
export function loadConfig(path) {
|
|
41
|
-
let value;
|
|
42
|
-
try {
|
|
43
|
-
value = JSON.parse(readPrivateText(path));
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
throw new Error("Cannot read configuration JSON");
|
|
47
|
-
}
|
|
48
|
-
return validateConfig(value);
|
|
49
|
-
}
|
|
50
|
-
export function validateConfig(value) {
|
|
51
|
-
const config = configSchema.parse(value);
|
|
52
|
-
if (!isAbsolute(config.dataDir) || /[\x00-\x1f\x7f]/.test(config.dataDir))
|
|
53
|
-
throw new Error("dataDir must be absolute without control characters");
|
|
54
|
-
const coordinatorKey = config.coordinator ? coordinatorCredentialEnv(config.coordinator) : undefined;
|
|
55
|
-
if (/^(NODE_|LD_|DYLD_)/.test(config.tokenEnv) || [...baseEnvKeys, ADMIN_TOKEN_ENV, "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY", coordinatorKey].includes(config.tokenEnv))
|
|
56
|
-
throw new Error("tokenEnv must be separate from base environment, administrator and model credentials");
|
|
57
|
-
if (coordinatorKey === ADMIN_TOKEN_ENV)
|
|
58
|
-
throw new Error("Administrator token must not be used as a model credential");
|
|
59
|
-
const ids = new Set();
|
|
60
|
-
for (const executor of config.executors) {
|
|
61
|
-
if (ids.has(executor.id))
|
|
62
|
-
throw new Error("Duplicate executor ID");
|
|
63
|
-
ids.add(executor.id);
|
|
64
|
-
if (!isAbsolute(executor.cwd))
|
|
65
|
-
throw new Error("Executor cwd must be absolute");
|
|
66
|
-
executor.cwd = realpathSync(executor.cwd);
|
|
67
|
-
if (!statSync(executor.cwd).isDirectory())
|
|
68
|
-
throw new Error("Invalid executor cwd");
|
|
69
|
-
}
|
|
70
|
-
return config;
|
|
71
|
-
}
|
|
72
|
-
//# sourceMappingURL=config.js.map
|
package/dist/executor-runtime.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
-
import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv } from "./config.js";
|
|
4
|
-
const commands = {
|
|
5
|
-
pi: { command: "pi-acp", args: [] },
|
|
6
|
-
codex: { command: "codex-acp", args: [] },
|
|
7
|
-
claude: { command: "claude-agent-acp", args: [] },
|
|
8
|
-
opencode: { command: "opencode", args: ["acp"] },
|
|
9
|
-
};
|
|
10
|
-
/** Fixed ACP adapters, resolved in the OS user's native PATH. Never a shell command. */
|
|
11
|
-
export function executorCommand(type, environment) {
|
|
12
|
-
const find = (name) => {
|
|
13
|
-
for (const directory of (environment.PATH ?? "").split(delimiter).filter(isAbsolute)) {
|
|
14
|
-
try {
|
|
15
|
-
const path = realpathSync(join(directory, name));
|
|
16
|
-
accessSync(path, constants.X_OK);
|
|
17
|
-
if (statSync(path).isFile())
|
|
18
|
-
return path;
|
|
19
|
-
}
|
|
20
|
-
catch { /* Try the next native PATH entry. */ }
|
|
21
|
-
}
|
|
22
|
-
throw new Error("Missing ACP dependency on native PATH: " + name);
|
|
23
|
-
};
|
|
24
|
-
const entry = commands[type];
|
|
25
|
-
if (type === "pi")
|
|
26
|
-
find("pi");
|
|
27
|
-
return { command: find(entry.command), args: [...entry.args] };
|
|
28
|
-
}
|
|
29
|
-
/** Private service inputs never enter workers, even through an alias with the same value. */
|
|
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
|
package/dist/lock.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
/** The stable installation lock prevents two launchers or uninstall 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
|
-
}
|
|
20
|
-
/** A dedicated SQLite transaction owns an OS lock until close or process death.
|
|
21
|
-
* Local filesystems only. Never unlink this file: that would create a second lock identity.
|
|
22
|
-
*/
|
|
23
|
-
export async function acquireLock(directory) {
|
|
24
|
-
if (!["linux", "darwin"].includes(process.platform))
|
|
25
|
-
throw new Error("Supported platforms: Linux and macOS");
|
|
26
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
27
|
-
const db = new DatabaseSync(join(directory, "service-lock.sqlite"));
|
|
28
|
-
try {
|
|
29
|
-
db.exec("PRAGMA busy_timeout=0; PRAGMA journal_mode=DELETE; BEGIN EXCLUSIVE");
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
db.close();
|
|
33
|
-
throw new Error("State directory is locked or its filesystem does not support SQLite locking", { cause: error });
|
|
34
|
-
}
|
|
35
|
-
let released = false;
|
|
36
|
-
return async () => {
|
|
37
|
-
if (released)
|
|
38
|
-
return;
|
|
39
|
-
db.close();
|
|
40
|
-
released = true;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
//# sourceMappingURL=lock.js.map
|
package/dist/main.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
-
import { loadConfig } from "./config.js";
|
|
6
|
-
import { baseEnvironment, readEnvironment, validateServiceEnvironment } from "./environment.js";
|
|
7
|
-
import { bearerVerifier } from "./http-auth.js";
|
|
8
|
-
import { Settings } from "./settings.js";
|
|
9
|
-
import { createWebRouter } from "./web.js";
|
|
10
|
-
import { acquireLock } from "./lock.js";
|
|
11
|
-
import { createMcpServer } from "./mcp.js";
|
|
12
|
-
import { errorText } from "./model.js";
|
|
13
|
-
import { TaskStore } from "./task-storage.js";
|
|
14
|
-
import { Tasks } from "./tasks.js";
|
|
15
|
-
import { Results } from "./results.js";
|
|
16
|
-
import { createExecution } from "./node-execution.js";
|
|
17
|
-
import { executorEnvironment } from "./executor-runtime.js";
|
|
18
|
-
import { createCoordinator } from "./coordinator.js";
|
|
19
|
-
process.umask(0o077);
|
|
20
|
-
const args = process.argv.slice(2);
|
|
21
|
-
if (args[0] !== "--config" || args.length !== 4 || args[2] !== "--environment") {
|
|
22
|
-
console.error("Internal runtime requires --config PATH --environment PATH; use agent-task start");
|
|
23
|
-
process.exit(2);
|
|
24
|
-
}
|
|
25
|
-
const configPath = resolve(args[1]), environmentPath = resolve(args[3]);
|
|
26
|
-
const native = { ...process.env }, base = baseEnvironment(native);
|
|
27
|
-
const environment = readEnvironment(environmentPath);
|
|
28
|
-
const instanceId = process.env.AGENT_TASK_INSTANCE_ID ?? randomUUID();
|
|
29
|
-
for (const key of Object.keys(process.env))
|
|
30
|
-
delete process.env[key];
|
|
31
|
-
Object.assign(process.env, base, environment);
|
|
32
|
-
const config = loadConfig(configPath);
|
|
33
|
-
const { token, adminToken } = validateServiceEnvironment(config, environment);
|
|
34
|
-
const authorized = bearerVerifier(token);
|
|
35
|
-
let http;
|
|
36
|
-
let tasks, taskStore, settings;
|
|
37
|
-
let release;
|
|
38
|
-
let shutting;
|
|
39
|
-
const connections = new Set();
|
|
40
|
-
function shutdown(code, drain = false) {
|
|
41
|
-
process.exitCode = Math.max(process.exitCode ? Number(process.exitCode) : 0, code);
|
|
42
|
-
return shutting ??= (async () => {
|
|
43
|
-
settings?.close();
|
|
44
|
-
try {
|
|
45
|
-
await tasks?.close();
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
console.error(errorText(error));
|
|
49
|
-
process.exitCode = 1;
|
|
50
|
-
}
|
|
51
|
-
for (const close of connections)
|
|
52
|
-
await close().catch(() => { });
|
|
53
|
-
if (http) {
|
|
54
|
-
if (drain)
|
|
55
|
-
http.close(); // Release the listener, but retain the in-flight Web restart response.
|
|
56
|
-
else {
|
|
57
|
-
http.closeAllConnections();
|
|
58
|
-
await new Promise(done => http.close(() => done()));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
taskStore?.close();
|
|
62
|
-
await release?.();
|
|
63
|
-
})().catch(error => { console.error(errorText(error)); process.exitCode = 1; });
|
|
64
|
-
}
|
|
65
|
-
const fatal = (error) => { console.error(errorText(error)); void shutdown(1).finally(() => { if (process.connected)
|
|
66
|
-
process.disconnect(); }); };
|
|
67
|
-
let restartPending = false;
|
|
68
|
-
const restart = async () => {
|
|
69
|
-
if (!process.connected || restartPending || shutting || settings?.status().writeBlocked)
|
|
70
|
-
throw new Error("Restart unavailable");
|
|
71
|
-
restartPending = true;
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
process.once("message", function receive(value) {
|
|
74
|
-
if (value.type !== "restarted") {
|
|
75
|
-
process.once("message", receive);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// The HTTP adapter finishes the authenticated response before this child exits.
|
|
79
|
-
setTimeout(() => { if (process.connected)
|
|
80
|
-
process.disconnect(); http?.closeAllConnections(); }, 200);
|
|
81
|
-
if (value.error)
|
|
82
|
-
reject(new Error(value.error));
|
|
83
|
-
else
|
|
84
|
-
resolve(value.ready);
|
|
85
|
-
});
|
|
86
|
-
process.send({ type: "restart" });
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
process.on("message", (value) => {
|
|
90
|
-
if (value.type === "drain")
|
|
91
|
-
void shutdown(0, true).then(() => {
|
|
92
|
-
if (process.exitCode) {
|
|
93
|
-
if (process.connected)
|
|
94
|
-
process.disconnect();
|
|
95
|
-
}
|
|
96
|
-
else
|
|
97
|
-
process.send?.({ type: "drained" });
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
const stop = () => { void shutdown(0).finally(() => { http?.closeAllConnections(); if (process.connected)
|
|
101
|
-
process.disconnect(); }); };
|
|
102
|
-
process.once("SIGINT", stop);
|
|
103
|
-
process.once("SIGTERM", stop);
|
|
104
|
-
process.once("disconnect", stop);
|
|
105
|
-
try {
|
|
106
|
-
release = await acquireLock(config.dataDir);
|
|
107
|
-
taskStore = new TaskStore(config.dataDir);
|
|
108
|
-
tasks = new Tasks(taskStore, config.executors, new Results(config.dataDir), createExecution(executorEnvironment(native, environment, config)), config.coordinator ? createCoordinator(config.dataDir, config.coordinator) : undefined, fatal);
|
|
109
|
-
const app = express();
|
|
110
|
-
app.disable("x-powered-by");
|
|
111
|
-
app.disable("etag");
|
|
112
|
-
app.use((_req, res, next) => {
|
|
113
|
-
if (shutting) {
|
|
114
|
-
res.status(503).end();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
next();
|
|
118
|
-
});
|
|
119
|
-
if (adminToken) {
|
|
120
|
-
settings = new Settings(configPath, environmentPath, config, base, process.env);
|
|
121
|
-
app.use("/admin", createWebRouter(tasks, settings, adminToken, fatal, restart, instanceId));
|
|
122
|
-
}
|
|
123
|
-
app.use("/mcp", (req, res, next) => {
|
|
124
|
-
const suffix = req.socket.localPort === 80 ? "" : ":" + req.socket.localPort;
|
|
125
|
-
if (req.headers.origin !== undefined || !["127.0.0.1" + suffix, "localhost" + suffix].includes(req.headers.host ?? "")) {
|
|
126
|
-
res.status(403).end();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (!authorized(req.headers.authorization)) {
|
|
130
|
-
res.status(401).set("WWW-Authenticate", "Bearer").end();
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
next();
|
|
134
|
-
}, express.json({ limit: "512kb" }));
|
|
135
|
-
app.post("/mcp", async (req, res) => {
|
|
136
|
-
const server = createMcpServer(tasks, fatal);
|
|
137
|
-
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
|
138
|
-
const close = async () => { connections.delete(close); await transport.close(); await server.close(); };
|
|
139
|
-
connections.add(close);
|
|
140
|
-
res.once("close", () => { void close().catch(() => { }); });
|
|
141
|
-
try {
|
|
142
|
-
await server.connect(transport);
|
|
143
|
-
await transport.handleRequest(req, res, req.body);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
if (!res.headersSent)
|
|
147
|
-
res.status(500).json({ error: "MCP request failed" });
|
|
148
|
-
await close();
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
app.all("/mcp", (_req, res) => { res.status(405).set("Allow", "POST").end(); });
|
|
152
|
-
app.use((_req, res) => { res.status(404).end(); });
|
|
153
|
-
app.use(((error, _req, res, _next) => {
|
|
154
|
-
res.status(error.status === 413 ? 413 : 400).json({ error: "Invalid request body" });
|
|
155
|
-
}));
|
|
156
|
-
http = await new Promise((done, reject) => {
|
|
157
|
-
const listening = app.listen(config.port, "127.0.0.1", () => done(listening));
|
|
158
|
-
listening.once("error", reject);
|
|
159
|
-
});
|
|
160
|
-
if (shutting) {
|
|
161
|
-
http.closeAllConnections();
|
|
162
|
-
http.close();
|
|
163
|
-
throw new Error("Startup stopped");
|
|
164
|
-
}
|
|
165
|
-
tasks.start();
|
|
166
|
-
const address = http.address();
|
|
167
|
-
const origin = "http://127.0.0.1:" + (typeof address === "object" && address ? address.port : config.port);
|
|
168
|
-
const ready = { instanceId, pid: process.pid, url: origin + "/mcp", consoleUrl: settings ? origin + "/admin/" : null };
|
|
169
|
-
console.log(JSON.stringify({ event: "ready", ...ready }));
|
|
170
|
-
process.send?.({ type: "ready", ready });
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
fatal(error);
|
|
174
|
-
}
|
|
175
|
-
//# sourceMappingURL=main.js.map
|
package/dist/model.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
export const timestamp = z.string().datetime({ offset: true }).transform(value => new Date(value).toISOString());
|
|
3
|
-
export const identifier = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/);
|
|
4
|
-
export const scheduleSchema = z.discriminatedUnion("type", [
|
|
5
|
-
z.object({ type: z.literal("now") }).strict(),
|
|
6
|
-
z.object({ type: z.literal("once"), at: timestamp }).strict(),
|
|
7
|
-
z.object({ type: z.literal("interval"), startAt: timestamp,
|
|
8
|
-
seconds: z.number().int().min(60).max(31_536_000) }).strict(),
|
|
9
|
-
]);
|
|
10
|
-
export const executorSchema = z.object({
|
|
11
|
-
id: identifier,
|
|
12
|
-
type: z.enum(["pi", "codex", "claude", "opencode"]),
|
|
13
|
-
note: z.string().max(2000).default(""),
|
|
14
|
-
cwd: z.string().min(1),
|
|
15
|
-
unattendedApproved: z.boolean().default(false),
|
|
16
|
-
}).strict();
|
|
17
|
-
export const OUTPUT_LIMIT = 262_144;
|
|
18
|
-
export const EVENT_LIMIT = 2000;
|
|
19
|
-
export const GRACE_MS = 30_000;
|
|
20
|
-
export function iso(now = Date.now()) { return new Date(now).toISOString(); }
|
|
21
|
-
export function errorText(error) { return (error instanceof Error ? error.message : String(error)).slice(0, 2000); }
|
|
22
|
-
export function initialTime(schedule, now) {
|
|
23
|
-
return schedule.type === "now" ? now : Date.parse(schedule.type === "once" ? schedule.at : schedule.startAt);
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=model.js.map
|