@aiaiai-pt/martha-cli 0.51.7 → 0.53.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/index.js +1289 -34
  3. 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 Error(`Client credentials login failed (${res.status}): ${text}`);
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.51.7";
11132
+ var CLI_VERSION = "0.53.1";
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 grantingClients = [];
11500
+ let scan = null;
11450
11501
  try {
11451
- const clientsResp = await ctx.api.get("/api/admin/clients");
11452
- const clientList = Array.isArray(clientsResp) ? clientsResp : clientsResp?.clients ?? clientsResp?.items ?? [];
11453
- const probes = await Promise.all(clientList.slice(0, 25).map(async (c) => {
11454
- try {
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
- for (const c of dx.checks) {
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,1062 @@ 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
+ function buzzAppCandidates(platform, env2, home) {
14774
+ if (platform === "darwin") {
14775
+ return ["/Applications/Buzz.app", join3(home, "Applications", "Buzz.app")];
14776
+ }
14777
+ if (platform === "win32") {
14778
+ const base = env2.LOCALAPPDATA ?? join3(home, "AppData", "Local");
14779
+ return [join3(base, "Programs", "Buzz")];
14780
+ }
14781
+ return [
14782
+ "/opt/Buzz",
14783
+ "/usr/bin/buzz-desktop",
14784
+ join3(home, ".local", "bin", "buzz-desktop")
14785
+ ];
14786
+ }
14787
+ var BUZZ_RELEASES_URL = "https://github.com/block/buzz/releases/latest";
14788
+ function missingAppDataMessage(candidates, appInstalled) {
14789
+ const looked = candidates.map((c) => ` ${c.dir} (${c.build})`).join(`
14790
+ `);
14791
+ if (appInstalled) {
14792
+ return `Buzz Desktop is installed but has never been launched — it creates its ` + `data directory on first run.
14793
+
14794
+ Looked for:
14795
+ ${looked}
14796
+ ` + `Open Buzz Desktop once, then re-run this. Or pass --app-data <dir>.`;
14797
+ }
14798
+ return `Buzz Desktop is not installed on this machine. This command registers ` + `Martha as a runtime INSIDE Desktop, so there is nothing to register with.
14799
+
14800
+ ` + `Looked for:
14801
+ ${looked}
14802
+ ` + `Install it: ${BUZZ_RELEASES_URL}
14803
+
14804
+ ` + `If you do not want Desktop at all, you do not need this command — run the ` + `adapter yourself instead:
14805
+ martha channels connect buzz --agent <name> --relay <url>`;
14806
+ }
14807
+ var DEFAULT_BUZZ_KINDS = "9,46010,40007,7";
14808
+ function buzzAcpEnv(opts) {
14809
+ const env2 = {
14810
+ BUZZ_RELAY_URL: opts.relayUrl,
14811
+ BUZZ_ACP_AGENT_COMMAND: opts.marthaBin,
14812
+ BUZZ_ACP_AGENT_ARGS: "acp-serve,--post-reply",
14813
+ BUZZ_ACP_SUBSCRIBE: opts.subscribe,
14814
+ BUZZ_ACP_MULTIPLE_EVENT_HANDLING: opts.multipleEventHandling,
14815
+ BUZZ_ACP_RESPOND_TO: opts.respondTo
14816
+ };
14817
+ if (opts.subscribe === "all")
14818
+ env2.BUZZ_ACP_KINDS = opts.kinds;
14819
+ return env2;
14820
+ }
14821
+ function resolveOnPath(bin, env2, platform, isExecutable) {
14822
+ if (bin.includes("/") || bin.includes("\\")) {
14823
+ return isExecutable(bin) ? bin : null;
14824
+ }
14825
+ const exts = platform === "win32" ? (env2.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
14826
+ const sep = platform === "win32" ? ";" : ":";
14827
+ for (const dir of (env2.PATH ?? "").split(sep).filter(Boolean)) {
14828
+ for (const ext of exts) {
14829
+ const candidate = join3(dir, bin + ext);
14830
+ if (isExecutable(candidate))
14831
+ return candidate;
14832
+ }
14833
+ }
14834
+ return null;
14835
+ }
14836
+ function executableOnDisk(p) {
14837
+ try {
14838
+ accessSync(p, constants.X_OK);
14839
+ return true;
14840
+ } catch {
14841
+ return false;
14842
+ }
14843
+ }
14844
+ function which(bin) {
14845
+ return resolveOnPath(bin, process.env, process.platform, executableOnDisk);
14846
+ }
14847
+ function runBin(bin, args, env2) {
14848
+ return new Promise((resolve2) => {
14849
+ execFile2(bin, args, { env: env2, timeout: 15000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
14850
+ const code = err && typeof err.code === "number" ? err.code ?? 1 : err ? 1 : 0;
14851
+ resolve2({
14852
+ code,
14853
+ stdout: stdout ?? "",
14854
+ stderr: (stderr ?? "").slice(0, 2000)
14855
+ });
14856
+ });
14857
+ });
14858
+ }
14859
+ async function reachable(url) {
14860
+ try {
14861
+ const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
14862
+ return { ok: true, detail: `${url} → HTTP ${res.status}` };
14863
+ } catch (err) {
14864
+ return {
14865
+ ok: false,
14866
+ detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`
14867
+ };
14868
+ }
14869
+ }
14870
+ function relayHttpUrl(relayUrl) {
14871
+ return relayUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
14872
+ }
14873
+ async function preflightBuzz(ctx, opts) {
14874
+ const checks = [];
14875
+ const marthaPath = which(opts.marthaBin);
14876
+ checks.push({
14877
+ ok: !!marthaPath,
14878
+ label: "martha CLI on PATH",
14879
+ detail: marthaPath ?? `'${opts.marthaBin}' not found`,
14880
+ fix: marthaPath ? undefined : "install it: npm i -g @aiaiai-pt/martha-cli (buzz-acp spawns it by name)"
14881
+ });
14882
+ if (marthaPath) {
14883
+ const probe = await runBin(marthaPath, ["--help"], process.env);
14884
+ const supported = /^\s+acp-serve\b/m.test(probe.stdout);
14885
+ checks.push({
14886
+ ok: supported,
14887
+ label: "that martha can serve ACP",
14888
+ detail: supported ? "acp-serve available" : `${marthaPath} has no acp-serve subcommand`,
14889
+ 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)"
14890
+ });
14891
+ }
14892
+ const acpPath = which(opts.buzzAcpBin);
14893
+ checks.push({
14894
+ ok: !!acpPath,
14895
+ label: "buzz-acp on PATH",
14896
+ detail: acpPath ?? `'${opts.buzzAcpBin}' not found`,
14897
+ fix: acpPath ? undefined : "build it from block/buzz: cargo build -p buzz-acp, then put target/debug on PATH (or pass --buzz-acp-bin)"
14898
+ });
14899
+ const buzzPath = which(opts.buzzBin);
14900
+ checks.push({
14901
+ ok: !!buzzPath,
14902
+ label: "buzz CLI on PATH",
14903
+ detail: buzzPath ?? `'${opts.buzzBin}' not found`,
14904
+ fix: buzzPath ? undefined : "build it from block/buzz: cargo build -p buzz-cli — acp-serve --post-reply shells it to deliver the answer"
14905
+ });
14906
+ let whoami = null;
14907
+ try {
14908
+ whoami = await ctx.api.get("/api/tasks/whoami");
14909
+ checks.push({
14910
+ ok: true,
14911
+ label: "Martha API + credential",
14912
+ detail: `${ctx.profile.api_url} — ${whoami.kind ?? "?"}${whoami.agent_name ? ` (${whoami.agent_name})` : whoami.sub ? ` (${whoami.sub})` : ""}`
14913
+ });
14914
+ } catch (err) {
14915
+ const status = err instanceof MarthaAPIError ? err.status : undefined;
14916
+ const rejectedByApi = status === 401 || status === 403;
14917
+ const rejectedByIdp = !rejectedByApi && err instanceof CLIError && err.exitCode === 2 /* Auth */;
14918
+ checks.push({
14919
+ ok: false,
14920
+ label: "Martha API + credential",
14921
+ 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)}`,
14922
+ 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"
14923
+ });
14924
+ }
14925
+ let def = null;
14926
+ try {
14927
+ def = await ctx.api.get(`${AGENTS_PATH2}/${encodeURIComponent(opts.agent)}`);
14928
+ } catch (err) {
14929
+ if (!(err instanceof MarthaAPIError && err.status === 404)) {
14930
+ checks.push({
14931
+ ok: null,
14932
+ label: "agent exists",
14933
+ detail: `could not read '${opts.agent}': ${err instanceof Error ? err.message : String(err)}`,
14934
+ fix: "this credential may not be allowed to read agent definitions — check with `martha agents list`"
14935
+ });
14936
+ }
14937
+ }
14938
+ if (def) {
14939
+ const marthaHosted = String(def.agent_type ?? "") !== "external";
14940
+ checks.push({
14941
+ ok: marthaHosted,
14942
+ label: "agent is Martha-hosted",
14943
+ detail: marthaHosted ? String(def.agent_type ?? "cloud") : "external (self-hosted)",
14944
+ fix: marthaHosted ? undefined : "acp-serve drives a Martha-hosted agent — recreate it with --reasoning martha"
14945
+ });
14946
+ const takeover = def.runs_as_chat === true;
14947
+ checks.push({
14948
+ ok: takeover,
14949
+ label: "chat takeover (runs_as_chat)",
14950
+ fix: takeover ? undefined : `run: martha principals bootstrap --mode agent --agent ${opts.agent}`
14951
+ });
14952
+ } else if (!checks.some((c) => c.label === "agent exists")) {
14953
+ checks.push({
14954
+ ok: false,
14955
+ label: "agent exists",
14956
+ detail: `'${opts.agent}' not found`,
14957
+ fix: `create it: martha principals bootstrap --mode agent --agent ${opts.agent} --prompt "…"`
14958
+ });
14959
+ }
14960
+ checks.push(await checkAgentPrincipal(ctx, opts.agent, def));
14961
+ const hasSa = !!(process.env.MARTHA_CLIENT_ID && process.env.MARTHA_CLIENT_SECRET);
14962
+ const hasToken = !!process.env.MARTHA_TOKEN;
14963
+ checks.push({
14964
+ ok: hasSa || hasToken ? true : null,
14965
+ label: "durable credential in env",
14966
+ 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",
14967
+ fix: hasSa || hasToken ? undefined : `mint one: martha principals bootstrap --mode agent --agent ${opts.agent}`
14968
+ });
14969
+ const relayProbe = await reachable(relayHttpUrl(opts.relayUrl));
14970
+ checks.push({
14971
+ ok: relayProbe.ok,
14972
+ label: "relay reachable",
14973
+ detail: relayProbe.detail,
14974
+ fix: relayProbe.ok ? undefined : `start the relay, or correct --relay (${opts.relayUrl})`
14975
+ });
14976
+ const identity2 = process.env.BUZZ_PRIVATE_KEY;
14977
+ checks.push({
14978
+ ok: !!identity2,
14979
+ label: "channel identity",
14980
+ detail: identity2 ? "BUZZ_PRIVATE_KEY set (not shown)" : "BUZZ_PRIVATE_KEY not set",
14981
+ fix: identity2 ? undefined : "export BUZZ_PRIVATE_KEY=<the agent's nsec> — Martha-issued identities land in #1119"
14982
+ });
14983
+ if (identity2 && relayProbe.ok && buzzPath) {
14984
+ checks.push(await checkMembership(buzzPath, opts.relayUrl, identity2, opts.channelId));
14985
+ } else {
14986
+ checks.push({
14987
+ ok: null,
14988
+ label: "channel membership",
14989
+ detail: "skipped — needs the identity, a reachable relay, and the buzz CLI"
14990
+ });
14991
+ }
14992
+ return checks;
14993
+ }
14994
+ async function checkAgentPrincipal(ctx, agent, def) {
14995
+ const label = "agent principal (bound client)";
14996
+ const fix = `run: martha principals bootstrap --mode agent --agent ${agent}`;
14997
+ try {
14998
+ const scan = await scanClientsForAgent(ctx, agent);
14999
+ if (!scan.granting.length) {
15000
+ return scan.incomplete ? {
15001
+ ok: null,
15002
+ label,
15003
+ detail: `inconclusive — read ${scan.scanned} of ${scan.total} clients`
15004
+ } : { ok: false, label, detail: "no client is bound to this agent", fix };
15005
+ }
15006
+ const shared = scan.granting.filter((h) => h.agents.length > 1);
15007
+ const names = scan.granting.map((h) => h.client);
15008
+ if (shared.length) {
15009
+ return {
15010
+ ok: null,
15011
+ label,
15012
+ detail: `bound to ${names.join(", ")} — but ${shared.map((h) => h.client).join(", ")} also has other agents, which disables chat takeover`,
15013
+ fix: "give this agent its own principal: martha principals bootstrap --mode agent --label <fresh>"
15014
+ };
15015
+ }
15016
+ return { ok: true, label, detail: names.join(", ") };
15017
+ } catch (err) {
15018
+ const status = err instanceof MarthaAPIError ? err.status : undefined;
15019
+ return {
15020
+ ok: null,
15021
+ label,
15022
+ detail: status === 403 || status === 401 ? `not verifiable with this credential (HTTP ${status})` : `not verifiable: ${err instanceof Error ? err.message : String(err)}`,
15023
+ fix: def ? undefined : fix
15024
+ };
15025
+ }
15026
+ }
15027
+ async function checkMembership(buzzBin, relayUrl, identity2, channelId) {
15028
+ const label = "channel membership";
15029
+ const res = await runBin(buzzBin, ["--format", "compact", "channels", "list", "--member"], {
15030
+ ...process.env,
15031
+ BUZZ_RELAY_URL: relayUrl,
15032
+ BUZZ_PRIVATE_KEY: identity2
15033
+ });
15034
+ if (res.code !== 0) {
15035
+ const why = res.code === 3 ? "the relay rejected this identity" : res.code === 2 ? "relay/network error" : `buzz exited ${res.code}`;
15036
+ return {
15037
+ ok: false,
15038
+ label,
15039
+ detail: `${why}: ${res.stderr.trim().slice(0, 200)}`,
15040
+ fix: "check BUZZ_PRIVATE_KEY belongs to a registered user on this relay"
15041
+ };
15042
+ }
15043
+ let channels = [];
15044
+ try {
15045
+ channels = JSON.parse(res.stdout.trim() || "[]");
15046
+ } catch {
15047
+ return {
15048
+ ok: null,
15049
+ label,
15050
+ detail: `could not parse \`buzz channels list\` output (${res.stdout.slice(0, 120)})`
15051
+ };
15052
+ }
15053
+ if (channelId) {
15054
+ const hit = channels.find((c) => c.channel_id === channelId);
15055
+ return {
15056
+ ok: !!hit,
15057
+ label,
15058
+ detail: hit ? `${hit.name ?? channelId}` : `not a member of ${channelId}`,
15059
+ fix: hit ? undefined : `buzz channels add-member --channel ${channelId} --pubkey <the agent's pubkey> --role member`
15060
+ };
15061
+ }
15062
+ return {
15063
+ ok: channels.length > 0,
15064
+ label,
15065
+ 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",
15066
+ fix: channels.length ? undefined : "buzz channels add-member --channel <id> --pubkey <the agent's pubkey> --role member"
15067
+ };
15068
+ }
15069
+ function emitManifest(kind, env2, opts) {
15070
+ const secretKeys = [
15071
+ "BUZZ_PRIVATE_KEY",
15072
+ "MARTHA_CLIENT_ID",
15073
+ "MARTHA_CLIENT_SECRET"
15074
+ ];
15075
+ const entries = Object.entries(env2);
15076
+ if (kind === "docker") {
15077
+ const lines = [
15078
+ "docker run --rm \\",
15079
+ ...entries.map(([k, v]) => ` -e ${k}=${JSON.stringify(v)} \\`),
15080
+ ...secretKeys.map((k) => ` -e ${k}="$${k}" \\`),
15081
+ ` ${opts.image}`
15082
+ ];
15083
+ return lines.join(`
15084
+ `);
15085
+ }
15086
+ if (kind === "compose") {
15087
+ return [
15088
+ "services:",
15089
+ ` martha-buzz-${opts.agent}:`,
15090
+ ` image: ${opts.image}`,
15091
+ " restart: unless-stopped",
15092
+ " environment:",
15093
+ ...entries.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`),
15094
+ ...secretKeys.map((k) => ` ${k}: \${${k}:?set ${k}}`)
15095
+ ].join(`
15096
+ `);
15097
+ }
15098
+ return [
15099
+ "apiVersion: apps/v1",
15100
+ "kind: Deployment",
15101
+ "metadata:",
15102
+ ` name: martha-buzz-${opts.agent}`,
15103
+ "spec:",
15104
+ " replicas: 1",
15105
+ " selector:",
15106
+ ` matchLabels: { app: martha-buzz-${opts.agent} }`,
15107
+ " template:",
15108
+ " metadata:",
15109
+ ` labels: { app: martha-buzz-${opts.agent} }`,
15110
+ " spec:",
15111
+ " containers:",
15112
+ ` - name: adapter`,
15113
+ ` image: ${opts.image}`,
15114
+ " env:",
15115
+ ...entries.flatMap(([k, v]) => [
15116
+ ` - name: ${k}`,
15117
+ ` value: ${JSON.stringify(v)}`
15118
+ ]),
15119
+ ...secretKeys.flatMap((k) => [
15120
+ ` - name: ${k}`,
15121
+ " valueFrom:",
15122
+ ` secretKeyRef: { name: martha-buzz-${opts.agent}, key: ${k} }`
15123
+ ])
15124
+ ].join(`
15125
+ `);
15126
+ }
15127
+ function assertSupportedChannel(kind) {
15128
+ if (!SUPPORTED_CHANNELS.includes(kind)) {
15129
+ 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.`);
15130
+ }
15131
+ }
15132
+ function registerChannelCommands(program2) {
15133
+ const channels = program2.command("channels").description("Put a Martha agent in an external conversation channel — from Buzz (install-harness) or from Martha (connect)");
15134
+ 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) => {
15135
+ assertSupportedChannel(type);
15136
+ const def = buzzHarnessDefinition({
15137
+ id: opts.id,
15138
+ label: opts.label,
15139
+ command: opts.command
15140
+ });
15141
+ if (!isValidHarnessId(def.id)) {
15142
+ 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.");
15143
+ }
15144
+ const json = JSON.stringify(def, null, 2) + `
15145
+ `;
15146
+ if (opts.print) {
15147
+ console.log(json);
15148
+ return;
15149
+ }
15150
+ const chosen = opts.appData ? { build: "release", dir: String(opts.appData) } : chooseAppDataDir(buzzAppDataCandidates(process.platform, process.env, homedir()), (d) => existsSync(d));
15151
+ if (!chosen) {
15152
+ const candidates = buzzAppDataCandidates(process.platform, process.env, homedir());
15153
+ const appInstalled = buzzAppCandidates(process.platform, process.env, homedir()).some((p) => existsSync(p));
15154
+ throw new CLIError(appInstalled ? "Buzz Desktop has no data directory yet." : "Buzz Desktop is not installed.", 3 /* NotFound */, missingAppDataMessage(candidates, appInstalled));
15155
+ }
15156
+ const dir = join3(chosen.dir, "custom_harnesses");
15157
+ const file = join3(dir, `${def.id}.json`);
15158
+ if (existsSync(file) && !opts.force) {
15159
+ throw new CLIError(`${file} already exists.`, 5 /* Conflict */, "Re-run with --force to overwrite it.");
15160
+ }
15161
+ mkdirSync(dir, { recursive: true });
15162
+ writeFileSync(file, json, "utf8");
15163
+ console.log(source_default.bold.green(`
15164
+ ✓ registered '${def.label}' as a Buzz runtime
15165
+ `));
15166
+ console.log(source_default.dim(` ${file} (${chosen.build} build)`));
15167
+ console.log();
15168
+ for (const line of installHarnessNotes(def))
15169
+ console.log(line);
15170
+ });
15171
+ 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) => {
15172
+ assertSupportedChannel(type);
15173
+ const ctx = contextFor(program2);
15174
+ const checks = await preflightBuzz(ctx, {
15175
+ agent: opts.agent,
15176
+ relayUrl: opts.relay,
15177
+ channelId: opts.channel,
15178
+ marthaBin: opts.marthaBin,
15179
+ buzzAcpBin: opts.buzzAcpBin,
15180
+ buzzBin: opts.buzzBin
15181
+ });
15182
+ reportPreflight(type, opts.agent, checks);
15183
+ if (checks.some((c) => c.ok === false))
15184
+ process.exitCode = 1 /* Error */;
15185
+ });
15186
+ 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) => {
15187
+ assertSupportedChannel(type);
15188
+ if (opts.subscribe !== "mentions" && opts.subscribe !== "all") {
15189
+ throw new CLIError(`--subscribe must be 'mentions' or 'all' (got '${opts.subscribe}').`, 4 /* Validation */);
15190
+ }
15191
+ const ctx = contextFor(program2);
15192
+ const checks = await preflightBuzz(ctx, {
15193
+ agent: opts.agent,
15194
+ relayUrl: opts.relay,
15195
+ channelId: opts.channel,
15196
+ marthaBin: opts.marthaBin,
15197
+ buzzAcpBin: opts.buzzAcpBin,
15198
+ buzzBin: opts.buzzBin
15199
+ });
15200
+ reportPreflight(type, opts.agent, checks, opts.emit ? "stderr" : "stdout");
15201
+ const env2 = buzzAcpEnv({
15202
+ relayUrl: opts.relay,
15203
+ subscribe: opts.subscribe,
15204
+ kinds: opts.kinds,
15205
+ multipleEventHandling: "queue",
15206
+ respondTo: opts.respondTo,
15207
+ marthaBin: opts.marthaBin
15208
+ });
15209
+ if (opts.emit) {
15210
+ if (!["docker", "compose", "k8s"].includes(opts.emit)) {
15211
+ throw new CLIError(`--emit must be docker, compose or k8s (got '${opts.emit}').`, 4 /* Validation */);
15212
+ }
15213
+ console.log(emitManifest(opts.emit, env2, {
15214
+ agent: opts.agent,
15215
+ image: opts.image
15216
+ }));
15217
+ return;
15218
+ }
15219
+ const failed = checks.filter((c) => c.ok === false);
15220
+ if (failed.length) {
15221
+ console.log(source_default.red.bold(`Not connecting — ${failed.length} precondition(s) unmet. Nothing was started.`));
15222
+ process.exitCode = 1 /* Error */;
15223
+ return;
15224
+ }
15225
+ const acp = which(opts.buzzAcpBin);
15226
+ if (opts.dryRun) {
15227
+ console.log(source_default.bold(`Would launch:
15228
+ `));
15229
+ for (const [k, v] of Object.entries(env2))
15230
+ console.log(` ${k}=${v}`);
15231
+ console.log(` ${acp}
15232
+ `);
15233
+ return;
15234
+ }
15235
+ console.log(source_default.bold(`
15236
+ Connecting '${opts.agent}' to ${opts.relay} …
15237
+ `));
15238
+ const child = spawn3(acp, [], {
15239
+ env: { ...process.env, ...env2 },
15240
+ stdio: "inherit"
15241
+ });
15242
+ await new Promise((resolve2) => {
15243
+ child.on("exit", (code, signal) => {
15244
+ if (signal) {
15245
+ console.log(source_default.dim(`
15246
+ buzz-acp terminated by ${signal}`));
15247
+ process.exitCode = 1 /* Error */;
15248
+ } else if (code) {
15249
+ process.exitCode = code;
15250
+ }
15251
+ resolve2();
15252
+ });
15253
+ child.on("error", (err) => {
15254
+ console.error(source_default.red(`failed to start ${acp}: ${err.message}`));
15255
+ process.exitCode = 1 /* Error */;
15256
+ resolve2();
15257
+ });
15258
+ });
15259
+ });
15260
+ }
15261
+ function contextFor(program2) {
15262
+ return createContext({
15263
+ profileOverride: program2.opts().profile,
15264
+ apiUrlOverride: program2.opts().apiUrl,
15265
+ verbose: program2.opts().verbose
15266
+ });
15267
+ }
15268
+ function reportPreflight(type, agent, checks, stream = "stdout") {
15269
+ const write = stream === "stderr" ? (line = "") => process.stderr.write(line + `
15270
+ `) : (line = "") => console.log(line);
15271
+ write(source_default.bold(`Martha CLI preflight — ${type} channel: ${agent}
15272
+ `));
15273
+ printChecks(checks, write);
15274
+ const failed = checks.filter((c) => c.ok === false).length;
15275
+ const unknown = checks.filter((c) => c.ok === null).length;
15276
+ write();
15277
+ if (failed) {
15278
+ write(source_default.red.bold(` ${failed} precondition(s) unmet — fix the FAIL items above.`));
15279
+ } else if (unknown) {
15280
+ write(source_default.yellow.bold(` All required preconditions met (${unknown} unverifiable).`));
15281
+ } else {
15282
+ write(source_default.green.bold(" Ready."));
15283
+ }
15284
+ }
15285
+ function installHarnessNotes(def) {
15286
+ return [
15287
+ "In Buzz Desktop:",
15288
+ source_default.cyan(` 1. New agent → runtime "${def.label}"`),
15289
+ source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
15290
+ source_default.dim(" (credentials belong per-agent, never in the shared definition file)"),
15291
+ "",
15292
+ source_default.dim(`Desktop owns the rest: the Nostr keypair in your keyring, the NIP-OA
15293
+ ` + "attestation, spawning buzz-acp, and attaching the agent to channels."),
15294
+ "",
15295
+ source_default.dim(`If you set BUZZ_ACP_SUBSCRIBE=all on the agent, you MUST also set
15296
+ ` + `BUZZ_ACP_KINDS=${DEFAULT_BUZZ_KINDS} — 'all' with no kinds is a wildcard
15297
+ ` + "subscription and turns typing and presence events into prompts.")
15298
+ ];
15299
+ }
15300
+
14212
15301
  // src/commands/config.ts
