@ouro.bot/cli 0.1.0-alpha.804 → 0.1.0-alpha.806

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.
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FrontendFrameWriter = void 0;
37
+ exports.frontendSocketPathForDaemon = frontendSocketPathForDaemon;
38
+ exports.startFrontendSocketServer = startFrontendSocketServer;
39
+ const node_crypto_1 = require("node:crypto");
40
+ const fs = __importStar(require("node:fs"));
41
+ const net = __importStar(require("node:net"));
42
+ const path = __importStar(require("node:path"));
43
+ const readline = __importStar(require("node:readline"));
44
+ const runtime_1 = require("../nerves/runtime");
45
+ const PROTOCOL_VERSION = 1;
46
+ const MAX_SOCKET_PATH_BYTES = 100;
47
+ function frontendSocketPathForDaemon(commandSocketPath) {
48
+ const candidate = `${commandSocketPath}.frontend`;
49
+ if (Buffer.byteLength(candidate) <= MAX_SOCKET_PATH_BYTES)
50
+ return candidate;
51
+ const digest = (0, node_crypto_1.createHash)("sha256").update(path.resolve(commandSocketPath)).digest("hex").slice(0, 16);
52
+ return `/tmp/ouro-frontend-${digest}.sock`;
53
+ }
54
+ class FrontendFrameWriter {
55
+ socket;
56
+ maxQueuedFrames;
57
+ onReplayRequired;
58
+ queued = [];
59
+ backpressured = false;
60
+ closeAfterDrain = false;
61
+ overflowed = false;
62
+ lastSequence = 0;
63
+ constructor(socket, maxQueuedFrames, onReplayRequired) {
64
+ if (!Number.isSafeInteger(maxQueuedFrames) || maxQueuedFrames < 1) {
65
+ throw new Error("maxQueuedFrames must be a positive integer");
66
+ }
67
+ this.socket = socket;
68
+ this.maxQueuedFrames = maxQueuedFrames;
69
+ this.onReplayRequired = onReplayRequired;
70
+ }
71
+ send(frame) {
72
+ const sequence = typeof frame.sequence === "number" ? frame.sequence : null;
73
+ const encoded = `${JSON.stringify(frame)}\n`;
74
+ if (this.backpressured) {
75
+ if (this.closeAfterDrain)
76
+ return;
77
+ if (this.queued.length >= this.maxQueuedFrames) {
78
+ this.queued.splice(0, this.queued.length, `${JSON.stringify({
79
+ protocolVersion: PROTOCOL_VERSION,
80
+ event: "replay_required",
81
+ lastSequence: this.lastSequence,
82
+ })}\n`);
83
+ this.closeAfterDrain = true;
84
+ this.overflowed = true;
85
+ this.onReplayRequired();
86
+ return;
87
+ }
88
+ this.queued.push(encoded);
89
+ return;
90
+ }
91
+ if (sequence !== null)
92
+ this.lastSequence = sequence;
93
+ if (!this.socket.write(encoded)) {
94
+ this.backpressured = true;
95
+ this.socket.once("drain", () => this.flush());
96
+ }
97
+ }
98
+ get replayRequired() {
99
+ return this.overflowed;
100
+ }
101
+ flush() {
102
+ this.backpressured = false;
103
+ while (this.queued.length > 0) {
104
+ const encoded = this.queued.shift();
105
+ if (!this.socket.write(encoded)) {
106
+ this.backpressured = true;
107
+ this.socket.once("drain", () => this.flush());
108
+ return;
109
+ }
110
+ }
111
+ if (this.closeAfterDrain)
112
+ this.socket.end();
113
+ }
114
+ }
115
+ exports.FrontendFrameWriter = FrontendFrameWriter;
116
+ function response(id, result) {
117
+ return { protocolVersion: PROTOCOL_VERSION, id, ok: true, result };
118
+ }
119
+ function errorResponse(id, code, message) {
120
+ return { protocolVersion: PROTOCOL_VERSION, id, ok: false, error: { code, message } };
121
+ }
122
+ function record(value) {
123
+ if (!value || typeof value !== "object" || Array.isArray(value))
124
+ throw new Error("params must be an object");
125
+ return value;
126
+ }
127
+ function requiredString(value, field) {
128
+ if (typeof value !== "string" || !value.trim())
129
+ throw new Error(`${field} must be a non-empty string`);
130
+ return value.trim();
131
+ }
132
+ function requiredExecutable(value, field) {
133
+ const executable = requiredString(value, field);
134
+ if (!path.isAbsolute(executable))
135
+ throw new Error(`${field} must be absolute`);
136
+ try {
137
+ if (!fs.statSync(executable).isFile())
138
+ throw new Error("not a file");
139
+ fs.accessSync(executable, fs.constants.X_OK);
140
+ }
141
+ catch {
142
+ throw new Error(`${field} must be executable`);
143
+ }
144
+ return executable;
145
+ }
146
+ function validatedRuntimeMcpEnvironment(value) {
147
+ if (value === undefined) {
148
+ throw new Error("runtimeMcpServers.ouro_workbench.env requires exactly BUN_BIN, CMUX_BUNDLED_CLI_PATH, CMUX_SOCKET_PATH, CMUX_SOCKET_CAPABILITY");
149
+ }
150
+ const env = record(value);
151
+ const keys = [
152
+ "BUN_BIN",
153
+ "CMUX_BUNDLED_CLI_PATH",
154
+ "CMUX_SOCKET_PATH",
155
+ "CMUX_SOCKET_CAPABILITY",
156
+ ];
157
+ if (Object.keys(env).length !== keys.length || Object.keys(env).some((key) => !keys.includes(key))) {
158
+ throw new Error(`runtimeMcpServers.ouro_workbench.env requires exactly ${keys.join(", ")}`);
159
+ }
160
+ const socketPath = requiredString(env.CMUX_SOCKET_PATH, "runtimeMcpServers.ouro_workbench.env.CMUX_SOCKET_PATH");
161
+ if (!path.isAbsolute(socketPath)) {
162
+ throw new Error("runtimeMcpServers.ouro_workbench.env.CMUX_SOCKET_PATH must be absolute");
163
+ }
164
+ try {
165
+ if (!fs.lstatSync(socketPath).isSocket())
166
+ throw new Error("not a socket");
167
+ }
168
+ catch {
169
+ throw new Error("runtimeMcpServers.ouro_workbench.env.CMUX_SOCKET_PATH must be a live Unix socket");
170
+ }
171
+ const capability = requiredString(env.CMUX_SOCKET_CAPABILITY, "runtimeMcpServers.ouro_workbench.env.CMUX_SOCKET_CAPABILITY");
172
+ if (capability.length > 2_048 || /\s/u.test(capability)) {
173
+ throw new Error("runtimeMcpServers.ouro_workbench.env.CMUX_SOCKET_CAPABILITY is invalid");
174
+ }
175
+ return {
176
+ BUN_BIN: requiredExecutable(env.BUN_BIN, "runtimeMcpServers.ouro_workbench.env.BUN_BIN"),
177
+ CMUX_BUNDLED_CLI_PATH: requiredExecutable(env.CMUX_BUNDLED_CLI_PATH, "runtimeMcpServers.ouro_workbench.env.CMUX_BUNDLED_CLI_PATH"),
178
+ CMUX_SOCKET_PATH: socketPath,
179
+ CMUX_SOCKET_CAPABILITY: capability,
180
+ };
181
+ }
182
+ function validatedRuntimeMcpServers(value) {
183
+ if (value === undefined)
184
+ return undefined;
185
+ const servers = record(value);
186
+ const names = Object.keys(servers);
187
+ if (names.length === 0)
188
+ return undefined;
189
+ if (names.length !== 1 || names[0] !== "ouro_workbench") {
190
+ throw new Error("runtimeMcpServers supports only ouro_workbench");
191
+ }
192
+ const config = record(servers.ouro_workbench);
193
+ if (Object.keys(config).some((key) => key !== "command" && key !== "args" && key !== "env")) {
194
+ throw new Error("runtimeMcpServers.ouro_workbench supports only command, args, and env");
195
+ }
196
+ const command = requiredExecutable(config.command, "runtimeMcpServers.ouro_workbench.command");
197
+ const args = config.args ?? [];
198
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string") || args.length > 0) {
199
+ throw new Error("runtimeMcpServers.ouro_workbench.args must be an empty string array");
200
+ }
201
+ return {
202
+ ouro_workbench: {
203
+ command,
204
+ args: [],
205
+ env: validatedRuntimeMcpEnvironment(config.env),
206
+ },
207
+ };
208
+ }
209
+ function removeSocket(socketPath) {
210
+ if (!fs.existsSync(socketPath))
211
+ return;
212
+ const stat = fs.lstatSync(socketPath);
213
+ if (!stat.isSocket())
214
+ throw new Error(`frontend socket path is not a socket: ${socketPath}`);
215
+ fs.unlinkSync(socketPath);
216
+ }
217
+ async function startFrontendSocketServer(options) {
218
+ const socketPath = options.socketPath;
219
+ const maxQueuedFrames = options.maxQueuedFrames ?? 64;
220
+ const clients = new Set();
221
+ const sequences = new Map();
222
+ let stopped = false;
223
+ removeSocket(socketPath);
224
+ function publish(sessionKey, event, data = {}) {
225
+ const sequence = (sequences.get(sessionKey) ?? 0) + 1;
226
+ sequences.set(sessionKey, sequence);
227
+ const frame = { protocolVersion: PROTOCOL_VERSION, event, sessionKey, sequence, ...data };
228
+ for (const client of clients) {
229
+ if (client.subscriptions.has(sessionKey))
230
+ client.writer.send(frame);
231
+ }
232
+ }
233
+ const unsubscribeFromService = options.service.subscribe((event) => {
234
+ publish(event.sessionKey, event.type.replaceAll("_", "."), {
235
+ turnId: event.turnId,
236
+ journalSequence: event.journalSequence,
237
+ ...event.data,
238
+ });
239
+ if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
240
+ for (const client of clients) {
241
+ client.ownedTurnIds.delete(event.turnId);
242
+ if (event.ephemeral)
243
+ client.subscriptions.delete(event.sessionKey);
244
+ }
245
+ if (event.ephemeral)
246
+ sequences.delete(event.sessionKey);
247
+ }
248
+ });
249
+ async function handleRequest(client, raw) {
250
+ const request = record(raw);
251
+ const id = typeof request.id === "string" ? request.id : null;
252
+ if (request.protocolVersion !== PROTOCOL_VERSION) {
253
+ client.writer.send(errorResponse(id, "unsupported_version", "protocolVersion must be 1"));
254
+ return;
255
+ }
256
+ const method = requiredString(request.method, "method");
257
+ const params = record(request.params ?? {});
258
+ if (method === "session.load") {
259
+ const result = options.service.loadSession({
260
+ agent: requiredString(params.agent, "agent"),
261
+ friendId: requiredString(params.friendId, "friendId"),
262
+ sessionKey: requiredString(params.sessionKey, "sessionKey"),
263
+ }, {
264
+ ...(params.afterSequence !== undefined ? { afterSequence: params.afterSequence } : {}),
265
+ ...(params.limit !== undefined ? { limit: params.limit } : {}),
266
+ });
267
+ client.writer.send(response(id, result));
268
+ return;
269
+ }
270
+ if (method === "session.subscribe" || method === "session.unsubscribe") {
271
+ const sessionKey = requiredString(params.sessionKey, "sessionKey");
272
+ if (method === "session.subscribe")
273
+ client.subscriptions.add(sessionKey);
274
+ else
275
+ client.subscriptions.delete(sessionKey);
276
+ client.writer.send(response(id, { subscribed: client.subscriptions.has(sessionKey) }));
277
+ return;
278
+ }
279
+ if (method === "turn.cancel") {
280
+ const turnId = requiredString(params.turnId, "turnId");
281
+ client.writer.send(response(id, { cancelled: options.service.cancelTurn(turnId) }));
282
+ return;
283
+ }
284
+ if (method === "permission.resolve") {
285
+ const requestId = requiredString(params.requestId, "requestId");
286
+ const optionId = requiredString(params.optionId, "optionId");
287
+ client.writer.send(response(id, { resolved: options.service.resolvePermission(requestId, optionId) }));
288
+ return;
289
+ }
290
+ if (method === "turn.start" || method === "turn.start.observe-only") {
291
+ const observeOnly = method === "turn.start.observe-only";
292
+ if (params.toolMode !== undefined) {
293
+ throw new Error("toolMode is unsupported; use turn.start.observe-only");
294
+ }
295
+ if (observeOnly && params.runtimeMcpServers !== undefined) {
296
+ throw new Error("turn.start.observe-only cannot include runtimeMcpServers");
297
+ }
298
+ const runtimeMcpServers = validatedRuntimeMcpServers(params.runtimeMcpServers);
299
+ const turnRequest = {
300
+ turnId: requiredString(params.turnId, "turnId"),
301
+ agent: requiredString(params.agent, "agent"),
302
+ friendId: requiredString(params.friendId, "friendId"),
303
+ channel: requiredString(params.channel, "channel"),
304
+ sessionKey: requiredString(params.sessionKey, "sessionKey"),
305
+ message: requiredString(params.message, "message"),
306
+ ...(observeOnly ? { disableTools: true, ephemeral: true } : {}),
307
+ ...(runtimeMcpServers ? { runtimeMcpServers } : {}),
308
+ };
309
+ const prepared = options.service.prepareTurn(turnRequest);
310
+ client.ownedTurnIds.add(turnRequest.turnId);
311
+ client.writer.send(response(id, { accepted: true, turnId: turnRequest.turnId }));
312
+ queueMicrotask(() => {
313
+ void options.service.runPreparedTurn(prepared).catch(() => undefined);
314
+ });
315
+ return;
316
+ }
317
+ client.writer.send(errorResponse(id, "method_not_found", `unknown frontend method: ${method}`));
318
+ }
319
+ const server = net.createServer((socket) => {
320
+ const ownedTurnIds = new Set();
321
+ const client = {
322
+ socket,
323
+ writer: new FrontendFrameWriter(socket, maxQueuedFrames, ownedTurnIds.clear.bind(ownedTurnIds)),
324
+ subscriptions: new Set(),
325
+ ownedTurnIds,
326
+ };
327
+ clients.add(client);
328
+ const lines = readline.createInterface({ input: socket, crlfDelay: Infinity });
329
+ let cleanedUp = false;
330
+ const cleanupClient = () => {
331
+ /* v8 ignore next -- socket error and close can race; the first cleanup owns the state transition @preserve */
332
+ if (cleanedUp)
333
+ return;
334
+ cleanedUp = true;
335
+ for (const turnId of client.ownedTurnIds)
336
+ options.service.cancelTurn(turnId);
337
+ clients.delete(client);
338
+ lines.close();
339
+ };
340
+ let dispatch = Promise.resolve();
341
+ lines.on("line", (line) => {
342
+ dispatch = dispatch.then(async () => {
343
+ let parsed;
344
+ try {
345
+ parsed = JSON.parse(line);
346
+ }
347
+ catch {
348
+ client.writer.send(errorResponse(null, "parse_error", "invalid JSON frame"));
349
+ return;
350
+ }
351
+ try {
352
+ await handleRequest(client, parsed);
353
+ }
354
+ catch (error) {
355
+ const id = parsed && typeof parsed === "object" && typeof parsed.id === "string"
356
+ ? parsed.id
357
+ : null;
358
+ client.writer.send(errorResponse(id, "invalid_params", error instanceof Error ? error.message : String(error)));
359
+ }
360
+ });
361
+ });
362
+ /* v8 ignore start -- Node transport error callbacks converge on the close-path cleanup covered by abrupt-disconnect tests @preserve */
363
+ socket.on("error", () => {
364
+ cleanupClient();
365
+ socket.destroy();
366
+ });
367
+ lines.on("error", () => {
368
+ cleanupClient();
369
+ socket.destroy();
370
+ });
371
+ /* v8 ignore stop */
372
+ socket.on("close", cleanupClient);
373
+ });
374
+ await new Promise((resolve, reject) => {
375
+ server.once("error", reject);
376
+ server.listen(socketPath, () => resolve());
377
+ });
378
+ fs.chmodSync(socketPath, 0o600);
379
+ (0, runtime_1.emitNervesEvent)({
380
+ component: "heart",
381
+ event: "heart.frontend_socket_ready",
382
+ message: "frontend socket ready",
383
+ });
384
+ return {
385
+ socketPath,
386
+ publish,
387
+ async stop() {
388
+ if (stopped)
389
+ return;
390
+ stopped = true;
391
+ unsubscribeFromService();
392
+ for (const client of clients)
393
+ client.socket.destroy();
394
+ await new Promise((resolve) => {
395
+ server.close(() => resolve());
396
+ });
397
+ removeSocket(socketPath);
398
+ },
399
+ };
400
+ }
@@ -337,6 +337,9 @@ function listVisibleBackgroundOperations(input) {
337
337
  agentRoot: input.agentRoot,
338
338
  limit,
339
339
  });
