@aiaiai-pt/martha-cli 0.54.0 → 0.57.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 +121 -0
- package/dist/index.js +202 -73
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,127 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project adheres to semver — `0.x` releases may include breaking changes between minor versions.
|
|
4
4
|
|
|
5
|
+
## [0.57.0] — 2026-08-03
|
|
6
|
+
|
|
7
|
+
### Fixed — `bootstrap --mode agent` produced an agent that could not do anything
|
|
8
|
+
|
|
9
|
+
Provisioned from scratch, the agent came back `✓ ready` and then, asked a
|
|
10
|
+
question in a channel, invented shell commands and fabricated four collections
|
|
11
|
+
that do not exist. Three separate holes, all of them ours:
|
|
12
|
+
|
|
13
|
+
- **No tools.** `--intents rag` granted the CLIENT functions and never the
|
|
14
|
+
agent's, because agent mode was composed client-side while the server's own
|
|
15
|
+
`agent` block — adopt-or-create with chat takeover, bind to the principal,
|
|
16
|
+
grant the intent's AGENT functions — was gated to human mode. That gate is
|
|
17
|
+
now open to integration mode (`agent_name` required: a service account is not
|
|
18
|
+
a person, so the name is never defaulted), and the CLI makes **one** call
|
|
19
|
+
instead of re-implementing it.
|
|
20
|
+
- **Nothing to read.** Intents grant retrieval *verbs*; the *nouns* are a
|
|
21
|
+
separate UMS grant. New `--collections <list>` grants the agent read on them
|
|
22
|
+
by name, slug or id — and when it is omitted, provisioning now says
|
|
23
|
+
`granted NO collections — its retrieval tools will return nothing`, because
|
|
24
|
+
"ready" must never mean "ready and blind".
|
|
25
|
+
- **A prompt the harness overrode.** buzz-acp tells agents they can run shell
|
|
26
|
+
commands; our default said only "don't emit `buzz messages send`". The
|
|
27
|
+
prompt for a newly created agent now states it has no shell and must never
|
|
28
|
+
write output it did not receive from a tool.
|
|
29
|
+
|
|
30
|
+
Also: the agent's system prompt applies to sessions created *after* it — an
|
|
31
|
+
adapter holding open sessions keeps the old one until restarted.
|
|
32
|
+
|
|
33
|
+
### Server
|
|
34
|
+
|
|
35
|
+
`POST /principals/bootstrap` accepts `agent` + `agent_name` in integration
|
|
36
|
+
mode, plus `agent_system_prompt` (applied only on create — never overwrites a
|
|
37
|
+
persona someone has edited) and `collections`. The response's `agent` block
|
|
38
|
+
gains `granted_collections`. Collection grants are issued after commit as UMS
|
|
39
|
+
`application` grantees with `agent_<uuid>` ids — UMS has no `agent` grantee
|
|
40
|
+
type and 422s the bare uuid, which is invisible unless you read the skip list.
|
|
41
|
+
|
|
42
|
+
## [0.56.0] — 2026-08-03
|
|
43
|
+
|
|
44
|
+
### Changed — Martha agents follow the conversation by default
|
|
45
|
+
|
|
46
|
+
The harness definition now pins `BUZZ_ACP_SUBSCRIBE=all` with
|
|
47
|
+
`BUZZ_ACP_KINDS=9,46010,40007,7`.
|
|
48
|
+
|
|
49
|
+
Buzz's default is `subscribe=mentions`, which filters every message that does
|
|
50
|
+
not `#p`-tag the agent — so a **DM got no reply**, and neither did a thread
|
|
51
|
+
reply or a follow-up question. The previous release left the pair out of the
|
|
52
|
+
definition on the reasoning that overriding Buzz's default would be
|
|
53
|
+
surprising; the actual result was that every single agent needed both
|
|
54
|
+
variables typed into Edit Agent by hand. Runtime-wide behaviour is exactly
|
|
55
|
+
what a runtime definition is for.
|
|
56
|
+
|
|
57
|
+
The two values are pinned together because `all` without kinds is a wildcard
|
|
58
|
+
subscription — typing indicators and presence updates arrive as prompts. The
|
|
59
|
+
set is buzz-acp's own mentions-mode set (chat, approval, reminder) plus
|
|
60
|
+
reactions, which `acp-serve` classifies as feedback and answers with silence.
|
|
61
|
+
|
|
62
|
+
For an agent that should only speak when named, set
|
|
63
|
+
`BUZZ_ACP_SUBSCRIBE=mentions` on that agent — per-agent env overrides the
|
|
64
|
+
definition. Existing agents pick this up after
|
|
65
|
+
`martha channels install-harness buzz --force` and a restart.
|
|
66
|
+
|
|
67
|
+
## [0.55.2] — 2026-08-03
|
|
68
|
+
|
|
69
|
+
### Fixed — a baked `MARTHA_PROFILE` is a default, and we didn't say so
|
|
70
|
+
|
|
71
|
+
0.55.0 let `install-harness --use-profile` put `MARTHA_PROFILE` in the harness
|
|
72
|
+
definition, and the output read as though that bound the Buzz agent to a Martha
|
|
73
|
+
agent. It doesn't: the definition is shared by **every** agent on the Martha
|
|
74
|
+
runtime, so a second Buzz agent would answer as the first one's Martha agent —
|
|
75
|
+
its own name and Nostr key in front of one brain, which is the opposite of what
|
|
76
|
+
a second agent is for.
|
|
77
|
+
|
|
78
|
+
Nothing to change in the mechanism, which already supports the right model:
|
|
79
|
+
per-agent env (Buzz's Stack A layer 3b) overrides definition env (layer 2b), so
|
|
80
|
+
`MARTHA_PROFILE=<its own profile>` in Edit Agent gives each Buzz agent its own
|
|
81
|
+
Martha agent — still one variable, still no secrets. `install-harness` now says
|
|
82
|
+
this after a successful write, `--use-profile --help` says it, and the guide has
|
|
83
|
+
it as a warning box.
|
|
84
|
+
|
|
85
|
+
## [0.55.1] — 2026-08-03
|
|
86
|
+
|
|
87
|
+
### Fixed — `definitions apply` no longer reports success on writes that don't land (#1147)
|
|
88
|
+
|
|
89
|
+
`kind: Agent` payloads are validated at load time against the server's
|
|
90
|
+
persisted-field set: a wrongly-nested `definition:` wrapper or any unknown
|
|
91
|
+
top-level field now fails with a validation error naming the field and the
|
|
92
|
+
expected level — instead of `updated → vN` while the server silently
|
|
93
|
+
discards the value. `--dry-run` surfaces the same rejection. And applying a
|
|
94
|
+
definition against a soft-deleted target now reactivates it (`is_active:
|
|
95
|
+
true`) instead of reporting `1 unchanged`, unless the local file explicitly
|
|
96
|
+
declares `is_active: false`.
|
|
97
|
+
|
|
98
|
+
## [0.55.0] — 2026-08-03
|
|
99
|
+
|
|
100
|
+
### Added — a profile can carry the agent's credential, so nothing is typed by hand
|
|
101
|
+
|
|
102
|
+
Setting up a Martha agent in Buzz Desktop meant pasting `MARTHA_CLIENT_ID` and
|
|
103
|
+
`MARTHA_CLIENT_SECRET` into Edit Agent, while Goose and Claude Code need
|
|
104
|
+
nothing there — because their own CLI already wrote a config file on that
|
|
105
|
+
machine. Martha's CLI has that file too; it just couldn't hold an agent
|
|
106
|
+
credential, only tokens. That was the whole difference, and it was ours to fix.
|
|
107
|
+
|
|
108
|
+
- `martha principals bootstrap --mode agent --save-profile <name>` writes the
|
|
109
|
+
minted credential straight into a profile: server, realm and client in
|
|
110
|
+
`config.yaml`, the secret in `credentials.json` (0600). It prints
|
|
111
|
+
**"nothing to copy"** instead of a secret to move around by hand.
|
|
112
|
+
- The credential chain uses it: with no `MARTHA_CLIENT_ID`/`SECRET` in the
|
|
113
|
+
environment, the profile's own service account authenticates. Env still wins,
|
|
114
|
+
so a one-off override needs no config change, and the same-client guard from
|
|
115
|
+
0.53.0 still refuses to hand a service-account caller a human token.
|
|
116
|
+
- `martha channels install-harness buzz --use-profile <name>` bakes
|
|
117
|
+
`MARTHA_PROFILE=<name>` into the harness definition — a profile *name* is not
|
|
118
|
+
a secret — so **Edit Agent needs nothing at all**. (`--use-profile`, because
|
|
119
|
+
the global `--profile` selects which profile the command itself talks to.)
|
|
120
|
+
- `martha channels connect --profile <name>` passes it to the adapter it
|
|
121
|
+
spawns, which is a separate process and would otherwise fall back to the
|
|
122
|
+
machine's default profile.
|
|
123
|
+
- The preflight's "durable credential" check counts a profile-held credential
|
|
124
|
+
as one, and names it.
|
|
125
|
+
|
|
5
126
|
## [0.54.0] — 2026-08-03
|
|
6
127
|
|
|
7
128
|
### Added — `channels` uses the Buzz binaries Desktop already ships
|
package/dist/index.js
CHANGED
|
@@ -9271,6 +9271,23 @@ class TokenStore {
|
|
|
9271
9271
|
delete creds.profiles[profile];
|
|
9272
9272
|
this.writeFile(creds);
|
|
9273
9273
|
}
|
|
9274
|
+
getServiceAccount(profile) {
|
|
9275
|
+
return this.readFile().service_accounts?.[profile] ?? null;
|
|
9276
|
+
}
|
|
9277
|
+
saveServiceAccount(profile, sa) {
|
|
9278
|
+
const creds = this.readFile();
|
|
9279
|
+
creds.service_accounts = {
|
|
9280
|
+
...creds.service_accounts ?? {},
|
|
9281
|
+
[profile]: sa
|
|
9282
|
+
};
|
|
9283
|
+
this.writeFile(creds);
|
|
9284
|
+
}
|
|
9285
|
+
clearServiceAccount(profile) {
|
|
9286
|
+
const creds = this.readFile();
|
|
9287
|
+
if (creds.service_accounts)
|
|
9288
|
+
delete creds.service_accounts[profile];
|
|
9289
|
+
this.writeFile(creds);
|
|
9290
|
+
}
|
|
9274
9291
|
isExpired(profile) {
|
|
9275
9292
|
const tokens = this.getTokens(profile);
|
|
9276
9293
|
if (!tokens)
|
|
@@ -9306,7 +9323,9 @@ class TokenStore {
|
|
|
9306
9323
|
if (!fs2.existsSync(dir)) {
|
|
9307
9324
|
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
9308
9325
|
}
|
|
9309
|
-
fs2.writeFileSync(this.credentialsPath, JSON.stringify(creds, null, 2), {
|
|
9326
|
+
fs2.writeFileSync(this.credentialsPath, JSON.stringify(creds, null, 2), {
|
|
9327
|
+
mode: 384
|
|
9328
|
+
});
|
|
9310
9329
|
}
|
|
9311
9330
|
}
|
|
9312
9331
|
function decodeJwtPayload(token) {
|
|
@@ -10746,12 +10765,13 @@ function createContext(opts) {
|
|
|
10746
10765
|
const envToken = process.env.MARTHA_TOKEN;
|
|
10747
10766
|
if (envToken)
|
|
10748
10767
|
return envToken;
|
|
10749
|
-
const
|
|
10750
|
-
const
|
|
10751
|
-
|
|
10768
|
+
const stored = tokenStore.getServiceAccount(profileName);
|
|
10769
|
+
const clientId = process.env.MARTHA_CLIENT_ID ?? stored?.client_id;
|
|
10770
|
+
const clientSecret = process.env.MARTHA_CLIENT_SECRET ?? stored?.client_secret;
|
|
10771
|
+
if (clientId && clientSecret) {
|
|
10752
10772
|
if (!tokenStore.needsRefresh(profileName)) {
|
|
10753
10773
|
const existing = tokenStore.getTokens(profileName);
|
|
10754
|
-
if (existing && belongsToClient(existing,
|
|
10774
|
+
if (existing && belongsToClient(existing, clientId)) {
|
|
10755
10775
|
return existing.access_token;
|
|
10756
10776
|
}
|
|
10757
10777
|
}
|
|
@@ -10759,8 +10779,8 @@ function createContext(opts) {
|
|
|
10759
10779
|
const tokens2 = await loginClientCredentials2({
|
|
10760
10780
|
keycloakUrl: profile.keycloak_url,
|
|
10761
10781
|
realm: profile.keycloak_realm,
|
|
10762
|
-
clientId
|
|
10763
|
-
clientSecret
|
|
10782
|
+
clientId,
|
|
10783
|
+
clientSecret
|
|
10764
10784
|
});
|
|
10765
10785
|
tokenStore.saveTokens(profileName, tokens2);
|
|
10766
10786
|
return tokens2.access_token;
|
|
@@ -10772,12 +10792,12 @@ function createContext(opts) {
|
|
|
10772
10792
|
}
|
|
10773
10793
|
const tokens = tokenStore.getTokens(profileName);
|
|
10774
10794
|
if (tokens?.refresh_token && !tokenStore.isRefreshExpired(profileName)) {
|
|
10775
|
-
const
|
|
10795
|
+
const clientId2 = tokens.auth_type === "client_credentials" ? profile.client_id : "martha-cli";
|
|
10776
10796
|
const refreshed = await refreshAccessToken({
|
|
10777
10797
|
keycloakUrl: profile.keycloak_url,
|
|
10778
10798
|
realm: profile.keycloak_realm,
|
|
10779
10799
|
refreshToken: tokens.refresh_token,
|
|
10780
|
-
clientId
|
|
10800
|
+
clientId: clientId2
|
|
10781
10801
|
});
|
|
10782
10802
|
tokenStore.saveTokens(profileName, refreshed);
|
|
10783
10803
|
return refreshed.access_token;
|
|
@@ -11129,7 +11149,7 @@ init_config();
|
|
|
11129
11149
|
init_errors();
|
|
11130
11150
|
|
|
11131
11151
|
// src/version.ts
|
|
11132
|
-
var CLI_VERSION = "0.
|
|
11152
|
+
var CLI_VERSION = "0.57.0";
|
|
11133
11153
|
|
|
11134
11154
|
// src/commands/sessions.ts
|
|
11135
11155
|
function relativeTime(iso) {
|
|
@@ -14558,67 +14578,72 @@ function registerAcpServeCommand(program2) {
|
|
|
14558
14578
|
|
|
14559
14579
|
// src/commands/principals.ts
|
|
14560
14580
|
init_errors();
|
|
14581
|
+
init_config();
|
|
14561
14582
|
var AGENTS_PATH = "/api/admin/definitions/agents";
|
|
14562
14583
|
var BOOTSTRAP_PATH = "/api/admin/definitions/principals/bootstrap";
|
|
14563
14584
|
function parseIntents(raw) {
|
|
14564
14585
|
return (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
14565
14586
|
}
|
|
14587
|
+
var CHANNEL_AGENT_PROMPT_SUFFIX = `
|
|
14588
|
+
|
|
14589
|
+
TOOLS
|
|
14590
|
+
You have no shell and no terminal. Your tools are function calls, and they ` + "are the only way you can learn anything. Never write `<bash>`, shell commands, or code " + "fences pretending to run one, and never write output you did not receive from a tool. " + "If a tool returns nothing, say it returned nothing." + `
|
|
14591
|
+
|
|
14592
|
+
DELIVERY
|
|
14593
|
+
Reply with the message content ONLY — delivery is handled for you. Never ` + "output delivery commands (e.g. `buzz messages send`).";
|
|
14566
14594
|
async function bootstrapAgentPrincipal(ctx, agent, opts = {}) {
|
|
14567
14595
|
const A = `${AGENTS_PATH}/${encodeURIComponent(agent)}`;
|
|
14568
14596
|
const steps = [];
|
|
14569
14597
|
const warnings = [];
|
|
14570
|
-
let
|
|
14598
|
+
let existing = null;
|
|
14571
14599
|
try {
|
|
14572
|
-
|
|
14600
|
+
existing = await ctx.api.get(A);
|
|
14573
14601
|
} catch (e) {
|
|
14574
14602
|
if (!(e instanceof MarthaAPIError && e.status === 404))
|
|
14575
14603
|
throw e;
|
|
14576
14604
|
}
|
|
14577
|
-
if (
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
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
|
-
}
|
|
14605
|
+
if (existing && String(existing.agent_type ?? "") === "external") {
|
|
14606
|
+
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 */);
|
|
14607
|
+
}
|
|
14608
|
+
if (!existing && !opts.prompt) {
|
|
14609
|
+
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 "..."`);
|
|
14600
14610
|
}
|
|
14601
|
-
const agentName = String(def?.name ?? agent);
|
|
14602
14611
|
const boot = await ctx.api.post(BOOTSTRAP_PATH, {
|
|
14603
14612
|
mode: "integration",
|
|
14604
|
-
label: opts.label ??
|
|
14605
|
-
intents: parseIntents(opts.intents)
|
|
14613
|
+
label: opts.label ?? agent,
|
|
14614
|
+
intents: parseIntents(opts.intents),
|
|
14615
|
+
agent: true,
|
|
14616
|
+
agent_name: agent,
|
|
14617
|
+
agent_system_prompt: opts.prompt ? opts.prompt + CHANNEL_AGENT_PROMPT_SUFFIX : undefined,
|
|
14618
|
+
collections: parseIntents(opts.collections)
|
|
14606
14619
|
});
|
|
14607
14620
|
if (!boot.service_account || boot.client_id == null) {
|
|
14608
14621
|
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
14622
|
}
|
|
14623
|
+
const agentName = boot.agent?.name ?? agent;
|
|
14610
14624
|
steps.push(boot.adopted ? `reusing principal '${boot.service_account.client_id}' (same secret — recovery, not rotation)` : `provisioned principal '${boot.service_account.client_id}'`);
|
|
14611
|
-
|
|
14612
|
-
const
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14625
|
+
steps.push(boot.agent?.adopted ? `bound existing agent '${agentName}' to client ${boot.client_id}` : `created agent '${agentName}' (runs_as_chat) and bound it to client ${boot.client_id}`);
|
|
14626
|
+
const fns = boot.agent?.granted_functions ?? [];
|
|
14627
|
+
steps.push(fns.length ? `granted its tools: ${fns.join(", ")}` : "granted NO tools — pass --intents rag (or documents/tasks) or it can do nothing");
|
|
14628
|
+
const cols = boot.agent?.granted_collections ?? [];
|
|
14629
|
+
steps.push(cols.length ? `granted it read on: ${cols.join(", ")}` : "granted NO collections — its retrieval tools will return nothing until you pass --collections");
|
|
14630
|
+
if (!fns.length) {
|
|
14631
|
+
warnings.push("no tools: this agent will answer from the model alone.");
|
|
14618
14632
|
}
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14633
|
+
if (fns.length && !cols.length) {
|
|
14634
|
+
warnings.push(`it can call list_collections and will get an empty list — grant one: martha collections grant <name> --to agent:${boot.agent?.agent_id ?? "<id>"}`);
|
|
14635
|
+
}
|
|
14636
|
+
for (const s of boot.skipped ?? []) {
|
|
14637
|
+
warnings.push(`skipped ${s.name}: ${s.reason}`);
|
|
14638
|
+
}
|
|
14639
|
+
try {
|
|
14640
|
+
const bound = await ctx.api.get(`/api/admin/definitions/clients/${boot.client_id}/agents`);
|
|
14641
|
+
const others = (Array.isArray(bound) ? bound : []).map((g) => String(g.name ?? g.agent_name ?? "")).filter((n) => n && n !== agentName);
|
|
14642
|
+
if (others.length) {
|
|
14643
|
+
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.`);
|
|
14644
|
+
}
|
|
14645
|
+
} catch {
|
|
14646
|
+
warnings.push(`could not read back client ${boot.client_id}'s agents — verify with: martha agents doctor ${agentName}`);
|
|
14622
14647
|
}
|
|
14623
14648
|
return {
|
|
14624
14649
|
agent: agentName,
|
|
@@ -14631,7 +14656,25 @@ When a message asks you to reply in a channel, reply with the ` + "message conte
|
|
|
14631
14656
|
warnings
|
|
14632
14657
|
};
|
|
14633
14658
|
}
|
|
14634
|
-
function
|
|
14659
|
+
function saveAgentProfile(ctx, name, r) {
|
|
14660
|
+
const config = loadConfig(ctx.configDir);
|
|
14661
|
+
config.profiles[name] = {
|
|
14662
|
+
api_url: ctx.profile.api_url,
|
|
14663
|
+
keycloak_url: ctx.profile.keycloak_url,
|
|
14664
|
+
keycloak_realm: ctx.profile.keycloak_realm,
|
|
14665
|
+
auth_type: "client_credentials",
|
|
14666
|
+
client_id: r.saClientId,
|
|
14667
|
+
default_agent: r.agent
|
|
14668
|
+
};
|
|
14669
|
+
saveConfig(config, ctx.configDir);
|
|
14670
|
+
ctx.tokenStore.saveServiceAccount(name, {
|
|
14671
|
+
client_id: r.saClientId,
|
|
14672
|
+
client_secret: r.saClientSecret,
|
|
14673
|
+
agent: r.agent
|
|
14674
|
+
});
|
|
14675
|
+
return name;
|
|
14676
|
+
}
|
|
14677
|
+
function printAgentPrincipalReady(r, savedProfile) {
|
|
14635
14678
|
console.log(source_default.bold.green(`
|
|
14636
14679
|
✓ agent principal ready — the credential acts as '${r.agent}'
|
|
14637
14680
|
`));
|
|
@@ -14640,25 +14683,33 @@ function printAgentPrincipalReady(r) {
|
|
|
14640
14683
|
for (const w of r.warnings)
|
|
14641
14684
|
console.log(source_default.yellow(` ! ${w}`));
|
|
14642
14685
|
console.log();
|
|
14643
|
-
|
|
14644
|
-
|
|
14686
|
+
if (savedProfile) {
|
|
14687
|
+
console.log(source_default.bold(`Saved as profile '${savedProfile}' — nothing to copy.`));
|
|
14688
|
+
console.log(source_default.dim(` ~/.martha/config.yaml + credentials.json (0600) now carry the server,
|
|
14689
|
+
` + ` the realm and this credential. Anything that should act as '${r.agent}'
|
|
14690
|
+
` + ` needs one variable:`));
|
|
14691
|
+
console.log(source_default.cyan(` MARTHA_PROFILE=${savedProfile}`));
|
|
14692
|
+
} else {
|
|
14693
|
+
console.log(source_default.bold("Service-account secret (shown once — store it like one):"));
|
|
14694
|
+
console.log(source_default.cyan(` export MARTHA_CLIENT_ID=${r.saClientId}
|
|
14645
14695
|
` + ` export MARTHA_CLIENT_SECRET=${r.saClientSecret}`));
|
|
14646
|
-
|
|
14696
|
+
console.log(source_default.dim(" The host also needs a Martha profile (api_url + keycloak): run `martha init` once,\n" + " or re-run this with --save-profile <name> to keep both in one place."));
|
|
14697
|
+
}
|
|
14647
14698
|
console.log();
|
|
14648
14699
|
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
14700
|
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>`));
|
|
14701
|
+
` + ` martha channels connect buzz --agent ${r.agent} --relay ws://<relay>:<port>` + (savedProfile ? ` --profile ${savedProfile}` : "")));
|
|
14651
14702
|
console.log(source_default.dim(`
|
|
14652
14703
|
It names anything missing (buzz-acp, the buzz CLI, membership, …) and
|
|
14653
14704
|
` + ` exits rather than half-starting. Starting from Buzz Desktop instead?
|
|
14654
|
-
` +
|
|
14705
|
+
` + ` \`martha channels install-harness buzz${savedProfile ? ` --profile ${savedProfile}` : ""}\`, then pick Martha in New agent.`));
|
|
14655
14706
|
console.log(source_default.dim(`
|
|
14656
14707
|
Then: buzz channels add-member --channel <id> --pubkey <agent pubkey> --role member
|
|
14657
14708
|
` + " and @mention the agent in the channel."));
|
|
14658
14709
|
}
|
|
14659
14710
|
function registerPrincipalCommands(program2) {
|
|
14660
14711
|
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) => {
|
|
14712
|
+
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)").option("--collections <list>", "Comma-separated collections (name, slug or id) the agent may read. Intents grant the retrieval TOOLS; without this they return nothing").option("--save-profile <name>", "Save the minted credential as a Martha profile, so anything that runs as this agent needs only MARTHA_PROFILE=<name> — no secret to copy").action(async (opts) => {
|
|
14662
14713
|
const ctx = createContext({
|
|
14663
14714
|
profileOverride: program2.opts().profile,
|
|
14664
14715
|
apiUrlOverride: program2.opts().apiUrl,
|
|
@@ -14673,8 +14724,10 @@ function registerPrincipalCommands(program2) {
|
|
|
14673
14724
|
const result = await bootstrapAgentPrincipal(ctx, opts.agent, {
|
|
14674
14725
|
label: opts.label,
|
|
14675
14726
|
prompt: opts.prompt,
|
|
14676
|
-
intents: opts.intents
|
|
14727
|
+
intents: opts.intents,
|
|
14728
|
+
collections: opts.collections
|
|
14677
14729
|
});
|
|
14730
|
+
const savedProfile = opts.saveProfile ? saveAgentProfile(ctx, opts.saveProfile, result) : null;
|
|
14678
14731
|
if (isJson) {
|
|
14679
14732
|
console.log(JSON.stringify({
|
|
14680
14733
|
mode: "agent",
|
|
@@ -14683,13 +14736,14 @@ function registerPrincipalCommands(program2) {
|
|
|
14683
14736
|
client_key: result.clientKey,
|
|
14684
14737
|
martha_client_id: result.saClientId,
|
|
14685
14738
|
martha_client_secret: result.saClientSecret,
|
|
14739
|
+
saved_profile: savedProfile,
|
|
14686
14740
|
token_endpoint: result.tokenEndpoint,
|
|
14687
14741
|
steps: result.steps,
|
|
14688
14742
|
warnings: result.warnings
|
|
14689
14743
|
}, null, 2));
|
|
14690
14744
|
return;
|
|
14691
14745
|
}
|
|
14692
|
-
printAgentPrincipalReady(result);
|
|
14746
|
+
printAgentPrincipalReady(result, savedProfile);
|
|
14693
14747
|
break;
|
|
14694
14748
|
}
|
|
14695
14749
|
case "integration":
|
|
@@ -14744,7 +14798,12 @@ function buzzHarnessDefinition(opts = {}) {
|
|
|
14744
14798
|
label: opts.label ?? "Martha",
|
|
14745
14799
|
command: opts.command ?? "martha",
|
|
14746
14800
|
args: ["acp-serve", "--post-reply"],
|
|
14747
|
-
env: {
|
|
14801
|
+
env: {
|
|
14802
|
+
BUZZ_ACP_MULTIPLE_EVENT_HANDLING: "queue",
|
|
14803
|
+
BUZZ_ACP_SUBSCRIBE: "all",
|
|
14804
|
+
BUZZ_ACP_KINDS: DEFAULT_BUZZ_KINDS,
|
|
14805
|
+
...opts.profile ? { MARTHA_PROFILE: opts.profile } : {}
|
|
14806
|
+
},
|
|
14748
14807
|
installInstructionsUrl: "https://docs.martha.nomadriver.co/cli/buzz",
|
|
14749
14808
|
installHint: "npm i -g @aiaiai-pt/martha-cli"
|
|
14750
14809
|
};
|
|
@@ -14834,7 +14893,8 @@ function buzzAcpEnv(opts) {
|
|
|
14834
14893
|
BUZZ_ACP_AGENT_ARGS: "acp-serve,--post-reply",
|
|
14835
14894
|
BUZZ_ACP_SUBSCRIBE: opts.subscribe,
|
|
14836
14895
|
BUZZ_ACP_MULTIPLE_EVENT_HANDLING: opts.multipleEventHandling,
|
|
14837
|
-
BUZZ_ACP_RESPOND_TO: opts.respondTo
|
|
14896
|
+
BUZZ_ACP_RESPOND_TO: opts.respondTo,
|
|
14897
|
+
...opts.profile ? { MARTHA_PROFILE: opts.profile } : {}
|
|
14838
14898
|
};
|
|
14839
14899
|
if (opts.subscribe === "all")
|
|
14840
14900
|
env2.BUZZ_ACP_KINDS = opts.kinds;
|
|
@@ -14992,11 +15052,12 @@ async function preflightBuzz(ctx, opts) {
|
|
|
14992
15052
|
checks.push(await checkAgentPrincipal(ctx, opts.agent, def));
|
|
14993
15053
|
const hasSa = !!(process.env.MARTHA_CLIENT_ID && process.env.MARTHA_CLIENT_SECRET);
|
|
14994
15054
|
const hasToken = !!process.env.MARTHA_TOKEN;
|
|
15055
|
+
const profileSa = ctx.tokenStore.getServiceAccount(ctx.profileName);
|
|
14995
15056
|
checks.push({
|
|
14996
|
-
ok: hasSa || hasToken ? true : null,
|
|
14997
|
-
label: "durable credential
|
|
14998
|
-
detail: hasSa ? `MARTHA_CLIENT_ID=${process.env.MARTHA_CLIENT_ID}` : hasToken ? "MARTHA_TOKEN" : "none — the adapter
|
|
14999
|
-
fix: hasSa || hasToken ? undefined : `mint one: martha principals bootstrap --mode agent --agent ${opts.agent}
|
|
15057
|
+
ok: hasSa || hasToken || profileSa ? true : null,
|
|
15058
|
+
label: "durable credential",
|
|
15059
|
+
detail: hasSa ? `MARTHA_CLIENT_ID=${process.env.MARTHA_CLIENT_ID} (env)` : hasToken ? "MARTHA_TOKEN (env)" : profileSa ? `profile '${ctx.profileName}' carries ${profileSa.client_id}${profileSa.agent ? ` (acts as ${profileSa.agent})` : ""}` : "none — the adapter would fall back to whatever token this profile has, which expires",
|
|
15060
|
+
fix: hasSa || hasToken || profileSa ? undefined : `mint one that lives in a profile: martha principals bootstrap --mode agent --agent ${opts.agent} --save-profile <name>`
|
|
15000
15061
|
});
|
|
15001
15062
|
const relayProbe = await reachable(relayHttpUrl(opts.relayUrl));
|
|
15002
15063
|
checks.push({
|
|
@@ -15163,12 +15224,13 @@ function assertSupportedChannel(kind) {
|
|
|
15163
15224
|
}
|
|
15164
15225
|
function registerChannelCommands(program2) {
|
|
15165
15226
|
const channels = program2.command("channels").description("Put a Martha agent in an external conversation channel — from Buzz (install-harness) or from Martha (connect)");
|
|
15166
|
-
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) => {
|
|
15227
|
+
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("--use-profile <name>", "Bake MARTHA_PROFILE into the definition as the DEFAULT for every agent on this runtime, so Edit Agent needs nothing. A per-agent MARTHA_PROFILE in Edit Agent overrides it — that is how two Buzz agents become two different Martha agents. Named --use-profile because the global --profile picks which profile this COMMAND talks to.").option("--print", "Print the definition instead of writing it").option("--force", "Overwrite an existing definition").action(async (type, opts) => {
|
|
15167
15228
|
assertSupportedChannel(type);
|
|
15168
15229
|
const def = buzzHarnessDefinition({
|
|
15169
15230
|
id: opts.id,
|
|
15170
15231
|
label: opts.label,
|
|
15171
|
-
command: opts.command
|
|
15232
|
+
command: opts.command,
|
|
15233
|
+
profile: opts.useProfile
|
|
15172
15234
|
});
|
|
15173
15235
|
if (!isValidHarnessId(def.id)) {
|
|
15174
15236
|
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.");
|
|
@@ -15236,7 +15298,8 @@ function registerChannelCommands(program2) {
|
|
|
15236
15298
|
kinds: opts.kinds,
|
|
15237
15299
|
multipleEventHandling: "queue",
|
|
15238
15300
|
respondTo: opts.respondTo,
|
|
15239
|
-
marthaBin: opts.marthaBin
|
|
15301
|
+
marthaBin: opts.marthaBin,
|
|
15302
|
+
profile: program2.opts().profile
|
|
15240
15303
|
});
|
|
15241
15304
|
if (opts.emit) {
|
|
15242
15305
|
if (!["docker", "compose", "k8s"].includes(opts.emit)) {
|
|
@@ -15319,18 +15382,37 @@ function reportPreflight(type, agent, checks, stream = "stdout") {
|
|
|
15319
15382
|
}
|
|
15320
15383
|
}
|
|
15321
15384
|
function installHarnessNotes(def) {
|
|
15385
|
+
const profile = def.env.MARTHA_PROFILE;
|
|
15322
15386
|
return [
|
|
15323
15387
|
"In Buzz Desktop:",
|
|
15324
15388
|
source_default.cyan(` 1. New agent → runtime "${def.label}"`),
|
|
15325
|
-
|
|
15326
|
-
|
|
15389
|
+
...profile ? [
|
|
15390
|
+
source_default.cyan(" 2. …that's it."),
|
|
15391
|
+
source_default.dim(` The definition names profile '${profile}'; the credential behind it
|
|
15392
|
+
` + ` stays in this machine's credentials file. Nothing to type in Buzz.`),
|
|
15393
|
+
"",
|
|
15394
|
+
source_default.dim(`A second Martha agent? '${profile}' is the DEFAULT for every agent on
|
|
15395
|
+
` + `this runtime — the definition is shared. Give the next one its own
|
|
15396
|
+
` + `Martha agent by setting ONE env var on it in Edit Agent, which wins
|
|
15397
|
+
` + `over the definition:`),
|
|
15398
|
+
source_default.cyan(` MARTHA_PROFILE=<its own profile>`),
|
|
15399
|
+
source_default.dim(` (mint that profile with: martha principals bootstrap --mode agent
|
|
15400
|
+
` + " --agent <name> --save-profile <name>)")
|
|
15401
|
+
] : [
|
|
15402
|
+
source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
|
|
15403
|
+
source_default.dim(" (a definition file is shared config, not a secret store)"),
|
|
15404
|
+
source_default.dim(" One variable instead of two secrets: mint with `martha principals\n" + " bootstrap --mode agent --agent <n> --save-profile <name>`, then set\n" + ` MARTHA_PROFILE=<name> here — per agent, so each Buzz agent can be a
|
|
15405
|
+
` + ` different Martha agent. Re-run this with --use-profile <name> to make
|
|
15406
|
+
` + " one of them the default for the runtime.")
|
|
15407
|
+
],
|
|
15327
15408
|
"",
|
|
15328
15409
|
source_default.dim(`Desktop owns the rest: the Nostr keypair in your keyring, the NIP-OA
|
|
15329
15410
|
` + "attestation, spawning buzz-acp, and attaching the agent to channels."),
|
|
15330
15411
|
"",
|
|
15331
|
-
source_default.dim(`
|
|
15332
|
-
` + `
|
|
15333
|
-
` +
|
|
15412
|
+
source_default.dim(`Every agent on this runtime follows the conversation: subscribe=all with
|
|
15413
|
+
` + `kinds ${DEFAULT_BUZZ_KINDS} pinned, so DMs, thread replies and follow-ups
|
|
15414
|
+
` + `reach it without being re-@mentioned. Want one that only answers when
|
|
15415
|
+
` + `named? Set BUZZ_ACP_SUBSCRIBE=mentions on that agent in Edit Agent.`)
|
|
15334
15416
|
];
|
|
15335
15417
|
}
|
|
15336
15418
|
|
|
@@ -15932,6 +16014,37 @@ var SERVER_GENERATED_FIELDS = new Set([
|
|
|
15932
16014
|
"tenant_id"
|
|
15933
16015
|
]);
|
|
15934
16016
|
var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
|
|
16017
|
+
var AGENT_WRITE_FIELDS = new Set([
|
|
16018
|
+
"name",
|
|
16019
|
+
"display_name",
|
|
16020
|
+
"description",
|
|
16021
|
+
"agent_type",
|
|
16022
|
+
"system_prompt",
|
|
16023
|
+
"llm_config",
|
|
16024
|
+
"loop_config",
|
|
16025
|
+
"runs_as_chat",
|
|
16026
|
+
"agent_metadata",
|
|
16027
|
+
"auth_method",
|
|
16028
|
+
"webhook_url",
|
|
16029
|
+
"webhook_secret",
|
|
16030
|
+
"webhook_timeout_s",
|
|
16031
|
+
"is_active",
|
|
16032
|
+
"self_grant_enabled",
|
|
16033
|
+
"self_grant_allowlist",
|
|
16034
|
+
"max_self_grant_scope",
|
|
16035
|
+
"self_grant_max_pending",
|
|
16036
|
+
"self_grant_collection_roots",
|
|
16037
|
+
...AGENT_MANAGED_FIELDS
|
|
16038
|
+
]);
|
|
16039
|
+
function validateAgentWriteBody(body, source) {
|
|
16040
|
+
if ("definition" in body) {
|
|
16041
|
+
throw new CLIError(`'definition:' wrapper in ${source} is not supported for kind: Agent — ` + `Agent fields live at the top level. Move the nested keys ` + `(${Object.keys(body.definition ?? {}).join(", ") || "…"}) ` + `up one level.`, 4 /* Validation */);
|
|
16042
|
+
}
|
|
16043
|
+
const unknown = Object.keys(body).filter((k) => !AGENT_WRITE_FIELDS.has(k));
|
|
16044
|
+
if (unknown.length > 0) {
|
|
16045
|
+
throw new CLIError(`Unknown Agent field${unknown.length > 1 ? "s" : ""} in ${source}: ` + `${unknown.map((k) => `'${k}'`).join(", ")} — the server would ` + `silently discard ${unknown.length > 1 ? "them" : "it"}. ` + `Supported top-level fields: ${[...AGENT_WRITE_FIELDS].sort().join(", ")}.`, 4 /* Validation */);
|
|
16046
|
+
}
|
|
16047
|
+
}
|
|
15935
16048
|
function loadDefinitions(inputPath) {
|
|
15936
16049
|
const resolved = path8.resolve(inputPath);
|
|
15937
16050
|
if (!fs8.existsSync(resolved)) {
|
|
@@ -16020,6 +16133,9 @@ function toLocalDefinition(raw, source) {
|
|
|
16020
16133
|
}
|
|
16021
16134
|
const typedKind = kind;
|
|
16022
16135
|
const normalizedBody = typedKind === "Function" || typedKind === "Workflow" ? normalizeDefinitionWriteBody(typedKind, body) : body;
|
|
16136
|
+
if (typedKind === "Agent") {
|
|
16137
|
+
validateAgentWriteBody(normalizedBody, source);
|
|
16138
|
+
}
|
|
16023
16139
|
return { kind: typedKind, name, body: normalizedBody, source };
|
|
16024
16140
|
}
|
|
16025
16141
|
async function computeOperations(defs, api) {
|
|
@@ -16046,6 +16162,10 @@ async function computeOperations(defs, api) {
|
|
|
16046
16162
|
continue;
|
|
16047
16163
|
}
|
|
16048
16164
|
const changedFields = diffFields(def.body, remote);
|
|
16165
|
+
const needsReactivation = remote.is_active === false && def.body.is_active !== false;
|
|
16166
|
+
if (needsReactivation && !changedFields.includes("is_active")) {
|
|
16167
|
+
changedFields.push("is_active");
|
|
16168
|
+
}
|
|
16049
16169
|
if (changedFields.length === 0) {
|
|
16050
16170
|
ops.push({
|
|
16051
16171
|
kind: def.kind,
|
|
@@ -16059,7 +16179,8 @@ async function computeOperations(defs, api) {
|
|
|
16059
16179
|
name: def.name,
|
|
16060
16180
|
action: "update",
|
|
16061
16181
|
changedFields,
|
|
16062
|
-
source: def.source
|
|
16182
|
+
source: def.source,
|
|
16183
|
+
reactivate: needsReactivation || undefined
|
|
16063
16184
|
});
|
|
16064
16185
|
}
|
|
16065
16186
|
}
|
|
@@ -16140,7 +16261,12 @@ function renderPlan(ops) {
|
|
|
16140
16261
|
grouped.get(op.kind).push(op);
|
|
16141
16262
|
}
|
|
16142
16263
|
const nameW = Math.max(16, ...ops.map((o) => o.name.length)) + 2;
|
|
16143
|
-
for (const kind of [
|
|
16264
|
+
for (const kind of [
|
|
16265
|
+
"Function",
|
|
16266
|
+
"Workflow",
|
|
16267
|
+
"Agent",
|
|
16268
|
+
"Trigger"
|
|
16269
|
+
]) {
|
|
16144
16270
|
const kindOps = grouped.get(kind);
|
|
16145
16271
|
if (!kindOps)
|
|
16146
16272
|
continue;
|
|
@@ -16221,6 +16347,9 @@ async function executeApply(api, ops, defs, warnings) {
|
|
|
16221
16347
|
port_warnings: portWarnings.length > 0 ? portWarnings : undefined
|
|
16222
16348
|
});
|
|
16223
16349
|
} else {
|
|
16350
|
+
if (op.reactivate) {
|
|
16351
|
+
body.is_active = true;
|
|
16352
|
+
}
|
|
16224
16353
|
const res = await api.put(`${apiPath}/${encodeURIComponent(op.name)}`, body);
|
|
16225
16354
|
if (op.kind === "Function" || op.kind === "Workflow") {
|
|
16226
16355
|
verifyDefinitionUpdateResponse(op.kind, body, res);
|