@ouro.bot/cli 0.1.0-alpha.837 → 0.1.0-alpha.839

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.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.839",
6
+ "changes": [
7
+ "CLI: provision A2A peers without editing files. ouro connect a2a --host/--port/--public-url stores this machine's A2A bind address and advertised URL (D-038); ouro a2a onboard --did <did:key> --name <name> onboards a card-less client such as a coding harness, keyed on its DID; ouro friend update sets --admission, --initiative and --profile as well as --trust (D-039).",
8
+ "CLI: a2a onboard keeps the card and DID paths in one command block so its coverage exemptions stay attached to the right code."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.838",
13
+ "changes": [
14
+ "Upgrade orchestrator: pausing host supervision first installs this version's keeper scripts and raises the shutdown flag every watchdog honours, so an older keeper can no longer restart the resident mid-upgrade (found by the first 830 to 835 rehearsal, which rolled back cleanly). A rehearsal now reports an unplanned failure as a failure instead of as the planned stop."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.837",
6
19
  "changes": [
@@ -59,6 +59,8 @@ const UPGRADE_JOURNAL = `${ROOT}/upgrade.json`
59
59
  // Host supervision skips its work while this flag is fresh (< 30 min), so a crashed
60
60
  // upgrade can never leave self-heal off for longer than that.
61
61
  const MAINTENANCE = "/run/ouro-authority-maintenance"
62
+ // The stop hook's flag: every keeper watchdog version stands down while it exists.
63
+ const SHUTDOWN_FLAG = "/run/ouro-authority-shutdown"
62
64
  const UPGRADE_STEPS = ["stop", "switch", "resident", "migrate", "start"]
63
65
 
64
66
  const sh = (file, args, opts = {}) => execFileSync(file, args, { encoding: "utf8", maxBuffer: 64 << 20, ...opts })
@@ -782,13 +784,25 @@ function buildFinalProof(version, out) {
782
784
 
783
785
  // ---- upgrade (in place, installed authority) --------------------------------
784
786
 
787
+ // A host still running an older keeper has a cron watchdog that ignores the maintenance
788
+ // flag: it resurrected the paused supervisor, which restarted the resident mid-upgrade
789
+ // (the first 830 -> 835 rehearsal). So put this version's keeper scripts in place first
790
+ // (they honour the flag) and also raise the shutdown flag every watchdog version honours;
791
+ // resumeSupervision clears both.
785
792
  function pauseSupervision() {
786
793
  writeFileSync(MAINTENANCE, `${new Date().toISOString()} ${process.pid}\n`)
794
+ writeFileSync(SHUTDOWN_FLAG, `upgrade ${process.pid}\n`)
795
+ for (const [path, body] of [[GATEWAY_SUPERVISOR, GATEWAY_SUPERVISOR_SH], [GATEWAY_WATCHDOG, GATEWAY_WATCHDOG_SH]]) {
796
+ writeFileSync(path, body)
797
+ sh("/bin/chown", ["0:0", path]); chmodSync(path, 0o700)
798
+ }
787
799
  sh("/bin/sh", ["-c", "for p in $(pgrep -f '^/bin/sh /boot/config/custom/ouro-authority/[g]ateway-supervisor' || true); do kill $p; done"])
788
- ok("host supervision paused (its flag expires on its own after 30 min)")
800
+ if (sh("/bin/sh", ["-c", "pgrep -fc '^/bin/sh /boot/config/custom/ouro-authority/[g]ateway-supervisor' || true"]).trim() !== "0") fail("a gateway supervisor survived the pause")
801
+ ok("host supervision paused (keeper scripts current; flags expire on their own)")
789
802
  }
790
803
  function resumeSupervision() {
791
804
  rmSync(MAINTENANCE, { force: true })
805
+ rmSync(SHUTDOWN_FLAG, { force: true })
792
806
  installGatewaySupervisor()
793
807
  }
794
808
 
@@ -839,8 +853,12 @@ function upgrade(version, rehearse) {
839
853
  say("lifecycle upgrade (stop → switch → resident → migrate → start)")
840
854
  ok(sh("/usr/local/bin/node", [lifecycle, "upgrade", id, image(version), ...(rehearse ? ["--fail-after", rehearse] : [])], { stdio: ["ignore", "pipe", "pipe"] }).trim())
841
855
  } catch (e) { failure = e }
856
+ // Only the lifecycle's own rehearsal stop is planned; anything else is a real failure,
857
+ // even during a rehearsal (the first 830 -> 835 rehearsal was mislabelled "as planned").
858
+ const reason = failure ? lifecycleFailure(failure) : ""
859
+ const planned = Boolean(failure && rehearse && reason.includes(`rehearsal stopped after ${rehearse}`))
842
860
  if (failure) {
843
- console.log(`\n!! ${rehearse ? "rehearsal stopped as planned" : "upgrade failed"}: ${lifecycleFailure(failure)}\n!! rolling back to the predecessor`)
861
+ console.log(`\n!! ${planned ? "rehearsal stopped as planned" : "upgrade FAILED"}: ${reason}\n!! rolling back to the predecessor`)
844
862
  try { ok(`rollback: ${sh("/usr/local/bin/node", [lifecycle, "upgrade-rollback"], { stdio: ["ignore", "pipe", "pipe"] }).trim()}`) }
845
863
  catch (e) {
846
864
  resumeSupervision()
@@ -853,8 +871,8 @@ function upgrade(version, rehearse) {
853
871
  sha12(POLICY) === policyBefore ? ok("steward policy unchanged") : fail("STEWARD POLICY CHANGED")
854
872
  const live = docker(["inspect", CONTAINER, "--format", "{{.Config.Image}} {{.State.Status}}/{{.State.Health.Status}}"]).trim()
855
873
  ok(`butler: ${live}`)
856
- if (failure && !rehearse) fail("upgrade rolled back; the Butler is on its prior version (see above)")
857
- console.log(rehearse ? "\nREHEARSAL done — the rollback restored the predecessor." : `\nUPGRADE done — ${version} live. Run verify.`)
874
+ if (failure && !planned) fail("upgrade rolled back; the Butler is on its prior version (see above)")
875
+ console.log(rehearse ? "\nREHEARSAL done — the upgrade reached the planned step and the rollback restored the predecessor." : `\nUPGRADE done — ${version} live. Run verify.`)
858
876
  }
859
877
 
860
878
  // ---- verify ---------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.837",
2
+ "runtimeVersion": "0.1.0-alpha.839",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.837</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.839</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.onboardA2APeer = onboardA2APeer;
37
+ exports.onboardA2AClientByDid = onboardA2AClientByDid;
37
38
  const path = __importStar(require("node:path"));
38
39
  const identity_1 = require("../heart/identity");
39
40
  const runtime_1 = require("../nerves/runtime");
@@ -108,3 +109,25 @@ async function onboardA2APeer(options) {
108
109
  });
109
110
  return record;
110
111
  }
112
+ /**
113
+ * Onboard a client that serves no agent card (for example a coding harness with a
114
+ * file-backed identity) by its did:key alone. The record is keyed on the DID, so its
115
+ * signed, sealed messages resolve to this friend; the key must parse, or nothing is written.
116
+ */
117
+ async function onboardA2AClientByDid(options) {
118
+ if ((0, a2a_client_1.parseDidKey)(options.did) === null)
119
+ throw new Error(`not a valid did:key: ${options.did}`);
120
+ const record = await (0, friends_1.upsertAgentPeer)(storeFor(options), {
121
+ name: options.name,
122
+ agentId: options.did,
123
+ ...(options.trustLevel ? { trustLevel: options.trustLevel } : {}),
124
+ a2a: { did: options.did, agentId: options.did },
125
+ });
126
+ (0, runtime_1.emitNervesEvent)({
127
+ component: "friends",
128
+ event: "friends.a2a_client_onboarded",
129
+ message: "onboarded card-less A2A client into friend model",
130
+ meta: { agentName: options.agentName, friendId: record.id, peerName: record.name, trustLevel: record.trustLevel, did: options.did },
131
+ });
132
+ return record;
133
+ }
@@ -5053,13 +5053,23 @@ async function executeConnectVoice(agent, deps) {
5053
5053
  deps.writeStdout(message);
5054
5054
  return message;
5055
5055
  }
5056
- async function executeConnectA2A(agent, deps) {
5056
+ async function executeConnectA2A(agent, deps, bind = {}) {
5057
5057
  const { defaultA2APort } = await Promise.resolve().then(() => __importStar(require("../../a2a/config")));
5058
5058
  enableAgentSense(agent, "a2a", deps);
5059
5059
  const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
5060
- const port = defaultA2APort(agent);
5060
+ // Bind address, port and advertised URL are per machine (D-038): a tailnet IP on one
5061
+ // host means nothing on another, so they live in this machine's runtime config.
5062
+ const a2a = {
5063
+ ...(bind.host !== undefined ? { host: bind.host } : {}),
5064
+ ...(bind.port !== undefined ? { port: bind.port } : {}),
5065
+ ...(bind.publicUrl !== undefined ? { publicUrl: bind.publicUrl } : {}),
5066
+ };
5067
+ if (Object.keys(a2a).length > 0)
5068
+ await (0, runtime_credentials_1.mergeMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { a2a }, providerCliNow(deps));
5069
+ const port = bind.port ?? defaultA2APort(agent);
5061
5070
  const message = [
5062
5071
  `A2A connected for ${agent}`,
5072
+ ...(Object.keys(a2a).length > 0 ? [`This machine's A2A listener: ${bind.host ?? "127.0.0.1"}:${port}${bind.publicUrl ? `, advertised as ${bind.publicUrl}` : ""} (applies on the next \`ouro up\`).`] : []),
5063
5073
  `The daemon-managed A2A sense will listen locally on port ${port} after \`ouro up\`.`,
5064
5074
  `Local agent card: http://127.0.0.1:${port}/.well-known/agent-card.json`,
5065
5075
  `Local JSON-RPC endpoint: http://127.0.0.1:${port}/a2a`,
@@ -5184,7 +5194,7 @@ async function executeConnect(command, deps) {
5184
5194
  if (command.target === "voice")
5185
5195
  return executeConnectVoice(command.agent, deps);
5186
5196
  if (command.target === "a2a")
5187
- return executeConnectA2A(command.agent, deps);
5197
+ return executeConnectA2A(command.agent, deps, { host: command.a2aHost, port: command.a2aPort, publicUrl: command.a2aPublicUrl });
5188
5198
  if (command.target === "telegram")
5189
5199
  return executeConnectTelegram(command.agent, deps);
5190
5200
  if (command.target === "workbench")
@@ -6034,10 +6044,26 @@ async function executeFriendCommand(command, store) {
6034
6044
  return `created: ${id} (${command.name}, ${trustLevel})`;
6035
6045
  }
6036
6046
  if (command.kind === "friend.update") {
6037
- const result = await (0, friends_1.setFriendTrust)(store, command.friendId, command.trustLevel);
6038
- if (result.status === "not_found")
6047
+ if (!await store.get(command.friendId))
6039
6048
  return `friend not found: ${command.friendId}`;
6040
- return `updated: ${command.friendId} → trust=${command.trustLevel}`;
6049
+ if (command.trustLevel)
6050
+ await (0, friends_1.setFriendTrust)(store, command.friendId, command.trustLevel);
6051
+ const relationship = {
6052
+ ...(command.admissionState ? { admissionState: command.admissionState } : {}),
6053
+ ...(command.initiativePolicy ? { initiativePolicy: command.initiativePolicy } : {}),
6054
+ ...(command.capabilityProfileId ? { capabilityProfileId: command.capabilityProfileId } : {}),
6055
+ };
6056
+ if (Object.keys(relationship).length > 0) {
6057
+ const current = (await store.get(command.friendId));
6058
+ await store.put(command.friendId, { ...current, ...relationship, updatedAt: new Date().toISOString() });
6059
+ }
6060
+ const changed = [
6061
+ command.trustLevel ? `trust=${command.trustLevel}` : "",
6062
+ command.admissionState ? `admission=${command.admissionState}` : "",
6063
+ command.initiativePolicy ? `initiative=${command.initiativePolicy}` : "",
6064
+ command.capabilityProfileId ? `profile=${command.capabilityProfileId}` : "",
6065
+ ].filter(Boolean).join(", ");
6066
+ return `updated: ${command.friendId} → ${changed}`;
6041
6067
  }
6042
6068
  if (command.kind === "friend.link") {
6043
6069
  const result = await (0, friends_1.linkExternalId)(store, command.friendId, {
@@ -6107,6 +6133,27 @@ async function executeA2ACommand(command, deps) {
6107
6133
  }
6108
6134
  /* v8 ignore next -- false branch is foreground serve, intentionally ignored below because it waits for signals @preserve */
6109
6135
  if (command.kind === "a2a.onboard") {
6136
+ if (command.did) {
6137
+ const { onboardA2AClientByDid } = await Promise.resolve().then(() => __importStar(require("../../a2a/onboarding")));
6138
+ const record = await onboardA2AClientByDid({
6139
+ agentName: command.agent,
6140
+ did: command.did,
6141
+ name: command.name,
6142
+ ...(command.trustLevel ? { trustLevel: command.trustLevel } : {}),
6143
+ /* v8 ignore next -- production CLI falls back to the canonical bundle root; tests inject isolated roots @preserve */
6144
+ ...(deps.bundlesRoot ? { bundlesRoot: deps.bundlesRoot } : {}),
6145
+ });
6146
+ const message = [
6147
+ `onboarded A2A client: ${record.name}`,
6148
+ `friend id: ${record.id}`,
6149
+ /* v8 ignore next -- upsertAgentPeer always writes an explicit TrustLevel @preserve */
6150
+ `trust: ${record.trustLevel ?? "unknown"}`,
6151
+ `did: ${command.did}`,
6152
+ `Next: ouro friend update ${record.id} --agent ${command.agent} --admission active --initiative reactive_only --profile <capability-profile-id>`,
6153
+ ].join("\n");
6154
+ deps.writeStdout(message);
6155
+ return message;
6156
+ }
6110
6157
  const { onboardA2APeer } = await Promise.resolve().then(() => __importStar(require("../../a2a/onboarding")));
6111
6158
  const record = await onboardA2APeer({
6112
6159
  agentName: command.agent,
@@ -448,7 +448,7 @@ const SUBCOMMAND_HELP = {
448
448
  },
449
449
  "connect a2a": {
450
450
  description: "Enable the agent-to-agent A2A sense",
451
- usage: "ouro connect a2a [--agent <name>]",
451
+ usage: "ouro connect a2a [--agent <name>] [--host <address>] [--port <port>] [--public-url <url>]",
452
452
  example: "ouro connect a2a --agent <agent>",
453
453
  },
454
454
  "connect telegram": {
@@ -468,7 +468,7 @@ const SUBCOMMAND_HELP = {
468
468
  },
469
469
  "a2a onboard": {
470
470
  description: "Onboard an A2A peer into the existing friend model",
471
- usage: "ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]",
471
+ usage: "ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]",
472
472
  example: "ouro a2a onboard --agent <agent> --card-url https://peer.example/.well-known/agent-card.json --trust friend",
473
473
  },
474
474
  "a2a serve": {
@@ -141,7 +141,7 @@ function usage() {
141
141
  " ouro friend list [--agent <name>]",
142
142
  " ouro friend show <id> [--agent <name>]",
143
143
  " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
144
- " ouro friend update <id> --trust <level> [--agent <name>]",
144
+ " ouro friend update <id> [--trust <level>] [--admission <state>] [--initiative <policy>] [--profile <id>] [--agent <name>]",
145
145
  " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
146
146
  " ouro private decisions [--agent <name>] [--limit <n>] [--json]",
147
147
  " ouro private status [--agent <name>] [--json]",
@@ -150,7 +150,7 @@ function usage() {
150
150
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
151
151
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
152
152
  " ouro a2a card [--agent <name>] [--base-url <url>] [--json]",
153
- " ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]",
153
+ " ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]",
154
154
  " ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]",
155
155
  " ouro a2a identity [--identity-file <path>] [--json]",
156
156
  " ouro a2a message --to <card-url> --text <text> [--context <id>] [--identity-file <path>] [--json]",
@@ -1099,20 +1099,58 @@ function extractMailSourceFlags(args, usageText) {
1099
1099
  hasMailSourceFlags,
1100
1100
  };
1101
1101
  }
1102
+ // `ouro connect a2a` bind flags: where the listener binds and the URL its card advertises (D-038).
1103
+ function extractA2AConnectFlags(args) {
1104
+ const rest = [];
1105
+ const flags = {};
1106
+ for (let i = 0; i < args.length; i += 1) {
1107
+ const value = args[i + 1];
1108
+ if (args[i] === "--host" && value) {
1109
+ flags.host = value;
1110
+ i += 1;
1111
+ continue;
1112
+ }
1113
+ if (args[i] === "--public-url" && value) {
1114
+ const url = new URL(value);
1115
+ if (url.protocol !== "http:" && url.protocol !== "https:")
1116
+ throw new Error("A2A public URL must be http(s)");
1117
+ flags.publicUrl = value.replace(/\/+$/u, "");
1118
+ i += 1;
1119
+ continue;
1120
+ }
1121
+ if (args[i] === "--port" && value) {
1122
+ const port = Number(value);
1123
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
1124
+ throw new Error("A2A port must be 1-65535");
1125
+ flags.port = port;
1126
+ i += 1;
1127
+ continue;
1128
+ }
1129
+ rest.push(args[i]);
1130
+ }
1131
+ return { rest, ...flags };
1132
+ }
1102
1133
  function parseConnectCommand(args) {
1103
- const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
1134
+ const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys] [a2a: --host <address> --port <port> --public-url <url>]";
1104
1135
  const { agent, rest: afterAgent } = extractAgentFlag(args);
1105
- const mailFlags = extractMailSourceFlags(afterAgent, usageText);
1136
+ const a2aFlags = extractA2AConnectFlags(afterAgent);
1137
+ const mailFlags = extractMailSourceFlags(a2aFlags.rest, usageText);
1106
1138
  if (mailFlags.rest.length > 1)
1107
1139
  throw new Error(usageText);
1108
1140
  const target = normalizeConnectTarget(mailFlags.rest[0]);
1109
1141
  if (mailFlags.hasMailSourceFlags && target !== "mail") {
1110
1142
  throw new Error("Mail source flags require `ouro connect mail`.");
1111
1143
  }
1144
+ const hasA2AFlags = a2aFlags.host !== undefined || a2aFlags.port !== undefined || a2aFlags.publicUrl !== undefined;
1145
+ if (hasA2AFlags && target !== "a2a")
1146
+ throw new Error("--host, --port and --public-url require `ouro connect a2a`.");
1112
1147
  return {
1113
1148
  kind: "connect",
1114
1149
  ...(agent ? { agent } : {}),
1115
1150
  ...(target ? { target } : {}),
1151
+ ...(a2aFlags.host !== undefined ? { a2aHost: a2aFlags.host } : {}),
1152
+ ...(a2aFlags.port !== undefined ? { a2aPort: a2aFlags.port } : {}),
1153
+ ...(a2aFlags.publicUrl !== undefined ? { a2aPublicUrl: a2aFlags.publicUrl } : {}),
1116
1154
  ...(mailFlags.ownerEmail !== undefined ? { ownerEmail: mailFlags.ownerEmail } : {}),
1117
1155
  ...(mailFlags.source !== undefined ? { source: mailFlags.source } : {}),
1118
1156
  ...(mailFlags.noDelegatedSource ? { noDelegatedSource: true } : {}),
@@ -1405,25 +1443,30 @@ function parseFriendCommand(args) {
1405
1443
  }
1406
1444
  if (sub === "update") {
1407
1445
  const friendId = rest[0];
1446
+ const usage = "Usage: ouro friend update <id> [--trust <stranger|acquaintance|friend|family>] [--admission <unverified|active|revoked>] [--initiative <none|reactive_only|request_follow_up_only|proactive>] [--profile <capability-profile-id>]";
1408
1447
  if (!friendId)
1409
- throw new Error(`Usage: ouro friend update <id> --trust <level>`);
1410
- let trustLevel;
1411
- /* v8 ignore start -- flag parsing loop: tested via CLI parsing tests @preserve */
1412
- for (let i = 1; i < rest.length; i++) {
1413
- if (rest[i] === "--trust" && rest[i + 1]) {
1414
- trustLevel = rest[i + 1];
1415
- i += 1;
1416
- }
1417
- }
1418
- /* v8 ignore stop */
1419
- const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
1420
- if (!trustLevel || !VALID_TRUST_LEVELS.has(trustLevel)) {
1421
- throw new Error(`Usage: ouro friend update <id> --trust <stranger|acquaintance|friend|family>`);
1448
+ throw new Error(usage);
1449
+ // Every relationship field an owner provisions, not just trust (D-039).
1450
+ const choices = {
1451
+ "--trust": { key: "trustLevel", values: ["stranger", "acquaintance", "friend", "family"] },
1452
+ "--admission": { key: "admissionState", values: ["unverified", "active", "revoked"] },
1453
+ "--initiative": { key: "initiativePolicy", values: ["none", "reactive_only", "request_follow_up_only", "proactive"] },
1454
+ "--profile": { key: "capabilityProfileId" },
1455
+ };
1456
+ const fields = {};
1457
+ for (let i = 1; i < rest.length; i += 2) {
1458
+ const choice = choices[rest[i]];
1459
+ const value = rest[i + 1];
1460
+ if (!choice || !value || (choice.values && !choice.values.includes(value)))
1461
+ throw new Error(usage);
1462
+ fields[choice.key] = value;
1422
1463
  }
1464
+ if (Object.keys(fields).length === 0)
1465
+ throw new Error(usage);
1423
1466
  return {
1424
1467
  kind: "friend.update",
1425
1468
  friendId,
1426
- trustLevel: trustLevel,
1469
+ ...fields,
1427
1470
  ...(agent ? { agent } : {}),
1428
1471
  };
1429
1472
  }
@@ -1504,6 +1547,7 @@ function parseA2ACommand(args) {
1504
1547
  }
1505
1548
  if (sub === "onboard") {
1506
1549
  let cardUrl;
1550
+ let did;
1507
1551
  let trustLevel;
1508
1552
  let name;
1509
1553
  const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
@@ -1512,10 +1556,14 @@ function parseA2ACommand(args) {
1512
1556
  cardUrl = rest[++i];
1513
1557
  continue;
1514
1558
  }
1559
+ if (rest[i] === "--did" && rest[i + 1]) {
1560
+ did = rest[++i];
1561
+ continue;
1562
+ }
1515
1563
  if (rest[i] === "--trust" && rest[i + 1]) {
1516
1564
  const raw = rest[++i];
1517
1565
  if (!VALID_TRUST_LEVELS.has(raw))
1518
- throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <stranger|acquaintance|friend|family>] [--name <name>]");
1566
+ throw new Error("Usage: ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <stranger|acquaintance|friend|family>] [--name <name>]");
1519
1567
  trustLevel = raw;
1520
1568
  continue;
1521
1569
  }
@@ -1523,11 +1571,12 @@ function parseA2ACommand(args) {
1523
1571
  name = rest[++i];
1524
1572
  continue;
1525
1573
  }
1526
- throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]");
1574
+ throw new Error("Usage: ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]");
1527
1575
  }
1528
- if (!cardUrl)
1529
- throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]");
1530
- return { kind: "a2a.onboard", cardUrl, ...(agent ? { agent } : {}), ...(trustLevel ? { trustLevel } : {}), ...(name ? { name } : {}) };
1576
+ // A client that serves no card (a coding harness) is onboarded by its did:key alone.
1577
+ if (!cardUrl === !did || (did && !name))
1578
+ throw new Error("Usage: ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]");
1579
+ return { kind: "a2a.onboard", ...(cardUrl ? { cardUrl } : { did: did }), ...(agent ? { agent } : {}), ...(trustLevel ? { trustLevel } : {}), ...(name ? { name } : {}) };
1531
1580
  }
1532
1581
  if (sub === "serve") {
1533
1582
  let host;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.837",
3
+ "version": "0.1.0-alpha.839",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.837",
9
+ "version": "0.1.0-alpha.839",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.837",
3
+ "version": "0.1.0-alpha.839",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },