@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.
Files changed (88) hide show
  1. package/README.md +4 -3
  2. package/bin/agent-task.mjs +23 -36
  3. package/dist/{launcher.js → application/launcher.js} +19 -42
  4. package/dist/application/main.js +122 -0
  5. package/dist/{startup.js → application/startup.js} +6 -11
  6. package/dist/application/task-runtime.js +42 -0
  7. package/dist/configuration/contract.js +28 -0
  8. package/dist/{environment.js → configuration/environment.js} +1 -5
  9. package/dist/configuration/files.js +43 -0
  10. package/dist/configuration/management.js +6 -0
  11. package/dist/configuration/model.js +2 -0
  12. package/dist/configuration/policy.js +29 -0
  13. package/dist/configuration/ports.js +2 -0
  14. package/dist/{settings.js → configuration/settings.js} +77 -39
  15. package/dist/executors/environment.js +10 -0
  16. package/dist/executors/model.js +2 -0
  17. package/dist/executors/registry.js +29 -0
  18. package/dist/executors/validation.js +10 -0
  19. package/dist/{lock.js → infrastructure/lock.js} +2 -2
  20. package/dist/{private-files.js → infrastructure/private-files.js} +1 -7
  21. package/dist/infrastructure/sqlite.js +19 -0
  22. package/dist/infrastructure/version.js +3 -0
  23. package/dist/{acp.js → integrations/acp/connection.js} +9 -7
  24. package/dist/{node-execution.js → integrations/acp/execution.js} +1 -1
  25. package/dist/{executor-runtime.js → integrations/acp/runtime.js} +1 -10
  26. package/dist/{worker.js → integrations/acp/worker.js} +3 -2
  27. package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
  28. package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
  29. package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
  30. package/dist/integrations/local-service/control.js +33 -0
  31. package/dist/protocol/cli/admin-client.js +45 -0
  32. package/dist/protocol/cli/commands.js +394 -0
  33. package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
  34. package/dist/protocol/http/server.js +111 -0
  35. package/dist/{web.js → protocol/http/web.js} +18 -17
  36. package/dist/{mcp.js → protocol/mcp.js} +13 -13
  37. package/dist/protocol/services.js +2 -0
  38. package/dist/protocol/settings-contract.js +40 -0
  39. package/dist/protocol/task-contract.js +28 -0
  40. package/dist/results/files.js +70 -0
  41. package/dist/results/ports.js +2 -0
  42. package/dist/results/service.js +42 -0
  43. package/dist/shared/errors.js +13 -0
  44. package/dist/shared/limits.js +3 -0
  45. package/dist/shared/time.js +2 -0
  46. package/dist/shared/validation.js +8 -0
  47. package/dist/tasks/definitions.js +83 -0
  48. package/dist/tasks/graph.js +23 -0
  49. package/dist/{human-input.js → tasks/human-input.js} +8 -7
  50. package/dist/tasks/model.js +2 -0
  51. package/dist/tasks/ports.js +2 -0
  52. package/dist/tasks/runs.js +263 -0
  53. package/dist/tasks/schedule.js +36 -0
  54. package/dist/tasks/sessions.js +62 -0
  55. package/dist/tasks/storage/records.js +45 -0
  56. package/dist/{task-storage.js → tasks/storage/store.js} +23 -36
  57. package/dist/tasks/supervisor.js +227 -0
  58. package/dist/tasks/validation.js +24 -0
  59. package/dist/web/settings-view.js +13 -6
  60. package/docs/architecture-guidelines.md +6 -4
  61. package/docs/cli.md +49 -0
  62. package/docs/deployment.md +16 -56
  63. package/docs/design-proposal.md +2 -2
  64. package/docs/release.md +14 -15
  65. package/docs/technical-design.md +47 -26
  66. package/docs/validation.md +49 -17
  67. package/docs/web-console.md +10 -8
  68. package/package.json +7 -7
  69. package/scripts/install-lib.mjs +4 -95
  70. package/scripts/mcp-call.mjs +1 -1
  71. package/scripts/probe-agent.mjs +6 -5
  72. package/scripts/service-runner.mjs +1 -1
  73. package/scripts/setup-wizard.mjs +1 -6
  74. package/scripts/setup.mjs +17 -30
  75. package/deploy/agent-task.service.example +0 -17
  76. package/dist/admin.js +0 -31
  77. package/dist/config.js +0 -72
  78. package/dist/main.js +0 -175
  79. package/dist/model.js +0 -25
  80. package/dist/results.js +0 -79
  81. package/dist/settings-contract.js +0 -16
  82. package/dist/task-contract.js +0 -37
  83. package/dist/task-model.js +0 -2
  84. package/dist/tasks.js +0 -508
  85. package/dist/version.js +0 -3
  86. package/scripts/uninstall.mjs +0 -57
  87. /package/dist/{permissions.js → executors/permissions.js} +0 -0
  88. /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
@@ -1,16 +1,12 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { DatabaseSync } from "node:sqlite";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync, realpathSync, readdirSync, unlinkSync, rmdirSync, openSync, closeSync, fsyncSync, renameSync } from "node:fs";
2
+ import { DatabaseSync } from "../dist/infrastructure/sqlite.js";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync, realpathSync, readdirSync, unlinkSync, openSync, closeSync, fsyncSync, renameSync } from "node:fs";
4
4
  import { dirname, join, resolve, isAbsolute, basename } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
- import { setTimeout as delay } from "node:timers/promises";
8
- import { controlService } from "../dist/launcher.js";
9
7
 
10
8
  export const packageRoot = realpathSync(fileURLToPath(new URL("..", import.meta.url)));
11
9
 
12
- export const label = "com.agent-task.service";
13
- export const unit = "agent-task.service";
14
10
  export const packages = {
15
11
  pi: ["@earendil-works/pi-coding-agent@1.3.0", "pi-acp@0.0.34"],
16
12
  codex: ["@agentclientprotocol/codex-acp@2.2.2"],
@@ -18,7 +14,7 @@ export const packages = {
18
14
  opencode: ["opencode-ai@1.19.3"],
19
15
  };
20
16
  export function argumentsFor(argv) {
21
- const flags = new Set(["prepare-only", "service", "check", "probe", "resume", "help"]), options = {};
17
+ const flags = new Set(["check", "probe", "resume", "help"]), options = {};
22
18
  for (let i = 0; i < argv.length; i++) {
23
19
  const key = argv[i].replace(/^--/, "");
24
20
  if (!argv[i].startsWith("--") || key in options) throw new Error("Invalid or repeated option");
@@ -61,7 +57,7 @@ export function run(command, args, options = {}) {
61
57
  export function operationLock(root) {
62
58
  const db = new DatabaseSync(join(root, "setup-lock.sqlite"));
63
59
  try { db.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE"); }
64
- catch { db.close(); throw new Error("Another setup/uninstall operation owns this installation"); }
60
+ catch { db.close(); throw new Error("Another setup operation owns this installation"); }
65
61
  return () => db.close();
66
62
  }
67
63
  export function readManifest(root) {
@@ -90,95 +86,8 @@ export function runtimeBaseEnv(manifest) {
90
86
  // Capture the OS user environment; the runtime isolates private service values before worker creation.
91
87
  return { ...process.env, HOME: manifest.home, PATH: [join(manifest.root, "components/node_modules/.bin"), dirname(manifest.node), process.env.PATH ?? "", "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].join(":"), LANG: process.env.LANG ?? "en_US.UTF-8" };
92
88
  }
93
- const xml = value => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
94
- const systemdQuote = value => '"' + value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("$", "$$") + '"';
95
- export function serviceDefinition(manifest) {
96
- const runner = join(manifest.app, "scripts/service-runner.mjs");
97
- if (manifest.platform === "linux") return {
98
- path: join(manifest.home, ".config/systemd/user", unit),
99
- text: `[Unit]\nDescription=Agent Task (managed by setup)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=${[manifest.node, runner, manifest.root].map(systemdQuote).join(" ")}\nRestart=on-failure\nRestartSec=5\nTimeoutStopSec=45\nKillMode=control-group\nUMask=0077\n\n[Install]\nWantedBy=default.target\n`,
100
- };
101
- return { path: join(manifest.home, "Library/LaunchAgents", label + ".plist"), text:
102
- `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>${label}</string>\n<key>ProgramArguments</key><array>${[manifest.node, runner, manifest.root].map(value => `<string>${xml(value)}</string>`).join("")}</array>\n<key>RunAtLoad</key><true/>\n<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n<key>ThrottleInterval</key><integer>5</integer>\n<key>ExitTimeOut</key><integer>45</integer>\n<key>AbandonProcessGroup</key><false/>\n<key>Umask</key><integer>63</integer>\n</dict></plist>\n` };
103
- }
104
- export function serviceEnv() {
105
- const keys = ["HOME", "PATH", "LANG", "LC_ALL", "DBUS_SESSION_BUS_ADDRESS"];
106
- return { ...Object.fromEntries(keys.filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]])),
107
- XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${process.getuid()}` };
108
- }
109
- export async function checkHealth(home, waitMs = 5000) {
110
- const deadline = Date.now() + waitMs;
111
- do {
112
- try { return await controlService(home, "status"); }
113
- catch { /* The launcher may still be starting; never log credentials. */ }
114
- await delay(200);
115
- } while (Date.now() < deadline);
116
- throw new Error("Authenticated MCP health check failed; inspect private logs and startup files");
117
- }
118
- export function verifyServiceFile(manifest) {
119
- const definition = serviceDefinition(manifest);
120
- if (existsSync(definition.path)) {
121
- privateFile(definition.path);
122
- if (readFileSync(definition.path, "utf8") !== definition.text) throw new Error("Service file differs; refusing to overwrite or remove it: " + definition.path);
123
- }
124
- return definition;
125
- }
126
- export function activate(manifest) {
127
- const definition = verifyServiceFile(manifest);
128
- if (!existsSync(definition.path)) createFile(definition.path, definition.text);
129
- if (manifest.platform === "linux") {
130
- run("systemctl", ["--user", "daemon-reload"], { env: serviceEnv() });
131
- run("systemctl", ["--user", "enable", "--now", unit], { env: serviceEnv() });
132
- run("systemctl", ["--user", "is-active", unit], { env: serviceEnv() });
133
- } else {
134
- const domain = `gui/${process.getuid()}`;
135
- const active = spawnSync("launchctl", ["print", domain + "/" + label], { stdio: "ignore" });
136
- if (active.status !== 0) run("launchctl", ["bootstrap", domain, definition.path]);
137
- run("launchctl", ["kickstart", domain + "/" + label]);
138
- run("launchctl", ["print", domain + "/" + label]);
139
- }
140
- }
141
- export async function deactivate(manifest) {
142
- const definition = verifyServiceFile(manifest);
143
- if (!existsSync(definition.path)) {
144
- if (manifest.serviceInstalled) throw new Error("Managed service file is missing; verify it is unloaded before cleanup");
145
- return;
146
- }
147
- if (manifest.platform === "linux") {
148
- run("systemctl", ["--user", "disable", "--now", unit], { env: serviceEnv() });
149
- // Do not remove code while the service is still active or deactivating.
150
- const state = run("systemctl", ["--user", "show", "--property=ActiveState", "--value", unit], { env: serviceEnv() });
151
- if (!["inactive", "failed"].includes(state)) throw new Error("Service has not stopped");
152
- } else {
153
- const target = `gui/${process.getuid()}/${label}`;
154
- const active = spawnSync("launchctl", ["print", target], { stdio: "ignore" });
155
- if (active.status === 0) run("launchctl", ["bootout", target]);
156
- // bootout can return while launchd still reports the job during graceful shutdown.
157
- const deadline = Date.now() + 50_000;
158
- while (spawnSync("launchctl", ["print", target], { stdio: "ignore" }).status === 0) {
159
- if (Date.now() >= deadline) throw new Error("LaunchAgent is still loaded");
160
- await delay(100);
161
- }
162
- }
163
- unlinkSync(definition.path);
164
- if (manifest.platform === "linux") run("systemctl", ["--user", "daemon-reload"], { env: serviceEnv() });
165
- }
166
89
  export function inventory(root, relative) {
167
90
  const path = join(root, relative), stat = lstatSync(path);
168
91
  if (!stat.isDirectory() || stat.isSymbolicLink()) return [relative];
169
92
  return readdirSync(path).flatMap(name => inventory(root, join(relative, name))).concat(relative);
170
93
  }
171
- export function removeInventory(root, files) {
172
- for (const relative of files) {
173
- if (!/^components(\/|$)/.test(relative) || relative.split("/").includes("..")) throw new Error("Invalid component inventory");
174
- const path = join(root, relative);
175
- // Never follow a directory replaced by a symlink outside the installation.
176
- try {
177
- if (realpathSync(dirname(path)) !== dirname(path)) throw new Error("Component parent path changed; cleanup stopped");
178
- const stat = lstatSync(path);
179
- if (stat.isDirectory() && !stat.isSymbolicLink()) {
180
- try { rmdirSync(path); } catch (error) { if (error.code !== "ENOTEMPTY") throw error; }
181
- } else unlinkSync(path);
182
- } catch (error) { if (error.code !== "ENOENT") throw error; }
183
- }
184
- }
@@ -1,6 +1,6 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
- import { version } from "../dist/version.js";
3
+ import { version } from "../dist/infrastructure/version.js";
4
4
  const [tool, input = "{}"] = process.argv.slice(2);
5
5
  if (!tool || !process.env.AGENT_TASK_TOKEN) {
6
6
  console.error("Usage: AGENT_TASK_TOKEN=... node scripts/mcp-call.mjs <tool|--list> '[JSON arguments]'\nAGENT_TASK_URL defaults to http://127.0.0.1:8765/mcp");
@@ -2,12 +2,13 @@
2
2
  import { spawn } from "node:child_process";
3
3
  import { Readable, Writable } from "node:stream";
4
4
  import * as acp from "@agentclientprotocol/sdk";
5
- import { loadConfig } from "../dist/config.js";
6
- import { executorCommand, executorEnvironment } from "../dist/executor-runtime.js";
7
- import { readEnvironment } from "../dist/environment.js";
5
+ import { loadConfig } from "../dist/configuration/files.js";
6
+ import { executorEnvironment } from "../dist/executors/environment.js";
7
+ import { executorCommand } from "../dist/integrations/acp/runtime.js";
8
+ import { readEnvironment } from "../dist/configuration/files.js";
8
9
  import { join } from "node:path";
9
- import { terminateGroup } from "../dist/acp.js";
10
- import { version } from "../dist/version.js";
10
+ import { terminateGroup } from "../dist/integrations/acp/connection.js";
11
+ import { version } from "../dist/infrastructure/version.js";
11
12
  const [file, id] = process.argv.slice(2);
12
13
  if (!file || !id) { console.error("Usage: node scripts/probe-agent.mjs <config.json> <executorId>"); process.exit(2); }
13
14
  const config = loadConfig(file);
@@ -1,5 +1,5 @@
1
1
  import { readManifest, runtimeBaseEnv } from "./install-lib.mjs";
2
- import { launch } from "../dist/launcher.js";
2
+ import { launch } from "../dist/application/launcher.js";
3
3
 
4
4
  process.umask(0o077);
5
5
  const root = process.argv[2];
@@ -7,7 +7,7 @@ export async function setupWizard(options, root) {
7
7
  input.on("SIGINT", () => abort.abort());
8
8
  const ask = async (question, fallback) => (await input.question(`${question}${fallback ? " [" + fallback + "]" : ""}: `, { signal: abort.signal })).trim() || fallback;
9
9
  try {
10
- console.log(`Private installation: ${root}\nNo credentials are requested here. Use the local Web console or a private editor after setup.\nExecutors are optional; installing one does not authorize it.`);
10
+ console.log(`Private installation: ${root}`);
11
11
  options.executors ??= await ask("Executors to install (none or comma-separated pi,codex,claude,opencode)", "none");
12
12
  options.cwd ??= await ask("Existing absolute workspace (outside installation and npm package)", process.cwd());
13
13
  if (!options.provider) {
@@ -16,11 +16,6 @@ export async function setupWizard(options, root) {
16
16
  }
17
17
  if (options.provider) options.model ??= await ask("Coordinator model ID", "");
18
18
  options.port ??= await ask("Loopback HTTP port", "8765");
19
- if (!options["prepare-only"] && !options.service) {
20
- const answer = await ask("Install and start the background service now? (yes/no)", "no");
21
- if (!["yes", "no"].includes(answer)) throw new Error("Expected yes or no; setup cancelled");
22
- if (answer === "yes") options.service = true;
23
- }
24
19
  if (await ask("Write private configuration and install selected components? (yes/no)", "no") !== "yes") throw new Error("Setup cancelled; no configuration written");
25
20
  } catch (error) {
26
21
  if (abort.signal.aborted) throw new Error("Setup cancelled; no configuration written");
package/scripts/setup.mjs CHANGED
@@ -5,22 +5,21 @@ import { pathToFileURL } from "node:url";
5
5
  import { homedir } from "node:os";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { argumentsFor, installRoot, packages, createFile, json, run, operationLock, readManifest, saveManifest,
8
- runtimeBaseEnv, activate, inventory, verifyServiceFile, packageRoot, checkHealth } from "./install-lib.mjs";
8
+ runtimeBaseEnv, inventory, packageRoot } from "./install-lib.mjs";
9
9
  import { setupWizard } from "./setup-wizard.mjs";
10
- import { startupPaths } from "../dist/startup.js";
11
- import { readEnvironment } from "../dist/environment.js";
12
- import { executorCommand, executorEnvironment } from "../dist/executor-runtime.js";
13
- import { acquireLauncherLock } from "../dist/lock.js";
10
+ import { startupPaths } from "../dist/configuration/files.js";
11
+ import { readEnvironment } from "../dist/configuration/files.js";
12
+ import { executorEnvironment } from "../dist/executors/environment.js";
13
+ import { executorCommand } from "../dist/integrations/acp/runtime.js";
14
14
 
15
15
  process.umask(0o077);
16
16
  let unlock;
17
17
  try {
18
18
  const options = argumentsFor(process.argv.slice(2));
19
19
  if (options.help) {
20
- console.log("Usage: agent-task setup [--home ABSOLUTE_PRIVATE_DIR]\nInteractive on a terminal for a new installation. Noninteractive: --executors pi,codex,claude,opencode|none --cwd ABSOLUTE_DIR [--provider openai|anthropic|deepseek --model ID] [--port 8765]\nDefault: --prepare-only (no background service). --service explicitly installs/starts the background service.\nExisting installation: --check [--probe] | --resume. No credentials accepted on the command line. Existing configuration is never replaced.");
20
+ console.log("Usage: agent-task setup [--home ABSOLUTE_PRIVATE_DIR]\nInteractive on a terminal for a new installation. Noninteractive: --executors pi,codex,claude,opencode|none --cwd ABSOLUTE_DIR [--provider openai|anthropic|deepseek --model ID] [--port 8765]\nExisting installation: --check [--probe] | --resume. No credentials accepted on the command line. Existing configuration is never replaced.");
21
21
  process.exit(0);
22
22
  }
23
- if (options.service && (options["prepare-only"] || options.check)) throw new Error("--service cannot be combined with --prepare-only or --check");
24
23
  if (options.probe && !options.check) throw new Error("--probe requires --check");
25
24
  const root = installRoot(options), manifestPath = join(root, "installation.json");
26
25
  if (!existsSync(manifestPath) && !options.check && !options.resume && (!options.executors || !options.cwd)) await setupWizard(options, root);
@@ -31,7 +30,7 @@ try {
31
30
  if (existsSync(manifestPath)) {
32
31
  manifest = readManifest(root);
33
32
  if (["executors", "cwd", "provider", "model", "port"].some(key => options[key] !== undefined)) throw new Error("Installation already exists; initialization options cannot replace existing configuration");
34
- if (manifest.phase !== "ready" && !options.resume) throw new Error("Incomplete or uninstalled components; use --resume explicitly (configuration will be preserved)");
33
+ if (manifest.phase !== "ready" && !options.resume) throw new Error("Incomplete components; use --resume explicitly (configuration will be preserved)");
35
34
  } else {
36
35
  if (options.check || options.probe || options.resume) throw new Error("No installation to check/resume");
37
36
  if (!options.executors || !options.cwd) throw new Error("Select --executors pi,codex,claude,opencode|none and --cwd ABSOLUTE_DIR");
@@ -49,26 +48,23 @@ try {
49
48
  }
50
49
  manifest = { project: "agent-task", root, home: homedir(), platform: process.platform, node: process.execPath, app: packageRoot,
51
50
  selected, cwd, port, coordinator: options.provider ? { provider: options.provider, model: options.model } : undefined,
52
- phase: "installing", serviceInstalled: false, files: [] };
51
+ phase: "installing", files: [] };
53
52
  createFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
54
53
  }
55
- verifyServiceFile(manifest);
56
54
  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)");
55
+ if (!existsSync(join(app, "dist/application/main.js"))) throw new Error("Prebuilt runtime is missing; reinstall the npm package (source contributors: npm run build)");
58
56
  if (manifest.phase !== "ready") {
59
- if (manifest.serviceInstalled) throw new Error("Stop/uninstall the existing service before rebuilding components");
60
57
  for (const name of ["components"]) {
61
58
  if (!existsSync(join(root, name))) continue;
62
59
  const known = new Set(manifest.files);
63
60
  if (inventory(root, name).some(file => !known.has(file))) throw new Error("Uninventoried component files exist; refusing to overwrite them during resume");
64
61
  }
65
62
  try {
66
- console.log("Using the prebuilt npm runtime. No source checkout or compilation required.");
67
63
  mkdirSync(components, { recursive: true, mode: 0o700 });
68
64
  if (!existsSync(join(components, "package.json"))) createFile(join(components, "package.json"), JSON.stringify({ private: true }));
69
65
  const dependencies = manifest.selected.flatMap(id => packages[id]);
70
66
  if (dependencies.length) {
71
- console.log("Installing selected ACP executors into private components (not global executables).");
67
+ console.log("Installing selected ACP executors...");
72
68
  const installArgs = existsSync(join(components, "package-lock.json"))
73
69
  ? ["ci", "--ignore-scripts", "--no-audit", "--no-fund"]
74
70
  : ["install", "--save-exact", "--ignore-scripts", "--no-audit", "--no-fund", ...dependencies];
@@ -94,13 +90,13 @@ try {
94
90
  }
95
91
  const paths = startupPaths(root), env = runtimeBaseEnv(manifest);
96
92
  // Use the same native launch environment; private values are read separately, never passed to probes.
97
- run(manifest.node, ["--input-type=module", "-e", `import {loadConfig} from ${JSON.stringify(pathToFileURL(join(app, "dist/config.js")).href)}; loadConfig(process.argv[1]);`, paths.configPath], { env });
93
+ run(manifest.node, ["--input-type=module", "-e", `import {loadConfig} from ${JSON.stringify(pathToFileURL(join(app, "dist/configuration/files.js")).href)}; loadConfig(process.argv[1]);`, paths.configPath], { env });
98
94
  const config = json(paths.configPath);
99
95
  for (const id of manifest.selected) {
100
96
  const cliEnv = executorEnvironment(env, readEnvironment(paths.environmentPath), config);
101
97
  executorCommand(id, cliEnv); // No native process is launched without an explicit --probe.
102
98
  }
103
- const report = { platform: process.platform, dependencies: "ready", configuration: "valid", background: "not-checked",
99
+ const report = { platform: process.platform, dependencies: "ready", configuration: "valid",
104
100
  coordinator: config.coordinator ? "configured; authentication not verified" : "not configured; task admission disabled",
105
101
  executors: config.executors.map(executor => ({ id: executor.id, authorized: executor.unattendedApproved,
106
102
  handshake: "not checked", execution: "not verified by setup" })) };
@@ -110,21 +106,12 @@ try {
110
106
  catch { entry.handshake = "failed; inspect native dependencies/configuration separately"; process.exitCode = 1; }
111
107
  }
112
108
  }
113
- if (options.service) {
114
- if (!manifest.serviceInstalled) acquireLauncherLock(root)(); // Refuse a live foreground owner.
115
- // Record intent before activating so a failed service start is still owned by uninstall.
116
- manifest.serviceInstalled = true; saveManifest(root, manifest);
117
- activate(manifest);
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
109
  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.`);
110
+ if (!options.check) {
111
+ console.log(`Config: ${paths.configPath}\nCredentials: ${paths.environmentPath}\nConsole: http://127.0.0.1:${config.port}/admin/\nSign in with AGENT_TASK_ADMIN_TOKEN from the credentials file.`);
112
+ if (config.executors.length) console.log("Prepare native executor authentication and review permissions before approving unattended execution.");
113
+ console.log(`Start: agent-task start --home '${root.replaceAll("'", "'\"'\"'")}'`);
114
+ }
128
115
  } catch (error) {
129
116
  console.error(error.message); process.exitCode = 1;
130
117
  } 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/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