@ouro.bot/cli 0.1.0-alpha.836 → 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,25 @@
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
+ },
17
+ {
18
+ "version": "0.1.0-alpha.837",
19
+ "changes": [
20
+ "Sanctuary: the root authority keeps a permanent empty child cgroup (ouro-keep), so Unraid's cgroup2-unraid can no longer remove the authority cgroup (D-026). The host keeper now lets the reaper run and pauses it only while the keep child is missing; every reader of the authority cgroup ignores the keep child."
21
+ ]
22
+ },
4
23
  {
5
24
  "version": "0.1.0-alpha.836",
6
25
  "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 })
@@ -428,12 +430,13 @@ const GATEWAY_SUPERVISOR = `${AUTHORITY_CUSTOM}/gateway-supervisor.sh`
428
430
  const GATEWAY_WATCHDOG = `${AUTHORITY_CUSTOM}/gateway-keeper-watchdog.sh`
429
431
  const UNRAID_STOP_HOOK = "/boot/config/stop"
430
432
  const GATEWAY_SUPERVISOR_SH = String.raw`#!/bin/sh
431
- # Ouro authority gateway keeper for Unraid (D-026 workaround).
432
- # The .830 root-authority gateway needs /sys/fs/cgroup/ouro-authority to persist while it
433
- # runs, but Unraid's cgroup2-unraid reaps empty cgroups and the gateway never populates it,
434
- # so the reaper deletes the cgroup and kills the gateway. A FIRM (not per-loop-racing) reaper
435
- # pause is required for the gateway to reach readiness. This keeper:
436
- # - firmly pauses the reaper and keeps it paused,
433
+ # Ouro authority gateway keeper for Unraid.
434
+ # The root-authority gateway needs /sys/fs/cgroup/ouro-authority to persist, and Unraid's
435
+ # cgroup2-unraid removes every top-level cgroup that reports populated 0. The kernel will not
436
+ # remove a cgroup that has a child cgroup, so the authority keeps an empty ouro-keep child
437
+ # (D-026). This keeper:
438
+ # - creates that keep child and lets the reaper run; it pauses the reaper only while the
439
+ # keep child is missing (a fresh boot, or an authority older than the keep child),
437
440
  # - keeps the gateway process alive,
438
441
  # - reconnects the resident (docker restart) if it is unhealthy while the gateway is ready
439
442
  # (handles reboot ordering, where Docker autostarts the resident before the gateway).
@@ -443,15 +446,17 @@ R=/mnt/user/appdata/ouro-authority
443
446
  GW=$R/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js
444
447
  CFG=$R/active.json
445
448
  LOG=/var/log/ouro-gateway.log
446
- pause_reaper() {
449
+ reaper_policy() {
447
450
  P=$(cat /run/cgroup2-unraid.pid 2>/dev/null) || return 0
448
451
  [ -n "$P" ] || return 0
452
+ if [ -d "$CG/ouro-keep" ]; then kill -CONT "$P" 2>/dev/null; return 0; fi
449
453
  S=$(ps -o stat= -p "$P" 2>/dev/null | tr -d ' ')
450
454
  case "$S" in T*) : ;; *) kill -STOP "$P" 2>/dev/null ;; esac
451
455
  }
452
456
  ensure_cg() {
453
457
  [ -d "$CG" ] || { mkdir -m 700 "$CG" 2>/dev/null; chown 0:0 "$CG" 2>/dev/null
454
458
  for c in cpu memory pids; do echo "+$c" > "$CG/cgroup.subtree_control" 2>/dev/null; done; }
459
+ [ -d "$CG/ouro-keep" ] || mkdir -m 700 "$CG/ouro-keep" 2>/dev/null
455
460
  }
456
461
  ensure_sock() {
457
462
  # Docker autostarts the resident before us and auto-creates its missing bind
@@ -475,12 +480,12 @@ start_gw() {
475
480
  }
476
481
  # An in-place upgrade pauses us with a flag; a stale flag (> 30 min) is ignored.
477
482
  maintenance() { [ -n "$(find /run/ouro-authority-maintenance -mmin -30 2>/dev/null)" ]; }
478
- pause_reaper; ensure_cg; ensure_sock
483
+ ensure_cg; reaper_policy; ensure_sock
479
484
  last_res=0
480
485
  while true; do
481
486
  if maintenance; then sleep 15; continue; fi
482
- pause_reaper
483
487
  ensure_cg
488
+ reaper_policy
484
489
  ensure_sock
485
490
  [ -n "$(gw_pid)" ] || { start_gw; sleep 8; }
486
491
  rd=$(jq -rc .status "$R"/epochs/*/readiness.json 2>/dev/null)
@@ -779,13 +784,25 @@ function buildFinalProof(version, out) {
779
784
 
780
785
  // ---- upgrade (in place, installed authority) --------------------------------
781
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.
782
792
  function pauseSupervision() {
783
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
+ }
784
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"])
785
- 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)")
786
802
  }
787
803
  function resumeSupervision() {
788
804
  rmSync(MAINTENANCE, { force: true })
805
+ rmSync(SHUTDOWN_FLAG, { force: true })
789
806
  installGatewaySupervisor()
790
807
  }
791
808
 
@@ -836,8 +853,12 @@ function upgrade(version, rehearse) {
836
853
  say("lifecycle upgrade (stop → switch → resident → migrate → start)")
837
854
  ok(sh("/usr/local/bin/node", [lifecycle, "upgrade", id, image(version), ...(rehearse ? ["--fail-after", rehearse] : [])], { stdio: ["ignore", "pipe", "pipe"] }).trim())
838
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}`))
839
860
  if (failure) {
840
- 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`)
841
862
  try { ok(`rollback: ${sh("/usr/local/bin/node", [lifecycle, "upgrade-rollback"], { stdio: ["ignore", "pipe", "pipe"] }).trim()}`) }
842
863
  catch (e) {
843
864
  resumeSupervision()
@@ -850,8 +871,8 @@ function upgrade(version, rehearse) {
850
871
  sha12(POLICY) === policyBefore ? ok("steward policy unchanged") : fail("STEWARD POLICY CHANGED")
851
872
  const live = docker(["inspect", CONTAINER, "--format", "{{.Config.Image}} {{.State.Status}}/{{.State.Health.Status}}"]).trim()
852
873
  ok(`butler: ${live}`)
853
- if (failure && !rehearse) fail("upgrade rolled back; the Butler is on its prior version (see above)")
854
- 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.`)
855
876
  }
