@ouro.bot/cli 0.1.0-alpha.914 → 0.1.0-alpha.916
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 +13 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/bluebubbles-host-account.js +78 -0
- package/dist/heart/daemon/cli-arg-redaction.js +26 -0
- package/dist/heart/daemon/cli-exec.js +64 -2
- package/dist/heart/daemon/doctor.js +12 -1
- package/dist/heart/daemon/ouro-bot-entry.js +2 -1
- package/dist/heart/daemon/ouro-entry.js +2 -1
- package/dist/heart/daemon/runtime-logging.js +1 -1
- package/dist/heart/daemon/sense-manager.js +18 -1
- package/dist/heart/identity.js +15 -0
- package/dist/heart/turn-context.js +3 -1
- package/dist/heart/versioning/ouro-bot-wrapper.js +2 -1
- package/dist/rsvp/cli.js +12 -2
- package/dist/senses/bluebubbles/client.js +24 -1
- package/dist/senses/bluebubbles/host-account-verifier.js +88 -0
- package/dist/senses/bluebubbles/index.js +37 -0
- package/dist/senses/bluebubbles/webhook-registration.js +30 -1
- package/dist/senses/teams-entry.js +10 -2
- package/dist/senses/teams.js +92 -29
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
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.916",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Security: the Teams sense waits for its bot credentials before listening and refuses to start without them (it used to start early in a developer mode that accepted messages from anyone); the daemon log no longer records values passed with --value, --secret, --password or --token.",
|
|
8
|
+
"Teams replies arrive: the reply was sent without a message type, so Teams dropped it and the chat showed a status bubble until \"This response was stopped\"; replies now finish with one AI label, no leading hourglass, and the status shows the current reasoning step's heading instead of raw reasoning. Delivery failures, and the Teams SDK's own warnings and errors, now reach the Teams log."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.915",
|
|
13
|
+
"changes": [
|
|
14
|
+
"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."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.914",
|
|
6
19
|
"changes": [
|
|
@@ -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.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.916</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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactCliArgs = redactCliArgs;
|
|
4
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
5
|
+
/** Flags whose next argument is a value the caller may consider secret. */
|
|
6
|
+
const VALUE_FLAGS = new Set(["--value", "--secret", "--password", "--token"]);
|
|
7
|
+
/**
|
|
8
|
+
* A command line that is safe to log: the argument after a value flag, and the value in
|
|
9
|
+
* `--flag=value`, is replaced. `ouro vault config set --value <secret>` is a supported way to
|
|
10
|
+
* store a credential, and the daemon log is not a place for it.
|
|
11
|
+
*/
|
|
12
|
+
function redactCliArgs(args) {
|
|
13
|
+
let redacted = 0;
|
|
14
|
+
const safe = args.map((arg, index) => {
|
|
15
|
+
const [flag] = arg.split("=", 1);
|
|
16
|
+
const afterFlag = index > 0 && VALUE_FLAGS.has(args[index - 1]);
|
|
17
|
+
const inline = arg.includes("=") && VALUE_FLAGS.has(flag);
|
|
18
|
+
if (afterFlag || inline)
|
|
19
|
+
redacted += 1;
|
|
20
|
+
return afterFlag ? "[redacted]" : inline ? `${flag}=[redacted]` : arg;
|
|
21
|
+
});
|
|
22
|
+
if (redacted > 0) {
|
|
23
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.cli_args_redacted", message: "kept command-line values out of the log", meta: { redacted } });
|
|
24
|
+
}
|
|
25
|
+
return safe;
|
|
26
|
+
}
|
|
@@ -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
|
-
|
|
3785
|
-
|
|
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",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const cli_arg_redaction_1 = require("./cli-arg-redaction");
|
|
4
5
|
const runtime_1 = require("../../nerves/runtime");
|
|
5
6
|
const runtime_logging_1 = require("./runtime-logging");
|
|
6
7
|
const ouro_bot_wrapper_1 = require("../versioning/ouro-bot-wrapper");
|
|
@@ -9,7 +10,7 @@ const ouro_bot_wrapper_1 = require("../versioning/ouro-bot-wrapper");
|
|
|
9
10
|
component: "daemon",
|
|
10
11
|
event: "daemon.ouro_bot_entry_start",
|
|
11
12
|
message: "starting ouro.bot wrapper entrypoint",
|
|
12
|
-
meta: { args: process.argv.slice(2) },
|
|
13
|
+
meta: { args: (0, cli_arg_redaction_1.redactCliArgs)(process.argv.slice(2)) },
|
|
13
14
|
});
|
|
14
15
|
void (0, ouro_bot_wrapper_1.runOuroBotWrapper)(process.argv.slice(2)).catch((error) => {
|
|
15
16
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const runtime_1 = require("../../nerves/runtime");
|
|
5
|
+
const cli_arg_redaction_1 = require("./cli-arg-redaction");
|
|
5
6
|
const daemon_cli_1 = require("./daemon-cli");
|
|
6
7
|
const runtime_logging_1 = require("./runtime-logging");
|
|
7
8
|
(0, runtime_logging_1.configureDaemonRuntimeLogger)("ouro");
|
|
@@ -9,7 +10,7 @@ const runtime_logging_1 = require("./runtime-logging");
|
|
|
9
10
|
component: "daemon",
|
|
10
11
|
event: "daemon.cli_entry_start",
|
|
11
12
|
message: "starting ouro CLI entrypoint",
|
|
12
|
-
meta: { args: process.argv.slice(2) },
|
|
13
|
+
meta: { args: (0, cli_arg_redaction_1.redactCliArgs)(process.argv.slice(2)) },
|
|
13
14
|
});
|
|
14
15
|
void (0, daemon_cli_1.runOuroCli)(process.argv.slice(2)).catch((error) => {
|
|
15
16
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -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
|
|
884
|
+
return currentRow();
|
|
868
885
|
}
|
|
869
886
|
listSenseRows() {
|
|
870
887
|
const runtime = new Map();
|
package/dist/heart/identity.js
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.runOuroBotWrapper = runOuroBotWrapper;
|
|
37
|
+
const cli_arg_redaction_1 = require("../daemon/cli-arg-redaction");
|
|
37
38
|
const runtime_1 = require("../../nerves/runtime");
|
|
38
39
|
const daemon_cli_1 = require("../daemon/daemon-cli");
|
|
39
40
|
async function defaultLoadCanonicalRunner() {
|
|
@@ -57,7 +58,7 @@ async function runOuroBotWrapper(args, deps = {}) {
|
|
|
57
58
|
component: "daemon",
|
|
58
59
|
event: "daemon.ouro_bot_wrapper_start",
|
|
59
60
|
message: "starting ouro.bot wrapper delegation",
|
|
60
|
-
meta: { args },
|
|
61
|
+
meta: { args: (0, cli_arg_redaction_1.redactCliArgs)(args) },
|
|
61
62
|
});
|
|
62
63
|
const loadCanonicalRunner = deps.loadCanonicalRunner ?? defaultLoadCanonicalRunner;
|
|
63
64
|
const fallbackRunCli = deps.fallbackRunCli ?? daemon_cli_1.runOuroCli;
|
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
|
|
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;
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const runtime_logging_1 = require("../heart/daemon/runtime-logging");
|
|
36
37
|
const runtime_1 = require("../nerves/runtime");
|
|
37
38
|
// Fail fast if --agent is missing (before any src/ code tries getAgentName())
|
|
38
39
|
const agentArgIndex = process.argv.indexOf("--agent");
|
|
@@ -42,14 +43,21 @@ if (!agentName) {
|
|
|
42
43
|
console.error("Missing required --agent <name> argument.\nUsage: node dist/senses/teams-entry.js --agent ouroboros");
|
|
43
44
|
process.exit(1);
|
|
44
45
|
}
|
|
46
|
+
// Without this the sense's events go to a console nobody reads: the daemon discards its output.
|
|
47
|
+
(0, runtime_logging_1.configureDaemonRuntimeLogger)("teams");
|
|
45
48
|
(0, runtime_1.emitNervesEvent)({
|
|
46
49
|
component: "senses",
|
|
47
50
|
event: "senses.entry_boot",
|
|
48
51
|
message: "booting Teams entrypoint",
|
|
49
52
|
meta: { entry: "teams", agentName },
|
|
50
53
|
});
|
|
51
|
-
Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"))).then(async ({ refreshRuntimeCredentialConfig }) => {
|
|
52
|
-
|
|
54
|
+
Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"))).then(async ({ waitForRuntimeCredentialBootstrap, readRuntimeCredentialConfig, refreshRuntimeCredentialConfig }) => {
|
|
55
|
+
// The bot's credentials must be loaded before the listener starts: without them the Teams SDK
|
|
56
|
+
// validates no sender, and startTeamsApp refuses to start.
|
|
57
|
+
const bootstrapped = await waitForRuntimeCredentialBootstrap(agentName);
|
|
58
|
+
if (!bootstrapped && !readRuntimeCredentialConfig(agentName).ok) {
|
|
59
|
+
await refreshRuntimeCredentialConfig(agentName, { preserveCachedOnFailure: true }).catch(() => undefined);
|
|
60
|
+
}
|
|
53
61
|
const { startTeamsApp } = await Promise.resolve().then(() => __importStar(require("./teams")));
|
|
54
62
|
startTeamsApp();
|
|
55
63
|
})
|
package/dist/senses/teams.js
CHANGED
|
@@ -41,8 +41,10 @@ exports.sanitizeFeedbackComment = sanitizeFeedbackComment;
|
|
|
41
41
|
exports.buildFeedbackSyntheticText = buildFeedbackSyntheticText;
|
|
42
42
|
exports.buildWelcomeCard = buildWelcomeCard;
|
|
43
43
|
exports.createTeamsCallbacks = createTeamsCallbacks;
|
|
44
|
+
exports.reasoningStatus = reasoningStatus;
|
|
44
45
|
exports.withConversationLock = withConversationLock;
|
|
45
46
|
exports.handleTeamsMessage = handleTeamsMessage;
|
|
47
|
+
exports.createTeamsSdkLogger = createTeamsSdkLogger;
|
|
46
48
|
exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
|
|
47
49
|
exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
|
|
48
50
|
exports.startTeamsApp = startTeamsApp;
|
|
@@ -206,14 +208,29 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
206
208
|
let lastPhrase = "";
|
|
207
209
|
let flushTimer = null;
|
|
208
210
|
const flushInterval = options?.flushIntervalMs ?? exports.DEFAULT_FLUSH_INTERVAL_MS;
|
|
211
|
+
// The SDK merges every chunk's entities into the final message, and Teams rejects a final
|
|
212
|
+
// message with more than one root message entity (400 BadSyntax), so only the first chunk
|
|
213
|
+
// carries the AI label.
|
|
214
|
+
let aiLabelAttached = false;
|
|
215
|
+
function streamChunk(text) {
|
|
216
|
+
if (aiLabelAttached)
|
|
217
|
+
return { type: "message", text, channelData: { feedbackLoopEnabled: true } };
|
|
218
|
+
aiLabelAttached = true;
|
|
219
|
+
return { type: "message", text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } };
|
|
220
|
+
}
|
|
209
221
|
// Track whether reasoning has changed since last flush (avoid redundant updates).
|
|
210
222
|
let lastFlushedReasoningLen = 0;
|
|
223
|
+
let lastReasoningStatus = "";
|
|
211
224
|
// Periodic tick: flush text buffer and push reasoning updates.
|
|
212
225
|
function flushTick() {
|
|
213
226
|
flushTextBuffer();
|
|
214
227
|
if (reasoningBuf.length > lastFlushedReasoningLen) {
|
|
215
|
-
safeUpdate(reasoningBuf);
|
|
216
228
|
lastFlushedReasoningLen = reasoningBuf.length;
|
|
229
|
+
const status = reasoningStatus(reasoningBuf);
|
|
230
|
+
if (status && status !== lastReasoningStatus) {
|
|
231
|
+
lastReasoningStatus = status;
|
|
232
|
+
safeUpdate(status);
|
|
233
|
+
}
|
|
217
234
|
}
|
|
218
235
|
}
|
|
219
236
|
// Start the periodic flush timer. Idempotent -- no-op if already running.
|
|
@@ -230,9 +247,16 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
230
247
|
}
|
|
231
248
|
}
|
|
232
249
|
// Mark stream as broken and abort the agent loop.
|
|
233
|
-
function markStopped() {
|
|
250
|
+
function markStopped(where, err) {
|
|
234
251
|
if (stopped)
|
|
235
252
|
return;
|
|
253
|
+
(0, runtime_1.emitNervesEvent)({
|
|
254
|
+
level: "warn",
|
|
255
|
+
component: "senses",
|
|
256
|
+
event: "teams.stream_stopped",
|
|
257
|
+
message: `teams stream stopped at ${where}`,
|
|
258
|
+
meta: { where, error: err instanceof Error ? err.message : err === undefined ? null : String(err), conversationId: options?.conversationId ?? null },
|
|
259
|
+
});
|
|
236
260
|
stopped = true;
|
|
237
261
|
stopPhraseRotation();
|
|
238
262
|
stopFlushTimer();
|
|
@@ -248,7 +272,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
248
272
|
// catch its rejection so we detect a dead stream and abort the agent loop.
|
|
249
273
|
function catchAsync(result) {
|
|
250
274
|
if (result && typeof result.catch === "function") {
|
|
251
|
-
result.catch(() => markStopped());
|
|
275
|
+
result.catch((err) => markStopped("stream-async", err));
|
|
252
276
|
}
|
|
253
277
|
}
|
|
254
278
|
// Safely emit a text delta to the stream with AI labels.
|
|
@@ -258,12 +282,12 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
258
282
|
if (stopped)
|
|
259
283
|
return;
|
|
260
284
|
try {
|
|
261
|
-
catchAsync(stream.emit(
|
|
285
|
+
catchAsync(stream.emit(streamChunk(text)));
|
|
262
286
|
streamHasContent = true;
|
|
263
287
|
totalEmitted += text.length;
|
|
264
288
|
}
|
|
265
|
-
catch {
|
|
266
|
-
markStopped();
|
|
289
|
+
catch (err) {
|
|
290
|
+
markStopped("safeEmit", err);
|
|
267
291
|
}
|
|
268
292
|
}
|
|
269
293
|
// Awaitable emit — returns true if the emit succeeded, false if it failed.
|
|
@@ -275,15 +299,15 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
275
299
|
try {
|
|
276
300
|
// stream.emit() is typed as void but the Teams SDK returns a Promise
|
|
277
301
|
// internally (async HTTP). Cast to capture the result for awaiting.
|
|
278
|
-
const result = stream.emit(
|
|
302
|
+
const result = stream.emit(streamChunk(text));
|
|
279
303
|
streamHasContent = true;
|
|
280
304
|
if (result && typeof result.then === "function") {
|
|
281
305
|
await result;
|
|
282
306
|
}
|
|
283
307
|
return true;
|
|
284
308
|
}
|
|
285
|
-
catch {
|
|
286
|
-
markStopped();
|
|
309
|
+
catch (err) {
|
|
310
|
+
markStopped("tryEmit", err);
|
|
287
311
|
return false;
|
|
288
312
|
}
|
|
289
313
|
}
|
|
@@ -296,7 +320,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
296
320
|
// responsible for the `!stopped` precondition; no defensive guard here.
|
|
297
321
|
async function tryEmitNoAbort(text) {
|
|
298
322
|
try {
|
|
299
|
-
const result = stream.emit(
|
|
323
|
+
const result = stream.emit(streamChunk(text));
|
|
300
324
|
streamHasContent = true;
|
|
301
325
|
if (result && typeof result.then === "function") {
|
|
302
326
|
await result;
|
|
@@ -315,8 +339,8 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
315
339
|
try {
|
|
316
340
|
catchAsync(stream.update(text));
|
|
317
341
|
}
|
|
318
|
-
catch {
|
|
319
|
-
markStopped();
|
|
342
|
+
catch (err) {
|
|
343
|
+
markStopped("safeUpdate", err);
|
|
320
344
|
}
|
|
321
345
|
}
|
|
322
346
|
// Safely send a separate message via sendMessage (for content after first emit).
|
|
@@ -331,11 +355,11 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
331
355
|
// Chain is idle -- start the send synchronously and mark busy
|
|
332
356
|
sendChainBusy = true;
|
|
333
357
|
try {
|
|
334
|
-
sendChain = sendMessage(text).catch(() => markStopped()).finally(() => { sendChainBusy = false; });
|
|
358
|
+
sendChain = sendMessage(text).catch((err) => markStopped("send", err)).finally(() => { sendChainBusy = false; });
|
|
335
359
|
}
|
|
336
|
-
catch {
|
|
360
|
+
catch (err) {
|
|
337
361
|
sendChainBusy = false;
|
|
338
|
-
markStopped();
|
|
362
|
+
markStopped("send", err);
|
|
339
363
|
}
|
|
340
364
|
}
|
|
341
365
|
else {
|
|
@@ -344,7 +368,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
344
368
|
if (stopped)
|
|
345
369
|
return;
|
|
346
370
|
return sendMessage(text);
|
|
347
|
-
}).catch(() => markStopped());
|
|
371
|
+
}).catch((err) => markStopped("send-queued", err));
|
|
348
372
|
}
|
|
349
373
|
}
|
|
350
374
|
// Flush accumulated text buffer via safeEmit. The Teams SDK accumulates
|
|
@@ -429,9 +453,12 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
429
453
|
/* v8 ignore next -- defensive guard: stopped set by prior error @preserve */
|
|
430
454
|
if (stopped)
|
|
431
455
|
return;
|
|
432
|
-
stopPhraseRotation();
|
|
433
|
-
hadRealOutput = true;
|
|
434
456
|
reasoningBuf += text;
|
|
457
|
+
// Keep the friendly phrases until a reasoning heading can replace them.
|
|
458
|
+
if (reasoningStatus(reasoningBuf)) {
|
|
459
|
+
stopPhraseRotation();
|
|
460
|
+
hadRealOutput = true;
|
|
461
|
+
}
|
|
435
462
|
startFlushTimer();
|
|
436
463
|
},
|
|
437
464
|
onTextChunk: (text) => {
|
|
@@ -491,7 +518,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
491
518
|
// All delivery paths exhausted — now it is correct to abort the turn.
|
|
492
519
|
// markStopped() halts further stream activity and aborts the controller
|
|
493
520
|
// so the engine catches up and ends the turn cleanly.
|
|
494
|
-
markStopped();
|
|
521
|
+
markStopped("speak", lastError);
|
|
495
522
|
throw new Error(`teams speak delivery failed: ${lastError?.message ?? "no fallback available"}`);
|
|
496
523
|
}
|
|
497
524
|
},
|
|
@@ -508,7 +535,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
508
535
|
onToolStart: (name, args) => {
|
|
509
536
|
// speak is flow-control: its visible output is the message itself
|
|
510
537
|
// (delivered via onTextChunk + flushNow). Do NOT stop phrase rotation
|
|
511
|
-
// here, do NOT
|
|
538
|
+
// here, do NOT post a tool-activity
|
|
512
539
|
// status update \u2014 all of those would create UI churn right before the
|
|
513
540
|
// actual speak content arrives.
|
|
514
541
|
if (name === "speak")
|
|
@@ -517,13 +544,6 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
517
544
|
// Force-flush any accumulated text, bypassing MIN_INITIAL_CHARS threshold
|
|
518
545
|
firstContentEmitted = true;
|
|
519
546
|
flushTextBuffer();
|
|
520
|
-
// Emit a placeholder to satisfy the 15s Copilot timeout for initial
|
|
521
|
-
// stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
|
|
522
|
-
// never emit before the timeout and the user sees "this response was
|
|
523
|
-
// stopped". The placeholder is replaced by actual content on next emit.
|
|
524
|
-
// https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
|
|
525
|
-
if (!streamHasContent)
|
|
526
|
-
safeEmit("\u23f3");
|
|
527
547
|
toolCbs.onToolStart(name, args);
|
|
528
548
|
hadToolRun = true;
|
|
529
549
|
},
|
|
@@ -577,7 +597,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
577
597
|
// and fall through to sendMessage recovery.
|
|
578
598
|
const ok = await tryEmit(text);
|
|
579
599
|
if (!ok)
|
|
580
|
-
markStopped();
|
|
600
|
+
markStopped("flush");
|
|
581
601
|
}
|
|
582
602
|
if (stopped && !streamFinalized && sendMessage) {
|
|
583
603
|
// Stream is dead (not from finalization) — fall back to sendMessage; split on failure as recovery.
|
|
@@ -594,9 +614,22 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
594
614
|
else if (!streamHasContent && !options?.suppressEmptyStreamMessage) {
|
|
595
615
|
safeEmit("(completed with tool calls only — no text response)");
|
|
596
616
|
}
|
|
617
|
+
(0, runtime_1.emitNervesEvent)({
|
|
618
|
+
component: "senses",
|
|
619
|
+
event: "teams.flush_done",
|
|
620
|
+
message: "teams end-of-turn flush finished",
|
|
621
|
+
meta: { stopped, streamHasContent, streamFinalized, conversationId: options?.conversationId ?? null },
|
|
622
|
+
});
|
|
597
623
|
},
|
|
598
624
|
};
|
|
599
625
|
}
|
|
626
|
+
// The status bubble shows the latest reasoning heading (models write "**Heading**"
|
|
627
|
+
// before each reasoning step), never the raw reasoning text itself.
|
|
628
|
+
function reasoningStatus(reasoning) {
|
|
629
|
+
const headings = [...reasoning.matchAll(/\*\*([^*\n]+)\*\*/g)];
|
|
630
|
+
const last = headings.at(-1)?.[1]?.trim();
|
|
631
|
+
return last ? `${last}…` : null;
|
|
632
|
+
}
|
|
600
633
|
const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
|
|
601
634
|
function teamsTurnKey(conversationId) {
|
|
602
635
|
return `teams:${conversationId}`;
|
|
@@ -891,6 +924,7 @@ function createBotApp(teamsConfig) {
|
|
|
891
924
|
tenantId: teamsConfig.tenantId,
|
|
892
925
|
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
893
926
|
...mentionStripping,
|
|
927
|
+
logger: createTeamsSdkLogger(),
|
|
894
928
|
}),
|
|
895
929
|
mode: "Bot Service (client secret)",
|
|
896
930
|
};
|
|
@@ -903,21 +937,50 @@ function createBotApp(teamsConfig) {
|
|
|
903
937
|
...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
|
|
904
938
|
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
905
939
|
...mentionStripping,
|
|
940
|
+
logger: createTeamsSdkLogger(),
|
|
906
941
|
}),
|
|
907
942
|
mode: "Bot Service (managed identity)",
|
|
908
943
|
};
|
|
909
944
|
}
|
|
910
945
|
else {
|
|
946
|
+
// With no credentials the Teams SDK validates no sender, so anyone who can reach the port can
|
|
947
|
+
// talk to the agent. That is only acceptable when a developer asks for it on their own machine.
|
|
948
|
+
if (process.env.OURO_TEAMS_DEVTOOLS !== "1") {
|
|
949
|
+
throw new Error("Teams bot credentials are missing (teams.clientId), so the Teams sense will not start: without them it would accept messages from anyone. Run `ouro connect teams`, or set OURO_TEAMS_DEVTOOLS=1 for local development with no real bot.");
|
|
950
|
+
}
|
|
911
951
|
return {
|
|
912
952
|
app: new teams_apps_1.App({
|
|
913
953
|
plugins: [new teams_dev_1.DevtoolsPlugin()],
|
|
914
954
|
...mentionStripping,
|
|
955
|
+
logger: createTeamsSdkLogger(),
|
|
915
956
|
}),
|
|
916
957
|
mode: "DevtoolsPlugin",
|
|
917
958
|
};
|
|
918
959
|
}
|
|
919
960
|
}
|
|
920
|
-
|
|
961
|
+
function createTeamsSdkLogger(name = "teams-sdk") {
|
|
962
|
+
const verbose = /stream/i.test(name);
|
|
963
|
+
const write = (level, msg) => {
|
|
964
|
+
if ((level === "debug" || level === "trace" || level === "info") && !verbose)
|
|
965
|
+
return;
|
|
966
|
+
(0, runtime_1.emitNervesEvent)({
|
|
967
|
+
level: level === "error" ? "error" : level === "warn" ? "warn" : "info",
|
|
968
|
+
component: "senses",
|
|
969
|
+
event: "teams.sdk_log",
|
|
970
|
+
message: `[${name}] ${msg.map((m) => (m instanceof Error ? m.message : typeof m === "string" ? m : JSON.stringify(m))).join(" ").slice(0, 300)}`,
|
|
971
|
+
meta: { logger: name, sdkLevel: level },
|
|
972
|
+
});
|
|
973
|
+
};
|
|
974
|
+
return {
|
|
975
|
+
debug: (...m) => write("debug", m),
|
|
976
|
+
info: (...m) => write("info", m),
|
|
977
|
+
warn: (...m) => write("warn", m),
|
|
978
|
+
error: (...m) => write("error", m),
|
|
979
|
+
trace: (...m) => write("trace", m),
|
|
980
|
+
log: (level, ...m) => write(level, m),
|
|
981
|
+
child: (child) => createTeamsSdkLogger(`${name}/${child}`),
|
|
982
|
+
};
|
|
983
|
+
}
|
|
921
984
|
// Register message, verify-state, and error handlers on an App instance.
|
|
922
985
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
923
986
|
function registerBotHandlers(app, label) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.916",
|
|
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.
|
|
9
|
+
"version": "0.1.0-alpha.916",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|