@aiaiai-pt/martha-cli 0.55.0 → 0.58.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 +108 -0
- package/README.md +21 -0
- package/dist/index.js +122 -51
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,114 @@
|
|
|
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.58.0] — 2026-08-04
|
|
6
|
+
|
|
7
|
+
### Fixed — the harness pointed strangers at a 404
|
|
8
|
+
|
|
9
|
+
Buzz shows a runtime's `installInstructionsUrl` exactly when its binary is not
|
|
10
|
+
on PATH — to someone who has just picked Martha and has nothing installed. Ours
|
|
11
|
+
pointed at `docs.martha.nomadriver.co/cli/buzz`, which has never existed. It now
|
|
12
|
+
points at **`/buzz`**, a page written for that arrival, and `installHint` is the
|
|
13
|
+
`npx` one-liner rather than `npm i -g`: a hint that requires a prior install is
|
|
14
|
+
no hint for someone who has installed nothing.
|
|
15
|
+
|
|
16
|
+
`--print` now writes a short header to **stderr** saying what the JSON is and
|
|
17
|
+
where it goes, so `--print > martha.json` still produces a clean file while a
|
|
18
|
+
paste into a Settings form or a gist carries its own context.
|
|
19
|
+
|
|
20
|
+
## [0.57.0] — 2026-08-03
|
|
21
|
+
|
|
22
|
+
### Fixed — `bootstrap --mode agent` produced an agent that could not do anything
|
|
23
|
+
|
|
24
|
+
Provisioned from scratch, the agent came back `✓ ready` and then, asked a
|
|
25
|
+
question in a channel, invented shell commands and fabricated four collections
|
|
26
|
+
that do not exist. Three separate holes, all of them ours:
|
|
27
|
+
|
|
28
|
+
- **No tools.** `--intents rag` granted the CLIENT functions and never the
|
|
29
|
+
agent's, because agent mode was composed client-side while the server's own
|
|
30
|
+
`agent` block — adopt-or-create with chat takeover, bind to the principal,
|
|
31
|
+
grant the intent's AGENT functions — was gated to human mode. That gate is
|
|
32
|
+
now open to integration mode (`agent_name` required: a service account is not
|
|
33
|
+
a person, so the name is never defaulted), and the CLI makes **one** call
|
|
34
|
+
instead of re-implementing it.
|
|
35
|
+
- **Nothing to read.** Intents grant retrieval *verbs*; the *nouns* are a
|
|
36
|
+
separate UMS grant. New `--collections <list>` grants the agent read on them
|
|
37
|
+
by name, slug or id — and when it is omitted, provisioning now says
|
|
38
|
+
`granted NO collections — its retrieval tools will return nothing`, because
|
|
39
|
+
"ready" must never mean "ready and blind".
|
|
40
|
+
- **A prompt the harness overrode.** buzz-acp tells agents they can run shell
|
|
41
|
+
commands; our default said only "don't emit `buzz messages send`". The
|
|
42
|
+
prompt for a newly created agent now states it has no shell and must never
|
|
43
|
+
write output it did not receive from a tool.
|
|
44
|
+
|
|
45
|
+
Also: the agent's system prompt applies to sessions created *after* it — an
|
|
46
|
+
adapter holding open sessions keeps the old one until restarted.
|
|
47
|
+
|
|
48
|
+
### Server
|
|
49
|
+
|
|
50
|
+
`POST /principals/bootstrap` accepts `agent` + `agent_name` in integration
|
|
51
|
+
mode, plus `agent_system_prompt` (applied only on create — never overwrites a
|
|
52
|
+
persona someone has edited) and `collections`. The response's `agent` block
|
|
53
|
+
gains `granted_collections`. Collection grants are issued after commit as UMS
|
|
54
|
+
`application` grantees with `agent_<uuid>` ids — UMS has no `agent` grantee
|
|
55
|
+
type and 422s the bare uuid, which is invisible unless you read the skip list.
|
|
56
|
+
|
|
57
|
+
## [0.56.0] — 2026-08-03
|
|
58
|
+
|
|
59
|
+
### Changed — Martha agents follow the conversation by default
|
|
60
|
+
|
|
61
|
+
The harness definition now pins `BUZZ_ACP_SUBSCRIBE=all` with
|
|
62
|
+
`BUZZ_ACP_KINDS=9,46010,40007,7`.
|
|
63
|
+
|
|
64
|
+
Buzz's default is `subscribe=mentions`, which filters every message that does
|
|
65
|
+
not `#p`-tag the agent — so a **DM got no reply**, and neither did a thread
|
|
66
|
+
reply or a follow-up question. The previous release left the pair out of the
|
|
67
|
+
definition on the reasoning that overriding Buzz's default would be
|
|
68
|
+
surprising; the actual result was that every single agent needed both
|
|
69
|
+
variables typed into Edit Agent by hand. Runtime-wide behaviour is exactly
|
|
70
|
+
what a runtime definition is for.
|
|
71
|
+
|
|
72
|
+
The two values are pinned together because `all` without kinds is a wildcard
|
|
73
|
+
subscription — typing indicators and presence updates arrive as prompts. The
|
|
74
|
+
set is buzz-acp's own mentions-mode set (chat, approval, reminder) plus
|
|
75
|
+
reactions, which `acp-serve` classifies as feedback and answers with silence.
|
|
76
|
+
|
|
77
|
+
For an agent that should only speak when named, set
|
|
78
|
+
`BUZZ_ACP_SUBSCRIBE=mentions` on that agent — per-agent env overrides the
|
|
79
|
+
definition. Existing agents pick this up after
|
|
80
|
+
`martha channels install-harness buzz --force` and a restart.
|
|
81
|
+
|
|
82
|
+
## [0.55.2] — 2026-08-03
|
|
83
|
+
|
|
84
|
+
### Fixed — a baked `MARTHA_PROFILE` is a default, and we didn't say so
|
|
85
|
+
|
|
86
|
+
0.55.0 let `install-harness --use-profile` put `MARTHA_PROFILE` in the harness
|
|
87
|
+
definition, and the output read as though that bound the Buzz agent to a Martha
|
|
88
|
+
agent. It doesn't: the definition is shared by **every** agent on the Martha
|
|
89
|
+
runtime, so a second Buzz agent would answer as the first one's Martha agent —
|
|
90
|
+
its own name and Nostr key in front of one brain, which is the opposite of what
|
|
91
|
+
a second agent is for.
|
|
92
|
+
|
|
93
|
+
Nothing to change in the mechanism, which already supports the right model:
|
|
94
|
+
per-agent env (Buzz's Stack A layer 3b) overrides definition env (layer 2b), so
|
|
95
|
+
`MARTHA_PROFILE=<its own profile>` in Edit Agent gives each Buzz agent its own
|
|
96
|
+
Martha agent — still one variable, still no secrets. `install-harness` now says
|
|
97
|
+
this after a successful write, `--use-profile --help` says it, and the guide has
|
|
98
|
+
it as a warning box.
|
|
99
|
+
|
|
100
|
+
## [0.55.1] — 2026-08-03
|
|
101
|
+
|
|
102
|
+
### Fixed — `definitions apply` no longer reports success on writes that don't land (#1147)
|
|
103
|
+
|
|
104
|
+
`kind: Agent` payloads are validated at load time against the server's
|
|
105
|
+
persisted-field set: a wrongly-nested `definition:` wrapper or any unknown
|
|
106
|
+
top-level field now fails with a validation error naming the field and the
|
|
107
|
+
expected level — instead of `updated → vN` while the server silently
|
|
108
|
+
discards the value. `--dry-run` surfaces the same rejection. And applying a
|
|
109
|
+
definition against a soft-deleted target now reactivates it (`is_active:
|
|
110
|
+
true`) instead of reporting `1 unchanged`, unless the local file explicitly
|
|
111
|
+
declares `is_active: false`.
|
|
112
|
+
|
|
5
113
|
## [0.55.0] — 2026-08-03
|
|
6
114
|
|
|
7
115
|
### Added — a profile can carry the agent's credential, so nothing is typed by hand
|
package/README.md
CHANGED
|
@@ -90,6 +90,27 @@ martha agents list --json | jq '.[] | {name, status}'
|
|
|
90
90
|
|
|
91
91
|
Exit codes: `0` success · `1` generic error · `2` auth failure · `3` not found · `4` validation · `5` conflict (409).
|
|
92
92
|
|
|
93
|
+
## Put an agent in a Buzz channel
|
|
94
|
+
|
|
95
|
+
One command, no install of its own:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx -y @aiaiai-pt/martha-cli channels install-harness buzz
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then in [Buzz](https://github.com/block/buzz) Desktop: **New agent → runtime "Martha"**. The command registers Martha as an ACP runtime by writing a definition into Buzz's own app-data directory — the supported route for any ACP agent, no PR and no plugin.
|
|
102
|
+
|
|
103
|
+
To have an agent for it to be, and leave Buzz's **Edit Agent** empty:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
martha principals bootstrap --mode agent --agent support \
|
|
107
|
+
--prompt "You answer support questions from our handbook." \
|
|
108
|
+
--intents rag --collections "Handbook" --save-profile support
|
|
109
|
+
martha channels install-harness buzz --use-profile support
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The profile *name* goes in the definition; the credential stays in `~/.martha/credentials.json` (0600). Check everything before trusting it with `martha channels preflight buzz --agent support --relay wss://…`, which names each missing precondition and exits non-zero. Full guide: <https://docs.martha.nomadriver.co/buzz>.
|
|
113
|
+
|
|
93
114
|
## Agent integration
|
|
94
115
|
|
|
95
116
|
This package ships an agent-facing skill reference at `skills/martha-cli/SKILL.md` describing every command, JSON contract, and conceptual primitive (tenant, agent, function, task, etc.). Agent harnesses can pull it directly:
|
package/dist/index.js
CHANGED
|
@@ -11149,7 +11149,7 @@ init_config();
|
|
|
11149
11149
|
init_errors();
|
|
11150
11150
|
|
|
11151
11151
|
// src/version.ts
|
|
11152
|
-
var CLI_VERSION = "0.
|
|
11152
|
+
var CLI_VERSION = "0.58.0";
|
|
11153
11153
|
|
|
11154
11154
|
// src/commands/sessions.ts
|
|
11155
11155
|
function relativeTime(iso) {
|
|
@@ -14584,62 +14584,66 @@ var BOOTSTRAP_PATH = "/api/admin/definitions/principals/bootstrap";
|
|
|
14584
14584
|
function parseIntents(raw) {
|
|
14585
14585
|
return (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
14586
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`).";
|
|
14587
14594
|
async function bootstrapAgentPrincipal(ctx, agent, opts = {}) {
|
|
14588
14595
|
const A = `${AGENTS_PATH}/${encodeURIComponent(agent)}`;
|
|
14589
14596
|
const steps = [];
|
|
14590
14597
|
const warnings = [];
|
|
14591
|
-
let
|
|
14598
|
+
let existing = null;
|
|
14592
14599
|
try {
|
|
14593
|
-
|
|
14600
|
+
existing = await ctx.api.get(A);
|
|
14594
14601
|
} catch (e) {
|
|
14595
14602
|
if (!(e instanceof MarthaAPIError && e.status === 404))
|
|
14596
14603
|
throw e;
|
|
14597
14604
|
}
|
|
14598
|
-
if (
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
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`).";
|
|
14605
|
-
def = await ctx.api.post(AGENTS_PATH, {
|
|
14606
|
-
name: agent,
|
|
14607
|
-
system_prompt: opts.prompt + deliveryNote,
|
|
14608
|
-
agent_type: "cloud",
|
|
14609
|
-
runs_as_chat: true,
|
|
14610
|
-
description: "Agent principal for external drivers (#1117)"
|
|
14611
|
-
});
|
|
14612
|
-
steps.push(`created Martha-hosted agent '${agent}' (runs_as_chat)`);
|
|
14613
|
-
} else {
|
|
14614
|
-
if (String(def.agent_type ?? "") === "external") {
|
|
14615
|
-
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 */);
|
|
14616
|
-
}
|
|
14617
|
-
if (def.runs_as_chat !== true) {
|
|
14618
|
-
await ctx.api.put(A, { runs_as_chat: true });
|
|
14619
|
-
steps.push("enabled chat takeover (runs_as_chat)");
|
|
14620
|
-
}
|
|
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 "..."`);
|
|
14621
14610
|
}
|
|
14622
|
-
const agentName = String(def?.name ?? agent);
|
|
14623
14611
|
const boot = await ctx.api.post(BOOTSTRAP_PATH, {
|
|
14624
14612
|
mode: "integration",
|
|
14625
|
-
label: opts.label ??
|
|
14626
|
-
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)
|
|
14627
14619
|
});
|
|
14628
14620
|
if (!boot.service_account || boot.client_id == null) {
|
|
14629
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).");
|
|
14630
14622
|
}
|
|
14623
|
+
const agentName = boot.agent?.name ?? agent;
|
|
14631
14624
|
steps.push(boot.adopted ? `reusing principal '${boot.service_account.client_id}' (same secret — recovery, not rotation)` : `provisioned principal '${boot.service_account.client_id}'`);
|
|
14632
|
-
|
|
14633
|
-
const
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
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.");
|
|
14632
|
+
}
|
|
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>"}`);
|
|
14639
14635
|
}
|
|
14640
|
-
const
|
|
14641
|
-
|
|
14642
|
-
|
|
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}`);
|
|
14643
14647
|
}
|
|
14644
14648
|
return {
|
|
14645
14649
|
agent: agentName,
|
|
@@ -14705,7 +14709,7 @@ function printAgentPrincipalReady(r, savedProfile) {
|
|
|
14705
14709
|
}
|
|
14706
14710
|
function registerPrincipalCommands(program2) {
|
|
14707
14711
|
const principals = program2.command("principals").description("Provision principals — who may act, with what credential (mirrors the API's principals/bootstrap)");
|
|
14708
|
-
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("--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) => {
|
|
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) => {
|
|
14709
14713
|
const ctx = createContext({
|
|
14710
14714
|
profileOverride: program2.opts().profile,
|
|
14711
14715
|
apiUrlOverride: program2.opts().apiUrl,
|
|
@@ -14720,7 +14724,8 @@ function registerPrincipalCommands(program2) {
|
|
|
14720
14724
|
const result = await bootstrapAgentPrincipal(ctx, opts.agent, {
|
|
14721
14725
|
label: opts.label,
|
|
14722
14726
|
prompt: opts.prompt,
|
|
14723
|
-
intents: opts.intents
|
|
14727
|
+
intents: opts.intents,
|
|
14728
|
+
collections: opts.collections
|
|
14724
14729
|
});
|
|
14725
14730
|
const savedProfile = opts.saveProfile ? saveAgentProfile(ctx, opts.saveProfile, result) : null;
|
|
14726
14731
|
if (isJson) {
|
|
@@ -14787,6 +14792,7 @@ init_errors();
|
|
|
14787
14792
|
var AGENTS_PATH2 = "/api/admin/definitions/agents";
|
|
14788
14793
|
var SUPPORTED_CHANNELS = ["buzz"];
|
|
14789
14794
|
var BUZZ_HARNESS_ID = "martha";
|
|
14795
|
+
var BUZZ_INSTALL_DOCS_URL = "https://docs.martha.nomadriver.co/buzz";
|
|
14790
14796
|
function buzzHarnessDefinition(opts = {}) {
|
|
14791
14797
|
return {
|
|
14792
14798
|
id: opts.id ?? BUZZ_HARNESS_ID,
|
|
@@ -14795,10 +14801,12 @@ function buzzHarnessDefinition(opts = {}) {
|
|
|
14795
14801
|
args: ["acp-serve", "--post-reply"],
|
|
14796
14802
|
env: {
|
|
14797
14803
|
BUZZ_ACP_MULTIPLE_EVENT_HANDLING: "queue",
|
|
14804
|
+
BUZZ_ACP_SUBSCRIBE: "all",
|
|
14805
|
+
BUZZ_ACP_KINDS: DEFAULT_BUZZ_KINDS,
|
|
14798
14806
|
...opts.profile ? { MARTHA_PROFILE: opts.profile } : {}
|
|
14799
14807
|
},
|
|
14800
|
-
installInstructionsUrl:
|
|
14801
|
-
installHint: "
|
|
14808
|
+
installInstructionsUrl: BUZZ_INSTALL_DOCS_URL,
|
|
14809
|
+
installHint: "npx -y @aiaiai-pt/martha-cli channels install-harness buzz"
|
|
14802
14810
|
};
|
|
14803
14811
|
}
|
|
14804
14812
|
function isValidHarnessId(id) {
|
|
@@ -15217,7 +15225,7 @@ function assertSupportedChannel(kind) {
|
|
|
15217
15225
|
}
|
|
15218
15226
|
function registerChannelCommands(program2) {
|
|
15219
15227
|
const channels = program2.command("channels").description("Put a Martha agent in an external conversation channel — from Buzz (install-harness) or from Martha (connect)");
|
|
15220
|
-
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, so
|
|
15228
|
+
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) => {
|
|
15221
15229
|
assertSupportedChannel(type);
|
|
15222
15230
|
const def = buzzHarnessDefinition({
|
|
15223
15231
|
id: opts.id,
|
|
@@ -15231,6 +15239,11 @@ function registerChannelCommands(program2) {
|
|
|
15231
15239
|
const json = JSON.stringify(def, null, 2) + `
|
|
15232
15240
|
`;
|
|
15233
15241
|
if (opts.print) {
|
|
15242
|
+
process.stderr.write(`# Buzz custom harness definition — save as ${def.id}.json in Buzz's
|
|
15243
|
+
` + `# custom_harnesses directory, or let the CLI place it:
|
|
15244
|
+
` + `# npx -y @aiaiai-pt/martha-cli channels install-harness buzz
|
|
15245
|
+
` + `# ${BUZZ_INSTALL_DOCS_URL}
|
|
15246
|
+
`);
|
|
15234
15247
|
console.log(json);
|
|
15235
15248
|
return;
|
|
15236
15249
|
}
|
|
@@ -15382,19 +15395,30 @@ function installHarnessNotes(def) {
|
|
|
15382
15395
|
...profile ? [
|
|
15383
15396
|
source_default.cyan(" 2. …that's it."),
|
|
15384
15397
|
source_default.dim(` The definition names profile '${profile}'; the credential behind it
|
|
15385
|
-
` + ` stays in this machine's credentials file. Nothing to type in Buzz.`)
|
|
15398
|
+
` + ` stays in this machine's credentials file. Nothing to type in Buzz.`),
|
|
15399
|
+
"",
|
|
15400
|
+
source_default.dim(`A second Martha agent? '${profile}' is the DEFAULT for every agent on
|
|
15401
|
+
` + `this runtime — the definition is shared. Give the next one its own
|
|
15402
|
+
` + `Martha agent by setting ONE env var on it in Edit Agent, which wins
|
|
15403
|
+
` + `over the definition:`),
|
|
15404
|
+
source_default.cyan(` MARTHA_PROFILE=<its own profile>`),
|
|
15405
|
+
source_default.dim(` (mint that profile with: martha principals bootstrap --mode agent
|
|
15406
|
+
` + " --agent <name> --save-profile <name>)")
|
|
15386
15407
|
] : [
|
|
15387
15408
|
source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
|
|
15388
15409
|
source_default.dim(" (a definition file is shared config, not a secret store)"),
|
|
15389
|
-
source_default.dim("
|
|
15410
|
+
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
|
|
15411
|
+
` + ` different Martha agent. Re-run this with --use-profile <name> to make
|
|
15412
|
+
` + " one of them the default for the runtime.")
|
|
15390
15413
|
],
|
|
15391
15414
|
"",
|
|
15392
15415
|
source_default.dim(`Desktop owns the rest: the Nostr keypair in your keyring, the NIP-OA
|
|
15393
15416
|
` + "attestation, spawning buzz-acp, and attaching the agent to channels."),
|
|
15394
15417
|
"",
|
|
15395
|
-
source_default.dim(`
|
|
15396
|
-
` + `
|
|
15397
|
-
` +
|
|
15418
|
+
source_default.dim(`Every agent on this runtime follows the conversation: subscribe=all with
|
|
15419
|
+
` + `kinds ${DEFAULT_BUZZ_KINDS} pinned, so DMs, thread replies and follow-ups
|
|
15420
|
+
` + `reach it without being re-@mentioned. Want one that only answers when
|
|
15421
|
+
` + `named? Set BUZZ_ACP_SUBSCRIBE=mentions on that agent in Edit Agent.`)
|
|
15398
15422
|
];
|
|
15399
15423
|
}
|
|
15400
15424
|
|
|
@@ -15996,6 +16020,37 @@ var SERVER_GENERATED_FIELDS = new Set([
|
|
|
15996
16020
|
"tenant_id"
|
|
15997
16021
|
]);
|
|
15998
16022
|
var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
|
|
16023
|
+
var AGENT_WRITE_FIELDS = new Set([
|
|
16024
|
+
"name",
|
|
16025
|
+
"display_name",
|
|
16026
|
+
"description",
|
|
16027
|
+
"agent_type",
|
|
16028
|
+
"system_prompt",
|
|
16029
|
+
"llm_config",
|
|
16030
|
+
"loop_config",
|
|
16031
|
+
"runs_as_chat",
|
|
16032
|
+
"agent_metadata",
|
|
16033
|
+
"auth_method",
|
|
16034
|
+
"webhook_url",
|
|
16035
|
+
"webhook_secret",
|
|
16036
|
+
"webhook_timeout_s",
|
|
16037
|
+
"is_active",
|
|
16038
|
+
"self_grant_enabled",
|
|
16039
|
+
"self_grant_allowlist",
|
|
16040
|
+
"max_self_grant_scope",
|
|
16041
|
+
"self_grant_max_pending",
|
|
16042
|
+
"self_grant_collection_roots",
|
|
16043
|
+
...AGENT_MANAGED_FIELDS
|
|
16044
|
+
]);
|
|
16045
|
+
function validateAgentWriteBody(body, source) {
|
|
16046
|
+
if ("definition" in body) {
|
|
16047
|
+
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 */);
|
|
16048
|
+
}
|
|
16049
|
+
const unknown = Object.keys(body).filter((k) => !AGENT_WRITE_FIELDS.has(k));
|
|
16050
|
+
if (unknown.length > 0) {
|
|
16051
|
+
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 */);
|
|
16052
|
+
}
|
|
16053
|
+
}
|
|
15999
16054
|
function loadDefinitions(inputPath) {
|
|
16000
16055
|
const resolved = path8.resolve(inputPath);
|
|
16001
16056
|
if (!fs8.existsSync(resolved)) {
|
|
@@ -16084,6 +16139,9 @@ function toLocalDefinition(raw, source) {
|
|
|
16084
16139
|
}
|
|
16085
16140
|
const typedKind = kind;
|
|
16086
16141
|
const normalizedBody = typedKind === "Function" || typedKind === "Workflow" ? normalizeDefinitionWriteBody(typedKind, body) : body;
|
|
16142
|
+
if (typedKind === "Agent") {
|
|
16143
|
+
validateAgentWriteBody(normalizedBody, source);
|
|
16144
|
+
}
|
|
16087
16145
|
return { kind: typedKind, name, body: normalizedBody, source };
|
|
16088
16146
|
}
|
|
16089
16147
|
async function computeOperations(defs, api) {
|
|
@@ -16110,6 +16168,10 @@ async function computeOperations(defs, api) {
|
|
|
16110
16168
|
continue;
|
|
16111
16169
|
}
|
|
16112
16170
|
const changedFields = diffFields(def.body, remote);
|
|
16171
|
+
const needsReactivation = remote.is_active === false && def.body.is_active !== false;
|
|
16172
|
+
if (needsReactivation && !changedFields.includes("is_active")) {
|
|
16173
|
+
changedFields.push("is_active");
|
|
16174
|
+
}
|
|
16113
16175
|
if (changedFields.length === 0) {
|
|
16114
16176
|
ops.push({
|
|
16115
16177
|
kind: def.kind,
|
|
@@ -16123,7 +16185,8 @@ async function computeOperations(defs, api) {
|
|
|
16123
16185
|
name: def.name,
|
|
16124
16186
|
action: "update",
|
|
16125
16187
|
changedFields,
|
|
16126
|
-
source: def.source
|
|
16188
|
+
source: def.source,
|
|
16189
|
+
reactivate: needsReactivation || undefined
|
|
16127
16190
|
});
|
|
16128
16191
|
}
|
|
16129
16192
|
}
|
|
@@ -16204,7 +16267,12 @@ function renderPlan(ops) {
|
|
|
16204
16267
|
grouped.get(op.kind).push(op);
|
|
16205
16268
|
}
|
|
16206
16269
|
const nameW = Math.max(16, ...ops.map((o) => o.name.length)) + 2;
|
|
16207
|
-
for (const kind of [
|
|
16270
|
+
for (const kind of [
|
|
16271
|
+
"Function",
|
|
16272
|
+
"Workflow",
|
|
16273
|
+
"Agent",
|
|
16274
|
+
"Trigger"
|
|
16275
|
+
]) {
|
|
16208
16276
|
const kindOps = grouped.get(kind);
|
|
16209
16277
|
if (!kindOps)
|
|
16210
16278
|
continue;
|
|
@@ -16285,6 +16353,9 @@ async function executeApply(api, ops, defs, warnings) {
|
|
|
16285
16353
|
port_warnings: portWarnings.length > 0 ? portWarnings : undefined
|
|
16286
16354
|
});
|
|
16287
16355
|
} else {
|
|
16356
|
+
if (op.reactivate) {
|
|
16357
|
+
body.is_active = true;
|
|
16358
|
+
}
|
|
16288
16359
|
const res = await api.put(`${apiPath}/${encodeURIComponent(op.name)}`, body);
|
|
16289
16360
|
if (op.kind === "Function" || op.kind === "Workflow") {
|
|
16290
16361
|
verifyDefinitionUpdateResponse(op.kind, body, res);
|