@ouro.bot/cli 0.1.0-alpha.38 → 0.1.0-alpha.39
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 +10 -0
- package/dist/heart/daemon/daemon-cli.js +129 -44
- package/dist/heart/daemon/daemon.js +90 -0
- package/dist/mind/friends/channel.js +27 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/pending.js +8 -0
- package/dist/mind/prompt.js +26 -22
- package/dist/repertoire/tools-base.js +1 -1
- package/dist/repertoire/tools.js +11 -16
- package/dist/senses/bluebubbles-model.js +10 -0
- package/dist/senses/bluebubbles.js +125 -29
- package/dist/senses/cli.js +59 -54
- package/dist/senses/inner-dialog.js +88 -45
- package/dist/senses/pipeline.js +124 -0
- package/dist/senses/teams.js +87 -65
- package/dist/senses/trust-gate.js +113 -2
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared per-turn pipeline for all senses.
|
|
3
|
+
// Senses are thin transport adapters; this module owns the common lifecycle:
|
|
4
|
+
// resolve friend -> trust gate -> load session -> drain pending -> runAgent -> postTurn -> token accumulation.
|
|
5
|
+
//
|
|
6
|
+
// Transport-level concerns (BB API calls, Teams cards, readline) stay in sense adapters.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.handleInboundTurn = handleInboundTurn;
|
|
9
|
+
const runtime_1 = require("../nerves/runtime");
|
|
10
|
+
// ── Pipeline ──────────────────────────────────────────────────────
|
|
11
|
+
async function handleInboundTurn(input) {
|
|
12
|
+
// Step 1: Resolve friend
|
|
13
|
+
const resolvedContext = await input.friendResolver.resolve();
|
|
14
|
+
(0, runtime_1.emitNervesEvent)({
|
|
15
|
+
component: "senses",
|
|
16
|
+
event: "senses.pipeline_start",
|
|
17
|
+
message: "inbound turn pipeline started",
|
|
18
|
+
meta: {
|
|
19
|
+
channel: input.channel,
|
|
20
|
+
friendId: resolvedContext.friend.id,
|
|
21
|
+
senseType: input.capabilities.senseType,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
// Step 2: Trust gate
|
|
25
|
+
const gateInput = {
|
|
26
|
+
friend: resolvedContext.friend,
|
|
27
|
+
provider: input.provider ?? "local",
|
|
28
|
+
externalId: input.externalId ?? "",
|
|
29
|
+
tenantId: input.tenantId,
|
|
30
|
+
channel: input.channel,
|
|
31
|
+
senseType: input.capabilities.senseType,
|
|
32
|
+
isGroupChat: input.isGroupChat ?? false,
|
|
33
|
+
groupHasFamilyMember: input.groupHasFamilyMember ?? false,
|
|
34
|
+
hasExistingGroupWithFamily: input.hasExistingGroupWithFamily ?? false,
|
|
35
|
+
};
|
|
36
|
+
const gateResult = input.enforceTrustGate(gateInput);
|
|
37
|
+
// Gate rejection: return early, no agent turn
|
|
38
|
+
if (!gateResult.allowed) {
|
|
39
|
+
(0, runtime_1.emitNervesEvent)({
|
|
40
|
+
component: "senses",
|
|
41
|
+
event: "senses.pipeline_gate_reject",
|
|
42
|
+
message: "trust gate rejected inbound turn",
|
|
43
|
+
meta: {
|
|
44
|
+
channel: input.channel,
|
|
45
|
+
friendId: resolvedContext.friend.id,
|
|
46
|
+
reason: gateResult.reason,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
resolvedContext,
|
|
51
|
+
gateResult,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Step 3: Load/create session
|
|
55
|
+
const session = await input.sessionLoader.loadOrCreate();
|
|
56
|
+
const sessionMessages = session.messages;
|
|
57
|
+
// Step 4: Drain pending messages
|
|
58
|
+
const pending = input.drainPending(input.pendingDir);
|
|
59
|
+
// Assemble messages: session messages + pending (formatted) + inbound user messages
|
|
60
|
+
if (pending.length > 0) {
|
|
61
|
+
// Format pending messages and prepend to the user content
|
|
62
|
+
const pendingSection = pending
|
|
63
|
+
.map((msg) => `[pending from ${msg.from}]: ${msg.content}`)
|
|
64
|
+
.join("\n");
|
|
65
|
+
// If there are inbound user messages, prepend pending to the first one
|
|
66
|
+
if (input.messages.length > 0) {
|
|
67
|
+
const firstMsg = input.messages[0];
|
|
68
|
+
if (firstMsg.role === "user") {
|
|
69
|
+
if (typeof firstMsg.content === "string") {
|
|
70
|
+
input.messages[0] = {
|
|
71
|
+
...firstMsg,
|
|
72
|
+
content: `## pending messages\n${pendingSection}\n\n${firstMsg.content}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
input.messages[0] = {
|
|
77
|
+
...firstMsg,
|
|
78
|
+
content: [
|
|
79
|
+
{ type: "text", text: `## pending messages\n${pendingSection}\n\n` },
|
|
80
|
+
...firstMsg.content,
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Append user messages from the inbound turn
|
|
88
|
+
for (const msg of input.messages) {
|
|
89
|
+
sessionMessages.push(msg);
|
|
90
|
+
}
|
|
91
|
+
// Step 5: runAgent
|
|
92
|
+
const existingToolContext = input.runAgentOptions?.toolContext;
|
|
93
|
+
const runAgentOptions = {
|
|
94
|
+
...input.runAgentOptions,
|
|
95
|
+
toolContext: {
|
|
96
|
+
/* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
|
|
97
|
+
signin: async () => undefined,
|
|
98
|
+
...existingToolContext,
|
|
99
|
+
context: resolvedContext,
|
|
100
|
+
friendStore: input.friendStore,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const result = await input.runAgent(sessionMessages, input.callbacks, input.channel, input.signal, runAgentOptions);
|
|
104
|
+
// Step 6: postTurn
|
|
105
|
+
input.postTurn(sessionMessages, session.sessionPath, result.usage);
|
|
106
|
+
// Step 7: Token accumulation
|
|
107
|
+
await input.accumulateFriendTokens(input.friendStore, resolvedContext.friend.id, result.usage);
|
|
108
|
+
(0, runtime_1.emitNervesEvent)({
|
|
109
|
+
component: "senses",
|
|
110
|
+
event: "senses.pipeline_end",
|
|
111
|
+
message: "inbound turn pipeline completed",
|
|
112
|
+
meta: {
|
|
113
|
+
channel: input.channel,
|
|
114
|
+
friendId: resolvedContext.friend.id,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
resolvedContext,
|
|
119
|
+
gateResult,
|
|
120
|
+
usage: result.usage,
|
|
121
|
+
sessionPath: session.sessionPath,
|
|
122
|
+
messages: sessionMessages,
|
|
123
|
+
};
|
|
124
|
+
}
|
package/dist/senses/teams.js
CHANGED
|
@@ -58,6 +58,7 @@ const commands_1 = require("./commands");
|
|
|
58
58
|
const nerves_1 = require("../nerves");
|
|
59
59
|
const runtime_1 = require("../nerves/runtime");
|
|
60
60
|
const store_file_1 = require("../mind/friends/store-file");
|
|
61
|
+
const types_1 = require("../mind/friends/types");
|
|
61
62
|
const resolver_1 = require("../mind/friends/resolver");
|
|
62
63
|
const tokens_1 = require("../mind/friends/tokens");
|
|
63
64
|
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
@@ -65,6 +66,8 @@ const identity_1 = require("../heart/identity");
|
|
|
65
66
|
const http = __importStar(require("http"));
|
|
66
67
|
const path = __importStar(require("path"));
|
|
67
68
|
const trust_gate_1 = require("./trust-gate");
|
|
69
|
+
const pipeline_1 = require("./pipeline");
|
|
70
|
+
const pending_1 = require("../mind/pending");
|
|
68
71
|
// Strip @mention markup from incoming messages.
|
|
69
72
|
// Removes <at>...</at> tags and trims extra whitespace.
|
|
70
73
|
// Fallback safety net -- the SDK's activity.mentions.stripText should handle
|
|
@@ -446,49 +449,24 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
446
449
|
// before sync I/O (session load, trim) blocks the event loop.
|
|
447
450
|
stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
|
|
448
451
|
await new Promise(r => setImmediate(r));
|
|
449
|
-
// Resolve
|
|
452
|
+
// Resolve identity provider early for friend resolution + slash command session path
|
|
450
453
|
const store = getFriendStore();
|
|
451
454
|
const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
|
|
452
455
|
const externalId = teamsContext?.aadObjectId || conversationId;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const resolver = new resolver_1.FriendResolver(store, {
|
|
465
|
-
provider,
|
|
466
|
-
externalId,
|
|
467
|
-
tenantId: teamsContext?.tenantId,
|
|
468
|
-
displayName: teamsContext?.displayName || "Unknown",
|
|
469
|
-
channel: "teams",
|
|
470
|
-
});
|
|
471
|
-
toolContext.context = await resolver.resolve();
|
|
472
|
-
}
|
|
473
|
-
const friendId = toolContext?.context?.friend?.id || "default";
|
|
474
|
-
if (toolContext?.context?.friend) {
|
|
475
|
-
const trustGate = (0, trust_gate_1.enforceTrustGate)({
|
|
476
|
-
friend: toolContext.context.friend,
|
|
477
|
-
provider,
|
|
478
|
-
externalId,
|
|
479
|
-
tenantId: teamsContext?.tenantId,
|
|
480
|
-
channel: "teams",
|
|
481
|
-
});
|
|
482
|
-
if (!trustGate.allowed) {
|
|
483
|
-
if (trustGate.reason === "stranger_first_reply") {
|
|
484
|
-
stream.emit(trustGate.autoReply);
|
|
485
|
-
}
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
456
|
+
// Build FriendResolver for the pipeline
|
|
457
|
+
const resolver = new resolver_1.FriendResolver(store, {
|
|
458
|
+
provider,
|
|
459
|
+
externalId,
|
|
460
|
+
tenantId: teamsContext?.tenantId,
|
|
461
|
+
displayName: teamsContext?.displayName || "Unknown",
|
|
462
|
+
channel: "teams",
|
|
463
|
+
});
|
|
464
|
+
// Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
|
|
465
|
+
const resolvedContext = await resolver.resolve();
|
|
466
|
+
const friendId = resolvedContext.friend.id;
|
|
489
467
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
490
468
|
(0, commands_1.registerDefaultCommands)(registry);
|
|
491
|
-
// Check for slash commands
|
|
469
|
+
// Check for slash commands (before pipeline -- these are transport-level concerns)
|
|
492
470
|
const parsed = (0, commands_1.parseSlashCommand)(text);
|
|
493
471
|
if (parsed) {
|
|
494
472
|
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
@@ -505,35 +483,86 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
505
483
|
}
|
|
506
484
|
}
|
|
507
485
|
}
|
|
508
|
-
//
|
|
509
|
-
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
510
|
-
const existing = (0, context_1.loadSession)(sessPath);
|
|
511
|
-
const messages = existing?.messages && existing.messages.length > 0
|
|
512
|
-
? existing.messages
|
|
513
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
|
|
514
|
-
// Repair any orphaned tool calls from a previous aborted turn
|
|
515
|
-
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
516
|
-
// Push user message
|
|
517
|
-
messages.push({ role: "user", content: text });
|
|
518
|
-
// Run agent
|
|
486
|
+
// ── Teams adapter concerns: controller, callbacks, session path ──────────
|
|
519
487
|
const controller = new AbortController();
|
|
520
488
|
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
521
489
|
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
|
|
522
490
|
const traceId = (0, nerves_1.createTraceId)();
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
491
|
+
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
492
|
+
const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
|
|
493
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
|
|
494
|
+
// Build Teams-specific toolContext fields for injection into the pipeline
|
|
495
|
+
const teamsToolContext = teamsContext ? {
|
|
496
|
+
graphToken: teamsContext.graphToken,
|
|
497
|
+
adoToken: teamsContext.adoToken,
|
|
498
|
+
githubToken: teamsContext.githubToken,
|
|
499
|
+
signin: teamsContext.signin,
|
|
500
|
+
summarize: (0, core_1.createSummarize)(),
|
|
501
|
+
tenantId: teamsContext.tenantId,
|
|
502
|
+
botApi: teamsContext.botApi,
|
|
503
|
+
} : {};
|
|
504
|
+
// Build runAgentOptions with Teams-specific fields
|
|
505
|
+
const agentOptions = {
|
|
506
|
+
traceId,
|
|
507
|
+
toolContext: teamsToolContext,
|
|
508
|
+
drainSteeringFollowUps: () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text })),
|
|
509
|
+
};
|
|
527
510
|
if (channelConfig.skipConfirmation)
|
|
528
511
|
agentOptions.skipConfirmation = true;
|
|
529
|
-
|
|
530
|
-
const result = await (0,
|
|
512
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
513
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
514
|
+
channel: "teams",
|
|
515
|
+
capabilities: teamsCapabilities,
|
|
516
|
+
messages: [{ role: "user", content: text }],
|
|
517
|
+
callbacks,
|
|
518
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
519
|
+
sessionLoader: {
|
|
520
|
+
loadOrCreate: async () => {
|
|
521
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
522
|
+
const messages = existing?.messages && existing.messages.length > 0
|
|
523
|
+
? existing.messages
|
|
524
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, resolvedContext) }];
|
|
525
|
+
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
526
|
+
return { messages, sessionPath: sessPath };
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
pendingDir,
|
|
530
|
+
friendStore: store,
|
|
531
|
+
provider,
|
|
532
|
+
externalId,
|
|
533
|
+
tenantId: teamsContext?.tenantId,
|
|
534
|
+
isGroupChat: false,
|
|
535
|
+
groupHasFamilyMember: false,
|
|
536
|
+
hasExistingGroupWithFamily: false,
|
|
537
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
538
|
+
drainPending: pending_1.drainPending,
|
|
539
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
540
|
+
...opts,
|
|
541
|
+
toolContext: {
|
|
542
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
543
|
+
signin: async () => undefined,
|
|
544
|
+
...opts?.toolContext,
|
|
545
|
+
summarize: teamsToolContext.summarize,
|
|
546
|
+
},
|
|
547
|
+
}),
|
|
548
|
+
postTurn: context_1.postTurn,
|
|
549
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
550
|
+
signal: controller.signal,
|
|
551
|
+
runAgentOptions: agentOptions,
|
|
552
|
+
});
|
|
553
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
554
|
+
if (!result.gateResult.allowed) {
|
|
555
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
556
|
+
stream.emit(result.gateResult.autoReply);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
531
560
|
// Flush any remaining accumulated text at end of turn
|
|
532
561
|
await callbacks.flush();
|
|
533
562
|
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
534
563
|
// This must happen after the stream is done so the OAuth card renders properly.
|
|
535
|
-
if (teamsContext) {
|
|
536
|
-
const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
564
|
+
if (teamsContext && result.messages) {
|
|
565
|
+
const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
537
566
|
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
538
567
|
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
539
568
|
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
@@ -541,12 +570,6 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
541
570
|
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
542
571
|
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
543
572
|
}
|
|
544
|
-
// Trim context and save session
|
|
545
|
-
(0, context_1.postTurn)(messages, sessPath, result.usage);
|
|
546
|
-
// Accumulate token usage on friend record
|
|
547
|
-
if (toolContext?.context?.friend?.id) {
|
|
548
|
-
await (0, tokens_1.accumulateFriendTokens)(store, toolContext.context.friend.id, result.usage);
|
|
549
|
-
}
|
|
550
573
|
// SDK auto-closes the stream after our handler returns (app.process.js)
|
|
551
574
|
}
|
|
552
575
|
// Internal port for the secondary bot App (not exposed externally).
|
|
@@ -739,7 +762,6 @@ function registerBotHandlers(app, label) {
|
|
|
739
762
|
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
|
|
740
763
|
});
|
|
741
764
|
}
|
|
742
|
-
const TEAMS_PROACTIVE_ALLOWED_TRUST = new Set(["family", "friend"]);
|
|
743
765
|
function findAadObjectId(friend) {
|
|
744
766
|
for (const ext of friend.externalIds) {
|
|
745
767
|
if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
|
|
@@ -838,7 +860,7 @@ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
|
|
|
838
860
|
});
|
|
839
861
|
continue;
|
|
840
862
|
}
|
|
841
|
-
if (!
|
|
863
|
+
if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
842
864
|
result.skipped++;
|
|
843
865
|
try {
|
|
844
866
|
fs.unlinkSync(filePath);
|
|
@@ -39,6 +39,10 @@ const fs = __importStar(require("fs"));
|
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const identity_1 = require("../heart/identity");
|
|
41
41
|
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
const types_1 = require("../mind/friends/types");
|
|
43
|
+
const pending_1 = require("../mind/pending");
|
|
44
|
+
// TODO: agent should pre-configure auto-reply voice
|
|
45
|
+
// This is a canned reply; in future the agent should compose their own first-contact message
|
|
42
46
|
exports.STRANGER_AUTO_REPLY = "I'm sorry, I'm not allowed to talk to strangers";
|
|
43
47
|
function buildExternalKey(provider, externalId, tenantId) {
|
|
44
48
|
return `${provider}:${tenantId ?? ""}:${externalId}`;
|
|
@@ -77,16 +81,107 @@ function appendPrimaryNotification(bundleRoot, provider, externalId, tenantId, n
|
|
|
77
81
|
fs.mkdirSync(inboxDir, { recursive: true });
|
|
78
82
|
fs.appendFileSync(notificationsPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
79
83
|
}
|
|
84
|
+
function writeInnerPendingNotice(bundleRoot, noticeContent, nowIso) {
|
|
85
|
+
const innerPendingDir = path.join(bundleRoot, "state", "pending", pending_1.INNER_DIALOG_PENDING.friendId, pending_1.INNER_DIALOG_PENDING.channel, pending_1.INNER_DIALOG_PENDING.key);
|
|
86
|
+
const fileName = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`;
|
|
87
|
+
const filePath = path.join(innerPendingDir, fileName);
|
|
88
|
+
const payload = {
|
|
89
|
+
from: "instinct",
|
|
90
|
+
content: noticeContent,
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
at: nowIso,
|
|
93
|
+
};
|
|
94
|
+
fs.mkdirSync(innerPendingDir, { recursive: true });
|
|
95
|
+
fs.writeFileSync(filePath, JSON.stringify(payload), "utf-8");
|
|
96
|
+
}
|
|
80
97
|
function enforceTrustGate(input) {
|
|
98
|
+
const { senseType } = input;
|
|
99
|
+
// Local (CLI) and internal (inner dialog) — always allow
|
|
100
|
+
if (senseType === "local" || senseType === "internal") {
|
|
101
|
+
return { allowed: true };
|
|
102
|
+
}
|
|
103
|
+
// Closed senses (Teams) — org already gates access, allow all trust levels
|
|
104
|
+
if (senseType === "closed") {
|
|
105
|
+
return { allowed: true };
|
|
106
|
+
}
|
|
107
|
+
// Open senses (BlueBubbles/iMessage) — enforce trust rules
|
|
81
108
|
const trustLevel = input.friend.trustLevel ?? "friend";
|
|
82
|
-
|
|
109
|
+
// Family and friend — always allow on open
|
|
110
|
+
if ((0, types_1.isTrustedLevel)(trustLevel)) {
|
|
83
111
|
return { allowed: true };
|
|
84
112
|
}
|
|
85
113
|
const bundleRoot = input.bundleRoot ?? (0, identity_1.getAgentRoot)();
|
|
86
|
-
const repliesPath = path.join(bundleRoot, "stranger-replies.json");
|
|
87
114
|
const nowIso = (input.now ?? (() => new Date()))().toISOString();
|
|
115
|
+
// Acquaintance rules
|
|
116
|
+
if (trustLevel === "acquaintance") {
|
|
117
|
+
return handleAcquaintance(input, bundleRoot, nowIso);
|
|
118
|
+
}
|
|
119
|
+
// Stranger rules (trustLevel === "stranger")
|
|
120
|
+
return handleStranger(input, bundleRoot, nowIso);
|
|
121
|
+
}
|
|
122
|
+
function handleAcquaintance(input, bundleRoot, nowIso) {
|
|
123
|
+
const { isGroupChat, groupHasFamilyMember, hasExistingGroupWithFamily } = input;
|
|
124
|
+
// Group chat with family member present — allow
|
|
125
|
+
if (isGroupChat && groupHasFamilyMember) {
|
|
126
|
+
return { allowed: true };
|
|
127
|
+
}
|
|
128
|
+
let result;
|
|
129
|
+
let noticeDetail;
|
|
130
|
+
if (isGroupChat) {
|
|
131
|
+
// Group chat without family member — reject silently
|
|
132
|
+
result = { allowed: false, reason: "acquaintance_group_no_family" };
|
|
133
|
+
noticeDetail = `acquaintance "${input.friend.name}" messaged in a group chat without a family member present`;
|
|
134
|
+
}
|
|
135
|
+
else if (hasExistingGroupWithFamily) {
|
|
136
|
+
// 1:1 but shares a group with family — redirect
|
|
137
|
+
result = {
|
|
138
|
+
allowed: false,
|
|
139
|
+
reason: "acquaintance_1on1_has_group",
|
|
140
|
+
autoReply: "Hey! Reach me in our group chat instead.",
|
|
141
|
+
};
|
|
142
|
+
noticeDetail = `acquaintance "${input.friend.name}" DMed me directly — redirected to our group chat`;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// 1:1, no shared group with family — redirect to any group
|
|
146
|
+
result = {
|
|
147
|
+
allowed: false,
|
|
148
|
+
reason: "acquaintance_1on1_no_group",
|
|
149
|
+
autoReply: "Hey! Reach me in a group chat instead.",
|
|
150
|
+
};
|
|
151
|
+
noticeDetail = `acquaintance "${input.friend.name}" DMed me directly — asked to reach me in a group chat`;
|
|
152
|
+
}
|
|
153
|
+
(0, runtime_1.emitNervesEvent)({
|
|
154
|
+
level: "warn",
|
|
155
|
+
component: "senses",
|
|
156
|
+
event: "senses.trust_gate",
|
|
157
|
+
message: "acquaintance message blocked",
|
|
158
|
+
meta: {
|
|
159
|
+
channel: input.channel,
|
|
160
|
+
provider: input.provider,
|
|
161
|
+
reason: result.reason,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
writeInnerPendingNotice(bundleRoot, noticeDetail, nowIso);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
(0, runtime_1.emitNervesEvent)({
|
|
169
|
+
level: "error",
|
|
170
|
+
component: "senses",
|
|
171
|
+
event: "senses.trust_gate_error",
|
|
172
|
+
message: "failed to write inner pending notice",
|
|
173
|
+
meta: {
|
|
174
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
function handleStranger(input, bundleRoot, nowIso) {
|
|
181
|
+
const repliesPath = path.join(bundleRoot, "stranger-replies.json");
|
|
88
182
|
const externalKey = buildExternalKey(input.provider, input.externalId, input.tenantId);
|
|
89
183
|
const state = loadRepliesState(repliesPath);
|
|
184
|
+
// Subsequent contact — silent drop
|
|
90
185
|
if (state[externalKey]) {
|
|
91
186
|
(0, runtime_1.emitNervesEvent)({
|
|
92
187
|
level: "warn",
|
|
@@ -103,6 +198,7 @@ function enforceTrustGate(input) {
|
|
|
103
198
|
reason: "stranger_silent_drop",
|
|
104
199
|
};
|
|
105
200
|
}
|
|
201
|
+
// First contact — auto-reply, persist state, notify agent
|
|
106
202
|
state[externalKey] = nowIso;
|
|
107
203
|
try {
|
|
108
204
|
persistRepliesState(repliesPath, state);
|
|
@@ -132,6 +228,21 @@ function enforceTrustGate(input) {
|
|
|
132
228
|
},
|
|
133
229
|
});
|
|
134
230
|
}
|
|
231
|
+
const noticeDetail = `stranger "${input.friend.name}" tried to reach me via ${input.channel}. Auto-replied once.`;
|
|
232
|
+
try {
|
|
233
|
+
writeInnerPendingNotice(bundleRoot, noticeDetail, nowIso);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
(0, runtime_1.emitNervesEvent)({
|
|
237
|
+
level: "error",
|
|
238
|
+
component: "senses",
|
|
239
|
+
event: "senses.trust_gate_error",
|
|
240
|
+
message: "failed to write inner pending notice",
|
|
241
|
+
meta: {
|
|
242
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
135
246
|
(0, runtime_1.emitNervesEvent)({
|
|
136
247
|
level: "warn",
|
|
137
248
|
component: "senses",
|