14213
15302
  init_config();
14214
15303
  init_errors();
14215
- import { createInterface as createInterface3 } from "node:readline";
15304
+ import { createInterface as createInterface4 } from "node:readline";
14216
15305
  function prompt(rl, question, defaultValue) {
14217
15306
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
14218
15307
  return new Promise((resolve2) => {
@@ -14237,7 +15326,7 @@ async function configInit(configDir) {
14237
15326
  console.log(`No configuration found. Let's set up your first profile.
14238
15327
  `);
14239
15328
  }
14240
- const rl = createInterface3({
15329
+ const rl = createInterface4({
14241
15330
  input: process.stdin,
14242
15331
  output: process.stderr
14243
15332
  });
@@ -14524,7 +15613,7 @@ function registerConfigCommand(program2) {
14524
15613
  throw new CLIError("Cannot confirm in non-interactive mode.", 4 /* Validation */, "Use --yes to skip confirmation.");
14525
15614
  }
14526
15615
  const ok = await new Promise((resolve2) => {
14527
- const rl = createInterface3({
15616
+ const rl = createInterface4({
14528
15617
  input: process.stdin,
14529
15618
  output: process.stderr
14530
15619
  });
@@ -14576,7 +15665,9 @@ var MUTABLE_FIELDS = {
14576
15665
  "is_active",
14577
15666
  "collection_id_param",
14578
15667
  "risk_tags",
14579
- "risk_level"
15668
+ "risk_level",
15669
+ "verification_safe",
15670
+ "idempotent"
14580
15671
  ]),
14581
15672
  Workflow: new Set([
14582
15673
  "description",
@@ -14591,9 +15682,27 @@ var MUTABLE_FIELDS = {
14591
15682
  "is_active"
14592
15683
  ])
14593
15684
  };
15685
+ var FUNCTION_DECLARATION_FIELDS = new Set([
15686
+ "verification_safe",
15687
+ "idempotent"
15688
+ ]);
14594
15689
  function isPlainObject(value) {
14595
15690
  return value !== null && typeof value === "object" && !Array.isArray(value);
14596
15691
  }
15692
+ function declarationEnabled(value) {
15693
+ if (isPlainObject(value))
15694
+ return value.enabled === true;
15695
+ return value === true;
15696
+ }
15697
+ function declarationFieldMatches(requested, observed) {
15698
+ if (!declarationEnabled(requested)) {
15699
+ return !declarationEnabled(observed);
15700
+ }
15701
+ if (isPlainObject(observed)) {
15702
+ return observed.enabled === true && requestedValueMatches(requested, observed);
15703
+ }
15704
+ return observed === true;
15705
+ }
14597
15706
  function valuesEqual(left, right) {
14598
15707
  if (left === right)
14599
15708
  return true;
@@ -14655,6 +15764,10 @@ function normalizeDefinitionWriteBody(kind, body) {
14655
15764
  }
14656
15765
  delete normalized.definition;
14657
15766
  for (const [key, nestedValue] of Object.entries(nested)) {
15767
+ if (kind === "Function" && FUNCTION_DECLARATION_FIELDS.has(key)) {
15768
+ normalized[key] = nestedValue;
15769
+ continue;
15770
+ }
14658
15771
  if (key in normalized && !valuesEqual(normalized[key], nestedValue)) {
14659
15772
  throw new CLIError(`Conflicting values for '${key}' in flat and nested ${kind} fields`, 4 /* Validation */);
14660
15773
  }
@@ -14722,6 +15835,9 @@ function fieldValueMatches(kind, field, requested, observed) {
14722
15835
  if (kind === "Function" && field === "parameters") {
14723
15836
  return parameterMapMatches(requested, observed);
14724
15837
  }
15838
+ if (kind === "Function" && FUNCTION_DECLARATION_FIELDS.has(field)) {
15839
+ return declarationFieldMatches(requested, observed);
15840
+ }
14725
15841
  const isExactTypedValue = kind === "Function" && (field === "request_body" || field === "auth") || kind === "Workflow" && (field === "nodes" || field === "edges");
14726
15842
  if (isExactTypedValue) {
14727
15843
  return valuesEqual(requested, observed);
@@ -16377,6 +17493,13 @@ function colorStatus2(status) {
16377
17493
  const fn = STATUS_COLORS2[status] ?? source_default.white;
16378
17494
  return fn(status);
16379
17495
  }
17496
+ function documentCountLabel(count, fetched) {
17497
+ if (count != null)
17498
+ return String(count);
17499
+ if (fetched != null)
17500
+ return String(fetched);
17501
+ return "unknown";
17502
+ }
16380
17503
  function unwrapList(res) {
16381
17504
  if (Array.isArray(res))
16382
17505
  return res;
@@ -16566,7 +17689,16 @@ ${items.length} collections`));
16566
17689
  if (col.slug)
16567
17690
  console.log(` Slug: ${col.slug}`);
16568
17691
  console.log(` Status: ${col.is_active !== false ? source_default.green("active") : source_default.dim("inactive")}`);
16569
- console.log(` Documents: ${col.document_count ?? 0}`);
17692
+ let fetchedCount = null;
17693
+ if (col.document_count == null) {
17694
+ try {
17695
+ const docsRes = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(col.id)}/documents`, { params: { active_only: "true" } });
17696
+ fetchedCount = unwrapList(docsRes).length;
17697
+ } catch {
17698
+ fetchedCount = null;
17699
+ }
17700
+ }
17701
+ console.log(` Documents: ${documentCountLabel(col.document_count, fetchedCount)}`);
16570
17702
  console.log(` Storage: ${col.storage_backend ?? "-"}`);
16571
17703
  console.log(` Drive: ${col.drive_folder_id ? source_default.cyan(col.drive_folder_id) : source_default.dim("not linked")}`);
16572
17704
  if (col.total_size_bytes != null) {
@@ -21301,6 +22433,125 @@ function registerPolicyCommands(program2) {
21301
22433
  });
21302
22434
  }
21303
22435
 
22436
+ // src/commands/agent-step.ts
22437
+ import { readFileSync as readFileSync3 } from "node:fs";
22438
+ init_errors();
22439
+ var BASE = "/api/admin/agent-step/trust-roots";
22440
+ function resolveKeyMaterial(value) {
22441
+ if (value.startsWith("@")) {
22442
+ const path9 = value.slice(1);
22443
+ try {
22444
+ return readFileSync3(path9, "utf8").trim();
22445
+ } catch (e) {
22446
+ throw new CLIError(`Could not read public key file '${path9}': ${e.message}`, 4 /* Validation */);
22447
+ }
22448
+ }
22449
+ return value;
22450
+ }
22451
+ function printOne(tr) {
22452
+ const status = tr.is_active ? source_default.green("active") : source_default.red("revoked");
22453
+ console.log(`${source_default.bold(tr.name)} [${status}]`);
22454
+ console.log(source_default.dim("-".repeat(40)));
22455
+ console.log(` Algorithm: ${tr.algorithm}`);
22456
+ if (tr.key_id)
22457
+ console.log(` Key id: ${tr.key_id}`);
22458
+ if (tr.bound_runner)
22459
+ console.log(` Bound runner: ${tr.bound_runner}`);
22460
+ if (tr.description)
22461
+ console.log(` Description: ${tr.description}`);
22462
+ if (tr.created_by)
22463
+ console.log(` Created by: ${tr.created_by}`);
22464
+ if (tr.created_at)
22465
+ console.log(` Created: ${tr.created_at}`);
22466
+ if (tr.updated_at)
22467
+ console.log(` Updated: ${tr.updated_at}`);
22468
+ }
22469
+ function registerAgentStepCommands(program2) {
22470
+ const cmd = program2.command("agent-step").description("Author agent_step assets (execution_proof trust roots)");
22471
+ const roots = cmd.command("trust-roots").description("Manage per-tenant/per-runner attestation trust roots (#1083)");
22472
+ function getCtx() {
22473
+ return createContext({
22474
+ profileOverride: program2.opts().profile,
22475
+ apiUrlOverride: program2.opts().apiUrl,
22476
+ verbose: program2.opts().verbose
22477
+ });
22478
+ }
22479
+ const isJson = () => !!program2.opts().json;
22480
+ roots.command("list").description("List the tenant's trust roots").action(async () => {
22481
+ const ctx = getCtx();
22482
+ const rows = await ctx.api.get(BASE);
22483
+ if (isJson()) {
22484
+ console.log(JSON.stringify(rows, null, 2));
22485
+ return;
22486
+ }
22487
+ if (rows.length === 0) {
22488
+ console.log(source_default.dim("No trust roots registered."));
22489
+ return;
22490
+ }
22491
+ for (const r of rows) {
22492
+ const status = r.is_active ? source_default.green("active") : source_default.red("revoked");
22493
+ const runner = r.bound_runner ? ` runner=${r.bound_runner}` : "";
22494
+ console.log(`${source_default.bold(r.name)} [${status}] ${r.algorithm}${runner}`);
22495
+ }
22496
+ });
22497
+ roots.command("get <name>").description("Show one trust root").action(async (name) => {
22498
+ const ctx = getCtx();
22499
+ const tr = await ctx.api.get(`${BASE}/${encodeURIComponent(name)}`);
22500
+ if (isJson()) {
22501
+ console.log(JSON.stringify(tr, null, 2));
22502
+ return;
22503
+ }
22504
+ printOne(tr);
22505
+ });
22506
+ 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) => {
22507
+ const ctx = getCtx();
22508
+ const body = {
22509
+ name,
22510
+ public_key: resolveKeyMaterial(opts.publicKey),
22511
+ algorithm: opts.algorithm
22512
+ };
22513
+ if (opts.keyId)
22514
+ body.key_id = opts.keyId;
22515
+ if (opts.boundRunner)
22516
+ body.bound_runner = opts.boundRunner;
22517
+ if (opts.description)
22518
+ body.description = opts.description;
22519
+ const tr = await ctx.api.post(BASE, body);
22520
+ if (isJson()) {
22521
+ console.log(JSON.stringify(tr, null, 2));
22522
+ return;
22523
+ }
22524
+ console.log(source_default.green(`Registered trust root '${tr.name}'`));
22525
+ printOne(tr);
22526
+ });
22527
+ roots.command("revoke <name>").description("Deactivate a trust root (no hard delete; preserves the audit trail)").action(async (name) => {
22528
+ const ctx = getCtx();
22529
+ const tr = await ctx.api.patch(`${BASE}/${encodeURIComponent(name)}`, {
22530
+ is_active: false
22531
+ });
22532
+ if (isJson()) {
22533
+ console.log(JSON.stringify(tr, null, 2));
22534
+ return;
22535
+ }
22536
+ console.log(source_default.yellow(`Revoked trust root '${tr.name}' (is_active=false)`));
22537
+ });
22538
+ 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) => {
22539
+ const ctx = getCtx();
22540
+ const body = {
22541
+ public_key: resolveKeyMaterial(opts.publicKey)
22542
+ };
22543
+ if (opts.keyId)
22544
+ body.key_id = opts.keyId;
22545
+ const tr = await ctx.api.patch(`${BASE}/${encodeURIComponent(name)}`, body);
22546
+ if (isJson()) {
22547
+ console.log(JSON.stringify(tr, null, 2));
22548
+ return;
22549
+ }
22550
+ console.log(source_default.green(`Rotated trust root '${tr.name}'`));
22551
+ printOne(tr);
22552
+ });
22553
+ }
22554
+
21304
22555
  // src/commands/authz.ts
21305
22556
  init_errors();
21306
22557
 
@@ -21862,7 +23113,7 @@ function normalizeList(values) {
21862
23113
  return [];
21863
23114
  return values.flatMap((v) => v.split(/[,\s]+/)).map((v) => v.trim()).filter(Boolean);
21864
23115
  }
21865
- function parseIntents(raw) {
23116
+ function parseIntents2(raw) {
21866
23117
  const out = [];
21867
23118
  for (const v of raw) {
21868
23119
  if (!VALID_INTENTS.includes(v)) {
@@ -21974,7 +23225,7 @@ Provision an access principal now so calls just work?`, true);
21974
23225
  } else {
21975
23226
  mode = "human";
21976
23227
  }
21977
- let intents = parseIntents(normalizeList(opts.intent));
23228
+ let intents = parseIntents2(normalizeList(opts.intent));
21978
23229
  if (intents.length === 0 && interactive) {
21979
23230
  intents = await askIntents();
21980
23231
  }
@@ -22179,7 +23430,7 @@ async function askIntents() {
22179
23430
  const n = Number(t);
22180
23431
  return Number.isInteger(n) && n >= 1 && n <= VALID_INTENTS.length ? VALID_INTENTS[n - 1] : t;
22181
23432
  });
22182
- return parseIntents(raw);
23433
+ return parseIntents2(raw);
22183
23434
  }
22184
23435
  function registerInitCommand(program2) {
22185
23436
  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 +23945,9 @@ auth.command("token").description("Print current access token").action(async ()
22694
23945
  await authTokenCommand(ctx);
22695
23946
  });
22696
23947
  registerChatCommand(program2);
23948
+ registerAcpServeCommand(program2);
23949
+ registerPrincipalCommands(program2);
23950
+ registerChannelCommands(program2);
22697
23951
  registerConfigCommand(program2);
22698
23952
  registerDefinitionCommands(program2, functionsConfig);
22699
23953
  registerDefinitionCommands(program2, workflowsConfig);
@@ -22719,6 +23973,7 @@ registerModelsCommand(program2);
22719
23973
  registerSessionCommands(program2);
22720
23974
  registerMCPCommands(program2);
22721
23975
  registerPolicyCommands(program2);
23976
+ registerAgentStepCommands(program2);
22722
23977
  registerAuthzCommands(program2);
22723
23978
  registerInitCommand(program2);
22724
23979
  registerDoctorCommand(program2);