856
877
 
857
878
  // ---- verify ---------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.836",
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.836</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;
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SANCTUARY_CGROUP_KEEP = void 0;
37
+ exports.sanctuaryCgroupChildren = sanctuaryCgroupChildren;
36
38
  exports.verifySanctuaryAuthorityInstallation = verifySanctuaryAuthorityInstallation;
37
39
  const node_crypto_1 = require("node:crypto");
38
40
  const fs = __importStar(require("node:fs"));
@@ -70,6 +72,16 @@ function readOwned(filePath, uid, gid, mode) {
70
72
  fs.closeSync(descriptor);
71
73
  }
72
74
  }
75
+ /**
76
+ * Unraid's cgroup2-unraid removes every top-level cgroup that reports `populated 0`,
77
+ * and the kernel refuses to remove a cgroup that has child cgroups. So the authority
78
+ * keeps one permanent empty child, and every reader of its cgroup ignores it (D-026).
79
+ */
80
+ exports.SANCTUARY_CGROUP_KEEP = "ouro-keep";
81
+ /** The authority cgroup's execution children: directories other than the keep child. */
82
+ function sanctuaryCgroupChildren(cgroupRoot) {
83
+ return fs.readdirSync(cgroupRoot).filter((name) => name !== exports.SANCTUARY_CGROUP_KEEP && fs.lstatSync(path.join(cgroupRoot, name)).isDirectory());
84
+ }
73
85
  function verifySanctuaryAuthorityInstallation(input) {
74
86
  const { expectedUid: uid, expectedGid: gid } = input;
75
87
  for (const root of [input.packageRoot, input.stateRoot, input.stagingRoot, input.cgroupRoot])
@@ -151,7 +151,7 @@ class SanctuaryAuthorityRootLifecycle {
151
151
  throw new Error("Resolve pending execution state before re-pinning");
152
152
  }
153
153
  }
