@aiaiai-pt/martha-cli 0.9.1 → 0.14.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 +7 -0
- package/dist/index.js +633 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.0] — 2026-06-10
|
|
8
|
+
|
|
9
|
+
### Added — #540 Phase 2.0/2.1 chat copilot + task operator
|
|
10
|
+
- `martha init --agent` provisions a personal **chat copilot** alongside the run-as bootstrap: a `cloud` agent (no extra credentials) linked to your client, with the intent's tools granted to the **agent** surface (where agent grants fire in chat). `--agent-name` names it; `--model` overrides the model (defaults to the platform default). Persists `default_agent` to the profile.
|
|
11
|
+
- `martha chat` now defaults to your bootstrapped client + copilot so its tools work out of the box; `--agent <name>` selects a specific copilot.
|
|
12
|
+
- **Task operator**: with the `tasks` intent, the copilot can `create_task` / `list_tasks` / `get_task` — tasks run on Martha's task pipeline and are scoped to the copilot's own tasks. (Backend platform functions; surfaced through chat.)
|
|
13
|
+
|
|
7
14
|
## [0.9.1] — 2026-06-10
|
|
8
15
|
|
|
9
16
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -10295,6 +10295,33 @@ async function statusCommand(ctx) {
|
|
|
10295
10295
|
init_token_store();
|
|
10296
10296
|
import { createServer } from "node:http";
|
|
10297
10297
|
import { randomBytes, createHash } from "node:crypto";
|
|
10298
|
+
|
|
10299
|
+
// src/lib/browser.ts
|
|
10300
|
+
async function openBrowser(url) {
|
|
10301
|
+
const { execFile } = await import("node:child_process");
|
|
10302
|
+
const { platform } = await import("node:os");
|
|
10303
|
+
const os3 = platform();
|
|
10304
|
+
return new Promise((resolve, reject) => {
|
|
10305
|
+
if (os3 === "win32") {
|
|
10306
|
+
execFile("cmd", ["/c", "start", "", url], (err) => {
|
|
10307
|
+
if (err)
|
|
10308
|
+
reject(err);
|
|
10309
|
+
else
|
|
10310
|
+
resolve();
|
|
10311
|
+
});
|
|
10312
|
+
} else {
|
|
10313
|
+
const cmd = os3 === "darwin" ? "open" : "xdg-open";
|
|
10314
|
+
execFile(cmd, [url], (err) => {
|
|
10315
|
+
if (err)
|
|
10316
|
+
reject(err);
|
|
10317
|
+
else
|
|
10318
|
+
resolve();
|
|
10319
|
+
});
|
|
10320
|
+
}
|
|
10321
|
+
});
|
|
10322
|
+
}
|
|
10323
|
+
|
|
10324
|
+
// src/lib/auth/oidc.ts
|
|
10298
10325
|
function generateCodeVerifier() {
|
|
10299
10326
|
return randomBytes(32).toString("base64url");
|
|
10300
10327
|
}
|
|
@@ -10413,29 +10440,6 @@ function waitForAuthCode(opts) {
|
|
|
10413
10440
|
}, timeoutMs);
|
|
10414
10441
|
});
|
|
10415
10442
|
}
|
|
10416
|
-
async function openBrowser(url) {
|
|
10417
|
-
const { execFile } = await import("node:child_process");
|
|
10418
|
-
const { platform } = await import("node:os");
|
|
10419
|
-
const os3 = platform();
|
|
10420
|
-
return new Promise((resolve, reject) => {
|
|
10421
|
-
if (os3 === "win32") {
|
|
10422
|
-
execFile("cmd", ["/c", "start", "", url], (err) => {
|
|
10423
|
-
if (err)
|
|
10424
|
-
reject(err);
|
|
10425
|
-
else
|
|
10426
|
-
resolve();
|
|
10427
|
-
});
|
|
10428
|
-
} else {
|
|
10429
|
-
const cmd = os3 === "darwin" ? "open" : "xdg-open";
|
|
10430
|
-
execFile(cmd, [url], (err) => {
|
|
10431
|
-
if (err)
|
|
10432
|
-
reject(err);
|
|
10433
|
-
else
|
|
10434
|
-
resolve();
|
|
10435
|
-
});
|
|
10436
|
-
}
|
|
10437
|
-
});
|
|
10438
|
-
}
|
|
10439
10443
|
async function exchangeCode(opts) {
|
|
10440
10444
|
const tokenUrl = `${opts.keycloakUrl}/realms/${opts.realm}/protocol/openid-connect/token`;
|
|
10441
10445
|
const body = new URLSearchParams({
|
|
@@ -10817,6 +10821,28 @@ async function confirm(message) {
|
|
|
10817
10821
|
});
|
|
10818
10822
|
});
|
|
10819
10823
|
}
|
|
10824
|
+
async function promptSecret(message) {
|
|
10825
|
+
if (!process.stdin.isTTY)
|
|
10826
|
+
return;
|
|
10827
|
+
const rl = createInterface({
|
|
10828
|
+
input: process.stdin,
|
|
10829
|
+
output: process.stderr,
|
|
10830
|
+
terminal: true
|
|
10831
|
+
});
|
|
10832
|
+
const muted = rl;
|
|
10833
|
+
const original = muted._writeToOutput?.bind(rl);
|
|
10834
|
+
process.stderr.write(`${message}: `);
|
|
10835
|
+
muted._writeToOutput = () => {};
|
|
10836
|
+
return new Promise((resolve) => {
|
|
10837
|
+
rl.question("", (answer) => {
|
|
10838
|
+
muted._writeToOutput = original;
|
|
10839
|
+
rl.close();
|
|
10840
|
+
process.stderr.write(`
|
|
10841
|
+
`);
|
|
10842
|
+
resolve(answer);
|
|
10843
|
+
});
|
|
10844
|
+
});
|
|
10845
|
+
}
|
|
10820
10846
|
|
|
10821
10847
|
// src/commands/definitions.ts
|
|
10822
10848
|
init_errors();
|
|
@@ -11051,7 +11077,7 @@ import { createInterface as createInterface2 } from "node:readline";
|
|
|
11051
11077
|
init_errors();
|
|
11052
11078
|
|
|
11053
11079
|
// src/version.ts
|
|
11054
|
-
var CLI_VERSION = "0.
|
|
11080
|
+
var CLI_VERSION = "0.14.0";
|
|
11055
11081
|
|
|
11056
11082
|
// src/commands/sessions.ts
|
|
11057
11083
|
function relativeTime(iso) {
|
|
@@ -11204,8 +11230,13 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
|
|
|
11204
11230
|
signal: opts.signal,
|
|
11205
11231
|
timeout: opts.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS
|
|
11206
11232
|
};
|
|
11207
|
-
if (opts.clientId) {
|
|
11208
|
-
|
|
11233
|
+
if (opts.clientId || opts.agentName) {
|
|
11234
|
+
const params = {};
|
|
11235
|
+
if (opts.clientId)
|
|
11236
|
+
params.selected_client = opts.clientId;
|
|
11237
|
+
if (opts.agentName)
|
|
11238
|
+
params.selected_agent = opts.agentName;
|
|
11239
|
+
reqOpts.params = params;
|
|
11209
11240
|
}
|
|
11210
11241
|
const res = await ctx.api.postRaw(`/api/chat/${encodeURIComponent(sessionId)}`, { content: message }, reqOpts);
|
|
11211
11242
|
const contentType = res.headers.get("content-type") ?? "";
|
|
@@ -11375,7 +11406,7 @@ async function showHistoryPicker(ctx, rl) {
|
|
|
11375
11406
|
}
|
|
11376
11407
|
return data.items[idx].session_id;
|
|
11377
11408
|
}
|
|
11378
|
-
async function runRepl(ctx, initialSessionId, clientId, showTools, timeoutMs) {
|
|
11409
|
+
async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, timeoutMs) {
|
|
11379
11410
|
let sessionId = initialSessionId;
|
|
11380
11411
|
process.stderr.write(source_default.dim(`martha v${CLI_VERSION} | session: ${sessionId.slice(0, 8)}...
|
|
11381
11412
|
`));
|
|
@@ -11483,7 +11514,8 @@ async function runRepl(ctx, initialSessionId, clientId, showTools, timeoutMs) {
|
|
|
11483
11514
|
onClear: showTools ? () => process.stderr.write(source_default.dim(` --- tool iteration ---
|
|
11484
11515
|
`)) : undefined,
|
|
11485
11516
|
signal: activeAbort.signal,
|
|
11486
|
-
clientId
|
|
11517
|
+
clientId,
|
|
11518
|
+
agentName
|
|
11487
11519
|
});
|
|
11488
11520
|
if (!result.aborted) {
|
|
11489
11521
|
process.stdout.write(`
|
|
@@ -11511,7 +11543,7 @@ Error: ${err instanceof Error ? err.message : String(err)}
|
|
|
11511
11543
|
process.stderr.write(`
|
|
11512
11544
|
`);
|
|
11513
11545
|
}
|
|
11514
|
-
async function runOneShot(ctx, sessionId, message, isJson, clientId, showTools, timeoutMs) {
|
|
11546
|
+
async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName, showTools, timeoutMs) {
|
|
11515
11547
|
const toolCalls = [];
|
|
11516
11548
|
function recordToolStatus(data) {
|
|
11517
11549
|
try {
|
|
@@ -11533,6 +11565,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, showTools,
|
|
|
11533
11565
|
}
|
|
11534
11566
|
const { response } = await sendMessage(ctx, sessionId, message, {
|
|
11535
11567
|
clientId,
|
|
11568
|
+
agentName,
|
|
11536
11569
|
timeoutMs,
|
|
11537
11570
|
onToolStatus: isJson ? recordToolStatus : showTools ? (data) => {
|
|
11538
11571
|
recordToolStatus(data);
|
|
@@ -11557,7 +11590,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, showTools,
|
|
|
11557
11590
|
}
|
|
11558
11591
|
}
|
|
11559
11592
|
function registerChatCommand(program2) {
|
|
11560
|
-
program2.command("chat").description("Chat with a Martha agent").option("--session <id>", "Resume an existing session").option("--client <nameOrId>", "Use a specific client (name or ID)").option("--message <text>", "Send a single message (non-interactive)").option("--show-tools", "Show tool calls and results during chat").option("--timeout <seconds>", "Per-message HTTP timeout in seconds (default: 600)").action(async (opts) => {
|
|
11593
|
+
program2.command("chat").description("Chat with a Martha agent").option("--session <id>", "Resume an existing session").option("--client <nameOrId>", "Use a specific client (name or ID)").option("--agent <name>", "Drive the chat with a specific copilot agent (defaults to profile.default_agent)").option("--message <text>", "Send a single message (non-interactive)").option("--show-tools", "Show tool calls and results during chat").option("--timeout <seconds>", "Per-message HTTP timeout in seconds (default: 600)").action(async (opts) => {
|
|
11561
11594
|
const ctx = createContext({
|
|
11562
11595
|
profileOverride: program2.opts().profile,
|
|
11563
11596
|
verbose: program2.opts().verbose
|
|
@@ -11580,6 +11613,10 @@ function registerChatCommand(program2) {
|
|
|
11580
11613
|
clientId = String(match.id);
|
|
11581
11614
|
}
|
|
11582
11615
|
}
|
|
11616
|
+
if (!clientId && ctx.profile.default_client) {
|
|
11617
|
+
clientId = ctx.profile.default_client;
|
|
11618
|
+
}
|
|
11619
|
+
const agentName = opts.agent ?? ctx.profile.default_agent;
|
|
11583
11620
|
let sessionId;
|
|
11584
11621
|
if (opts.session) {
|
|
11585
11622
|
sessionId = opts.session;
|
|
@@ -11589,12 +11626,12 @@ function registerChatCommand(program2) {
|
|
|
11589
11626
|
const showTools = !!opts.showTools;
|
|
11590
11627
|
const timeoutMs = opts.timeout ? parseTimeoutSeconds(opts.timeout) * 1000 : undefined;
|
|
11591
11628
|
if (opts.message) {
|
|
11592
|
-
await runOneShot(ctx, sessionId, opts.message, isJson, clientId, showTools, timeoutMs);
|
|
11629
|
+
await runOneShot(ctx, sessionId, opts.message, isJson, clientId, agentName, showTools, timeoutMs);
|
|
11593
11630
|
} else {
|
|
11594
11631
|
if (isJson) {
|
|
11595
11632
|
throw new CLIError("The --json flag requires --message for non-interactive mode.", 4 /* Validation */, 'Example: martha --json chat --message "hello"');
|
|
11596
11633
|
}
|
|
11597
|
-
await runRepl(ctx, sessionId, clientId, showTools, timeoutMs);
|
|
11634
|
+
await runRepl(ctx, sessionId, clientId, agentName, showTools, timeoutMs);
|
|
11598
11635
|
}
|
|
11599
11636
|
});
|
|
11600
11637
|
}
|
|
@@ -13270,6 +13307,25 @@ function formatConfig(config) {
|
|
|
13270
13307
|
const full = parts.join(", ");
|
|
13271
13308
|
return full.length > 40 ? full.slice(0, 37) + "..." : full;
|
|
13272
13309
|
}
|
|
13310
|
+
function policyView(a) {
|
|
13311
|
+
return {
|
|
13312
|
+
self_grant_enabled: a.self_grant_enabled === true,
|
|
13313
|
+
max_self_grant_scope: a.max_self_grant_scope ?? "none",
|
|
13314
|
+
self_grant_max_pending: a.self_grant_max_pending ?? 5,
|
|
13315
|
+
self_grant_allowlist: a.self_grant_allowlist ?? [],
|
|
13316
|
+
self_grant_collection_roots: a.self_grant_collection_roots ?? []
|
|
13317
|
+
};
|
|
13318
|
+
}
|
|
13319
|
+
function printPolicy(a) {
|
|
13320
|
+
const enabled = a.self_grant_enabled === true;
|
|
13321
|
+
const allow = a.self_grant_allowlist ?? [];
|
|
13322
|
+
const roots = a.self_grant_collection_roots ?? [];
|
|
13323
|
+
console.log(` Self-provisioning: ${enabled ? source_default.green("enabled") : source_default.dim("disabled")}`);
|
|
13324
|
+
console.log(` Scope ceiling: ${a.max_self_grant_scope ?? "none"}`);
|
|
13325
|
+
console.log(` Max pending: ${a.self_grant_max_pending ?? 5}`);
|
|
13326
|
+
console.log(` Allow-list: ${allow.length ? allow.join(", ") : source_default.dim("(none — nothing requestable)")}`);
|
|
13327
|
+
console.log(` Collection roots: ${roots.length ? roots.join(", ") : source_default.dim("(none)")}`);
|
|
13328
|
+
}
|
|
13273
13329
|
var API_PATH = "/api/admin/definitions/agents";
|
|
13274
13330
|
var LLM_CONFIG_KEYS = new Set([
|
|
13275
13331
|
"model",
|
|
@@ -13520,6 +13576,70 @@ Usage:
|
|
|
13520
13576
|
].join(" "));
|
|
13521
13577
|
}
|
|
13522
13578
|
});
|
|
13579
|
+
parentCmd.command("self-grant <agent>").description("View or configure an agent's self-provisioning policy (request-then-approve). " + "With no flags, prints the current policy.").option("--enable", "Allow the agent to REQUEST capability grants").option("--disable", "Disallow self-provisioning").option("--scope <scope>", "Scope ceiling: none | read_only | read_write").option("--allow <ref...>", "Add allow-list refs (function:NAME or mcp:integration/name)").option("--remove <ref...>", "Remove allow-list refs").option("--collection-root <id...>", "Add collection-subtree roots the agent may self-grant collection-scoped functions into").option("--remove-collection-root <id...>", "Remove collection roots").option("--max-pending <n>", "Max concurrently-pending requests").action(async (agent, opts) => {
|
|
13580
|
+
const ctx = getCtx();
|
|
13581
|
+
const path5 = `${API_PATH}/${encodeURIComponent(agent)}`;
|
|
13582
|
+
const current = await ctx.api.get(path5);
|
|
13583
|
+
const mutating = opts.enable || opts.disable || opts.scope !== undefined || (opts.allow?.length ?? 0) > 0 || (opts.remove?.length ?? 0) > 0 || (opts.collectionRoot?.length ?? 0) > 0 || (opts.removeCollectionRoot?.length ?? 0) > 0 || opts.maxPending !== undefined;
|
|
13584
|
+
if (!mutating) {
|
|
13585
|
+
if (isJson()) {
|
|
13586
|
+
console.log(JSON.stringify(policyView(current), null, 2));
|
|
13587
|
+
return;
|
|
13588
|
+
}
|
|
13589
|
+
console.log(source_default.bold(`Self-provisioning policy: ${agent}
|
|
13590
|
+
`));
|
|
13591
|
+
printPolicy(current);
|
|
13592
|
+
return;
|
|
13593
|
+
}
|
|
13594
|
+
if (opts.enable && opts.disable) {
|
|
13595
|
+
throw new CLIError("Cannot use --enable and --disable together.", 4 /* Validation */);
|
|
13596
|
+
}
|
|
13597
|
+
if (opts.scope !== undefined && !["none", "read_only", "read_write"].includes(opts.scope)) {
|
|
13598
|
+
throw new CLIError(`Invalid --scope "${opts.scope}". Use none | read_only | read_write.`, 4 /* Validation */);
|
|
13599
|
+
}
|
|
13600
|
+
const body = {};
|
|
13601
|
+
if (opts.enable)
|
|
13602
|
+
body.self_grant_enabled = true;
|
|
13603
|
+
if (opts.disable)
|
|
13604
|
+
body.self_grant_enabled = false;
|
|
13605
|
+
if (opts.scope !== undefined)
|
|
13606
|
+
body.max_self_grant_scope = opts.scope;
|
|
13607
|
+
if (opts.maxPending !== undefined) {
|
|
13608
|
+
const n = parseInt(opts.maxPending, 10);
|
|
13609
|
+
if (Number.isNaN(n) || n < 0) {
|
|
13610
|
+
throw new CLIError("--max-pending must be a non-negative integer.", 4 /* Validation */);
|
|
13611
|
+
}
|
|
13612
|
+
body.self_grant_max_pending = n;
|
|
13613
|
+
}
|
|
13614
|
+
if (opts.allow?.length || opts.remove?.length) {
|
|
13615
|
+
const refs = new Set(current.self_grant_allowlist ?? []);
|
|
13616
|
+
for (const r of opts.allow ?? []) {
|
|
13617
|
+
if (!r.includes(":")) {
|
|
13618
|
+
throw new CLIError(`Invalid ref "${r}". Expected function:NAME or mcp:integration/name.`, 4 /* Validation */);
|
|
13619
|
+
}
|
|
13620
|
+
refs.add(r);
|
|
13621
|
+
}
|
|
13622
|
+
for (const r of opts.remove ?? [])
|
|
13623
|
+
refs.delete(r);
|
|
13624
|
+
body.self_grant_allowlist = [...refs];
|
|
13625
|
+
}
|
|
13626
|
+
if (opts.collectionRoot?.length || opts.removeCollectionRoot?.length) {
|
|
13627
|
+
const roots = new Set((current.self_grant_collection_roots ?? []).map(String));
|
|
13628
|
+
for (const c of opts.collectionRoot ?? [])
|
|
13629
|
+
roots.add(c);
|
|
13630
|
+
for (const c of opts.removeCollectionRoot ?? [])
|
|
13631
|
+
roots.delete(c);
|
|
13632
|
+
body.self_grant_collection_roots = [...roots];
|
|
13633
|
+
}
|
|
13634
|
+
const updated = await ctx.api.put(path5, body);
|
|
13635
|
+
if (isJson()) {
|
|
13636
|
+
console.log(JSON.stringify(policyView(updated), null, 2));
|
|
13637
|
+
return;
|
|
13638
|
+
}
|
|
13639
|
+
console.log(source_default.bold(`Updated self-provisioning policy: ${agent}
|
|
13640
|
+
`));
|
|
13641
|
+
printPolicy(updated);
|
|
13642
|
+
});
|
|
13523
13643
|
}
|
|
13524
13644
|
};
|
|
13525
13645
|
|
|
@@ -17482,6 +17602,154 @@ function registerMCPCommands(program2) {
|
|
|
17482
17602
|
});
|
|
17483
17603
|
}
|
|
17484
17604
|
});
|
|
17605
|
+
cmd.command("add [target]").description("Connect an MCP server to your copilot in one step: create-or-adopt the " + "connection, discover its tools, and grant access. [target] is a catalog " + "name (e.g. `github`, `linear` — see `mcp catalog`) or an existing " + "connection (id or name); --url adds a custom server. Catalog/OAuth " + "servers need no credential and no --auth-type.").option("--url <serverUrl>", "Create a connection from a direct MCP URL").option("--name <name>", "Connection name when creating (default: derived)").option("--integration <name>", "Integration name when creating", "mcp").option("--auth-type <type>", "Auth type when creating (bearer|api_key|basic)", "bearer").option("--credential-value <value>", "Secret for a new connection ('-' stdin, '@path' file). Prompted if omitted in a TTY.").option("--agent <name>", "Grant to this agent (default: profile.default_agent)").option("--no-agent", "Do not grant the agent surface (chat)").option("--client <id>", "Also grant this client (workflow-node surface)").option("--tools <list>", "Comma-separated allow-list of tool names").option("--disabled", "Create the grant disabled").option("--no-browser", "For OAuth servers: print the consent URL instead of auto-opening it, " + "then poll (use on headless/SSH/CI where you'll open the URL yourself).").option("--yes", "Assume yes for prompts (required in non-TTY)").action(async (connection, opts) => {
|
|
17606
|
+
const ctx = getCtx();
|
|
17607
|
+
const json = isJson();
|
|
17608
|
+
const conn = await resolveOrCreateConnection(ctx, connection, opts);
|
|
17609
|
+
const toolCount = await discoverOrDriveOAuth(ctx, conn, opts, json);
|
|
17610
|
+
const { agent, client } = resolveTargets(opts, ctx.profile);
|
|
17611
|
+
if (!agent && !client) {
|
|
17612
|
+
throw new CLIError("No grant target. Pass --agent <name>, set profile.default_agent, or --client <id>.", 4 /* Validation */);
|
|
17613
|
+
}
|
|
17614
|
+
const config = buildGrantConfig(opts.tools);
|
|
17615
|
+
const enabled = !opts.disabled;
|
|
17616
|
+
const granted = [];
|
|
17617
|
+
if (agent) {
|
|
17618
|
+
await ctx.api.post(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`, {
|
|
17619
|
+
connection_id: conn.id,
|
|
17620
|
+
enabled,
|
|
17621
|
+
config
|
|
17622
|
+
});
|
|
17623
|
+
granted.push(`agent '${agent}'`);
|
|
17624
|
+
}
|
|
17625
|
+
if (client) {
|
|
17626
|
+
await ctx.api.post(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp`, {
|
|
17627
|
+
connection_id: conn.id,
|
|
17628
|
+
enabled,
|
|
17629
|
+
config
|
|
17630
|
+
});
|
|
17631
|
+
granted.push(`client ${client}`);
|
|
17632
|
+
}
|
|
17633
|
+
if (json) {
|
|
17634
|
+
console.log(JSON.stringify({
|
|
17635
|
+
connection_id: conn.id,
|
|
17636
|
+
connection_name: conn.name,
|
|
17637
|
+
tools: toolCount,
|
|
17638
|
+
granted_to: granted,
|
|
17639
|
+
enabled
|
|
17640
|
+
}, null, 2));
|
|
17641
|
+
return;
|
|
17642
|
+
}
|
|
17643
|
+
console.log(source_default.green(`Connected '${conn.name}' — ${toolCount} tool${toolCount === 1 ? "" : "s"}. ` + `Granted to ${granted.join(", ")}${enabled ? "" : " (disabled)"}.`));
|
|
17644
|
+
});
|
|
17645
|
+
cmd.command("catalog").description("Browse Martha's catalog of known MCP servers. Add one by name with " + "`mcp add <name>` — no URL, no credential for OAuth servers.").option("--search <query>", "Filter by name/description").action(async (opts) => {
|
|
17646
|
+
const ctx = getCtx();
|
|
17647
|
+
const qs = opts.search ? `?search=${encodeURIComponent(opts.search)}` : "";
|
|
17648
|
+
const entries = await ctx.api.get(`/api/admin/mcp/catalog${qs}`);
|
|
17649
|
+
if (isJson()) {
|
|
17650
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
17651
|
+
return;
|
|
17652
|
+
}
|
|
17653
|
+
if (!entries.length) {
|
|
17654
|
+
console.log(source_default.dim("No catalog entries match."));
|
|
17655
|
+
return;
|
|
17656
|
+
}
|
|
17657
|
+
for (const e of entries) {
|
|
17658
|
+
const auth = e.auth_mode === "oauth2" ? source_default.dim("(OAuth)") : source_default.dim(`(${e.auth_mode})`);
|
|
17659
|
+
console.log(` ${source_default.cyan(e.catalog_key)} ${e.display_name} ${auth} ` + `${source_default.dim(e.trust_tier)}`);
|
|
17660
|
+
if (e.description)
|
|
17661
|
+
console.log(source_default.dim(` ${e.description}`));
|
|
17662
|
+
}
|
|
17663
|
+
console.log(source_default.dim(`
|
|
17664
|
+
Add one: martha mcp add <name> --agent <agent>`));
|
|
17665
|
+
});
|
|
17666
|
+
cmd.command("ls").description("List MCP connections granted to an agent (and optionally a client)").option("--agent <name>", "Agent to inspect (default: profile.default_agent)").option("--client <id>", "Also list this client's grants").action(async (opts) => {
|
|
17667
|
+
const ctx = getCtx();
|
|
17668
|
+
const json = isJson();
|
|
17669
|
+
const agent = opts.agent ?? ctx.profile.default_agent;
|
|
17670
|
+
const out = {};
|
|
17671
|
+
if (agent) {
|
|
17672
|
+
out[`agent:${agent}`] = await ctx.api.get(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`);
|
|
17673
|
+
}
|
|
17674
|
+
if (opts.client) {
|
|
17675
|
+
out[`client:${opts.client}`] = await ctx.api.get(`/api/admin/definitions/clients/${encodeURIComponent(opts.client)}/mcp`);
|
|
17676
|
+
}
|
|
17677
|
+
if (!agent && !opts.client) {
|
|
17678
|
+
throw new CLIError("Nothing to list. Pass --agent, set profile.default_agent, or --client <id>.", 4 /* Validation */);
|
|
17679
|
+
}
|
|
17680
|
+
if (json) {
|
|
17681
|
+
console.log(JSON.stringify(out, null, 2));
|
|
17682
|
+
return;
|
|
17683
|
+
}
|
|
17684
|
+
for (const [scope, grants] of Object.entries(out)) {
|
|
17685
|
+
console.log(source_default.bold(scope));
|
|
17686
|
+
if (!grants.length) {
|
|
17687
|
+
console.log(source_default.dim(" (none)"));
|
|
17688
|
+
continue;
|
|
17689
|
+
}
|
|
17690
|
+
for (const g of grants) {
|
|
17691
|
+
const allow = g.config?.tools?.length;
|
|
17692
|
+
console.log(` ${source_default.cyan(g.connection_name ?? g.connection_id)}` + `${g.enabled ? "" : source_default.dim(" [disabled]")}` + `${allow ? source_default.dim(` (${allow} tool allow-list)`) : ""}`);
|
|
17693
|
+
}
|
|
17694
|
+
}
|
|
17695
|
+
});
|
|
17696
|
+
cmd.command("grant <connection>").description("Grant an existing connection to an agent and/or client").option("--agent <name>", "Agent (default: profile.default_agent)").option("--no-agent", "Skip the agent surface").option("--client <id>", "Client (workflow-node surface)").option("--tools <list>", "Comma-separated allow-list").action(async (connection, opts) => {
|
|
17697
|
+
const ctx = getCtx();
|
|
17698
|
+
const conn = await resolveConnection(ctx, connection);
|
|
17699
|
+
const { agent, client } = resolveTargets(opts, ctx.profile);
|
|
17700
|
+
if (!agent && !client) {
|
|
17701
|
+
throw new CLIError("No grant target (--agent / default_agent / --client).", 4 /* Validation */);
|
|
17702
|
+
}
|
|
17703
|
+
const config = buildGrantConfig(opts.tools);
|
|
17704
|
+
if (agent)
|
|
17705
|
+
await ctx.api.post(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`, {
|
|
17706
|
+
connection_id: conn.id,
|
|
17707
|
+
enabled: true,
|
|
17708
|
+
config
|
|
17709
|
+
});
|
|
17710
|
+
if (client)
|
|
17711
|
+
await ctx.api.post(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp`, {
|
|
17712
|
+
connection_id: conn.id,
|
|
17713
|
+
enabled: true,
|
|
17714
|
+
config
|
|
17715
|
+
});
|
|
17716
|
+
if (isJson())
|
|
17717
|
+
console.log(JSON.stringify({ connection_id: conn.id, granted: true }, null, 2));
|
|
17718
|
+
else
|
|
17719
|
+
console.log(source_default.green(`Granted '${conn.name}'.`));
|
|
17720
|
+
});
|
|
17721
|
+
cmd.command("revoke <connection>").description("Revoke a connection from an agent and/or client").option("--agent <name>", "Agent (default: profile.default_agent)").option("--no-agent", "Skip the agent surface").option("--client <id>", "Client").action(async (connection, opts) => {
|
|
17722
|
+
const ctx = getCtx();
|
|
17723
|
+
const conn = await resolveConnection(ctx, connection);
|
|
17724
|
+
const { agent, client } = resolveTargets(opts, ctx.profile);
|
|
17725
|
+
if (agent)
|
|
17726
|
+
await ctx.api.del(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp/${conn.id}`);
|
|
17727
|
+
if (client)
|
|
17728
|
+
await ctx.api.del(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp/${conn.id}`);
|
|
17729
|
+
if (isJson())
|
|
17730
|
+
console.log(JSON.stringify({ connection_id: conn.id, revoked: true }, null, 2));
|
|
17731
|
+
else
|
|
17732
|
+
console.log(source_default.green(`Revoked '${conn.name}'.`));
|
|
17733
|
+
});
|
|
17734
|
+
cmd.command("disable <connection>").description("Disable a grant without revoking it (keeps the row, enabled=false)").option("--agent <name>", "Agent (default: profile.default_agent)").option("--no-agent", "Skip the agent surface").option("--client <id>", "Client").action(async (connection, opts) => {
|
|
17735
|
+
const ctx = getCtx();
|
|
17736
|
+
const conn = await resolveConnection(ctx, connection);
|
|
17737
|
+
const { agent, client } = resolveTargets(opts, ctx.profile);
|
|
17738
|
+
if (agent)
|
|
17739
|
+
await ctx.api.post(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`, {
|
|
17740
|
+
connection_id: conn.id,
|
|
17741
|
+
enabled: false
|
|
17742
|
+
});
|
|
17743
|
+
if (client)
|
|
17744
|
+
await ctx.api.post(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp`, {
|
|
17745
|
+
connection_id: conn.id,
|
|
17746
|
+
enabled: false
|
|
17747
|
+
});
|
|
17748
|
+
if (isJson())
|
|
17749
|
+
console.log(JSON.stringify({ connection_id: conn.id, enabled: false }, null, 2));
|
|
17750
|
+
else
|
|
17751
|
+
console.log(source_default.green(`Disabled '${conn.name}'.`));
|
|
17752
|
+
});
|
|
17485
17753
|
}
|
|
17486
17754
|
function mcpOAuthAuthorizationUrl(err) {
|
|
17487
17755
|
if (!(err instanceof MarthaAPIError) || err.status !== 428)
|
|
@@ -17497,6 +17765,68 @@ function mcpOAuthAuthorizationUrl(err) {
|
|
|
17497
17765
|
return null;
|
|
17498
17766
|
return typeof nested.authorization_url === "string" ? nested.authorization_url : null;
|
|
17499
17767
|
}
|
|
17768
|
+
async function discoverOrDriveOAuth(ctx, conn, opts, json) {
|
|
17769
|
+
try {
|
|
17770
|
+
const disc = await ctx.api.post("/api/admin/mcp/discover", {
|
|
17771
|
+
connection_id: conn.id
|
|
17772
|
+
});
|
|
17773
|
+
return disc.tools?.length ?? 0;
|
|
17774
|
+
} catch (err) {
|
|
17775
|
+
const authUrl = mcpOAuthAuthorizationUrl(err);
|
|
17776
|
+
if (!authUrl) {
|
|
17777
|
+
throw new CLIError(`Could not reach MCP server for connection '${conn.name}'.`, 1 /* Error */, err instanceof Error ? err.message : undefined);
|
|
17778
|
+
}
|
|
17779
|
+
await driveMcpOAuth(ctx, conn, authUrl, opts, json);
|
|
17780
|
+
const disc = await ctx.api.post("/api/admin/mcp/discover", {
|
|
17781
|
+
connection_id: conn.id
|
|
17782
|
+
});
|
|
17783
|
+
return disc.tools?.length ?? 0;
|
|
17784
|
+
}
|
|
17785
|
+
}
|
|
17786
|
+
async function driveMcpOAuth(ctx, conn, authUrl, opts, json) {
|
|
17787
|
+
const wantsBrowser = opts.browser !== false;
|
|
17788
|
+
const interactive = process.stdin.isTTY || !wantsBrowser;
|
|
17789
|
+
if (!interactive) {
|
|
17790
|
+
throw new CLIError(`Connection '${conn.name}' needs OAuth, which requires interactive browser consent.`, 4 /* Validation */, `Open this URL in a browser to authorize:
|
|
17791
|
+
${authUrl}
|
|
17792
|
+
|
|
17793
|
+
` + `Then re-run 'martha mcp add ${conn.name} ...' to adopt the now-authorized connection. ` + `In CI, pass --no-browser to print the URL and poll, or pre-create the ` + `connection in the admin UI (Integrations → Connections).`);
|
|
17794
|
+
}
|
|
17795
|
+
const log = (msg) => process.stderr.write(`${msg}
|
|
17796
|
+
`);
|
|
17797
|
+
log(source_default.bold(`Authorize '${conn.name}' in your browser:`));
|
|
17798
|
+
log(` ${authUrl}`);
|
|
17799
|
+
if (process.stdin.isTTY && wantsBrowser) {
|
|
17800
|
+
openBrowser(authUrl).catch(() => {
|
|
17801
|
+
log(source_default.dim("(couldn't open a browser automatically — open the URL above)"));
|
|
17802
|
+
});
|
|
17803
|
+
}
|
|
17804
|
+
log(source_default.dim("Waiting for authorization to complete…"));
|
|
17805
|
+
await pollConnectionActive(ctx, conn.id);
|
|
17806
|
+
if (!json)
|
|
17807
|
+
log(source_default.green("Authorized."));
|
|
17808
|
+
}
|
|
17809
|
+
async function pollConnectionActive(ctx, connectionId) {
|
|
17810
|
+
const intervalMs = positiveIntEnv("MARTHA_MCP_OAUTH_POLL_MS", 2000);
|
|
17811
|
+
const timeoutMs = positiveIntEnv("MARTHA_MCP_OAUTH_TIMEOUT_MS", 180000);
|
|
17812
|
+
const started = Date.now();
|
|
17813
|
+
while (Date.now() - started < timeoutMs) {
|
|
17814
|
+
const conn = await ctx.api.get(`/api/admin/connections/${encodeURIComponent(connectionId)}`).catch(() => {
|
|
17815
|
+
return;
|
|
17816
|
+
});
|
|
17817
|
+
if (conn?.status === "active")
|
|
17818
|
+
return;
|
|
17819
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
17820
|
+
}
|
|
17821
|
+
throw new CLIError(`Timed out waiting for OAuth authorization (${Math.round(timeoutMs / 1000)}s).`, 1 /* Error */, "The browser consent may not have completed. Re-run to try again, or check " + "the connection status in the admin UI.");
|
|
17822
|
+
}
|
|
17823
|
+
function positiveIntEnv(name, fallback) {
|
|
17824
|
+
const raw = process.env[name];
|
|
17825
|
+
if (!raw)
|
|
17826
|
+
return fallback;
|
|
17827
|
+
const n = Number.parseInt(raw, 10);
|
|
17828
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
17829
|
+
}
|
|
17500
17830
|
function buildLocatorBody(opts) {
|
|
17501
17831
|
const body = {};
|
|
17502
17832
|
if (opts.url)
|
|
@@ -17564,6 +17894,251 @@ function extractMCPOutput(status) {
|
|
|
17564
17894
|
}
|
|
17565
17895
|
return output;
|
|
17566
17896
|
}
|
|
17897
|
+
function resolveTargets(opts, profile) {
|
|
17898
|
+
let agent;
|
|
17899
|
+
if (opts.agent === false)
|
|
17900
|
+
agent = undefined;
|
|
17901
|
+
else if (typeof opts.agent === "string")
|
|
17902
|
+
agent = opts.agent;
|
|
17903
|
+
else
|
|
17904
|
+
agent = profile.default_agent;
|
|
17905
|
+
return { agent, client: opts.client };
|
|
17906
|
+
}
|
|
17907
|
+
function buildGrantConfig(tools) {
|
|
17908
|
+
if (!tools)
|
|
17909
|
+
return;
|
|
17910
|
+
const list = tools.split(",").map((t) => t.trim()).filter(Boolean);
|
|
17911
|
+
return list.length ? { tools: list } : undefined;
|
|
17912
|
+
}
|
|
17913
|
+
async function findConnection(ctx, idOrName) {
|
|
17914
|
+
const conns = await ctx.api.get("/api/admin/connections");
|
|
17915
|
+
const byId = conns.find((c) => c.id === idOrName);
|
|
17916
|
+
if (byId)
|
|
17917
|
+
return byId;
|
|
17918
|
+
const byName = conns.filter((c) => c.name === idOrName);
|
|
17919
|
+
if (byName.length === 1)
|
|
17920
|
+
return byName[0];
|
|
17921
|
+
if (byName.length > 1) {
|
|
17922
|
+
throw new CLIError(`Multiple connections named '${idOrName}'; pass the connection id instead.`, 4 /* Validation */);
|
|
17923
|
+
}
|
|
17924
|
+
return null;
|
|
17925
|
+
}
|
|
17926
|
+
async function resolveConnection(ctx, idOrName) {
|
|
17927
|
+
const conn = await findConnection(ctx, idOrName);
|
|
17928
|
+
if (!conn) {
|
|
17929
|
+
throw new CLIError(`Connection '${idOrName}' not found.`, 3 /* NotFound */);
|
|
17930
|
+
}
|
|
17931
|
+
return conn;
|
|
17932
|
+
}
|
|
17933
|
+
async function lookupCatalogEntry(ctx, key) {
|
|
17934
|
+
const entries = await ctx.api.get(`/api/admin/mcp/catalog?search=${encodeURIComponent(key)}`).catch(() => []);
|
|
17935
|
+
return entries.find((e) => e.catalog_key === key) ?? null;
|
|
17936
|
+
}
|
|
17937
|
+
async function resolveOrCreateConnection(ctx, positional, opts) {
|
|
17938
|
+
if (positional) {
|
|
17939
|
+
const existing = await findConnection(ctx, positional);
|
|
17940
|
+
if (existing)
|
|
17941
|
+
return existing;
|
|
17942
|
+
const entry = await lookupCatalogEntry(ctx, positional);
|
|
17943
|
+
if (entry) {
|
|
17944
|
+
return createConnectionFromSpec(ctx, {
|
|
17945
|
+
name: opts.name ?? entry.catalog_key,
|
|
17946
|
+
integration: opts.integration,
|
|
17947
|
+
serverUrl: entry.server_url,
|
|
17948
|
+
authType: entry.auth_mode,
|
|
17949
|
+
scopes: entry.default_scopes ?? undefined
|
|
17950
|
+
}, opts);
|
|
17951
|
+
}
|
|
17952
|
+
throw new CLIError(`'${positional}' is not a known connection or catalog entry.`, 3 /* NotFound */, "Pass --url to add a custom MCP server, or run `martha mcp catalog` to see known servers.");
|
|
17953
|
+
}
|
|
17954
|
+
if (!opts.url) {
|
|
17955
|
+
throw new CLIError("Provide a catalog name, an existing connection (id or name), or --url.", 4 /* Validation */);
|
|
17956
|
+
}
|
|
17957
|
+
return createConnectionFromSpec(ctx, {
|
|
17958
|
+
name: opts.name ?? deriveConnectionName(opts.url),
|
|
17959
|
+
integration: opts.integration,
|
|
17960
|
+
serverUrl: opts.url,
|
|
17961
|
+
authType: opts.authType
|
|
17962
|
+
}, opts);
|
|
17963
|
+
}
|
|
17964
|
+
async function createConnectionFromSpec(ctx, spec, opts) {
|
|
17965
|
+
const existing = (await ctx.api.get("/api/admin/connections")).filter((c) => c.name === spec.name);
|
|
17966
|
+
if (existing.length === 1)
|
|
17967
|
+
return existing[0];
|
|
17968
|
+
const body = {
|
|
17969
|
+
integration_name: spec.integration,
|
|
17970
|
+
name: spec.name,
|
|
17971
|
+
auth_type: spec.authType,
|
|
17972
|
+
config: { server_url: spec.serverUrl },
|
|
17973
|
+
scope: "tenant",
|
|
17974
|
+
is_default: true
|
|
17975
|
+
};
|
|
17976
|
+
if (spec.authType === "oauth2") {
|
|
17977
|
+
if (spec.scopes?.length)
|
|
17978
|
+
body.oauth_config = { scopes: spec.scopes };
|
|
17979
|
+
return ctx.api.post("/api/admin/connections", body);
|
|
17980
|
+
}
|
|
17981
|
+
let credential = await resolveCredentialFlag(opts.credentialValue);
|
|
17982
|
+
if (!credential) {
|
|
17983
|
+
credential = await promptSecret(`${spec.authType} secret for '${spec.name}'`);
|
|
17984
|
+
}
|
|
17985
|
+
if (!credential) {
|
|
17986
|
+
throw new CLIError("A credential is required to create a connection (use --credential-value, '-' for stdin, '@path', or run in a TTY to be prompted).", 4 /* Validation */);
|
|
17987
|
+
}
|
|
17988
|
+
body.credential_value = credential;
|
|
17989
|
+
return ctx.api.post("/api/admin/connections", body);
|
|
17990
|
+
}
|
|
17991
|
+
function deriveConnectionName(url) {
|
|
17992
|
+
try {
|
|
17993
|
+
const host = new URL(url).hostname.replace(/^www\./, "");
|
|
17994
|
+
return `mcp-${host}`.toLowerCase();
|
|
17995
|
+
} catch {
|
|
17996
|
+
return `mcp-${Date.now().toString(36)}`;
|
|
17997
|
+
}
|
|
17998
|
+
}
|
|
17999
|
+
async function resolveCredentialFlag(value) {
|
|
18000
|
+
if (!value)
|
|
18001
|
+
return;
|
|
18002
|
+
if (value === "-") {
|
|
18003
|
+
const chunks = [];
|
|
18004
|
+
for await (const c of process.stdin)
|
|
18005
|
+
chunks.push(c);
|
|
18006
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
18007
|
+
}
|
|
18008
|
+
if (value.startsWith("@")) {
|
|
18009
|
+
const fs9 = await import("node:fs/promises");
|
|
18010
|
+
return (await fs9.readFile(value.slice(1), "utf-8")).trim();
|
|
18011
|
+
}
|
|
18012
|
+
return value;
|
|
18013
|
+
}
|
|
18014
|
+
|
|
18015
|
+
// src/commands/policy.ts
|
|
18016
|
+
init_errors();
|
|
18017
|
+
var ACTIONS = ["allow", "require_approval", "block"];
|
|
18018
|
+
var KINDS = ["risk_tag", "risk_level", "capability"];
|
|
18019
|
+
var SCOPES = ["any", "agent", "client"];
|
|
18020
|
+
var truncate4 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
18021
|
+
function assertOneOf(value, allowed, flag) {
|
|
18022
|
+
if (!allowed.includes(value)) {
|
|
18023
|
+
throw new CLIError(`${flag} must be one of: ${allowed.join(", ")}`, 4 /* Validation */);
|
|
18024
|
+
}
|
|
18025
|
+
}
|
|
18026
|
+
function registerPolicyCommands(program2) {
|
|
18027
|
+
const cmd = program2.command("policy").description("Manage the tenant's invoke-time capability policy");
|
|
18028
|
+
function getCtx() {
|
|
18029
|
+
const ctx = createContext({
|
|
18030
|
+
profileOverride: program2.opts().profile,
|
|
18031
|
+
verbose: program2.opts().verbose
|
|
18032
|
+
});
|
|
18033
|
+
if (program2.opts().apiUrl)
|
|
18034
|
+
ctx.profile.api_url = program2.opts().apiUrl;
|
|
18035
|
+
return ctx;
|
|
18036
|
+
}
|
|
18037
|
+
const isJson = () => !!program2.opts().json;
|
|
18038
|
+
cmd.command("show").description("Show the current policy: settings + rules").action(async () => {
|
|
18039
|
+
const ctx = getCtx();
|
|
18040
|
+
const cfg = await ctx.api.get("/api/admin/policy");
|
|
18041
|
+
if (isJson()) {
|
|
18042
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
18043
|
+
return;
|
|
18044
|
+
}
|
|
18045
|
+
const s = cfg.settings;
|
|
18046
|
+
console.log(source_default.bold("Policy settings"));
|
|
18047
|
+
console.log(source_default.dim("-".repeat(40)));
|
|
18048
|
+
console.log(`${source_default.cyan("Default action:")} ${s.default_action}` + source_default.dim(" (applied when no rule matches)"));
|
|
18049
|
+
const sd = s.safe_default_enabled ? source_default.yellow("on") : source_default.dim("off");
|
|
18050
|
+
console.log(`${source_default.cyan("Safe-default:")} ${sd}`);
|
|
18051
|
+
console.log(source_default.dim(` bundle -> require_approval for: ${cfg.safe_default_tags.join(", ")}`));
|
|
18052
|
+
console.log();
|
|
18053
|
+
console.log(source_default.bold(`Rules (${cfg.rules.length})`));
|
|
18054
|
+
console.log(source_default.dim("-".repeat(40)));
|
|
18055
|
+
if (cfg.rules.length === 0) {
|
|
18056
|
+
console.log(source_default.dim("No explicit rules — fully opt-in."));
|
|
18057
|
+
return;
|
|
18058
|
+
}
|
|
18059
|
+
const columns = [
|
|
18060
|
+
{
|
|
18061
|
+
header: "ID",
|
|
18062
|
+
get: (r) => String(r.id ?? "").slice(0, 8)
|
|
18063
|
+
},
|
|
18064
|
+
{
|
|
18065
|
+
header: "KIND",
|
|
18066
|
+
get: (r) => String(r.match_kind ?? "-")
|
|
18067
|
+
},
|
|
18068
|
+
{
|
|
18069
|
+
header: "VALUE",
|
|
18070
|
+
get: (r) => truncate4(String(r.match_value ?? "-"), 28)
|
|
18071
|
+
},
|
|
18072
|
+
{
|
|
18073
|
+
header: "SCOPE",
|
|
18074
|
+
get: (r) => String(r.principal_scope ?? "any")
|
|
18075
|
+
},
|
|
18076
|
+
{
|
|
18077
|
+
header: "ACTION",
|
|
18078
|
+
get: (r) => String(r.action ?? "-")
|
|
18079
|
+
}
|
|
18080
|
+
];
|
|
18081
|
+
const widths = columns.map((c) => Math.max(c.header.length, ...cfg.rules.map((r) => c.get(r).length)));
|
|
18082
|
+
const head = columns.map((c, i) => c.header.padEnd(widths[i])).join(" ");
|
|
18083
|
+
console.log(source_default.bold(head));
|
|
18084
|
+
console.log(source_default.dim("-".repeat(head.length)));
|
|
18085
|
+
for (const r of cfg.rules) {
|
|
18086
|
+
console.log(columns.map((c, i) => c.get(r).padEnd(widths[i])).join(" "));
|
|
18087
|
+
}
|
|
18088
|
+
});
|
|
18089
|
+
cmd.command("set-default <action>").description(`Set the opt-in default action (${ACTIONS.join("|")})`).action(async (action) => {
|
|
18090
|
+
assertOneOf(action, ACTIONS, "action");
|
|
18091
|
+
const ctx = getCtx();
|
|
18092
|
+
const res = await ctx.api.put("/api/admin/policy/settings", { default_action: action });
|
|
18093
|
+
if (isJson()) {
|
|
18094
|
+
console.log(JSON.stringify(res, null, 2));
|
|
18095
|
+
return;
|
|
18096
|
+
}
|
|
18097
|
+
console.log(`Default action set to ${source_default.cyan(action)}.`);
|
|
18098
|
+
});
|
|
18099
|
+
cmd.command("safe-default <state>").description("Turn the safe-default bundle on|off").action(async (state) => {
|
|
18100
|
+
const normalized = state.toLowerCase();
|
|
18101
|
+
if (!["on", "off", "true", "false"].includes(normalized)) {
|
|
18102
|
+
throw new CLIError("state must be on|off", 4 /* Validation */);
|
|
18103
|
+
}
|
|
18104
|
+
const enabled = normalized === "on" || normalized === "true";
|
|
18105
|
+
const ctx = getCtx();
|
|
18106
|
+
const res = await ctx.api.put("/api/admin/policy/settings", { safe_default_enabled: enabled });
|
|
18107
|
+
if (isJson()) {
|
|
18108
|
+
console.log(JSON.stringify(res, null, 2));
|
|
18109
|
+
return;
|
|
18110
|
+
}
|
|
18111
|
+
console.log(`Safe-default bundle ${enabled ? source_default.yellow("enabled") : source_default.dim("disabled")}.`);
|
|
18112
|
+
});
|
|
18113
|
+
const rule = cmd.command("rule").description("Manage explicit policy rules");
|
|
18114
|
+
rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS.join("|")})`).requiredOption("--value <value>", "Match value (a risk tag, level, or capability ref)").requiredOption(`--action <action>`, `Action (${ACTIONS.join("|")})`).option(`--scope <scope>`, `Principal scope (${SCOPES.join("|")})`, "any").option("--note <text>", "Optional human note").action(async (opts) => {
|
|
18115
|
+
assertOneOf(opts.kind, KINDS, "--kind");
|
|
18116
|
+
assertOneOf(opts.action, ACTIONS, "--action");
|
|
18117
|
+
assertOneOf(opts.scope, SCOPES, "--scope");
|
|
18118
|
+
const ctx = getCtx();
|
|
18119
|
+
const res = await ctx.api.post("/api/admin/policy/rules", {
|
|
18120
|
+
match_kind: opts.kind,
|
|
18121
|
+
match_value: opts.value,
|
|
18122
|
+
action: opts.action,
|
|
18123
|
+
principal_scope: opts.scope,
|
|
18124
|
+
...opts.note ? { note: opts.note } : {}
|
|
18125
|
+
});
|
|
18126
|
+
if (isJson()) {
|
|
18127
|
+
console.log(JSON.stringify(res, null, 2));
|
|
18128
|
+
return;
|
|
18129
|
+
}
|
|
18130
|
+
console.log(`Rule ${source_default.dim(String(res.id).slice(0, 8))}: ` + `${opts.kind}=${source_default.cyan(opts.value)} (${opts.scope}) -> ${source_default.cyan(opts.action)}`);
|
|
18131
|
+
});
|
|
18132
|
+
rule.command("rm <id>").description("Delete a rule by id").action(async (id) => {
|
|
18133
|
+
const ctx = getCtx();
|
|
18134
|
+
await ctx.api.del(`/api/admin/policy/rules/${encodeURIComponent(id)}`);
|
|
18135
|
+
if (isJson()) {
|
|
18136
|
+
console.log(JSON.stringify({ deleted: id }, null, 2));
|
|
18137
|
+
return;
|
|
18138
|
+
}
|
|
18139
|
+
console.log(`Deleted rule ${id}.`);
|
|
18140
|
+
});
|
|
18141
|
+
}
|
|
17567
18142
|
|
|
17568
18143
|
// src/commands/init.ts
|
|
17569
18144
|
import readline from "node:readline/promises";
|
|
@@ -17732,26 +18307,36 @@ Provision an access principal now so calls just work?`, true);
|
|
|
17732
18307
|
if (intents.length === 0 && workflows.length === 0 && functions.length === 0) {
|
|
17733
18308
|
throw new CLIError("Nothing to grant: declare at least one --intent (rag/documents/workflows/custom) or a --workflow/--function.", 4 /* Validation */);
|
|
17734
18309
|
}
|
|
18310
|
+
let wantAgent = mode === "human" && !!opts.agent;
|
|
18311
|
+
if (mode === "human" && !opts.agent && interactive) {
|
|
18312
|
+
wantAgent = await askYesNo(`
|
|
18313
|
+
Also set up a chat copilot you can talk to (martha chat)?`, true);
|
|
18314
|
+
}
|
|
17735
18315
|
const ctx = createContext({ profileOverride: profileName });
|
|
17736
18316
|
if (opts.apiUrl)
|
|
17737
18317
|
ctx.profile.api_url = opts.apiUrl;
|
|
17738
18318
|
await ensureAuthenticated(ctx, interactive, jsonMode);
|
|
17739
18319
|
if (!jsonMode) {
|
|
17740
18320
|
console.log();
|
|
17741
|
-
console.log(source_default.dim(` Provisioning ${mode === "integration" ? "an integration service account" : "your access principal"}…`));
|
|
18321
|
+
console.log(source_default.dim(` Provisioning ${mode === "integration" ? "an integration service account" : "your access principal"}${wantAgent ? " + copilot" : ""}…`));
|
|
17742
18322
|
}
|
|
17743
18323
|
const body = {
|
|
17744
18324
|
mode,
|
|
17745
18325
|
intents,
|
|
17746
18326
|
workflows,
|
|
17747
18327
|
functions,
|
|
17748
|
-
...opts.label ? { label: opts.label } : {}
|
|
18328
|
+
...opts.label ? { label: opts.label } : {},
|
|
18329
|
+
...wantAgent ? { agent: true } : {},
|
|
18330
|
+
...wantAgent && opts.agentName ? { agent_name: opts.agentName } : {},
|
|
18331
|
+
...wantAgent && opts.model ? { model: opts.model } : {}
|
|
17749
18332
|
};
|
|
17750
18333
|
const resp = await ctx.api.post("/api/admin/definitions/principals/bootstrap", body);
|
|
17751
18334
|
if (mode === "human" && resp.client_key) {
|
|
17752
18335
|
const cfg = loadConfig();
|
|
17753
18336
|
if (cfg.profiles[profileName]) {
|
|
17754
18337
|
cfg.profiles[profileName].default_client = resp.client_key;
|
|
18338
|
+
if (resp.agent)
|
|
18339
|
+
cfg.profiles[profileName].default_agent = resp.agent.name;
|
|
17755
18340
|
saveConfig(cfg);
|
|
17756
18341
|
}
|
|
17757
18342
|
}
|
|
@@ -17799,6 +18384,15 @@ function printBootstrapResult(resp, profileName) {
|
|
|
17799
18384
|
for (const s of resp.skipped) {
|
|
17800
18385
|
console.log(source_default.yellow(` Skipped ${s.name} — ${s.reason}`));
|
|
17801
18386
|
}
|
|
18387
|
+
if (resp.agent) {
|
|
18388
|
+
const a = resp.agent;
|
|
18389
|
+
console.log();
|
|
18390
|
+
const averb = a.adopted ? "Using existing" : "Created";
|
|
18391
|
+
console.log(source_default.green(` ${averb} copilot ${source_default.bold(a.name)}.`));
|
|
18392
|
+
if (a.granted_functions.length > 0) {
|
|
18393
|
+
console.log(` Copilot tools: ${source_default.cyan(a.granted_functions.join(", "))}`);
|
|
18394
|
+
}
|
|
18395
|
+
}
|
|
17802
18396
|
if (resp.service_account) {
|
|
17803
18397
|
const sa = resp.service_account;
|
|
17804
18398
|
console.log();
|
|
@@ -17813,6 +18407,9 @@ function printBootstrapResult(resp, profileName) {
|
|
|
17813
18407
|
console.log(source_default.dim(` Profile ${source_default.cyan(profileName)} will now run as this client.`));
|
|
17814
18408
|
console.log();
|
|
17815
18409
|
console.log(source_default.dim(" Next:"));
|
|
18410
|
+
if (resp.agent) {
|
|
18411
|
+
console.log(source_default.dim(' martha chat "what can you do?"'));
|
|
18412
|
+
}
|
|
17816
18413
|
console.log(source_default.dim(" martha workflows list"));
|
|
17817
18414
|
console.log(source_default.dim(" martha workflows execute <name> --inputs '{}'"));
|
|
17818
18415
|
}
|
|
@@ -17869,7 +18466,7 @@ async function askIntents() {
|
|
|
17869
18466
|
return parseIntents(raw);
|
|
17870
18467
|
}
|
|
17871
18468
|
function registerInitCommand(program2) {
|
|
17872
|
-
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("--yes", "Acknowledge non-interactive bootstrap side effects").action(async (opts, command) => {
|
|
18469
|
+
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("--yes", "Acknowledge non-interactive bootstrap side effects").action(async (opts, command) => {
|
|
17873
18470
|
const globals = command.parent?.opts() ?? {};
|
|
17874
18471
|
await initCommand({ ...opts, json: !!globals.json });
|
|
17875
18472
|
});
|
|
@@ -18212,6 +18809,7 @@ registerClientCommands(program2);
|
|
|
18212
18809
|
registerModelsCommand(program2);
|
|
18213
18810
|
registerSessionCommands(program2);
|
|
18214
18811
|
registerMCPCommands(program2);
|
|
18812
|
+
registerPolicyCommands(program2);
|
|
18215
18813
|
registerInitCommand(program2);
|
|
18216
18814
|
registerDoctorCommand(program2);
|
|
18217
18815
|
registerSkillCommand(program2);
|