@ouro.bot/cli 0.1.0-alpha.659 → 0.1.0-alpha.660

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,13 @@
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.660",
6
+ "changes": [
7
+ "Add `ouro mcp-serve --workbench-mcp [<path>]` so Ouro Workbench injects its `ouro_workbench` MCP into the boss agent's turn at runtime — merged per-turn and per-agent in the daemon with no cross-agent leak — instead of writing a machine-specific entry into the git-synced agent bundle.",
8
+ "Drop the Workbench MCP bundle-write from the boss path (the explicit `ouro connect workbench` opt-in escape hatch is unchanged) and update the agent self-model copy so the boss understands it receives `ouro_workbench` via runtime injection rather than treating the missing bundle entry as a blocked or trust-level state."
9
+ ]
10
+ },
4
11
  {
5
12
  "version": "0.1.0-alpha.659",
6
13
  "changes": [
@@ -46,6 +46,7 @@ exports.mergeStartupStability = mergeStartupStability;
46
46
  exports.ensureDaemonRunning = ensureDaemonRunning;
47
47
  exports.listGithubCopilotModels = listGithubCopilotModels;
48
48
  exports.checkManualCloneBundles = checkManualCloneBundles;
49
+ exports.resolveWorkbenchRuntimeMcp = resolveWorkbenchRuntimeMcp;
49
50
  exports.resolveMailImportFilePath = resolveMailImportFilePath;
50
51
  exports.runOuroCli = runOuroCli;
51
52
  const child_process_1 = require("child_process");
@@ -2540,6 +2541,28 @@ function findInstalledWorkbenchMcp(deps, preferred) {
2540
2541
  ];
2541
2542
  return candidates.find((candidate) => cliPathExists(deps, candidate)) ?? null;
2542
2543
  }
2544
+ /**
2545
+ * Resolve the `--workbench-mcp [<path>]` flag into a per-turn runtime MCP
2546
+ * override for the `ouro mcp-serve` bridge, or null when the flag is absent /
2547
+ * unresolvable.
2548
+ *
2549
+ * - `undefined` (flag not passed) → null (no runtime injection).
2550
+ * - `true` (bare opt-in) → self-discover the installed OuroWorkbenchMCP.
2551
+ * - a string path → use it if it exists, otherwise fall back to discovery
2552
+ * (treating the string as the preferred candidate).
2553
+ *
2554
+ * Returns `{ ouro_workbench: { command, args: [] } }` so the daemon merges it
2555
+ * into the boss agent's toolset for the turn without writing to agent.json.
2556
+ */
2557
+ function resolveWorkbenchRuntimeMcp(flag, deps) {
2558
+ if (flag === undefined)
2559
+ return null;
2560
+ const preferred = typeof flag === "string" ? flag : null;
2561
+ const command = findInstalledWorkbenchMcp(deps, preferred);
2562
+ if (!command)
2563
+ return null;
2564
+ return { ouro_workbench: { command, args: [] } };
2565
+ }
2543
2566
  function writeWorkbenchMcpRegistration(agent, executablePath, deps) {
2544
2567
  const { configPath } = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot);
2545
2568
  enableAgentSense(agent, "workbench", deps);
@@ -2758,6 +2781,14 @@ async function buildConnectMenu(agent, deps, onProgress) {
2758
2781
  installedWorkbenchMcp
2759
2782
  ? `OuroWorkbenchMCP found: ${installedWorkbenchMcp}`
2760
2783
  : "OuroWorkbenchMCP not found in ~/Applications or /Applications",
2784
+ // Runtime injection is the boss's normal path: the Workbench app launches
2785
+ // its boss with `ouro mcp-serve --workbench-mcp`, which injects the
2786
+ // ouro_workbench MCP per-turn without any agent.json entry. A blank/“not
2787
+ // registered” bundle state above is expected for a runtime-only boss; this
2788
+ // `connect workbench` command only writes a persistent entry as an opt-in.
2789
+ installedWorkbenchMcp
2790
+ ? "boss runtime injection: when the Workbench app launches this agent it injects ouro_workbench per-turn; an agent.json entry is not required (this command writes one as an opt-in escape hatch)."
2791
+ : "boss runtime injection: install Ouro Workbench.app so the app can inject ouro_workbench per-turn when it launches this agent as boss (no agent.json entry required).",
2761
2792
  ];
2762
2793
  const entries = [
2763
2794
  {
@@ -6674,19 +6705,28 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6674
6705
  const { createMcpServer } = await Promise.resolve().then(() => __importStar(require("../mcp/mcp-server")));
6675
6706
  const friendId = command.friendId ?? `local-${os.userInfo().username}`;
6676
6707
  const mcpSocketPath = command.socketOverride ?? deps.socketPath;
6708
+ // Resolve the optional --workbench-mcp flag into a concrete runtime MCP
6709
+ // override. A string is taken as the explicit binary path (falling back to
6710
+ // discovery if it does not exist); a boolean opt-in (true) self-discovers
6711
+ // the installed OuroWorkbenchMCP. The resolved override is forwarded by the
6712
+ // bridge on every senseTurn and merged per-turn in the daemon — nothing is
6713
+ // written to agent.json.
6714
+ const workbenchMcpFlag = command.workbenchMcp;
6715
+ const runtimeMcp = resolveWorkbenchRuntimeMcp(workbenchMcpFlag, deps);
6677
6716
  const server = createMcpServer({
6678
6717
  agent: command.agent,
6679
6718
  friendId,
6680
6719
  socketPath: mcpSocketPath,
6681
6720
  stdin: process.stdin,
6682
6721
  stdout: process.stdout,
6722
+ ...(runtimeMcp ? { runtimeMcp } : {}),
6683
6723
  });
6684
6724
  server.start();
6685
6725
  (0, runtime_1.emitNervesEvent)({
6686
6726
  component: "daemon",
6687
6727
  event: "daemon.mcp_serve_started",
6688
6728
  message: "MCP server started via CLI",
6689
- meta: { agent: command.agent, friendId },
6729
+ meta: { agent: command.agent, friendId, workbenchRuntimeMcp: !!runtimeMcp },
6690
6730
  });
6691
6731
  // Keep process alive until stdin closes
6692
6732
  await new Promise((resolve) => {
@@ -1293,6 +1293,11 @@ function parseMcpServeCommand(args) {
1293
1293
  let agent;
1294
1294
  let friendId;
1295
1295
  let socketOverride;
1296
+ // Optional runtime Workbench MCP injection. The path is OPTIONAL: when a
1297
+ // concrete path follows the flag it is carried verbatim; when the flag stands
1298
+ // alone (or is immediately followed by another `--flag`) it is a boolean
1299
+ // opt-in and the daemon self-discovers the installed OuroWorkbenchMCP binary.
1300
+ let workbenchMcp;
1296
1301
  for (let i = 0; i < args.length; i++) {
1297
1302
  if (args[i] === "--agent" && args[i + 1]) {
1298
1303
  agent = args[++i];
@@ -1306,10 +1311,26 @@ function parseMcpServeCommand(args) {
1306
1311
  socketOverride = args[++i];
1307
1312
  continue;
1308
1313
  }
1314
+ if (args[i] === "--workbench-mcp") {
1315
+ const next = args[i + 1];
1316
+ if (next && !next.startsWith("--")) {
1317
+ workbenchMcp = args[++i];
1318
+ }
1319
+ else {
1320
+ workbenchMcp = true;
1321
+ }
1322
+ continue;
1323
+ }
1309
1324
  }
1310
1325
  if (!agent)
1311
1326
  throw new Error("mcp-serve requires --agent <name>");
1312
- return { kind: "mcp-serve", agent, ...(friendId ? { friendId } : {}), ...(socketOverride ? { socketOverride } : {}) };
1327
+ return {
1328
+ kind: "mcp-serve",
1329
+ agent,
1330
+ ...(friendId ? { friendId } : {}),
1331
+ ...(socketOverride ? { socketOverride } : {}),
1332
+ ...(workbenchMcp !== undefined ? { workbenchMcp } : {}),
1333
+ };
1313
1334
  }
1314
1335
  function parseSetupCommand(args) {
1315
1336
  let tool;
@@ -472,6 +472,10 @@ async function handleAgentSenseTurn(command, runtime) {
472
472
  friendId: command.friendId,
473
473
  userMessage: command.message,
474
474
  ...(runtime?.socketPath ? { toolContext: { daemonSocketPath: runtime.socketPath } } : {}),
475
+ // Per-turn, per-agent runtime MCP injection (e.g. Workbench's ouro_workbench).
476
+ // Scoped to THIS turn only — never stored as module state, so a concurrent
477
+ // turn for a different agent cannot inherit these servers.
478
+ ...(command.runtimeMcp ? { runtimeMcpServers: command.runtimeMcp } : {}),
475
479
  });
476
480
  return {
477
481
  ok: true,
@@ -165,6 +165,7 @@ function canonicalizeMcpFriendId(agent, rawFriendId) {
165
165
  function createMcpServer(options) {
166
166
  const { agent, socketPath, stdin, stdout } = options;
167
167
  const rawFriendId = options.friendId;
168
+ const runtimeMcp = options.runtimeMcp;
168
169
  const friendId = canonicalizeMcpFriendId(agent, rawFriendId);
169
170
  let buffer = "";
170
171
  let running = false;
@@ -374,6 +375,9 @@ function createMcpServer(options) {
374
375
  channel: "mcp",
375
376
  sessionKey: sessionId,
376
377
  message,
378
+ // Forward the resolved runtime MCP override (if any) so the daemon merges
379
+ // it into this agent's toolset for the turn. Per-turn parameter data only.
380
+ ...(runtimeMcp ? { runtimeMcp } : {}),
377
381
  });
378
382
  /* v8 ignore next -- branch: ?? fallback for empty daemon response @preserve */
379
383
  const text = response.message ?? "(empty response)";
@@ -213,7 +213,15 @@ function readSenseStatusLines() {
213
213
  },
214
214
  {
215
215
  label: "Workbench",
216
- status: !senses.workbench.enabled ? "disabled" : configured.workbench ? "ready" : "needs_config",
216
+ // Workbench can be injected at runtime (the Workbench app spawns
217
+ // `ouro mcp-serve --workbench-mcp` for its boss) with NO agent.json entry.
218
+ // A disabled/needs_config row here does NOT mean the tools are absent —
219
+ // the authoritative signal is whether `workbench_*` tools are in the
220
+ // toolset this turn. The annotation prevents the agent from misreading the
221
+ // row as "blocked".
222
+ status: !senses.workbench.enabled
223
+ ? "disabled (runtime-injected when launched by Workbench app)"
224
+ : configured.workbench ? "ready" : "needs_config",
217
225
  },
218
226
  ];
219
227
  return rows.map((row) => `- ${row.label}: ${row.status}`);
@@ -492,7 +492,15 @@ function localSenseStatusLines() {
492
492
  },
493
493
  {
494
494
  label: "Workbench",
495
- status: !senses.workbench.enabled ? "disabled" : configured.workbench ? "ready" : "needs_config",
495
+ // Workbench can be injected at runtime (the Workbench app spawns
496
+ // `ouro mcp-serve --workbench-mcp` for its boss) with NO agent.json entry.
497
+ // A disabled/needs_config row here does NOT mean the tools are absent —
498
+ // the authoritative signal is whether `workbench_*` tools are in the
499
+ // toolset this turn. The annotation prevents the agent from misreading the
500
+ // row as "blocked".
501
+ status: !senses.workbench.enabled
502
+ ? "disabled (runtime-injected when launched by Workbench app)"
503
+ : configured.workbench ? "ready" : "needs_config",
496
504
  },
497
505
  ];
498
506
  return rows.map((row) => `- ${row.label}: ${row.status}`);
@@ -512,7 +520,7 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
512
520
  lines.push("teams setup truth: run `ouro connect teams --agent <agent>` from the connect bay; it stores Teams runtime/config fields and enables `senses.teams.enabled`.");
513
521
  lines.push("bluebubbles setup truth: run `ouro connect bluebubbles --agent <agent>` from the connect bay; it stores this machine's BlueBubbles URL/password/listener config in the agent vault machine runtime item.");
514
522
  lines.push("a2a setup truth: run `ouro connect a2a --agent <agent>` to enable the A2A sense, `ouro a2a card --agent <agent> --base-url <public-url>` to publish an agent card, and `ouro a2a onboard --agent <agent> --card-url <peer-card-url>` to add a peer as an agent friend. A2A uses the existing friend trust model, not a separate trust registry.");
515
- lines.push("workbench setup truth: Ouro Workbench is the local machine sense for terminal/TUI agents. Enabling it means `agent.json` has `senses.workbench.enabled=true` and an `mcpServers.ouro_workbench` entry pointing at the installed `OuroWorkbenchMCP` executable. The boss observes and queues auditable Workbench actions through `workbench_status`, `workbench_sense`, `workbench_transcript_tail`, `workbench_search_transcripts`, `workbench_recovery_drill`, and `workbench_request_action`; raw provider secrets remain in the agent vault, and Apple notarization is unrelated to local use.");
523
+ lines.push("workbench setup truth: Ouro Workbench is the local machine sense for terminal/TUI agents. When the Workbench app launches me as its boss it injects the `ouro_workbench` MCP into my turn at runtime (it spawns `ouro mcp-serve --agent <me> --workbench-mcp`), so I receive the tools for the served session without any `agent.json` `mcpServers.ouro_workbench` entry nothing is written to the bundle, so this stays path-free and cross-machine clean. The authoritative signal that Workbench is active is simply that the `workbench_*` tools are present in my toolset this turn; the sense table or `agent.json` may still read as disabled/needs_config because no bundle entry exists, and that is expected under runtime injection — I do NOT treat that as blocked or as a trust-level problem. I observe and queue auditable Workbench actions through `workbench_status`, `workbench_sense`, `workbench_transcript_tail`, `workbench_search_transcripts`, `workbench_recovery_drill`, and `workbench_request_action`; raw provider secrets remain in the agent vault, and Apple notarization is unrelated to local use. The explicit `ouro connect workbench --agent <me>` command still writes a persistent bundle entry as an opt-in escape hatch, but the boss does not need it.");
516
524
  lines.push("mail setup AX: if a human asks me to set up email, I do not hand them a terminal checklist. I guide the flow end-to-end: name the current phase, run agent-runnable commands myself with shell/tools when available, ask the human only for human-required facts or browser actions, wait for their reply, verify the result, then continue.");
517
525
  lines.push("mail setup hard rule: never tell the human to run `ouro account ensure`, `ouro connect mail`, `ouro mail import-mbox`, `ouro status`, or `ouro doctor` for setup. Say what I am about to run, run it myself, and report the result. If my current surface cannot run shell/tools, I ask for a tool-capable Ouro setup session or companion to continue; I do not offload CLI operation to the human.");
518
526
  lines.push("mail setup truth: Agent Mail uses Mailroom, not HEY OAuth/IMAP. For the full work substrate account, the agent-runnable command is `ouro account ensure --agent <agent> --owner-email <email> --source hey`; use `ouro connect mail --agent <agent> --owner-email <email> --source hey` for mail-only repair/provisioning, or `--no-delegated-source` for native-only mail. The detailed runbook is `docs/agent-mail-setup.md`.");
@@ -21,8 +21,17 @@ const RESTART_DELAY_MS = 1000;
21
21
  * (re-read on each turn). Both code paths MUST use the same merge logic — if
22
22
  * reconcile reads only builtin, plugin servers get classified as "removed"
23
23
  * on the second turn and torn down. See alpha.635 fix.
24
+ *
25
+ * `runtimeServers` are per-turn, per-agent overrides (e.g. Workbench's
26
+ * `ouro_workbench`) supplied as PARAMETER data for the current turn — never read
27
+ * from module state. They merge with the HIGHEST precedence (after builtin), so
28
+ * a stale `agent.json` entry loses to the live runtime path. Because they are a
29
+ * parameter, a turn that omits them produces a merged set WITHOUT them, and
30
+ * `reconcile()` then tears the runtime server down — this is the no-leak
31
+ * invariant that keeps the runtime MCP from bleeding into a different agent's
32
+ * concurrent turn on the shared daemon.
24
33
  */
25
- function buildMergedServerConfig() {
34
+ function buildMergedServerConfig(runtimeServers) {
26
35
  const config = (0, identity_1.loadAgentConfig)();
27
36
  const builtinServers = config.mcpServers ?? {};
28
37
  const pluginServers = (0, plugin_mcp_1.listPluginMcpServers)();
@@ -37,6 +46,15 @@ function buildMergedServerConfig() {
37
46
  for (const [name, cfg] of Object.entries(builtinServers)) {
38
47
  mergedServers[name] = cfg;
39
48
  }
49
+ // Runtime overrides win over both plugin and builtin (highest precedence).
50
+ // They are NOT recorded in pluginOrigins, so they surface as builtin-style
51
+ // (un-namespaced) tools — matching how an agent.json mcpServers entry would.
52
+ if (runtimeServers) {
53
+ for (const [name, cfg] of Object.entries(runtimeServers)) {
54
+ mergedServers[name] = cfg;
55
+ delete pluginOrigins[name];
56
+ }
57
+ }
40
58
  return { mergedServers, pluginOrigins };
41
59
  }
42
60
  class McpManager {
@@ -119,14 +137,19 @@ class McpManager {
119
137
  }
120
138
  return results;
121
139
  }
122
- /* v8 ignore start — reconcile: dynamic MCP server management, tested via integration @preserve */
123
140
  /** Re-read agent config AND enabled-plugin .mcp.json files, then connect new
124
141
  * servers / disconnect removed ones. Must include plugin-declared servers
125
142
  * in the desired set — otherwise plugin servers (e.g. mcp__desk__*) are
126
- * treated as "removed" on every call and get torn down between turns. */
127
- async reconcile() {
143
+ * treated as "removed" on every call and get torn down between turns.
144
+ *
145
+ * `runtimeServers` are the current turn's per-agent overrides. They MUST be
146
+ * passed on every reconcile for the agent that owns them, otherwise the
147
+ * runtime server (e.g. ouro_workbench) is classified as "removed" and torn
148
+ * down — which is exactly the desired no-leak behavior for a turn that omits
149
+ * them. */
150
+ async reconcile(runtimeServers) {
128
151
  try {
129
- const { mergedServers, pluginOrigins } = buildMergedServerConfig();
152
+ const { mergedServers, pluginOrigins } = buildMergedServerConfig(runtimeServers);
130
153
  const currentNames = new Set(this.servers.keys());
131
154
  const desiredNames = new Set(Object.keys(mergedServers));
132
155
  // Connect new servers
@@ -151,6 +174,7 @@ class McpManager {
151
174
  meta: { server: name },
152
175
  });
153
176
  const entry = this.servers.get(name);
177
+ /* v8 ignore next -- defensive: name comes from this.servers.keys() this same tick, so entry is always present; the guard only protects against an awaited connectServer crash-handler racing a delete @preserve */
154
178
  if (entry)
155
179
  entry.client.shutdown();
156
180
  this.servers.delete(name);
@@ -167,7 +191,6 @@ class McpManager {
167
191
  });
168
192
  }
169
193
  }
170
- /* v8 ignore stop */
171
194
  shutdown() {
172
195
  this.shuttingDown = true;
173
196
  // `_end` (not `_stop`) to pair with `mcp.manager_start` under the
@@ -346,22 +369,30 @@ let _sharedManagerPromise = null;
346
369
  * Get or create a shared McpManager instance from the agent's config.
347
370
  * Returns null if no mcpServers are configured.
348
371
  * Safe to call from multiple senses — will only create one instance.
372
+ *
373
+ * `options.runtimeServers` are the current turn's per-agent MCP overrides (e.g.
374
+ * Workbench's `ouro_workbench`). They are PARAMETER data for this call only —
375
+ * passed into the merge for both the initial `start()` and every subsequent
376
+ * `reconcile()`, and never persisted as module state. A call that omits them
377
+ * reconciles to a set WITHOUT them, tearing any prior runtime server down — the
378
+ * no-leak invariant for the shared multi-agent daemon.
349
379
  */
350
- async function getSharedMcpManager() {
380
+ async function getSharedMcpManager(options) {
381
+ const runtimeServers = options?.runtimeServers;
351
382
  // If manager exists, reconcile to pick up config changes (new/removed servers)
352
- /* v8 ignore start reconcile on existing manager @preserve */
383
+ // AND this turn's runtime overrides. Passing runtimeServers per-call is what
384
+ // scopes the runtime MCP to the active agent's turn.
353
385
  if (_sharedManager) {
354
- await _sharedManager.reconcile();
386
+ await _sharedManager.reconcile(runtimeServers);
355
387
  return _sharedManager;
356
388
  }
357
- /* v8 ignore stop */
358
389
  /* v8 ignore next -- race guard: deduplicates concurrent initialization calls @preserve */
359
390
  if (_sharedManagerPromise)
360
391
  return _sharedManagerPromise;
361
392
  // Always re-check config — agent may have added servers since last call
362
393
  _sharedManagerPromise = (async () => {
363
394
  try {
364
- const { mergedServers, pluginOrigins } = buildMergedServerConfig();
395
+ const { mergedServers, pluginOrigins } = buildMergedServerConfig(runtimeServers);
365
396
  if (Object.keys(mergedServers).length === 0)
366
397
  return null;
367
398
  const manager = new McpManager();
@@ -230,8 +230,9 @@ async function runSenseTurn(options) {
230
230
  resolverParams = { provider: "local", externalId: username, displayName: username, channel };
231
231
  }
232
232
  const resolver = new resolver_1.FriendResolver(friendStore, resolverParams);
233
- // Initialize MCP manager so MCP tools appear as first-class tools in the agent's tool list
234
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
233
+ // Initialize MCP manager so MCP tools appear as first-class tools in the agent's tool list.
234
+ // Runtime MCP servers (e.g. Workbench's ouro_workbench) are passed per-turn for THIS agent only.
235
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(options.runtimeMcpServers ? { runtimeServers: options.runtimeMcpServers } : undefined) ?? undefined;
235
236
  // Session path and loading
236
237
  const sessionDir = path.join(agentRoot, "state", "sessions", friendId, channel);
237
238
  fs.mkdirSync(sessionDir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.659",
3
+ "version": "0.1.0-alpha.660",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",