154
- if (fs.readdirSync(this.#p(CGROUP)).some(name => fs.lstatSync(this.#p(`${CGROUP}/${name}`)).isDirectory()))
154
+ if ((0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
155
155
  throw new Error("Resolve pending cgroups before re-pinning");
156
156
  this.#verifyPrimitive("/usr/bin/prlimit", prlimitDigest);
157
157
  this.#verifyPrimitive("/usr/bin/setsid", setsidDigest);
@@ -294,9 +294,26 @@ class SanctuaryAuthorityRootLifecycle {
294
294
  throw new Error("Sanctuary installed boot lifecycle changed");
295
295
  return true;
296
296
  }
297
+ /** Create the authority cgroup with its keep child. The reaper can remove a freshly
298
+ * made (still empty) cgroup before the child exists, so retry that race a few times. */
299
+ #cgroup() {
300
+ const keep = this.#p(`${CGROUP}/${sanctuary_authority_installation_1.SANCTUARY_CGROUP_KEEP}`);
301
+ for (let attempt = 1; !fs.existsSync(keep); attempt += 1) {
302
+ try {
303
+ this.#directory(CGROUP);
304
+ fs.mkdirSync(keep, { mode: 0o700 });
305
+ }
306
+ catch (error) {
307
+ if (error.code !== "ENOENT" || attempt >= 5)
308
+ throw error;
309
+ }
310
+ }
311
+ this.#directory(CGROUP);
312
+ }
297
313
  #runtimeDirectories() {
298
- for (const directory of [this.#epochRoot(), STAGING, CGROUP])
314
+ for (const directory of [this.#epochRoot(), STAGING])
299
315
  this.#directory(directory);
316
+ this.#cgroup();
300
317
  this.#directory(SOCKET, 0o750, this.#socketGid);
301
318
  // Add only the required controllers; never replace another owner's controls.
302
319
  for (const root of ["/sys/fs/cgroup", CGROUP]) {
@@ -546,7 +563,7 @@ class SanctuaryAuthorityRootLifecycle {
546
563
  const epoch = this.#epoch();
547
564
  if (proof.schemaVersion !== 1 || proof.keyId !== epoch.epochId || proof.publicKeyDigest !== epoch.publicKeyDigest || proof.quiescent !== true
548
565
  || !Number.isSafeInteger(proof.cursor) || proof.cursor < epoch.predecessorCursor
549
- || fs.readdirSync(this.#p(CGROUP)).some((entry) => fs.lstatSync(this.#p(`${CGROUP}/${entry}`)).isDirectory()))
566
+ || (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
550
567
  throw new Error("Sanctuary gateway retirement cleanup is unproven");
551
568
  return true;
552
569
  }
@@ -854,6 +871,7 @@ class SanctuaryAuthorityRootLifecycle {
854
871
  }
855
872
  /** Start the gateway, then the resident, and prove both. */
856
873
  async #startUpgraded() {
874
+ this.#runtimeDirectories();
857
875
  await this.#startGateway();
858
876
  this.#docker(["start", "ouro-butler"]);
859
877
  await this.#wait(async () => this.#target().State.Running && this.#target().State.Health?.Status === "healthy" && await this.#ready(), 300_000);
@@ -39,6 +39,7 @@ const node_child_process_1 = require("node:child_process");
39
39
  const fs = __importStar(require("node:fs"));
40
40
  const path = __importStar(require("node:path"));
41
41
  const runtime_1 = require("../../nerves/runtime");
42
+ const sanctuary_authority_installation_1 = require("./sanctuary-authority-installation");
42
43
  const SCHEMA_VERSION = 1;
43
44
  const DIGEST = /^sha256:[a-f0-9]{64}$/u;
44
45
  const PERMIT_ID = /^permit-[A-Za-z0-9_-]{43}$/u;
@@ -183,10 +184,8 @@ class DetachedSanctuaryHostSupervisor {
183
184
  throw new Error("Sanctuary host supervisor state is unmatched");
184
185
  }
185
186
  }
186
- for (const permitId of fs.readdirSync(this.#options.cgroupRoot)) {
187
+ for (const permitId of (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#options.cgroupRoot)) {
187
188
  const cgroupPath = path.join(this.#options.cgroupRoot, permitId);
188
- if (!fs.lstatSync(cgroupPath).isDirectory())
189
- continue;
190
189
  if (!PERMIT_ID.test(permitId))
191
190
  throw new Error("Sanctuary host cgroup entry is invalid");
192
191
  if (!known.has(permitId) && !await this.#reconcileCgroup(cgroupPath)) {
@@ -346,7 +346,7 @@ async function startSanctuaryTelegramAuthority(options) {
346
346
  hostAuthority.retireRegistrations(ledger);
347
347
  await reconcileExecutions();
348
348
  if (hostAuthority.retireRegistrations(ledger).length !== 0
349
- || fs.readdirSync(config.hostCgroupRoot).some((entry) => fs.lstatSync(path.join(config.hostCgroupRoot, entry)).isDirectory()))
349
+ || (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(config.hostCgroupRoot).length !== 0)
350
350
  throw new Error("Sanctuary authority retirement cleanup is unproven");
351
351
  writePrivateJson(path.join(config.epochRoot, "retirement.json"), {
352
352
  schemaVersion: 1, keyId: config.keyId, publicKeyDigest: config.publicKeyDigest,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.836",
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.836",
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.836",
3
+ "version": "0.1.0-alpha.839",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },