@ouro.bot/cli 0.1.0-alpha.727 → 0.1.0-alpha.728

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/README.md CHANGED
@@ -179,6 +179,9 @@ If you are changing runtime code, keep all three green.
179
179
  ```bash
180
180
  ouro # open the interactive home deck in a human TTY
181
181
  ouro up # start daemon from installed production version
182
+ ouro up --latest # preflight latest, then replace any exact rollback pin
183
+ ouro rollback <version> # pin normal starts to an exact installed version
184
+ ouro versions # show installed versions and current intent
182
185
  ouro dev # start daemon from local repo build (auto-detects CWD)
183
186
  ouro dev --repo-path /path # start from a specific repo checkout
184
187
  ouro dev --clone # clone repo to ~/Projects/ouroboros, build, start
@@ -198,6 +201,7 @@ ouro connect perplexity --agent <name>
198
201
  ouro connect embeddings --agent <name>
199
202
  ouro connect teams --agent <name>
200
203
  ouro connect bluebubbles --agent <name>
204
+ ouro bluebubbles host status --json
201
205
  ouro connect voice --agent <name>
202
206
  ouro auth --agent <name>
203
207
  ouro auth --agent <name> --provider <provider>
@@ -218,11 +222,18 @@ ouro attention --agent <agent> # attention queue
218
222
  ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>
219
223
  ouro setup --tool <tool> --agent <name> # register MCP server + hooks with a dev tool
220
224
  ouro mcp-serve --agent <name> # start MCP server on stdin/stdout (used by dev tools)
225
+ ouro mcp doctor --agent <name> --json # bounded direct bridge evidence
221
226
  ouro hook <event> --agent <name> # fire a lifecycle hook (SessionStart, Stop, PostToolUse)
222
227
  ```
223
228
 
224
229
  The generic secret primitive is a vault item / credential in the owning agent vault: stable item name/path, hidden secret material, optional public fields, notes, timestamps/provenance, and no assumed use. `ouro connect` is for harness-managed workflows; workflow bindings reference ordinary vault items when they need secret material.
225
230
 
231
+ ### Standard BlueBubbles setup
232
+
233
+ `ouro connect bluebubbles --agent <name>` is the standard local-Mac setup path. Besides saving the machine-scoped attachment, it installs or verifies the native-compatible BlueBubbles LaunchAgent for a same-user bridge and reconciles one Ouro-owned `[*]` webhook after the listener is bound. The daemon repairs that owned callback every 180 seconds, preserves unrelated callbacks, and creates the desired callback before removing a stale owned one. If the listener or BlueBubbles API is unavailable, connect says the attachment was saved but setup is incomplete; `ouro doctor` and `ouro bluebubbles host status --json` separate app, service, process, HTTP, and webhook failures.
234
+
235
+ When BlueBubbles runs in another logged-in macOS account, standard setup installs a generic helper and returns one nonce-bound `human-required` Terminal command for that account plus `ouro bluebubbles host collect --request-id <id>`. Ouro never asks for or stores the other account's login password. The receipt proves that one handoff and reports launchd only as point-in-time evidence; current process and HTTP health are checked separately.
236
+
226
237
  ## Setting Up On Another Machine
227
238
 
228
239
  To clone an existing agent onto a new machine (macOS, Linux, or Windows via WSL2), see **[docs/cross-machine-setup.md](docs/cross-machine-setup.md)**. The short version is bundle plus vault: `npx ouro.bot@latest`, open the home deck, choose clone, enter the bundle's git remote URL, unlock the agent vault, refresh/verify credentials, and start with `ouro up`.
@@ -252,7 +263,9 @@ ouro setup --tool claude-code --agent <name>
252
263
  ouro setup --tool codex --agent <name>
