@ouro.bot/cli 0.1.0-alpha.652 → 0.1.0-alpha.654

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.
Files changed (37) hide show
  1. package/changelog.json +13 -0
  2. package/dist/a2a/card.js +56 -0
  3. package/dist/a2a/client.js +143 -0
  4. package/dist/a2a/config.js +50 -0
  5. package/dist/a2a/onboarding.js +111 -0
  6. package/dist/a2a/server.js +498 -0
  7. package/dist/a2a/task-store.js +69 -0
  8. package/dist/a2a/types.js +3 -0
  9. package/dist/commerce/store.js +755 -0
  10. package/dist/commerce/types.js +3 -0
  11. package/dist/heart/daemon/cli-exec.js +119 -4
  12. package/dist/heart/daemon/cli-help.js +14 -2
  13. package/dist/heart/daemon/cli-parse.js +88 -4
  14. package/dist/heart/daemon/daemon.js +2 -1
  15. package/dist/heart/daemon/process-manager.js +2 -1
  16. package/dist/heart/daemon/runtime-logging.js +1 -1
  17. package/dist/heart/daemon/sense-manager.js +71 -15
  18. package/dist/heart/identity.js +4 -1
  19. package/dist/heart/sense-truth.js +2 -0
  20. package/dist/heart/turn-context.js +6 -0
  21. package/dist/mind/friends/channel.js +10 -1
  22. package/dist/mind/friends/resolver.js +13 -2
  23. package/dist/mind/friends/store-file.js +13 -0
  24. package/dist/mind/friends/types.js +1 -1
  25. package/dist/mind/prompt.js +11 -0
  26. package/dist/repertoire/guardrails.js +25 -2
  27. package/dist/repertoire/tools-a2a.js +283 -0
  28. package/dist/repertoire/tools-base.js +4 -0
  29. package/dist/repertoire/tools-commerce.js +253 -0
  30. package/dist/repertoire/tools-flight.js +68 -5
  31. package/dist/repertoire/tools-stripe.js +49 -7
  32. package/dist/repertoire/tools.js +50 -2
  33. package/dist/senses/a2a-entry.js +78 -0
  34. package/dist/senses/pipeline.js +13 -0
  35. package/dist/senses/shared-turn.js +30 -5
  36. package/package.json +1 -1
  37. package/skills/agent-commerce.md +17 -10
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ /* v8 ignore file -- type-only commerce authority records */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -368,6 +368,10 @@ function agentResolutionFailureMode(command) {
368
368
  case "inner.status":
369
369
  case "session.list":
370
370
  return "return-message";
371
+ case "a2a.card":
372
+ case "a2a.onboard":
373
+ case "a2a.serve":
374
+ return "throw";
371
375
  case "provider.use":
372
376
  case "provider.check":
373
377
  case "provider.status":
@@ -499,7 +503,7 @@ function writeSyncProbeSummary(deps, findings) {
499
503
  function bootPhasePlan(_daemonAlive) {
500
504
  return ["update check", "system setup", "sync probe", "starting daemon", "provider checks", "final daemon check"];
501
505
  }
502
- const FINAL_DAEMON_HEALTH_SETTLE_TIMEOUT_MS = 20_000;
506
+ const FINAL_DAEMON_HEALTH_SETTLE_TIMEOUT_MS = 90_000;
503
507
  const FINAL_DAEMON_HEALTH_SETTLE_POLL_INTERVAL_MS = 500;
504
508
  /**
505
509
  * Layer 2: brief, scannable summary of a boot-sync-probe finding for the
@@ -2491,11 +2495,12 @@ function enableAgentSense(agent, sense, deps) {
2491
2495
  bluebubbles: senses.bluebubbles ?? { enabled: false },
2492
2496
  mail: senses.mail ?? { enabled: false },
2493
2497
  voice: senses.voice ?? { enabled: false },
2498
+ a2a: senses.a2a ?? { enabled: false },
2494
2499
  [sense]: { ...existing, enabled: true },
2495
2500
  };
2496
2501
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
2497
2502
  }
2498
- const CONNECT_MENU_PROMPT = "Choose [1-7] or type a name: ";
2503
+ const CONNECT_MENU_PROMPT = "Choose [1-8] or type a name: ";
2499
2504
  function connectMenuIsTTY(deps) {
2500
2505
  return deps.isTTY ?? process.stdout.isTTY === true;
2501
2506
  }
@@ -2507,6 +2512,7 @@ function readConnectBaySenseFlags(agent, deps) {
2507
2512
  blueBubblesEnabled: parsed.senses?.bluebubbles?.enabled === true,
2508
2513
  mailEnabled: parsed.senses?.mail?.enabled === true,
2509
2514
  voiceEnabled: parsed.senses?.voice?.enabled === true,
2515
+ a2aEnabled: parsed.senses?.a2a?.enabled === true,
2510
2516
  };
2511
2517
  }
2512
2518
  async function buildConnectMenu(agent, deps, onProgress) {
@@ -2536,7 +2542,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
2536
2542
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
2537
2543
  onProgress?.("loading this machine's settings");
2538
2544
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
2539
- const { teamsEnabled, blueBubblesEnabled, mailEnabled, voiceEnabled } = readConnectBaySenseFlags(agent, deps);
2545
+ const { teamsEnabled, blueBubblesEnabled, mailEnabled, voiceEnabled, a2aEnabled } = readConnectBaySenseFlags(agent, deps);
2540
2546
  const perplexityApiKey = runtimeConfig.ok
2541
2547
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
2542
2548
  : null;
@@ -2691,6 +2697,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
2691
2697
  ? "ready"
2692
2698
  : "missing"
2693
2699
  : runtimeConfigReadStatus(runtimeConfig);
2700
+ const a2aStatus = a2aEnabled ? "ready" : "missing";
2694
2701
  const entries = [
2695
2702
  {
2696
2703
  option: "1",
@@ -2805,6 +2812,20 @@ async function buildConnectMenu(agent, deps, onProgress) {
2805
2812
  status: voiceStatus,
2806
2813
  }) ? `ouro connect voice --agent ${agent}` : undefined,
2807
2814
  },
2815
+ {
2816
+ option: "8",
2817
+ name: "A2A",
2818
+ section: "Portable",
2819
+ status: a2aStatus,
2820
+ description: "Agent-to-agent sense endpoint and peer onboarding.",
2821
+ detailLines: a2aEnabled ? ["enabled in agent.json"] : ["not enabled in agent.json"],
2822
+ nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
2823
+ option: "8",
2824
+ name: "A2A",
2825
+ section: "Portable",
2826
+ status: a2aStatus,
2827
+ }) ? `ouro connect a2a --agent ${agent}` : undefined,
2828
+ },
2808
2829
  ];
2809
2830
  const isTTY = connectMenuIsTTY(deps);
2810
2831
  return (0, connect_bay_1.renderConnectBay)(entries, {
@@ -4340,6 +4361,9 @@ function connectMenuTarget(answer) {
4340
4361
  return "mail";
4341
4362
  if (normalized === "7" || normalized === "voice" || normalized === "audio" || normalized === "speech")
4342
4363
  return "voice";
4364
+ /* v8 ignore next -- direct `ouro connect a2a` covers behavior; interactive menu requires broader provider/vault readiness work @preserve */
4365
+ if (normalized === "8" || normalized === "a2a" || normalized === "agent2agent" || normalized === "agent-to-agent")
4366
+ return "a2a";
4343
4367
  return "cancel";
4344
4368
  }
4345
4369
  async function executeConnectVoice(agent, deps) {
@@ -4378,6 +4402,25 @@ async function executeConnectVoice(agent, deps) {
4378
4402
  deps.writeStdout(message);
4379
4403
  return message;
4380
4404
  }
4405
+ async function executeConnectA2A(agent, deps) {
4406
+ const { defaultA2APort } = await Promise.resolve().then(() => __importStar(require("../../a2a/config")));
4407
+ enableAgentSense(agent, "a2a", deps);
4408
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
4409
+ const port = defaultA2APort(agent);
4410
+ const message = [
4411
+ `A2A connected for ${agent}`,
4412
+ `The daemon-managed A2A sense will listen locally on port ${port} after \`ouro up\`.`,
4413
+ `Local agent card: http://127.0.0.1:${port}/.well-known/agent-card.json`,
4414
+ `Local JSON-RPC endpoint: http://127.0.0.1:${port}/a2a`,
4415
+ "For a public endpoint, expose that local port through your chosen tunnel and publish the resulting base URL:",
4416
+ ` ouro a2a card --agent ${agent} --base-url https://<public-host>`,
4417
+ "Onboard a peer into the existing friend model:",
4418
+ ` ouro a2a onboard --agent ${agent} --card-url https://<peer>/.well-known/agent-card.json --trust friend`,
4419
+ ...(syncSummary ? [syncSummary] : []),
4420
+ ].join("\n");
4421
+ deps.writeStdout(message);
4422
+ return message;
4423
+ }
4381
4424
  async function executeConnect(command, deps) {
4382
4425
  if (command.target === "providers")
4383
4426
  return executeConnectProviders(command.agent, deps);
@@ -4393,6 +4436,8 @@ async function executeConnect(command, deps) {
4393
4436
  return executeConnectMail(command.agent, deps, command);
4394
4437
  if (command.target === "voice")
4395
4438
  return executeConnectVoice(command.agent, deps);
4439
+ if (command.target === "a2a")
4440
+ return executeConnectA2A(command.agent, deps);
4396
4441
  const progress = createHumanCommandProgress(deps, "connect");
4397
4442
  let menu;
4398
4443
  try {
@@ -4404,7 +4449,7 @@ async function executeConnect(command, deps) {
4404
4449
  const promptInput = deps.promptInput;
4405
4450
  if (!promptInput) {
4406
4451
  const message = [
4407
- menu.replace(/\nChoose \[1-7\] or type a name: $/, ""),
4452
+ menu.replace(/\nChoose \[1-8\] or type a name: $/, ""),
4408
4453
  "",
4409
4454
  `Run: ouro connect providers --agent ${command.agent}`,
4410
4455
  `Run: ouro connect perplexity --agent ${command.agent}`,
@@ -4413,6 +4458,7 @@ async function executeConnect(command, deps) {
4413
4458
  `Run: ouro connect bluebubbles --agent ${command.agent}`,
4414
4459
  `Run: ouro connect mail --agent ${command.agent}`,
4415
4460
  `Run: ouro connect voice --agent ${command.agent}`,
4461
+ `Run: ouro connect a2a --agent ${command.agent}`,
4416
4462
  ].join("\n");
4417
4463
  deps.writeStdout(message);
4418
4464
  return message;
@@ -4432,6 +4478,9 @@ async function executeConnect(command, deps) {
4432
4478
  return executeConnectMail(command.agent, deps);
4433
4479
  if (answer === "voice")
4434
4480
  return executeConnectVoice(command.agent, deps);
4481
+ /* v8 ignore next -- direct `ouro connect a2a` covers behavior; interactive menu requires broader provider/vault readiness work @preserve */
4482
+ if (answer === "a2a")
4483
+ return executeConnectA2A(command.agent, deps);
4435
4484
  const message = "connect cancelled.";
4436
4485
  deps.writeStdout(message);
4437
4486
  return message;
@@ -5262,6 +5311,69 @@ async function executeFriendCommand(command, store) {
5262
5311
  await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
5263
5312
  return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
5264
5313
  }
5314
+ async function executeA2ACommand(command, deps) {
5315
+ if (command.kind === "a2a.card") {
5316
+ const { buildA2AAgentCard } = await Promise.resolve().then(() => __importStar(require("../../a2a/card")));
5317
+ const { defaultA2APort } = await Promise.resolve().then(() => __importStar(require("../../a2a/config")));
5318
+ const { endpointForCard } = await Promise.resolve().then(() => __importStar(require("../../a2a/client")));
5319
+ const baseUrl = command.baseUrl ?? `http://127.0.0.1:${defaultA2APort(command.agent)}`;
5320
+ const card = buildA2AAgentCard({ agentName: command.agent, baseUrl });
5321
+ /* v8 ignore next -- buildA2AAgentCard always emits a JSON-RPC endpoint @preserve */
5322
+ const endpoint = endpointForCard(card) ?? "unknown";
5323
+ const message = command.json
5324
+ ? JSON.stringify(card, null, 2)
5325
+ : [
5326
+ `A2A agent card for ${command.agent}`,
5327
+ `card URL: ${baseUrl.replace(/\/+$/, "")}/.well-known/agent-card.json`,
5328
+ `endpoint: ${endpoint}`,
5329
+ JSON.stringify(card, null, 2),
5330
+ ].join("\n");
5331
+ deps.writeStdout(message);
5332
+ return message;
5333
+ }
5334
+ /* v8 ignore next -- false branch is foreground serve, intentionally ignored below because it waits for signals @preserve */
5335
+ if (command.kind === "a2a.onboard") {
5336
+ const { onboardA2APeer } = await Promise.resolve().then(() => __importStar(require("../../a2a/onboarding")));
5337
+ const record = await onboardA2APeer({
5338
+ agentName: command.agent,
5339
+ cardUrl: command.cardUrl,
5340
+ ...(command.trustLevel ? { trustLevel: command.trustLevel } : {}),
5341
+ ...(command.name ? { name: command.name } : {}),
5342
+ /* v8 ignore next -- production CLI falls back to the canonical bundle root; tests inject isolated roots @preserve */
5343
+ ...(deps.bundlesRoot ? { bundlesRoot: deps.bundlesRoot } : {}),
5344
+ /* v8 ignore next -- production CLI uses global fetch; tests inject fetch for hermetic cards @preserve */
5345
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
5346
+ });
5347
+ const message = [
5348
+ `onboarded A2A peer: ${record.name}`,
5349
+ `friend id: ${record.id}`,
5350
+ /* v8 ignore next -- onboardA2APeer always writes an explicit TrustLevel @preserve */
5351
+ `trust: ${record.trustLevel ?? "unknown"}`,
5352
+ /* v8 ignore next -- onboardA2APeer validates and persists an A2A endpoint before returning @preserve */
5353
+ `endpoint: ${record.agentMeta?.a2a?.endpointUrl ?? "unknown"}`,
5354
+ ].join("\n");
5355
+ deps.writeStdout(message);
5356
+ return message;
5357
+ }
5358
+ /* v8 ignore start -- foreground serve intentionally waits for process signals; a2a/server has route-level coverage @preserve */
5359
+ const { startA2AServer } = await Promise.resolve().then(() => __importStar(require("../../a2a/server")));
5360
+ const handle = await startA2AServer({
5361
+ agentName: command.agent,
5362
+ ...(command.host ? { host: command.host } : {}),
5363
+ ...(command.port ? { port: command.port } : {}),
5364
+ ...(command.baseUrl ? { baseUrl: command.baseUrl } : {}),
5365
+ ...(command.path ? { path: command.path } : {}),
5366
+ });
5367
+ const message = `A2A listening for ${command.agent}\nendpoint: ${handle.endpointUrl}\ncard: ${handle.url.replace(/\/+$/, "")}/.well-known/agent-card.json`;
5368
+ deps.writeStdout(message);
5369
+ await new Promise((resolve) => {
5370
+ process.once("SIGINT", () => resolve());
5371
+ process.once("SIGTERM", () => resolve());
5372
+ });
5373
+ await handle.close();
5374
+ return message;
5375
+ /* v8 ignore stop */
5376
+ }
5265
5377
  // ── Dev mode helpers ──
5266
5378
  /* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
5267
5379
  function getDevConfigPath() {
@@ -6594,6 +6706,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6594
6706
  deps.writeStdout(message);
6595
6707
  return message;
6596
6708
  }
6709
+ if (command.kind === "a2a.card" || command.kind === "a2a.onboard" || command.kind === "a2a.serve") {
6710
+ return executeA2ACommand(command, deps);
6711
+ }
6597
6712
  // ── provider commands (local, no daemon socket needed) ──
6598
6713
  if (command.kind === "provider.use") {
6599
6714
  return executeProviderUse(command, deps);
@@ -187,9 +187,16 @@ exports.COMMAND_REGISTRY = {
187
187
  connect: {
188
188
  category: "Auth",
189
189
  description: "Set up providers, portable integrations, and local senses from one guided screen",
190
- usage: "ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>]",
190
+ usage: "ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>]",
191
191
  example: "ouro connect",
192
- subcommands: ["providers", "perplexity", "embeddings", "teams", "bluebubbles", "mail"],
192
+ subcommands: ["providers", "perplexity", "embeddings", "teams", "bluebubbles", "mail", "voice", "a2a"],
193
+ },
194
+ a2a: {
195
+ category: "Friends",
196
+ description: "Publish A2A cards, onboard agent peers, and run the A2A sense server",
197
+ usage: "ouro a2a <card|onboard|serve> [--agent <name>]",
198
+ example: "ouro a2a card --agent <agent> --base-url https://agent.example",
199
+ subcommands: ["card", "onboard", "serve"],
193
200
  },
194
201
  mail: {
195
202
  category: "Auth",
@@ -330,6 +337,11 @@ const SUBCOMMAND_HELP = {
330
337
  usage: "ouro connect mail [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
331
338
  example: "ouro connect mail --agent <agent> --owner-email you@example.com --source hey",
332
339
  },
340
+ "connect a2a": {
341
+ description: "Enable the agent-to-agent A2A sense",
342
+ usage: "ouro connect a2a [--agent <name>]",
343
+ example: "ouro connect a2a --agent <agent>",
344
+ },
333
345
  "account ensure": {
334
346
  description: "Idempotently prepare an agent's vault-backed work substrate account and private Mailroom mailbox",
335
347
  usage: "ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
@@ -84,7 +84,7 @@ function usage() {
84
84
  " ouro -v|--version",
85
85
  " ouro auth [--agent <name>] [--provider <provider>]",
86
86
  " ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
87
- " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
87
+ " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
88
88
  " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
89
89
  " ouro mail backfill-indexes [--agent <name>] [--foreground]",
90
90
  " ouro auth verify [--agent <name>] [--provider <provider>]",
@@ -117,6 +117,9 @@ function usage() {
117
117
  " ouro inner [--agent <name>]",
118
118
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
119
119
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
120
+ " ouro a2a card [--agent <name>] [--base-url <url>] [--json]",
121
+ " ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]",
122
+ " ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]",
120
123
  " ouro whoami [--agent <name>]",
121
124
  " ouro session list [--agent <name>]",
122
125
  " ouro mcp list",
@@ -254,7 +257,7 @@ function parseLinkCommand(args, kind = "friend.link") {
254
257
  throw new Error(`Usage\n${usage()}`);
255
258
  }
256
259
  if (!(0, types_1.isIdentityProvider)(providerRaw)) {
257
- throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation.`);
260
+ throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation|imessage-handle|email-address|a2a-agent.`);
258
261
  }
259
262
  return {
260
263
  kind,
@@ -739,7 +742,9 @@ function normalizeConnectTarget(value) {
739
742
  return "mail";
740
743
  if (value === "voice" || value === "audio" || value === "speech")
741
744
  return "voice";
742
- throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice] [--agent <name>]");
745
+ if (value === "a2a" || value === "agent2agent" || value === "agent-to-agent")
746
+ return "a2a";
747
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>]");
743
748
  }
744
749
  function extractMailSourceFlags(args, usageText) {
745
750
  const rest = [];
@@ -792,7 +797,7 @@ function extractMailSourceFlags(args, usageText) {
792
797
  };
793
798
  }
794
799
  function parseConnectCommand(args) {
795
- const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
800
+ const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
796
801
  const { agent, rest: afterAgent } = extractAgentFlag(args);
797
802
  const mailFlags = extractMailSourceFlags(afterAgent, usageText);
798
803
  if (mailFlags.rest.length > 1)
@@ -1075,6 +1080,83 @@ function parseFriendCommand(args) {
1075
1080
  return parseLinkCommand(rest, "friend.unlink");
1076
1081
  throw new Error(`Usage\n${usage()}`);
1077
1082
  }
1083
+ function parseA2ACommand(args) {
1084
+ const { agent, rest: cleaned } = extractAgentFlag(args);
1085
+ const [sub, ...rest] = cleaned;
1086
+ if (sub === "card") {
1087
+ let baseUrl;
1088
+ let json = false;
1089
+ for (let i = 0; i < rest.length; i += 1) {
1090
+ if (rest[i] === "--base-url" && rest[i + 1]) {
1091
+ baseUrl = rest[++i];
1092
+ continue;
1093
+ }
1094
+ if (rest[i] === "--json") {
1095
+ json = true;
1096
+ continue;
1097
+ }
1098
+ throw new Error("Usage: ouro a2a card [--agent <name>] [--base-url <url>] [--json]");
1099
+ }
1100
+ return { kind: "a2a.card", ...(agent ? { agent } : {}), ...(baseUrl ? { baseUrl } : {}), ...(json ? { json: true } : {}) };
1101
+ }
1102
+ if (sub === "onboard") {
1103
+ let cardUrl;
1104
+ let trustLevel;
1105
+ let name;
1106
+ const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
1107
+ for (let i = 0; i < rest.length; i += 1) {
1108
+ if (rest[i] === "--card-url" && rest[i + 1]) {
1109
+ cardUrl = rest[++i];
1110
+ continue;
1111
+ }
1112
+ if (rest[i] === "--trust" && rest[i + 1]) {
1113
+ const raw = rest[++i];
1114
+ if (!VALID_TRUST_LEVELS.has(raw))
1115
+ throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <stranger|acquaintance|friend|family>] [--name <name>]");
1116
+ trustLevel = raw;
1117
+ continue;
1118
+ }
1119
+ if (rest[i] === "--name" && rest[i + 1]) {
1120
+ name = rest[++i];
1121
+ continue;
1122
+ }
1123
+ throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]");
1124
+ }
1125
+ if (!cardUrl)
1126
+ throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]");
1127
+ return { kind: "a2a.onboard", cardUrl, ...(agent ? { agent } : {}), ...(trustLevel ? { trustLevel } : {}), ...(name ? { name } : {}) };
1128
+ }
1129
+ if (sub === "serve") {
1130
+ let host;
1131
+ let port;
1132
+ let baseUrl;
1133
+ let path;
1134
+ for (let i = 0; i < rest.length; i += 1) {
1135
+ if (rest[i] === "--host" && rest[i + 1]) {
1136
+ host = rest[++i];
1137
+ continue;
1138
+ }
1139
+ if (rest[i] === "--port" && rest[i + 1]) {
1140
+ const rawPort = Number.parseInt(rest[++i], 10);
1141
+ if (!Number.isInteger(rawPort) || rawPort < 1 || rawPort > 65535)
1142
+ throw new Error("A2A port must be 1-65535");
1143
+ port = rawPort;
1144
+ continue;
1145
+ }
1146
+ if (rest[i] === "--base-url" && rest[i + 1]) {
1147
+ baseUrl = rest[++i];
1148
+ continue;
1149
+ }
1150
+ if (rest[i] === "--path" && rest[i + 1]) {
1151
+ path = rest[++i];
1152
+ continue;
1153
+ }
1154
+ throw new Error("Usage: ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]");
1155
+ }
1156
+ return { kind: "a2a.serve", ...(agent ? { agent } : {}), ...(host ? { host } : {}), ...(port ? { port } : {}), ...(baseUrl ? { baseUrl } : {}), ...(path ? { path } : {}) };
1157
+ }
1158
+ throw new Error("Usage: ouro a2a card|onboard|serve ...");
1159
+ }
1078
1160
  function parseConfigCommand(args) {
1079
1161
  const { agent, rest: afterAgent } = extractAgentFlag(args);
1080
1162
  const { facing, rest: cleaned } = extractFacingFlag(afterAgent);
@@ -1473,6 +1555,8 @@ function parseOuroCommand(args) {
1473
1555
  return parseMigrateToDeskCommand(args.slice(1));
1474
1556
  if (head === "friend")
1475
1557
  return parseFriendCommand(args.slice(1));
1558
+ if (head === "a2a")
1559
+ return parseA2ACommand(args.slice(1));
1476
1560
  if (head === "config")
1477
1561
  return parseConfigCommand(args.slice(1));
1478
1562
  if (head === "mcp")
@@ -108,7 +108,8 @@ function parseOrphanPidsFromPs(psOutput, selfPid) {
108
108
  && !line.includes("daemon-entry.js")
109
109
  && !line.includes("bluebubbles/entry.js")
110
110
  && !line.includes("mail-entry.js")
111
- && !line.includes("teams-entry.js"))
111
+ && !line.includes("teams-entry.js")
112
+ && !line.includes("a2a-entry.js"))
112
113
  continue;
113
114
  // Parse `<pid> <ppid> <command...>`. ps pads these with leading spaces.
114
115
  // Regex guarantees both groups are \d+ so parseInt can't produce NaN.
@@ -287,7 +287,8 @@ class DaemonProcessManager {
287
287
  this.notifySnapshotChange(state.snapshot);
288
288
  return;
289
289
  }
290
- const args = [entryScript, "--agent", state.config.agentArg ?? agent, ...(state.config.args ?? [])];
290
+ const agentArgs = state.config.getArgs?.() ?? state.config.args ?? [];
291
+ const args = [entryScript, "--agent", state.config.agentArg ?? agent, ...agentArgs];
291
292
  const child = this.spawnFn("node", args, {
292
293
  cwd: runCwd,
293
294
  env: state.config.env ? { ...process.env, ...state.config.env } : process.env,
@@ -51,7 +51,7 @@ function defaultLoggingForProcess(processName) {
51
51
  sinks: ["ndjson"],
52
52
  };
53
53
  }
54
- if (processName === "bluebubbles" || processName === "mail" || processName === "voice") {
54
+ if (processName === "bluebubbles" || processName === "mail" || processName === "voice" || processName === "a2a") {
55
55
  return {
56
56
  level: "warn",
57
57
  sinks: ["terminal", "ndjson"],
@@ -44,6 +44,7 @@ const provider_credentials_1 = require("../provider-credentials");
44
44
  const sense_truth_1 = require("../sense-truth");
45
45
  const machine_identity_1 = require("../machine-identity");
46
46
  const process_manager_1 = require("./process-manager");
47
+ const config_1 = require("../../a2a/config");
47
48
  const DEFAULT_TEAMS_PORT = 3978;
48
49
  const DEFAULT_BLUEBUBBLES_PORT = 18790;
49
50
  const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
@@ -55,6 +56,7 @@ function defaultSenses() {
55
56
  bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
56
57
  mail: { ...identity_1.DEFAULT_AGENT_SENSES.mail },
57
58
  voice: { ...identity_1.DEFAULT_AGENT_SENSES.voice },
59
+ a2a: { ...identity_1.DEFAULT_AGENT_SENSES.a2a },
58
60
  };
59
61
  }
60
62
  function readAgentSenses(agentJsonPath) {
@@ -80,7 +82,7 @@ function readAgentSenses(agentJsonPath) {
80
82
  if (!rawSenses || typeof rawSenses !== "object" || Array.isArray(rawSenses)) {
81
83
  return defaults;
82
84
  }
83
- for (const sense of ["cli", "teams", "bluebubbles", "mail", "voice"]) {
85
+ for (const sense of ["cli", "teams", "bluebubbles", "mail", "voice", "a2a"]) {
84
86
  const rawSense = rawSenses[sense];
85
87
  if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
86
88
  continue;
@@ -103,6 +105,25 @@ function numberField(record, key, fallback) {
103
105
  const value = record?.[key];
104
106
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
105
107
  }
108
+ function a2aMachineRuntimeConfig(agent) {
109
+ const runtimeConfig = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
110
+ const payload = runtimeConfig.ok ? runtimeConfig.config : {};
111
+ const a2a = payload.a2a;
112
+ return a2a && typeof a2a === "object" && !Array.isArray(a2a) ? a2a : undefined;
113
+ }
114
+ function managedA2AArgs(agent) {
115
+ const a2a = a2aMachineRuntimeConfig(agent);
116
+ const args = ["--port", String(numberField(a2a, "port", (0, config_1.defaultA2APort)(agent)))];
117
+ const host = textField(a2a, "host");
118
+ const endpointPath = (0, config_1.normalizeA2APath)(textField(a2a, "path") || config_1.A2A_DEFAULT_PATH);
119
+ const publicUrl = textField(a2a, "publicUrl");
120
+ if (host)
121
+ args.push("--host", host);
122
+ args.push("--path", endpointPath);
123
+ if (publicUrl)
124
+ args.push("--base-url", publicUrl);
125
+ return args;
126
+ }
106
127
  function compactRuntimeConfigError(agent, error) {
107
128
  const compact = error.replace(/\s+/g, " ").trim();
108
129
  if (/credential vault is locked|vault locked|vault is locked/i.test(compact)) {
@@ -125,6 +146,7 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
125
146
  bluebubbles: { configured: false, detail: "not enabled in agent.json" },
126
147
  mail: { configured: false, detail: "not enabled in agent.json" },
127
148
  voice: { configured: false, detail: "not enabled in agent.json" },
149
+ a2a: { configured: false, detail: "not enabled in agent.json" },
128
150
  };
129
151
  const payload = runtimeConfig.ok ? runtimeConfig.config : {};
130
152
  const unavailableDetail = runtimeConfigUnavailableDetail(agent, runtimeConfig);
@@ -136,6 +158,7 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
136
158
  const mailroom = payload.mailroom;
137
159
  const integrations = payload.integrations;
138
160
  const voice = machinePayload.voice;
161
+ const a2a = machinePayload.a2a;
139
162
  if (senses.teams.enabled) {
140
163
  const missing = [];
141
164
  if (!textField(teams, "clientId"))
@@ -268,6 +291,16 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
268
291
  : runtimeConfigUnavailableDetail(agent, machineRuntimeConfig),
269
292
  };
270
293
  }
294
+ if (senses.a2a.enabled) {
295
+ const port = numberField(a2a, "port", (0, config_1.defaultA2APort)(agent));
296
+ const endpointPath = (0, config_1.normalizeA2APath)(textField(a2a, "path") || config_1.A2A_DEFAULT_PATH);
297
+ const publicUrl = textField(a2a, "publicUrl");
298
+ base.a2a = {
299
+ configured: true,
300
+ /* v8 ignore next -- listSenseRows tests cover the public URL; daemon defaults cover the local port in live startup smoke @preserve */
301
+ detail: publicUrl ? `${publicUrl}${endpointPath}` : `:${port} ${endpointPath}`,
302
+ };
303
+ }
271
304
  return base;
272
305
  }
273
306
  function senseRepairHint(agent, sense) {
@@ -280,6 +313,10 @@ function senseRepairHint(agent, sense) {
280
313
  if (sense === "voice") {
281
314
  return `Agent-runnable: run 'ouro connect voice --agent ${agent}' for config guidance; use voice.twilioConversationEngine=openai-sip with voice.openaiRealtimeApiKey, voice.openaiSipProjectId, and voice.openaiSipWebhookSecret for preferred SIP phone voice; use openai-realtime for Media Streams fallback, or save ElevenLabs and local Whisper.cpp settings for cascade fallback; then run 'ouro up' again.`;
282
315
  }
316
+ /* v8 ignore next -- A2A currently has no credential-gated not-configured state; kept for future repair copy symmetry @preserve */
317
+ if (sense === "a2a") {
318
+ return `Agent-runnable: run 'ouro connect a2a --agent ${agent}', then restart with 'ouro up'.`;
319
+ }
283
320
  return `Run 'ouro connect bluebubbles --agent ${agent}' to attach BlueBubbles on this machine; then run 'ouro up' again.`;
284
321
  }
285
322
  function currentMachineId() {
@@ -290,7 +327,7 @@ function parseSenseSnapshotName(name) {
290
327
  if (parts.length !== 2)
291
328
  return null;
292
329
  const [agent, sense] = parts;
293
- if (sense !== "teams" && sense !== "bluebubbles" && sense !== "mail" && sense !== "voice")
330
+ if (sense !== "teams" && sense !== "bluebubbles" && sense !== "mail" && sense !== "voice" && sense !== "a2a")
294
331
  return null;
295
332
  return { agent, sense };
296
333
  }
@@ -306,12 +343,14 @@ function managedSenseEntry(sense) {
306
343
  return "senses/bluebubbles/entry.js";
307
344
  if (sense === "voice")
308
345
  return "senses/voice-entry.js";
346
+ if (sense === "a2a")
347
+ return "senses/a2a-entry.js";
309
348
  return "senses/mail-entry.js";
310
349
  }
311
350
  function runtimeCredentialBootstrapFor(agent, sense) {
312
351
  const runtime = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent);
313
- const machineId = sense === "bluebubbles" || sense === "voice" ? currentMachineId() : undefined;
314
- const machine = sense === "bluebubbles" || sense === "voice" ? (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent) : null;
352
+ const machineId = sense === "bluebubbles" || sense === "voice" || sense === "a2a" ? currentMachineId() : undefined;
353
+ const machine = sense === "bluebubbles" || sense === "voice" || sense === "a2a" ? (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent) : null;
315
354
  const providerPool = (0, provider_credentials_1.readProviderCredentialPool)(agent);
316
355
  const providerCredentialRecords = providerPool.ok
317
356
  ? Object.values(providerPool.pool.providers).filter((record) => !!record)
@@ -478,7 +517,7 @@ class DaemonSenseManager {
478
517
  return [agent, { senses, facts }];
479
518
  }));
480
519
  const managedSenseAgents = [...this.contexts.entries()].flatMap(([agent, context]) => {
481
- return ["teams", "bluebubbles", "mail", "voice"]
520
+ return ["teams", "bluebubbles", "mail", "voice", "a2a"]
482
521
  .filter((sense) => context.senses[sense].enabled)
483
522
  .map((sense) => ({
484
523
  name: `${agent}:${sense}`,
@@ -486,6 +525,7 @@ class DaemonSenseManager {
486
525
  entry: managedSenseEntry(sense),
487
526
  channel: sense,
488
527
  autoStart: true,
528
+ ...(sense === "a2a" ? { args: managedA2AArgs(agent), getArgs: () => managedA2AArgs(agent) } : {}),
489
529
  getRuntimeCredentialBootstrap: () => runtimeCredentialBootstrapFor(agent, sense),
490
530
  }));
491
531
  });
@@ -537,7 +577,7 @@ class DaemonSenseManager {
537
577
  async refreshSenseConfigAndRetry(name, parsed) {
538
578
  try {
539
579
  const refreshed = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
540
- const machineRefreshed = parsed.sense === "bluebubbles" || parsed.sense === "voice"
580
+ const machineRefreshed = parsed.sense === "bluebubbles" || parsed.sense === "voice" || parsed.sense === "a2a"
541
581
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
542
582
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
543
583
  const context = this.contexts.get(parsed.agent);
@@ -576,14 +616,14 @@ class DaemonSenseManager {
576
616
  }
577
617
  async refreshEnabledSenseConfigs() {
578
618
  const refreshes = [...this.contexts.entries()].map(async ([agent, context]) => {
579
- const enabledManagedSenses = ["teams", "bluebubbles", "mail", "voice"]
619
+ const enabledManagedSenses = ["teams", "bluebubbles", "mail", "voice", "a2a"]
580
620
  .filter((sense) => context.senses[sense].enabled);
581
621
  /* v8 ignore next -- periodic refresh work only exists when a managed background sense is enabled @preserve */
582
622
  if (enabledManagedSenses.length === 0)
583
623
  return;
584
624
  /* v8 ignore start -- periodic freshness refresh uses the same runtime readers covered by startup integration tests @preserve */
585
625
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
586
- const needsMachineConfig = enabledManagedSenses.some((sense) => sense === "bluebubbles" || sense === "voice");
626
+ const needsMachineConfig = enabledManagedSenses.some((sense) => sense === "bluebubbles" || sense === "voice" || sense === "a2a");
587
627
  const machineRuntimeConfig = needsMachineConfig
588
628
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(), { preserveCachedOnFailure: true })
589
629
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
@@ -597,17 +637,29 @@ class DaemonSenseManager {
597
637
  await this.processManager.startAutoStartAgents();
598
638
  }
599
639
  triggerAutoStartSenses() {
600
- if (this.processManager.triggerAutoStartAgents) {
601
- this.processManager.triggerAutoStartAgents();
602
- return;
603
- }
604
- void this.processManager.startAutoStartAgents().catch((error) => {
640
+ void this.refreshEnabledSenseConfigs()
641
+ .catch((error) => {
605
642
  (0, runtime_1.emitNervesEvent)({
606
643
  level: "error",
607
644
  component: "channels",
608
645
  event: "channel.daemon_sense_autostart_error",
609
- message: "sense autostart failed",
610
- meta: { error: error instanceof Error ? error.message : String(error) },
646
+ message: "sense config refresh failed",
647
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error refresh rejection @preserve */ String(error) },
648
+ });
649
+ })
650
+ .then(() => {
651
+ if (this.processManager.triggerAutoStartAgents) {
652
+ this.processManager.triggerAutoStartAgents();
653
+ return;
654
+ }
655
+ void this.processManager.startAutoStartAgents().catch((error) => {
656
+ (0, runtime_1.emitNervesEvent)({
657
+ level: "error",
658
+ component: "channels",
659
+ event: "channel.daemon_sense_autostart_error",
660
+ message: "sense autostart failed",
661
+ meta: { error: error instanceof Error ? error.message : String(error) },
662
+ });
611
663
  });
612
664
  });
613
665
  }
@@ -712,6 +764,10 @@ class DaemonSenseManager {
712
764
  optional: context.facts.voice.optional,
713
765
  ...(runtime.get(agent)?.voice ?? {}),
714
766
  },
767
+ a2a: {
768
+ configured: context.facts.a2a.configured,
769
+ ...(runtime.get(agent)?.a2a ?? {}),
770
+ },
715
771
  };
716
772
  const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
717
773
  return inventory.map((entry) => ({