@ouro.bot/cli 0.1.0-alpha.742 → 0.1.0-alpha.743
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 +26 -0
- package/deploy/unraid/Dockerfile +7 -2
- package/deploy/unraid/README.txt +704 -64
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
- package/deploy/unraid/sanctuary.xml +13 -10
- package/dist/arc/cares.js +171 -37
- package/dist/arc/obligations.js +38 -0
- package/dist/heart/awaiting/await-alert.js +4 -2
- package/dist/heart/awaiting/await-expiry.js +39 -4
- package/dist/heart/awaiting/await-parser.js +8 -0
- package/dist/heart/core.js +37 -7
- package/dist/heart/cross-chat-delivery.js +1 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
- package/dist/heart/daemon/container-spec-auditor.js +38 -11
- package/dist/heart/daemon/daemon-entry.js +19 -5
- package/dist/heart/daemon/daemon.js +154 -22
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +126 -42
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
- package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
- package/dist/heart/external-events/router.js +1025 -43
- package/dist/heart/session-events.js +23 -5
- package/dist/heart/steward-policy.js +302 -0
- package/dist/heart/tool-approval.js +3 -1
- package/dist/heart/turn-context.js +1 -0
- package/dist/mind/context.js +8 -3
- package/dist/mind/pending.js +25 -0
- package/dist/mind/prompt.js +49 -1
- package/dist/repertoire/guardrails.js +2 -1
- package/dist/repertoire/relationship-authorization.js +194 -0
- package/dist/repertoire/tools-awaiting.js +229 -26
- package/dist/repertoire/tools-base.js +15 -0
- package/dist/repertoire/tools-continuity.js +259 -4
- package/dist/repertoire/tools-notes.js +29 -3
- package/dist/repertoire/tools-session.js +219 -4
- package/dist/repertoire/tools-steward-policy.js +88 -0
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-unraid.js +113 -6
- package/dist/repertoire/tools.js +104 -9
- package/dist/repertoire/unraid-restart.js +86 -4
- package/dist/senses/private-runtime-worker.js +36 -3
- package/dist/senses/private-runtime.js +310 -63
- package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
- package/dist/senses/sanctuary-health-runner.js +47 -103
- package/dist/senses/sanctuary-health.js +53 -105
- package/dist/senses/sanctuary-runtime.js +36 -2
- package/dist/senses/sanctuary-sab.js +80 -0
- package/dist/senses/shared-turn.js +64 -9
- package/dist/senses/telegram-admission.js +684 -0
- package/dist/senses/telegram-approval-runtime.js +46 -6
- package/dist/senses/telegram-client.js +212 -66
- package/dist/senses/telegram-effect-adapter.js +787 -0
- package/dist/senses/telegram.js +728 -48
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/dist/senses/telegram.js
CHANGED
|
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createSabQueueProtectiveStateVerifier = createSabQueueProtectiveStateVerifier;
|
|
36
37
|
exports.telegramAcceptanceAuditOwnerDigest = telegramAcceptanceAuditOwnerDigest;
|
|
37
38
|
exports.sanctuaryTelegramTurnReceiptDigest = sanctuaryTelegramTurnReceiptDigest;
|
|
38
39
|
exports.sanctuaryTelegramTurnReceiptMac = sanctuaryTelegramTurnReceiptMac;
|
|
39
40
|
exports.sanctuaryTelegramAuditLifecycleMac = sanctuaryTelegramAuditLifecycleMac;
|
|
40
41
|
exports.sanctuaryTelegramUnauthorizedDropMac = sanctuaryTelegramUnauthorizedDropMac;
|
|
41
42
|
exports.sanctuaryTelegramApprovalEvidenceMac = sanctuaryTelegramApprovalEvidenceMac;
|
|
43
|
+
exports.telegramBotIdFromToken = telegramBotIdFromToken;
|
|
42
44
|
exports.readOrCreateTelegramIdentityKey = readOrCreateTelegramIdentityKey;
|
|
43
45
|
exports.opaqueTelegramSubject = opaqueTelegramSubject;
|
|
44
46
|
exports.migrateTelegramFriendIdentity = migrateTelegramFriendIdentity;
|
|
@@ -46,7 +48,10 @@ exports.migrateTelegramSessionIdentity = migrateTelegramSessionIdentity;
|
|
|
46
48
|
exports.parseTelegramSenseCredentials = parseTelegramSenseCredentials;
|
|
47
49
|
exports.createTelegramSenseApp = createTelegramSenseApp;
|
|
48
50
|
exports.loadTelegramSenseCredentials = loadTelegramSenseCredentials;
|
|
51
|
+
exports.createProductionTelegramRelationshipComposition = createProductionTelegramRelationshipComposition;
|
|
49
52
|
exports.startTelegramSenseApp = startTelegramSenseApp;
|
|
53
|
+
exports.sendTelegramExternalEventDecision = sendTelegramExternalEventDecision;
|
|
54
|
+
exports.sendTelegramAwaitFollowUp = sendTelegramAwaitFollowUp;
|
|
50
55
|
const node_crypto_1 = require("node:crypto");
|
|
51
56
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
52
57
|
const node_fs_1 = require("node:fs");
|
|
@@ -58,12 +63,37 @@ const runtime_credentials_1 = require("../heart/runtime-credentials");
|
|
|
58
63
|
const runtime_1 = require("../nerves/runtime");
|
|
59
64
|
const nerves_1 = require("../nerves");
|
|
60
65
|
const sanctuary_interactive_control_1 = require("./sanctuary-interactive-control");
|
|
66
|
+
const sanctuary_sab_1 = require("./sanctuary-sab");
|
|
61
67
|
const shared_turn_1 = require("./shared-turn");
|
|
62
68
|
const telegram_client_1 = require("./telegram-client");
|
|
63
69
|
const sanctuary_runtime_1 = require("./sanctuary-runtime");
|
|
64
70
|
const sanctuary_grounding_1 = require("./sanctuary-grounding");
|
|
65
71
|
const telegram_approval_runtime_1 = require("./telegram-approval-runtime");
|
|
66
72
|
const telegram_audit_ledger_1 = require("./telegram-audit-ledger");
|
|
73
|
+
const session_events_1 = require("../heart/session-events");
|
|
74
|
+
const router_1 = require("../heart/external-events/router");
|
|
75
|
+
const session_transaction_1 = require("../mind/session-transaction");
|
|
76
|
+
const approval_files_1 = require("../heart/approval-files");
|
|
77
|
+
const approval_store_1 = require("../heart/approval-store");
|
|
78
|
+
const tools_awaiting_1 = require("../repertoire/tools-awaiting");
|
|
79
|
+
const obligations_1 = require("../arc/obligations");
|
|
80
|
+
const orientation_frame_1 = require("../heart/orientation-frame");
|
|
81
|
+
const pending_1 = require("../mind/pending");
|
|
82
|
+
const relationship_authorization_1 = require("../repertoire/relationship-authorization");
|
|
83
|
+
const telegram_admission_1 = require("./telegram-admission");
|
|
84
|
+
const telegram_effect_adapter_1 = require("./telegram-effect-adapter");
|
|
85
|
+
const SANCTUARY_SAB_CONFIG_PATH = "/run/sanctuary/sabnzbd.ini";
|
|
86
|
+
function createSabQueueProtectiveStateVerifier(options = {}) {
|
|
87
|
+
let client = null;
|
|
88
|
+
return async (action) => {
|
|
89
|
+
client ??= (0, sanctuary_sab_1.createSanctuarySabClient)({ iniPath: options.iniPath ?? SANCTUARY_SAB_CONFIG_PATH, fetch: options.fetch, now: options.now });
|
|
90
|
+
const snapshot = await client.readQueue();
|
|
91
|
+
const paused = snapshot.paused;
|
|
92
|
+
const digest = (0, node_crypto_1.createHash)("sha256").update(`sabnzbd.queue.paused=${String(paused)}`).digest("hex");
|
|
93
|
+
const verified = action.action === "sabnzbd.pause" && paused && action.verification.verified && action.verification.digest === digest;
|
|
94
|
+
return { verified, reference: `sabnzbd.queue.paused:${digest}:${snapshot.observedAt}` };
|
|
95
|
+
};
|
|
96
|
+
}
|
|
67
97
|
const APPROVAL_EXPIRY_RECONCILE_INTERVAL_MS = 1_000;
|
|
68
98
|
const TELEGRAM_SUBJECT_DOMAIN = "ouroboros.telegram.subject.v1";
|
|
69
99
|
const TELEGRAM_IDENTITY_KEY = /^[A-Za-z0-9_-]{43}$/u;
|
|
@@ -343,6 +373,12 @@ function canonicalTelegramId(value, label) {
|
|
|
343
373
|
throw new Error(`Telegram ${label} must be a canonical positive decimal string`);
|
|
344
374
|
return text;
|
|
345
375
|
}
|
|
376
|
+
function telegramBotIdFromToken(token) {
|
|
377
|
+
const match = /^([1-9][0-9]*):/u.exec(token.trim());
|
|
378
|
+
if (!match)
|
|
379
|
+
throw new Error("Telegram bot token does not expose a canonical numeric bot id");
|
|
380
|
+
return match[1];
|
|
381
|
+
}
|
|
346
382
|
function canonicalTelegramIdentityKey(value) {
|
|
347
383
|
const key = value.trim();
|
|
348
384
|
if (!TELEGRAM_IDENTITY_KEY.test(key) || Buffer.from(key, "base64url").length !== 32) {
|
|
@@ -626,17 +662,23 @@ function parseTelegramSenseCredentials(value) {
|
|
|
626
662
|
}
|
|
627
663
|
function createTelegramSenseApp(options) {
|
|
628
664
|
const botToken = requiredText(options.credentials.botToken, "bot token");
|
|
665
|
+
const botId = options.credentials.botId !== undefined
|
|
666
|
+
? canonicalTelegramId(options.credentials.botId, "bot id")
|
|
667
|
+
: options.admission ? telegramBotIdFromToken(botToken) : null;
|
|
629
668
|
const authorizedUserId = canonicalTelegramId(options.credentials.authorizedUserId, "authorized user id");
|
|
630
669
|
const authorizedChatId = canonicalTelegramId(options.credentials.authorizedChatId, "authorized chat id");
|
|
631
670
|
const agentRoot = options._agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName);
|
|
632
671
|
const identityKey = options.identityKey === undefined
|
|
633
672
|
? readOrCreateTelegramIdentityKey(agentRoot)
|
|
634
673
|
: canonicalTelegramIdentityKey(options.identityKey);
|
|
635
|
-
const subject = opaqueTelegramSubject(identityKey, botToken, authorizedUserId, authorizedChatId);
|
|
674
|
+
const subject = opaqueTelegramSubject(identityKey, botId ?? botToken, authorizedUserId, authorizedChatId);
|
|
636
675
|
const transportError = (error) => redactTelegramPrivateValues(error, [botToken, authorizedUserId, authorizedChatId]);
|
|
637
676
|
const api = options.api ?? (0, telegram_client_1.createTelegramBotApi)({ token: botToken });
|
|
638
677
|
const offsetStore = options.offsetStore ?? new telegram_client_1.FileTelegramOffsetStore(path.join(agentRoot, "state", "senses", "telegram", "offset.json"));
|
|
639
678
|
const inboxStore = options.inboxStore ?? new telegram_client_1.FileTelegramUpdateInboxStore(path.join(agentRoot, "state", "senses", "telegram", "inbox.json"));
|
|
679
|
+
const admissionStore = options.admission
|
|
680
|
+
? new telegram_admission_1.FileTelegramAdmissionStore(path.join(agentRoot, "state", "senses", "telegram", "admissions"))
|
|
681
|
+
: undefined;
|
|
640
682
|
const runTurn = options.runTurn ?? options._runTurn ?? shared_turn_1.runSenseTurn;
|
|
641
683
|
const collectToolReceipts = options._runWithToolReceiptCollection ?? sanctuary_runtime_1.runWithSanctuaryToolReceiptCollection;
|
|
642
684
|
const useSanctuaryRuntime = options.agentName === "sanctuary" && !options.runTurn;
|
|
@@ -781,12 +823,59 @@ function createTelegramSenseApp(options) {
|
|
|
781
823
|
}
|
|
782
824
|
throw primaryError;
|
|
783
825
|
};
|
|
826
|
+
let effectJournal;
|
|
827
|
+
const getEffectJournal = () => {
|
|
828
|
+
effectJournal ??= new telegram_effect_adapter_1.FileTelegramEffectJournal(path.join(agentRoot, "state", "telegram", "effects"));
|
|
829
|
+
return effectJournal;
|
|
830
|
+
};
|
|
831
|
+
const configuredOwnerFriendId = options.admission?.ownerFriendId ?? `telegram-user:${subject}`;
|
|
832
|
+
const configuredOwnerSessionKey = `telegram:${subject}`;
|
|
833
|
+
const defaultEffectAuthorization = async (input) => {
|
|
834
|
+
const target = input.target;
|
|
835
|
+
if (target.kind === "admission_gate") {
|
|
836
|
+
const pending = admissionStore.read(target.admissionId);
|
|
837
|
+
const authorization = (0, relationship_authorization_1.authorizeRelationshipAccess)({
|
|
838
|
+
profiles: [],
|
|
839
|
+
pendingAdmission: { admissionId: pending.id, botId: pending.botId, userId: pending.userId, chatId: pending.chatId, expiresAt: new Date(pending.expiresAt).toISOString() },
|
|
840
|
+
request: { kind: "admission_gate", admissionId: target.admissionId, botId: target.botId, userId: target.userId, chatId: target.chatId, effect: "fixed_ack", idempotencyKey: input.idempotencyKey, expiresAt: new Date(pending.expiresAt).toISOString() },
|
|
841
|
+
});
|
|
842
|
+
const admissionAuthorization = authorization;
|
|
843
|
+
return { allowed: true, receiptId: admissionAuthorization.receiptId, expiresAt: admissionAuthorization.expiresAt, transport: { chatId: target.chatId } };
|
|
844
|
+
}
|
|
845
|
+
if (input.authorClass === "system_failsafe" && (input.effect.kind !== "text" || input.effect.text !== telegram_effect_adapter_1.FIXED_USENET_SYSTEM_FAILSAFE || !input.idempotencyKey.startsWith("system-failsafe:"))) {
|
|
846
|
+
return { allowed: false, reason: "system failsafe shape is not fixed" };
|
|
847
|
+
}
|
|
848
|
+
if (options.authorizeRelationshipEffect) {
|
|
849
|
+
if (target.friendId === configuredOwnerFriendId && target.sessionKey !== configuredOwnerSessionKey)
|
|
850
|
+
return { allowed: false, reason: "owner relationship session binding changed" };
|
|
851
|
+
return options.authorizeRelationshipEffect(input);
|
|
852
|
+
}
|
|
853
|
+
if (target.friendId !== `telegram-user:${subject}` || target.sessionKey !== `telegram:${subject}`) {
|
|
854
|
+
return { allowed: false, reason: "effect target is not the configured owner relationship" };
|
|
855
|
+
}
|
|
856
|
+
return { allowed: true, receiptId: `configured-owner:${subject}`, expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), transport: { chatId: authorizedChatId } };
|
|
857
|
+
};
|
|
858
|
+
const authorizeEffect = options.authorizeEffect ?? defaultEffectAuthorization;
|
|
859
|
+
let recordAcceptedEffects;
|
|
860
|
+
const configuredOwnerTarget = () => ({ kind: "approved_relationship", friendId: configuredOwnerFriendId, sessionKey: configuredOwnerSessionKey });
|
|
861
|
+
const executeAuthorizedEffect = (0, telegram_effect_adapter_1.createTelegramAuthorizedEffectExecutor)({
|
|
862
|
+
store: getEffectJournal,
|
|
863
|
+
api,
|
|
864
|
+
authorize: authorizeEffect,
|
|
865
|
+
barrier: acceptanceAuditBarrier,
|
|
866
|
+
});
|
|
867
|
+
const recordConfiguredOwnerEffect = async (artifact, causalEventId) => {
|
|
868
|
+
await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot), [artifact], undefined, causalEventId ? { [artifact.id]: causalEventId } : undefined);
|
|
869
|
+
};
|
|
870
|
+
const approvalEffects = (0, telegram_effect_adapter_1.createTelegramApprovalEffectPort)({ target: configuredOwnerTarget(), chatId: authorizedChatId, execute: executeAuthorizedEffect, record: recordConfiguredOwnerEffect });
|
|
784
871
|
let toolContext;
|
|
785
872
|
let approvalRuntime;
|
|
786
873
|
let approvalTransport;
|
|
787
874
|
let interactiveControl;
|
|
788
875
|
try {
|
|
789
876
|
toolContext = useSanctuaryRuntime ? (options._toolContext ?? (0, sanctuary_runtime_1.createSanctuaryToolContext)(options.agentName)) : undefined;
|
|
877
|
+
if (toolContext && options.telegramContactManager)
|
|
878
|
+
toolContext.telegramContactManager = options.telegramContactManager;
|
|
790
879
|
approvalRuntime = options.approvalRuntime ?? (useSanctuaryRuntime ? (options._createApprovalRuntime ?? telegram_approval_runtime_1.createTelegramApprovalRuntime)({
|
|
791
880
|
agentName: options.agentName,
|
|
792
881
|
api,
|
|
@@ -795,6 +884,7 @@ function createTelegramSenseApp(options) {
|
|
|
795
884
|
subject,
|
|
796
885
|
identityKey,
|
|
797
886
|
toolContext: toolContext ?? {},
|
|
887
|
+
effects: approvalEffects,
|
|
798
888
|
effectBarrier: acceptanceAuditBarrier,
|
|
799
889
|
dependencies: {
|
|
800
890
|
acceptanceMarker: () => {
|
|
@@ -818,7 +908,6 @@ function createTelegramSenseApp(options) {
|
|
|
818
908
|
catch (error) {
|
|
819
909
|
releaseAcceptanceAudit(error);
|
|
820
910
|
}
|
|
821
|
-
const healthSweep = options.healthSweep;
|
|
822
911
|
const migrateIdentity = options.migrateIdentity ?? (async () => {
|
|
823
912
|
const legacySubjects = new Set([
|
|
824
913
|
...readTelegramSubjectIndex(agentRoot, subject),
|
|
@@ -848,6 +937,7 @@ function createTelegramSenseApp(options) {
|
|
|
848
937
|
let approvalReconciliationActive = false;
|
|
849
938
|
const approvalReconciliationsInFlight = new Set();
|
|
850
939
|
let nextApprovalReconcileDeadline;
|
|
940
|
+
let reconcileSystemFailsafes;
|
|
851
941
|
let stopPromise;
|
|
852
942
|
let runPromise;
|
|
853
943
|
const clearApprovalReconcileTimer = () => {
|
|
@@ -877,46 +967,244 @@ function createTelegramSenseApp(options) {
|
|
|
877
967
|
approvalReconciliationsInFlight.delete(reconciliation);
|
|
878
968
|
});
|
|
879
969
|
approvalReconciliationsInFlight.add(reconciliation);
|
|
970
|
+
const failsafe = runWithAcceptanceAuditOwner(reconcileSystemFailsafes)
|
|
971
|
+
.finally(() => { approvalReconciliationsInFlight.delete(failsafe); });
|
|
972
|
+
approvalReconciliationsInFlight.add(failsafe);
|
|
880
973
|
}, delay);
|
|
881
974
|
};
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
975
|
+
const deliverButlerEffect = async (text, idempotencyKey, signal, onMessageDelivered) => {
|
|
976
|
+
return executeAuthorizedEffect({
|
|
977
|
+
idempotencyKey,
|
|
978
|
+
target: configuredOwnerTarget(),
|
|
979
|
+
authorClass: "butler",
|
|
980
|
+
effect: { kind: "text", text },
|
|
981
|
+
...(signal ? { signal } : {}),
|
|
982
|
+
...(onMessageDelivered ? { onMessageDelivered } : {}),
|
|
983
|
+
});
|
|
984
|
+
};
|
|
985
|
+
recordAcceptedEffects = async (sessionPath, artifacts, bootstrapInbound, causalEventIds) => {
|
|
986
|
+
await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts, ...(bootstrapInbound ? { inbound: bootstrapInbound } : {}), ...(causalEventIds ? { causalEventIds } : {}) });
|
|
987
|
+
for (const artifact of artifacts) {
|
|
988
|
+
if (artifact.target.kind !== "approved_relationship" || !artifact.target.requestId || artifact.effect.kind !== "text")
|
|
989
|
+
continue;
|
|
990
|
+
const recorded = getEffectJournal().read(artifact.id);
|
|
991
|
+
if (!recorded.parts.every((part) => part.state === "session_recorded"))
|
|
992
|
+
continue;
|
|
993
|
+
if (!artifact.obligationReturnId)
|
|
994
|
+
continue;
|
|
995
|
+
const obligation = (0, obligations_1.readObligation)(agentRoot, artifact.obligationReturnId);
|
|
996
|
+
if (obligation?.returnReadyAt && obligation.requestId === artifact.target.requestId
|
|
997
|
+
&& obligation.owedTo?.friendId === artifact.target.friendId && obligation.owedTo.channel === "telegram"
|
|
998
|
+
&& obligation.owedTo.key === artifact.target.sessionKey)
|
|
999
|
+
(0, obligations_1.fulfillObligation)(agentRoot, obligation.id);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const deliverAwaitFollowUp = async (request) => {
|
|
1003
|
+
if (!request.requestId)
|
|
1004
|
+
return { status: "blocked", detail: "Telegram follow-up is missing its request binding" };
|
|
885
1005
|
try {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
message: "Sanctuary health notification was delivered",
|
|
900
|
-
meta: { agentName: options.agentName, deliveryCount: messageIds.length, ...(0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)(options.agentName) },
|
|
901
|
-
});
|
|
902
|
-
}
|
|
1006
|
+
const obligation = (0, obligations_1.findPendingObligationForRequest)(agentRoot, { requestId: request.requestId, owedTo: { friendId: request.friendId, channel: "telegram", key: request.key } });
|
|
1007
|
+
if (obligation && !obligation.returnReadyAt)
|
|
1008
|
+
(0, obligations_1.markObligationReturnReady)(agentRoot, obligation.id, request.deliveryId ?? `telegram-return:${request.requestId}`);
|
|
1009
|
+
const expiry = request.deliveryId?.endsWith(":expired") === true;
|
|
1010
|
+
const artifact = await executeAuthorizedEffect({
|
|
1011
|
+
idempotencyKey: `await-${expiry ? "expiry-" : ""}follow-up:${request.requestId}:${(0, node_crypto_1.createHash)("sha256").update(request.deliveryId ?? request.content).digest("hex")}`,
|
|
1012
|
+
target: { kind: "approved_relationship", friendId: request.friendId, sessionKey: request.key, requestId: request.requestId },
|
|
1013
|
+
authorClass: "butler",
|
|
1014
|
+
effect: { kind: "text", text: request.content },
|
|
1015
|
+
...(obligation ? { obligationReturnId: obligation.id } : {}),
|
|
1016
|
+
});
|
|
1017
|
+
await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, request.friendId, "telegram", request.key, agentRoot), [artifact]);
|
|
1018
|
+
return { status: "delivered_now", detail: "sent to the exact request-bound Telegram chat" };
|
|
903
1019
|
}
|
|
904
1020
|
catch (error) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1021
|
+
return { status: "blocked", detail: error instanceof Error ? error.message : String(error) };
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
(0, tools_awaiting_1.setAwaitToolDeps)({
|
|
1025
|
+
buildDeliveryDeps: () => ({
|
|
1026
|
+
agentName: options.agentName,
|
|
1027
|
+
queuePending: (message) => (0, pending_1.queuePendingMessage)((0, pending_1.getPrivateRuntimePendingDir)(options.agentName), message),
|
|
1028
|
+
deliverers: {
|
|
1029
|
+
telegram: deliverAwaitFollowUp,
|
|
1030
|
+
},
|
|
1031
|
+
}),
|
|
1032
|
+
});
|
|
1033
|
+
reconcileSystemFailsafes = async () => {
|
|
1034
|
+
if (!options.privilegedFailsafe)
|
|
1035
|
+
return;
|
|
1036
|
+
const target = configuredOwnerTarget();
|
|
1037
|
+
try {
|
|
1038
|
+
await (0, telegram_effect_adapter_1.sweepTelegramSystemFailsafes)({
|
|
1039
|
+
eventRoot: options.privilegedFailsafe.eventRoot,
|
|
1040
|
+
target,
|
|
1041
|
+
verifyProtectiveState: options.privilegedFailsafe.verifyProtectiveState,
|
|
1042
|
+
execute: executeAuthorizedEffect,
|
|
1043
|
+
recordArtifact: async (artifact) => {
|
|
1044
|
+
await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, target.friendId, "telegram", target.sessionKey, agentRoot), [artifact]);
|
|
1045
|
+
},
|
|
912
1046
|
});
|
|
913
1047
|
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.telegram_system_failsafe_error", message: "Telegram system failsafe reconciliation failed", meta: { agentName: options.agentName, subject, error: transportError(error) } });
|
|
1050
|
+
}
|
|
914
1051
|
};
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
1052
|
+
const prepareRelationshipRunAgentOptions = (input) => {
|
|
1053
|
+
if (!options.resolveRelationshipAuthorization)
|
|
1054
|
+
throw new Error("Telegram relationship authorization resolver is unavailable");
|
|
1055
|
+
const relationshipCoordinates = { ...input, botId: botId };
|
|
1056
|
+
const resolveLiveRelationshipAuthorization = async () => {
|
|
1057
|
+
const authorization = await options.resolveRelationshipAuthorization(relationshipCoordinates);
|
|
1058
|
+
if (authorization.subject.friendId !== input.friendId || authorization.subject.admissionState !== "active")
|
|
1059
|
+
throw new Error("Telegram relationship admission is not active");
|
|
1060
|
+
return {
|
|
1061
|
+
requestId: input.requestId,
|
|
1062
|
+
profileId: authorization.profileId,
|
|
1063
|
+
authorizedContextScopes: authorization.authorizedContextScopes,
|
|
1064
|
+
advertisedToolNames: authorization.advertisedToolNames,
|
|
1065
|
+
actor: authorization.actor,
|
|
1066
|
+
authorizeTool: async (name, args) => (await options.resolveRelationshipAuthorization(relationshipCoordinates)).authorizeTool(name, args),
|
|
1067
|
+
};
|
|
1068
|
+
};
|
|
1069
|
+
return async ({ runAgentOptions }) => ({
|
|
1070
|
+
...runAgentOptions,
|
|
1071
|
+
toolContext: { ...runAgentOptions.toolContext, relationshipAuthorization: await resolveLiveRelationshipAuthorization() },
|
|
1072
|
+
});
|
|
1073
|
+
};
|
|
1074
|
+
const dispatchResolvedRelationshipTurn = async (input) => {
|
|
1075
|
+
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, input.friendId, "telegram", input.sessionKey, agentRoot);
|
|
1076
|
+
const effects = [];
|
|
1077
|
+
let ordinal = 0;
|
|
1078
|
+
const deliver = async (text, terminalReturn = false) => {
|
|
1079
|
+
const obligation = terminalReturn ? (0, obligations_1.findPendingObligationForRequest)(agentRoot, {
|
|
1080
|
+
requestId: input.requestId,
|
|
1081
|
+
owedTo: { friendId: input.friendId, channel: "telegram", key: input.sessionKey },
|
|
1082
|
+
}) : undefined;
|
|
1083
|
+
effects.push(await executeAuthorizedEffect({
|
|
1084
|
+
idempotencyKey: `relationship-turn:${input.requestId}:delivery:${ordinal++}`,
|
|
1085
|
+
target: { kind: "approved_relationship", friendId: input.friendId, sessionKey: input.sessionKey, requestId: input.requestId },
|
|
1086
|
+
authorClass: "butler",
|
|
1087
|
+
effect: { kind: "text", text },
|
|
1088
|
+
...(obligation?.returnReadyAt ? { obligationReturnId: obligation.id } : {}),
|
|
1089
|
+
}));
|
|
1090
|
+
};
|
|
1091
|
+
const orientationFrame = input.orientation?.kind === "newly_admitted"
|
|
1092
|
+
? {
|
|
1093
|
+
...(0, orientation_frame_1.buildOrientationFrame)({ channel: "telegram", messages: [{ role: "user", content: input.text }] }),
|
|
1094
|
+
source: {
|
|
1095
|
+
kind: "telegram_newly_admitted",
|
|
1096
|
+
authority: "presentation_only",
|
|
1097
|
+
routingHint: "This is this person’s first admitted turn. Welcome them warmly and briefly explain what the household Butler can help with before answering their request.",
|
|
1098
|
+
},
|
|
1099
|
+
}
|
|
1100
|
+
: undefined;
|
|
1101
|
+
const result = await runTurn({
|
|
1102
|
+
agentName: options.agentName,
|
|
1103
|
+
channel: "telegram",
|
|
1104
|
+
sessionKey: input.sessionKey,
|
|
1105
|
+
friendId: input.friendId,
|
|
1106
|
+
identity: { provider: "telegram-user", externalId: input.userId, displayName: "Household member", tenantId: botId },
|
|
1107
|
+
userMessage: input.text,
|
|
1108
|
+
ingressRelations: { replyToEventId: null, threadRootEventId: null, references: [input.reference] },
|
|
1109
|
+
precommittedIngress: { eventId: input.eventId, reference: input.reference },
|
|
1110
|
+
...(orientationFrame ? { orientationFrame } : {}),
|
|
1111
|
+
toolContext: { ...(toolContext ?? {}) },
|
|
1112
|
+
prepareRunAgentOptions: prepareRelationshipRunAgentOptions({ friendId: input.friendId, requestId: input.requestId,
|
|
1113
|
+
sessionEventId: input.eventId, userId: input.userId, chatId: input.chatId, sessionKey: input.sessionKey }),
|
|
1114
|
+
deliverySink: { onDelivery: (delivery) => deliver(delivery.text, delivery.kind === "settle") },
|
|
1115
|
+
});
|
|
1116
|
+
if (effects.length === 0 && result.response.trim())
|
|
1117
|
+
await deliver(result.response, true);
|
|
1118
|
+
const causalEventIds = Object.fromEntries(effects.flatMap((artifact, index) => {
|
|
1119
|
+
const eventId = result.causalSessionEventIds?.[index] ?? (effects.length === 1 ? result.responseCausalSessionEventId : undefined);
|
|
1120
|
+
return eventId ? [[artifact.id, eventId]] : [];
|
|
1121
|
+
}));
|
|
1122
|
+
await recordAcceptedEffects(sessionPath, effects, undefined, causalEventIds);
|
|
1123
|
+
};
|
|
1124
|
+
const admissionController = options.admission && admissionStore ? (0, telegram_admission_1.createTelegramAdmissionController)({
|
|
1125
|
+
store: admissionStore,
|
|
1126
|
+
owner: { friendId: configuredOwnerFriendId, sessionKey: configuredOwnerSessionKey },
|
|
1127
|
+
sendEffect: async (request) => {
|
|
1128
|
+
const artifact = await executeAuthorizedEffect(request);
|
|
1129
|
+
if (artifact.target.kind === "approved_relationship") {
|
|
1130
|
+
await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, artifact.target.friendId, "telegram", artifact.target.sessionKey, agentRoot), [artifact]);
|
|
1131
|
+
}
|
|
1132
|
+
return artifact.id;
|
|
1133
|
+
},
|
|
1134
|
+
resolveOwnerCard: (messageId) => {
|
|
1135
|
+
const resolved = (0, telegram_effect_adapter_1.resolveTelegramControlArtifact)(getEffectJournal(), { messageId, friendId: configuredOwnerFriendId, sessionKey: configuredOwnerSessionKey });
|
|
1136
|
+
return resolved?.requestId
|
|
1137
|
+
? { artifactId: resolved.artifactId, admissionId: resolved.requestId }
|
|
1138
|
+
: null;
|
|
1139
|
+
},
|
|
1140
|
+
resolveOwnerCardMessageId: (artifactId) => {
|
|
1141
|
+
const artifact = getEffectJournal().read(artifactId);
|
|
1142
|
+
if (artifact.authorClass !== "control" || artifact.target.kind !== "approved_relationship"
|
|
1143
|
+
|| artifact.target.friendId !== configuredOwnerFriendId || artifact.target.sessionKey !== configuredOwnerSessionKey)
|
|
1144
|
+
return null;
|
|
1145
|
+
return artifact.parts.find((part) => part.state === "session_recorded" && part.messageId)?.messageId ?? null;
|
|
1146
|
+
},
|
|
1147
|
+
claimFriend: options.admission.claimFriend,
|
|
1148
|
+
revokeFriend: options.admission.revokeFriend,
|
|
1149
|
+
commitApprovedIngress: async (input) => {
|
|
1150
|
+
const sessionKey = `telegram:${input.botId}:${input.userId}`;
|
|
1151
|
+
const reference = `telegram-admission:${input.admissionId}`;
|
|
1152
|
+
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, input.friendId, "telegram", sessionKey, agentRoot);
|
|
1153
|
+
const receipt = await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts: [], inbound: { text: input.text, reference } });
|
|
1154
|
+
return { admissionId: input.admissionId, friendId: input.friendId, sessionKey, eventId: receipt.eventId, reference };
|
|
1155
|
+
},
|
|
1156
|
+
enqueueApprovedWork: async (input) => {
|
|
1157
|
+
if (!inboxStore.captureAdmittedWork)
|
|
1158
|
+
throw new Error("Telegram admitted work inbox is unavailable");
|
|
1159
|
+
inboxStore.captureAdmittedWork(input);
|
|
1160
|
+
},
|
|
1161
|
+
dispatchApprovedWork: async (input) => {
|
|
1162
|
+
if (!inboxStore.admittedWorkState || !inboxStore.claimAdmittedWork || !inboxStore.completeAdmittedWork)
|
|
1163
|
+
return "indeterminate";
|
|
1164
|
+
const state = inboxStore.admittedWorkState(input.admissionId);
|
|
1165
|
+
if (state === "settled")
|
|
1166
|
+
return "settled";
|
|
1167
|
+
if (state !== "pending" || !inboxStore.claimAdmittedWork(input.admissionId))
|
|
1168
|
+
return "indeterminate";
|
|
1169
|
+
try {
|
|
1170
|
+
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, input.friendId, "telegram", input.sessionKey, agentRoot);
|
|
1171
|
+
const envelope = (0, session_events_1.loadSessionEnvelopeFile)(sessionPath);
|
|
1172
|
+
const event = envelope?.events.find((candidate) => candidate.id === input.eventId
|
|
1173
|
+
&& candidate.role === "user" && candidate.relations.references.includes(input.reference));
|
|
1174
|
+
if (!event || typeof event.content !== "string")
|
|
1175
|
+
throw new Error("Telegram admitted work ingress is unavailable");
|
|
1176
|
+
const record = admissionStore.read(input.admissionId);
|
|
1177
|
+
await dispatchResolvedRelationshipTurn({ friendId: input.friendId, sessionKey: input.sessionKey, requestId: input.admissionId,
|
|
1178
|
+
eventId: input.eventId, reference: input.reference, text: event.content, userId: record.userId, chatId: record.chatId, orientation: input.orientation });
|
|
1179
|
+
inboxStore.completeAdmittedWork(input.admissionId);
|
|
1180
|
+
return "settled";
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
return "indeterminate";
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
withDecisionLease: (admissionId, work) => (0, session_transaction_1.withSessionTurnLease)(path.join(agentRoot, "state", "senses", "telegram", "admission-decisions", admissionId), async () => work()),
|
|
1187
|
+
...(options.admission.createDisplayCode ? { createDisplayCode: options.admission.createDisplayCode } : {}),
|
|
1188
|
+
}) : undefined;
|
|
1189
|
+
const recoverEffectOutbox = async () => {
|
|
1190
|
+
if (!effectJournal && !(0, node_fs_1.existsSync)(path.join(agentRoot, "state", "telegram", "effects")))
|
|
1191
|
+
return;
|
|
1192
|
+
await (0, telegram_effect_adapter_1.recoverTelegramEffectOutbox)({
|
|
1193
|
+
store: getEffectJournal(),
|
|
1194
|
+
execute: executeAuthorizedEffect,
|
|
1195
|
+
matches: (artifact) => artifact.target.kind === "approved_relationship",
|
|
1196
|
+
});
|
|
1197
|
+
const accepted = getEffectJournal().list().filter((artifact) => artifact.target.kind === "approved_relationship" && artifact.parts.some((part) => part.state === "accepted"));
|
|
1198
|
+
for (const artifact of accepted) {
|
|
1199
|
+
const target = artifact.target;
|
|
1200
|
+
await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, target.friendId, "telegram", target.sessionKey, agentRoot), [artifact]);
|
|
1201
|
+
}
|
|
918
1202
|
};
|
|
919
1203
|
const onMessageBody = async (message) => {
|
|
1204
|
+
const currentFriendId = configuredOwnerFriendId;
|
|
1205
|
+
const currentSessionKey = configuredOwnerSessionKey;
|
|
1206
|
+
const currentSessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, currentFriendId, "telegram", currentSessionKey, agentRoot);
|
|
1207
|
+
const inboundReference = `telegram-inbound:${(0, node_crypto_1.createHmac)("sha256", identityKey).update(`${message.updateId}\0${message.messageId}`).digest("hex")}`;
|
|
920
1208
|
const acceptanceMarker = options.acceptanceMarker ? options.acceptanceMarker() : (0, sanctuary_acceptance_marker_1.readSanctuaryAcceptanceMarker)(options.agentName);
|
|
921
1209
|
const acceptanceMeta = (0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)(options.agentName);
|
|
922
1210
|
const groundedAcceptance = acceptanceMarker?.label === "unit-16d-whats-up" || acceptanceMarker?.label === "unit-16d-1-space";
|
|
@@ -950,23 +1238,51 @@ function createTelegramSenseApp(options) {
|
|
|
950
1238
|
const turnMetricsObserver = { providerInvocationCount: 0, toolInvocationCount: 0 };
|
|
951
1239
|
const toolReceiptObserver = { toolResultDigests: [], toolGroundings: [] };
|
|
952
1240
|
const normalizedRequest = message.text.normalize("NFKC").trim().toLocaleLowerCase("en-US");
|
|
953
|
-
const groundingIntentTool =
|
|
954
|
-
|
|
955
|
-
:
|
|
1241
|
+
const groundingIntentTool = groundedAcceptance
|
|
1242
|
+
? /^(?:what['’]?s up|status)\??$/u.test(normalizedRequest) ? "unraid_get_system"
|
|
1243
|
+
: /^how much (?:space|storage) is left\??$/u.test(normalizedRequest) ? "unraid_get_storage"
|
|
1244
|
+
: null
|
|
1245
|
+
: null;
|
|
956
1246
|
const bufferedGroundedDeliveries = [];
|
|
1247
|
+
const turnEffects = [];
|
|
1248
|
+
let deliveryOrdinal = 0;
|
|
1249
|
+
await recoverEffectOutbox();
|
|
1250
|
+
let ingressRelations = { replyToEventId: null, threadRootEventId: null, references: [inboundReference] };
|
|
1251
|
+
if (message.replyToMessageId && /^[1-9][0-9]*$/u.test(message.replyToMessageId)) {
|
|
1252
|
+
const replyMessageId = Number(message.replyToMessageId);
|
|
1253
|
+
const reply = (0, telegram_effect_adapter_1.resolveTelegramReply)(getEffectJournal(), { messageId: replyMessageId, friendId: currentFriendId, sessionKey: currentSessionKey });
|
|
1254
|
+
const session = reply ? (0, session_events_1.loadSessionEnvelopeFile)(currentSessionPath) : null;
|
|
1255
|
+
if (reply && session?.events.some((event) => event.id === reply.sessionEventId))
|
|
1256
|
+
ingressRelations = {
|
|
1257
|
+
replyToEventId: reply.sessionEventId,
|
|
1258
|
+
threadRootEventId: null,
|
|
1259
|
+
references: [inboundReference, `telegram-artifact:${reply.artifactId}`, ...(reply.requestId ? [`request:${reply.requestId}`] : [])],
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
957
1262
|
try {
|
|
958
1263
|
acceptanceAuditBarrier();
|
|
1264
|
+
const ingressReceipt = options.resolveRelationshipAuthorization
|
|
1265
|
+
? await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({
|
|
1266
|
+
store: getEffectJournal(),
|
|
1267
|
+
sessionPath: currentSessionPath,
|
|
1268
|
+
artifacts: [],
|
|
1269
|
+
inbound: { text: message.text, reference: inboundReference, relations: ingressRelations },
|
|
1270
|
+
})
|
|
1271
|
+
: null;
|
|
959
1272
|
const collected = await collectToolReceipts(() => runTurn({
|
|
960
1273
|
agentName: options.agentName,
|
|
961
1274
|
channel: "telegram",
|
|
962
|
-
sessionKey:
|
|
963
|
-
friendId:
|
|
1275
|
+
sessionKey: currentSessionKey,
|
|
1276
|
+
friendId: currentFriendId,
|
|
964
1277
|
identity: {
|
|
965
1278
|
provider: "telegram-user",
|
|
966
|
-
externalId: subject,
|
|
967
|
-
displayName: `Telegram user ${subject}`,
|
|
1279
|
+
externalId: options.admission ? message.userId : subject,
|
|
1280
|
+
displayName: options.admission ? "Ari" : `Telegram user ${subject}`,
|
|
1281
|
+
...(options.admission ? { tenantId: botId } : {}),
|
|
968
1282
|
},
|
|
969
1283
|
userMessage: message.text,
|
|
1284
|
+
ingressRelations,
|
|
1285
|
+
...(ingressReceipt ? { precommittedIngress: ingressReceipt } : {}),
|
|
970
1286
|
turnMetricsObserver,
|
|
971
1287
|
deliverySink: {
|
|
972
1288
|
onDelivery: async (delivery) => {
|
|
@@ -974,15 +1290,26 @@ function createTelegramSenseApp(options) {
|
|
|
974
1290
|
bufferedGroundedDeliveries.push(delivery.kind);
|
|
975
1291
|
}
|
|
976
1292
|
else {
|
|
977
|
-
await
|
|
1293
|
+
turnEffects.push(await deliverButlerEffect(delivery.text, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
|
|
978
1294
|
deliveryCount += 1;
|
|
979
1295
|
}
|
|
980
1296
|
},
|
|
981
1297
|
},
|
|
982
1298
|
...(toolContext ? { toolContext } : {}),
|
|
1299
|
+
...(options.resolveRelationshipAuthorization ? {
|
|
1300
|
+
prepareRunAgentOptions: prepareRelationshipRunAgentOptions({
|
|
1301
|
+
friendId: currentFriendId,
|
|
1302
|
+
requestId: inboundReference,
|
|
1303
|
+
sessionEventId: ingressReceipt.eventId,
|
|
1304
|
+
userId: message.userId,
|
|
1305
|
+
chatId: message.chatId,
|
|
1306
|
+
sessionKey: currentSessionKey,
|
|
1307
|
+
}),
|
|
1308
|
+
} : {}),
|
|
983
1309
|
...(approvalRuntime ? { approvalCoordinatorFactory: approvalRuntime.coordinator } : {}),
|
|
984
1310
|
}), toolReceiptObserver);
|
|
985
1311
|
const result = collected.result;
|
|
1312
|
+
let responseFallbackArtifactId;
|
|
986
1313
|
turnMetricsObserver.providerInvocationCount = Math.max(turnMetricsObserver.providerInvocationCount, result.providerInvocationCount ?? 0);
|
|
987
1314
|
turnMetricsObserver.toolInvocationCount = Math.max(turnMetricsObserver.toolInvocationCount, result.toolInvocationCount ?? 0);
|
|
988
1315
|
if (groundingIntentTool) {
|
|
@@ -991,11 +1318,24 @@ function createTelegramSenseApp(options) {
|
|
|
991
1318
|
|| (bufferedGroundedDeliveries.length === 1 && bufferedGroundedDeliveries[0] !== "settle"))
|
|
992
1319
|
throw new Error("Canonical Sanctuary query did not produce exactly one matching grounded settle");
|
|
993
1320
|
const canonical = (0, sanctuary_grounding_1.renderSanctuaryGroundedResponse)(grounding.toolName, grounding.facts);
|
|
994
|
-
await
|
|
1321
|
+
turnEffects.push(await deliverButlerEffect(canonical, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
|
|
995
1322
|
deliveryCount = 1;
|
|
996
1323
|
}
|
|
997
1324
|
else if (deliveryCount === 0 && result.response.trim()) {
|
|
998
|
-
await
|
|
1325
|
+
const artifact = await deliverButlerEffect(result.response, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); });
|
|
1326
|
+
turnEffects.push(artifact);
|
|
1327
|
+
responseFallbackArtifactId = artifact.id;
|
|
1328
|
+
}
|
|
1329
|
+
if (result.sessionPath) {
|
|
1330
|
+
const causalEventIds = Object.fromEntries((groundingIntentTool ? [] : turnEffects).flatMap((artifact, index) => {
|
|
1331
|
+
const eventId = result.causalSessionEventIds?.[index];
|
|
1332
|
+
if (eventId)
|
|
1333
|
+
return [[artifact.id, eventId]];
|
|
1334
|
+
return artifact.id === responseFallbackArtifactId && result.responseCausalSessionEventId
|
|
1335
|
+
? [[artifact.id, result.responseCausalSessionEventId]]
|
|
1336
|
+
: [];
|
|
1337
|
+
}));
|
|
1338
|
+
await recordAcceptedEffects(result.sessionPath, turnEffects, undefined, causalEventIds);
|
|
999
1339
|
}
|
|
1000
1340
|
(0, runtime_1.emitNervesEvent)({
|
|
1001
1341
|
component: "senses",
|
|
@@ -1026,7 +1366,8 @@ function createTelegramSenseApp(options) {
|
|
|
1026
1366
|
});
|
|
1027
1367
|
const fallback = "I couldn't complete that turn. The failure was recorded; please try again.";
|
|
1028
1368
|
if (deliveredMessageIds.length === 0)
|
|
1029
|
-
await
|
|
1369
|
+
turnEffects.push(await deliverButlerEffect(fallback, `turn:${subject}:${message.updateId}:fallback`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
|
|
1370
|
+
await recordAcceptedEffects(currentSessionPath, turnEffects, { text: message.text, reference: inboundReference });
|
|
1030
1371
|
}
|
|
1031
1372
|
finally {
|
|
1032
1373
|
if (acceptanceMarker) {
|
|
@@ -1068,9 +1409,54 @@ function createTelegramSenseApp(options) {
|
|
|
1068
1409
|
}
|
|
1069
1410
|
}
|
|
1070
1411
|
};
|
|
1071
|
-
const onMessage = (message) => runWithAcceptanceAuditOwner(() =>
|
|
1412
|
+
const onMessage = (message) => runWithAcceptanceAuditOwner(async () => {
|
|
1413
|
+
const ownerDecision = admissionController?.parseOwnerDecision({
|
|
1414
|
+
text: message.text,
|
|
1415
|
+
...(message.replyToMessageId ? { replyToMessageId: Number(message.replyToMessageId) } : {}),
|
|
1416
|
+
});
|
|
1417
|
+
if (ownerDecision) {
|
|
1418
|
+
const actor = await options.admission?.resolveOwner({ botId: botId, userId: message.userId, chatId: message.chatId, sessionKey: configuredOwnerSessionKey });
|
|
1419
|
+
if (!actor)
|
|
1420
|
+
throw new Error("Telegram admission owner decision identity is invalid");
|
|
1421
|
+
await admissionController.decide({ ...ownerDecision, actorFriendId: actor.friendId });
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
await onMessageBody(message);
|
|
1425
|
+
});
|
|
1426
|
+
const onUnknownMessage = admissionController && options.admission ? async (message) => {
|
|
1427
|
+
const approved = await options.admission.resolveApprovedFriend(message);
|
|
1428
|
+
if (!approved) {
|
|
1429
|
+
await admissionController.handleUnknown(message);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const sessionKey = `telegram:${message.botId}:${message.userId}`;
|
|
1433
|
+
const reference = `telegram-inbound:${(0, node_crypto_1.createHmac)("sha256", identityKey).update(`${message.botId}\0${message.userId}\0${message.updateId}\0${message.messageId}`).digest("hex")}`;
|
|
1434
|
+
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, approved.friendId, "telegram", sessionKey, agentRoot);
|
|
1435
|
+
const receipt = await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts: [], inbound: { text: message.text, reference } });
|
|
1436
|
+
await dispatchResolvedRelationshipTurn({ friendId: approved.friendId, sessionKey, requestId: reference, eventId: receipt.eventId,
|
|
1437
|
+
reference, text: message.text, userId: message.userId, chatId: message.chatId });
|
|
1438
|
+
} : undefined;
|
|
1072
1439
|
const onUpdate = async (update) => {
|
|
1073
|
-
|
|
1440
|
+
const callback = update.callback_query;
|
|
1441
|
+
if (callback?.data?.startsWith("admit:") && admissionController && callback.message) {
|
|
1442
|
+
return runWithAcceptanceAuditOwner(async () => {
|
|
1443
|
+
const actor = await options.admission?.resolveOwner({ botId: botId, userId: String(callback.from.id), chatId: String(callback.message.chat.id), sessionKey: configuredOwnerSessionKey });
|
|
1444
|
+
if (!actor)
|
|
1445
|
+
throw new Error("Telegram admission owner callback identity is invalid");
|
|
1446
|
+
const decision = admissionController.parseCallback(callback.data, callback.message.message_id);
|
|
1447
|
+
await admissionController.decide({ ...decision, actorFriendId: actor.friendId });
|
|
1448
|
+
const ownerTarget = configuredOwnerTarget();
|
|
1449
|
+
const artifact = await executeAuthorizedEffect({
|
|
1450
|
+
idempotencyKey: `admission-callback:${callback.id}`,
|
|
1451
|
+
target: { ...ownerTarget, requestId: decision.admissionId },
|
|
1452
|
+
authorClass: "control",
|
|
1453
|
+
effect: { kind: "callback_ack", callbackQueryId: callback.id },
|
|
1454
|
+
});
|
|
1455
|
+
await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, ownerTarget.friendId, "telegram", ownerTarget.sessionKey, agentRoot), [artifact]);
|
|
1456
|
+
return true;
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
if (!callback || !approvalTransport)
|
|
1074
1460
|
return false;
|
|
1075
1461
|
return runWithAcceptanceAuditOwner(async () => (await approvalTransport.handleUpdate(update)).handled);
|
|
1076
1462
|
};
|
|
@@ -1080,6 +1466,7 @@ function createTelegramSenseApp(options) {
|
|
|
1080
1466
|
api,
|
|
1081
1467
|
expectedUserId: authorizedUserId,
|
|
1082
1468
|
expectedChatId: authorizedChatId,
|
|
1469
|
+
...(onUnknownMessage ? { botId: botId, onUnknownMessage } : {}),
|
|
1083
1470
|
offsetStore,
|
|
1084
1471
|
inboxStore,
|
|
1085
1472
|
onMessage,
|
|
@@ -1122,6 +1509,16 @@ function createTelegramSenseApp(options) {
|
|
|
1122
1509
|
return runPromise;
|
|
1123
1510
|
runPromise = (async () => {
|
|
1124
1511
|
await runWithAcceptanceAuditOwner(migrateIdentity);
|
|
1512
|
+
inboxStore.quarantineStrandedAdmittedWork?.();
|
|
1513
|
+
await runWithAcceptanceAuditOwner(async () => { await admissionController?.recover(); });
|
|
1514
|
+
await runWithAcceptanceAuditOwner(recoverEffectOutbox);
|
|
1515
|
+
await runWithAcceptanceAuditOwner(reconcileSystemFailsafes);
|
|
1516
|
+
try {
|
|
1517
|
+
await runWithAcceptanceAuditOwner(async () => { await toolContext?.sanctuary?.recoverRoutineActions?.(); });
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.sanctuary_routine_recovery_error", message: "Sanctuary routine action recovery requires inspection", meta: { agentName: options.agentName, subject, category: error instanceof Error ? error.name : "unknown" } });
|
|
1521
|
+
}
|
|
1125
1522
|
await runWithAcceptanceAuditOwner(async () => { await approvalRuntime?.recover(); });
|
|
1126
1523
|
await runWithAcceptanceAuditOwner(async () => { await interactiveControl?.start(); });
|
|
1127
1524
|
try {
|
|
@@ -1137,7 +1534,6 @@ function createTelegramSenseApp(options) {
|
|
|
1137
1534
|
meta: { agentName: options.agentName, subject, error: transportError(error) },
|
|
1138
1535
|
});
|
|
1139
1536
|
}
|
|
1140
|
-
await runWithAcceptanceAuditOwner(runHealthSweep);
|
|
1141
1537
|
(0, runtime_1.emitNervesEvent)({
|
|
1142
1538
|
component: "senses",
|
|
1143
1539
|
event: "senses.telegram_poll_start",
|
|
@@ -1174,7 +1570,26 @@ function createTelegramSenseApp(options) {
|
|
|
1174
1570
|
return runPromise;
|
|
1175
1571
|
},
|
|
1176
1572
|
async sendProactive(text, signal) {
|
|
1177
|
-
await runWithAcceptanceAuditOwner(() =>
|
|
1573
|
+
await runWithAcceptanceAuditOwner(async () => {
|
|
1574
|
+
const effect = await deliverButlerEffect(requiredText(text, "proactive message"), `proactive:${(0, node_crypto_1.randomUUID)()}`, signal);
|
|
1575
|
+
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
1576
|
+
await recordAcceptedEffects(sessionPath, [effect]);
|
|
1577
|
+
});
|
|
1578
|
+
},
|
|
1579
|
+
async sendExternalEventDecision(input) {
|
|
1580
|
+
const source = requiredText(input.source, "external event source");
|
|
1581
|
+
const eventId = requiredText(input.eventId, "external event id");
|
|
1582
|
+
if (Buffer.byteLength(source) > 512 || Buffer.byteLength(eventId) > 512 || !Number.isSafeInteger(input.generation) || input.generation < 1)
|
|
1583
|
+
throw new Error("Telegram external event identity is invalid");
|
|
1584
|
+
await runWithAcceptanceAuditOwner(async () => {
|
|
1585
|
+
const receiptIdentity = JSON.stringify([source, eventId, input.generation]);
|
|
1586
|
+
const effect = await deliverButlerEffect(requiredText(input.text, "external event decision"), `external-event:${receiptIdentity}`, input.signal);
|
|
1587
|
+
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
1588
|
+
await recordAcceptedEffects(sessionPath, [effect]);
|
|
1589
|
+
});
|
|
1590
|
+
},
|
|
1591
|
+
async sendAwaitFollowUp(request) {
|
|
1592
|
+
return runWithAcceptanceAuditOwner(() => deliverAwaitFollowUp(request));
|
|
1178
1593
|
},
|
|
1179
1594
|
stop() {
|
|
1180
1595
|
if (stopPromise)
|
|
@@ -1195,6 +1610,9 @@ function createTelegramSenseApp(options) {
|
|
|
1195
1610
|
await attempt(async () => { await interactiveControl?.stop(); });
|
|
1196
1611
|
await attempt(() => api.stop());
|
|
1197
1612
|
await attempt(() => approvalRuntime?.close());
|
|
1613
|
+
await attempt(() => effectJournal?.close());
|
|
1614
|
+
await attempt(() => admissionStore?.close());
|
|
1615
|
+
await attempt(() => (0, tools_awaiting_1.resetAwaitToolDeps)());
|
|
1198
1616
|
await attempt(() => runWithAcceptanceAuditOwner(() => {
|
|
1199
1617
|
(0, runtime_1.emitNervesEvent)({
|
|
1200
1618
|
component: "senses",
|
|
@@ -1220,8 +1638,252 @@ function loadTelegramSenseCredentials(agentName) {
|
|
|
1220
1638
|
}
|
|
1221
1639
|
return parseTelegramSenseCredentials(runtime.config);
|
|
1222
1640
|
}
|
|
1641
|
+
async function createProductionTelegramRelationshipComposition(agentName, credentials, agentRootOverride) {
|
|
1642
|
+
const agentRoot = agentRootOverride ?? (0, identity_1.getAgentRoot)(agentName);
|
|
1643
|
+
const botId = canonicalTelegramId(credentials.botId ?? telegramBotIdFromToken(credentials.botToken), "bot id");
|
|
1644
|
+
const ownerUserId = canonicalTelegramId(credentials.authorizedUserId, "authorized user id");
|
|
1645
|
+
const ownerChatId = canonicalTelegramId(credentials.authorizedChatId, "authorized chat id");
|
|
1646
|
+
if (ownerUserId !== ownerChatId)
|
|
1647
|
+
throw new Error("Telegram owner relationship requires a private user-bound chat");
|
|
1648
|
+
const ownerSessionKey = `telegram:${opaqueTelegramSubject(readOrCreateTelegramIdentityKey(agentRoot), botId, ownerUserId, ownerChatId)}`;
|
|
1649
|
+
const store = new friends_1.FileFriendStore(path.join(agentRoot, "friends"));
|
|
1650
|
+
const sameBotTelegramBindings = (friend) => friend.externalIds.filter((external) => external.provider === "telegram-user" && external.tenantId === botId);
|
|
1651
|
+
const exactTelegramIdentity = (friend, userId) => {
|
|
1652
|
+
const bindings = sameBotTelegramBindings(friend);
|
|
1653
|
+
return bindings.length === 1 && /^[1-9][0-9]*$/u.test(bindings[0].externalId) && bindings[0].externalId === userId;
|
|
1654
|
+
};
|
|
1655
|
+
const canonicalOwner = (friend) => Boolean(friend
|
|
1656
|
+
&& exactTelegramIdentity(friend, ownerUserId) && friend.admissionState === "active" && friend.trustLevel === "family"
|
|
1657
|
+
&& friend.initiativePolicy === "proactive" && friend.capabilityProfileId === "sanctuary-owner");
|
|
1658
|
+
const canonicalHousehold = (friend, userId) => Boolean(friend
|
|
1659
|
+
&& exactTelegramIdentity(friend, userId) && friend.admissionState === "active" && friend.trustLevel === "friend"
|
|
1660
|
+
&& friend.initiativePolicy === "request_follow_up_only" && friend.capabilityProfileId === "sanctuary-household");
|
|
1661
|
+
const owner = await store.findByExternalId("telegram-user", ownerUserId, botId);
|
|
1662
|
+
if (!owner || !canonicalOwner(owner)) {
|
|
1663
|
+
throw new Error("Telegram owner Friend must be active with the sanctuary-owner profile and exact bot identity");
|
|
1664
|
+
}
|
|
1665
|
+
const registry = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot);
|
|
1666
|
+
if (!registry.profiles["sanctuary-owner"] || !registry.profiles["sanctuary-household"]) {
|
|
1667
|
+
throw new Error("Telegram relationship capability registry is missing canonical profiles");
|
|
1668
|
+
}
|
|
1669
|
+
const assertLiveOwner = async (actorFriendId) => {
|
|
1670
|
+
const current = await store.get(owner.id);
|
|
1671
|
+
if (actorFriendId !== owner.id || !canonicalOwner(current))
|
|
1672
|
+
throw new Error("Only the live Telegram owner may manage contacts");
|
|
1673
|
+
};
|
|
1674
|
+
const admissionsRoot = path.join(agentRoot, "state", "senses", "telegram", "admissions");
|
|
1675
|
+
const invalidatePendingApprovals = (sessionPath) => {
|
|
1676
|
+
const stateRoot = path.join(agentRoot, "state", "approvals");
|
|
1677
|
+
const checkpoints = new approval_files_1.FileApprovalCheckpointStore(path.join(stateRoot, "checkpoints.json"));
|
|
1678
|
+
const tokens = new approval_files_1.FileApprovalTokenStore(path.join(stateRoot, "tokens.json"));
|
|
1679
|
+
const approvals = (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite") });
|
|
1680
|
+
try {
|
|
1681
|
+
for (const checkpoint of checkpoints.list()) {
|
|
1682
|
+
const approval = approvals.read(checkpoint.approvalId);
|
|
1683
|
+
if (!approval || path.resolve(approval.sessionPath) !== path.resolve(sessionPath))
|
|
1684
|
+
continue;
|
|
1685
|
+
const decisionToken = tokens.get(checkpoint.approvalId);
|
|
1686
|
+
if (approval.state === "preparing")
|
|
1687
|
+
approvals.recoverPreparing({ approvalId: approval.approvalId, state: "abandoned_before_attempt", reason: "requesting relationship was revoked" });
|
|
1688
|
+
else if (approval.state === "awaiting_prompt_binding")
|
|
1689
|
+
approvals.abandonPromptBinding({ approvalId: approval.approvalId, reason: "requesting relationship was revoked" });
|
|
1690
|
+
else if (approval.state === "proposed" && decisionToken)
|
|
1691
|
+
approvals.decide({
|
|
1692
|
+
approvalId: approval.approvalId, decisionToken, decision: "deny", requesterId: approval.requesterId,
|
|
1693
|
+
transport: approval.transport, transportUserId: approval.transportUserId, transportChatId: approval.transportChatId,
|
|
1694
|
+
transportMessageId: approval.transportMessageId, sessionKey: approval.sessionKey, ownerId: `relationship-revocation-${(0, node_crypto_1.randomUUID)()}`,
|
|
1695
|
+
});
|
|
1696
|
+
else if (approval.state === "proposed")
|
|
1697
|
+
throw new Error(`Cannot revoke relationship while approval ${approval.approvalId} is missing its decision token`);
|
|
1698
|
+
else if (approval.state === "claimed" && approval.ownerId)
|
|
1699
|
+
approvals.abandonBeforeAttempt({ approvalId: approval.approvalId, ownerId: approval.ownerId, epoch: approval.epoch, reason: "requesting relationship was revoked" });
|
|
1700
|
+
else if (approval.state === "claimed")
|
|
1701
|
+
throw new Error(`Cannot revoke relationship while approval ${approval.approvalId} is missing its claim owner`);
|
|
1702
|
+
const terminal = approvals.read(checkpoint.approvalId);
|
|
1703
|
+
if (terminal && terminal.state !== "attempted")
|
|
1704
|
+
checkpoints.remove(checkpoint.approvalId);
|
|
1705
|
+
tokens.remove(checkpoint.approvalId);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
finally {
|
|
1709
|
+
approvals.close();
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
const telegramContactManager = {
|
|
1713
|
+
list: async ({ actorFriendId }) => {
|
|
1714
|
+
await assertLiveOwner(actorFriendId);
|
|
1715
|
+
const contacts = (await store.listAll()).flatMap((friend) => {
|
|
1716
|
+
if (friend.id === owner.id)
|
|
1717
|
+
return [];
|
|
1718
|
+
const bindings = sameBotTelegramBindings(friend);
|
|
1719
|
+
if (bindings.length !== 1 || !/^[1-9][0-9]*$/u.test(bindings[0].externalId))
|
|
1720
|
+
return [];
|
|
1721
|
+
return [{ friendId: friend.id, name: friend.name, userId: bindings[0].externalId, admissionState: friend.admissionState ?? "unverified", initiativePolicy: friend.initiativePolicy ?? "none" }];
|
|
1722
|
+
}).slice(0, 256);
|
|
1723
|
+
const admissions = new telegram_admission_1.FileTelegramAdmissionStore(admissionsRoot);
|
|
1724
|
+
try {
|
|
1725
|
+
const blocked = admissions.list().filter((record) => record.status === "blocked" && record.botId === botId)
|
|
1726
|
+
.slice(0, 256).map((record) => ({ admissionId: record.id, userId: record.userId, displayCode: record.displayCode, status: "blocked", createdAt: new Date(record.createdAt).toISOString() }));
|
|
1727
|
+
return { contacts, blocked };
|
|
1728
|
+
}
|
|
1729
|
+
finally {
|
|
1730
|
+
admissions.close();
|
|
1731
|
+
}
|
|
1732
|
+
},
|
|
1733
|
+
revoke: async ({ actorFriendId, friendId }) => {
|
|
1734
|
+
await assertLiveOwner(actorFriendId);
|
|
1735
|
+
if (friendId === owner.id)
|
|
1736
|
+
throw new Error("The Telegram owner relationship cannot revoke itself");
|
|
1737
|
+
const current = await store.get(friendId);
|
|
1738
|
+
if (!current)
|
|
1739
|
+
throw new Error("Telegram contact was not found");
|
|
1740
|
+
const bindings = sameBotTelegramBindings(current);
|
|
1741
|
+
if (bindings.length !== 1 || !/^[1-9][0-9]*$/u.test(bindings[0].externalId) || bindings[0].externalId === ownerUserId)
|
|
1742
|
+
throw new Error("Telegram contact identity is not exact");
|
|
1743
|
+
const sessionKey = `telegram:${botId}:${bindings[0].externalId}`;
|
|
1744
|
+
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(agentName, current.id, "telegram", sessionKey, agentRoot);
|
|
1745
|
+
await (0, session_transaction_1.withSessionTurnLease)(sessionPath, async () => {
|
|
1746
|
+
invalidatePendingApprovals(sessionPath);
|
|
1747
|
+
const live = await store.get(current.id);
|
|
1748
|
+
if (!live || !exactTelegramIdentity(live, bindings[0].externalId))
|
|
1749
|
+
throw new Error("Telegram contact identity changed");
|
|
1750
|
+
const { capabilityProfileId: _removedProfile, ...withoutProfile } = live;
|
|
1751
|
+
await store.put(live.id, { ...withoutProfile, admissionState: "revoked", initiativePolicy: "none", updatedAt: new Date().toISOString() });
|
|
1752
|
+
(0, tools_awaiting_1.cancelRelationshipFollowUps)(agentRoot, agentName, { friendId: live.id, channel: "telegram", key: sessionKey });
|
|
1753
|
+
});
|
|
1754
|
+
return { revoked: true, friendId };
|
|
1755
|
+
},
|
|
1756
|
+
unblock: async ({ actorFriendId, admissionId }) => {
|
|
1757
|
+
await assertLiveOwner(actorFriendId);
|
|
1758
|
+
const admissions = new telegram_admission_1.FileTelegramAdmissionStore(admissionsRoot);
|
|
1759
|
+
try {
|
|
1760
|
+
const record = admissions.read(admissionId);
|
|
1761
|
+
if (record.botId !== botId || record.userId !== record.chatId)
|
|
1762
|
+
throw new Error("Blocked Telegram contact identity changed");
|
|
1763
|
+
admissions.releaseBlock(admissionId);
|
|
1764
|
+
}
|
|
1765
|
+
finally {
|
|
1766
|
+
admissions.close();
|
|
1767
|
+
}
|
|
1768
|
+
return { unblocked: true, admissionId };
|
|
1769
|
+
},
|
|
1770
|
+
};
|
|
1771
|
+
const resolveEvaluator = async (input) => {
|
|
1772
|
+
const friend = await store.get(input.friendId);
|
|
1773
|
+
const ownerBound = friend?.id === owner.id;
|
|
1774
|
+
const coordinatesValid = ownerBound
|
|
1775
|
+
? input.botId === botId && input.userId === ownerUserId && input.chatId === ownerChatId && input.sessionKey === ownerSessionKey
|
|
1776
|
+
: input.botId === botId && input.userId === input.chatId && input.sessionKey === `telegram:${botId}:${input.userId}`;
|
|
1777
|
+
if (!friend || !coordinatesValid || (ownerBound ? !canonicalOwner(friend) : !canonicalHousehold(friend, input.userId))) {
|
|
1778
|
+
throw new Error("Telegram relationship identity binding is not active");
|
|
1779
|
+
}
|
|
1780
|
+
return (0, relationship_authorization_1.createRelationshipAuthorizationEvaluator)({
|
|
1781
|
+
friend,
|
|
1782
|
+
registry: (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot),
|
|
1783
|
+
requestId: input.requestId,
|
|
1784
|
+
requestPhase: "inbound",
|
|
1785
|
+
sessionEventId: input.sessionEventId,
|
|
1786
|
+
});
|
|
1787
|
+
};
|
|
1788
|
+
const admission = {
|
|
1789
|
+
ownerFriendId: owner.id,
|
|
1790
|
+
resolveOwner: async ({ botId: candidateBotId, userId, chatId, sessionKey }) => {
|
|
1791
|
+
if (candidateBotId !== botId || userId !== ownerUserId || chatId !== ownerChatId || sessionKey !== ownerSessionKey)
|
|
1792
|
+
return null;
|
|
1793
|
+
const exact = await store.findByExternalId("telegram-user", userId, botId);
|
|
1794
|
+
const current = await store.get(owner.id);
|
|
1795
|
+
return exact?.id === owner.id && canonicalOwner(current)
|
|
1796
|
+
? { friendId: owner.id }
|
|
1797
|
+
: null;
|
|
1798
|
+
},
|
|
1799
|
+
resolveApprovedFriend: async ({ botId: candidateBotId, userId, chatId }) => {
|
|
1800
|
+
if (candidateBotId !== botId || userId !== chatId)
|
|
1801
|
+
return null;
|
|
1802
|
+
const friend = await store.findByExternalId("telegram-user", userId, botId);
|
|
1803
|
+
return friend && canonicalHousehold(friend, userId) ? { friendId: friend.id } : null;
|
|
1804
|
+
},
|
|
1805
|
+
claimFriend: async (input) => {
|
|
1806
|
+
if (input.botId !== botId || input.userId !== input.chatId)
|
|
1807
|
+
return { kind: "collision", reason: "Telegram identity is not bound to this bot and private chat" };
|
|
1808
|
+
const claimed = await store.claimExternalId({
|
|
1809
|
+
externalId: { provider: "telegram-user", externalId: input.userId, tenantId: botId, linkedAt: new Date().toISOString() },
|
|
1810
|
+
target: { kind: "create", record: { id: (0, node_crypto_1.randomUUID)(), name: "Household member" } },
|
|
1811
|
+
});
|
|
1812
|
+
const current = claimed.record;
|
|
1813
|
+
const updated = { ...current, trustLevel: input.defaults.trustLevel, admissionState: input.defaults.admissionState,
|
|
1814
|
+
initiativePolicy: input.defaults.initiativePolicy, capabilityProfileId: input.defaults.capabilityProfileId, updatedAt: new Date().toISOString() };
|
|
1815
|
+
await store.put(current.id, updated);
|
|
1816
|
+
return { kind: claimed.status === "created" ? "created" : "existing", friendId: current.id };
|
|
1817
|
+
},
|
|
1818
|
+
revokeFriend: async (input) => {
|
|
1819
|
+
if (input.botId !== botId || input.userId !== input.chatId)
|
|
1820
|
+
return { kind: "collision", reason: "Telegram revocation identity is invalid" };
|
|
1821
|
+
const current = await store.findByExternalId("telegram-user", input.userId, botId);
|
|
1822
|
+
if (!current || current.id !== input.friendId || !exactTelegramIdentity(current, input.userId))
|
|
1823
|
+
return { kind: "collision", reason: "Telegram revocation identity changed" };
|
|
1824
|
+
const { capabilityProfileId: _removedProfile, ...withoutProfile } = current;
|
|
1825
|
+
await store.put(current.id, { ...withoutProfile, admissionState: "revoked", initiativePolicy: "none", updatedAt: new Date().toISOString() });
|
|
1826
|
+
(0, tools_awaiting_1.cancelRelationshipFollowUps)(agentRoot, agentName, { friendId: current.id, channel: "telegram", key: `telegram:${botId}:${input.userId}` });
|
|
1827
|
+
return { kind: "revoked" };
|
|
1828
|
+
},
|
|
1829
|
+
};
|
|
1830
|
+
return {
|
|
1831
|
+
telegramContactManager,
|
|
1832
|
+
admission,
|
|
1833
|
+
resolveRelationshipAuthorization: resolveEvaluator,
|
|
1834
|
+
authorizeRelationshipEffect: async (input) => {
|
|
1835
|
+
if (input.target.kind !== "approved_relationship")
|
|
1836
|
+
return { allowed: false, reason: "relationship effect target is required" };
|
|
1837
|
+
const friend = await store.get(input.target.friendId);
|
|
1838
|
+
if (!friend)
|
|
1839
|
+
return { allowed: false, reason: "relationship admission is not active" };
|
|
1840
|
+
const bindings = sameBotTelegramBindings(friend);
|
|
1841
|
+
if (bindings.length !== 1 || !/^[1-9][0-9]*$/u.test(bindings[0].externalId))
|
|
1842
|
+
return { allowed: false, reason: "relationship Telegram identity binding is unavailable" };
|
|
1843
|
+
const chatId = bindings[0].externalId;
|
|
1844
|
+
if (friend.id === owner.id ? !canonicalOwner(friend) : !canonicalHousehold(friend, chatId))
|
|
1845
|
+
return { allowed: false, reason: "relationship admission is not active or canonical" };
|
|
1846
|
+
if (friend.id === owner.id) {
|
|
1847
|
+
if (chatId !== ownerUserId || chatId !== ownerChatId || input.target.sessionKey !== ownerSessionKey)
|
|
1848
|
+
return { allowed: false, reason: "owner Telegram identity or session binding changed" };
|
|
1849
|
+
}
|
|
1850
|
+
else if (input.target.sessionKey !== `telegram:${botId}:${chatId}`)
|
|
1851
|
+
return { allowed: false, reason: "relationship Telegram session binding changed" };
|
|
1852
|
+
const inboundIdempotency = input.target.requestId
|
|
1853
|
+
? new RegExp(`^relationship-turn:${input.target.requestId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}:delivery:[0-9]+$`, "u").test(input.idempotencyKey)
|
|
1854
|
+
: false;
|
|
1855
|
+
const durableFollowUp = friend.id !== owner.id && input.target.requestId
|
|
1856
|
+
? (0, tools_awaiting_1.hasActiveRelationshipFollowUp)(agentRoot, { friendId: friend.id, channel: "telegram", key: input.target.sessionKey, requestId: input.target.requestId, allowElapsed: input.idempotencyKey.startsWith("await-expiry-follow-up:") })
|
|
1857
|
+
: false;
|
|
1858
|
+
if (friend.id !== owner.id && input.target.requestId && !inboundIdempotency && !durableFollowUp)
|
|
1859
|
+
return { allowed: false, reason: "relationship follow-up is not bound to an active request await" };
|
|
1860
|
+
const evaluator = (0, relationship_authorization_1.createRelationshipAuthorizationEvaluator)({
|
|
1861
|
+
friend,
|
|
1862
|
+
registry: (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot),
|
|
1863
|
+
...(input.target.requestId ? { requestId: input.target.requestId, requestPhase: inboundIdempotency ? "inbound" : "follow_up" } : {}),
|
|
1864
|
+
});
|
|
1865
|
+
const scope = input.target.friendId === owner.id
|
|
1866
|
+
? (input.target.requestId ? "telegram.owner_event" : "telegram.proactive")
|
|
1867
|
+
: "telegram.request_return";
|
|
1868
|
+
const authorization = evaluator.authorizeEffect(scope);
|
|
1869
|
+
return authorization.allowed
|
|
1870
|
+
? { allowed: true, receiptId: authorization.receiptId, expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), transport: { chatId } }
|
|
1871
|
+
: authorization;
|
|
1872
|
+
},
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1223
1875
|
async function startTelegramSenseApp(agentName) {
|
|
1224
|
-
const
|
|
1876
|
+
const loaded = loadTelegramSenseCredentials(agentName);
|
|
1877
|
+
const credentials = { ...loaded, botId: telegramBotIdFromToken(loaded.botToken) };
|
|
1878
|
+
const app = createTelegramSenseApp({
|
|
1879
|
+
agentName,
|
|
1880
|
+
credentials,
|
|
1881
|
+
...(await createProductionTelegramRelationshipComposition(agentName, credentials)),
|
|
1882
|
+
...(agentName === "sanctuary" ? { privilegedFailsafe: {
|
|
1883
|
+
eventRoot: (0, router_1.getExternalEventRoot)(),
|
|
1884
|
+
verifyProtectiveState: createSabQueueProtectiveStateVerifier(),
|
|
1885
|
+
} } : {}),
|
|
1886
|
+
});
|
|
1225
1887
|
(0, runtime_1.emitNervesEvent)({
|
|
1226
1888
|
component: "senses",
|
|
1227
1889
|
event: "senses.telegram_app_ready",
|
|
@@ -1230,3 +1892,21 @@ async function startTelegramSenseApp(agentName) {
|
|
|
1230
1892
|
});
|
|
1231
1893
|
return app;
|
|
1232
1894
|
}
|
|
1895
|
+
async function sendTelegramExternalEventDecision(agentName, input) {
|
|
1896
|
+
const app = await startTelegramSenseApp(agentName);
|
|
1897
|
+
try {
|
|
1898
|
+
await app.sendExternalEventDecision(input);
|
|
1899
|
+
}
|
|
1900
|
+
finally {
|
|
1901
|
+
await app.stop();
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
async function sendTelegramAwaitFollowUp(agentName, request) {
|
|
1905
|
+
const app = await startTelegramSenseApp(agentName);
|
|
1906
|
+
try {
|
|
1907
|
+
return await app.sendAwaitFollowUp(request);
|
|
1908
|
+
}
|
|
1909
|
+
finally {
|
|
1910
|
+
await app.stop();
|
|
1911
|
+
}
|
|
1912
|
+
}
|