@realqiyan/agent-task 0.0.1-rc.1 → 0.0.1-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/agent-task.mjs +7 -12
- package/deploy/agent-task.service.example +2 -2
- package/dist/acp.js +7 -6
- package/dist/admin.js +3 -3
- package/dist/config.js +15 -56
- package/dist/environment.js +13 -0
- package/dist/executor-runtime.js +37 -0
- package/dist/launcher.js +238 -0
- package/dist/lock.js +16 -0
- package/dist/main.js +74 -30
- package/dist/model.js +2 -5
- package/dist/node-execution.js +2 -3
- package/dist/permissions.js +3 -4
- package/dist/private-files.js +16 -2
- package/dist/results.js +9 -15
- package/dist/settings-contract.js +1 -1
- package/dist/settings.js +25 -8
- package/dist/startup.js +56 -0
- package/dist/tasks.js +5 -6
- package/dist/web/api.js +1 -1
- package/dist/web/app.js +7 -0
- package/dist/web/settings-view.js +33 -28
- package/dist/web.js +14 -2
- package/dist/worker.js +2 -2
- package/docs/architecture-guidelines.md +2 -2
- package/docs/deployment.md +40 -38
- package/docs/design-proposal.md +2 -2
- package/docs/release.md +8 -8
- package/docs/technical-design.md +3 -3
- package/docs/validation.md +17 -9
- package/docs/web-console.md +10 -16
- package/package.json +2 -2
- package/scripts/install-lib.mjs +17 -24
- package/scripts/probe-agent.mjs +8 -3
- package/scripts/service-runner.mjs +2 -7
- package/scripts/setup-wizard.mjs +2 -2
- package/scripts/setup.mjs +24 -20
- package/scripts/uninstall.mjs +13 -9
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
// Same process as the service: the service manager supervises the actual HTTP owner.
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { readManifest, runtimeBaseEnv } from "./install-lib.mjs";
|
|
2
|
+
import { launch } from "../dist/launcher.js";
|
|
4
3
|
|
|
5
4
|
process.umask(0o077);
|
|
6
5
|
const root = process.argv[2];
|
|
7
6
|
if (!root || process.argv.length !== 3) throw new Error("Expected installation home");
|
|
8
7
|
const manifest = readManifest(root);
|
|
9
8
|
if (manifest.phase !== "ready") throw new Error("Installation is not ready");
|
|
10
|
-
|
|
11
|
-
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
12
|
-
Object.assign(process.env, env);
|
|
13
|
-
process.argv = [process.execPath, join(manifest.app, "dist/main.js"), "--config", join(root, "config.json"), "--environment", join(root, "environment.json")];
|
|
14
|
-
await import("../dist/main.js");
|
|
9
|
+
await launch(root, runtimeBaseEnv(manifest));
|
package/scripts/setup-wizard.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
|
|
3
3
|
export async function setupWizard(options, root) {
|
|
4
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive setup requires a terminal; use --executors none|pi,codex,opencode and --cwd ABSOLUTE_DIR for noninteractive setup");
|
|
4
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive setup requires a terminal; use --executors none|pi,codex,claude,opencode and --cwd ABSOLUTE_DIR for noninteractive setup");
|
|
5
5
|
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
6
6
|
const abort = new AbortController();
|
|
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
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.`);
|
|
11
|
-
options.executors ??= await ask("Executors to install (none or comma-separated pi,codex,opencode)", "none");
|
|
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) {
|
|
14
14
|
const provider = await ask("Coordinator provider (none/openai/anthropic/deepseek; configurable later in Web)", "none");
|
package/scripts/setup.mjs
CHANGED
|
@@ -5,15 +5,19 @@ 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
|
-
|
|
8
|
+
runtimeBaseEnv, activate, inventory, verifyServiceFile, packageRoot, checkHealth } 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
14
|
|
|
11
15
|
process.umask(0o077);
|
|
12
16
|
let unlock;
|
|
13
17
|
try {
|
|
14
18
|
const options = argumentsFor(process.argv.slice(2));
|
|
15
19
|
if (options.help) {
|
|
16
|
-
console.log("Usage: agent-task setup [--home ABSOLUTE_PRIVATE_DIR]\nInteractive on a terminal for a new installation. Noninteractive: --executors pi,codex,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]\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.");
|
|
17
21
|
process.exit(0);
|
|
18
22
|
}
|
|
19
23
|
if (options.service && (options["prepare-only"] || options.check)) throw new Error("--service cannot be combined with --prepare-only or --check");
|
|
@@ -30,7 +34,7 @@ try {
|
|
|
30
34
|
if (manifest.phase !== "ready" && !options.resume) throw new Error("Incomplete or uninstalled components; use --resume explicitly (configuration will be preserved)");
|
|
31
35
|
} else {
|
|
32
36
|
if (options.check || options.probe || options.resume) throw new Error("No installation to check/resume");
|
|
33
|
-
if (!options.executors || !options.cwd) throw new Error("Select --executors pi,codex,opencode|none and --cwd ABSOLUTE_DIR");
|
|
37
|
+
if (!options.executors || !options.cwd) throw new Error("Select --executors pi,codex,claude,opencode|none and --cwd ABSOLUTE_DIR");
|
|
34
38
|
const selected = options.executors === "none" ? [] : options.executors.split(",");
|
|
35
39
|
if (selected.some(id => !Object.hasOwn(packages, id)) || new Set(selected).size !== selected.length) throw new Error("Invalid executor selection");
|
|
36
40
|
if (!options.cwd.startsWith("/") || !lstatSync(realpathSync(options.cwd)).isDirectory()) throw new Error("cwd must be an existing absolute directory");
|
|
@@ -40,7 +44,7 @@ try {
|
|
|
40
44
|
const port = Number(options.port ?? 8765);
|
|
41
45
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid port");
|
|
42
46
|
// Reject unowned artifacts. This script never adopts or clears an existing installation.
|
|
43
|
-
for (const name of ["components", "
|
|
47
|
+
for (const name of ["components", "launch.json", "data"]) {
|
|
44
48
|
if (existsSync(join(root, name))) throw new Error("Existing unowned installation artifact: " + name);
|
|
45
49
|
}
|
|
46
50
|
manifest = { project: "agent-task", root, home: homedir(), platform: process.platform, node: process.execPath, app: packageRoot,
|
|
@@ -75,27 +79,26 @@ try {
|
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
mkdirSync(join(root, "data/results"), { recursive: true, mode: 0o700 });
|
|
78
|
-
|
|
79
|
-
if (!existsSync(join(root, "
|
|
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,
|
|
80
86
|
tokenEnv: "AGENT_TASK_TOKEN", coordinator: manifest.coordinator,
|
|
81
|
-
executors: manifest.selected.map(id => ({ id,
|
|
82
|
-
args: id === "opencode" ? ["acp"] : [], cwd: manifest.cwd, envKeys: ["HOME"],
|
|
83
|
-
permissionAllowlist: [], unattendedApproved: false, resultReadRoots: [] })) }, null, 2) + "\n");
|
|
87
|
+
executors: manifest.selected.map(id => ({ id, type: id, note: "", cwd: manifest.cwd, unattendedApproved: false })) }, null, 2) + "\n");
|
|
84
88
|
manifest.phase = "ready";
|
|
89
|
+
delete manifest.port; delete manifest.cwd; delete manifest.coordinator;
|
|
85
90
|
} finally {
|
|
86
91
|
manifest.files = ["components"].filter(name => existsSync(join(root, name))).flatMap(name => inventory(root, name));
|
|
87
92
|
saveManifest(root, manifest);
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
|
-
const env =
|
|
91
|
-
//
|
|
92
|
-
run(manifest.node, ["--input-type=module", "-e", `import {loadConfig} from ${JSON.stringify(pathToFileURL(join(app, "dist/config.js")).href)}; loadConfig(process.argv[1]);`,
|
|
93
|
-
const config = json(
|
|
94
|
-
const { executorEnv } = await import(pathToFileURL(join(app, "dist/config.js")).href);
|
|
95
|
+
const paths = startupPaths(root), env = runtimeBaseEnv(manifest);
|
|
96
|
+
// 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 });
|
|
98
|
+
const config = json(paths.configPath);
|
|
95
99
|
for (const id of manifest.selected) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
run(join(components, "node_modules/.bin", binary), ["--version"], { env: cliEnv, timeout: 20000 });
|
|
100
|
+
const cliEnv = executorEnvironment(env, readEnvironment(paths.environmentPath), config);
|
|
101
|
+
executorCommand(id, cliEnv); // No native process is launched without an explicit --probe.
|
|
99
102
|
}
|
|
100
103
|
const report = { platform: process.platform, dependencies: "ready", configuration: "valid", background: "not-checked",
|
|
101
104
|
coordinator: config.coordinator ? "configured; authentication not verified" : "not configured; task admission disabled",
|
|
@@ -103,11 +106,12 @@ try {
|
|
|
103
106
|
handshake: "not checked", execution: "not verified by setup" })) };
|
|
104
107
|
if (options.probe) {
|
|
105
108
|
for (const entry of report.executors) {
|
|
106
|
-
try { run(manifest.node, [join(app, "scripts/probe-agent.mjs"),
|
|
109
|
+
try { run(manifest.node, [join(app, "scripts/probe-agent.mjs"), paths.configPath, entry.id], { env, timeout: 20000 }); entry.handshake = "passed (initialize only)"; }
|
|
107
110
|
catch { entry.handshake = "failed; inspect native dependencies/configuration separately"; process.exitCode = 1; }
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
if (options.service) {
|
|
114
|
+
if (!manifest.serviceInstalled) acquireLauncherLock(root)(); // Refuse a live foreground owner.
|
|
111
115
|
// Record intent before activating so a failed service start is still owned by uninstall.
|
|
112
116
|
manifest.serviceInstalled = true; saveManifest(root, manifest);
|
|
113
117
|
activate(manifest);
|
|
@@ -115,12 +119,12 @@ try {
|
|
|
115
119
|
}
|
|
116
120
|
if (manifest.serviceInstalled) {
|
|
117
121
|
try {
|
|
118
|
-
await checkHealth(
|
|
122
|
+
await checkHealth(root, 15000);
|
|
119
123
|
report.background = "authenticated MCP health check passed";
|
|
120
124
|
} catch { report.background = "not ready; run --check after startup and inspect the service manager"; process.exitCode = 1; }
|
|
121
125
|
}
|
|
122
126
|
console.log(JSON.stringify(report, null, 2));
|
|
123
|
-
console.log(`Config: ${
|
|
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.`);
|
|
124
128
|
} catch (error) {
|
|
125
129
|
console.error(error.message); process.exitCode = 1;
|
|
126
130
|
} finally { unlock?.(); }
|
package/scripts/uninstall.mjs
CHANGED
|
@@ -4,13 +4,16 @@ import { join } from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { argumentsFor, installRoot, readManifest, saveManifest, operationLock, deactivate, removeInventory, json } from "./install-lib.mjs";
|
|
6
6
|
|
|
7
|
+
import { startupPaths } from "../dist/startup.js";
|
|
8
|
+
import { acquireLauncherLock } from "../dist/lock.js";
|
|
9
|
+
|
|
7
10
|
process.umask(0o077);
|
|
8
11
|
const options = argumentsFor(process.argv.slice(2));
|
|
9
12
|
if (options.help) {
|
|
10
13
|
console.log("Usage: agent-task uninstall [--home ABSOLUTE_PRIVATE_DIR]\nStops/removes the owned service and installed components only. Keeps configuration, credentials, logs, task history and workspaces.");
|
|
11
14
|
process.exit(0);
|
|
12
15
|
}
|
|
13
|
-
let unlock, release;
|
|
16
|
+
let unlock, release, unlockLauncher;
|
|
14
17
|
try {
|
|
15
18
|
if (Object.keys(options).some(key => key !== "home")) throw new Error("Only --home is accepted");
|
|
16
19
|
const root = installRoot(options);
|
|
@@ -18,20 +21,21 @@ try {
|
|
|
18
21
|
readManifest(root);
|
|
19
22
|
unlock = operationLock(root);
|
|
20
23
|
const manifest = readManifest(root);
|
|
21
|
-
deactivate(manifest);
|
|
24
|
+
await deactivate(manifest);
|
|
22
25
|
manifest.serviceInstalled = false; saveManifest(root, manifest);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
+
unlockLauncher = acquireLauncherLock(root);
|
|
27
|
+
if (existsSync(join(root, "launch.json"))) {
|
|
28
|
+
const paths = startupPaths(root), config = json(paths.configPath);
|
|
29
|
+
const protectedPaths = [paths.dataDir, config.dataDir, ...config.executors.map(executor => executor.cwd)];
|
|
26
30
|
// Verify the service is stopped, including any manually started owner.
|
|
27
31
|
const lockModule = join(manifest.app, "dist/lock.js");
|
|
28
32
|
if (existsSync(lockModule)) {
|
|
29
33
|
const { acquireLock } = await import(pathToFileURL(lockModule).href);
|
|
30
|
-
release = await acquireLock(
|
|
34
|
+
release = await acquireLock(paths.dataDir);
|
|
31
35
|
const storeModule = join(manifest.app, "dist/task-storage.js");
|
|
32
|
-
if (existsSync(join(
|
|
36
|
+
if (existsSync(join(paths.dataDir, "tasks.sqlite")) && existsSync(storeModule)) {
|
|
33
37
|
const { TaskStore } = await import(pathToFileURL(storeModule).href);
|
|
34
|
-
const store = new TaskStore(
|
|
38
|
+
const store = new TaskStore(paths.dataDir);
|
|
35
39
|
try {
|
|
36
40
|
for (const task of store.definitions()) protectedPaths.push(task.workspace, task.input.workspace, ...Object.values(task.executors).map(executor => executor.cwd));
|
|
37
41
|
for (const run of store.rounds()) for (const node of run.nodes) protectedPaths.push(node.workspace);
|
|
@@ -50,4 +54,4 @@ try {
|
|
|
50
54
|
manifest.phase = "uninstalled"; manifest.files = []; saveManifest(root, manifest);
|
|
51
55
|
console.log("Owned service and inventoried components removed. Configuration, environment.json, logs, data, workspaces and all native/shared executor authentication are preserved. Repeated uninstall is safe.");
|
|
52
56
|
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
53
|
-
finally { await release?.(); unlock?.(); }
|
|
57
|
+
finally { await release?.(); unlockLauncher?.(); unlock?.(); }
|