agentcorp-broker 0.1.0-alpha.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/CHANGELOG.md +52 -0
- package/CONTRIBUTING.md +20 -0
- package/LICENSE +201 -0
- package/README.md +250 -0
- package/SECURITY.md +23 -0
- package/dist/audit.d.ts +25 -0
- package/dist/audit.js +203 -0
- package/dist/audit.js.map +1 -0
- package/dist/broker.d.ts +103 -0
- package/dist/broker.js +805 -0
- package/dist/broker.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +712 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +41 -0
- package/dist/config.js.map +1 -0
- package/dist/console/console.css +794 -0
- package/dist/console/console.js +802 -0
- package/dist/console/index.html +309 -0
- package/dist/credentials.d.ts +16 -0
- package/dist/credentials.js +72 -0
- package/dist/credentials.js.map +1 -0
- package/dist/database.d.ts +119 -0
- package/dist/database.js +1356 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +44 -0
- package/dist/diagnostics.js +357 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +215 -0
- package/dist/mcp.js.map +1 -0
- package/dist/migrations.d.ts +13 -0
- package/dist/migrations.js +266 -0
- package/dist/migrations.js.map +1 -0
- package/dist/policy.d.ts +22 -0
- package/dist/policy.js +33 -0
- package/dist/policy.js.map +1 -0
- package/dist/server.d.ts +49 -0
- package/dist/server.js +529 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio-adapter.d.ts +230 -0
- package/dist/stdio-adapter.js +406 -0
- package/dist/stdio-adapter.js.map +1 -0
- package/dist/tui.d.ts +26 -0
- package/dist/tui.js +291 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +355 -0
- package/dist/types.js +85 -0
- package/dist/types.js.map +1 -0
- package/docs/ARCHITECTURE.md +119 -0
- package/docs/README.md +37 -0
- package/docs/RELEASING.md +228 -0
- package/docs/ROADMAP.md +112 -0
- package/docs/cli-reference.md +133 -0
- package/docs/dogfooding-report.md +83 -0
- package/docs/getting-started.md +228 -0
- package/docs/guides/antigravity-setup.md +84 -0
- package/docs/guides/claude-cursor-setup.md +76 -0
- package/docs/guides/codex-setup.md +75 -0
- package/docs/guides/human-console.md +177 -0
- package/docs/mcp-tools-reference.md +235 -0
- package/docs/policy-guide.md +105 -0
- package/examples/org.toml +65 -0
- package/package.json +68 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { stdin as input, stdout as outputStream } from "node:process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { exportAuditTrail } from "./audit.js";
|
|
9
|
+
import { AgentCorpBroker } from "./broker.js";
|
|
10
|
+
import { loadOrgConfig } from "./config.js";
|
|
11
|
+
import { ensureCredentials, loadCredentials } from "./credentials.js";
|
|
12
|
+
import { AgentCorpDatabase } from "./database.js";
|
|
13
|
+
import { recordCrashDiagnostics, RotatingLogger, runDoctor, sanitizeBrokerEventForLog } from "./diagnostics.js";
|
|
14
|
+
import { AgentCorpError } from "./errors.js";
|
|
15
|
+
import { AgentCorpServer, readDaemonInfo } from "./server.js";
|
|
16
|
+
import { ensureDaemonRunning, isDaemonHealthy, resolveDefaultPath, runStdioAdapter } from "./stdio-adapter.js";
|
|
17
|
+
import { AgentCorpTui } from "./tui.js";
|
|
18
|
+
process.on("uncaughtException", (error) => {
|
|
19
|
+
recordCrashDiagnostics(".agentcorp/crash.log", error, {
|
|
20
|
+
args: process.argv,
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
});
|
|
23
|
+
console.error("Fatal uncaught exception recorded in .agentcorp/crash.log");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
process.on("unhandledRejection", (reason) => {
|
|
27
|
+
recordCrashDiagnostics(".agentcorp/crash.log", reason, {
|
|
28
|
+
args: process.argv,
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
});
|
|
31
|
+
console.error("Fatal unhandled rejection recorded in .agentcorp/crash.log");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
const SAMPLE_ORG = `[company]
|
|
35
|
+
name = "My Agent Company"
|
|
36
|
+
|
|
37
|
+
[limits]
|
|
38
|
+
max_request_body_bytes = 2097152
|
|
39
|
+
max_message_payload_bytes = 1048576
|
|
40
|
+
max_artifact_bytes = 5242880
|
|
41
|
+
default_page_size = 50
|
|
42
|
+
max_page_size = 200
|
|
43
|
+
max_audit_payload_bytes = 65536
|
|
44
|
+
|
|
45
|
+
[[roles]]
|
|
46
|
+
id = "architect"
|
|
47
|
+
display_name = "Architect"
|
|
48
|
+
model = "openai/codex"
|
|
49
|
+
interface = "mcp"
|
|
50
|
+
capabilities = ["propose_plan", "review", "approve_merge"]
|
|
51
|
+
allowed_peers = ["developer"]
|
|
52
|
+
artifact_visibility = ["architect", "developer"]
|
|
53
|
+
|
|
54
|
+
[[roles]]
|
|
55
|
+
id = "developer"
|
|
56
|
+
display_name = "Developer"
|
|
57
|
+
model = "any/mcp-capable-agent"
|
|
58
|
+
interface = "mcp"
|
|
59
|
+
capabilities = ["write_code", "run_tests", "report"]
|
|
60
|
+
allowed_peers = ["architect"]
|
|
61
|
+
artifact_visibility = ["architect", "developer"]
|
|
62
|
+
|
|
63
|
+
# Higher priority rules win. If no rule matches, human approval is required.
|
|
64
|
+
[[policies]]
|
|
65
|
+
id = "gate-critical-proposals"
|
|
66
|
+
subject = "message"
|
|
67
|
+
message_type = "proposal"
|
|
68
|
+
priority = 200
|
|
69
|
+
action = "require_human"
|
|
70
|
+
|
|
71
|
+
[[policies]]
|
|
72
|
+
id = "allow-read-only-status-updates"
|
|
73
|
+
subject = "message"
|
|
74
|
+
message_type = "status_update"
|
|
75
|
+
priority = 100
|
|
76
|
+
risk_tags = ["read_only"]
|
|
77
|
+
action = "auto_approve"
|
|
78
|
+
|
|
79
|
+
[[policies]]
|
|
80
|
+
id = "allow-read-only-reports"
|
|
81
|
+
subject = "message"
|
|
82
|
+
message_type = "report"
|
|
83
|
+
priority = 100
|
|
84
|
+
risk_tags = ["read_only"]
|
|
85
|
+
action = "auto_approve"
|
|
86
|
+
|
|
87
|
+
[[policies]]
|
|
88
|
+
id = "allow-progress-updates"
|
|
89
|
+
subject = "task"
|
|
90
|
+
priority = 50
|
|
91
|
+
to_status = "in_progress"
|
|
92
|
+
action = "auto_approve"
|
|
93
|
+
|
|
94
|
+
[[policies]]
|
|
95
|
+
id = "gate-task-completion"
|
|
96
|
+
subject = "task"
|
|
97
|
+
priority = 100
|
|
98
|
+
to_status = "completed"
|
|
99
|
+
action = "require_human"
|
|
100
|
+
`;
|
|
101
|
+
function output(value) {
|
|
102
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
async function refreshDaemonInfo(recorded) {
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(`${recorded.url}/health`, { signal: AbortSignal.timeout(1500) });
|
|
107
|
+
if (!res.ok)
|
|
108
|
+
return null;
|
|
109
|
+
const health = await res.json();
|
|
110
|
+
if (health.status !== "ok" || !Number.isInteger(health.pid) || !health.startedAt)
|
|
111
|
+
return null;
|
|
112
|
+
const info = { ...recorded, pid: health.pid, startedAt: health.startedAt };
|
|
113
|
+
const changed = info.pid !== recorded.pid || info.startedAt !== recorded.startedAt;
|
|
114
|
+
let controlFileRepaired = false;
|
|
115
|
+
if (changed) {
|
|
116
|
+
try {
|
|
117
|
+
writeFileSync(resolve(".agentcorp/daemon.json"), JSON.stringify(info, null, 2), "utf8");
|
|
118
|
+
controlFileRepaired = true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Health remains authoritative even if the local control file cannot be repaired.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { info, health, controlFileRepaired };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function resolveDaemonInfo(explicitUrl) {
|
|
131
|
+
const recorded = readDaemonInfo();
|
|
132
|
+
if (recorded) {
|
|
133
|
+
const refreshed = await refreshDaemonInfo(recorded);
|
|
134
|
+
if (refreshed)
|
|
135
|
+
return refreshed;
|
|
136
|
+
}
|
|
137
|
+
const candidateUrl = explicitUrl ?? "http://127.0.0.1:54321";
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`${candidateUrl}/health`, { signal: AbortSignal.timeout(1500) });
|
|
140
|
+
if (!res.ok)
|
|
141
|
+
return null;
|
|
142
|
+
const health = (await res.json());
|
|
143
|
+
if (health.status !== "ok" || !Number.isInteger(health.pid) || !health.startedAt)
|
|
144
|
+
return null;
|
|
145
|
+
const parsed = new URL(candidateUrl);
|
|
146
|
+
const info = {
|
|
147
|
+
pid: health.pid,
|
|
148
|
+
port: Number(parsed.port || 54321),
|
|
149
|
+
host: parsed.hostname,
|
|
150
|
+
url: candidateUrl,
|
|
151
|
+
startedAt: health.startedAt,
|
|
152
|
+
};
|
|
153
|
+
let controlFileRepaired = false;
|
|
154
|
+
try {
|
|
155
|
+
writeFileSync(resolve(".agentcorp/daemon.json"), JSON.stringify(info, null, 2), "utf8");
|
|
156
|
+
controlFileRepaired = true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Best effort
|
|
160
|
+
}
|
|
161
|
+
return { info, health, controlFileRepaired };
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function openBroker(options) {
|
|
168
|
+
const config = loadOrgConfig(options.config);
|
|
169
|
+
const db = new AgentCorpDatabase(options.db, {
|
|
170
|
+
...(config.limits?.default_page_size !== undefined ? { defaultPageSize: config.limits.default_page_size } : {}),
|
|
171
|
+
...(config.limits?.max_page_size !== undefined ? { maxPageSize: config.limits.max_page_size } : {}),
|
|
172
|
+
});
|
|
173
|
+
return { broker: new AgentCorpBroker(config, db), db };
|
|
174
|
+
}
|
|
175
|
+
async function getAdminClient(options) {
|
|
176
|
+
const live = await resolveDaemonInfo();
|
|
177
|
+
if (live) {
|
|
178
|
+
const credPath = options.credentials ?? resolveDefaultPath({ configPath: options.config, dbPath: options.db }, "credentials.json");
|
|
179
|
+
const creds = loadCredentials(credPath);
|
|
180
|
+
const adminToken = creds?.adminToken;
|
|
181
|
+
if (adminToken) {
|
|
182
|
+
const headers = {
|
|
183
|
+
Authorization: `Bearer ${adminToken}`,
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
isDaemon: true,
|
|
188
|
+
listApprovals: async () => (await (await fetch(`${live.info.url}/api/approvals`, { headers })).json()),
|
|
189
|
+
approve: async (id, note, payload) => await (await fetch(`${live.info.url}/api/approvals/${id}/approve`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers,
|
|
192
|
+
body: JSON.stringify({ note, payload }),
|
|
193
|
+
})).json(),
|
|
194
|
+
reject: async (id, note) => await (await fetch(`${live.info.url}/api/approvals/${id}/reject`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers,
|
|
197
|
+
body: JSON.stringify({ note }),
|
|
198
|
+
})).json(),
|
|
199
|
+
listPolicies: async () => (await (await fetch(`${live.info.url}/api/policies`, { headers })).json()),
|
|
200
|
+
savePolicy: async (rule) => await (await fetch(`${live.info.url}/api/policies`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers,
|
|
203
|
+
body: JSON.stringify(rule),
|
|
204
|
+
})).json(),
|
|
205
|
+
setPolicyEnabled: async (id, enabled) => await (await fetch(`${live.info.url}/api/policies/${id}/${enabled ? "enable" : "disable"}`, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers,
|
|
208
|
+
})).json(),
|
|
209
|
+
prune: async (olderThanDays, dryRun, deleteArtifacts) => await (await fetch(`${live.info.url}/api/maintenance/prune`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers,
|
|
212
|
+
body: JSON.stringify({ olderThanDays, dryRun, deleteArtifacts }),
|
|
213
|
+
})).json(),
|
|
214
|
+
compact: async () => await (await fetch(`${live.info.url}/api/maintenance/compact`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers,
|
|
217
|
+
})).json(),
|
|
218
|
+
close: () => { },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const { broker, db } = openBroker(options);
|
|
223
|
+
return {
|
|
224
|
+
isDaemon: false,
|
|
225
|
+
listApprovals: async () => broker.listPendingApprovals(),
|
|
226
|
+
approve: async (id, note, payload) => broker.approve(id, note, payload),
|
|
227
|
+
reject: async (id, note) => broker.reject(id, note),
|
|
228
|
+
listPolicies: async () => broker.listPolicies(),
|
|
229
|
+
savePolicy: async (rule) => broker.savePolicy(rule),
|
|
230
|
+
setPolicyEnabled: async (id, enabled) => broker.setPolicyEnabled(id, enabled),
|
|
231
|
+
prune: async (olderThanDays, dryRun, deleteArtifacts) => broker.prune({ olderThanDays, dryRun, deleteArtifacts }),
|
|
232
|
+
compact: async () => broker.checkpointAndCompact(),
|
|
233
|
+
close: () => db.close(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function openInBrowser(url) {
|
|
237
|
+
const plat = process.platform;
|
|
238
|
+
try {
|
|
239
|
+
if (plat === "win32") {
|
|
240
|
+
spawn("cmd.exe", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
241
|
+
}
|
|
242
|
+
else if (plat === "darwin") {
|
|
243
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Best-effort browser launch
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function getTui(options) {
|
|
254
|
+
const live = await resolveDaemonInfo();
|
|
255
|
+
if (live) {
|
|
256
|
+
const creds = loadCredentials();
|
|
257
|
+
return {
|
|
258
|
+
tui: new AgentCorpTui({
|
|
259
|
+
daemonUrl: live.info.url,
|
|
260
|
+
adminToken: creds?.adminToken,
|
|
261
|
+
}),
|
|
262
|
+
close: () => { },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const { broker, db } = openBroker(options);
|
|
266
|
+
return {
|
|
267
|
+
tui: new AgentCorpTui({ broker }),
|
|
268
|
+
close: () => db.close(),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const program = new Command()
|
|
272
|
+
.name("agentcorp")
|
|
273
|
+
.description("Coordinate role-based AI agent teams over MCP")
|
|
274
|
+
.version("0.1.0-alpha.1")
|
|
275
|
+
.option("--config <path>", "organization configuration", "org.toml")
|
|
276
|
+
.option("--db <path>", "SQLite broker database", ".agentcorp/agentcorp.db")
|
|
277
|
+
.option("--credentials <path>", "credentials file path");
|
|
278
|
+
program
|
|
279
|
+
.command("init")
|
|
280
|
+
.description("Create starter org.toml and initialize credentials")
|
|
281
|
+
.option("--force", "overwrite an existing org.toml")
|
|
282
|
+
.action((options) => {
|
|
283
|
+
const target = resolve(program.opts().config);
|
|
284
|
+
if (existsSync(target) && !options.force) {
|
|
285
|
+
throw new AgentCorpError("CONFIG_EXISTS", `${target} already exists; use --force to replace it`);
|
|
286
|
+
}
|
|
287
|
+
writeFileSync(target, SAMPLE_ORG, "utf8");
|
|
288
|
+
const config = loadOrgConfig(target);
|
|
289
|
+
const creds = ensureCredentials(config);
|
|
290
|
+
output({
|
|
291
|
+
created: target,
|
|
292
|
+
credentials: ".agentcorp/credentials.json",
|
|
293
|
+
roles: Object.keys(creds.roleTokens),
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
program
|
|
297
|
+
.command("validate")
|
|
298
|
+
.description("Validate org.toml and its role graph")
|
|
299
|
+
.action(() => {
|
|
300
|
+
const config = loadOrgConfig(program.opts().config);
|
|
301
|
+
output({ valid: true, company: config.company.name, roles: config.roles.map((role) => role.id) });
|
|
302
|
+
});
|
|
303
|
+
program
|
|
304
|
+
.command("start")
|
|
305
|
+
.description("Start the central local broker daemon")
|
|
306
|
+
.option("--port <number>", "HTTP port to listen on; 0 selects an available port", "0")
|
|
307
|
+
.option("--host <string>", "Host address to bind to", "127.0.0.1")
|
|
308
|
+
.option("--daemon", "Run detached in the background")
|
|
309
|
+
.option("--daemon-file <path>", "path to daemon.json control file")
|
|
310
|
+
.action(async (options) => {
|
|
311
|
+
const gOpts = program.opts();
|
|
312
|
+
if (options.daemon) {
|
|
313
|
+
const targetUrl = `http://${options.host}:${options.port}`;
|
|
314
|
+
const live = await resolveDaemonInfo(targetUrl);
|
|
315
|
+
if (live) {
|
|
316
|
+
output({ startedInBackground: false, alreadyRunning: true, ...live.info });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const recorded = readDaemonInfo(options.daemonFile);
|
|
320
|
+
if (recorded && (await isDaemonHealthy(recorded.url))) {
|
|
321
|
+
output({
|
|
322
|
+
startedInBackground: false,
|
|
323
|
+
alreadyRunning: true,
|
|
324
|
+
...recorded,
|
|
325
|
+
identityVerified: false,
|
|
326
|
+
message: "Daemon is healthy but predates live identity reporting; restart it before relying on PID control",
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const args = [
|
|
331
|
+
resolve(process.argv[1] ?? "dist/cli.js"),
|
|
332
|
+
"start",
|
|
333
|
+
"--port",
|
|
334
|
+
options.port,
|
|
335
|
+
"--host",
|
|
336
|
+
options.host,
|
|
337
|
+
"--config",
|
|
338
|
+
resolve(gOpts.config),
|
|
339
|
+
"--db",
|
|
340
|
+
resolve(gOpts.db),
|
|
341
|
+
];
|
|
342
|
+
if (options.daemonFile) {
|
|
343
|
+
args.push("--daemon-file", resolve(options.daemonFile));
|
|
344
|
+
}
|
|
345
|
+
if (gOpts.credentials) {
|
|
346
|
+
args.push("--credentials", resolve(gOpts.credentials));
|
|
347
|
+
}
|
|
348
|
+
const child = spawn(process.execPath, args, {
|
|
349
|
+
detached: true,
|
|
350
|
+
stdio: "ignore",
|
|
351
|
+
windowsHide: true,
|
|
352
|
+
env: process.env,
|
|
353
|
+
});
|
|
354
|
+
child.unref();
|
|
355
|
+
const deadline = Date.now() + 6000;
|
|
356
|
+
while (Date.now() < deadline) {
|
|
357
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 150));
|
|
358
|
+
const spawnedLive = await resolveDaemonInfo(targetUrl);
|
|
359
|
+
if (spawnedLive) {
|
|
360
|
+
output({ startedInBackground: true, ...spawnedLive.info });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
throw new AgentCorpError("DAEMON_SPAWN_FAILED", `Background daemon process ${child.pid ?? "unknown"} did not become healthy within 6 seconds. ` +
|
|
365
|
+
"Inspect .agentcorp/daemon.log or run 'agentcorp start' without --daemon to see the startup error.");
|
|
366
|
+
}
|
|
367
|
+
const { broker } = openBroker(gOpts);
|
|
368
|
+
const credPath = gOpts.credentials ?? resolveDefaultPath({ configPath: gOpts.config, dbPath: gOpts.db }, "credentials.json");
|
|
369
|
+
const creds = ensureCredentials(broker.config, credPath);
|
|
370
|
+
const targetDaemonFile = options.daemonFile
|
|
371
|
+
? resolve(options.daemonFile)
|
|
372
|
+
: resolveDefaultPath({ configPath: gOpts.config, dbPath: gOpts.db }, "daemon.json");
|
|
373
|
+
const server = new AgentCorpServer(broker, creds, {
|
|
374
|
+
port: parseInt(options.port, 10),
|
|
375
|
+
host: options.host,
|
|
376
|
+
daemonFilePath: targetDaemonFile,
|
|
377
|
+
});
|
|
378
|
+
const daemonLogDir = dirname(targetDaemonFile);
|
|
379
|
+
const daemonLogger = new RotatingLogger(resolve(daemonLogDir, "daemon.log"));
|
|
380
|
+
daemonLogger.write(`AgentCorp daemon starting on ${options.host}:${options.port} (PID: ${process.pid}, config: ${gOpts.config}, db: ${gOpts.db})`);
|
|
381
|
+
broker.on("event", (evt) => {
|
|
382
|
+
const sanitized = sanitizeBrokerEventForLog(evt);
|
|
383
|
+
daemonLogger.write(`[event:${evt.type}] ${JSON.stringify(sanitized)}`);
|
|
384
|
+
});
|
|
385
|
+
let info;
|
|
386
|
+
try {
|
|
387
|
+
info = await server.start();
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
391
|
+
daemonLogger.write(`AgentCorp daemon failed to start (PID: ${process.pid}): ${message}`);
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
daemonLogger.write(`AgentCorp daemon listening at ${info.url}`);
|
|
395
|
+
console.error(`AgentCorp Daemon started at ${info.url} (PID: ${info.pid})`);
|
|
396
|
+
output({
|
|
397
|
+
status: "running",
|
|
398
|
+
...info,
|
|
399
|
+
adminToken: creds.adminToken,
|
|
400
|
+
roles: Object.keys(creds.roleTokens),
|
|
401
|
+
});
|
|
402
|
+
const shutdown = async () => {
|
|
403
|
+
console.error("\nShutting down AgentCorp daemon...");
|
|
404
|
+
daemonLogger.write(`AgentCorp daemon shut down cleanly (PID: ${process.pid})`);
|
|
405
|
+
await server.stop();
|
|
406
|
+
process.exit(0);
|
|
407
|
+
};
|
|
408
|
+
process.on("SIGINT", () => void shutdown());
|
|
409
|
+
process.on("SIGTERM", () => void shutdown());
|
|
410
|
+
});
|
|
411
|
+
program
|
|
412
|
+
.command("stop")
|
|
413
|
+
.description("Stop the running central local broker daemon")
|
|
414
|
+
.action(async () => {
|
|
415
|
+
const live = await resolveDaemonInfo();
|
|
416
|
+
if (!live) {
|
|
417
|
+
const info = readDaemonInfo();
|
|
418
|
+
const healthyLegacyDaemon = info ? await isDaemonHealthy(info.url) : false;
|
|
419
|
+
output({
|
|
420
|
+
stopped: false,
|
|
421
|
+
message: healthyLegacyDaemon
|
|
422
|
+
? "Daemon is healthy but does not report a verifiable PID; refusing unsafe process termination"
|
|
423
|
+
: "No running daemon recorded",
|
|
424
|
+
recordedPid: info?.pid,
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
process.kill(live.info.pid, "SIGTERM");
|
|
430
|
+
output({ stopped: true, pid: live.info.pid, controlFileRepaired: live.controlFileRepaired });
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
output({ stopped: false, message: String(err) });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
program
|
|
437
|
+
.command("status")
|
|
438
|
+
.description("Check daemon status and health")
|
|
439
|
+
.action(async () => {
|
|
440
|
+
const live = await resolveDaemonInfo();
|
|
441
|
+
if (!live) {
|
|
442
|
+
const info = readDaemonInfo();
|
|
443
|
+
const healthyLegacyDaemon = info ? await isDaemonHealthy(info.url) : false;
|
|
444
|
+
output({
|
|
445
|
+
status: healthyLegacyDaemon ? "running" : "stopped",
|
|
446
|
+
...(info ?? {}),
|
|
447
|
+
identityVerified: false,
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
output({
|
|
452
|
+
status: "running",
|
|
453
|
+
...live.info,
|
|
454
|
+
health: live.health,
|
|
455
|
+
controlFileRepaired: live.controlFileRepaired,
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
program
|
|
459
|
+
.command("doctor")
|
|
460
|
+
.description("Perform comprehensive self-healing and observability diagnostics")
|
|
461
|
+
.action(async () => {
|
|
462
|
+
const gOpts = program.opts();
|
|
463
|
+
const report = await runDoctor({
|
|
464
|
+
configPath: gOpts.config,
|
|
465
|
+
dbPath: gOpts.db,
|
|
466
|
+
});
|
|
467
|
+
output(report);
|
|
468
|
+
});
|
|
469
|
+
program
|
|
470
|
+
.command("mcp")
|
|
471
|
+
.description("Run a role-bound AgentCorp MCP server over stdio (proxies to central daemon)")
|
|
472
|
+
.requiredOption("--role <id>", "role identity for this connection")
|
|
473
|
+
.option("--no-spawn", "do not auto-start daemon if it is not running")
|
|
474
|
+
.option("--daemon-url <url>", "explicit daemon URL to connect to")
|
|
475
|
+
.action(async (options) => {
|
|
476
|
+
const gOpts = program.opts();
|
|
477
|
+
await runStdioAdapter({
|
|
478
|
+
role: options.role,
|
|
479
|
+
configPath: gOpts.config,
|
|
480
|
+
dbPath: gOpts.db,
|
|
481
|
+
credentialsPath: gOpts.credentials,
|
|
482
|
+
noSpawn: options.spawn === false,
|
|
483
|
+
daemonUrl: options.daemonUrl,
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
program
|
|
487
|
+
.command("console")
|
|
488
|
+
.description("Launch terminal-native human console or open web dashboard")
|
|
489
|
+
.option("--browser", "Open the modern web dashboard in your default browser")
|
|
490
|
+
.option("--web", "Alias for --browser")
|
|
491
|
+
.action(async (options) => {
|
|
492
|
+
const gOpts = program.opts();
|
|
493
|
+
if (options.browser || options.web) {
|
|
494
|
+
const daemon = await ensureDaemonRunning({
|
|
495
|
+
configPath: gOpts.config,
|
|
496
|
+
dbPath: gOpts.db,
|
|
497
|
+
});
|
|
498
|
+
const consoleUrl = `${daemon.url}/console`;
|
|
499
|
+
console.log(`\nOpening Web Dashboard at: ${consoleUrl}\n`);
|
|
500
|
+
openInBrowser(consoleUrl);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const { tui, close } = await getTui(gOpts);
|
|
504
|
+
try {
|
|
505
|
+
const summary = await tui.runDashboard();
|
|
506
|
+
if (summary.pendingCount > 0) {
|
|
507
|
+
const rl = createInterface({ input, output: outputStream });
|
|
508
|
+
try {
|
|
509
|
+
const answer = (await rl.question(" Review pending approvals now? [Y/n] ")).trim().toLowerCase();
|
|
510
|
+
if (answer === "" || answer === "y" || answer === "yes") {
|
|
511
|
+
console.log("");
|
|
512
|
+
await tui.runReview();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
rl.close();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
close();
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
program
|
|
525
|
+
.command("review")
|
|
526
|
+
.description("Interactive terminal-native approval review (alias for 'agentcorp approvals review')")
|
|
527
|
+
.action(async () => {
|
|
528
|
+
const { tui, close } = await getTui(program.opts());
|
|
529
|
+
try {
|
|
530
|
+
await tui.runReview();
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
close();
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
const approvals = program.command("approvals").description("Human approval console commands");
|
|
537
|
+
approvals
|
|
538
|
+
.command("review")
|
|
539
|
+
.description("Interactive terminal-native human approval review loop")
|
|
540
|
+
.action(async () => {
|
|
541
|
+
const { tui, close } = await getTui(program.opts());
|
|
542
|
+
try {
|
|
543
|
+
await tui.runReview();
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
close();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
approvals
|
|
550
|
+
.command("list")
|
|
551
|
+
.description("List pending approvals")
|
|
552
|
+
.action(async () => {
|
|
553
|
+
const admin = await getAdminClient(program.opts());
|
|
554
|
+
try {
|
|
555
|
+
output(await admin.listApprovals());
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
admin.close();
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
approvals
|
|
562
|
+
.command("approve")
|
|
563
|
+
.description("Approve a pending message or task transition")
|
|
564
|
+
.argument("<approval-id>")
|
|
565
|
+
.option("--note <text>", "decision note")
|
|
566
|
+
.option("--payload <json>", "edited message payload as JSON")
|
|
567
|
+
.action(async (approvalId, options) => {
|
|
568
|
+
const admin = await getAdminClient(program.opts());
|
|
569
|
+
try {
|
|
570
|
+
const payload = options.payload === undefined ? undefined : JSON.parse(options.payload);
|
|
571
|
+
output(await admin.approve(approvalId, options.note, payload));
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
admin.close();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
approvals
|
|
578
|
+
.command("reject")
|
|
579
|
+
.description("Reject a pending message or task transition")
|
|
580
|
+
.argument("<approval-id>")
|
|
581
|
+
.option("--note <text>", "decision note")
|
|
582
|
+
.action(async (approvalId, options) => {
|
|
583
|
+
const admin = await getAdminClient(program.opts());
|
|
584
|
+
try {
|
|
585
|
+
output(await admin.reject(approvalId, options.note));
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
admin.close();
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
const policies = program.command("policies").description("Manage runtime approval policies");
|
|
592
|
+
policies
|
|
593
|
+
.command("list")
|
|
594
|
+
.description("List approval policies")
|
|
595
|
+
.action(async () => {
|
|
596
|
+
const admin = await getAdminClient(program.opts());
|
|
597
|
+
try {
|
|
598
|
+
output(await admin.listPolicies());
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
admin.close();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
policies
|
|
605
|
+
.command("set")
|
|
606
|
+
.description("Create or replace a policy from a JSON object")
|
|
607
|
+
.argument("<json>", "policy JSON")
|
|
608
|
+
.action(async (raw) => {
|
|
609
|
+
const admin = await getAdminClient(program.opts());
|
|
610
|
+
try {
|
|
611
|
+
output(await admin.savePolicy(JSON.parse(raw)));
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
admin.close();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
policies
|
|
618
|
+
.command("enable")
|
|
619
|
+
.description("Enable a policy")
|
|
620
|
+
.argument("<policy-id>")
|
|
621
|
+
.action(async (policyId) => {
|
|
622
|
+
const admin = await getAdminClient(program.opts());
|
|
623
|
+
try {
|
|
624
|
+
output(await admin.setPolicyEnabled(policyId, true));
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
admin.close();
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
policies
|
|
631
|
+
.command("disable")
|
|
632
|
+
.description("Disable a policy without deleting its history")
|
|
633
|
+
.argument("<policy-id>")
|
|
634
|
+
.action(async (policyId) => {
|
|
635
|
+
const admin = await getAdminClient(program.opts());
|
|
636
|
+
try {
|
|
637
|
+
output(await admin.setPolicyEnabled(policyId, false));
|
|
638
|
+
}
|
|
639
|
+
finally {
|
|
640
|
+
admin.close();
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
const audit = program.command("audit").description("Audit export commands");
|
|
644
|
+
audit
|
|
645
|
+
.command("export")
|
|
646
|
+
.description("Export human-readable Markdown and JSON audit trail to /coord")
|
|
647
|
+
.option("--out <dir>", "output directory", "coord")
|
|
648
|
+
.option("--limit <number>", "limit exported entries per category")
|
|
649
|
+
.option("--since <iso-date>", "only export entries created/updated since ISO timestamp")
|
|
650
|
+
.action((options) => {
|
|
651
|
+
const { broker, db } = openBroker(program.opts());
|
|
652
|
+
try {
|
|
653
|
+
const result = exportAuditTrail(broker, options.out, {
|
|
654
|
+
limit: options.limit ? parseInt(options.limit, 10) : undefined,
|
|
655
|
+
since: options.since,
|
|
656
|
+
});
|
|
657
|
+
output({
|
|
658
|
+
exported: true,
|
|
659
|
+
markdownPath: result.markdownPath,
|
|
660
|
+
jsonPath: result.jsonPath,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
db.close();
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
program
|
|
668
|
+
.command("prune")
|
|
669
|
+
.description("Prune resolved history older than specified days (defaults to dry-run)")
|
|
670
|
+
.option("--older-than <days>", "age in days of resolved tasks/messages to prune", "30")
|
|
671
|
+
.option("--execute", "perform live deletion (defaults to safe simulation without deleting)")
|
|
672
|
+
.option("--delete-artifacts", "delete associated artifacts instead of detaching them")
|
|
673
|
+
.option("--compact", "run WAL checkpoint and VACUUM after pruning")
|
|
674
|
+
.action(async (options) => {
|
|
675
|
+
const admin = await getAdminClient(program.opts());
|
|
676
|
+
try {
|
|
677
|
+
const olderThanDays = parseInt(options.olderThan, 10);
|
|
678
|
+
const dryRun = !options.execute;
|
|
679
|
+
const pruneResult = await admin.prune(olderThanDays, dryRun, options.deleteArtifacts);
|
|
680
|
+
let compactResult = undefined;
|
|
681
|
+
if (options.compact && !dryRun) {
|
|
682
|
+
compactResult = await admin.compact();
|
|
683
|
+
}
|
|
684
|
+
output({
|
|
685
|
+
...pruneResult,
|
|
686
|
+
...(dryRun ? { notice: "Dry run completed safely without deleting data. Pass --execute to delete records." } : {}),
|
|
687
|
+
...(compactResult ? { compact: compactResult } : {}),
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
admin.close();
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
program
|
|
695
|
+
.command("compact")
|
|
696
|
+
.description("Run SQLite WAL checkpoint (TRUNCATE) and VACUUM to reclaim disk space")
|
|
697
|
+
.action(async () => {
|
|
698
|
+
const admin = await getAdminClient(program.opts());
|
|
699
|
+
try {
|
|
700
|
+
output(await admin.compact());
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
admin.close();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
program.parseAsync().catch((error) => {
|
|
707
|
+
const code = error instanceof AgentCorpError ? error.code : "UNEXPECTED_ERROR";
|
|
708
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
709
|
+
process.stderr.write(`${JSON.stringify({ error: code, message })}\n`);
|
|
710
|
+
process.exitCode = 1;
|
|
711
|
+
});
|
|
712
|
+
//# sourceMappingURL=cli.js.map
|