340
+ if (input.includeAmbientDiscovery === false) {
341
+ return persisted;
342
+ }
340
343
  const ambient = listAmbientMailImportOperations({
341
344
  agentName: input.agentName,
342
345
  agentRoot: input.agentRoot,
@@ -388,6 +388,7 @@ async function buildTurnContext(input) {
388
388
  homeDir: process.env.HOME,
389
389
  nowMs: Date.now(),
390
390
  limit: 5,
391
+ includeAmbientDiscovery: input.includeAmbientBackgroundDiscovery,
391
392
  });
392
393
  // Private-runtime work state
393
394
  const innerWorkState = readInnerWorkState();
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withTurnExecutionLease = withTurnExecutionLease;
4
+ const node_async_hooks_1 = require("node:async_hooks");
5
+ const runtime_1 = require("../nerves/runtime");
6
+ const leaseContext = new node_async_hooks_1.AsyncLocalStorage();
7
+ let queueTail = Promise.resolve();
8
+ async function withTurnExecutionLease(work) {
9
+ if (leaseContext.getStore())
10
+ return work();
11
+ let release;
12
+ const previous = queueTail;
13
+ queueTail = new Promise((resolve) => {
14
+ release = resolve;
15
+ });
16
+ await previous;
17
+ (0, runtime_1.emitNervesEvent)({
18
+ component: "heart",
19
+ event: "heart.turn_execution_lease_acquired",
20
+ message: "turn execution lease acquired",
21
+ });
22
+ return leaseContext.run(true, async () => {
23
+ try {
24
+ return await work();
25
+ }
26
+ finally {
27
+ release();
28
+ }
29
+ });
30
+ }
@@ -322,6 +322,7 @@ my bones give me the \`ouro\` cli. always pass \`--agent ${agentName}\`:
322
322
  ouro mcp list --agent ${agentName}
323
323
  ouro mcp call --agent ${agentName} <server> <tool> --args '{...}'
324
324
  ouro mcp-serve --agent ${agentName}
325
+ ouro acp-serve --agent ${agentName}
325
326
  ouro versions --agent ${agentName}
326
327
  ouro rollback --agent ${agentName} [<version>]
327
328
  ouro --help
@@ -560,7 +561,7 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
560
561
  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`.");
561
562
  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.");
562
563
  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.");
563
- 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 may read as disabled when no bundle entry exists, or as `stale_bundle_entry` when legacy bundle config needs cleanup, and neither state proves the runtime-injected tools are absent — 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 verifies the installed MCP binary and removes stale Workbench bundle entries; it does not enable `senses.workbench` or write `mcpServers.ouro_workbench`.");
564
+ 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 acp-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 may read as disabled when no bundle entry exists, or as `stale_bundle_entry` when legacy bundle config needs cleanup, and neither state proves the runtime-injected tools are absent — 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 verifies the installed MCP binary and removes stale Workbench bundle entries; it does not enable `senses.workbench` or write `mcpServers.ouro_workbench`.");
564
565
  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.");
565
566
  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.");
566
567
  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`.");
@@ -623,6 +624,8 @@ function uniqueToolsByName(tools) {
623
624
  return unique;
624
625
  }
625
626
  function toolsSection(channel, options, context) {
627
+ if (options?.hardDisableTools)
628
+ return "## my tools\nnone for this observe-only turn";
626
629
  const channelTools = options?.tools ?? (0, tools_1.getToolsForChannel)((0, friends_1.getChannelCapabilities)(channel), undefined, context, options?.providerCapabilities, undefined, options?.chatModel);
627
630
  const activeTools = channel === "inner"
628
631
  ? uniqueToolsByName([