@ouro.bot/cli 0.1.0-alpha.914 → 0.1.0-alpha.915

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,12 @@
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.915",
6
+ "changes": [
7
+ "BlueBubbles (iMessage) only acts as the Apple ID of the macOS account an agent declares as senses.bluebubbles.hostUsername. Before every send, edit, typing indicator, read receipt and health check, Ouro confirms the BlueBubbles server runs under that account (wherever Ouro itself runs), and refuses otherwise; every send rechecks. The sense also registers its webhook only on a server under that account rechecks before taking in each incoming webhook event, and refuses to read messages (catch-up, queued recovery, message lookups, bluebubbles replay and context-smoke) from a server it cannot confirm, so an agent never reads another account's iMessages either; queued messages stay queued until it can. The sense, send_message and RSVP sends are all covered. With no declared host, BlueBubbles does not start or send, ouro doctor fails (as does its webhook check when the server runs under another account), and the agent does not see it as ready. ouro connect bluebubbles asks for the host account by name and records it before installing, and ouro bluebubbles host install|repair refuses undeclared accounts."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.914",
6
12
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.914",
2
+ "runtimeVersion": "0.1.0-alpha.915",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.914</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.915</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -0,0 +1,78 @@
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.currentOsAccount = currentOsAccount;
37
+ exports.checkBlueBubblesHostDeclared = checkBlueBubblesHostDeclared;
38
+ exports.checkBlueBubblesHostTarget = checkBlueBubblesHostTarget;
39
+ const os = __importStar(require("os"));
40
+ const runtime_1 = require("../nerves/runtime");
41
+ const HUMAN_DECISION = "Choosing senses.bluebubbles.hostUsername is a human decision, because it chooses whose Apple ID the agent speaks as.";
42
+ function currentOsAccount() {
43
+ const info = os.userInfo();
44
+ return { username: info.username, uid: info.uid };
45
+ }
46
+ function refuse(reason, message, meta) {
47
+ (0, runtime_1.emitNervesEvent)({
48
+ level: "error",
49
+ component: "senses",
50
+ event: "senses.bluebubbles_host_account_refused",
51
+ message: "refused BlueBubbles outside its declared macOS host account",
52
+ meta: { reason, ...meta },
53
+ });
54
+ return { ok: false, reason, message };
55
+ }
56
+ /** Does `agent` declare its iMessage host account at all? */
57
+ function checkBlueBubblesHostDeclared(agent, hostUsername) {
58
+ if (hostUsername !== undefined)
59
+ return { ok: true };
60
+ return refuse("unset", `BlueBubbles refused for agent "${agent}": no host account is declared (senses.bluebubbles.hostUsername), so Ouro cannot tell whose Apple ID would send. `
61
+ + `Not starting. Fix: a human sets senses.bluebubbles.hostUsername in agent.json to the macOS account signed in to the agent's own Apple ID. ${HUMAN_DECISION}`, { agent });
62
+ }
63
+ /**
64
+ * May the BlueBubbles server be installed or repaired under `targetUsername`?
65
+ * Only if some agent with BlueBubbles enabled declares that account as its host.
66
+ */
67
+ function checkBlueBubblesHostTarget(targetUsername, declared) {
68
+ if (declared.some((entry) => entry.hostUsername === targetUsername))
69
+ return { ok: true };
70
+ const withHost = declared.filter((entry) => entry.hostUsername !== undefined);
71
+ if (withHost.length === 0) {
72
+ return refuse("unset", `BlueBubbles host setup refused for macOS account "${targetUsername}": no agent with BlueBubbles enabled declares senses.bluebubbles.hostUsername. `
73
+ + `Fix: a human declares the macOS account signed in to the agent's own Apple ID as senses.bluebubbles.hostUsername. ${HUMAN_DECISION}`, { targetUsername });
74
+ }
75
+ const hosts = withHost.map((entry) => `${entry.agent} -> "${entry.hostUsername}"`).join(", ");
76
+ return refuse("mismatch", `BlueBubbles host setup refused for macOS account "${targetUsername}": no agent declares it as its iMessage host (declared: ${hosts}). `
77
+ + `Set up BlueBubbles under a declared account instead. ${HUMAN_DECISION}`, { targetUsername, hosts });
78
+ }
@@ -53,6 +53,7 @@ exports.setupBlueBubblesHostForConnect = setupBlueBubblesHostForConnect;
53
53
  exports.resolveMailImportFilePath = resolveMailImportFilePath;
54
54
  exports.runOuroCli = runOuroCli;
55
55
  const child_process_1 = require("child_process");
56
+ const bluebubbles_host_account_1 = require("../bluebubbles-host-account");
56
57
  const crypto_1 = require("crypto");
57
58
  const fs = __importStar(require("fs"));
58
59
  const os = __importStar(require("os"));
@@ -2891,6 +2892,15 @@ function enableAgentSense(agent, sense, deps) {
2891
2892
  };
2892
2893
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
2893
2894
  }
2895
+ /** Record `senses.bluebubbles.hostUsername` in agent.json, leaving the enabled flag as it is. */
2896
+ function declareBlueBubblesHost(agent, hostUsername, deps) {
2897
+ // readAgentConfigForAgent has already validated senses (an object when present).
2898
+ const { configPath } = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot);
2899
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2900
+ const senses = raw.senses ?? {};
2901
+ raw.senses = { ...senses, bluebubbles: { ...(senses.bluebubbles ?? { enabled: false }), hostUsername } };
2902
+ fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
2903
+ }
2894
2904
  const CONNECT_MENU_PROMPT = "Choose [1-9] or type a name: ";
2895
2905
  function connectMenuIsTTY(deps) {
2896
2906
  return deps.isTTY ?? process.stdout.isTTY === true;
@@ -3687,6 +3697,36 @@ async function executeConnectTeams(agent, deps) {
3687
3697
  ],
3688
3698
  });
3689
3699
  }
3700
+ /** Host accounts declared by every bundle agent that has BlueBubbles enabled. */
3701
+ function declaredBlueBubblesHosts(bundlesRoot) {
3702
+ let entries;
3703
+ try {
3704
+ entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
3705
+ }
3706
+ catch {
3707
+ return [];
3708
+ }
3709
+ const declared = [];
3710
+ for (const entry of entries) {
3711
+ if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
3712
+ continue;
3713
+ let parsed;
3714
+ try {
3715
+ parsed = JSON.parse(fs.readFileSync(path.join(bundlesRoot, entry.name, "agent.json"), "utf-8"));
3716
+ }
3717
+ catch {
3718
+ continue;
3719
+ }
3720
+ const bluebubbles = parsed.senses?.bluebubbles;
3721
+ if (bluebubbles?.enabled !== true)
3722
+ continue;
3723
+ const hostUsername = typeof bluebubbles.hostUsername === "string" && bluebubbles.hostUsername.trim()
3724
+ ? bluebubbles.hostUsername.trim()
3725
+ : undefined;
3726
+ declared.push({ agent: entry.name.slice(0, -".ouro".length), ...(hostUsername ? { hostUsername } : {}) });
3727
+ }
3728
+ return declared;
3729
+ }
3690
3730
  async function setupBlueBubblesHostForConnect(bridgeUsername, serverUrl, requestTimeoutMs, deps, hostAccountDeps = {
3691
3731
  userInfo: os.userInfo,
3692
3732
  execFileSync: child_process_1.execFileSync,
@@ -3781,8 +3821,20 @@ async function executeConnectBlueBubbles(agent, deps) {
3781
3821
  const ownHandles = ownHandlesRaw
3782
3822
  ? ownHandlesRaw.split(",").map((h) => h.trim()).filter((h) => h.length > 0)
3783
3823
  : [];
3784
- const bridgeUsernameAnswer = (await promptInput(`BlueBubbles macOS account username [${os.userInfo().username}]: `)).trim();
3785
- const bridgeUsername = bridgeUsernameAnswer || os.userInfo().username;
3824
+ // iMessage sends as whichever Apple ID the host account is signed in to, so the
3825
+ // host account is declared explicitly (never defaulted to whoever runs this).
3826
+ const declaredHost = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot).config.senses?.bluebubbles.hostUsername;
3827
+ const bridgeUsernameAnswer = (await promptInput(declaredHost
3828
+ ? `BlueBubbles macOS account username [${declaredHost}]: `
3829
+ : `macOS account signed in to ${agent}'s own Apple ID (BlueBubbles host): `)).trim();
3830
+ const bridgeUsername = bridgeUsernameAnswer || declaredHost;
3831
+ if (!bridgeUsername)
3832
+ throw new Error(`BlueBubbles host account cannot be blank: name the macOS account signed in to ${agent}'s own Apple ID`);
3833
+ if (declaredHost) {
3834
+ const hostCheck = (0, bluebubbles_host_account_1.checkBlueBubblesHostTarget)(bridgeUsername, [{ agent, hostUsername: declaredHost }]);
3835
+ if (!hostCheck.ok)
3836
+ throw new Error(hostCheck.message);
3837
+ }
3786
3838
  const machineId = currentMachineId(deps);
3787
3839
  const progress = createHumanCommandProgress(deps, "connect bluebubbles");
3788
3840
  let stored;
@@ -3797,6 +3849,8 @@ async function executeConnectBlueBubbles(agent, deps) {
3797
3849
  progress.end();
3798
3850
  throw new Error(`cannot read existing machine runtime credentials from ${current.itemPath}: ${current.error}`);
3799
3851
  }
3852
+ // Declare the host account before installing anything under it.
3853
+ declareBlueBubblesHost(agent, bridgeUsername, deps);
3800
3854
  progress.updateDetail("configuring native BlueBubbles host continuity");
3801
3855
  hostSetup = await setupBlueBubblesHostForConnect(bridgeUsername, serverUrl, requestTimeoutMs, deps);
3802
3856
  const blueBubblesPatch = {
@@ -3831,6 +3885,7 @@ async function executeConnectBlueBubbles(agent, deps) {
3831
3885
  machineId,
3832
3886
  requestTimeoutMs,
3833
3887
  listenerReady: (0, bluebubbles_cli_integration_1.blueBubblesListenerReadyAfterApply)(daemonApply),
3888
+ hostUsername: bridgeUsername,
3834
3889
  };
3835
3890
  webhookRegistration = await (0, bluebubbles_cli_integration_1.reconcileBlueBubblesWebhookAfterConnect)(webhookInput, {
3836
3891
  ...(deps.reconcileBlueBubblesWebhook ? { reconcile: deps.reconcileBlueBubblesWebhook } : {}),
@@ -6692,6 +6747,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6692
6747
  return text;
6693
6748
  }
6694
6749
  if (command.kind === "bluebubbles.host") {
6750
+ if (command.action === "install" || command.action === "repair") {
6751
+ // Only an account some agent declares as its iMessage host may get a BlueBubbles server.
6752
+ const targetUsername = command.target?.username ?? (0, bluebubbles_host_account_1.currentOsAccount)().username;
6753
+ const hostCheck = (0, bluebubbles_host_account_1.checkBlueBubblesHostTarget)(targetUsername, declaredBlueBubblesHosts(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)()));
6754
+ if (!hostCheck.ok)
6755
+ throw new Error(hostCheck.message);
6756
+ }
6695
6757
  if (command.target) {
6696
6758
  (0, bluebubbles_host_1.installBlueBubblesHostSharedHelper)({
6697
6759
  assetPath: path.resolve(__dirname, "../../../assets/bluebubbles-host"),
@@ -240,6 +240,7 @@ const DEFINITIVE_BLUEBUBBLES_WEBHOOK_STATES = new Set([
240
240
  "auth-failed",
241
241
  "malformed",
242
242
  "listener-not-ready",
243
+ "host-account-refused",
243
244
  ]);
244
245
  function blueBubblesUpstreamEvidence(probe) {
245
246
  if (probe.ok)
@@ -463,10 +464,19 @@ async function checkSenses(deps) {
463
464
  });
464
465
  continue;
465
466
  }
467
+ const hostUsername = typeof senseObj.hostUsername === "string" ? senseObj.hostUsername.trim() : "";
468
+ if (!hostUsername) {
469
+ checks.push({
470
+ label: `${agentDir} bluebubbles host account`,
471
+ status: "fail",
472
+ detail: "no host account declared (senses.bluebubbles.hostUsername); BlueBubbles will not start or send. A human sets it to the macOS account signed in to the agent's own Apple ID.",
473
+ });
474
+ continue;
475
+ }
466
476
  checks.push({
467
477
  label: `${agentDir} bluebubbles config`,
468
478
  status: "pass",
469
- detail: serverUrl,
479
+ detail: `${serverUrl} (host account ${hostUsername})`,
470
480
  });
471
481
  let upstreamContext = "upstream probe not run in this pass";
472
482
  let infrastructure = { upstream: "not-run", webhook: null };
@@ -496,6 +506,7 @@ async function checkSenses(deps) {
496
506
  machineId,
497
507
  requestTimeoutMs,
498
508
  listenerReady: true,
509
+ hostUsername,
499
510
  }, { fetchImpl: deps.fetchImpl });
500
511
  checks.push({
501
512
  id: "senses.bluebubbles.webhook",
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DaemonSenseManager = void 0;
37
37
  const fs = __importStar(require("fs"));
38
+ const bluebubbles_host_account_1 = require("../bluebubbles-host-account");
38
39
  const os = __importStar(require("os"));
39
40
  const path = __importStar(require("path"));
40
41
  const runtime_1 = require("../../nerves/runtime");
@@ -111,6 +112,11 @@ function readAgentSenses(agentJsonPath) {
111
112
  const enabled = rawSense.enabled;
112
113
  if (typeof enabled === "boolean") {
113
114
  defaults[sense] = { enabled };
115
+ const hostUsername = rawSense.hostUsername;
116
+ // A missing or malformed host account stays unset, so BlueBubbles fails closed.
117
+ if (sense === "bluebubbles" && typeof hostUsername === "string" && hostUsername.trim().length > 0) {
118
+ defaults[sense] = { enabled, hostUsername: hostUsername.trim() };
119
+ }
114
120
  }
115
121
  }
116
122
  if (workbenchHasStaleBundleEntry) {
@@ -244,6 +250,12 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
244
250
  : runtimeConfigUnavailableDetail(agent, machineRuntimeConfig),
245
251
  };
246
252
  }
253
+ if (senses.bluebubbles.enabled && base.bluebubbles.configured) {
254
+ // Attached here, but whose Apple ID? Without a declared host, never start.
255
+ const declared = (0, bluebubbles_host_account_1.checkBlueBubblesHostDeclared)(agent, senses.bluebubbles.hostUsername);
256
+ if (!declared.ok)
257
+ base.bluebubbles = { configured: false, hostRefused: true, detail: declared.message };
258
+ }
247
259
  if (senses.mail.enabled) {
248
260
  const privateKeys = mailroom?.privateKeys;
249
261
  const hasPrivateKeys = !!privateKeys && typeof privateKeys === "object" && !Array.isArray(privateKeys) && Object.values(privateKeys).some((value) => typeof value === "string" && value.trim().length > 0);
@@ -856,15 +868,20 @@ class DaemonSenseManager {
856
868
  if (!context || !context.senses[parsed.sense].enabled)
857
869
  return null;
858
870
  const managedName = `${parsed.agent}:${parsed.sense}`;
871
+ const currentRow = () => this.listSenseRows().find((row) => row.agent === parsed.agent && row.sense === parsed.sense) ?? null;
859
872
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
860
873
  const needsMachineConfig = needsMachineRuntimeConfig(parsed.agent, parsed.sense);
861
874
  const machineRuntimeConfig = needsMachineConfig
862
875
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
863
876
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
864
877
  context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, runtimeConfig, machineRuntimeConfig, context.workbenchHasStaleBundleEntry);
878
+ if (context.facts[parsed.sense].hostRefused) {
879
+ // Never start iMessage without a declared host account.
880
+ return currentRow();
881
+ }
865
882
  this.processManager.resetAgentFailureState?.(managedName);
866
883
  await this.processManager.startAgent?.(managedName);
867
- return this.listSenseRows().find((row) => row.agent === parsed.agent && row.sense === parsed.sense) ?? null;
884
+ return currentRow();
868
885
  }
869
886
  listSenseRows() {
870
887
  const runtime = new Map();
@@ -200,6 +200,21 @@ function normalizeSenses(value, configFile) {
200
200
  });
201
201
  throw new Error(`agent.json at ${configFile} must include senses.${senseName}.enabled as boolean.`);
202
202
  }
203
+ const hostUsername = rawSense.hostUsername;
204
+ if (senseName === "bluebubbles" && hostUsername !== undefined) {
205
+ if (typeof hostUsername !== "string" || hostUsername.trim().length === 0) {
206
+ (0, runtime_1.emitNervesEvent)({
207
+ level: "error",
208
+ event: "config_identity.error",
209
+ component: "config/identity",
210
+ message: "agent config has invalid BlueBubbles host username",
211
+ meta: { path: configFile, sense: senseName },
212
+ });
213
+ throw new Error(`agent.json at ${configFile}: senses.bluebubbles.hostUsername must be a non-empty string.`);
214
+ }
215
+ defaults[senseName] = { enabled, hostUsername: hostUsername.trim() };
216
+ continue;
217
+ }
203
218
  defaults[senseName] = { enabled };
204
219
  }
205
220
  return defaults;
@@ -205,7 +205,9 @@ function readSenseStatusLines() {
205
205
  const configured = {
206
206
  cli: true,
207
207
  teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
208
- bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
208
+ // Without a declared host account, every BlueBubbles send is refused, so it is not ready.
209
+ bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password")
210
+ && config.senses?.bluebubbles?.hostUsername !== undefined,
209
211
  mail: hasTextField(mailroom, "mailboxAddress") && !!privateKeys && typeof privateKeys === "object" && !Array.isArray(privateKeys),
210
212
  /* v8 ignore next 5 -- full voice mode matrix lives in the voice runtime resolver; this row only mirrors that result into prompt context @preserve */
211
213
  voice: voiceConversationEngine === "openai-sip"
package/dist/rsvp/cli.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.rsvpBlueBubblesHostOptions = rsvpBlueBubblesHostOptions;
36
37
  exports.runRsvpCliCommand = runRsvpCliCommand;
37
38
  const fs = __importStar(require("node:fs"));
38
39
  const os = __importStar(require("node:os"));
@@ -329,6 +330,15 @@ function recordField(record, key) {
329
330
  ? value
330
331
  : null;
331
332
  }
333
+ /** The agent's declared iMessage host account; an unreadable config leaves it unset, so sending fails closed. */
334
+ function rsvpBlueBubblesHostOptions(agent, agentRoot) {
335
+ try {
336
+ return { agent, hostUsername: (0, identity_1.loadAgentConfig)({ agentName: agent, agentRoot }).senses?.bluebubbles.hostUsername };
337
+ }
338
+ catch {
339
+ return { agent, hostUsername: undefined };
340
+ }
341
+ }
332
342
  function rsvpBlueBubblesClientConfig(nativeConfig, machineRuntimeConfig) {
333
343
  if (!machineRuntimeConfig.ok) {
334
344
  throw new Error(`BlueBubbles runtime config unavailable at ${machineRuntimeConfig.itemPath}: ${machineRuntimeConfig.error}`);
@@ -721,7 +731,7 @@ async function executeRefreshInner(command, deps, transportCounter, hardNoSend)
721
731
  };
722
732
  }
723
733
  const bluebubbles = rsvpBlueBubblesClientConfig(configResult.config, machineRuntimeConfig);
724
- const client = (0, client_1.createBlueBubblesClient)(bluebubbles.config, bluebubbles.channelConfig);
734
+ const client = (0, client_1.createBlueBubblesClient)(bluebubbles.config, bluebubbles.channelConfig, rsvpBlueBubblesHostOptions(command.agent, agentRoot));
725
735
  sendBoundary = await (0, outbound_state_1.executeRsvpSendBoundary)({
726
736
  agentRoot,
727
737
  habitId: command.habitName,
@@ -880,7 +890,7 @@ async function executeSmoke(command, deps) {
880
890
  let delivery;
881
891
  if (sendAllowed) {
882
892
  const bluebubbles = rsvpBlueBubblesClientConfig(configResult.config, await resolveRsvpMachineRuntimeCredentialConfig(command.agent));
883
- const sent = await (0, client_1.createBlueBubblesClient)(bluebubbles.config, bluebubbles.channelConfig).sendText({
893
+ const sent = await (0, client_1.createBlueBubblesClient)(bluebubbles.config, bluebubbles.channelConfig, rsvpBlueBubblesHostOptions(command.agent, agentRoot)).sendText({
884
894
  chat: blueBubblesChatFor(configResult.config),
885
895
  text: answer.text,
886
896
  });
@@ -44,6 +44,7 @@ const minimax_1 = require("../../heart/providers/minimax");
44
44
  const minimax_vlm_1 = require("../../heart/providers/minimax-vlm");
45
45
  const model_1 = require("./model");
46
46
  const media_1 = require("./media");
47
+ const host_account_verifier_1 = require("./host-account-verifier");
47
48
  class BlueBubblesSendError extends Error {
48
49
  httpStatus;
49
50
  status;
@@ -303,13 +304,27 @@ async function resolveChatGuid(chat, config, channelConfig) {
303
304
  return chat.chatGuid
304
305
  ?? await resolveChatGuidForIdentifier(config, channelConfig, chat.chatIdentifier ?? "");
305
306
  }
306
- function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(), channelConfig = (0, config_1.getBlueBubblesChannelConfig)()) {
307
+ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(), channelConfig = (0, config_1.getBlueBubblesChannelConfig)(), hostOptions = {}) {
308
+ // Everything that acts as the signed-in Apple ID first confirms the server
309
+ // runs under the agent's declared macOS host account.
310
+ // Sends recheck every time; quieter actions may reuse a recent confirmation.
311
+ const assertHostAccount = (options = {}) => (0, host_account_verifier_1.verifyBlueBubblesServerAccount)({
312
+ agent: hostOptions.agent ?? (0, identity_1.getAgentName)(),
313
+ hostUsername: "hostUsername" in hostOptions
314
+ ? hostOptions.hostUsername
315
+ : (0, identity_1.loadAgentConfig)().senses?.bluebubbles.hostUsername,
316
+ serverUrl: config.serverUrl,
317
+ password: config.password,
318
+ requestTimeoutMs: channelConfig.requestTimeoutMs,
319
+ fresh: options.fresh,
320
+ });
307
321
  return {
308
322
  async sendText(params) {
309
323
  const trimmedText = params.text.trim();
310
324
  if (!trimmedText) {
311
325
  throw new Error("BlueBubbles send requires non-empty text.");
312
326
  }
327
+ await assertHostAccount({ fresh: true });
313
328
  const resolvedChatGuid = await resolveChatGuid(params.chat, config, channelConfig);
314
329
  if (!resolvedChatGuid) {
315
330
  throw new Error("BlueBubbles send currently requires chat.chatGuid from the inbound event.");
@@ -411,6 +426,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
411
426
  return { messageGuid };
412
427
  },
413
428
  async editMessage(params) {
429
+ await assertHostAccount();
414
430
  const messageGuid = params.messageGuid.trim();
415
431
  const text = params.text.trim();
416
432
  if (!messageGuid) {
@@ -437,6 +453,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
437
453
  }
438
454
  },
439
455
  async setTyping(chat, typing, signal) {
456
+ await assertHostAccount();
440
457
  const resolvedChatGuid = await resolveChatGuid(chat, config, channelConfig);
441
458
  if (!resolvedChatGuid) {
442
459
  return;
@@ -453,6 +470,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
453
470
  }
454
471
  },
455
472
  async markChatRead(chat, signal) {
473
+ await assertHostAccount();
456
474
  const resolvedChatGuid = await resolveChatGuid(chat, config, channelConfig);
457
475
  if (!resolvedChatGuid) {
458
476
  return;
@@ -497,6 +515,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
497
515
  });
498
516
  throw new Error(result.detail);
499
517
  }
518
+ await assertHostAccount();
500
519
  (0, runtime_1.emitNervesEvent)({
501
520
  component: "senses",
502
521
  event: "senses.bluebubbles_healthcheck_end",
@@ -507,6 +526,8 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
507
526
  async queryRecentMessagesWithMetadata(params = {}) {
508
527
  const limit = Math.max(1, Math.min(100, Math.floor(params.limit ?? 50)));
509
528
  const offset = Math.max(0, Math.floor(params.offset ?? 0));
529
+ // Reading messages is gated like sending: never read another account's iMessages.
530
+ await assertHostAccount();
510
531
  const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/query", config.password);
511
532
  (0, runtime_1.emitNervesEvent)({
512
533
  component: "senses",
@@ -618,6 +639,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
618
639
  eventType: event.eventType,
619
640
  },
620
641
  });
642
+ await assertHostAccount();
621
643
  const url = buildRepairUrl(config.serverUrl, event.messageGuid, config.password);
622
644
  try {
623
645
  const response = await fetch(url, {
@@ -748,6 +770,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
748
770
  }
749
771
  },
750
772
  async getMessageText(messageGuid) {
773
+ await assertHostAccount();
751
774
  const url = buildRepairUrl(config.serverUrl, messageGuid, config.password);
752
775
  try {
753
776
  const response = await fetch(url, {
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BlueBubblesHostAccountError = void 0;
4
+ exports.resetBlueBubblesServerAccountCache = resetBlueBubblesServerAccountCache;
5
+ exports.verifyBlueBubblesServerAccount = verifyBlueBubblesServerAccount;
6
+ const runtime_1 = require("../../nerves/runtime");
7
+ /**
8
+ * BlueBubbles acts as whichever Apple ID is signed in to the macOS account its
9
+ * server runs under, wherever Ouro itself runs. Before anything that acts as
10
+ * that Apple ID (sending, editing, typing, read receipts), confirm the server
11
+ * reports the agent's declared host account; the inbound webhook and its
12
+ * registration are gated the same way, so the agent neither speaks nor listens
13
+ * as another account's Apple ID. BlueBubbles server info reports `computer_id`
14
+ * as `<macOS username>@<hostname>`. Anything unconfirmed fails closed.
15
+ */
16
+ class BlueBubblesHostAccountError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "BlueBubblesHostAccountError";
20
+ }
21
+ }
22
+ exports.BlueBubblesHostAccountError = BlueBubblesHostAccountError;
23
+ const VERIFIED_TTL_MS = 60_000;
24
+ const verifiedAt = new Map();
25
+ /** Test seam: forget cached verifications. */
26
+ function resetBlueBubblesServerAccountCache() {
27
+ verifiedAt.clear();
28
+ }
29
+ function refuse(message, meta) {
30
+ (0, runtime_1.emitNervesEvent)({
31
+ level: "error",
32
+ component: "senses",
33
+ event: "senses.bluebubbles_host_account_refused",
34
+ message: "refused BlueBubbles action outside the agent's declared host account",
35
+ meta,
36
+ });
37
+ throw new BlueBubblesHostAccountError(message);
38
+ }
39
+ function serverInfoUrl(serverUrl, password) {
40
+ const root = serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
41
+ const url = new URL("api/v1/server/info", root);
42
+ url.searchParams.set("password", password);
43
+ return url.toString();
44
+ }
45
+ async function readServerUsername(input) {
46
+ try {
47
+ const response = await (input.fetchImpl ?? fetch)(serverInfoUrl(input.serverUrl, input.password), {
48
+ method: "GET",
49
+ signal: AbortSignal.timeout(input.requestTimeoutMs),
50
+ });
51
+ if (!response.ok)
52
+ return null;
53
+ const body = await response.json();
54
+ const computerId = body?.data?.computer_id;
55
+ if (typeof computerId !== "string")
56
+ return null;
57
+ const at = computerId.indexOf("@");
58
+ const username = at > 0 ? computerId.slice(0, at) : "";
59
+ return username || null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ async function verifyBlueBubblesServerAccount(input) {
66
+ const { agent, hostUsername, serverUrl } = input;
67
+ if (hostUsername === undefined) {
68
+ refuse(`BlueBubbles refused for agent "${agent}": no host account is declared (senses.bluebubbles.hostUsername), so Ouro cannot tell whose Apple ID this is. `
69
+ + "Not acting. Fix: a human sets senses.bluebubbles.hostUsername in agent.json to the macOS account signed in to the agent's own Apple ID.", { agent, reason: "unset" });
70
+ }
71
+ const now = (input.now ?? Date.now)();
72
+ const key = `${serverUrl}\u0000${hostUsername}`;
73
+ const cached = verifiedAt.get(key);
74
+ if (!input.fresh && cached !== undefined && now - cached < VERIFIED_TTL_MS)
75
+ return;
76
+ const serverUsername = await readServerUsername(input);
77
+ if (serverUsername === null) {
78
+ verifiedAt.delete(key);
79
+ refuse(`BlueBubbles refused for agent "${agent}": could not confirm which macOS account the BlueBubbles server at ${serverUrl} runs under. Not acting.`, { agent, reason: "unconfirmed", hostUsername });
80
+ }
81
+ if (serverUsername !== hostUsername) {
82
+ verifiedAt.delete(key);
83
+ refuse(`BlueBubbles refused for agent "${agent}": the BlueBubbles server at ${serverUrl} runs under macOS account "${serverUsername}", `
84
+ + `but the agent's iMessage host is "${hostUsername}". Not acting, so the agent neither speaks nor listens as "${serverUsername}"'s Apple ID. `
85
+ + `Fix: run BlueBubbles under "${hostUsername}" and point bluebubbles.serverUrl at it. Changing senses.bluebubbles.hostUsername is a human decision, because it chooses whose Apple ID the agent speaks as.`, { agent, reason: "mismatch", hostUsername, serverUsername });
86
+ }
87
+ verifiedAt.set(key, now);
88
+ }
@@ -78,6 +78,7 @@ const proactive_content_guard_1 = require("../proactive-content-guard");
78
78
  const bluebubbles_meta_guard_1 = require("../bluebubbles-meta-guard");
79
79
  const model_1 = require("./model");
80
80
  const client_1 = require("./client");
81
+ const host_account_verifier_1 = require("./host-account-verifier");
81
82
  const webhook_registration_1 = require("./webhook-registration");
82
83
  const inbound_log_1 = require("./inbound-log");
83
84
  const mutation_log_1 = require("./mutation-log");
@@ -308,6 +309,16 @@ const defaultDeps = {
308
309
  },
309
310
  recordProcessed: processed_log_1.recordProcessedBlueBubblesMessage,
310
311
  withSessionTurnLease: session_transaction_1.withSessionTurnLease,
312
+ // Incoming events recheck every time, like sends, so a server that takes over
313
+ // the same URL under another account is refused immediately.
314
+ verifyHostAccount: () => (0, host_account_verifier_1.verifyBlueBubblesServerAccount)({
315
+ agent: (0, identity_1.getAgentName)(),
316
+ hostUsername: (0, identity_1.loadAgentConfig)().senses?.bluebubbles.hostUsername,
317
+ serverUrl: (0, config_1.getBlueBubblesConfig)().serverUrl,
318
+ password: (0, config_1.getBlueBubblesConfig)().password,
319
+ requestTimeoutMs: (0, config_1.getBlueBubblesChannelConfig)().requestTimeoutMs,
320
+ fresh: true,
321
+ }),
311
322
  };
312
323
  const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
313
324
  const BLUEBUBBLES_RECOVERY_PASS_DELAY_MS = 1_000;
@@ -2804,6 +2815,22 @@ async function recoverQueuedBlueBubblesMessages(deps = {}, options = {}) {
2804
2815
  if (initialPending === 0) {
2805
2816
  return { recovered: 0, skipped: 0, failed: 0, pendingRecoveryCount: 0 };
2806
2817
  }
2818
+ // Recovery re-reads queued messages from the server, so it only runs against
2819
+ // a server confirmed to be under the agent's own host account. Queued work
2820
+ // stays queued (not failed) until it is.
2821
+ try {
2822
+ await resolvedDeps.verifyHostAccount();
2823
+ }
2824
+ catch (error) {
2825
+ (0, runtime_1.emitNervesEvent)({
2826
+ level: "warn",
2827
+ component: "senses",
2828
+ event: "senses.bluebubbles_recovery_host_refused",
2829
+ message: "skipped bluebubbles recovery because the server's host account is not confirmed",
2830
+ meta: { pendingRecoveryCount: initialPending, reason: error instanceof Error ? error.message : String(error) },
2831
+ });
2832
+ return { recovered: 0, skipped: 0, failed: 0, pendingRecoveryCount: initialPending };
2833
+ }
2807
2834
  const captured = await recoverCapturedBlueBubblesInboundMessages(resolvedDeps, options);
2808
2835
  options.lifecycleSignal?.throwIfAborted();
2809
2836
  const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
@@ -3198,6 +3225,15 @@ function createBlueBubblesWebhookHandler(deps = {}, options = {}) {
3198
3225
  writeJson(res, 401, { error: "Unauthorized" });
3199
3226
  return;
3200
3227
  }
3228
+ // Only take in messages from the agent's own Apple ID: a server running
3229
+ // under another macOS account would hand the agent that person's iMessages.
3230
+ try {
3231
+ await { ...defaultDeps, ...deps }.verifyHostAccount();
3232
+ }
3233
+ catch (error) {
3234
+ writeJson(res, 403, { handled: false, notifiedAgent: false, error: error instanceof Error ? error.message : String(error) });
3235
+ return;
3236
+ }
3201
3237
  let payload;
3202
3238
  try {
3203
3239
  const rawBody = await readRequestBody(req);
@@ -3828,6 +3864,7 @@ function startBlueBubblesApp(deps = {}) {
3828
3864
  machineId: (0, machine_identity_1.loadOrCreateMachineIdentity)().machineId,
3829
3865
  requestTimeoutMs: channelConfig.requestTimeoutMs,
3830
3866
  listenerReady: true,
3867
+ hostUsername: (0, identity_1.loadAgentConfig)().senses?.bluebubbles.hostUsername,
3831
3868
  });
3832
3869
  (0, runtime_1.emitNervesEvent)({
3833
3870
  component: "channels",
@@ -9,6 +9,7 @@ exports.reconcileBlueBubblesWebhookRegistration = reconcileBlueBubblesWebhookReg
9
9
  exports.createBlueBubblesWebhookReconciler = createBlueBubblesWebhookReconciler;
10
10
  const crypto_1 = require("crypto");
11
11
  const runtime_1 = require("../../nerves/runtime");
12
+ const host_account_verifier_1 = require("./host-account-verifier");
12
13
  exports.BLUEBUBBLES_WEBHOOK_RECONCILE_INTERVAL_MS = 180_000;
13
14
  function result(state, detail, counts = {}) {
14
15
  return {
@@ -131,10 +132,35 @@ function inspectHooks(hooks, input) {
131
132
  exactCount: exact.length,
132
133
  });
133
134
  }
135
+ /**
136
+ * A webhook subscribes the agent to every message of the Apple ID the server runs
137
+ * as, so registering or inspecting one first confirms that is the agent's own
138
+ * declared host account. Returns a refusal result, or null when confirmed.
139
+ */
140
+ async function refuseForeignHostAccount(input, fetchImpl) {
141
+ try {
142
+ await (0, host_account_verifier_1.verifyBlueBubblesServerAccount)({
143
+ agent: input.agentName,
144
+ hostUsername: input.hostUsername,
145
+ serverUrl: input.serverUrl,
146
+ password: input.password,
147
+ requestTimeoutMs: input.requestTimeoutMs,
148
+ fetchImpl,
149
+ });
150
+ return null;
151
+ }
152
+ catch (error) {
153
+ return result("host-account-refused", error instanceof Error ? error.message : String(error));
154
+ }
155
+ }
134
156
  async function inspectBlueBubblesWebhookRegistration(input, deps = {}) {
135
157
  if (!input.listenerReady)
136
158
  return result("listener-not-ready", "the local BlueBubbles listener is not ready");
137
- const listed = await readHooks(input, deps.fetchImpl ?? fetch, deps.signal);
159
+ const fetchImpl = deps.fetchImpl ?? fetch;
160
+ const refused = await refuseForeignHostAccount(input, fetchImpl);
161
+ if (refused)
162
+ return refused;
163
+ const listed = await readHooks(input, fetchImpl, deps.signal);
138
164
  return "hooks" in listed ? inspectHooks(listed.hooks, input) : listed;
139
165
  }
140
166
  async function mutate(input, fetchImpl, method, id, signal) {
@@ -152,6 +178,9 @@ async function reconcileBlueBubblesWebhookRegistration(input, deps = {}) {
152
178
  if (!input.listenerReady)
153
179
  return result("listener-not-ready", "the local BlueBubbles listener is not ready");
154
180
  const fetchImpl = deps.fetchImpl ?? fetch;
181
+ const refused = await refuseForeignHostAccount(input, fetchImpl);
182
+ if (refused)
183
+ return refused;
155
184
  const listed = await readHooks(input, fetchImpl, deps.signal);
156
185
  if (!("hooks" in listed))
157
186
  return listed;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.914",
3
+ "version": "0.1.0-alpha.915",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.914",
9
+ "version": "0.1.0-alpha.915",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.914",
3
+ "version": "0.1.0-alpha.915",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },