@aiaiai-pt/martha-cli 0.51.2 → 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 +109 -1
- package/dist/index.js +1553 -76
- 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({
|
|
@@ -11042,6 +11051,7 @@ ${items.length} ${config.typeNamePlural}`));
|
|
|
11042
11051
|
if (config.normalizeBody)
|
|
11043
11052
|
body = config.normalizeBody(body);
|
|
11044
11053
|
const result = await ctx.api.put(`${config.apiPath}/${encodeURIComponent(name)}`, body);
|
|
11054
|
+
config.verifyUpdateResult?.(body, result);
|
|
11045
11055
|
if (isJson()) {
|
|
11046
11056
|
console.log(JSON.stringify(result, null, 2));
|
|
11047
11057
|
return;
|
|
@@ -11119,7 +11129,7 @@ init_config();
|
|
|
11119
11129
|
init_errors();
|
|
11120
11130
|
|
|
11121
11131
|
// src/version.ts
|
|
11122
|
-
var CLI_VERSION = "0.
|
|
11132
|
+
var CLI_VERSION = "0.53.0";
|
|
11123
11133
|
|
|
11124
11134
|
// src/commands/sessions.ts
|
|
11125
11135
|
function relativeTime(iso) {
|
|
@@ -11395,6 +11405,48 @@ Then drive it from chat:`));
|
|
|
11395
11405
|
console.log(source_default.dim(`
|
|
11396
11406
|
Check it's connected any time: martha agents doctor ${r.agent}`));
|
|
11397
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
|
+
}
|
|
11398
11450
|
async function diagnoseHostRunner(ctx, name) {
|
|
11399
11451
|
const A = `${API_PATH}/${encodeURIComponent(name)}`;
|
|
11400
11452
|
const checks = [];
|
|
@@ -11445,25 +11497,18 @@ async function diagnoseHostRunner(ctx, name) {
|
|
|
11445
11497
|
detail: hostTools.length ? hostTools.join(", ") : "none",
|
|
11446
11498
|
fix: hostTools.length ? undefined : setupFix
|
|
11447
11499
|
});
|
|
11448
|
-
let
|
|
11500
|
+
let scan = null;
|
|
11449
11501
|
try {
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
const acc = await ctx.api.get(`/api/admin/definitions/clients/${c.id}/agents/access`);
|
|
11455
|
-
const has = (Array.isArray(acc) ? acc : []).some((r) => String(r.agent_name ?? r.name ?? "") === name || String(r.agent_id ?? "") === String(def.id ?? ""));
|
|
11456
|
-
return has ? String(c.name ?? c.id) : null;
|
|
11457
|
-
} catch {
|
|
11458
|
-
return null;
|
|
11459
|
-
}
|
|
11460
|
-
}));
|
|
11461
|
-
grantingClients = probes.filter((x) => !!x);
|
|
11502
|
+
scan = await scanClientsForAgent(ctx, name, {
|
|
11503
|
+
subPath: "agents/access",
|
|
11504
|
+
agentId: String(def.id ?? "")
|
|
11505
|
+
});
|
|
11462
11506
|
} catch {}
|
|
11507
|
+
const grantingClients = scan?.granting.map((g) => g.client) ?? [];
|
|
11463
11508
|
checks.push({
|
|
11464
|
-
ok: grantingClients.length > 0,
|
|
11509
|
+
ok: grantingClients.length > 0 ? true : !scan || scan.incomplete ? null : false,
|
|
11465
11510
|
label: "a chat client can select it",
|
|
11466
|
-
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",
|
|
11467
11512
|
fix: grantingClients.length ? undefined : setupFix
|
|
11468
11513
|
});
|
|
11469
11514
|
const takeover = def.runs_as_chat === true;
|
|
@@ -11864,14 +11909,7 @@ Attach it as this agent's host executor:
|
|
|
11864
11909
|
}
|
|
11865
11910
|
console.log(source_default.bold(`Martha CLI doctor — host runner: ${agent}
|
|
11866
11911
|
`));
|
|
11867
|
-
|
|
11868
|
-
const glyph = c.ok === true ? source_default.green("PASS") : c.ok === false ? source_default.red("FAIL") : source_default.yellow("WARN");
|
|
11869
|
-
const detail = c.detail ? source_default.dim(c.detail) : "";
|
|
11870
|
-
console.log(` ${glyph} ${c.label.padEnd(32)} ${detail}`);
|
|
11871
|
-
if (c.ok === false && c.fix) {
|
|
11872
|
-
console.log(` ${source_default.dim("→ " + c.fix)}`);
|
|
11873
|
-
}
|
|
11874
|
-
}
|
|
11912
|
+
printChecks(dx.checks);
|
|
11875
11913
|
console.log();
|
|
11876
11914
|
if (dx.drivableFromChat && dx.runnerAttached) {
|
|
11877
11915
|
console.log(source_default.green.bold("Ready — a runner is attached and the agent can be driven from chat."));
|
|
@@ -12262,6 +12300,7 @@ function parseLocalAgentRequests(data) {
|
|
|
12262
12300
|
}
|
|
12263
12301
|
|
|
12264
12302
|
// src/lib/local-tool-runner.ts
|
|
12303
|
+
import path7 from "node:path";
|
|
12265
12304
|
import { createHash as createHash2 } from "node:crypto";
|
|
12266
12305
|
init_errors();
|
|
12267
12306
|
|
|
@@ -12686,23 +12725,54 @@ ${lines.join(`
|
|
|
12686
12725
|
return { error: true, message: `${req.action} failed: ${String(e)}` };
|
|
12687
12726
|
}
|
|
12688
12727
|
}
|
|
12728
|
+
var REAL_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
12689
12729
|
|
|
12690
12730
|
class BrowserSession {
|
|
12691
12731
|
browser = null;
|
|
12692
12732
|
page = null;
|
|
12693
12733
|
consoleBuffer = [];
|
|
12734
|
+
headful() {
|
|
12735
|
+
const v = (process.env.MARTHA_BROWSER_HEADFUL || "").toLowerCase();
|
|
12736
|
+
return v === "1" || v === "true" || v === "yes";
|
|
12737
|
+
}
|
|
12694
12738
|
async ensurePage() {
|
|
12695
12739
|
if (this.page)
|
|
12696
12740
|
return this.page;
|
|
12697
12741
|
const moduleName = "playwright";
|
|
12698
12742
|
let chromium;
|
|
12743
|
+
const candidates = [];
|
|
12699
12744
|
try {
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12745
|
+
const { createRequire: createRequire2 } = await import("node:module");
|
|
12746
|
+
const { pathToFileURL } = await import("node:url");
|
|
12747
|
+
const path5 = await import("node:path");
|
|
12748
|
+
const req = createRequire2(pathToFileURL(path5.join(process.cwd(), "noop.js")));
|
|
12749
|
+
candidates.push(pathToFileURL(req.resolve(moduleName)).href);
|
|
12750
|
+
} catch {}
|
|
12751
|
+
candidates.push(moduleName);
|
|
12752
|
+
let lastErr;
|
|
12753
|
+
let loaded = null;
|
|
12754
|
+
for (const spec of candidates) {
|
|
12755
|
+
try {
|
|
12756
|
+
loaded = await import(spec);
|
|
12757
|
+
break;
|
|
12758
|
+
} catch (e) {
|
|
12759
|
+
lastErr = e;
|
|
12760
|
+
}
|
|
12761
|
+
}
|
|
12762
|
+
if (!loaded) {
|
|
12763
|
+
throw new Error(`playwright is not installed where the runner can load it (looked in ${process.cwd()} and alongside the martha CLI). Install it in the runner's --cwd: \`npm i -D playwright && npx playwright install chromium\`` + (lastErr instanceof Error ? ` — ${lastErr.message}` : ""));
|
|
12764
|
+
}
|
|
12765
|
+
const pw = loaded.chromium ? loaded : loaded.default;
|
|
12766
|
+
if (!pw?.chromium) {
|
|
12767
|
+
throw new Error(`playwright loaded but exposes no \`chromium\` export (from ${process.cwd()}); check the installed playwright version.`);
|
|
12703
12768
|
}
|
|
12704
|
-
|
|
12705
|
-
|
|
12769
|
+
chromium = pw.chromium;
|
|
12770
|
+
const headful = this.headful();
|
|
12771
|
+
this.browser = await chromium.launch({ headless: !headful });
|
|
12772
|
+
this.page = headful ? await (await this.browser.newContext({
|
|
12773
|
+
userAgent: REAL_UA,
|
|
12774
|
+
viewport: { width: 1440, height: 900 }
|
|
12775
|
+
})).newPage() : await this.browser.newPage();
|
|
12706
12776
|
this.page.on("console", (msg) => this.consoleBuffer.push({
|
|
12707
12777
|
kind: "console",
|
|
12708
12778
|
level: msg.type(),
|
|
@@ -13078,6 +13148,15 @@ function parseJobToolRequests(data, toolName) {
|
|
|
13078
13148
|
}
|
|
13079
13149
|
|
|
13080
13150
|
// src/lib/local-tool-runner.ts
|
|
13151
|
+
function resolveRequestCwd(stateCwd, reqCwd) {
|
|
13152
|
+
if (!reqCwd)
|
|
13153
|
+
return stateCwd;
|
|
13154
|
+
const base = path7.resolve(stateCwd);
|
|
13155
|
+
const target = path7.isAbsolute(reqCwd) ? path7.resolve(reqCwd) : path7.resolve(base, reqCwd);
|
|
13156
|
+
const rel = path7.relative(base, target);
|
|
13157
|
+
const escapes = rel.startsWith("..") || path7.isAbsolute(rel);
|
|
13158
|
+
return escapes ? base : target;
|
|
13159
|
+
}
|
|
13081
13160
|
function selectedClientOptions(clientId) {
|
|
13082
13161
|
return clientId ? { params: { selected_client: clientId } } : undefined;
|
|
13083
13162
|
}
|
|
@@ -13118,7 +13197,7 @@ async function resolveHostExec(sessionId, toolCallId, req, state) {
|
|
|
13118
13197
|
message: "Host execution declined; re-run with --yes to allow."
|
|
13119
13198
|
};
|
|
13120
13199
|
}
|
|
13121
|
-
const cwd =
|
|
13200
|
+
const cwd = resolveRequestCwd(state.cwd, req.cwd);
|
|
13122
13201
|
if (req.background) {
|
|
13123
13202
|
const jobId = mintJobId();
|
|
13124
13203
|
process.stderr.write(source_default.dim(`
|
|
@@ -13281,7 +13360,7 @@ async function resolveLocalAgent(sessionId, toolCallId, req, state) {
|
|
|
13281
13360
|
message: `A previous local_agent run (${decision.command}) did not complete; ` + `not re-running. Its log survives under .martha-runner/ — read it with host_read.`
|
|
13282
13361
|
};
|
|
13283
13362
|
}
|
|
13284
|
-
const cwd =
|
|
13363
|
+
const cwd = resolveRequestCwd(state.cwd, req.cwd);
|
|
13285
13364
|
const { bin, args, stdin } = buildSpawn(invocation, req.task);
|
|
13286
13365
|
if (req.background) {
|
|
13287
13366
|
const jobId = mintJobId();
|
|
@@ -14167,10 +14246,1030 @@ function parseTimeoutSeconds(raw) {
|
|
|
14167
14246
|
return n;
|
|
14168
14247
|
}
|
|
14169
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
|
+
|
|
14170
15269
|
// src/commands/config.ts
|
|
14171
15270
|
init_config();
|
|
14172
15271
|
init_errors();
|
|
14173
|
-
import { createInterface as
|
|
15272
|
+
import { createInterface as createInterface4 } from "node:readline";
|
|
14174
15273
|
function prompt(rl, question, defaultValue) {
|
|
14175
15274
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
14176
15275
|
return new Promise((resolve2) => {
|
|
@@ -14195,7 +15294,7 @@ async function configInit(configDir) {
|
|
|
14195
15294
|
console.log(`No configuration found. Let's set up your first profile.
|
|
14196
15295
|
`);
|
|
14197
15296
|
}
|
|
14198
|
-
const rl =
|
|
15297
|
+
const rl = createInterface4({
|
|
14199
15298
|
input: process.stdin,
|
|
14200
15299
|
output: process.stderr
|
|
14201
15300
|
});
|
|
@@ -14482,7 +15581,7 @@ function registerConfigCommand(program2) {
|
|
|
14482
15581
|
throw new CLIError("Cannot confirm in non-interactive mode.", 4 /* Validation */, "Use --yes to skip confirmation.");
|
|
14483
15582
|
}
|
|
14484
15583
|
const ok = await new Promise((resolve2) => {
|
|
14485
|
-
const rl =
|
|
15584
|
+
const rl = createInterface4({
|
|
14486
15585
|
input: process.stdin,
|
|
14487
15586
|
output: process.stderr
|
|
14488
15587
|
});
|
|
@@ -14512,9 +15611,222 @@ function registerConfigCommand(program2) {
|
|
|
14512
15611
|
|
|
14513
15612
|
// src/commands/definitions-apply.ts
|
|
14514
15613
|
import fs8 from "node:fs";
|
|
14515
|
-
import
|
|
15614
|
+
import path8 from "node:path";
|
|
14516
15615
|
init_dist();
|
|
14517
15616
|
init_errors();
|
|
15617
|
+
|
|
15618
|
+
// src/commands/definition-payload.ts
|
|
15619
|
+
init_errors();
|
|
15620
|
+
var MUTABLE_FIELDS = {
|
|
15621
|
+
Function: new Set([
|
|
15622
|
+
"tool_name",
|
|
15623
|
+
"description",
|
|
15624
|
+
"endpoint",
|
|
15625
|
+
"http_method",
|
|
15626
|
+
"handler",
|
|
15627
|
+
"category",
|
|
15628
|
+
"parameters",
|
|
15629
|
+
"request_body",
|
|
15630
|
+
"auth",
|
|
15631
|
+
"extra_headers",
|
|
15632
|
+
"tags",
|
|
15633
|
+
"is_active",
|
|
15634
|
+
"collection_id_param",
|
|
15635
|
+
"risk_tags",
|
|
15636
|
+
"risk_level",
|
|
15637
|
+
"verification_safe",
|
|
15638
|
+
"idempotent"
|
|
15639
|
+
]),
|
|
15640
|
+
Workflow: new Set([
|
|
15641
|
+
"description",
|
|
15642
|
+
"format_version",
|
|
15643
|
+
"nodes",
|
|
15644
|
+
"edges",
|
|
15645
|
+
"variables",
|
|
15646
|
+
"input_schema",
|
|
15647
|
+
"on_error",
|
|
15648
|
+
"timeout_seconds",
|
|
15649
|
+
"ui_metadata",
|
|
15650
|
+
"is_active"
|
|
15651
|
+
])
|
|
15652
|
+
};
|
|
15653
|
+
var FUNCTION_DECLARATION_FIELDS = new Set([
|
|
15654
|
+
"verification_safe",
|
|
15655
|
+
"idempotent"
|
|
15656
|
+
]);
|
|
15657
|
+
function isPlainObject(value) {
|
|
15658
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
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
|
+
}
|
|
15674
|
+
function valuesEqual(left, right) {
|
|
15675
|
+
if (left === right)
|
|
15676
|
+
return true;
|
|
15677
|
+
if (left === null || right === null)
|
|
15678
|
+
return false;
|
|
15679
|
+
if (left === undefined || right === undefined)
|
|
15680
|
+
return false;
|
|
15681
|
+
if (typeof left !== typeof right)
|
|
15682
|
+
return false;
|
|
15683
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
15684
|
+
if (!Array.isArray(left) || !Array.isArray(right))
|
|
15685
|
+
return false;
|
|
15686
|
+
return left.length === right.length && left.every((value, index) => valuesEqual(value, right[index]));
|
|
15687
|
+
}
|
|
15688
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
15689
|
+
const leftKeys = Object.keys(left);
|
|
15690
|
+
const rightKeys = Object.keys(right);
|
|
15691
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => (key in right) && valuesEqual(left[key], right[key]));
|
|
15692
|
+
}
|
|
15693
|
+
return false;
|
|
15694
|
+
}
|
|
15695
|
+
function requestedValueMatches(requested, observed) {
|
|
15696
|
+
if (requested === observed)
|
|
15697
|
+
return true;
|
|
15698
|
+
if (requested === null || requested === undefined) {
|
|
15699
|
+
return observed === null || observed === undefined;
|
|
15700
|
+
}
|
|
15701
|
+
if (observed === null || observed === undefined)
|
|
15702
|
+
return false;
|
|
15703
|
+
if (typeof requested !== typeof observed)
|
|
15704
|
+
return false;
|
|
15705
|
+
if (Array.isArray(requested)) {
|
|
15706
|
+
if (!Array.isArray(observed) || requested.length !== observed.length) {
|
|
15707
|
+
return false;
|
|
15708
|
+
}
|
|
15709
|
+
return requested.every((value, index) => requestedValueMatches(value, observed[index]));
|
|
15710
|
+
}
|
|
15711
|
+
if (isPlainObject(requested)) {
|
|
15712
|
+
if (!isPlainObject(observed))
|
|
15713
|
+
return false;
|
|
15714
|
+
const requestedKeys = Object.keys(requested);
|
|
15715
|
+
if (requestedKeys.length === 0) {
|
|
15716
|
+
return Object.keys(observed).length === 0;
|
|
15717
|
+
}
|
|
15718
|
+
return requestedKeys.every((key) => requestedValueMatches(requested[key], observed[key]));
|
|
15719
|
+
}
|
|
15720
|
+
return false;
|
|
15721
|
+
}
|
|
15722
|
+
function normalizeDefinitionWriteBody(kind, body) {
|
|
15723
|
+
const normalized = { ...body };
|
|
15724
|
+
if (!("definition" in normalized))
|
|
15725
|
+
return normalized;
|
|
15726
|
+
const nested = normalized.definition;
|
|
15727
|
+
if (!isPlainObject(nested)) {
|
|
15728
|
+
throw new CLIError("'definition' must be an object", 4 /* Validation */);
|
|
15729
|
+
}
|
|
15730
|
+
if ("definition" in nested) {
|
|
15731
|
+
throw new CLIError("Only one 'definition' wrapper is supported", 4 /* Validation */);
|
|
15732
|
+
}
|
|
15733
|
+
delete normalized.definition;
|
|
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
|
+
}
|
|
15739
|
+
if (key in normalized && !valuesEqual(normalized[key], nestedValue)) {
|
|
15740
|
+
throw new CLIError(`Conflicting values for '${key}' in flat and nested ${kind} fields`, 4 /* Validation */);
|
|
15741
|
+
}
|
|
15742
|
+
if (!(key in normalized))
|
|
15743
|
+
normalized[key] = nestedValue;
|
|
15744
|
+
}
|
|
15745
|
+
return normalized;
|
|
15746
|
+
}
|
|
15747
|
+
function canonicalizeWireValue(kind, field, value) {
|
|
15748
|
+
if (kind === "Function" && field === "http_method") {
|
|
15749
|
+
return typeof value === "string" ? value.toUpperCase() : value;
|
|
15750
|
+
}
|
|
15751
|
+
if (kind === "Function" && field === "request_body" && isPlainObject(value)) {
|
|
15752
|
+
const canonical = {
|
|
15753
|
+
content_type: "application/json",
|
|
15754
|
+
required: false,
|
|
15755
|
+
schema_def: null,
|
|
15756
|
+
...value
|
|
15757
|
+
};
|
|
15758
|
+
if ("schema" in canonical) {
|
|
15759
|
+
canonical.schema_def = canonical.schema;
|
|
15760
|
+
delete canonical.schema;
|
|
15761
|
+
}
|
|
15762
|
+
return canonical;
|
|
15763
|
+
}
|
|
15764
|
+
if (kind === "Function" && field === "auth" && isPlainObject(value)) {
|
|
15765
|
+
return {
|
|
15766
|
+
scheme: null,
|
|
15767
|
+
header: null,
|
|
15768
|
+
query_param: null,
|
|
15769
|
+
value: null,
|
|
15770
|
+
credential_source: null,
|
|
15771
|
+
...value
|
|
15772
|
+
};
|
|
15773
|
+
}
|
|
15774
|
+
if (kind === "Workflow" && field === "nodes" && Array.isArray(value)) {
|
|
15775
|
+
return value.map((node) => isPlainObject(node) ? { label: null, config: {}, position: null, ...node } : node);
|
|
15776
|
+
}
|
|
15777
|
+
if (kind === "Workflow" && field === "edges" && Array.isArray(value)) {
|
|
15778
|
+
return value.map((edge) => isPlainObject(edge) ? { source_handle: null, condition: null, label: null, ...edge } : edge);
|
|
15779
|
+
}
|
|
15780
|
+
return value;
|
|
15781
|
+
}
|
|
15782
|
+
function canonicalizeParameter(value) {
|
|
15783
|
+
if (!isPlainObject(value))
|
|
15784
|
+
return value;
|
|
15785
|
+
return {
|
|
15786
|
+
description: null,
|
|
15787
|
+
required: false,
|
|
15788
|
+
location: "query",
|
|
15789
|
+
enum: null,
|
|
15790
|
+
default: null,
|
|
15791
|
+
...value
|
|
15792
|
+
};
|
|
15793
|
+
}
|
|
15794
|
+
function parameterMapMatches(requested, observed) {
|
|
15795
|
+
if (!isPlainObject(requested) || !isPlainObject(observed)) {
|
|
15796
|
+
return requestedValueMatches(requested, observed);
|
|
15797
|
+
}
|
|
15798
|
+
const requestedNames = Object.keys(requested);
|
|
15799
|
+
const observedNames = Object.keys(observed);
|
|
15800
|
+
return requestedNames.length === observedNames.length && requestedNames.every((name) => (name in observed) && valuesEqual(canonicalizeParameter(requested[name]), canonicalizeParameter(observed[name])));
|
|
15801
|
+
}
|
|
15802
|
+
function fieldValueMatches(kind, field, requested, observed) {
|
|
15803
|
+
if (kind === "Function" && field === "parameters") {
|
|
15804
|
+
return parameterMapMatches(requested, observed);
|
|
15805
|
+
}
|
|
15806
|
+
if (kind === "Function" && FUNCTION_DECLARATION_FIELDS.has(field)) {
|
|
15807
|
+
return declarationFieldMatches(requested, observed);
|
|
15808
|
+
}
|
|
15809
|
+
const isExactTypedValue = kind === "Function" && (field === "request_body" || field === "auth") || kind === "Workflow" && (field === "nodes" || field === "edges");
|
|
15810
|
+
if (isExactTypedValue) {
|
|
15811
|
+
return valuesEqual(requested, observed);
|
|
15812
|
+
}
|
|
15813
|
+
const isExactReplacementMap = kind === "Function" && field === "extra_headers" || kind === "Workflow" && (field === "variables" || field === "input_schema" || field === "ui_metadata");
|
|
15814
|
+
if (isExactReplacementMap) {
|
|
15815
|
+
return valuesEqual(requested, observed);
|
|
15816
|
+
}
|
|
15817
|
+
return requestedValueMatches(requested, observed);
|
|
15818
|
+
}
|
|
15819
|
+
function verifyDefinitionUpdateResponse(kind, requestedBody, response) {
|
|
15820
|
+
const requested = normalizeDefinitionWriteBody(kind, requestedBody);
|
|
15821
|
+
const observed = normalizeDefinitionWriteBody(kind, response);
|
|
15822
|
+
const mutableFields = MUTABLE_FIELDS[kind];
|
|
15823
|
+
const mismatches = Object.keys(requested).filter((field) => mutableFields.has(field) && !fieldValueMatches(kind, field, canonicalizeWireValue(kind, field, requested[field]), canonicalizeWireValue(kind, field, observed[field])));
|
|
15824
|
+
if (mismatches.length > 0) {
|
|
15825
|
+
throw new CLIError(`Server response did not persist requested field(s): ${mismatches.join(", ")}`, 1 /* Error */);
|
|
15826
|
+
}
|
|
15827
|
+
}
|
|
15828
|
+
|
|
15829
|
+
// src/commands/definitions-apply.ts
|
|
14518
15830
|
var VALID_KINDS = new Set([
|
|
14519
15831
|
"Function",
|
|
14520
15832
|
"Workflow",
|
|
@@ -14549,12 +15861,11 @@ var SERVER_GENERATED_FIELDS = new Set([
|
|
|
14549
15861
|
"has_parallel",
|
|
14550
15862
|
"has_conditions",
|
|
14551
15863
|
"function_refs",
|
|
14552
|
-
"format_version",
|
|
14553
15864
|
"tenant_id"
|
|
14554
15865
|
]);
|
|
14555
15866
|
var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
|
|
14556
15867
|
function loadDefinitions(inputPath) {
|
|
14557
|
-
const resolved =
|
|
15868
|
+
const resolved = path8.resolve(inputPath);
|
|
14558
15869
|
if (!fs8.existsSync(resolved)) {
|
|
14559
15870
|
throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
|
|
14560
15871
|
}
|
|
@@ -14567,7 +15878,7 @@ function loadDefinitions(inputPath) {
|
|
|
14567
15878
|
function loadDirectory(dirPath) {
|
|
14568
15879
|
const entries = fs8.readdirSync(dirPath).sort();
|
|
14569
15880
|
const validExts = new Set([".yaml", ".yml", ".json"]);
|
|
14570
|
-
const files = entries.filter((e) => validExts.has(
|
|
15881
|
+
const files = entries.filter((e) => validExts.has(path8.extname(e).toLowerCase())).map((e) => path8.join(dirPath, e));
|
|
14571
15882
|
if (files.length === 0) {
|
|
14572
15883
|
throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
|
|
14573
15884
|
}
|
|
@@ -14579,7 +15890,7 @@ function loadDirectory(dirPath) {
|
|
|
14579
15890
|
}
|
|
14580
15891
|
function loadFile(filePath) {
|
|
14581
15892
|
const content = fs8.readFileSync(filePath, "utf-8");
|
|
14582
|
-
const ext =
|
|
15893
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
14583
15894
|
if (ext === ".json") {
|
|
14584
15895
|
const parsed = parseJsonFile(content, filePath);
|
|
14585
15896
|
return [toLocalDefinition(parsed, filePath)];
|
|
@@ -14639,7 +15950,9 @@ function toLocalDefinition(raw, source) {
|
|
|
14639
15950
|
continue;
|
|
14640
15951
|
body[k] = v;
|
|
14641
15952
|
}
|
|
14642
|
-
|
|
15953
|
+
const typedKind = kind;
|
|
15954
|
+
const normalizedBody = typedKind === "Function" || typedKind === "Workflow" ? normalizeDefinitionWriteBody(typedKind, body) : body;
|
|
15955
|
+
return { kind: typedKind, name, body: normalizedBody, source };
|
|
14643
15956
|
}
|
|
14644
15957
|
async function computeOperations(defs, api) {
|
|
14645
15958
|
const ops = [];
|
|
@@ -14814,7 +16127,7 @@ async function executeApply(api, ops, defs, warnings) {
|
|
|
14814
16127
|
}
|
|
14815
16128
|
const def = defsByKey.get(`${op.kind}:${op.name}`);
|
|
14816
16129
|
const apiPath = KIND_TO_API_PATH[op.kind];
|
|
14817
|
-
const body = { ...def.body };
|
|
16130
|
+
const body = op.kind === "Function" || op.kind === "Workflow" ? normalizeDefinitionWriteBody(op.kind, def.body) : { ...def.body };
|
|
14818
16131
|
if (op.kind === "Agent") {
|
|
14819
16132
|
for (const field of AGENT_MANAGED_FIELDS) {
|
|
14820
16133
|
if (field in body) {
|
|
@@ -14841,6 +16154,9 @@ async function executeApply(api, ops, defs, warnings) {
|
|
|
14841
16154
|
});
|
|
14842
16155
|
} else {
|
|
14843
16156
|
const res = await api.put(`${apiPath}/${encodeURIComponent(op.name)}`, body);
|
|
16157
|
+
if (op.kind === "Function" || op.kind === "Workflow") {
|
|
16158
|
+
verifyDefinitionUpdateResponse(op.kind, body, res);
|
|
16159
|
+
}
|
|
14844
16160
|
const portWarnings = extractPortWarnings(res);
|
|
14845
16161
|
results.push({
|
|
14846
16162
|
name: op.name,
|
|
@@ -15123,6 +16439,8 @@ var functionsConfig = {
|
|
|
15123
16439
|
typeName: "function",
|
|
15124
16440
|
typeNamePlural: "functions",
|
|
15125
16441
|
apiPath: "/api/admin/definitions/functions",
|
|
16442
|
+
normalizeBody: (body) => normalizeDefinitionWriteBody("Function", body),
|
|
16443
|
+
verifyUpdateResult: (body, result) => verifyDefinitionUpdateResponse("Function", body, result),
|
|
15126
16444
|
extractList: (data) => {
|
|
15127
16445
|
if (!Array.isArray(data)) {
|
|
15128
16446
|
throw new Error("Unexpected API response shape for functions list");
|
|
@@ -15754,6 +17072,8 @@ var workflowsConfig = {
|
|
|
15754
17072
|
typeName: "workflow",
|
|
15755
17073
|
typeNamePlural: "workflows",
|
|
15756
17074
|
apiPath: "/api/admin/definitions/workflows",
|
|
17075
|
+
normalizeBody: (body) => normalizeDefinitionWriteBody("Workflow", body),
|
|
17076
|
+
verifyUpdateResult: (body, result) => verifyDefinitionUpdateResponse("Workflow", body, result),
|
|
15757
17077
|
extractList: (data) => {
|
|
15758
17078
|
if (!Array.isArray(data)) {
|
|
15759
17079
|
throw new Error("Unexpected API response shape for workflows list");
|
|
@@ -16141,6 +17461,21 @@ function colorStatus2(status) {
|
|
|
16141
17461
|
const fn = STATUS_COLORS2[status] ?? source_default.white;
|
|
16142
17462
|
return fn(status);
|
|
16143
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
|
+
}
|
|
17471
|
+
function unwrapList(res) {
|
|
17472
|
+
if (Array.isArray(res))
|
|
17473
|
+
return res;
|
|
17474
|
+
return res?.items ?? [];
|
|
17475
|
+
}
|
|
17476
|
+
function ingestionLabel(doc) {
|
|
17477
|
+
return doc.ingestion_status ?? "not ingested";
|
|
17478
|
+
}
|
|
16144
17479
|
async function pollIngestionUntilDone(ctx, documentId, intervalMs = 2000, timeoutMs = 300000) {
|
|
16145
17480
|
const deadline = Date.now() + timeoutMs;
|
|
16146
17481
|
while (Date.now() < deadline) {
|
|
@@ -16322,7 +17657,16 @@ ${items.length} collections`));
|
|
|
16322
17657
|
if (col.slug)
|
|
16323
17658
|
console.log(` Slug: ${col.slug}`);
|
|
16324
17659
|
console.log(` Status: ${col.is_active !== false ? source_default.green("active") : source_default.dim("inactive")}`);
|
|
16325
|
-
|
|
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)}`);
|
|
16326
17670
|
console.log(` Storage: ${col.storage_backend ?? "-"}`);
|
|
16327
17671
|
console.log(` Drive: ${col.drive_folder_id ? source_default.cyan(col.drive_folder_id) : source_default.dim("not linked")}`);
|
|
16328
17672
|
if (col.total_size_bytes != null) {
|
|
@@ -16380,7 +17724,9 @@ ${items.length} collections`));
|
|
|
16380
17724
|
const ctx = getCtx();
|
|
16381
17725
|
const col = await resolveCollection(ctx, id);
|
|
16382
17726
|
const extraPromptIds = opts.clear ? [] : promptIds;
|
|
16383
|
-
const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(col.id)}`, {
|
|
17727
|
+
const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(col.id)}`, {
|
|
17728
|
+
extra_prompt_ids: extraPromptIds
|
|
17729
|
+
});
|
|
16384
17730
|
if (isJson()) {
|
|
16385
17731
|
console.log(JSON.stringify({ extra_prompt_ids: result.extra_prompt_ids ?? [] }, null, 2));
|
|
16386
17732
|
return;
|
|
@@ -16486,9 +17832,10 @@ ${items.length} collections`));
|
|
|
16486
17832
|
const params = {};
|
|
16487
17833
|
if (!opts.inactive)
|
|
16488
17834
|
params.active_only = "true";
|
|
16489
|
-
const
|
|
17835
|
+
const res = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, { params });
|
|
17836
|
+
const items = unwrapList(res);
|
|
16490
17837
|
if (isJson()) {
|
|
16491
|
-
console.log(JSON.stringify(
|
|
17838
|
+
console.log(JSON.stringify(res, null, 2));
|
|
16492
17839
|
return;
|
|
16493
17840
|
}
|
|
16494
17841
|
const columns = [
|
|
@@ -16502,8 +17849,8 @@ ${items.length} collections`));
|
|
|
16502
17849
|
accessor: (i) => i.size_bytes != null ? formatBytes(i.size_bytes) : "-"
|
|
16503
17850
|
},
|
|
16504
17851
|
{
|
|
16505
|
-
header: "
|
|
16506
|
-
accessor: (i) => i
|
|
17852
|
+
header: "INGESTION",
|
|
17853
|
+
accessor: (i) => ingestionLabel(i)
|
|
16507
17854
|
}
|
|
16508
17855
|
];
|
|
16509
17856
|
const widths = columns.map((col) => items.length === 0 ? col.header.length : Math.max(col.header.length, ...items.map((item) => col.accessor(item).length)));
|
|
@@ -16532,9 +17879,11 @@ ${items.length} documents`));
|
|
|
16532
17879
|
if (doc.size_bytes != null) {
|
|
16533
17880
|
console.log(` Size: ${formatBytes(doc.size_bytes)}`);
|
|
16534
17881
|
}
|
|
16535
|
-
console.log(`
|
|
17882
|
+
console.log(` Upload: ${colorStatus2(doc.status ?? "-")}`);
|
|
17883
|
+
console.log(` Ingestion: ${colorStatus2(ingestionLabel(doc))}`);
|
|
16536
17884
|
if (doc.chunk_count != null) {
|
|
16537
|
-
|
|
17885
|
+
const warn = doc.ingestion_status === "ready" && doc.chunk_count === 0 ? source_default.yellow(" ← ready but nothing retrievable") : "";
|
|
17886
|
+
console.log(` Chunks: ${doc.chunk_count}${warn}`);
|
|
16538
17887
|
}
|
|
16539
17888
|
console.log(` Active: ${doc.is_active !== false ? source_default.green("yes") : source_default.dim("no")}`);
|
|
16540
17889
|
if (doc.created_at)
|
|
@@ -18204,8 +19553,11 @@ ${data.length} spec(s)`));
|
|
|
18204
19553
|
if (opts.force)
|
|
18205
19554
|
params.force = "true";
|
|
18206
19555
|
const result = await ctx.api.post(`/api/admin/definitions/openapi-specs/${encodeURIComponent(id)}/sync`, undefined, { params });
|
|
19556
|
+
const syncFailed = result.status === "error" || result.status === "partial";
|
|
18207
19557
|
if (isJson()) {
|
|
18208
19558
|
console.log(JSON.stringify(result, null, 2));
|
|
19559
|
+
if (syncFailed)
|
|
19560
|
+
process.exitCode = 1 /* Error */;
|
|
18209
19561
|
return;
|
|
18210
19562
|
}
|
|
18211
19563
|
console.log(result.message ?? "Sync completed.");
|
|
@@ -18221,6 +19573,8 @@ ${data.length} spec(s)`));
|
|
|
18221
19573
|
console.log(source_default.red(` - ${err}`));
|
|
18222
19574
|
}
|
|
18223
19575
|
}
|
|
19576
|
+
if (syncFailed)
|
|
19577
|
+
process.exitCode = 1 /* Error */;
|
|
18224
19578
|
});
|
|
18225
19579
|
cmd.command("import-openapi").description("Import an OpenAPI spec").requiredOption("--source <url>", "OpenAPI spec URL").requiredOption("--name <name>", "Integration name").option("--prefix <prefix>", "Function name prefix").option("--include-tags <tags>", "Comma-separated tags to include").option("--exclude-tags <tags>", "Comma-separated tags to exclude").option("--dry-run", "Preview without creating (default)", true).option("--no-dry-run", "Actually create the functions").option("--force", "Force import even with conflicts").option("--auth-scheme <scheme>", "Auth scheme: apiKey, bearer, basic, oauth2, none").option("--auth-header <header>", "Header name for auth (default: Authorization)").option("--auth-value <value>", "Auth value or env var reference (${VAR})").option("--auth-query-param <param>", "Query parameter name for apiKey auth").option("--auth-credential-source <key>", "Vault credential lookup key").action(async (opts) => {
|
|
18226
19580
|
const ctx = getCtx();
|
|
@@ -18370,8 +19724,8 @@ ${data.length} spec(s)`));
|
|
|
18370
19724
|
Resources:
|
|
18371
19725
|
`));
|
|
18372
19726
|
for (const r of resources) {
|
|
18373
|
-
const
|
|
18374
|
-
console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${
|
|
19727
|
+
const path9 = r.path || `/${r.name}`;
|
|
19728
|
+
console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path9}`));
|
|
18375
19729
|
}
|
|
18376
19730
|
}
|
|
18377
19731
|
try {
|
|
@@ -18391,14 +19745,14 @@ Functions:
|
|
|
18391
19745
|
console.log(source_default.dim(`
|
|
18392
19746
|
Proxy: martha integrations proxy ${name} GET /<path>`));
|
|
18393
19747
|
});
|
|
18394
|
-
cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method,
|
|
19748
|
+
cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method, path9, opts) => {
|
|
18395
19749
|
const ctx = getCtx();
|
|
18396
19750
|
const upperMethod = method.toUpperCase();
|
|
18397
19751
|
const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
18398
19752
|
if (!allowed.has(upperMethod)) {
|
|
18399
19753
|
throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
|
|
18400
19754
|
}
|
|
18401
|
-
const cleanPath =
|
|
19755
|
+
const cleanPath = path9.startsWith("/") ? path9.slice(1) : path9;
|
|
18402
19756
|
const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
|
|
18403
19757
|
const params = {};
|
|
18404
19758
|
if (opts.query) {
|
|
@@ -18776,11 +20130,11 @@ async function readStdin2() {
|
|
|
18776
20130
|
|
|
18777
20131
|
// src/commands/messaging.ts
|
|
18778
20132
|
init_errors();
|
|
18779
|
-
async function messagingFetch(baseUrl,
|
|
18780
|
-
if (/^(https?:)?\/\//i.test(
|
|
20133
|
+
async function messagingFetch(baseUrl, path9, opts) {
|
|
20134
|
+
if (/^(https?:)?\/\//i.test(path9)) {
|
|
18781
20135
|
throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
|
|
18782
20136
|
}
|
|
18783
|
-
const url = new URL(
|
|
20137
|
+
const url = new URL(path9, baseUrl);
|
|
18784
20138
|
const headers = {
|
|
18785
20139
|
"Content-Type": "application/json"
|
|
18786
20140
|
};
|
|
@@ -20218,16 +21572,16 @@ ${wikis.length} wiki${wikis.length === 1 ? "" : "s"}`));
|
|
|
20218
21572
|
console.log(source_default.dim(`
|
|
20219
21573
|
${pages.length} pages`));
|
|
20220
21574
|
});
|
|
20221
|
-
cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--wiki <slug>", "Wiki slug (default: the tenant default wiki)").option("--out <file>", "Write body to file instead of stdout").action(async (
|
|
21575
|
+
cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--wiki <slug>", "Wiki slug (default: the tenant default wiki)").option("--out <file>", "Write body to file instead of stdout").action(async (path9, opts) => {
|
|
20222
21576
|
const ctx = getCtx();
|
|
20223
|
-
const safe =
|
|
21577
|
+
const safe = path9.split("/").map(encodeURIComponent).join("/");
|
|
20224
21578
|
const slug = encodeURIComponent(opts.wiki ?? DEFAULT_WIKI_SLUG);
|
|
20225
21579
|
const endpoint = `/api/wiki/${slug}/pages/${safe}`;
|
|
20226
21580
|
const resp = await ctx.api.getRaw(endpoint, {
|
|
20227
21581
|
headers: { Accept: "text/markdown,*/*" }
|
|
20228
21582
|
});
|
|
20229
21583
|
if (resp.status === 404) {
|
|
20230
|
-
throw new CLIError(`page not found: ${
|
|
21584
|
+
throw new CLIError(`page not found: ${path9}`, 3 /* NotFound */);
|
|
20231
21585
|
}
|
|
20232
21586
|
if (!resp.ok) {
|
|
20233
21587
|
const detail = await resp.text();
|
|
@@ -20239,12 +21593,12 @@ ${pages.length} pages`));
|
|
|
20239
21593
|
if (!isJson()) {
|
|
20240
21594
|
console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
|
|
20241
21595
|
} else {
|
|
20242
|
-
console.log(JSON.stringify({ path:
|
|
21596
|
+
console.log(JSON.stringify({ path: path9, bytes: body.length, file: opts.out }));
|
|
20243
21597
|
}
|
|
20244
21598
|
return;
|
|
20245
21599
|
}
|
|
20246
21600
|
if (isJson()) {
|
|
20247
|
-
console.log(JSON.stringify({ path:
|
|
21601
|
+
console.log(JSON.stringify({ path: path9, body, etag: resp.headers.get("ETag") }));
|
|
20248
21602
|
} else {
|
|
20249
21603
|
process.stdout.write(body);
|
|
20250
21604
|
}
|
|
@@ -21047,6 +22401,125 @@ function registerPolicyCommands(program2) {
|
|
|
21047
22401
|
});
|
|
21048
22402
|
}
|
|
21049
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
|
+
|
|
21050
22523
|
// src/commands/authz.ts
|
|
21051
22524
|
init_errors();
|
|
21052
22525
|
|
|
@@ -21608,7 +23081,7 @@ function normalizeList(values) {
|
|
|
21608
23081
|
return [];
|
|
21609
23082
|
return values.flatMap((v) => v.split(/[,\s]+/)).map((v) => v.trim()).filter(Boolean);
|
|
21610
23083
|
}
|
|
21611
|
-
function
|
|
23084
|
+
function parseIntents2(raw) {
|
|
21612
23085
|
const out = [];
|
|
21613
23086
|
for (const v of raw) {
|
|
21614
23087
|
if (!VALID_INTENTS.includes(v)) {
|
|
@@ -21720,7 +23193,7 @@ Provision an access principal now so calls just work?`, true);
|
|
|
21720
23193
|
} else {
|
|
21721
23194
|
mode = "human";
|
|
21722
23195
|
}
|
|
21723
|
-
let intents =
|
|
23196
|
+
let intents = parseIntents2(normalizeList(opts.intent));
|
|
21724
23197
|
if (intents.length === 0 && interactive) {
|
|
21725
23198
|
intents = await askIntents();
|
|
21726
23199
|
}
|
|
@@ -21925,7 +23398,7 @@ async function askIntents() {
|
|
|
21925
23398
|
const n = Number(t);
|
|
21926
23399
|
return Number.isInteger(n) && n >= 1 && n <= VALID_INTENTS.length ? VALID_INTENTS[n - 1] : t;
|
|
21927
23400
|
});
|
|
21928
|
-
return
|
|
23401
|
+
return parseIntents2(raw);
|
|
21929
23402
|
}
|
|
21930
23403
|
function registerInitCommand(program2) {
|
|
21931
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) => {
|
|
@@ -21936,11 +23409,11 @@ function registerInitCommand(program2) {
|
|
|
21936
23409
|
|
|
21937
23410
|
// src/commands/doctor.ts
|
|
21938
23411
|
var FETCH_TIMEOUT_MS = 5000;
|
|
21939
|
-
function resolveApiUrl(base,
|
|
21940
|
-
return new URL(
|
|
23412
|
+
function resolveApiUrl(base, path9) {
|
|
23413
|
+
return new URL(path9, base).toString();
|
|
21941
23414
|
}
|
|
21942
|
-
function apiUrl(ctx,
|
|
21943
|
-
return resolveApiUrl(ctx.profile.api_url,
|
|
23415
|
+
function apiUrl(ctx, path9) {
|
|
23416
|
+
return resolveApiUrl(ctx.profile.api_url, path9);
|
|
21944
23417
|
}
|
|
21945
23418
|
async function fetchJSON(url, init) {
|
|
21946
23419
|
const res = await fetch(url, {
|
|
@@ -22238,15 +23711,15 @@ function registerDoctorCommand(program2) {
|
|
|
22238
23711
|
// src/commands/skill.ts
|
|
22239
23712
|
init_errors();
|
|
22240
23713
|
import fs12 from "node:fs";
|
|
22241
|
-
import
|
|
23714
|
+
import path9 from "node:path";
|
|
22242
23715
|
import { fileURLToPath } from "node:url";
|
|
22243
23716
|
function locateSkill() {
|
|
22244
|
-
const here =
|
|
23717
|
+
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
22245
23718
|
const candidates = [
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
23719
|
+
path9.join(here, "skills", "martha-cli", "SKILL.md"),
|
|
23720
|
+
path9.join(here, "..", "skills", "martha-cli", "SKILL.md"),
|
|
23721
|
+
path9.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
|
|
23722
|
+
path9.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
|
|
22250
23723
|
];
|
|
22251
23724
|
for (const p of candidates) {
|
|
22252
23725
|
if (fs12.existsSync(p))
|
|
@@ -22440,6 +23913,9 @@ auth.command("token").description("Print current access token").action(async ()
|
|
|
22440
23913
|
await authTokenCommand(ctx);
|
|
22441
23914
|
});
|
|
22442
23915
|
registerChatCommand(program2);
|
|
23916
|
+
registerAcpServeCommand(program2);
|
|
23917
|
+
registerPrincipalCommands(program2);
|
|
23918
|
+
registerChannelCommands(program2);
|
|
22443
23919
|
registerConfigCommand(program2);
|
|
22444
23920
|
registerDefinitionCommands(program2, functionsConfig);
|
|
22445
23921
|
registerDefinitionCommands(program2, workflowsConfig);
|
|
@@ -22465,6 +23941,7 @@ registerModelsCommand(program2);
|
|
|
22465
23941
|
registerSessionCommands(program2);
|
|
22466
23942
|
registerMCPCommands(program2);
|
|
22467
23943
|
registerPolicyCommands(program2);
|
|
23944
|
+
registerAgentStepCommands(program2);
|
|
22468
23945
|
registerAuthzCommands(program2);
|
|
22469
23946
|
registerInitCommand(program2);
|
|
22470
23947
|
registerDoctorCommand(program2);
|