@aiaiai-pt/martha-cli 0.51.7 → 0.53.0
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 +72 -1
- package/dist/index.js +1257 -34
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9339,7 +9339,7 @@ async function loginClientCredentials(opts) {
|
|
|
9339
9339
|
});
|
|
9340
9340
|
if (!res.ok) {
|
|
9341
9341
|
const text = await res.text();
|
|
9342
|
-
throw new
|
|
9342
|
+
throw new CLIError(`Client credentials login failed (${res.status}): ${text}`, 2 /* Auth */, "Check MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET and that the profile's keycloak_url + keycloak_realm match the principal.");
|
|
9343
9343
|
}
|
|
9344
9344
|
const data = await res.json();
|
|
9345
9345
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -9356,6 +9356,7 @@ async function loginClientCredentials(opts) {
|
|
|
9356
9356
|
}
|
|
9357
9357
|
var init_client_credentials = __esm(() => {
|
|
9358
9358
|
init_token_store();
|
|
9359
|
+
init_errors();
|
|
9359
9360
|
});
|
|
9360
9361
|
|
|
9361
9362
|
// src/lib/sse.ts
|
|
@@ -10726,6 +10727,13 @@ async function refreshAccessToken(opts) {
|
|
|
10726
10727
|
// src/lib/context.ts
|
|
10727
10728
|
init_api_client();
|
|
10728
10729
|
init_errors();
|
|
10730
|
+
function belongsToClient(tokens, clientId) {
|
|
10731
|
+
if (tokens.auth_type !== "client_credentials")
|
|
10732
|
+
return false;
|
|
10733
|
+
const claims = decodeJwtPayload(tokens.access_token);
|
|
10734
|
+
const azp = claims?.azp ?? claims?.client_id;
|
|
10735
|
+
return azp ? azp === clientId : true;
|
|
10736
|
+
}
|
|
10729
10737
|
function createContext(opts) {
|
|
10730
10738
|
const config = loadConfig(opts.configDir);
|
|
10731
10739
|
const profileName = opts.profileOverride ?? process.env.MARTHA_PROFILE ?? config.current_profile;
|
|
@@ -10743,8 +10751,9 @@ function createContext(opts) {
|
|
|
10743
10751
|
if (envClientId && envSecret) {
|
|
10744
10752
|
if (!tokenStore.needsRefresh(profileName)) {
|
|
10745
10753
|
const existing = tokenStore.getTokens(profileName);
|
|
10746
|
-
if (existing)
|
|
10754
|
+
if (existing && belongsToClient(existing, envClientId)) {
|
|
10747
10755
|
return existing.access_token;
|
|
10756
|
+
}
|
|
10748
10757
|
}
|
|
10749
10758
|
const { loginClientCredentials: loginClientCredentials2 } = await Promise.resolve().then(() => (init_client_credentials(), exports_client_credentials));
|
|
10750
10759
|
const tokens2 = await loginClientCredentials2({
|
|
@@ -11120,7 +11129,7 @@ init_config();
|
|
|
11120
11129
|
init_errors();
|
|
11121
11130
|
|
|
11122
11131
|
// src/version.ts
|
|
11123
|
-
var CLI_VERSION = "0.
|
|
11132
|
+
var CLI_VERSION = "0.53.0";
|
|
11124
11133
|
|
|
11125
11134
|
// src/commands/sessions.ts
|
|
11126
11135
|
function relativeTime(iso) {
|
|
@@ -11396,6 +11405,48 @@ Then drive it from chat:`));
|
|
|
11396
11405
|
console.log(source_default.dim(`
|
|
11397
11406
|
Check it's connected any time: martha agents doctor ${r.agent}`));
|
|
11398
11407
|
}
|
|
11408
|
+
function printChecks(checks, write = (line) => console.log(line)) {
|
|
11409
|
+
for (const c of checks) {
|
|
11410
|
+
const glyph = c.ok === true ? source_default.green("PASS") : c.ok === false ? source_default.red("FAIL") : source_default.yellow("WARN");
|
|
11411
|
+
const detail = c.detail ? source_default.dim(c.detail) : "";
|
|
11412
|
+
write(` ${glyph} ${c.label.padEnd(32)} ${detail}`);
|
|
11413
|
+
if (c.ok !== true && c.fix) {
|
|
11414
|
+
write(` ${source_default.dim("→ " + c.fix)}`);
|
|
11415
|
+
}
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11418
|
+
var CLIENT_SCAN_CAP = 200;
|
|
11419
|
+
var CLIENT_SCAN_CONCURRENCY = 8;
|
|
11420
|
+
async function scanClientsForAgent(ctx, agentName, opts = {}) {
|
|
11421
|
+
const subPath = opts.subPath ?? "agents";
|
|
11422
|
+
const clientsResp = await ctx.api.get("/api/admin/clients");
|
|
11423
|
+
const clients = Array.isArray(clientsResp) ? clientsResp : clientsResp?.clients ?? clientsResp?.items ?? [];
|
|
11424
|
+
const target = clients.slice(0, CLIENT_SCAN_CAP);
|
|
11425
|
+
const granting = [];
|
|
11426
|
+
let failed = false;
|
|
11427
|
+
for (let i = 0;i < target.length; i += CLIENT_SCAN_CONCURRENCY) {
|
|
11428
|
+
const batch = target.slice(i, i + CLIENT_SCAN_CONCURRENCY);
|
|
11429
|
+
const results = await Promise.all(batch.map(async (c) => {
|
|
11430
|
+
try {
|
|
11431
|
+
const rows = await ctx.api.get(`/api/admin/definitions/clients/${c.id}/${subPath}`);
|
|
11432
|
+
const names = (Array.isArray(rows) ? rows : []).map((r) => String(r.name ?? r.agent_name ?? "")).filter(Boolean);
|
|
11433
|
+
const ids = (Array.isArray(rows) ? rows : []).map((r) => String(r.agent_id ?? r.id ?? ""));
|
|
11434
|
+
const hit = names.includes(agentName) || !!opts.agentId && ids.includes(opts.agentId);
|
|
11435
|
+
return hit ? { client: String(c.name ?? c.id), agents: names } : null;
|
|
11436
|
+
} catch {
|
|
11437
|
+
failed = true;
|
|
11438
|
+
return null;
|
|
11439
|
+
}
|
|
11440
|
+
}));
|
|
11441
|
+
granting.push(...results.filter((r) => !!r));
|
|
11442
|
+
}
|
|
11443
|
+
return {
|
|
11444
|
+
granting,
|
|
11445
|
+
scanned: target.length,
|
|
11446
|
+
total: clients.length,
|
|
11447
|
+
incomplete: failed || clients.length > CLIENT_SCAN_CAP
|
|
11448
|
+
};
|
|
11449
|
+
}
|
|
11399
11450
|
async function diagnoseHostRunner(ctx, name) {
|
|
11400
11451
|
const A = `${API_PATH}/${encodeURIComponent(name)}`;
|
|
11401
11452
|
const checks = [];
|
|
@@ -11446,25 +11497,18 @@ async function diagnoseHostRunner(ctx, name) {
|
|
|
11446
11497
|
detail: hostTools.length ? hostTools.join(", ") : "none",
|
|
11447
11498
|
fix: hostTools.length ? undefined : setupFix
|
|
11448
11499
|
});
|
|
11449
|
-
let
|
|
11500
|
+
let scan = null;
|
|
11450
11501
|
try {
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11455
|
-
const acc = await ctx.api.get(`/api/admin/definitions/clients/${c.id}/agents/access`);
|
|
11456
|
-
const has = (Array.isArray(acc) ? acc : []).some((r) => String(r.agent_name ?? r.name ?? "") === name || String(r.agent_id ?? "") === String(def.id ?? ""));
|
|
11457
|
-
return has ? String(c.name ?? c.id) : null;
|
|
11458
|
-
} catch {
|
|
11459
|
-
return null;
|
|
11460
|
-
}
|
|
11461
|
-
}));
|
|
11462
|
-
grantingClients = probes.filter((x) => !!x);
|
|
11502
|
+
scan = await scanClientsForAgent(ctx, name, {
|
|
11503
|
+
subPath: "agents/access",
|
|
11504
|
+
agentId: String(def.id ?? "")
|
|
11505
|
+
});
|
|
11463
11506
|
} catch {}
|
|
11507
|
+
const grantingClients = scan?.granting.map((g) => g.client) ?? [];
|
|
11464
11508
|
checks.push({
|
|
11465
|
-
ok: grantingClients.length > 0,
|
|
11509
|
+
ok: grantingClients.length > 0 ? true : !scan || scan.incomplete ? null : false,
|
|
11466
11510
|
label: "a chat client can select it",
|
|
11467
|
-
detail: grantingClients.length ? grantingClients.join(", ") : "no client granted access",
|
|
11511
|
+
detail: grantingClients.length ? grantingClients.join(", ") : !scan ? "could not read the client list" : scan.incomplete ? `inconclusive — read ${scan.scanned} of ${scan.total} clients` : "no client granted access",
|
|
11468
11512
|
fix: grantingClients.length ? undefined : setupFix
|
|
11469
11513
|
});
|
|
11470
11514
|
const takeover = def.runs_as_chat === true;
|
|
@@ -11865,14 +11909,7 @@ Attach it as this agent's host executor:
|
|
|
11865
11909
|
}
|
|
11866
11910
|
console.log(source_default.bold(`Martha CLI doctor — host runner: ${agent}
|
|
11867
11911
|
`));
|
|
11868
|
-
|
|
11869
|
-
const glyph = c.ok === true ? source_default.green("PASS") : c.ok === false ? source_default.red("FAIL") : source_default.yellow("WARN");
|
|
11870
|
-
const detail = c.detail ? source_default.dim(c.detail) : "";
|
|
11871
|
-
console.log(` ${glyph} ${c.label.padEnd(32)} ${detail}`);
|
|
11872
|
-
if (c.ok === false && c.fix) {
|
|
11873
|
-
console.log(` ${source_default.dim("→ " + c.fix)}`);
|
|
11874
|
-
}
|
|
11875
|
-
}
|
|
11912
|
+
printChecks(dx.checks);
|
|
11876
11913
|
console.log();
|
|
11877
11914
|
if (dx.drivableFromChat && dx.runnerAttached) {
|
|
11878
11915
|
console.log(source_default.green.bold("Ready — a runner is attached and the agent can be driven from chat."));
|
|
@@ -14209,10 +14246,1030 @@ function parseTimeoutSeconds(raw) {
|
|
|
14209
14246
|
return n;
|
|
14210
14247
|
}
|
|
14211
14248
|
|
|
14249
|
+
// src/commands/acp-serve.ts
|
|
14250
|
+
import { createInterface as createInterface3 } from "node:readline";
|
|
14251
|
+
import { execFile } from "node:child_process";
|
|
14252
|
+
init_errors();
|
|
14253
|
+
function logJson(level, msg, fields) {
|
|
14254
|
+
process.stderr.write(JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields }) + `
|
|
14255
|
+
`);
|
|
14256
|
+
}
|
|
14257
|
+
function extractPromptText(prompt) {
|
|
14258
|
+
if (!Array.isArray(prompt))
|
|
14259
|
+
return "";
|
|
14260
|
+
return prompt.filter((b) => typeof b === "object" && b !== null && b.type === "text" && typeof b.text === "string").map((b) => b.text).join(`
|
|
14261
|
+
|
|
14262
|
+
`);
|
|
14263
|
+
}
|
|
14264
|
+
function parseBuzzReplyTarget(promptText) {
|
|
14265
|
+
const uuid = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
14266
|
+
const named = promptText.match(new RegExp(`Channel:[^\\n]*\\(#(${uuid})\\)`));
|
|
14267
|
+
const bare = promptText.match(new RegExp(`Channel:\\s*(${uuid})`));
|
|
14268
|
+
const channelId = named?.[1] ?? bare?.[1];
|
|
14269
|
+
const instructed = promptText.match(/--reply-to\s+([0-9a-f]{64})/);
|
|
14270
|
+
if (instructed)
|
|
14271
|
+
return { channelId, replyTo: instructed[1] };
|
|
14272
|
+
const steered = promptText.match(/root=([0-9a-f]{64})/);
|
|
14273
|
+
if (steered)
|
|
14274
|
+
return { channelId, replyTo: steered[1] };
|
|
14275
|
+
const newRoot = promptText.match(/Event ID:\s*([0-9a-f]{64})/);
|
|
14276
|
+
if (newRoot)
|
|
14277
|
+
return { channelId, replyTo: newRoot[1] };
|
|
14278
|
+
return { channelId, replyTo: undefined };
|
|
14279
|
+
}
|
|
14280
|
+
function agentMessageChunk(sessionId, text) {
|
|
14281
|
+
return {
|
|
14282
|
+
jsonrpc: "2.0",
|
|
14283
|
+
method: "session/update",
|
|
14284
|
+
params: {
|
|
14285
|
+
sessionId,
|
|
14286
|
+
update: {
|
|
14287
|
+
sessionUpdate: "agent_message_chunk",
|
|
14288
|
+
content: { type: "text", text }
|
|
14289
|
+
}
|
|
14290
|
+
}
|
|
14291
|
+
};
|
|
14292
|
+
}
|
|
14293
|
+
function classifyPrompt(promptText) {
|
|
14294
|
+
const kinds = [...promptText.matchAll(/^\s*Kind:\s*(\d+)/gm)].map((m) => Number(m[1]));
|
|
14295
|
+
if (kinds.length > 0 && kinds.every((k) => k === 7))
|
|
14296
|
+
return "reaction";
|
|
14297
|
+
return "message";
|
|
14298
|
+
}
|
|
14299
|
+
function captureDeliveryTarget(promptText) {
|
|
14300
|
+
const { channelId, replyTo } = parseBuzzReplyTarget(promptText);
|
|
14301
|
+
if (!channelId)
|
|
14302
|
+
return null;
|
|
14303
|
+
return { channelId, threadRef: replyTo };
|
|
14304
|
+
}
|
|
14305
|
+
async function deliverToChannel(buzzBin, target, text) {
|
|
14306
|
+
const args = ["messages", "send", "--channel", target.channelId, "--content", text];
|
|
14307
|
+
if (target.threadRef)
|
|
14308
|
+
args.push("--reply-to", target.threadRef);
|
|
14309
|
+
const send = () => new Promise((resolve2) => {
|
|
14310
|
+
execFile(buzzBin, args, { timeout: 20000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
14311
|
+
resolve2({
|
|
14312
|
+
ok: !err,
|
|
14313
|
+
out: stdout?.slice(0, 2000) ?? "",
|
|
14314
|
+
err: err ? `${err.message}
|
|
14315
|
+
${stderr ?? ""}`.slice(0, 2000) : ""
|
|
14316
|
+
});
|
|
14317
|
+
});
|
|
14318
|
+
});
|
|
14319
|
+
let res = await send();
|
|
14320
|
+
if (!res.ok) {
|
|
14321
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
14322
|
+
res = await send();
|
|
14323
|
+
}
|
|
14324
|
+
if (res.ok) {
|
|
14325
|
+
logJson("info", "delivered to channel", {
|
|
14326
|
+
channel: target.channelId,
|
|
14327
|
+
thread_ref: target.threadRef,
|
|
14328
|
+
output: res.out
|
|
14329
|
+
});
|
|
14330
|
+
} else {
|
|
14331
|
+
logJson("error", "delivery failed — answer NOT relocated", {
|
|
14332
|
+
channel: target.channelId,
|
|
14333
|
+
thread_ref: target.threadRef,
|
|
14334
|
+
error: res.err,
|
|
14335
|
+
undelivered: text.slice(0, 4000)
|
|
14336
|
+
});
|
|
14337
|
+
}
|
|
14338
|
+
}
|
|
14339
|
+
async function runAcpServer(ctx, input, output, opts) {
|
|
14340
|
+
const sessions = new Map;
|
|
14341
|
+
let writeChain = Promise.resolve();
|
|
14342
|
+
const writeLine = (msg) => {
|
|
14343
|
+
writeChain = writeChain.then(() => new Promise((resolve2) => {
|
|
14344
|
+
output.write(JSON.stringify(msg) + `
|
|
14345
|
+
`, () => resolve2());
|
|
14346
|
+
}));
|
|
14347
|
+
return writeChain;
|
|
14348
|
+
};
|
|
14349
|
+
const respond = (id, result) => writeLine({ jsonrpc: "2.0", id, result });
|
|
14350
|
+
const respondError = (id, code, message) => writeLine({ jsonrpc: "2.0", id, error: { code, message } });
|
|
14351
|
+
async function handlePrompt(id, params) {
|
|
14352
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
|
|
14353
|
+
const session = sessions.get(sessionId);
|
|
14354
|
+
if (!session) {
|
|
14355
|
+
await respondError(id, -32602, `unknown sessionId: ${sessionId}`);
|
|
14356
|
+
return;
|
|
14357
|
+
}
|
|
14358
|
+
if (session.inFlight) {
|
|
14359
|
+
await respondError(id, -32000, "another prompt is already in flight for this session");
|
|
14360
|
+
return;
|
|
14361
|
+
}
|
|
14362
|
+
const text = extractPromptText(params.prompt);
|
|
14363
|
+
if (!text) {
|
|
14364
|
+
await respondError(id, -32602, "prompt contains no text content blocks");
|
|
14365
|
+
return;
|
|
14366
|
+
}
|
|
14367
|
+
if (classifyPrompt(text) === "reaction") {
|
|
14368
|
+
logJson("info", "reaction observed — feedback, not a request", {
|
|
14369
|
+
session_id: sessionId,
|
|
14370
|
+
excerpt: text.slice(0, 300)
|
|
14371
|
+
});
|
|
14372
|
+
await respond(id, { stopReason: "end_turn" });
|
|
14373
|
+
return;
|
|
14374
|
+
}
|
|
14375
|
+
const abort = new AbortController;
|
|
14376
|
+
session.inFlight = abort;
|
|
14377
|
+
logJson("info", "session/prompt started", {
|
|
14378
|
+
session_id: sessionId,
|
|
14379
|
+
prompt_chars: text.length
|
|
14380
|
+
});
|
|
14381
|
+
try {
|
|
14382
|
+
const result = await sendMessage(ctx, sessionId, text, {
|
|
14383
|
+
signal: abort.signal,
|
|
14384
|
+
clientId: opts.clientId,
|
|
14385
|
+
agentName: opts.agentName,
|
|
14386
|
+
timeoutMs: opts.timeoutMs,
|
|
14387
|
+
onToken: (token) => {
|
|
14388
|
+
writeLine(agentMessageChunk(sessionId, token));
|
|
14389
|
+
}
|
|
14390
|
+
});
|
|
14391
|
+
if (result.aborted) {
|
|
14392
|
+
logJson("info", "session/prompt cancelled", { session_id: sessionId });
|
|
14393
|
+
session.inFlight = null;
|
|
14394
|
+
await respond(id, { stopReason: "cancelled" });
|
|
14395
|
+
return;
|
|
14396
|
+
}
|
|
14397
|
+
if (opts.postReply) {
|
|
14398
|
+
const target = captureDeliveryTarget(text);
|
|
14399
|
+
if (target) {
|
|
14400
|
+
await deliverToChannel(opts.buzzBin, target, result.response);
|
|
14401
|
+
} else {
|
|
14402
|
+
logJson("warn", "no channel in prompt — nothing to deliver to");
|
|
14403
|
+
}
|
|
14404
|
+
}
|
|
14405
|
+
logJson("info", "session/prompt completed", {
|
|
14406
|
+
session_id: sessionId,
|
|
14407
|
+
response_chars: result.response.length
|
|
14408
|
+
});
|
|
14409
|
+
session.inFlight = null;
|
|
14410
|
+
await respond(id, { stopReason: "end_turn" });
|
|
14411
|
+
} catch (err) {
|
|
14412
|
+
session.inFlight = null;
|
|
14413
|
+
if (abort.signal.aborted) {
|
|
14414
|
+
await respond(id, { stopReason: "cancelled" });
|
|
14415
|
+
return;
|
|
14416
|
+
}
|
|
14417
|
+
const message = err instanceof MarthaAPIError ? `Martha API ${err.status}: ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
14418
|
+
logJson("error", "session/prompt failed", {
|
|
14419
|
+
session_id: sessionId,
|
|
14420
|
+
error: message
|
|
14421
|
+
});
|
|
14422
|
+
await respondError(id, -32000, message);
|
|
14423
|
+
}
|
|
14424
|
+
}
|
|
14425
|
+
async function dispatch(msg) {
|
|
14426
|
+
const { id, method } = msg;
|
|
14427
|
+
const params = msg.params ?? {};
|
|
14428
|
+
if (!method)
|
|
14429
|
+
return;
|
|
14430
|
+
switch (method) {
|
|
14431
|
+
case "initialize": {
|
|
14432
|
+
const requested = params.protocolVersion;
|
|
14433
|
+
await respond(id, {
|
|
14434
|
+
protocolVersion: typeof requested === "number" ? requested : 1,
|
|
14435
|
+
agentCapabilities: {
|
|
14436
|
+
loadSession: false,
|
|
14437
|
+
promptCapabilities: {
|
|
14438
|
+
image: false,
|
|
14439
|
+
audio: false,
|
|
14440
|
+
embeddedContext: false
|
|
14441
|
+
}
|
|
14442
|
+
},
|
|
14443
|
+
authMethods: [],
|
|
14444
|
+
agentInfo: { name: "martha-acp-serve", version: CLI_VERSION }
|
|
14445
|
+
});
|
|
14446
|
+
logJson("info", "initialized", { client: params.clientInfo });
|
|
14447
|
+
break;
|
|
14448
|
+
}
|
|
14449
|
+
case "session/new": {
|
|
14450
|
+
if (params.systemPrompt) {
|
|
14451
|
+
logJson("warn", "session/new: systemPrompt ignored in v1", {
|
|
14452
|
+
chars: String(params.systemPrompt).length
|
|
14453
|
+
});
|
|
14454
|
+
}
|
|
14455
|
+
try {
|
|
14456
|
+
const sessionId = await createSession(ctx, opts.clientId);
|
|
14457
|
+
sessions.set(sessionId, { inFlight: null });
|
|
14458
|
+
logJson("info", "session created", { session_id: sessionId });
|
|
14459
|
+
await respond(id, { sessionId });
|
|
14460
|
+
} catch (err) {
|
|
14461
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14462
|
+
logJson("error", "session/new failed", { error: message });
|
|
14463
|
+
await respondError(id, -32000, message);
|
|
14464
|
+
}
|
|
14465
|
+
break;
|
|
14466
|
+
}
|
|
14467
|
+
case "session/prompt":
|
|
14468
|
+
handlePrompt(id, params);
|
|
14469
|
+
break;
|
|
14470
|
+
case "session/cancel": {
|
|
14471
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
|
|
14472
|
+
const session = sessions.get(sessionId);
|
|
14473
|
+
if (session?.inFlight) {
|
|
14474
|
+
logJson("info", "session/cancel: aborting in-flight prompt", {
|
|
14475
|
+
session_id: sessionId
|
|
14476
|
+
});
|
|
14477
|
+
session.inFlight.abort();
|
|
14478
|
+
} else {
|
|
14479
|
+
logJson("debug", "session/cancel: nothing in flight", {
|
|
14480
|
+
session_id: sessionId
|
|
14481
|
+
});
|
|
14482
|
+
}
|
|
14483
|
+
break;
|
|
14484
|
+
}
|
|
14485
|
+
case "session/set_config_option":
|
|
14486
|
+
case "session/set_model":
|
|
14487
|
+
logJson("debug", `${method} ignored (v1 fence)`, { params });
|
|
14488
|
+
if (id !== undefined && id !== null)
|
|
14489
|
+
await respond(id, {});
|
|
14490
|
+
break;
|
|
14491
|
+
case "authenticate":
|
|
14492
|
+
if (id !== undefined && id !== null)
|
|
14493
|
+
await respond(id, {});
|
|
14494
|
+
break;
|
|
14495
|
+
default:
|
|
14496
|
+
if (id !== undefined && id !== null) {
|
|
14497
|
+
await respondError(id, -32601, `Method not found: ${method}`);
|
|
14498
|
+
} else {
|
|
14499
|
+
logJson("debug", "ignoring unknown notification", { method });
|
|
14500
|
+
}
|
|
14501
|
+
}
|
|
14502
|
+
}
|
|
14503
|
+
const rl = createInterface3({ input, terminal: false });
|
|
14504
|
+
for await (const line of rl) {
|
|
14505
|
+
const trimmed = line.trim();
|
|
14506
|
+
if (!trimmed)
|
|
14507
|
+
continue;
|
|
14508
|
+
let msg;
|
|
14509
|
+
try {
|
|
14510
|
+
msg = JSON.parse(trimmed);
|
|
14511
|
+
} catch {
|
|
14512
|
+
logJson("warn", "skipping non-JSON input line", {
|
|
14513
|
+
head: trimmed.slice(0, 120)
|
|
14514
|
+
});
|
|
14515
|
+
continue;
|
|
14516
|
+
}
|
|
14517
|
+
await dispatch(msg);
|
|
14518
|
+
}
|
|
14519
|
+
await writeChain;
|
|
14520
|
+
logJson("info", "stdin closed — exiting");
|
|
14521
|
+
}
|
|
14522
|
+
async function probeStartup(ctx) {
|
|
14523
|
+
try {
|
|
14524
|
+
const who = await ctx.api.get("/api/tasks/whoami");
|
|
14525
|
+
logJson("info", "authenticated", { kind: who.kind, subject: who.subject });
|
|
14526
|
+
} catch (err) {
|
|
14527
|
+
if (err instanceof MarthaAPIError && (err.status === 401 || err.status === 403)) {
|
|
14528
|
+
throw new CLIError(`ACP startup failed: Martha API rejected the credential (${err.status}: ${err.message})`, 2 /* Auth */, "Set MARTHA_CLIENT_ID/MARTHA_CLIENT_SECRET (bootstrap integration principal), or MARTHA_TOKEN, or run `martha auth login`.");
|
|
14529
|
+
}
|
|
14530
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14531
|
+
throw new CLIError(`ACP startup failed: cannot reach Martha API at ${ctx.profile.api_url}: ${message}`, 1 /* Error */, "Check the API URL (--api-url / profile) and that the Martha API is up.");
|
|
14532
|
+
}
|
|
14533
|
+
}
|
|
14534
|
+
function registerAcpServeCommand(program2) {
|
|
14535
|
+
program2.command("acp-serve").description("Serve the Agent Client Protocol over stdio (newline-delimited JSON-RPC 2.0) so an ACP harness like Buzz's buzz-acp can drive a Martha agent").option("--client <nameOrId>", "Use a specific client id for the Martha session").option("--agent <name>", "Drive turns with a specific Martha agent (defaults to the credential/profile binding)").option("--post-reply", "After each completed turn, post the final answer back to the triggering Buzz channel via the `buzz` CLI (parsed from the prompt's [Context] section)").option("--buzz-bin <path>", "Path to the buzz CLI for --post-reply", "buzz").option("--timeout <seconds>", "Per-prompt HTTP timeout in seconds (default: 600)").action(async (opts) => {
|
|
14536
|
+
const ctx = createContext({
|
|
14537
|
+
profileOverride: program2.opts().profile,
|
|
14538
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
14539
|
+
verbose: program2.opts().verbose
|
|
14540
|
+
});
|
|
14541
|
+
const timeoutMs = opts.timeout ? Math.max(1, Number(opts.timeout)) * 1000 : undefined;
|
|
14542
|
+
logJson("info", "martha acp-serve starting", {
|
|
14543
|
+
version: CLI_VERSION,
|
|
14544
|
+
api_url: ctx.profile.api_url,
|
|
14545
|
+
post_reply: !!opts.postReply
|
|
14546
|
+
});
|
|
14547
|
+
await probeStartup(ctx);
|
|
14548
|
+
logJson("info", "startup probe ok — speaking ACP on stdio");
|
|
14549
|
+
await runAcpServer(ctx, process.stdin, process.stdout, {
|
|
14550
|
+
clientId: opts.client,
|
|
14551
|
+
agentName: opts.agent,
|
|
14552
|
+
postReply: !!opts.postReply,
|
|
14553
|
+
buzzBin: opts.buzzBin,
|
|
14554
|
+
timeoutMs
|
|
14555
|
+
});
|
|
14556
|
+
});
|
|
14557
|
+
}
|
|
14558
|
+
|
|
14559
|
+
// src/commands/principals.ts
|
|
14560
|
+
init_errors();
|
|
14561
|
+
var AGENTS_PATH = "/api/admin/definitions/agents";
|
|
14562
|
+
var BOOTSTRAP_PATH = "/api/admin/definitions/principals/bootstrap";
|
|
14563
|
+
function parseIntents(raw) {
|
|
14564
|
+
return (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
14565
|
+
}
|
|
14566
|
+
async function bootstrapAgentPrincipal(ctx, agent, opts = {}) {
|
|
14567
|
+
const A = `${AGENTS_PATH}/${encodeURIComponent(agent)}`;
|
|
14568
|
+
const steps = [];
|
|
14569
|
+
const warnings = [];
|
|
14570
|
+
let def = null;
|
|
14571
|
+
try {
|
|
14572
|
+
def = await ctx.api.get(A);
|
|
14573
|
+
} catch (e) {
|
|
14574
|
+
if (!(e instanceof MarthaAPIError && e.status === 404))
|
|
14575
|
+
throw e;
|
|
14576
|
+
}
|
|
14577
|
+
if (!def) {
|
|
14578
|
+
if (!opts.prompt) {
|
|
14579
|
+
throw new CLIError(`Agent '${agent}' not found.`, 3 /* NotFound */, `Pass --prompt "<system prompt>" to create it, or create it first with: martha agents create --name ${agent} --reasoning martha --prompt "..."`);
|
|
14580
|
+
}
|
|
14581
|
+
const deliveryNote = `
|
|
14582
|
+
|
|
14583
|
+
When a message asks you to reply in a channel, reply with the ` + "message content ONLY — delivery is handled for you. Never output " + "delivery commands (e.g. `buzz messages send`).";
|
|
14584
|
+
def = await ctx.api.post(AGENTS_PATH, {
|
|
14585
|
+
name: agent,
|
|
14586
|
+
system_prompt: opts.prompt + deliveryNote,
|
|
14587
|
+
agent_type: "cloud",
|
|
14588
|
+
runs_as_chat: true,
|
|
14589
|
+
description: "Agent principal for external drivers (#1117)"
|
|
14590
|
+
});
|
|
14591
|
+
steps.push(`created Martha-hosted agent '${agent}' (runs_as_chat)`);
|
|
14592
|
+
} else {
|
|
14593
|
+
if (String(def.agent_type ?? "") === "external") {
|
|
14594
|
+
throw new CLIError(`Agent '${agent}' is self-hosted (reasoning runs in your own service). ` + `An agent principal drives a Martha-hosted agent — recreate it with --reasoning martha.`, 4 /* Validation */);
|
|
14595
|
+
}
|
|
14596
|
+
if (def.runs_as_chat !== true) {
|
|
14597
|
+
await ctx.api.put(A, { runs_as_chat: true });
|
|
14598
|
+
steps.push("enabled chat takeover (runs_as_chat)");
|
|
14599
|
+
}
|
|
14600
|
+
}
|
|
14601
|
+
const agentName = String(def?.name ?? agent);
|
|
14602
|
+
const boot = await ctx.api.post(BOOTSTRAP_PATH, {
|
|
14603
|
+
mode: "integration",
|
|
14604
|
+
label: opts.label ?? agentName,
|
|
14605
|
+
intents: parseIntents(opts.intents)
|
|
14606
|
+
});
|
|
14607
|
+
if (!boot.service_account || boot.client_id == null) {
|
|
14608
|
+
throw new CLIError("Bootstrap did not return service-account credentials.", 1 /* Error */, "Check that your user has the admin realm role (integration mode requires it).");
|
|
14609
|
+
}
|
|
14610
|
+
steps.push(boot.adopted ? `reusing principal '${boot.service_account.client_id}' (same secret — recovery, not rotation)` : `provisioned principal '${boot.service_account.client_id}'`);
|
|
14611
|
+
const granted = await ctx.api.get(`/api/admin/definitions/clients/${boot.client_id}/agents`);
|
|
14612
|
+
const names = granted.map((g) => String(g.name ?? g.agent_name ?? ""));
|
|
14613
|
+
if (!names.includes(agentName)) {
|
|
14614
|
+
await ctx.api.post(`/api/admin/definitions/clients/${boot.client_id}/agents`, { agent_name: agentName });
|
|
14615
|
+
steps.push(`bound '${agentName}' to client ${boot.client_id}`);
|
|
14616
|
+
} else {
|
|
14617
|
+
steps.push(`'${agentName}' already bound to client ${boot.client_id}`);
|
|
14618
|
+
}
|
|
14619
|
+
const others = names.filter((n) => n && n !== agentName);
|
|
14620
|
+
if (others.length) {
|
|
14621
|
+
warnings.push(`client ${boot.client_id} also has agent(s) ${others.join(", ")} — chat takeover requires exactly ONE bound agent; revoke the others or use a fresh --label.`);
|
|
14622
|
+
}
|
|
14623
|
+
return {
|
|
14624
|
+
agent: agentName,
|
|
14625
|
+
clientId: boot.client_id,
|
|
14626
|
+
clientKey: boot.client_key ?? boot.service_account.client_id,
|
|
14627
|
+
saClientId: boot.service_account.client_id,
|
|
14628
|
+
saClientSecret: boot.service_account.client_secret,
|
|
14629
|
+
tokenEndpoint: boot.service_account.token_endpoint,
|
|
14630
|
+
steps,
|
|
14631
|
+
warnings
|
|
14632
|
+
};
|
|
14633
|
+
}
|
|
14634
|
+
function printAgentPrincipalReady(r) {
|
|
14635
|
+
console.log(source_default.bold.green(`
|
|
14636
|
+
✓ agent principal ready — the credential acts as '${r.agent}'
|
|
14637
|
+
`));
|
|
14638
|
+
for (const s of r.steps)
|
|
14639
|
+
console.log(source_default.dim(` • ${s}`));
|
|
14640
|
+
for (const w of r.warnings)
|
|
14641
|
+
console.log(source_default.yellow(` ! ${w}`));
|
|
14642
|
+
console.log();
|
|
14643
|
+
console.log(source_default.bold("Service-account secret (shown once — store it like one):"));
|
|
14644
|
+
console.log(source_default.cyan(` export MARTHA_CLIENT_ID=${r.saClientId}
|
|
14645
|
+
` + ` export MARTHA_CLIENT_SECRET=${r.saClientSecret}`));
|
|
14646
|
+
console.log(source_default.dim(" The host also needs a Martha profile (api_url + keycloak): run `martha init` once."));
|
|
14647
|
+
console.log();
|
|
14648
|
+
console.log("Any ACP client can now drive the agent via `martha acp-serve`. For Buzz,\n" + "let the CLI check the preconditions and start the adapter for you:");
|
|
14649
|
+
console.log(source_default.cyan(` export BUZZ_PRIVATE_KEY=<agent nsec> # buzz-admin generate-key
|
|
14650
|
+
` + ` martha channels connect buzz --agent ${r.agent} --relay ws://<relay>:<port>`));
|
|
14651
|
+
console.log(source_default.dim(`
|
|
14652
|
+
It names anything missing (buzz-acp, the buzz CLI, membership, …) and
|
|
14653
|
+
` + ` exits rather than half-starting. Starting from Buzz Desktop instead?
|
|
14654
|
+
` + " `martha channels install-harness buzz`, then pick Martha in New agent."));
|
|
14655
|
+
console.log(source_default.dim(`
|
|
14656
|
+
Then: buzz channels add-member --channel <id> --pubkey <agent pubkey> --role member
|
|
14657
|
+
` + " and @mention the agent in the channel."));
|
|
14658
|
+
}
|
|
14659
|
+
function registerPrincipalCommands(program2) {
|
|
14660
|
+
const principals = program2.command("principals").description("Provision principals — who may act, with what credential (mirrors the API's principals/bootstrap)");
|
|
14661
|
+
principals.command("bootstrap").description("Provision a principal. --mode human: your run-as client (what `martha init` wraps). " + "--mode integration: headless service account. --mode agent: service account bound " + "to ONE Martha-hosted agent (chat takeover) so the credential acts as the agent — " + "the shape an external ACP driver (Buzz, Zed) needs.").requiredOption("--mode <mode>", "human | integration | agent").option("--agent <name>", "Agent to bind (agent mode)").option("--prompt <text>", "System prompt — creates the agent when it doesn't exist yet (agent mode)").option("--label <label>", "Label for the service-account client id (default: the agent name in agent mode)").option("--intents <list>", "Comma-separated tool intents to grant (rag, documents, workflows, tasks)").action(async (opts) => {
|
|
14662
|
+
const ctx = createContext({
|
|
14663
|
+
profileOverride: program2.opts().profile,
|
|
14664
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
14665
|
+
verbose: program2.opts().verbose
|
|
14666
|
+
});
|
|
14667
|
+
const isJson = !!program2.opts().json;
|
|
14668
|
+
switch (opts.mode) {
|
|
14669
|
+
case "agent": {
|
|
14670
|
+
if (!opts.agent) {
|
|
14671
|
+
throw new CLIError("--mode agent requires --agent <name>", 4 /* Validation */);
|
|
14672
|
+
}
|
|
14673
|
+
const result = await bootstrapAgentPrincipal(ctx, opts.agent, {
|
|
14674
|
+
label: opts.label,
|
|
14675
|
+
prompt: opts.prompt,
|
|
14676
|
+
intents: opts.intents
|
|
14677
|
+
});
|
|
14678
|
+
if (isJson) {
|
|
14679
|
+
console.log(JSON.stringify({
|
|
14680
|
+
mode: "agent",
|
|
14681
|
+
agent: result.agent,
|
|
14682
|
+
client_id: result.clientId,
|
|
14683
|
+
client_key: result.clientKey,
|
|
14684
|
+
martha_client_id: result.saClientId,
|
|
14685
|
+
martha_client_secret: result.saClientSecret,
|
|
14686
|
+
token_endpoint: result.tokenEndpoint,
|
|
14687
|
+
steps: result.steps,
|
|
14688
|
+
warnings: result.warnings
|
|
14689
|
+
}, null, 2));
|
|
14690
|
+
return;
|
|
14691
|
+
}
|
|
14692
|
+
printAgentPrincipalReady(result);
|
|
14693
|
+
break;
|
|
14694
|
+
}
|
|
14695
|
+
case "integration":
|
|
14696
|
+
case "human": {
|
|
14697
|
+
const body = {
|
|
14698
|
+
mode: opts.mode,
|
|
14699
|
+
intents: parseIntents(opts.intents)
|
|
14700
|
+
};
|
|
14701
|
+
if (opts.label)
|
|
14702
|
+
body.label = opts.label;
|
|
14703
|
+
const resp = await ctx.api.post(BOOTSTRAP_PATH, body);
|
|
14704
|
+
if (isJson) {
|
|
14705
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
14706
|
+
return;
|
|
14707
|
+
}
|
|
14708
|
+
console.log(source_default.bold.green(`
|
|
14709
|
+
✓ ${opts.mode} principal ready
|
|
14710
|
+
`));
|
|
14711
|
+
if (resp.service_account) {
|
|
14712
|
+
console.log(source_default.bold("Service-account secret (shown once):"));
|
|
14713
|
+
console.log(source_default.cyan(` export MARTHA_CLIENT_ID=${resp.service_account.client_id}
|
|
14714
|
+
` + ` export MARTHA_CLIENT_SECRET=${resp.service_account.client_secret}`));
|
|
14715
|
+
console.log(source_default.dim(" Re-running with the same --label returns this same secret (recovery, not rotation)."));
|
|
14716
|
+
}
|
|
14717
|
+
if (resp.granted_functions?.length) {
|
|
14718
|
+
console.log(source_default.dim(` functions: ${resp.granted_functions.join(", ")}`));
|
|
14719
|
+
}
|
|
14720
|
+
if (opts.mode === "human") {
|
|
14721
|
+
console.log(source_default.dim(" Tip: `martha init` wraps this mode and also saves the client onto your profile."));
|
|
14722
|
+
}
|
|
14723
|
+
break;
|
|
14724
|
+
}
|
|
14725
|
+
default:
|
|
14726
|
+
throw new CLIError(`Invalid --mode '${opts.mode}'. Use human | integration | agent.`, 4 /* Validation */);
|
|
14727
|
+
}
|
|
14728
|
+
});
|
|
14729
|
+
}
|
|
14730
|
+
|
|
14731
|
+
// src/commands/channels.ts
|
|
14732
|
+
import { accessSync, constants, mkdirSync, writeFileSync } from "node:fs";
|
|
14733
|
+
import { existsSync } from "node:fs";
|
|
14734
|
+
import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
|
|
14735
|
+
import { homedir } from "node:os";
|
|
14736
|
+
import { join as join3 } from "node:path";
|
|
14737
|
+
init_errors();
|
|
14738
|
+
var AGENTS_PATH2 = "/api/admin/definitions/agents";
|
|
14739
|
+
var SUPPORTED_CHANNELS = ["buzz"];
|
|
14740
|
+
var BUZZ_HARNESS_ID = "martha";
|
|
14741
|
+
function buzzHarnessDefinition(opts = {}) {
|
|
14742
|
+
return {
|
|
14743
|
+
id: opts.id ?? BUZZ_HARNESS_ID,
|
|
14744
|
+
label: opts.label ?? "Martha",
|
|
14745
|
+
command: opts.command ?? "martha",
|
|
14746
|
+
args: ["acp-serve", "--post-reply"],
|
|
14747
|
+
env: { BUZZ_ACP_MULTIPLE_EVENT_HANDLING: "queue" },
|
|
14748
|
+
installInstructionsUrl: "https://docs.martha.nomadriver.co/cli/buzz",
|
|
14749
|
+
installHint: "npm i -g @aiaiai-pt/martha-cli"
|
|
14750
|
+
};
|
|
14751
|
+
}
|
|
14752
|
+
function isValidHarnessId(id) {
|
|
14753
|
+
return /^[a-z0-9_][a-z0-9_-]*$/.test(id);
|
|
14754
|
+
}
|
|
14755
|
+
function buzzAppDataCandidates(platform, env2, home) {
|
|
14756
|
+
const ids = [
|
|
14757
|
+
{ build: "release", id: "xyz.block.buzz.app" },
|
|
14758
|
+
{ build: "dev", id: "xyz.block.buzz.app.dev" }
|
|
14759
|
+
];
|
|
14760
|
+
const base = () => {
|
|
14761
|
+
if (platform === "darwin")
|
|
14762
|
+
return join3(home, "Library", "Application Support");
|
|
14763
|
+
if (platform === "win32")
|
|
14764
|
+
return env2.APPDATA ?? join3(home, "AppData", "Roaming");
|
|
14765
|
+
return env2.XDG_DATA_HOME ?? join3(home, ".local", "share");
|
|
14766
|
+
};
|
|
14767
|
+
const root = base();
|
|
14768
|
+
return ids.map(({ build, id }) => ({ build, dir: join3(root, id) }));
|
|
14769
|
+
}
|
|
14770
|
+
function chooseAppDataDir(candidates, exists) {
|
|
14771
|
+
return candidates.find((c) => exists(c.dir)) ?? null;
|
|
14772
|
+
}
|
|
14773
|
+
var DEFAULT_BUZZ_KINDS = "9,46010,40007,7";
|
|
14774
|
+
function buzzAcpEnv(opts) {
|
|
14775
|
+
const env2 = {
|
|
14776
|
+
BUZZ_RELAY_URL: opts.relayUrl,
|
|
14777
|
+
BUZZ_ACP_AGENT_COMMAND: opts.marthaBin,
|
|
14778
|
+
BUZZ_ACP_AGENT_ARGS: "acp-serve,--post-reply",
|
|
14779
|
+
BUZZ_ACP_SUBSCRIBE: opts.subscribe,
|
|
14780
|
+
BUZZ_ACP_MULTIPLE_EVENT_HANDLING: opts.multipleEventHandling,
|
|
14781
|
+
BUZZ_ACP_RESPOND_TO: opts.respondTo
|
|
14782
|
+
};
|
|
14783
|
+
if (opts.subscribe === "all")
|
|
14784
|
+
env2.BUZZ_ACP_KINDS = opts.kinds;
|
|
14785
|
+
return env2;
|
|
14786
|
+
}
|
|
14787
|
+
function resolveOnPath(bin, env2, platform, isExecutable) {
|
|
14788
|
+
if (bin.includes("/") || bin.includes("\\")) {
|
|
14789
|
+
return isExecutable(bin) ? bin : null;
|
|
14790
|
+
}
|
|
14791
|
+
const exts = platform === "win32" ? (env2.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
14792
|
+
const sep = platform === "win32" ? ";" : ":";
|
|
14793
|
+
for (const dir of (env2.PATH ?? "").split(sep).filter(Boolean)) {
|
|
14794
|
+
for (const ext of exts) {
|
|
14795
|
+
const candidate = join3(dir, bin + ext);
|
|
14796
|
+
if (isExecutable(candidate))
|
|
14797
|
+
return candidate;
|
|
14798
|
+
}
|
|
14799
|
+
}
|
|
14800
|
+
return null;
|
|
14801
|
+
}
|
|
14802
|
+
function executableOnDisk(p) {
|
|
14803
|
+
try {
|
|
14804
|
+
accessSync(p, constants.X_OK);
|
|
14805
|
+
return true;
|
|
14806
|
+
} catch {
|
|
14807
|
+
return false;
|
|
14808
|
+
}
|
|
14809
|
+
}
|
|
14810
|
+
function which(bin) {
|
|
14811
|
+
return resolveOnPath(bin, process.env, process.platform, executableOnDisk);
|
|
14812
|
+
}
|
|
14813
|
+
function runBin(bin, args, env2) {
|
|
14814
|
+
return new Promise((resolve2) => {
|
|
14815
|
+
execFile2(bin, args, { env: env2, timeout: 15000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
14816
|
+
const code = err && typeof err.code === "number" ? err.code ?? 1 : err ? 1 : 0;
|
|
14817
|
+
resolve2({
|
|
14818
|
+
code,
|
|
14819
|
+
stdout: stdout ?? "",
|
|
14820
|
+
stderr: (stderr ?? "").slice(0, 2000)
|
|
14821
|
+
});
|
|
14822
|
+
});
|
|
14823
|
+
});
|
|
14824
|
+
}
|
|
14825
|
+
async function reachable(url) {
|
|
14826
|
+
try {
|
|
14827
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
14828
|
+
return { ok: true, detail: `${url} → HTTP ${res.status}` };
|
|
14829
|
+
} catch (err) {
|
|
14830
|
+
return {
|
|
14831
|
+
ok: false,
|
|
14832
|
+
detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`
|
|
14833
|
+
};
|
|
14834
|
+
}
|
|
14835
|
+
}
|
|
14836
|
+
function relayHttpUrl(relayUrl) {
|
|
14837
|
+
return relayUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
|
|
14838
|
+
}
|
|
14839
|
+
async function preflightBuzz(ctx, opts) {
|
|
14840
|
+
const checks = [];
|
|
14841
|
+
const marthaPath = which(opts.marthaBin);
|
|
14842
|
+
checks.push({
|
|
14843
|
+
ok: !!marthaPath,
|
|
14844
|
+
label: "martha CLI on PATH",
|
|
14845
|
+
detail: marthaPath ?? `'${opts.marthaBin}' not found`,
|
|
14846
|
+
fix: marthaPath ? undefined : "install it: npm i -g @aiaiai-pt/martha-cli (buzz-acp spawns it by name)"
|
|
14847
|
+
});
|
|
14848
|
+
if (marthaPath) {
|
|
14849
|
+
const probe = await runBin(marthaPath, ["--help"], process.env);
|
|
14850
|
+
const supported = /^\s+acp-serve\b/m.test(probe.stdout);
|
|
14851
|
+
checks.push({
|
|
14852
|
+
ok: supported,
|
|
14853
|
+
label: "that martha can serve ACP",
|
|
14854
|
+
detail: supported ? "acp-serve available" : `${marthaPath} has no acp-serve subcommand`,
|
|
14855
|
+
fix: supported ? undefined : "upgrade the CLI that is FIRST on PATH: npm i -g @aiaiai-pt/martha-cli@latest (or point --martha-bin at the right one)"
|
|
14856
|
+
});
|
|
14857
|
+
}
|
|
14858
|
+
const acpPath = which(opts.buzzAcpBin);
|
|
14859
|
+
checks.push({
|
|
14860
|
+
ok: !!acpPath,
|
|
14861
|
+
label: "buzz-acp on PATH",
|
|
14862
|
+
detail: acpPath ?? `'${opts.buzzAcpBin}' not found`,
|
|
14863
|
+
fix: acpPath ? undefined : "build it from block/buzz: cargo build -p buzz-acp, then put target/debug on PATH (or pass --buzz-acp-bin)"
|
|
14864
|
+
});
|
|
14865
|
+
const buzzPath = which(opts.buzzBin);
|
|
14866
|
+
checks.push({
|
|
14867
|
+
ok: !!buzzPath,
|
|
14868
|
+
label: "buzz CLI on PATH",
|
|
14869
|
+
detail: buzzPath ?? `'${opts.buzzBin}' not found`,
|
|
14870
|
+
fix: buzzPath ? undefined : "build it from block/buzz: cargo build -p buzz-cli — acp-serve --post-reply shells it to deliver the answer"
|
|
14871
|
+
});
|
|
14872
|
+
let whoami = null;
|
|
14873
|
+
try {
|
|
14874
|
+
whoami = await ctx.api.get("/api/tasks/whoami");
|
|
14875
|
+
checks.push({
|
|
14876
|
+
ok: true,
|
|
14877
|
+
label: "Martha API + credential",
|
|
14878
|
+
detail: `${ctx.profile.api_url} — ${whoami.kind ?? "?"}${whoami.agent_name ? ` (${whoami.agent_name})` : whoami.sub ? ` (${whoami.sub})` : ""}`
|
|
14879
|
+
});
|
|
14880
|
+
} catch (err) {
|
|
14881
|
+
const status = err instanceof MarthaAPIError ? err.status : undefined;
|
|
14882
|
+
const rejectedByApi = status === 401 || status === 403;
|
|
14883
|
+
const rejectedByIdp = !rejectedByApi && err instanceof CLIError && err.exitCode === 2 /* Auth */;
|
|
14884
|
+
checks.push({
|
|
14885
|
+
ok: false,
|
|
14886
|
+
label: "Martha API + credential",
|
|
14887
|
+
detail: rejectedByApi ? `credential rejected by the API (HTTP ${status})` : rejectedByIdp ? `credential rejected by Keycloak: ${err instanceof Error ? err.message : String(err)}` : `${ctx.profile.api_url}: ${err instanceof Error ? err.message : String(err)}`,
|
|
14888
|
+
fix: rejectedByApi ? "export MARTHA_CLIENT_ID/MARTHA_CLIENT_SECRET for the agent's principal, or run `martha auth login`" : rejectedByIdp ? `check MARTHA_CLIENT_ID/MARTHA_CLIENT_SECRET, and that profile '${ctx.profileName}' points at the Keycloak that issued them — or mint a fresh one: martha principals bootstrap --mode agent --agent ${opts.agent}` : "check api_url in ~/.martha/config.yaml and that the Martha API is up"
|
|
14889
|
+
});
|
|
14890
|
+
}
|
|
14891
|
+
let def = null;
|
|
14892
|
+
try {
|
|
14893
|
+
def = await ctx.api.get(`${AGENTS_PATH2}/${encodeURIComponent(opts.agent)}`);
|
|
14894
|
+
} catch (err) {
|
|
14895
|
+
if (!(err instanceof MarthaAPIError && err.status === 404)) {
|
|
14896
|
+
checks.push({
|
|
14897
|
+
ok: null,
|
|
14898
|
+
label: "agent exists",
|
|
14899
|
+
detail: `could not read '${opts.agent}': ${err instanceof Error ? err.message : String(err)}`,
|
|
14900
|
+
fix: "this credential may not be allowed to read agent definitions — check with `martha agents list`"
|
|
14901
|
+
});
|
|
14902
|
+
}
|
|
14903
|
+
}
|
|
14904
|
+
if (def) {
|
|
14905
|
+
const marthaHosted = String(def.agent_type ?? "") !== "external";
|
|
14906
|
+
checks.push({
|
|
14907
|
+
ok: marthaHosted,
|
|
14908
|
+
label: "agent is Martha-hosted",
|
|
14909
|
+
detail: marthaHosted ? String(def.agent_type ?? "cloud") : "external (self-hosted)",
|
|
14910
|
+
fix: marthaHosted ? undefined : "acp-serve drives a Martha-hosted agent — recreate it with --reasoning martha"
|
|
14911
|
+
});
|
|
14912
|
+
const takeover = def.runs_as_chat === true;
|
|
14913
|
+
checks.push({
|
|
14914
|
+
ok: takeover,
|
|
14915
|
+
label: "chat takeover (runs_as_chat)",
|
|
14916
|
+
fix: takeover ? undefined : `run: martha principals bootstrap --mode agent --agent ${opts.agent}`
|
|
14917
|
+
});
|
|
14918
|
+
} else if (!checks.some((c) => c.label === "agent exists")) {
|
|
14919
|
+
checks.push({
|
|
14920
|
+
ok: false,
|
|
14921
|
+
label: "agent exists",
|
|
14922
|
+
detail: `'${opts.agent}' not found`,
|
|
14923
|
+
fix: `create it: martha principals bootstrap --mode agent --agent ${opts.agent} --prompt "…"`
|
|
14924
|
+
});
|
|
14925
|
+
}
|
|
14926
|
+
checks.push(await checkAgentPrincipal(ctx, opts.agent, def));
|
|
14927
|
+
const hasSa = !!(process.env.MARTHA_CLIENT_ID && process.env.MARTHA_CLIENT_SECRET);
|
|
14928
|
+
const hasToken = !!process.env.MARTHA_TOKEN;
|
|
14929
|
+
checks.push({
|
|
14930
|
+
ok: hasSa || hasToken ? true : null,
|
|
14931
|
+
label: "durable credential in env",
|
|
14932
|
+
detail: hasSa ? `MARTHA_CLIENT_ID=${process.env.MARTHA_CLIENT_ID}` : hasToken ? "MARTHA_TOKEN" : "none — the adapter will fall back to the profile's stored token",
|
|
14933
|
+
fix: hasSa || hasToken ? undefined : `mint one: martha principals bootstrap --mode agent --agent ${opts.agent}`
|
|
14934
|
+
});
|
|
14935
|
+
const relayProbe = await reachable(relayHttpUrl(opts.relayUrl));
|
|
14936
|
+
checks.push({
|
|
14937
|
+
ok: relayProbe.ok,
|
|
14938
|
+
label: "relay reachable",
|
|
14939
|
+
detail: relayProbe.detail,
|
|
14940
|
+
fix: relayProbe.ok ? undefined : `start the relay, or correct --relay (${opts.relayUrl})`
|
|
14941
|
+
});
|
|
14942
|
+
const identity2 = process.env.BUZZ_PRIVATE_KEY;
|
|
14943
|
+
checks.push({
|
|
14944
|
+
ok: !!identity2,
|
|
14945
|
+
label: "channel identity",
|
|
14946
|
+
detail: identity2 ? "BUZZ_PRIVATE_KEY set (not shown)" : "BUZZ_PRIVATE_KEY not set",
|
|
14947
|
+
fix: identity2 ? undefined : "export BUZZ_PRIVATE_KEY=<the agent's nsec> — Martha-issued identities land in #1119"
|
|
14948
|
+
});
|
|
14949
|
+
if (identity2 && relayProbe.ok && buzzPath) {
|
|
14950
|
+
checks.push(await checkMembership(buzzPath, opts.relayUrl, identity2, opts.channelId));
|
|
14951
|
+
} else {
|
|
14952
|
+
checks.push({
|
|
14953
|
+
ok: null,
|
|
14954
|
+
label: "channel membership",
|
|
14955
|
+
detail: "skipped — needs the identity, a reachable relay, and the buzz CLI"
|
|
14956
|
+
});
|
|
14957
|
+
}
|
|
14958
|
+
return checks;
|
|
14959
|
+
}
|
|
14960
|
+
async function checkAgentPrincipal(ctx, agent, def) {
|
|
14961
|
+
const label = "agent principal (bound client)";
|
|
14962
|
+
const fix = `run: martha principals bootstrap --mode agent --agent ${agent}`;
|
|
14963
|
+
try {
|
|
14964
|
+
const scan = await scanClientsForAgent(ctx, agent);
|
|
14965
|
+
if (!scan.granting.length) {
|
|
14966
|
+
return scan.incomplete ? {
|
|
14967
|
+
ok: null,
|
|
14968
|
+
label,
|
|
14969
|
+
detail: `inconclusive — read ${scan.scanned} of ${scan.total} clients`
|
|
14970
|
+
} : { ok: false, label, detail: "no client is bound to this agent", fix };
|
|
14971
|
+
}
|
|
14972
|
+
const shared = scan.granting.filter((h) => h.agents.length > 1);
|
|
14973
|
+
const names = scan.granting.map((h) => h.client);
|
|
14974
|
+
if (shared.length) {
|
|
14975
|
+
return {
|
|
14976
|
+
ok: null,
|
|
14977
|
+
label,
|
|
14978
|
+
detail: `bound to ${names.join(", ")} — but ${shared.map((h) => h.client).join(", ")} also has other agents, which disables chat takeover`,
|
|
14979
|
+
fix: "give this agent its own principal: martha principals bootstrap --mode agent --label <fresh>"
|
|
14980
|
+
};
|
|
14981
|
+
}
|
|
14982
|
+
return { ok: true, label, detail: names.join(", ") };
|
|
14983
|
+
} catch (err) {
|
|
14984
|
+
const status = err instanceof MarthaAPIError ? err.status : undefined;
|
|
14985
|
+
return {
|
|
14986
|
+
ok: null,
|
|
14987
|
+
label,
|
|
14988
|
+
detail: status === 403 || status === 401 ? `not verifiable with this credential (HTTP ${status})` : `not verifiable: ${err instanceof Error ? err.message : String(err)}`,
|
|
14989
|
+
fix: def ? undefined : fix
|
|
14990
|
+
};
|
|
14991
|
+
}
|
|
14992
|
+
}
|
|
14993
|
+
async function checkMembership(buzzBin, relayUrl, identity2, channelId) {
|
|
14994
|
+
const label = "channel membership";
|
|
14995
|
+
const res = await runBin(buzzBin, ["--format", "compact", "channels", "list", "--member"], {
|
|
14996
|
+
...process.env,
|
|
14997
|
+
BUZZ_RELAY_URL: relayUrl,
|
|
14998
|
+
BUZZ_PRIVATE_KEY: identity2
|
|
14999
|
+
});
|
|
15000
|
+
if (res.code !== 0) {
|
|
15001
|
+
const why = res.code === 3 ? "the relay rejected this identity" : res.code === 2 ? "relay/network error" : `buzz exited ${res.code}`;
|
|
15002
|
+
return {
|
|
15003
|
+
ok: false,
|
|
15004
|
+
label,
|
|
15005
|
+
detail: `${why}: ${res.stderr.trim().slice(0, 200)}`,
|
|
15006
|
+
fix: "check BUZZ_PRIVATE_KEY belongs to a registered user on this relay"
|
|
15007
|
+
};
|
|
15008
|
+
}
|
|
15009
|
+
let channels = [];
|
|
15010
|
+
try {
|
|
15011
|
+
channels = JSON.parse(res.stdout.trim() || "[]");
|
|
15012
|
+
} catch {
|
|
15013
|
+
return {
|
|
15014
|
+
ok: null,
|
|
15015
|
+
label,
|
|
15016
|
+
detail: `could not parse \`buzz channels list\` output (${res.stdout.slice(0, 120)})`
|
|
15017
|
+
};
|
|
15018
|
+
}
|
|
15019
|
+
if (channelId) {
|
|
15020
|
+
const hit = channels.find((c) => c.channel_id === channelId);
|
|
15021
|
+
return {
|
|
15022
|
+
ok: !!hit,
|
|
15023
|
+
label,
|
|
15024
|
+
detail: hit ? `${hit.name ?? channelId}` : `not a member of ${channelId}`,
|
|
15025
|
+
fix: hit ? undefined : `buzz channels add-member --channel ${channelId} --pubkey <the agent's pubkey> --role member`
|
|
15026
|
+
};
|
|
15027
|
+
}
|
|
15028
|
+
return {
|
|
15029
|
+
ok: channels.length > 0,
|
|
15030
|
+
label,
|
|
15031
|
+
detail: channels.length ? `${channels.length} channel(s): ${channels.map((c) => c.name ?? c.channel_id).slice(0, 5).join(", ")}` : "the identity is in no channels",
|
|
15032
|
+
fix: channels.length ? undefined : "buzz channels add-member --channel <id> --pubkey <the agent's pubkey> --role member"
|
|
15033
|
+
};
|
|
15034
|
+
}
|
|
15035
|
+
function emitManifest(kind, env2, opts) {
|
|
15036
|
+
const secretKeys = [
|
|
15037
|
+
"BUZZ_PRIVATE_KEY",
|
|
15038
|
+
"MARTHA_CLIENT_ID",
|
|
15039
|
+
"MARTHA_CLIENT_SECRET"
|
|
15040
|
+
];
|
|
15041
|
+
const entries = Object.entries(env2);
|
|
15042
|
+
if (kind === "docker") {
|
|
15043
|
+
const lines = [
|
|
15044
|
+
"docker run --rm \\",
|
|
15045
|
+
...entries.map(([k, v]) => ` -e ${k}=${JSON.stringify(v)} \\`),
|
|
15046
|
+
...secretKeys.map((k) => ` -e ${k}="$${k}" \\`),
|
|
15047
|
+
` ${opts.image}`
|
|
15048
|
+
];
|
|
15049
|
+
return lines.join(`
|
|
15050
|
+
`);
|
|
15051
|
+
}
|
|
15052
|
+
if (kind === "compose") {
|
|
15053
|
+
return [
|
|
15054
|
+
"services:",
|
|
15055
|
+
` martha-buzz-${opts.agent}:`,
|
|
15056
|
+
` image: ${opts.image}`,
|
|
15057
|
+
" restart: unless-stopped",
|
|
15058
|
+
" environment:",
|
|
15059
|
+
...entries.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`),
|
|
15060
|
+
...secretKeys.map((k) => ` ${k}: \${${k}:?set ${k}}`)
|
|
15061
|
+
].join(`
|
|
15062
|
+
`);
|
|
15063
|
+
}
|
|
15064
|
+
return [
|
|
15065
|
+
"apiVersion: apps/v1",
|
|
15066
|
+
"kind: Deployment",
|
|
15067
|
+
"metadata:",
|
|
15068
|
+
` name: martha-buzz-${opts.agent}`,
|
|
15069
|
+
"spec:",
|
|
15070
|
+
" replicas: 1",
|
|
15071
|
+
" selector:",
|
|
15072
|
+
` matchLabels: { app: martha-buzz-${opts.agent} }`,
|
|
15073
|
+
" template:",
|
|
15074
|
+
" metadata:",
|
|
15075
|
+
` labels: { app: martha-buzz-${opts.agent} }`,
|
|
15076
|
+
" spec:",
|
|
15077
|
+
" containers:",
|
|
15078
|
+
` - name: adapter`,
|
|
15079
|
+
` image: ${opts.image}`,
|
|
15080
|
+
" env:",
|
|
15081
|
+
...entries.flatMap(([k, v]) => [
|
|
15082
|
+
` - name: ${k}`,
|
|
15083
|
+
` value: ${JSON.stringify(v)}`
|
|
15084
|
+
]),
|
|
15085
|
+
...secretKeys.flatMap((k) => [
|
|
15086
|
+
` - name: ${k}`,
|
|
15087
|
+
" valueFrom:",
|
|
15088
|
+
` secretKeyRef: { name: martha-buzz-${opts.agent}, key: ${k} }`
|
|
15089
|
+
])
|
|
15090
|
+
].join(`
|
|
15091
|
+
`);
|
|
15092
|
+
}
|
|
15093
|
+
function assertSupportedChannel(kind) {
|
|
15094
|
+
if (!SUPPORTED_CHANNELS.includes(kind)) {
|
|
15095
|
+
throw new CLIError(`Channel type '${kind}' is not implemented.`, 4 /* Validation */, `Supported: ${SUPPORTED_CHANNELS.join(", ")}. Slack and others land as separate slices — the verb and the preflight generalize, the precondition list does not.`);
|
|
15096
|
+
}
|
|
15097
|
+
}
|
|
15098
|
+
function registerChannelCommands(program2) {
|
|
15099
|
+
const channels = program2.command("channels").description("Put a Martha agent in an external conversation channel — from Buzz (install-harness) or from Martha (connect)");
|
|
15100
|
+
channels.command("install-harness <type>").description("Register Martha as a runtime in Buzz Desktop (a tier-3 custom harness definition). " + "Desktop then owns the agent's keypair, attestation and lifecycle — we only declare how to start the adapter.").option("--app-data <dir>", "Buzz Desktop app-data dir (default: auto-detect)").option("--id <id>", "Harness id", BUZZ_HARNESS_ID).option("--label <label>", "Name shown in Buzz's runtime picker", "Martha").option("--command <bin>", "Executable Buzz should spawn", "martha").option("--print", "Print the definition instead of writing it").option("--force", "Overwrite an existing definition").action(async (type, opts) => {
|
|
15101
|
+
assertSupportedChannel(type);
|
|
15102
|
+
const def = buzzHarnessDefinition({
|
|
15103
|
+
id: opts.id,
|
|
15104
|
+
label: opts.label,
|
|
15105
|
+
command: opts.command
|
|
15106
|
+
});
|
|
15107
|
+
if (!isValidHarnessId(def.id)) {
|
|
15108
|
+
throw new CLIError(`Harness id '${def.id}' is invalid.`, 4 /* Validation */, "Buzz requires [a-z0-9_][a-z0-9_-]* — a file with any other id is skipped by their loader.");
|
|
15109
|
+
}
|
|
15110
|
+
const json = JSON.stringify(def, null, 2) + `
|
|
15111
|
+
`;
|
|
15112
|
+
if (opts.print) {
|
|
15113
|
+
console.log(json);
|
|
15114
|
+
return;
|
|
15115
|
+
}
|
|
15116
|
+
const chosen = opts.appData ? { build: "release", dir: String(opts.appData) } : chooseAppDataDir(buzzAppDataCandidates(process.platform, process.env, homedir()), (d) => existsSync(d));
|
|
15117
|
+
if (!chosen) {
|
|
15118
|
+
const candidates = buzzAppDataCandidates(process.platform, process.env, homedir());
|
|
15119
|
+
throw new CLIError("No Buzz Desktop app-data directory found.", 3 /* NotFound */, `Looked for:
|
|
15120
|
+
${candidates.map((c) => ` ${c.dir} (${c.build})`).join(`
|
|
15121
|
+
`)}
|
|
15122
|
+
Launch Buzz Desktop once so it creates one, or pass --app-data <dir>.`);
|
|
15123
|
+
}
|
|
15124
|
+
const dir = join3(chosen.dir, "custom_harnesses");
|
|
15125
|
+
const file = join3(dir, `${def.id}.json`);
|
|
15126
|
+
if (existsSync(file) && !opts.force) {
|
|
15127
|
+
throw new CLIError(`${file} already exists.`, 5 /* Conflict */, "Re-run with --force to overwrite it.");
|
|
15128
|
+
}
|
|
15129
|
+
mkdirSync(dir, { recursive: true });
|
|
15130
|
+
writeFileSync(file, json, "utf8");
|
|
15131
|
+
console.log(source_default.bold.green(`
|
|
15132
|
+
✓ registered '${def.label}' as a Buzz runtime
|
|
15133
|
+
`));
|
|
15134
|
+
console.log(source_default.dim(` ${file} (${chosen.build} build)`));
|
|
15135
|
+
console.log();
|
|
15136
|
+
for (const line of installHarnessNotes(def))
|
|
15137
|
+
console.log(line);
|
|
15138
|
+
});
|
|
15139
|
+
channels.command("preflight <type>").description("Check every precondition for an agent to answer in a channel, and name the one command that fixes each gap. Exits non-zero if any check fails.").requiredOption("--agent <name>", "The Martha agent that will answer").option("--relay <url>", "Relay URL", "ws://localhost:3030").option("--channel <uuid>", "Check membership of one specific channel").option("--buzz-acp-bin <path>", "buzz-acp binary", "buzz-acp").option("--buzz-bin <path>", "buzz CLI binary", "buzz").option("--martha-bin <path>", "martha binary buzz-acp will spawn", "martha").action(async (type, opts) => {
|
|
15140
|
+
assertSupportedChannel(type);
|
|
15141
|
+
const ctx = contextFor(program2);
|
|
15142
|
+
const checks = await preflightBuzz(ctx, {
|
|
15143
|
+
agent: opts.agent,
|
|
15144
|
+
relayUrl: opts.relay,
|
|
15145
|
+
channelId: opts.channel,
|
|
15146
|
+
marthaBin: opts.marthaBin,
|
|
15147
|
+
buzzAcpBin: opts.buzzAcpBin,
|
|
15148
|
+
buzzBin: opts.buzzBin
|
|
15149
|
+
});
|
|
15150
|
+
reportPreflight(type, opts.agent, checks);
|
|
15151
|
+
if (checks.some((c) => c.ok === false))
|
|
15152
|
+
process.exitCode = 1 /* Error */;
|
|
15153
|
+
});
|
|
15154
|
+
channels.command("connect <type>").description("Preflight, then launch the channel adapter (buzz-acp → martha acp-serve) — or emit a deployable unit with --emit. Never half-starts: a failed check exits non-zero.").requiredOption("--agent <name>", "The Martha agent that will answer").option("--relay <url>", "Relay URL", "ws://localhost:3030").option("--channel <uuid>", "Check membership of one specific channel").option("--subscribe <mode>", "mentions | all", "all").option("--kinds <list>", "Event kinds when --subscribe all (a wildcard REQ without it)", DEFAULT_BUZZ_KINDS).option("--respond-to <mode>", "Who the agent answers (buzz-acp gate)", "anyone").option("--buzz-acp-bin <path>", "buzz-acp binary", "buzz-acp").option("--buzz-bin <path>", "buzz CLI binary", "buzz").option("--martha-bin <path>", "martha binary buzz-acp will spawn", "martha").option("--emit <target>", "Print a deployable unit instead of launching: docker | compose | k8s").option("--image <ref>", "Image for --emit", "ghcr.io/westeuropeco/martha-buzz-adapter:latest").option("--dry-run", "Preflight and print the launch, but don't spawn anything").action(async (type, opts) => {
|
|
15155
|
+
assertSupportedChannel(type);
|
|
15156
|
+
if (opts.subscribe !== "mentions" && opts.subscribe !== "all") {
|
|
15157
|
+
throw new CLIError(`--subscribe must be 'mentions' or 'all' (got '${opts.subscribe}').`, 4 /* Validation */);
|
|
15158
|
+
}
|
|
15159
|
+
const ctx = contextFor(program2);
|
|
15160
|
+
const checks = await preflightBuzz(ctx, {
|
|
15161
|
+
agent: opts.agent,
|
|
15162
|
+
relayUrl: opts.relay,
|
|
15163
|
+
channelId: opts.channel,
|
|
15164
|
+
marthaBin: opts.marthaBin,
|
|
15165
|
+
buzzAcpBin: opts.buzzAcpBin,
|
|
15166
|
+
buzzBin: opts.buzzBin
|
|
15167
|
+
});
|
|
15168
|
+
reportPreflight(type, opts.agent, checks, opts.emit ? "stderr" : "stdout");
|
|
15169
|
+
const env2 = buzzAcpEnv({
|
|
15170
|
+
relayUrl: opts.relay,
|
|
15171
|
+
subscribe: opts.subscribe,
|
|
15172
|
+
kinds: opts.kinds,
|
|
15173
|
+
multipleEventHandling: "queue",
|
|
15174
|
+
respondTo: opts.respondTo,
|
|
15175
|
+
marthaBin: opts.marthaBin
|
|
15176
|
+
});
|
|
15177
|
+
if (opts.emit) {
|
|
15178
|
+
if (!["docker", "compose", "k8s"].includes(opts.emit)) {
|
|
15179
|
+
throw new CLIError(`--emit must be docker, compose or k8s (got '${opts.emit}').`, 4 /* Validation */);
|
|
15180
|
+
}
|
|
15181
|
+
console.log(emitManifest(opts.emit, env2, {
|
|
15182
|
+
agent: opts.agent,
|
|
15183
|
+
image: opts.image
|
|
15184
|
+
}));
|
|
15185
|
+
return;
|
|
15186
|
+
}
|
|
15187
|
+
const failed = checks.filter((c) => c.ok === false);
|
|
15188
|
+
if (failed.length) {
|
|
15189
|
+
console.log(source_default.red.bold(`Not connecting — ${failed.length} precondition(s) unmet. Nothing was started.`));
|
|
15190
|
+
process.exitCode = 1 /* Error */;
|
|
15191
|
+
return;
|
|
15192
|
+
}
|
|
15193
|
+
const acp = which(opts.buzzAcpBin);
|
|
15194
|
+
if (opts.dryRun) {
|
|
15195
|
+
console.log(source_default.bold(`Would launch:
|
|
15196
|
+
`));
|
|
15197
|
+
for (const [k, v] of Object.entries(env2))
|
|
15198
|
+
console.log(` ${k}=${v}`);
|
|
15199
|
+
console.log(` ${acp}
|
|
15200
|
+
`);
|
|
15201
|
+
return;
|
|
15202
|
+
}
|
|
15203
|
+
console.log(source_default.bold(`
|
|
15204
|
+
Connecting '${opts.agent}' to ${opts.relay} …
|
|
15205
|
+
`));
|
|
15206
|
+
const child = spawn3(acp, [], {
|
|
15207
|
+
env: { ...process.env, ...env2 },
|
|
15208
|
+
stdio: "inherit"
|
|
15209
|
+
});
|
|
15210
|
+
await new Promise((resolve2) => {
|
|
15211
|
+
child.on("exit", (code, signal) => {
|
|
15212
|
+
if (signal) {
|
|
15213
|
+
console.log(source_default.dim(`
|
|
15214
|
+
buzz-acp terminated by ${signal}`));
|
|
15215
|
+
process.exitCode = 1 /* Error */;
|
|
15216
|
+
} else if (code) {
|
|
15217
|
+
process.exitCode = code;
|
|
15218
|
+
}
|
|
15219
|
+
resolve2();
|
|
15220
|
+
});
|
|
15221
|
+
child.on("error", (err) => {
|
|
15222
|
+
console.error(source_default.red(`failed to start ${acp}: ${err.message}`));
|
|
15223
|
+
process.exitCode = 1 /* Error */;
|
|
15224
|
+
resolve2();
|
|
15225
|
+
});
|
|
15226
|
+
});
|
|
15227
|
+
});
|
|
15228
|
+
}
|
|
15229
|
+
function contextFor(program2) {
|
|
15230
|
+
return createContext({
|
|
15231
|
+
profileOverride: program2.opts().profile,
|
|
15232
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
15233
|
+
verbose: program2.opts().verbose
|
|
15234
|
+
});
|
|
15235
|
+
}
|
|
15236
|
+
function reportPreflight(type, agent, checks, stream = "stdout") {
|
|
15237
|
+
const write = stream === "stderr" ? (line = "") => process.stderr.write(line + `
|
|
15238
|
+
`) : (line = "") => console.log(line);
|
|
15239
|
+
write(source_default.bold(`Martha CLI preflight — ${type} channel: ${agent}
|
|
15240
|
+
`));
|
|
15241
|
+
printChecks(checks, write);
|
|
15242
|
+
const failed = checks.filter((c) => c.ok === false).length;
|
|
15243
|
+
const unknown = checks.filter((c) => c.ok === null).length;
|
|
15244
|
+
write();
|
|
15245
|
+
if (failed) {
|
|
15246
|
+
write(source_default.red.bold(` ${failed} precondition(s) unmet — fix the FAIL items above.`));
|
|
15247
|
+
} else if (unknown) {
|
|
15248
|
+
write(source_default.yellow.bold(` All required preconditions met (${unknown} unverifiable).`));
|
|
15249
|
+
} else {
|
|
15250
|
+
write(source_default.green.bold(" Ready."));
|
|
15251
|
+
}
|
|
15252
|
+
}
|
|
15253
|
+
function installHarnessNotes(def) {
|
|
15254
|
+
return [
|
|
15255
|
+
"In Buzz Desktop:",
|
|
15256
|
+
source_default.cyan(` 1. New agent → runtime "${def.label}"`),
|
|
15257
|
+
source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
|
|
15258
|
+
source_default.dim(" (credentials belong per-agent, never in the shared definition file)"),
|
|
15259
|
+
"",
|
|
15260
|
+
source_default.dim(`Desktop owns the rest: the Nostr keypair in your keyring, the NIP-OA
|
|
15261
|
+
` + "attestation, spawning buzz-acp, and attaching the agent to channels."),
|
|
15262
|
+
"",
|
|
15263
|
+
source_default.dim(`If you set BUZZ_ACP_SUBSCRIBE=all on the agent, you MUST also set
|
|
15264
|
+
` + `BUZZ_ACP_KINDS=${DEFAULT_BUZZ_KINDS} — 'all' with no kinds is a wildcard
|
|
15265
|
+
` + "subscription and turns typing and presence events into prompts.")
|
|
15266
|
+
];
|
|
15267
|
+
}
|
|
15268
|
+
|
|
14212
15269
|
// src/commands/config.ts
|
|
14213
15270
|
init_config();
|
|
14214
15271
|
init_errors();
|
|
14215
|
-
import { createInterface as
|
|
15272
|
+
import { createInterface as createInterface4 } from "node:readline";
|
|
14216
15273
|
function prompt(rl, question, defaultValue) {
|
|
14217
15274
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
14218
15275
|
return new Promise((resolve2) => {
|
|
@@ -14237,7 +15294,7 @@ async function configInit(configDir) {
|
|
|
14237
15294
|
console.log(`No configuration found. Let's set up your first profile.
|
|
14238
15295
|
`);
|
|
14239
15296
|
}
|
|
14240
|
-
const rl =
|
|
15297
|
+
const rl = createInterface4({
|
|
14241
15298
|
input: process.stdin,
|
|
14242
15299
|
output: process.stderr
|
|
14243
15300
|
});
|
|
@@ -14524,7 +15581,7 @@ function registerConfigCommand(program2) {
|
|
|
14524
15581
|
throw new CLIError("Cannot confirm in non-interactive mode.", 4 /* Validation */, "Use --yes to skip confirmation.");
|
|
14525
15582
|
}
|
|
14526
15583
|
const ok = await new Promise((resolve2) => {
|
|
14527
|
-
const rl =
|
|
15584
|
+
const rl = createInterface4({
|
|
14528
15585
|
input: process.stdin,
|
|
14529
15586
|
output: process.stderr
|
|
14530
15587
|
});
|
|
@@ -14576,7 +15633,9 @@ var MUTABLE_FIELDS = {
|
|
|
14576
15633
|
"is_active",
|
|
14577
15634
|
"collection_id_param",
|
|
14578
15635
|
"risk_tags",
|
|
14579
|
-
"risk_level"
|
|
15636
|
+
"risk_level",
|
|
15637
|
+
"verification_safe",
|
|
15638
|
+
"idempotent"
|
|
14580
15639
|
]),
|
|
14581
15640
|
Workflow: new Set([
|
|
14582
15641
|
"description",
|
|
@@ -14591,9 +15650,27 @@ var MUTABLE_FIELDS = {
|
|
|
14591
15650
|
"is_active"
|
|
14592
15651
|
])
|
|
14593
15652
|
};
|
|
15653
|
+
var FUNCTION_DECLARATION_FIELDS = new Set([
|
|
15654
|
+
"verification_safe",
|
|
15655
|
+
"idempotent"
|
|
15656
|
+
]);
|
|
14594
15657
|
function isPlainObject(value) {
|
|
14595
15658
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14596
15659
|
}
|
|
15660
|
+
function declarationEnabled(value) {
|
|
15661
|
+
if (isPlainObject(value))
|
|
15662
|
+
return value.enabled === true;
|
|
15663
|
+
return value === true;
|
|
15664
|
+
}
|
|
15665
|
+
function declarationFieldMatches(requested, observed) {
|
|
15666
|
+
if (!declarationEnabled(requested)) {
|
|
15667
|
+
return !declarationEnabled(observed);
|
|
15668
|
+
}
|
|
15669
|
+
if (isPlainObject(observed)) {
|
|
15670
|
+
return observed.enabled === true && requestedValueMatches(requested, observed);
|
|
15671
|
+
}
|
|
15672
|
+
return observed === true;
|
|
15673
|
+
}
|
|
14597
15674
|
function valuesEqual(left, right) {
|
|
14598
15675
|
if (left === right)
|
|
14599
15676
|
return true;
|
|
@@ -14655,6 +15732,10 @@ function normalizeDefinitionWriteBody(kind, body) {
|
|
|
14655
15732
|
}
|
|
14656
15733
|
delete normalized.definition;
|
|
14657
15734
|
for (const [key, nestedValue] of Object.entries(nested)) {
|
|
15735
|
+
if (kind === "Function" && FUNCTION_DECLARATION_FIELDS.has(key)) {
|
|
15736
|
+
normalized[key] = nestedValue;
|
|
15737
|
+
continue;
|
|
15738
|
+
}
|
|
14658
15739
|
if (key in normalized && !valuesEqual(normalized[key], nestedValue)) {
|
|
14659
15740
|
throw new CLIError(`Conflicting values for '${key}' in flat and nested ${kind} fields`, 4 /* Validation */);
|
|
14660
15741
|
}
|
|
@@ -14722,6 +15803,9 @@ function fieldValueMatches(kind, field, requested, observed) {
|
|
|
14722
15803
|
if (kind === "Function" && field === "parameters") {
|
|
14723
15804
|
return parameterMapMatches(requested, observed);
|
|
14724
15805
|
}
|
|
15806
|
+
if (kind === "Function" && FUNCTION_DECLARATION_FIELDS.has(field)) {
|
|
15807
|
+
return declarationFieldMatches(requested, observed);
|
|
15808
|
+
}
|
|
14725
15809
|
const isExactTypedValue = kind === "Function" && (field === "request_body" || field === "auth") || kind === "Workflow" && (field === "nodes" || field === "edges");
|
|
14726
15810
|
if (isExactTypedValue) {
|
|
14727
15811
|
return valuesEqual(requested, observed);
|
|
@@ -16377,6 +17461,13 @@ function colorStatus2(status) {
|
|
|
16377
17461
|
const fn = STATUS_COLORS2[status] ?? source_default.white;
|
|
16378
17462
|
return fn(status);
|
|
16379
17463
|
}
|
|
17464
|
+
function documentCountLabel(count, fetched) {
|
|
17465
|
+
if (count != null)
|
|
17466
|
+
return String(count);
|
|
17467
|
+
if (fetched != null)
|
|
17468
|
+
return String(fetched);
|
|
17469
|
+
return "unknown";
|
|
17470
|
+
}
|
|
16380
17471
|
function unwrapList(res) {
|
|
16381
17472
|
if (Array.isArray(res))
|
|
16382
17473
|
return res;
|
|
@@ -16566,7 +17657,16 @@ ${items.length} collections`));
|
|
|
16566
17657
|
if (col.slug)
|
|
16567
17658
|
console.log(` Slug: ${col.slug}`);
|
|
16568
17659
|
console.log(` Status: ${col.is_active !== false ? source_default.green("active") : source_default.dim("inactive")}`);
|
|
16569
|
-
|
|
17660
|
+
let fetchedCount = null;
|
|
17661
|
+
if (col.document_count == null) {
|
|
17662
|
+
try {
|
|
17663
|
+
const docsRes = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(col.id)}/documents`, { params: { active_only: "true" } });
|
|
17664
|
+
fetchedCount = unwrapList(docsRes).length;
|
|
17665
|
+
} catch {
|
|
17666
|
+
fetchedCount = null;
|
|
17667
|
+
}
|
|
17668
|
+
}
|
|
17669
|
+
console.log(` Documents: ${documentCountLabel(col.document_count, fetchedCount)}`);
|
|
16570
17670
|
console.log(` Storage: ${col.storage_backend ?? "-"}`);
|
|
16571
17671
|
console.log(` Drive: ${col.drive_folder_id ? source_default.cyan(col.drive_folder_id) : source_default.dim("not linked")}`);
|
|
16572
17672
|
if (col.total_size_bytes != null) {
|
|
@@ -21301,6 +22401,125 @@ function registerPolicyCommands(program2) {
|
|
|
21301
22401
|
});
|
|
21302
22402
|
}
|
|
21303
22403
|
|
|
22404
|
+
// src/commands/agent-step.ts
|
|
22405
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
22406
|
+
init_errors();
|
|
22407
|
+
var BASE = "/api/admin/agent-step/trust-roots";
|
|
22408
|
+
function resolveKeyMaterial(value) {
|
|
22409
|
+
if (value.startsWith("@")) {
|
|
22410
|
+
const path9 = value.slice(1);
|
|
22411
|
+
try {
|
|
22412
|
+
return readFileSync3(path9, "utf8").trim();
|
|
22413
|
+
} catch (e) {
|
|
22414
|
+
throw new CLIError(`Could not read public key file '${path9}': ${e.message}`, 4 /* Validation */);
|
|
22415
|
+
}
|
|
22416
|
+
}
|
|
22417
|
+
return value;
|
|
22418
|
+
}
|
|
22419
|
+
function printOne(tr) {
|
|
22420
|
+
const status = tr.is_active ? source_default.green("active") : source_default.red("revoked");
|
|
22421
|
+
console.log(`${source_default.bold(tr.name)} [${status}]`);
|
|
22422
|
+
console.log(source_default.dim("-".repeat(40)));
|
|
22423
|
+
console.log(` Algorithm: ${tr.algorithm}`);
|
|
22424
|
+
if (tr.key_id)
|
|
22425
|
+
console.log(` Key id: ${tr.key_id}`);
|
|
22426
|
+
if (tr.bound_runner)
|
|
22427
|
+
console.log(` Bound runner: ${tr.bound_runner}`);
|
|
22428
|
+
if (tr.description)
|
|
22429
|
+
console.log(` Description: ${tr.description}`);
|
|
22430
|
+
if (tr.created_by)
|
|
22431
|
+
console.log(` Created by: ${tr.created_by}`);
|
|
22432
|
+
if (tr.created_at)
|
|
22433
|
+
console.log(` Created: ${tr.created_at}`);
|
|
22434
|
+
if (tr.updated_at)
|
|
22435
|
+
console.log(` Updated: ${tr.updated_at}`);
|
|
22436
|
+
}
|
|
22437
|
+
function registerAgentStepCommands(program2) {
|
|
22438
|
+
const cmd = program2.command("agent-step").description("Author agent_step assets (execution_proof trust roots)");
|
|
22439
|
+
const roots = cmd.command("trust-roots").description("Manage per-tenant/per-runner attestation trust roots (#1083)");
|
|
22440
|
+
function getCtx() {
|
|
22441
|
+
return createContext({
|
|
22442
|
+
profileOverride: program2.opts().profile,
|
|
22443
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
22444
|
+
verbose: program2.opts().verbose
|
|
22445
|
+
});
|
|
22446
|
+
}
|
|
22447
|
+
const isJson = () => !!program2.opts().json;
|
|
22448
|
+
roots.command("list").description("List the tenant's trust roots").action(async () => {
|
|
22449
|
+
const ctx = getCtx();
|
|
22450
|
+
const rows = await ctx.api.get(BASE);
|
|
22451
|
+
if (isJson()) {
|
|
22452
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
22453
|
+
return;
|
|
22454
|
+
}
|
|
22455
|
+
if (rows.length === 0) {
|
|
22456
|
+
console.log(source_default.dim("No trust roots registered."));
|
|
22457
|
+
return;
|
|
22458
|
+
}
|
|
22459
|
+
for (const r of rows) {
|
|
22460
|
+
const status = r.is_active ? source_default.green("active") : source_default.red("revoked");
|
|
22461
|
+
const runner = r.bound_runner ? ` runner=${r.bound_runner}` : "";
|
|
22462
|
+
console.log(`${source_default.bold(r.name)} [${status}] ${r.algorithm}${runner}`);
|
|
22463
|
+
}
|
|
22464
|
+
});
|
|
22465
|
+
roots.command("get <name>").description("Show one trust root").action(async (name) => {
|
|
22466
|
+
const ctx = getCtx();
|
|
22467
|
+
const tr = await ctx.api.get(`${BASE}/${encodeURIComponent(name)}`);
|
|
22468
|
+
if (isJson()) {
|
|
22469
|
+
console.log(JSON.stringify(tr, null, 2));
|
|
22470
|
+
return;
|
|
22471
|
+
}
|
|
22472
|
+
printOne(tr);
|
|
22473
|
+
});
|
|
22474
|
+
roots.command("register <name>").alias("create").description("Register a signer's public key as a trust root").requiredOption("-k, --public-key <pemOrBase64>", "PEM text or raw-base64 public key, or @path to read it from a file").option("-a, --algorithm <algo>", "Signature algorithm", "ed25519").option("--key-id <id>", "Key id the signed envelope must match").option("--bound-runner <runner>", "AgentDefinition/runner name this identity belongs to").option("-d, --description <text>", "Human description").action(async (name, opts) => {
|
|
22475
|
+
const ctx = getCtx();
|
|
22476
|
+
const body = {
|
|
22477
|
+
name,
|
|
22478
|
+
public_key: resolveKeyMaterial(opts.publicKey),
|
|
22479
|
+
algorithm: opts.algorithm
|
|
22480
|
+
};
|
|
22481
|
+
if (opts.keyId)
|
|
22482
|
+
body.key_id = opts.keyId;
|
|
22483
|
+
if (opts.boundRunner)
|
|
22484
|
+
body.bound_runner = opts.boundRunner;
|
|
22485
|
+
if (opts.description)
|
|
22486
|
+
body.description = opts.description;
|
|
22487
|
+
const tr = await ctx.api.post(BASE, body);
|
|
22488
|
+
if (isJson()) {
|
|
22489
|
+
console.log(JSON.stringify(tr, null, 2));
|
|
22490
|
+
return;
|
|
22491
|
+
}
|
|
22492
|
+
console.log(source_default.green(`Registered trust root '${tr.name}'`));
|
|
22493
|
+
printOne(tr);
|
|
22494
|
+
});
|
|
22495
|
+
roots.command("revoke <name>").description("Deactivate a trust root (no hard delete; preserves the audit trail)").action(async (name) => {
|
|
22496
|
+
const ctx = getCtx();
|
|
22497
|
+
const tr = await ctx.api.patch(`${BASE}/${encodeURIComponent(name)}`, {
|
|
22498
|
+
is_active: false
|
|
22499
|
+
});
|
|
22500
|
+
if (isJson()) {
|
|
22501
|
+
console.log(JSON.stringify(tr, null, 2));
|
|
22502
|
+
return;
|
|
22503
|
+
}
|
|
22504
|
+
console.log(source_default.yellow(`Revoked trust root '${tr.name}' (is_active=false)`));
|
|
22505
|
+
});
|
|
22506
|
+
roots.command("rotate <name>").description("Rotate a trust root's public key (and optionally key id)").requiredOption("-k, --public-key <pemOrBase64>", "New PEM text or raw-base64 public key, or @path to read it from a file").option("--key-id <id>", "New key id the signed envelope must match").action(async (name, opts) => {
|
|
22507
|
+
const ctx = getCtx();
|
|
22508
|
+
const body = {
|
|
22509
|
+
public_key: resolveKeyMaterial(opts.publicKey)
|
|
22510
|
+
};
|
|
22511
|
+
if (opts.keyId)
|
|
22512
|
+
body.key_id = opts.keyId;
|
|
22513
|
+
const tr = await ctx.api.patch(`${BASE}/${encodeURIComponent(name)}`, body);
|
|
22514
|
+
if (isJson()) {
|
|
22515
|
+
console.log(JSON.stringify(tr, null, 2));
|
|
22516
|
+
return;
|
|
22517
|
+
}
|
|
22518
|
+
console.log(source_default.green(`Rotated trust root '${tr.name}'`));
|
|
22519
|
+
printOne(tr);
|
|
22520
|
+
});
|
|
22521
|
+
}
|
|
22522
|
+
|
|
21304
22523
|
// src/commands/authz.ts
|
|
21305
22524
|
init_errors();
|
|
21306
22525
|
|
|
@@ -21862,7 +23081,7 @@ function normalizeList(values) {
|
|
|
21862
23081
|
return [];
|
|
21863
23082
|
return values.flatMap((v) => v.split(/[,\s]+/)).map((v) => v.trim()).filter(Boolean);
|
|
21864
23083
|
}
|
|
21865
|
-
function
|
|
23084
|
+
function parseIntents2(raw) {
|
|
21866
23085
|
const out = [];
|
|
21867
23086
|
for (const v of raw) {
|
|
21868
23087
|
if (!VALID_INTENTS.includes(v)) {
|
|
@@ -21974,7 +23193,7 @@ Provision an access principal now so calls just work?`, true);
|
|
|
21974
23193
|
} else {
|
|
21975
23194
|
mode = "human";
|
|
21976
23195
|
}
|
|
21977
|
-
let intents =
|
|
23196
|
+
let intents = parseIntents2(normalizeList(opts.intent));
|
|
21978
23197
|
if (intents.length === 0 && interactive) {
|
|
21979
23198
|
intents = await askIntents();
|
|
21980
23199
|
}
|
|
@@ -22179,7 +23398,7 @@ async function askIntents() {
|
|
|
22179
23398
|
const n = Number(t);
|
|
22180
23399
|
return Number.isInteger(n) && n >= 1 && n <= VALID_INTENTS.length ? VALID_INTENTS[n - 1] : t;
|
|
22181
23400
|
});
|
|
22182
|
-
return
|
|
23401
|
+
return parseIntents2(raw);
|
|
22183
23402
|
}
|
|
22184
23403
|
function registerInitCommand(program2) {
|
|
22185
23404
|
program2.command("init").description("Create or update a profile, and optionally bootstrap an access principal").option("--preset <name>", "Preset: cloud or local", "cloud").option("--name <name>", "Profile name (defaults to preset name)").option("--force", "Overwrite an existing profile").option("--api-url <url>", "Override the API URL").option("--keycloak-url <url>", "Override the Keycloak URL").option("--keycloak-realm <realm>", "Override the Keycloak realm").option("--no-interactive", "Skip prompts; use defaults / overrides only").option("--bootstrap", "Provision an intent-scoped principal + grants after saving the profile").option("--mode <mode>", "Bootstrap mode: 'human' (your own client) or 'integration' (service account)").option("--intent <intents...>", "Usage buckets to grant: rag, documents, workflows, custom").option("--workflow <names...>", "Workflow name(s) to grant (workflows/custom intent)").option("--function <names...>", "Extra function name(s) to grant (custom intent)").option("--label <name>", "Integration mode: label for the SA client id").option("--agent", "Also provision a personal chat copilot (human mode) and set it as the default").option("--agent-name <name>", "Copilot agent name (defaults to copilot-{user})").option("--model <id>", "Copilot LLM model id (defaults to the platform default)").option("--host-runner", "Also set the copilot up to run host tools on THIS machine (mints a runner key + prints the `martha runner` command)").option("--yes", "Acknowledge non-interactive bootstrap side effects").action(async (opts, command) => {
|
|
@@ -22694,6 +23913,9 @@ auth.command("token").description("Print current access token").action(async ()
|
|
|
22694
23913
|
await authTokenCommand(ctx);
|
|
22695
23914
|
});
|
|
22696
23915
|
registerChatCommand(program2);
|
|
23916
|
+
registerAcpServeCommand(program2);
|
|
23917
|
+
registerPrincipalCommands(program2);
|
|
23918
|
+
registerChannelCommands(program2);
|
|
22697
23919
|
registerConfigCommand(program2);
|
|
22698
23920
|
registerDefinitionCommands(program2, functionsConfig);
|
|
22699
23921
|
registerDefinitionCommands(program2, workflowsConfig);
|
|
@@ -22719,6 +23941,7 @@ registerModelsCommand(program2);
|
|
|
22719
23941
|
registerSessionCommands(program2);
|
|
22720
23942
|
registerMCPCommands(program2);
|
|
22721
23943
|
registerPolicyCommands(program2);
|
|
23944
|
+
registerAgentStepCommands(program2);
|
|
22722
23945
|
registerAuthzCommands(program2);
|
|
22723
23946
|
registerInitCommand(program2);
|
|
22724
23947
|
registerDoctorCommand(program2);
|