@songsid/agend 2.1.4-beta.60 → 2.1.4-beta.61
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/dist/fleet-manager.d.ts +40 -0
- package/dist/fleet-manager.js +104 -37
- package/dist/fleet-manager.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -574,6 +574,46 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
574
574
|
* message actually went out — a failed delivery keeps them queued.
|
|
575
575
|
*/
|
|
576
576
|
private pendingReactionsMeta;
|
|
577
|
+
/**
|
|
578
|
+
* Which adapter's access policy governs an inbound message.
|
|
579
|
+
*
|
|
580
|
+
* `authoritative` means the thread resolved to an owning instance, so that
|
|
581
|
+
* adapter's policy is the only one entitled to decide. A resolved owner whose
|
|
582
|
+
* policy cannot be read is NOT the same as having no owner: the owner's world
|
|
583
|
+
* may not exist yet (an adapter that fails its token check returns before its
|
|
584
|
+
* world and AccessManager are created) while sibling bots are already live and
|
|
585
|
+
* receiving copies. Falling back to a sibling's policy — or to the fleet-wide
|
|
586
|
+
* one — would let the owner's rules be decided by another adapter, so callers
|
|
587
|
+
* must refuse instead.
|
|
588
|
+
*
|
|
589
|
+
* Without an owner (classic channels, the no-thread Telegram path, unrouted
|
|
590
|
+
* threads, no adapter identity) the receiving adapter's policy applies, as
|
|
591
|
+
* before.
|
|
592
|
+
*/
|
|
593
|
+
private governingAccess;
|
|
594
|
+
/**
|
|
595
|
+
* Why this adapter should leave an inbound message to another adapter, or null
|
|
596
|
+
* to handle it.
|
|
597
|
+
*
|
|
598
|
+
* A fleet topic is served by exactly one instance, and that instance is
|
|
599
|
+
* answered by exactly one adapter. When several bots share a guild they each
|
|
600
|
+
* receive their own copy of every message, so the copy that is acted on must
|
|
601
|
+
* be chosen by ownership rather than by which inbound happened to fire first.
|
|
602
|
+
* Ownership is per-instance — the adapter the instance is bound to, falling
|
|
603
|
+
* back to channels[0] when it is not bound — so an instance answered by a
|
|
604
|
+
* non-default bot still receives its own traffic, and the access policy that
|
|
605
|
+
* applies is that instance's own adapter's.
|
|
606
|
+
*
|
|
607
|
+
* Callers must ask before claiming the shared dedup key: a copy that is going
|
|
608
|
+
* to be left alone must not consume the key, or the owner's copy would be
|
|
609
|
+
* discarded as a duplicate and the message lost.
|
|
610
|
+
*
|
|
611
|
+
* Exempt: classic channels (each bot owns its own agent there and handles its
|
|
612
|
+
* own copy, which is why their dedup key is adapter-scoped), the no-thread
|
|
613
|
+
* Telegram path (no thread to resolve an instance from), and messages with no
|
|
614
|
+
* adapter id (single-adapter fleets).
|
|
615
|
+
*/
|
|
616
|
+
private topicOwnerDropReason;
|
|
577
617
|
/**
|
|
578
618
|
* Why this adapter must ignore a bot/webhook message, or null to accept it.
|
|
579
619
|
* Pure: callers rely on being able to ask before claiming the dedup key.
|
package/dist/fleet-manager.js
CHANGED
|
@@ -3780,6 +3780,69 @@ export class FleetManager {
|
|
|
3780
3780
|
consume: () => this.eventLog?.markReactionsConsumed(instanceName, pending.maxId),
|
|
3781
3781
|
};
|
|
3782
3782
|
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Which adapter's access policy governs an inbound message.
|
|
3785
|
+
*
|
|
3786
|
+
* `authoritative` means the thread resolved to an owning instance, so that
|
|
3787
|
+
* adapter's policy is the only one entitled to decide. A resolved owner whose
|
|
3788
|
+
* policy cannot be read is NOT the same as having no owner: the owner's world
|
|
3789
|
+
* may not exist yet (an adapter that fails its token check returns before its
|
|
3790
|
+
* world and AccessManager are created) while sibling bots are already live and
|
|
3791
|
+
* receiving copies. Falling back to a sibling's policy — or to the fleet-wide
|
|
3792
|
+
* one — would let the owner's rules be decided by another adapter, so callers
|
|
3793
|
+
* must refuse instead.
|
|
3794
|
+
*
|
|
3795
|
+
* Without an owner (classic channels, the no-thread Telegram path, unrouted
|
|
3796
|
+
* threads, no adapter identity) the receiving adapter's policy applies, as
|
|
3797
|
+
* before.
|
|
3798
|
+
*/
|
|
3799
|
+
governingAccess(msg, threadId) {
|
|
3800
|
+
if (threadId && !this.classicChannels?.hasChannel(threadId)) {
|
|
3801
|
+
const target = this.routing.resolve(threadId);
|
|
3802
|
+
if (target) {
|
|
3803
|
+
const owner = this.getInstanceAdapterId(target.name);
|
|
3804
|
+
if (owner)
|
|
3805
|
+
return { adapterId: owner, authoritative: true };
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
return { adapterId: msg.adapterId, authoritative: false };
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Why this adapter should leave an inbound message to another adapter, or null
|
|
3812
|
+
* to handle it.
|
|
3813
|
+
*
|
|
3814
|
+
* A fleet topic is served by exactly one instance, and that instance is
|
|
3815
|
+
* answered by exactly one adapter. When several bots share a guild they each
|
|
3816
|
+
* receive their own copy of every message, so the copy that is acted on must
|
|
3817
|
+
* be chosen by ownership rather than by which inbound happened to fire first.
|
|
3818
|
+
* Ownership is per-instance — the adapter the instance is bound to, falling
|
|
3819
|
+
* back to channels[0] when it is not bound — so an instance answered by a
|
|
3820
|
+
* non-default bot still receives its own traffic, and the access policy that
|
|
3821
|
+
* applies is that instance's own adapter's.
|
|
3822
|
+
*
|
|
3823
|
+
* Callers must ask before claiming the shared dedup key: a copy that is going
|
|
3824
|
+
* to be left alone must not consume the key, or the owner's copy would be
|
|
3825
|
+
* discarded as a duplicate and the message lost.
|
|
3826
|
+
*
|
|
3827
|
+
* Exempt: classic channels (each bot owns its own agent there and handles its
|
|
3828
|
+
* own copy, which is why their dedup key is adapter-scoped), the no-thread
|
|
3829
|
+
* Telegram path (no thread to resolve an instance from), and messages with no
|
|
3830
|
+
* adapter id (single-adapter fleets).
|
|
3831
|
+
*/
|
|
3832
|
+
topicOwnerDropReason(msg, threadId) {
|
|
3833
|
+
if (!threadId || !msg.adapterId)
|
|
3834
|
+
return null;
|
|
3835
|
+
if (this.classicChannels?.hasChannel(threadId))
|
|
3836
|
+
return null;
|
|
3837
|
+
const target = this.routing.resolve(threadId);
|
|
3838
|
+
if (!target)
|
|
3839
|
+
return null;
|
|
3840
|
+
const ownerAdapterId = this.getInstanceAdapterId(target.name);
|
|
3841
|
+
if (ownerAdapterId && msg.adapterId !== ownerAdapterId) {
|
|
3842
|
+
return `not the adapter bound to ${target.name} (that is ${ownerAdapterId})`;
|
|
3843
|
+
}
|
|
3844
|
+
return null;
|
|
3845
|
+
}
|
|
3783
3846
|
/**
|
|
3784
3847
|
* Why this adapter must ignore a bot/webhook message, or null to accept it.
|
|
3785
3848
|
* Pure: callers rely on being able to ask before claiming the dedup key.
|
|
@@ -3806,18 +3869,6 @@ export class FleetManager {
|
|
|
3806
3869
|
const target = this.routing.resolve(threadId);
|
|
3807
3870
|
if (!target)
|
|
3808
3871
|
return "fleet topic: no instance routed for this thread";
|
|
3809
|
-
// A fleet topic is served by exactly one instance, so exactly one bot should
|
|
3810
|
-
// answer a webhook there. Several bots sharing the guild each receive their
|
|
3811
|
-
// own copy; pick that instance's own adapter deterministically rather than
|
|
3812
|
-
// letting whichever inbound fired first win. The primary is per-instance —
|
|
3813
|
-
// the adapter the instance is bound to, falling back to channels[0] when it
|
|
3814
|
-
// is not bound — so an instance answered by a non-default bot still receives
|
|
3815
|
-
// its webhooks. (Classic channels are exempt above: there each bot owns its
|
|
3816
|
-
// own agent and must handle its own copy.)
|
|
3817
|
-
const ownerAdapterId = this.getInstanceAdapterId(target.name);
|
|
3818
|
-
if (msg.adapterId && ownerAdapterId && msg.adapterId !== ownerAdapterId) {
|
|
3819
|
-
return `fleet topic: not the adapter bound to ${target.name} (that is ${ownerAdapterId})`;
|
|
3820
|
-
}
|
|
3821
3872
|
// Fleet topic: allow if collab enabled OR access mode is open
|
|
3822
3873
|
const isOpen = this.getChannelConfig(msg.adapterId)?.access?.mode === "open";
|
|
3823
3874
|
if (!isOpen && !this.collabInstances.has(target.name)) {
|
|
@@ -3828,18 +3879,47 @@ export class FleetManager {
|
|
|
3828
3879
|
async handleInboundMessage(msg) {
|
|
3829
3880
|
const threadId = msg.threadId || undefined;
|
|
3830
3881
|
this.logger.debug({ source: msg.source, chatId: msg.chatId, threadId, userId: msg.userId, isBotMessage: msg.isBotMessage, textLen: (msg.text ?? "").length, text: (msg.text ?? "").slice(0, 80) }, "handleInboundMessage entry");
|
|
3831
|
-
//
|
|
3832
|
-
//
|
|
3833
|
-
//
|
|
3834
|
-
//
|
|
3835
|
-
//
|
|
3836
|
-
//
|
|
3837
|
-
//
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3882
|
+
// Ownership and per-adapter filtering both run before the dedup claim below.
|
|
3883
|
+
// When several bots share a guild they all receive the same message; a copy
|
|
3884
|
+
// this adapter is going to leave alone must not consume the shared dedup key,
|
|
3885
|
+
// or the owning adapter's copy would be discarded as a duplicate and the
|
|
3886
|
+
// message lost entirely. Claiming the key only on surviving copies also keeps
|
|
3887
|
+
// delivery single when more than one adapter would otherwise accept it.
|
|
3888
|
+
// Only bot messages are settled by ownership: a bot answering in a fleet
|
|
3889
|
+
// topic would speak as the wrong identity, whereas a human message is just
|
|
3890
|
+
// input for the instance and its reply is canonicalized to the owning
|
|
3891
|
+
// adapter downstream — so a topic only a non-owner bot can see still works.
|
|
3892
|
+
const drop = msg.isBotMessage
|
|
3893
|
+
? (this.topicOwnerDropReason(msg, threadId) ?? this.botMessageDropReason(msg, threadId))
|
|
3894
|
+
: null;
|
|
3895
|
+
if (drop) {
|
|
3896
|
+
this.logger.debug({ adapterId: msg.adapterId, threadId: threadId ?? null, messageId: msg.messageId, reason: drop }, "Inbound message dropped before the dedup claim — key not consumed");
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
// Access control — classic channels are open to all, others require an allowed
|
|
3900
|
+
// user. This runs before the dedup claim, and is judged by the adapter that
|
|
3901
|
+
// owns the topic rather than the one that happened to receive the message:
|
|
3902
|
+
// otherwise a sibling bot could settle the message under its own policy —
|
|
3903
|
+
// claiming the shared key and then refusing it, or admitting a user the
|
|
3904
|
+
// owning adapter does not allow — purely by arriving first.
|
|
3905
|
+
const governing = this.governingAccess(msg, threadId);
|
|
3906
|
+
const ownerWorld = governing.adapterId ? this.worlds.get(governing.adapterId) : undefined;
|
|
3907
|
+
if (governing.authoritative && !ownerWorld) {
|
|
3908
|
+
this.logger.warn({ adapterId: msg.adapterId, owner: governing.adapterId, threadId, messageId: msg.messageId }, "Refusing inbound: the owning adapter is not running, so its access policy cannot be applied");
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
const am = governing.authoritative
|
|
3912
|
+
? ownerWorld?.accessManager
|
|
3913
|
+
: (ownerWorld?.accessManager ?? this.accessManager);
|
|
3914
|
+
if (am && !am.isAllowed(msg.userId)) {
|
|
3915
|
+
const adapterGroupId = String(this.getChannelConfig(msg.adapterId)?.group_id ?? "");
|
|
3916
|
+
const isTelegramClassicCandidate = msg.source === "telegram" && msg.chatId !== adapterGroupId && !threadId;
|
|
3917
|
+
if (!isTelegramClassicCandidate) {
|
|
3918
|
+
// Classic channels are open to all; check per-bot ownership (or fleet topic).
|
|
3919
|
+
const isClassic = !!(threadId && this.classicChannels?.hasChannel(threadId));
|
|
3920
|
+
this.logger.info({ userId: msg.userId, threadId, isClassic, owner: governing.adapterId }, "Access DENIED for non-allowed user");
|
|
3921
|
+
if (!isClassic)
|
|
3922
|
+
return;
|
|
3843
3923
|
}
|
|
3844
3924
|
}
|
|
3845
3925
|
// Multi-adapter dedup: when several bots share a guild, each adapter fires
|
|
@@ -3870,19 +3950,6 @@ export class FleetManager {
|
|
|
3870
3950
|
this.recentMessageIds.delete(oldest);
|
|
3871
3951
|
}
|
|
3872
3952
|
}
|
|
3873
|
-
// Access control — classic channels are open to all, others require allowed user
|
|
3874
|
-
const am = (msg.adapterId ? this.worlds.get(msg.adapterId)?.accessManager : undefined) ?? this.accessManager;
|
|
3875
|
-
if (am && !am.isAllowed(msg.userId)) {
|
|
3876
|
-
const adapterGroupId = String(this.getChannelConfig(msg.adapterId)?.group_id ?? "");
|
|
3877
|
-
const isTelegramClassicCandidate = msg.source === "telegram" && msg.chatId !== adapterGroupId && !threadId;
|
|
3878
|
-
if (!isTelegramClassicCandidate) {
|
|
3879
|
-
// Classic channels are open to all; check per-bot ownership (or fleet topic).
|
|
3880
|
-
const isClassic = !!(threadId && this.classicChannels?.hasChannel(threadId));
|
|
3881
|
-
this.logger.info({ userId: msg.userId, threadId, isClassic }, "Access DENIED for non-allowed user");
|
|
3882
|
-
if (!isClassic)
|
|
3883
|
-
return;
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
3953
|
if (threadId == null) {
|
|
3887
3954
|
// ── Telegram Classic Mode ──
|
|
3888
3955
|
// Messages from chats other than the primary forum group are classic mode candidates.
|