253
264
  ```
254
265
 
255
- This registers the MCP server, installs lifecycle hooks (SessionStart, Stop, PostToolUse), and detects dev vs installed mode automatically.
266
+ This registers the MCP server, installs lifecycle hooks (SessionStart, Stop, PostToolUse), detects dev vs installed mode automatically, and runs a bounded direct canary. Registration success and canary health are reported separately.
267
+
268
+ If the dev-tool host appears frozen, run `ouro mcp doctor --agent <name> --json`. Its classification is deliberately narrow: `ouro-bridge-failed`, `ouro-bridge-healthy-at-capture`, or `host-stall-unexplained`. Add `--host-stall-observed` only when the host stall was independently observed. A healthy bridge canary does not prove that Codex or another host caused the stall; it only bounds what Ouro observed at capture time.
256
269
 
257
270
  **How it works:** When a developer starts a Claude Code session, the MCP server launches as a subprocess. The dev tool sees your MCP tools (`send_message`, `ask`, `check_response`, `status`, `search_facts`, `delegate`, etc.) and can invoke them mid-session. Conversation-shaped tools such as `send_message`, `ask`, `delegate`, `check_guidance`, and `report_progress` run full agent turns — you get your system prompt, your Desk record, your tools, everything. Read-only inspection tools such as `status` and `search_facts` do local lookup only. Missing `search_facts` hits are not evidence that the agent has no belief or preference.
258
271
 
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ "use strict"
3
+
4
+ const fs = require("fs")
5
+ const os = require("os")
6
+ const path = require("path")
7
+ const { execFileSync } = require("child_process")
8
+
9
+ const SCHEMA_VERSION = 1
10
+ const HELPER_VERSION = 1
11
+ const FRESHNESS_MS = 300000
12
+ const SHARED_ROOT = "/Users/Shared/Ouro"
13
+ const REQUESTS_ROOT = path.join(SHARED_ROOT, "bluebubbles-host-requests")
14
+ const RECEIPTS_ROOT = path.join(SHARED_ROOT, "bluebubbles-host-receipts")
15
+ const APP_PATH = "/Applications/BlueBubbles.app"
16
+ const EXECUTABLE_PATH = `${APP_PATH}/Contents/MacOS/BlueBubbles`
17
+ const LABEL = "com.bluebubbles.server"
18
+
19
+ function createRuntime(overrides = {}) {
20
+ return {
21
+ fs,
22
+ homedir: os.homedir,
23
+ now: Date.now,
24
+ getuid: () => process.getuid(),
25
+ execFileSync,
26
+ argv: process.argv,
27
+ writeStdout: (value) => process.stdout.write(value),
28
+ writeStderr: (value) => process.stderr.write(value),
29
+ setExitCode: (code) => { process.exitCode = code },
30
+ ...overrides,
31
+ }
32
+ }
33
+
34
+ function fail(message, runtime) {
35
+ runtime.writeStderr(`bluebubbles-host: ${message}\n`)
36
+ runtime.setExitCode(1)
37
+ }
38
+
39
+ function runLaunchctl(args, runtime) {
40
+ try {
41
+ const output = runtime.execFileSync("launchctl", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
42
+ return { ok: true, detail: output.trim() || "ok" }
43
+ } catch (error) {
44
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) }
45
+ }
46
+ }
47
+
48
+ function requireLaunchctl(args, operation, runtime) {
49
+ const result = runLaunchctl(args, runtime)
50
+ if (result.ok) {
51
+ return result.detail
52
+ } else {
53
+ throw new Error(`launchctl ${operation} failed: ${result.detail}`)
54
+ }
55
+ }
56
+
57
+ function nativePlist() {
58
+ return `<?xml version="1.0" encoding="UTF-8"?>
59
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
60
+ <plist version="1.0">
61
+ <dict>
62
+ <key>AssociatedBundleIdentifiers</key>
63
+ <array>
64
+ <string>com.BlueBubbles.BlueBubbles-Server</string>
65
+ </array>
66
+ <key>Label</key>
67
+ <string>com.bluebubbles.server</string>
68
+ <key>Program</key>
69
+ <string>${EXECUTABLE_PATH}</string>
70
+ <key>RunAtLoad</key>
71
+ <true/>
72
+ <key>KeepAlive</key>
73
+ <dict>
74
+ <key>SuccessfulExit</key>
75
+ <false/>
76
+ <key>Crashed</key>
77
+ <true/>
78
+ </dict>
79
+ </dict>
80
+ </plist>`
81
+ }
82
+
83
+ function validateRequest(request, requestPath, runtime) {
84
+ if (path.dirname(path.resolve(requestPath)) !== REQUESTS_ROOT) throw new Error("request path is outside the shared request directory")
85
+ if (request.schemaVersion !== SCHEMA_VERSION) throw new Error("unsupported request schema")
86
+ if (request.helperVersion !== HELPER_VERSION) throw new Error("unsupported helper version")
87
+ if (!/^[A-Za-z_][A-Za-z0-9._-]*$/.test(request.username)) throw new Error("invalid target username")
88
+ if (!Number.isInteger(request.uid) || request.uid < 500) throw new Error("invalid target uid")
89
+ if (!/^[0-9a-f]{64}$/.test(request.nonce)) throw new Error("invalid request nonce")
90
+ if (request.requestId !== `${request.uid}-${request.nonce}`) throw new Error("request id mismatch")
91
+ if (!new Set(["install", "status", "repair", "remove"]).has(request.action)) throw new Error("unsupported host action")
92
+ const requestedAt = Date.parse(request.requestedAt)
93
+ const expiresAt = Date.parse(request.expiresAt)
94
+ const now = runtime.now()
95
+ if (!Number.isFinite(requestedAt) || !Number.isFinite(expiresAt)) throw new Error("invalid request time")
96
+ if (expiresAt - requestedAt > FRESHNESS_MS) throw new Error("request freshness window is too large")
97
+ if (requestedAt > now || now > expiresAt) throw new Error("request is not fresh")
98
+
99
+ const currentUid = runtime.getuid()
100
+ const currentUsername = runtime.execFileSync("id", ["-un"], { encoding: "utf8" }).trim()
101
+ if (currentUid !== request.uid || currentUsername !== request.username) throw new Error("run this request as its exact target user")
102
+ requireLaunchctl(["print", `gui/${currentUid}`], `print gui/${currentUid}`, runtime)
103
+ return request
104
+ }
105
+
106
+ function inspect(request, runtime) {
107
+ const plistPath = path.join(runtime.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`)
108
+ const serviceTarget = `gui/${request.uid}/${LABEL}`
109
+ const service = runLaunchctl(["print", serviceTarget], runtime)
110
+ const plist = runtime.fs.existsSync(plistPath)
111
+ ? runtime.fs.readFileSync(plistPath, "utf8") === nativePlist() ? "current" : "drifted"
112
+ : "missing"
113
+ return { plistPath, serviceTarget, service, plist }
114
+ }
115
+
116
+ function receiptState(request, runtime) {
117
+ return {
118
+ plistPath: path.join(runtime.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`),
119
+ serviceTarget: `gui/${request.uid}/${LABEL}`,
120
+ }
121
+ }
122
+
123
+ function requireApp(runtime) {
124
+ if (runtime.fs.existsSync(APP_PATH)) {
125
+ return APP_PATH
126
+ } else {
127
+ throw new Error(`BlueBubbles app is missing at ${APP_PATH}`)
128
+ }
129
+ }
130
+
131
+ function unloadService(state, disableFirst, runtime) {
132
+ if (!state.service.ok) {
133
+ return false
134
+ } else {
135
+ disableFirst && requireLaunchctl(["disable", state.serviceTarget], "disable", runtime)
136
+ requireLaunchctl(["bootout", state.serviceTarget], "bootout", runtime)
137
+ return true
138
+ }
139
+ }
140
+
141
+ function installPlistIfNeeded(state, runtime) {
142
+ if (state.plist === "current") {
143
+ return false
144
+ } else {
145
+ runtime.fs.mkdirSync(path.dirname(state.plistPath), { recursive: true })
146
+ runtime.fs.writeFileSync(state.plistPath, nativePlist(), { mode: 0o644 })
147
+ return true
148
+ }
149
+ }
150
+
151
+ function execute(request, runtime) {
152
+ const before = inspect(request, runtime)
153
+ if (request.action === "status") {
154
+ if (before.plist === "current" && before.service.ok) {
155
+ return before
156
+ } else {
157
+ throw new Error(`native service is not current and loaded (${before.plist}; ${before.service.detail})`)
158
+ }
159
+ }
160
+ if (request.action === "remove") {
161
+ unloadService(before, true, runtime)
162
+ before.plist !== "missing" && runtime.fs.unlinkSync(before.plistPath)
163
+ const after = inspect(request, runtime)
164
+ if (after.plist !== "missing" || after.service.ok) throw new Error("native service removal did not verify")
165
+ return after
166
+ } else {
167
+ requireApp(runtime)
168
+ if (before.plist === "current" && before.service.ok) {
169
+ return before
170
+ } else {
171
+ installPlistIfNeeded(before, runtime)
172
+ unloadService(before, false, runtime)
173
+ requireLaunchctl(["disable", before.serviceTarget], "disable", runtime)
174
+ requireLaunchctl(["enable", before.serviceTarget], "enable", runtime)
175
+ requireLaunchctl(["bootstrap", `gui/${request.uid}`, before.plistPath], "bootstrap", runtime)
176
+ const after = inspect(request, runtime)
177
+ if (after.plist === "current" && after.service.ok) {
178
+ return after
179
+ } else {
180
+ throw new Error("native service installation did not verify")
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ function publishReceipt(request, result, detail, state, runtime) {
187
+ const receipt = {
188
+ ...request,
189
+ appPath: APP_PATH,
190
+ plistPath: state.plistPath,
191
+ launchAgentLabel: LABEL,
192
+ launchdDomain: `gui/${request.uid}`,
193
+ result,
194
+ detail,
195
+ verifiedAt: new Date(runtime.now()).toISOString(),
196
+ }
197
+ const finalPath = path.join(RECEIPTS_ROOT, `${request.requestId}.json`)
198
+ const temporaryPath = `${finalPath}.${request.nonce}.tmp`
199
+ runtime.fs.writeFileSync(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o444, flag: "wx" })
200
+ try {
201
+ runtime.fs.chmodSync(temporaryPath, 0o444)
202
+ runtime.fs.linkSync(temporaryPath, finalPath)
203
+ runtime.fs.chmodSync(finalPath, 0o444)
204
+ } finally {
205
+ if (runtime.fs.existsSync(temporaryPath)) runtime.fs.unlinkSync(temporaryPath)
206
+ }
207
+ runtime.writeStdout(`${JSON.stringify({ ok: result === "verified", requestId: request.requestId, receiptPath: finalPath })}\n`)
208
+ }
209
+
210
+ function main(runtime) {
211
+ const requestFlag = runtime.argv.indexOf("--request")
212
+ if (requestFlag >= 0 && runtime.argv[requestFlag + 1]) {
213
+ const requestPath = path.resolve(runtime.argv[requestFlag + 1])
214
+ const request = validateRequest(JSON.parse(runtime.fs.readFileSync(requestPath, "utf8")), requestPath, runtime)
215
+ let state = receiptState(request, runtime)
216
+ try {
217
+ state = execute(request, runtime)
218
+ publishReceipt(request, "verified", `native ${request.action} verified`, state, runtime)
219
+ } catch (error) {
220
+ const detail = error instanceof Error ? error.message : String(error)
221
+ publishReceipt(request, "failed", detail, state, runtime)
222
+ throw error
223
+ }
224
+ } else {
225
+ throw new Error("usage: bluebubbles-host --request <request.json>")
226
+ }
227
+ }
228
+
229
+ function runCli(runtime = createRuntime()) {
230
+ try {
231
+ main(runtime)
232
+ } catch (error) {
233
+ fail(error instanceof Error ? error.message : String(error), runtime)
234
+ }
235
+ }
236
+
237
+ function runCliIfMain(isMain, runtime) {
238
+ if (isMain) {
239
+ runCli(runtime)
240
+ } else {
241
+ return false
242
+ }
243
+ return true
244
+ }
245
+
246
+ runCliIfMain(require.main === module)
247
+
248
+ module.exports = {
249
+ constants: {
250
+ schemaVersion: SCHEMA_VERSION,
251
+ helperVersion: HELPER_VERSION,
252
+ freshnessMs: FRESHNESS_MS,
253
+ sharedRoot: SHARED_ROOT,
254
+ },
255
+ createRuntime,
256
+ nativePlist,
257
+ validateRequest,
258
+ inspect,
259
+ execute,
260
+ publishReceipt,
261
+ main,
262
+ runCli,
263
+ runCliIfMain,
264
+ }
package/changelog.json CHANGED
@@ -1,6 +1,15 @@
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.728",
6
+ "changes": [
7
+ "Make exact rollback intent durable, bound status and MCP diagnostics, and make BlueBubbles host continuity plus owner-safe webhook reconciliation part of standard setup.",
8
+ "Verify the packed BlueBubbles host helper and exact wrapper intent across fresh, older, equal, and newer installations, and replace the vulnerable mail parsing dependency chain.",
9
+ "Enforce status as an absolute wall-clock deadline even under trickled socket data, and keep sanitized MCP failure evidence compliant with the runtime logging schema.",
10
+ "Stage published latest targets in either direction, verify the recovery launcher before version-intent commits, and require BlueBubbles host repair to finish with a current loaded LaunchAgent."
11
+ ]
12
+ },
4
13
  {
5
14
  "version": "0.1.0-alpha.727",
6
15
  "changes": [
@@ -315,8 +315,7 @@ function getBlueBubblesConfig() {
315
315
  }
316
316
  function getBlueBubblesChannelConfig() {
317
317
  const config = loadConfig();
318
- const { port, webhookPath, requestTimeoutMs } = config.bluebubblesChannel;
319
- return { port, webhookPath, requestTimeoutMs };
318
+ return { ...config.bluebubblesChannel };
320
319
  }
321
320
  function getIntegrationsConfig() {
322
321
  const config = loadConfig();
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.blueBubblesListenerReadyAfterApply = blueBubblesListenerReadyAfterApply;
4
+ exports.formatBlueBubblesWebhookConnectLine = formatBlueBubblesWebhookConnectLine;
5
+ exports.formatBlueBubblesHostActionText = formatBlueBubblesHostActionText;
6
+ exports.reconcileBlueBubblesWebhookAfterConnect = reconcileBlueBubblesWebhookAfterConnect;
7
+ const runtime_1 = require("../../nerves/runtime");
8
+ const webhook_registration_1 = require("../../senses/bluebubbles/webhook-registration");
9
+ function blueBubblesListenerReadyAfterApply(daemonApply) {
10
+ return daemonApply.startsWith("restarted Ouro;");
11
+ }
12
+ function formatBlueBubblesWebhookConnectLine(result) {
13
+ return result.ok
14
+ ? `webhook: ${result.detail}`
15
+ : `webhook saved-but-incomplete: ${result.detail}`;
16
+ }
17
+ function formatBlueBubblesHostActionText(outcome) {
18
+ const httpStatus = outcome.state.http.ok === true
19
+ ? "healthy"
20
+ : outcome.state.http.ok === false
21
+ ? "unhealthy"
22
+ : "not checked";
23
+ return [
24
+ `BlueBubbles host ${outcome.action}`,
25
+ "actor: agent-runnable",
26
+ `app: ${outcome.state.app}`,
27
+ `plist: ${outcome.state.plist} (${outcome.state.plistPath})`,
28
+ `service: ${outcome.state.service}${outcome.state.serviceDetail ? ` (${outcome.state.serviceDetail})` : ""}`,
29
+ `process: ${outcome.state.process}`,
30
+ `HTTP: ${httpStatus} (${outcome.state.http.detail})`,
31
+ "repair: ouro bluebubbles host repair",
32
+ ].join("\n");
33
+ }
34
+ async function reconcileBlueBubblesWebhookAfterConnect(input, deps = {}) {
35
+ (0, runtime_1.emitNervesEvent)({
36
+ component: "daemon",
37
+ event: "daemon.bluebubbles_connect_webhook_start",
38
+ message: "verifying BlueBubbles webhook after connect",
39
+ meta: { agent: input.agentName, listenerReady: input.listenerReady },
40
+ });
41
+ try {
42
+ const result = deps.reconcile
43
+ ? await deps.reconcile(input)
44
+ : await (0, webhook_registration_1.reconcileBlueBubblesWebhookRegistration)(input, { fetchImpl: deps.fetchImpl ?? fetch });
45
+ (0, runtime_1.emitNervesEvent)({
46
+ component: "daemon",
47
+ event: "daemon.bluebubbles_connect_webhook_end",
48
+ level: result.ok ? "info" : "warn",
49
+ message: "verified BlueBubbles webhook after connect",
50
+ meta: { agent: input.agentName, state: result.state, ok: result.ok },
51
+ });
52
+ return result;
53
+ }
54
+ catch {
55
+ (0, runtime_1.emitNervesEvent)({
56
+ component: "daemon",
57
+ event: "daemon.bluebubbles_connect_webhook_error",
58
+ level: "error",
59
+ message: "BlueBubbles webhook verification failed before returning diagnostics",
60
+ meta: { agent: input.agentName, listenerReady: input.listenerReady },
61
+ });
62
+ return {
63
+ ok: false,
64
+ state: "api-unreachable",
65
+ changed: false,
66
+ ownedCount: 0,
67
+ exactCount: 0,
68
+ detail: "webhook verification failed before BlueBubbles returned a diagnostic result",
69
+ };
70
+ }
71
+ }