@ouro.bot/cli 0.1.0-alpha.37 → 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 +22 -0
- package/dist/heart/daemon/daemon-cli.js +444 -32
- package/dist/heart/daemon/daemon.js +90 -0
- package/dist/heart/daemon/specialist-prompt.js +2 -1
- package/dist/heart/daemon/specialist-tools.js +48 -2
- package/dist/heart/kicks.js +1 -1
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/pending.js +8 -0
- package/dist/mind/prompt.js +126 -2
- package/dist/repertoire/tools-base.js +193 -271
- package/dist/repertoire/tools.js +18 -41
- package/dist/senses/bluebubbles-model.js +10 -0
- package/dist/senses/bluebubbles.js +301 -27
- package/dist/senses/cli.js +73 -50
- package/dist/senses/inner-dialog.js +99 -54
- package/dist/senses/pipeline.js +124 -0
- package/dist/senses/teams.js +264 -63
- package/dist/senses/trust-gate.js +113 -2
- package/package.json +2 -1
package/dist/senses/teams.js
CHANGED
|
@@ -40,7 +40,9 @@ exports.createTeamsCallbacks = createTeamsCallbacks;
|
|
|
40
40
|
exports.resolvePendingConfirmation = resolvePendingConfirmation;
|
|
41
41
|
exports.withConversationLock = withConversationLock;
|
|
42
42
|
exports.handleTeamsMessage = handleTeamsMessage;
|
|
43
|
+
exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
|
|
43
44
|
exports.startTeamsApp = startTeamsApp;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
44
46
|
const teams_apps_1 = require("@microsoft/teams.apps");
|
|
45
47
|
const teams_dev_1 = require("@microsoft/teams.dev");
|
|
46
48
|
const core_1 = require("../heart/core");
|
|
@@ -56,6 +58,7 @@ const commands_1 = require("./commands");
|
|
|
56
58
|
const nerves_1 = require("../nerves");
|
|
57
59
|
const runtime_1 = require("../nerves/runtime");
|
|
58
60
|
const store_file_1 = require("../mind/friends/store-file");
|
|
61
|
+
const types_1 = require("../mind/friends/types");
|
|
59
62
|
const resolver_1 = require("../mind/friends/resolver");
|
|
60
63
|
const tokens_1 = require("../mind/friends/tokens");
|
|
61
64
|
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
@@ -63,6 +66,8 @@ const identity_1 = require("../heart/identity");
|
|
|
63
66
|
const http = __importStar(require("http"));
|
|
64
67
|
const path = __importStar(require("path"));
|
|
65
68
|
const trust_gate_1 = require("./trust-gate");
|
|
69
|
+
const pipeline_1 = require("./pipeline");
|
|
70
|
+
const pending_1 = require("../mind/pending");
|
|
66
71
|
// Strip @mention markup from incoming messages.
|
|
67
72
|
// Removes <at>...</at> tags and trims extra whitespace.
|
|
68
73
|
// Fallback safety net -- the SDK's activity.mentions.stripText should handle
|
|
@@ -444,49 +449,24 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
444
449
|
// before sync I/O (session load, trim) blocks the event loop.
|
|
445
450
|
stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
|
|
446
451
|
await new Promise(r => setImmediate(r));
|
|
447
|
-
// Resolve
|
|
452
|
+
// Resolve identity provider early for friend resolution + slash command session path
|
|
448
453
|
const store = getFriendStore();
|
|
449
454
|
const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
|
|
450
455
|
const externalId = teamsContext?.aadObjectId || conversationId;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const resolver = new resolver_1.FriendResolver(store, {
|
|
463
|
-
provider,
|
|
464
|
-
externalId,
|
|
465
|
-
tenantId: teamsContext?.tenantId,
|
|
466
|
-
displayName: teamsContext?.displayName || "Unknown",
|
|
467
|
-
channel: "teams",
|
|
468
|
-
});
|
|
469
|
-
toolContext.context = await resolver.resolve();
|
|
470
|
-
}
|
|
471
|
-
const friendId = toolContext?.context?.friend?.id || "default";
|
|
472
|
-
if (toolContext?.context?.friend) {
|
|
473
|
-
const trustGate = (0, trust_gate_1.enforceTrustGate)({
|
|
474
|
-
friend: toolContext.context.friend,
|
|
475
|
-
provider,
|
|
476
|
-
externalId,
|
|
477
|
-
tenantId: teamsContext?.tenantId,
|
|
478
|
-
channel: "teams",
|
|
479
|
-
});
|
|
480
|
-
if (!trustGate.allowed) {
|
|
481
|
-
if (trustGate.reason === "stranger_first_reply") {
|
|
482
|
-
stream.emit(trustGate.autoReply);
|
|
483
|
-
}
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
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;
|
|
487
467
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
488
468
|
(0, commands_1.registerDefaultCommands)(registry);
|
|
489
|
-
// Check for slash commands
|
|
469
|
+
// Check for slash commands (before pipeline -- these are transport-level concerns)
|
|
490
470
|
const parsed = (0, commands_1.parseSlashCommand)(text);
|
|
491
471
|
if (parsed) {
|
|
492
472
|
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
@@ -503,35 +483,86 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
503
483
|
}
|
|
504
484
|
}
|
|
505
485
|
}
|
|
506
|
-
//
|
|
507
|
-
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
508
|
-
const existing = (0, context_1.loadSession)(sessPath);
|
|
509
|
-
const messages = existing?.messages && existing.messages.length > 0
|
|
510
|
-
? existing.messages
|
|
511
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
|
|
512
|
-
// Repair any orphaned tool calls from a previous aborted turn
|
|
513
|
-
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
514
|
-
// Push user message
|
|
515
|
-
messages.push({ role: "user", content: text });
|
|
516
|
-
// Run agent
|
|
486
|
+
// ── Teams adapter concerns: controller, callbacks, session path ──────────
|
|
517
487
|
const controller = new AbortController();
|
|
518
488
|
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
519
489
|
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
|
|
520
490
|
const traceId = (0, nerves_1.createTraceId)();
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
};
|
|
525
510
|
if (channelConfig.skipConfirmation)
|
|
526
511
|
agentOptions.skipConfirmation = true;
|
|
527
|
-
|
|
528
|
-
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
|
+
}
|
|
529
560
|
// Flush any remaining accumulated text at end of turn
|
|
530
561
|
await callbacks.flush();
|
|
531
562
|
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
532
563
|
// This must happen after the stream is done so the OAuth card renders properly.
|
|
533
|
-
if (teamsContext) {
|
|
534
|
-
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");
|
|
535
566
|
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
536
567
|
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
537
568
|
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
@@ -539,12 +570,6 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
539
570
|
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
540
571
|
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
541
572
|
}
|
|
542
|
-
// Trim context and save session
|
|
543
|
-
(0, context_1.postTurn)(messages, sessPath, result.usage);
|
|
544
|
-
// Accumulate token usage on friend record
|
|
545
|
-
if (toolContext?.context?.friend?.id) {
|
|
546
|
-
await (0, tokens_1.accumulateFriendTokens)(store, toolContext.context.friend.id, result.usage);
|
|
547
|
-
}
|
|
548
573
|
// SDK auto-closes the stream after our handler returns (app.process.js)
|
|
549
574
|
}
|
|
550
575
|
// Internal port for the secondary bot App (not exposed externally).
|
|
@@ -737,6 +762,182 @@ function registerBotHandlers(app, label) {
|
|
|
737
762
|
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
|
|
738
763
|
});
|
|
739
764
|
}
|
|
765
|
+
function findAadObjectId(friend) {
|
|
766
|
+
for (const ext of friend.externalIds) {
|
|
767
|
+
if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
|
|
768
|
+
return { aadObjectId: ext.externalId, tenantId: ext.tenantId };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
function scanPendingTeamsFiles(pendingRoot) {
|
|
774
|
+
const results = [];
|
|
775
|
+
let friendIds;
|
|
776
|
+
try {
|
|
777
|
+
friendIds = fs.readdirSync(pendingRoot);
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return results;
|
|
781
|
+
}
|
|
782
|
+
for (const friendId of friendIds) {
|
|
783
|
+
const teamsDir = path.join(pendingRoot, friendId, "teams");
|
|
784
|
+
let keys;
|
|
785
|
+
try {
|
|
786
|
+
keys = fs.readdirSync(teamsDir);
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
for (const key of keys) {
|
|
792
|
+
const keyDir = path.join(teamsDir, key);
|
|
793
|
+
let files;
|
|
794
|
+
try {
|
|
795
|
+
files = fs.readdirSync(keyDir);
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
|
|
801
|
+
const filePath = path.join(keyDir, file);
|
|
802
|
+
try {
|
|
803
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
804
|
+
results.push({ friendId, key, filePath, content });
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
// skip unreadable files
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return results;
|
|
813
|
+
}
|
|
814
|
+
async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
|
|
815
|
+
const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
|
|
816
|
+
const pendingFiles = scanPendingTeamsFiles(root);
|
|
817
|
+
const result = { sent: 0, skipped: 0, failed: 0 };
|
|
818
|
+
const conversations = botApi.conversations;
|
|
819
|
+
for (const { friendId, filePath, content } of pendingFiles) {
|
|
820
|
+
let parsed;
|
|
821
|
+
try {
|
|
822
|
+
parsed = JSON.parse(content);
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
result.failed++;
|
|
826
|
+
try {
|
|
827
|
+
fs.unlinkSync(filePath);
|
|
828
|
+
}
|
|
829
|
+
catch { /* ignore */ }
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const messageText = typeof parsed.content === "string" ? parsed.content : "";
|
|
833
|
+
if (!messageText.trim()) {
|
|
834
|
+
result.skipped++;
|
|
835
|
+
try {
|
|
836
|
+
fs.unlinkSync(filePath);
|
|
837
|
+
}
|
|
838
|
+
catch { /* ignore */ }
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
let friend;
|
|
842
|
+
try {
|
|
843
|
+
friend = await store.get(friendId);
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
friend = null;
|
|
847
|
+
}
|
|
848
|
+
if (!friend) {
|
|
849
|
+
result.skipped++;
|
|
850
|
+
try {
|
|
851
|
+
fs.unlinkSync(filePath);
|
|
852
|
+
}
|
|
853
|
+
catch { /* ignore */ }
|
|
854
|
+
(0, runtime_1.emitNervesEvent)({
|
|
855
|
+
level: "warn",
|
|
856
|
+
component: "senses",
|
|
857
|
+
event: "senses.teams_proactive_no_friend",
|
|
858
|
+
message: "proactive send skipped: friend not found",
|
|
859
|
+
meta: { friendId },
|
|
860
|
+
});
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
864
|
+
result.skipped++;
|
|
865
|
+
try {
|
|
866
|
+
fs.unlinkSync(filePath);
|
|
867
|
+
}
|
|
868
|
+
catch { /* ignore */ }
|
|
869
|
+
(0, runtime_1.emitNervesEvent)({
|
|
870
|
+
component: "senses",
|
|
871
|
+
event: "senses.teams_proactive_trust_skip",
|
|
872
|
+
message: "proactive send skipped: trust level not allowed",
|
|
873
|
+
meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
|
|
874
|
+
});
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const aadInfo = findAadObjectId(friend);
|
|
878
|
+
if (!aadInfo) {
|
|
879
|
+
result.skipped++;
|
|
880
|
+
try {
|
|
881
|
+
fs.unlinkSync(filePath);
|
|
882
|
+
}
|
|
883
|
+
catch { /* ignore */ }
|
|
884
|
+
(0, runtime_1.emitNervesEvent)({
|
|
885
|
+
level: "warn",
|
|
886
|
+
component: "senses",
|
|
887
|
+
event: "senses.teams_proactive_no_aad_id",
|
|
888
|
+
message: "proactive send skipped: no AAD object ID found",
|
|
889
|
+
meta: { friendId },
|
|
890
|
+
});
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
const conversation = await conversations.create({
|
|
895
|
+
bot: { id: botApi.id },
|
|
896
|
+
members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
|
|
897
|
+
tenantId: aadInfo.tenantId,
|
|
898
|
+
isGroup: false,
|
|
899
|
+
});
|
|
900
|
+
await conversations.activities(conversation.id).create({
|
|
901
|
+
type: "message",
|
|
902
|
+
text: messageText,
|
|
903
|
+
});
|
|
904
|
+
result.sent++;
|
|
905
|
+
try {
|
|
906
|
+
fs.unlinkSync(filePath);
|
|
907
|
+
}
|
|
908
|
+
catch { /* ignore */ }
|
|
909
|
+
(0, runtime_1.emitNervesEvent)({
|
|
910
|
+
component: "senses",
|
|
911
|
+
event: "senses.teams_proactive_sent",
|
|
912
|
+
message: "proactive teams message sent",
|
|
913
|
+
meta: { friendId, aadObjectId: aadInfo.aadObjectId },
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
catch (error) {
|
|
917
|
+
result.failed++;
|
|
918
|
+
(0, runtime_1.emitNervesEvent)({
|
|
919
|
+
level: "error",
|
|
920
|
+
component: "senses",
|
|
921
|
+
event: "senses.teams_proactive_send_error",
|
|
922
|
+
message: "proactive teams send failed",
|
|
923
|
+
meta: {
|
|
924
|
+
friendId,
|
|
925
|
+
aadObjectId: aadInfo.aadObjectId,
|
|
926
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
|
|
932
|
+
(0, runtime_1.emitNervesEvent)({
|
|
933
|
+
component: "senses",
|
|
934
|
+
event: "senses.teams_proactive_drain_complete",
|
|
935
|
+
message: "teams proactive drain complete",
|
|
936
|
+
meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
740
941
|
// Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
|
|
741
942
|
// Mode is determined by getTeamsConfig().clientId.
|
|
742
943
|
// Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
|
|
@@ -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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.39",
|
|
4
4
|
"main": "dist/heart/daemon/ouro-entry.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cli": "dist/heart/daemon/ouro-bot-entry.js",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
36
36
|
"@microsoft/teams.apps": "^2.0.5",
|
|
37
37
|
"@microsoft/teams.dev": "^2.0.5",
|
|
38
|
+
"fast-glob": "^3.3.3",
|
|
38
39
|
"openai": "^6.27.0",
|
|
39
40
|
"semver": "^7.7.4"
|
|
40
41
|
},
|