@pinet/slack-bridge 0.2.1 → 0.2.4
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/README.md +302 -448
- package/dist/broker/adapters/slack.d.ts +11 -0
- package/dist/broker/adapters/slack.js +90 -2
- package/dist/broker/client.d.ts +3 -1
- package/dist/broker/client.js +11 -0
- package/dist/broker/socket-server.d.ts +2 -0
- package/dist/broker/socket-server.js +89 -2
- package/dist/helpers.d.ts +17 -0
- package/dist/helpers.js +12 -1
- package/dist/index.js +3 -1
- package/dist/pinet-mesh-ops.d.ts +6 -1
- package/dist/pinet-mesh-ops.js +20 -0
- package/dist/pinet-session-formatting.d.ts +13 -0
- package/dist/pinet-session-formatting.js +80 -0
- package/dist/pinet-tools.d.ts +4 -1
- package/dist/pinet-tools.js +148 -3
- package/dist/slack-markdown.d.ts +1 -0
- package/dist/slack-markdown.js +128 -0
- package/dist/slack-pinet-runtime-adapter.js +7 -0
- package/dist/slack-tools.d.ts +8 -0
- package/dist/slack-tools.js +26 -49
- package/dist/subtree-broker-runtime.d.ts +3 -0
- package/dist/subtree-broker-runtime.js +3 -0
- package/package.json +5 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ParsedAppHomeOpened, type ParsedSlashCommand, type ParsedThreadStarted } from "../../slack-access.js";
|
|
2
|
+
import { type SlackIngressGuardSettings } from "../../helpers.js";
|
|
2
3
|
import { type ReactionCommandSettings } from "../../reaction-triggers.js";
|
|
3
4
|
import type { AdapterCapabilityRequest, AdapterCapabilityResult, InboundMessage, OutboundMessage, MessageAdapter } from "./types.js";
|
|
4
5
|
export { classifyMessage, extractAppHomeOpened, extractThreadStarted, parseMemberJoinedChannel, parseSocketFrame, RECONNECT_DELAY_MS, } from "../../slack-access.js";
|
|
@@ -7,6 +8,7 @@ export interface SlackAdapterConfig {
|
|
|
7
8
|
appToken: string;
|
|
8
9
|
allowedUsers?: string[];
|
|
9
10
|
allowAllWorkspaceUsers?: boolean;
|
|
11
|
+
ingressGuard?: SlackIngressGuardSettings;
|
|
10
12
|
suggestedPrompts?: {
|
|
11
13
|
title: string;
|
|
12
14
|
message: string;
|
|
@@ -33,6 +35,8 @@ export interface SlackAdapterConfig {
|
|
|
33
35
|
* Slack messages (#812).
|
|
34
36
|
*/
|
|
35
37
|
isReactionThreadAuthorized?: (threadTs: string, channelId: string) => boolean;
|
|
38
|
+
/** Check whether a known Slack thread is Pinet-owned for mixed-participant mention gating. */
|
|
39
|
+
isPinetOwnedThread?: (threadTs: string, channelId: string) => boolean;
|
|
36
40
|
/** Best-effort callback for Home tab opens. */
|
|
37
41
|
onAppHomeOpened?: (event: ParsedAppHomeOpened) => Promise<void> | void;
|
|
38
42
|
/** Best-effort callback for Slack slash commands handled by the broker process. */
|
|
@@ -43,6 +47,7 @@ export declare const SLACK_THREAD_CACHE_TTL_MS: number;
|
|
|
43
47
|
export declare const SLACK_PENDING_ATTENTION_MAX_THREADS = 1000;
|
|
44
48
|
export declare const SLACK_PENDING_ATTENTION_TTL_MS: number;
|
|
45
49
|
export declare const SLACK_PENDING_ATTENTION_MAX_MESSAGES_PER_THREAD = 50;
|
|
50
|
+
export declare const SLACK_INGRESS_GUARD_THREAD_PARTICIPANT_LIMIT = 200;
|
|
46
51
|
export declare class SlackAdapter implements MessageAdapter {
|
|
47
52
|
readonly name = "slack";
|
|
48
53
|
private readonly config;
|
|
@@ -79,6 +84,12 @@ export declare class SlackAdapter implements MessageAdapter {
|
|
|
79
84
|
private onReactionAdded;
|
|
80
85
|
private getCachedThread;
|
|
81
86
|
private isReactionThreadAuthorized;
|
|
87
|
+
private messageMentionsBot;
|
|
88
|
+
private requiresMentionInChannel;
|
|
89
|
+
private shouldRequireMentionForMixedParticipantThread;
|
|
90
|
+
private isPinetOwnedThreadForIngressGuard;
|
|
91
|
+
private fetchThreadParticipantUserIds;
|
|
92
|
+
private shouldRequireExplicitMention;
|
|
82
93
|
private onMessage;
|
|
83
94
|
private emitInteractiveInbound;
|
|
84
95
|
private onMemberJoined;
|
|
@@ -5,6 +5,7 @@ import { buildReactionTriggerMessage, normalizeReactionName, resolveReactionComm
|
|
|
5
5
|
import { TtlCache, TtlSet } from "../../ttl-cache.js";
|
|
6
6
|
import { SLACK_SOCKET_DELIVERY_DEDUP_MAX_SIZE, SLACK_SOCKET_DELIVERY_DEDUP_TTL_MS, } from "../../slack-access.js";
|
|
7
7
|
import { DEFAULT_SLACK_THREAD_STATUS, SlackThreadStatusManager, } from "../../slack-thread-status.js";
|
|
8
|
+
import { renderMarkdownForSlackMrkdwn } from "../../slack-markdown.js";
|
|
8
9
|
import { performSlackUploads, prepareSlackUpload } from "../../slack-upload.js";
|
|
9
10
|
export { classifyMessage, extractAppHomeOpened, extractThreadStarted, parseMemberJoinedChannel, parseSocketFrame, RECONNECT_DELAY_MS, } from "../../slack-access.js";
|
|
10
11
|
export const SLACK_THREAD_CACHE_MAX_SIZE = 5000;
|
|
@@ -12,6 +13,7 @@ export const SLACK_THREAD_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
12
13
|
export const SLACK_PENDING_ATTENTION_MAX_THREADS = 1000;
|
|
13
14
|
export const SLACK_PENDING_ATTENTION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
14
15
|
export const SLACK_PENDING_ATTENTION_MAX_MESSAGES_PER_THREAD = 50;
|
|
16
|
+
export const SLACK_INGRESS_GUARD_THREAD_PARTICIPANT_LIMIT = 200;
|
|
15
17
|
export class SlackAdapter {
|
|
16
18
|
name = "slack";
|
|
17
19
|
config;
|
|
@@ -124,9 +126,10 @@ export class SlackAdapter {
|
|
|
124
126
|
: msg.blocks && msg.blocks.length > 0
|
|
125
127
|
? msg.blocks
|
|
126
128
|
: undefined;
|
|
129
|
+
const renderedText = renderMarkdownForSlackMrkdwn(msg.content?.markdown ?? msg.content?.text ?? msg.text);
|
|
127
130
|
const body = {
|
|
128
131
|
channel: msg.channel,
|
|
129
|
-
text:
|
|
132
|
+
text: renderedText,
|
|
130
133
|
thread_ts: msg.threadId,
|
|
131
134
|
...(slackBlocks ? { blocks: slackBlocks } : {}),
|
|
132
135
|
};
|
|
@@ -157,7 +160,7 @@ export class SlackAdapter {
|
|
|
157
160
|
uploads,
|
|
158
161
|
channelId: msg.channel,
|
|
159
162
|
threadTs: msg.threadId,
|
|
160
|
-
initialComment:
|
|
163
|
+
initialComment: renderedText,
|
|
161
164
|
slack: this.callSlack.bind(this),
|
|
162
165
|
token: this.config.botToken,
|
|
163
166
|
});
|
|
@@ -464,6 +467,87 @@ export class SlackAdapter {
|
|
|
464
467
|
return false;
|
|
465
468
|
}
|
|
466
469
|
}
|
|
470
|
+
messageMentionsBot(evt) {
|
|
471
|
+
if (!this.botUserId)
|
|
472
|
+
return false;
|
|
473
|
+
const text = typeof evt.text === "string" ? evt.text : "";
|
|
474
|
+
return text.includes(`<@${this.botUserId}>`) || text.includes(`<@${this.botUserId}|`);
|
|
475
|
+
}
|
|
476
|
+
requiresMentionInChannel(channelId) {
|
|
477
|
+
const channels = this.config.ingressGuard?.requireMention?.channels ?? [];
|
|
478
|
+
return channels.some((channel) => channel.trim() === channelId);
|
|
479
|
+
}
|
|
480
|
+
async shouldRequireMentionForMixedParticipantThread(input) {
|
|
481
|
+
const config = this.config.ingressGuard?.requireMention?.mixedParticipantThreads;
|
|
482
|
+
if (!config?.enabled || input.isDM || typeof input.evt.thread_ts !== "string") {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (!this.isPinetOwnedThreadForIngressGuard(input.threadTs, input.channel)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
const participants = await this.fetchThreadParticipantUserIds(input.channel, input.threadTs, input.userId);
|
|
489
|
+
if (!participants) {
|
|
490
|
+
// Fail closed for this guard: if we cannot inspect the thread makeup,
|
|
491
|
+
// require an explicit mention before Pinet acts in a configured mixed-thread posture.
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
const trustedUsers = new Set((config.trustedUsers ?? []).map((user) => user.trim()).filter(Boolean));
|
|
495
|
+
for (const participant of participants) {
|
|
496
|
+
if (this.botUserId && participant === this.botUserId)
|
|
497
|
+
continue;
|
|
498
|
+
if (trustedUsers.has(participant))
|
|
499
|
+
continue;
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
isPinetOwnedThreadForIngressGuard(threadTs, channelId) {
|
|
505
|
+
const isOwned = this.config.isPinetOwnedThread;
|
|
506
|
+
if (isOwned) {
|
|
507
|
+
try {
|
|
508
|
+
return isOwned(threadTs, channelId);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
console.error(`[slack-adapter] Pinet-owned thread check failed: ${errorMsg(error)}`);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const thread = this.getThread(threadTs);
|
|
516
|
+
return !!thread && thread.channelId === channelId;
|
|
517
|
+
}
|
|
518
|
+
async fetchThreadParticipantUserIds(channelId, threadTs, currentUserId) {
|
|
519
|
+
const participants = new Set();
|
|
520
|
+
if (currentUserId)
|
|
521
|
+
participants.add(currentUserId);
|
|
522
|
+
try {
|
|
523
|
+
const response = await this.callSlack("conversations.replies", this.config.botToken, {
|
|
524
|
+
channel: channelId,
|
|
525
|
+
ts: threadTs,
|
|
526
|
+
limit: SLACK_INGRESS_GUARD_THREAD_PARTICIPANT_LIMIT,
|
|
527
|
+
});
|
|
528
|
+
const messages = Array.isArray(response.messages) ? response.messages : [];
|
|
529
|
+
for (const message of messages) {
|
|
530
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
531
|
+
continue;
|
|
532
|
+
const userId = message.user;
|
|
533
|
+
if (typeof userId === "string" && userId.length > 0) {
|
|
534
|
+
participants.add(userId);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return participants;
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
console.error(`[slack-adapter] failed to inspect Slack thread participants: ${errorMsg(error)}`);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async shouldRequireExplicitMention(input) {
|
|
545
|
+
if (!this.config.ingressGuard?.requireMention)
|
|
546
|
+
return false;
|
|
547
|
+
if (this.requiresMentionInChannel(input.channel))
|
|
548
|
+
return true;
|
|
549
|
+
return this.shouldRequireMentionForMixedParticipantThread(input);
|
|
550
|
+
}
|
|
467
551
|
async onMessage(evt) {
|
|
468
552
|
if (this.shuttingDown)
|
|
469
553
|
return;
|
|
@@ -480,6 +564,10 @@ export class SlackAdapter {
|
|
|
480
564
|
// traffic can never mint known-thread/affinity side effects (#812).
|
|
481
565
|
if (!isSlackUserAllowed(this.allowlist, userId))
|
|
482
566
|
return;
|
|
567
|
+
if (!this.messageMentionsBot(evt) &&
|
|
568
|
+
(await this.shouldRequireExplicitMention({ evt, channel, threadTs, userId, isDM }))) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
483
571
|
if (!this.getThread(threadTs)) {
|
|
484
572
|
this.threads.set(threadTs, {
|
|
485
573
|
channelId: channel,
|
package/dist/broker/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PinetReadOptions, PinetReadResult } from "@pinet/pinet-core/pinet-read-formatting";
|
|
2
|
-
import type { ClientAgentInfo, NormalizedMessageContent, OutboundAttachmentFile, PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./types.js";
|
|
2
|
+
import type { AgentSessionSearchInfo, AgentSessionSearchOptions, ClientAgentInfo, NormalizedMessageContent, OutboundAttachmentFile, PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./types.js";
|
|
3
3
|
export interface InboxItem {
|
|
4
4
|
inboxId: number;
|
|
5
5
|
message: {
|
|
@@ -25,6 +25,7 @@ export interface ThreadInfo {
|
|
|
25
25
|
updatedAt: string;
|
|
26
26
|
}
|
|
27
27
|
export type AgentInfo = ClientAgentInfo;
|
|
28
|
+
export type { AgentSessionSearchInfo, AgentSessionSearchOptions };
|
|
28
29
|
export interface ScheduledWakeupInfo {
|
|
29
30
|
id: number;
|
|
30
31
|
threadId: string;
|
|
@@ -136,6 +137,7 @@ export declare class BrokerClient {
|
|
|
136
137
|
expirePortLeases(): Promise<PortLeaseInfo[]>;
|
|
137
138
|
listThreads(): Promise<ThreadInfo[]>;
|
|
138
139
|
listAgents(includeDisconnected?: boolean): Promise<AgentInfo[]>;
|
|
140
|
+
searchAgentSessions(options?: AgentSessionSearchOptions): Promise<AgentSessionSearchInfo[]>;
|
|
139
141
|
invokeAdapterCapability(adapter: string, capability: string, params?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
140
142
|
/**
|
|
141
143
|
* Compatibility wrapper for callers that still need direct Slack API access.
|
package/dist/broker/client.js
CHANGED
|
@@ -366,6 +366,17 @@ export class BrokerClient {
|
|
|
366
366
|
const result = (await this.request("agents.list", includeDisconnected ? { includeDisconnected: true } : undefined));
|
|
367
367
|
return result;
|
|
368
368
|
}
|
|
369
|
+
async searchAgentSessions(options = {}) {
|
|
370
|
+
try {
|
|
371
|
+
return (await this.request("agent.sessions.search", options));
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
if (isRpcMethodNotFoundError(err, "agent.sessions.search")) {
|
|
375
|
+
throw new Error("Broker does not support Pinet session search (`agent.sessions.search`). Upgrade the broker before using pinet action=sessions.");
|
|
376
|
+
}
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
369
380
|
// ─── Adapter capabilities ───────────────────────────
|
|
370
381
|
async invokeAdapterCapability(adapter, capability, params = {}) {
|
|
371
382
|
const result = (await this.request("adapter.capability", {
|
|
@@ -98,6 +98,7 @@ export declare class BrokerSocketServer {
|
|
|
98
98
|
private handleMessageSend;
|
|
99
99
|
private handleThreadsList;
|
|
100
100
|
private handleAgentsList;
|
|
101
|
+
private handleAgentSessionsSearch;
|
|
101
102
|
private handleThreadClaim;
|
|
102
103
|
private handleResolveThread;
|
|
103
104
|
private handleAgentMessage;
|
|
@@ -116,5 +117,6 @@ export declare class BrokerSocketServer {
|
|
|
116
117
|
private handleAdapterCapability;
|
|
117
118
|
private handleLegacySlackProxy;
|
|
118
119
|
private dispatchAdapterCapability;
|
|
120
|
+
private checkAdapterCapabilityThreadOwnership;
|
|
119
121
|
private applyAdapterCapabilityEffects;
|
|
120
122
|
}
|
|
@@ -7,14 +7,17 @@ import { MessageRouter } from "./router.js";
|
|
|
7
7
|
import { dispatchDirectAgentMessage } from "./agent-messaging.js";
|
|
8
8
|
import { sendBrokerMessage } from "./message-send.js";
|
|
9
9
|
import { assertLoopbackTcpHost } from "./raw-tcp-loopback.js";
|
|
10
|
+
import { summarizePinetStableId } from "../pinet-session-formatting.js";
|
|
10
11
|
import { RPC_PARSE_ERROR, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_INVALID_PARAMS, RPC_INTERNAL_ERROR, RPC_AUTH_REQUIRED, RPC_AGENT_NAME_CONFLICT, RPC_AGENT_STABLE_ID_CONFLICT, } from "./types.js";
|
|
11
12
|
export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 15_000;
|
|
12
13
|
export const DEFAULT_PRUNE_INTERVAL_MS = 5_000;
|
|
13
14
|
export const DEFAULT_AUTH_TIMEOUT_MS = 2_000;
|
|
14
15
|
function toClientAgentInfo(agent) {
|
|
15
16
|
const { stableId, ...clientAgent } = agent;
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
return {
|
|
18
|
+
...clientAgent,
|
|
19
|
+
session: summarizePinetStableId(stableId),
|
|
20
|
+
};
|
|
18
21
|
}
|
|
19
22
|
// ─── RPC helpers ─────────────────────────────────────────
|
|
20
23
|
function rpcOk(id, result) {
|
|
@@ -351,6 +354,8 @@ export class BrokerSocketServer {
|
|
|
351
354
|
return this.handleThreadsList(req, state);
|
|
352
355
|
case "agents.list":
|
|
353
356
|
return this.handleAgentsList(req);
|
|
357
|
+
case "agent.sessions.search":
|
|
358
|
+
return this.handleAgentSessionsSearch(req, state);
|
|
354
359
|
case "thread.claim":
|
|
355
360
|
return this.handleThreadClaim(req, state);
|
|
356
361
|
case "resolveThread":
|
|
@@ -686,6 +691,35 @@ export class BrokerSocketServer {
|
|
|
686
691
|
}));
|
|
687
692
|
return rpcOk(req.id, agents);
|
|
688
693
|
}
|
|
694
|
+
handleAgentSessionsSearch(req, state) {
|
|
695
|
+
if (!state.agentId) {
|
|
696
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
697
|
+
}
|
|
698
|
+
const caller = this.db.getAgentById(state.agentId);
|
|
699
|
+
const metadataRole = typeof caller?.metadata?.role === "string" ? caller.metadata.role.trim().toLowerCase() : "";
|
|
700
|
+
const capabilities = caller?.metadata?.capabilities &&
|
|
701
|
+
typeof caller.metadata.capabilities === "object" &&
|
|
702
|
+
!Array.isArray(caller.metadata.capabilities)
|
|
703
|
+
? caller.metadata.capabilities
|
|
704
|
+
: null;
|
|
705
|
+
const capabilityRole = typeof capabilities?.role === "string" ? capabilities.role.trim().toLowerCase() : "";
|
|
706
|
+
if (metadataRole !== "broker" && capabilityRole !== "broker") {
|
|
707
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "agent.sessions.search requires a broker agent");
|
|
708
|
+
}
|
|
709
|
+
const params = req.params ?? {};
|
|
710
|
+
const options = {
|
|
711
|
+
...(typeof params.agentName === "string" ? { agentName: params.agentName } : {}),
|
|
712
|
+
...(typeof params.agentId === "string" ? { agentId: params.agentId } : {}),
|
|
713
|
+
...(typeof params.threadId === "string" ? { threadId: params.threadId } : {}),
|
|
714
|
+
...(typeof params.repo === "string" ? { repo: params.repo } : {}),
|
|
715
|
+
...(typeof params.worktreePath === "string" ? { worktreePath: params.worktreePath } : {}),
|
|
716
|
+
...(typeof params.tmuxSession === "string" ? { tmuxSession: params.tmuxSession } : {}),
|
|
717
|
+
...(typeof params.since === "string" ? { since: params.since } : {}),
|
|
718
|
+
...(typeof params.until === "string" ? { until: params.until } : {}),
|
|
719
|
+
...(typeof params.limit === "number" ? { limit: params.limit } : {}),
|
|
720
|
+
};
|
|
721
|
+
return rpcOk(req.id, this.db.searchAgentSessions(options));
|
|
722
|
+
}
|
|
689
723
|
// ─── Thread claim handler ─────────────────────────────
|
|
690
724
|
handleThreadClaim(req, state) {
|
|
691
725
|
if (!state.agentId) {
|
|
@@ -1044,6 +1078,14 @@ export class BrokerSocketServer {
|
|
|
1044
1078
|
}
|
|
1045
1079
|
// ─── Adapter capability handler ───────────────────────
|
|
1046
1080
|
async handleAdapterCapability(req, state) {
|
|
1081
|
+
// #855: adapter.capability must be identity-bound so the broker can
|
|
1082
|
+
// enforce thread ownership. Unregistered callers cannot own or claim
|
|
1083
|
+
// threads, so they must not be able to invoke outbound-side capabilities
|
|
1084
|
+
// (chat.postMessage in particular) that would race a first-responder
|
|
1085
|
+
// claim or take over a thread already owned by another agent.
|
|
1086
|
+
if (!state.agentId) {
|
|
1087
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
1088
|
+
}
|
|
1047
1089
|
const params = req.params ?? {};
|
|
1048
1090
|
const adapterName = typeof params.adapter === "string"
|
|
1049
1091
|
? params.adapter.trim()
|
|
@@ -1063,6 +1105,12 @@ export class BrokerSocketServer {
|
|
|
1063
1105
|
return await this.dispatchAdapterCapability(req.id, adapterName, capability, capabilityParams, state);
|
|
1064
1106
|
}
|
|
1065
1107
|
async handleLegacySlackProxy(req, state) {
|
|
1108
|
+
// #855: legacy slack.proxy is a compatibility wrapper over
|
|
1109
|
+
// adapter.capability and must enforce the same registration bar so
|
|
1110
|
+
// unregistered callers cannot bypass thread ownership via chat.postMessage.
|
|
1111
|
+
if (!state.agentId) {
|
|
1112
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
1113
|
+
}
|
|
1066
1114
|
const params = req.params ?? {};
|
|
1067
1115
|
const method = typeof params.method === "string" ? params.method.trim() : "";
|
|
1068
1116
|
if (!method) {
|
|
@@ -1078,6 +1126,15 @@ export class BrokerSocketServer {
|
|
|
1078
1126
|
if (!adapter?.invokeCapability) {
|
|
1079
1127
|
return rpcError(id, RPC_METHOD_NOT_FOUND, `Adapter ${adapterName} does not implement capability ${capability}`);
|
|
1080
1128
|
}
|
|
1129
|
+
// #855: refuse cross-owner Slack chat.postMessage before hitting Slack.
|
|
1130
|
+
// Without this pre-check the adapter posts first and the broker races a
|
|
1131
|
+
// first-responder-wins claim via effects.claimThread — letting an
|
|
1132
|
+
// unauthorized follower take over a thread already owned by another
|
|
1133
|
+
// agent simply by winning the send.
|
|
1134
|
+
const ownershipError = this.checkAdapterCapabilityThreadOwnership(adapterName, capability, capabilityParams, state.agentId);
|
|
1135
|
+
if (ownershipError) {
|
|
1136
|
+
return rpcError(id, RPC_INVALID_PARAMS, `${errorPrefix}: ${ownershipError}`);
|
|
1137
|
+
}
|
|
1081
1138
|
try {
|
|
1082
1139
|
const response = await adapter.invokeCapability({ capability, params: capabilityParams });
|
|
1083
1140
|
this.applyAdapterCapabilityEffects(adapterName, response, state);
|
|
@@ -1088,6 +1145,36 @@ export class BrokerSocketServer {
|
|
|
1088
1145
|
return rpcError(id, RPC_INTERNAL_ERROR, `${errorPrefix}: ${message}`);
|
|
1089
1146
|
}
|
|
1090
1147
|
}
|
|
1148
|
+
checkAdapterCapabilityThreadOwnership(adapterName, capability, capabilityParams, callerAgentId) {
|
|
1149
|
+
if (adapterName !== "slack")
|
|
1150
|
+
return null;
|
|
1151
|
+
if (capability !== "api.call")
|
|
1152
|
+
return null;
|
|
1153
|
+
const method = typeof capabilityParams.method === "string" ? capabilityParams.method.trim() : "";
|
|
1154
|
+
if (method !== "chat.postMessage")
|
|
1155
|
+
return null;
|
|
1156
|
+
const inner = capabilityParams.params &&
|
|
1157
|
+
typeof capabilityParams.params === "object" &&
|
|
1158
|
+
!Array.isArray(capabilityParams.params)
|
|
1159
|
+
? capabilityParams.params
|
|
1160
|
+
: {};
|
|
1161
|
+
const threadTs = typeof inner.thread_ts === "string" ? inner.thread_ts.trim() : "";
|
|
1162
|
+
if (!threadTs)
|
|
1163
|
+
return null;
|
|
1164
|
+
// Defense in depth (#855): even if a future call path forgot the
|
|
1165
|
+
// registration guard on the handler, refuse threaded chat.postMessage
|
|
1166
|
+
// for unregistered callers here — they cannot own a Slack thread and
|
|
1167
|
+
// must not be able to post into one.
|
|
1168
|
+
if (!callerAgentId) {
|
|
1169
|
+
return `Slack thread ${threadTs}: refusing threaded chat.postMessage from an unregistered caller`;
|
|
1170
|
+
}
|
|
1171
|
+
const thread = this.db.getThread(threadTs);
|
|
1172
|
+
if (!thread?.ownerAgent)
|
|
1173
|
+
return null;
|
|
1174
|
+
if (thread.ownerAgent === callerAgentId)
|
|
1175
|
+
return null;
|
|
1176
|
+
return `Slack thread ${threadTs} is already owned by another agent; refusing cross-owner chat.postMessage`;
|
|
1177
|
+
}
|
|
1091
1178
|
applyAdapterCapabilityEffects(adapterName, response, state) {
|
|
1092
1179
|
if (!state.agentId)
|
|
1093
1180
|
return;
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { type RuntimeScopeCarrier } from "@pinet/transport-core";
|
|
2
2
|
import type { ReactionCommandSettings } from "./reaction-triggers.js";
|
|
3
|
+
import type { AgentSessionSummary } from "./broker/types.js";
|
|
4
|
+
export interface SlackIngressGuardSettings {
|
|
5
|
+
requireMention?: {
|
|
6
|
+
/** Slack channel IDs where every actionable message must explicitly mention the bot. */
|
|
7
|
+
channels?: string[];
|
|
8
|
+
/** Require mention in Pinet-owned threads once anyone outside trustedUsers + the bot participates. */
|
|
9
|
+
mixedParticipantThreads?: {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
trustedUsers?: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
3
15
|
export interface SlackBridgeSettings {
|
|
4
16
|
botToken?: string;
|
|
5
17
|
appToken?: string;
|
|
@@ -7,6 +19,7 @@ export interface SlackBridgeSettings {
|
|
|
7
19
|
appConfigToken?: string;
|
|
8
20
|
allowedUsers?: string[];
|
|
9
21
|
allowAllWorkspaceUsers?: boolean;
|
|
22
|
+
ingressGuard?: SlackIngressGuardSettings;
|
|
10
23
|
defaultChannel?: string;
|
|
11
24
|
logChannel?: string;
|
|
12
25
|
logLevel?: "errors" | "actions" | "verbose";
|
|
@@ -157,6 +170,8 @@ export interface AgentDisplayInfo {
|
|
|
157
170
|
name: string;
|
|
158
171
|
id: string;
|
|
159
172
|
pid?: number;
|
|
173
|
+
stableId?: string | null;
|
|
174
|
+
session?: AgentSessionSummary | null;
|
|
160
175
|
status: "working" | "idle";
|
|
161
176
|
metadata?: {
|
|
162
177
|
cwd?: string;
|
|
@@ -210,6 +225,8 @@ export interface AgentVisibilityInput {
|
|
|
210
225
|
name: string;
|
|
211
226
|
id: string;
|
|
212
227
|
pid?: number;
|
|
228
|
+
stableId?: string | null;
|
|
229
|
+
session?: AgentSessionSummary | null;
|
|
213
230
|
status: "working" | "idle";
|
|
214
231
|
metadata?: Record<string, unknown> | null;
|
|
215
232
|
lastHeartbeat?: string;
|
package/dist/helpers.js
CHANGED
|
@@ -5,6 +5,7 @@ import { classifyPinetMail, formatPinetMailClassLabel, } from "@pinet/broker-cor
|
|
|
5
5
|
import { buildCompatibilityInstanceScope, buildCompatibilityWorkspaceScope, buildRuntimeScopeCarrier, } from "@pinet/transport-core";
|
|
6
6
|
import { buildPinetReadPointer } from "./broker-inbound-persistence.js";
|
|
7
7
|
import { matchesToolPattern } from "./guardrails.js";
|
|
8
|
+
import { getPinetSessionPath, summarizePinetStableId } from "./pinet-session-formatting.js";
|
|
8
9
|
function normalizeOptionalSetting(value) {
|
|
9
10
|
const trimmed = value?.trim();
|
|
10
11
|
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
@@ -855,6 +856,8 @@ export function buildAgentDisplayInfo(agent, options = {}) {
|
|
|
855
856
|
name: agent.name,
|
|
856
857
|
id: agent.id,
|
|
857
858
|
...(agent.pid != null ? { pid: agent.pid } : {}),
|
|
859
|
+
...(agent.stableId !== undefined ? { stableId: agent.stableId } : {}),
|
|
860
|
+
session: agent.session ?? summarizePinetStableId(agent.stableId),
|
|
858
861
|
status: agent.status,
|
|
859
862
|
metadata: metadata || hasHierarchyMetadata
|
|
860
863
|
? {
|
|
@@ -1733,7 +1736,15 @@ export function formatAgentList(agents, homedir) {
|
|
|
1733
1736
|
: "";
|
|
1734
1737
|
const stuckTag = a.stuck ? " [stuck]" : "";
|
|
1735
1738
|
const pid = a.pid != null ? ` pid:${a.pid}` : "";
|
|
1736
|
-
|
|
1739
|
+
const sessionRef = a.session?.ref ? ` session:${a.session.ref}` : "";
|
|
1740
|
+
let line = `${a.emoji} ${a.name} (${a.id}) \u2014 ${a.status}${statusFlavor}${health}${stuckTag}${pid}${sessionRef}`;
|
|
1741
|
+
if (a.stableId) {
|
|
1742
|
+
const sessionPath = getPinetSessionPath(a.stableId);
|
|
1743
|
+
line += `\n stable: ${a.stableId}`;
|
|
1744
|
+
if (sessionPath) {
|
|
1745
|
+
line += `\n jsonl: ${sessionPath}`;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1737
1748
|
const meta = a.metadata;
|
|
1738
1749
|
if (meta && (meta.cwd || meta.branch || meta.host || meta.gitProbeFailed)) {
|
|
1739
1750
|
const cwd = meta.cwd ? shortenPath(meta.cwd, homedir) : "";
|
package/dist/index.js
CHANGED
|
@@ -733,7 +733,7 @@ export default function (pi) {
|
|
|
733
733
|
brokerRuntime.logActivity(entry);
|
|
734
734
|
},
|
|
735
735
|
});
|
|
736
|
-
const { sendPinetAgentMessage, sendPinetBroadcastMessage, scheduleBrokerWakeup, scheduleFollowerWakeup, listBrokerAgents, listFollowerAgents, } = pinetMeshOps;
|
|
736
|
+
const { sendPinetAgentMessage, sendPinetBroadcastMessage, scheduleBrokerWakeup, scheduleFollowerWakeup, listBrokerAgents, listFollowerAgents, searchPinetSessions, } = pinetMeshOps;
|
|
737
737
|
async function listPinetLanes(options) {
|
|
738
738
|
if (brokerRole === "broker") {
|
|
739
739
|
const db = getActiveBrokerDb();
|
|
@@ -1002,6 +1002,7 @@ export default function (pi) {
|
|
|
1002
1002
|
getBotUserId: () => botUserId,
|
|
1003
1003
|
registerConfirmationRequest,
|
|
1004
1004
|
pinetDelivery: {
|
|
1005
|
+
isEnabled: () => pinetEnabled,
|
|
1005
1006
|
isAvailable: () => pinetEnabled && brokerRole !== null,
|
|
1006
1007
|
sendSlackMessage: async (input) => {
|
|
1007
1008
|
const content = {
|
|
@@ -1072,6 +1073,7 @@ export default function (pi) {
|
|
|
1072
1073
|
readPinetInbox,
|
|
1073
1074
|
listBrokerAgents,
|
|
1074
1075
|
listFollowerAgents,
|
|
1076
|
+
searchPinetSessions,
|
|
1075
1077
|
listSubtreeAgents: (includeGhosts) => subtreeBrokerRuntime.listAgents(includeGhosts),
|
|
1076
1078
|
getSubtreeSelfAgentId: () => subtreeBrokerRuntime.getStatus().selfAgentId,
|
|
1077
1079
|
spawnSubtreeWorker: async (input) => {
|
package/dist/pinet-mesh-ops.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type AgentMessageStorage } from "./broker/agent-messaging.js";
|
|
2
|
-
import type { AgentInfo, TaskAssignmentInfo, TaskAssignmentKind } from "./broker/types.js";
|
|
2
|
+
import type { AgentInfo, AgentSessionSearchInfo, AgentSessionSearchOptions, AgentSessionSummary, TaskAssignmentInfo, TaskAssignmentKind } from "./broker/types.js";
|
|
3
3
|
import type { ActivityLogEntry } from "./activity-log.js";
|
|
4
4
|
export interface PinetMeshOpsAgentRecord {
|
|
5
5
|
emoji: string;
|
|
6
6
|
name: string;
|
|
7
7
|
id: string;
|
|
8
8
|
pid?: number;
|
|
9
|
+
stableId?: string | null;
|
|
10
|
+
session?: AgentSessionSummary | null;
|
|
9
11
|
status: "working" | "idle";
|
|
10
12
|
metadata: Record<string, unknown> | null;
|
|
11
13
|
lastHeartbeat: string;
|
|
@@ -32,6 +34,7 @@ export interface PinetMeshOpsTransferableThread {
|
|
|
32
34
|
}
|
|
33
35
|
export interface PinetMeshOpsBrokerDbPort extends AgentMessageStorage {
|
|
34
36
|
getAllAgents: () => AgentInfo[];
|
|
37
|
+
searchAgentSessions: (options?: AgentSessionSearchOptions) => AgentSessionSearchInfo[];
|
|
35
38
|
getPendingInboxCount: (agentId: string) => number;
|
|
36
39
|
getThread: (threadId: string) => PinetMeshOpsTransferableThread | null;
|
|
37
40
|
transferThreadOwnership: (threadId: string, ownerAgent: string) => {
|
|
@@ -62,6 +65,7 @@ export interface PinetMeshOpsFollowerClientPort {
|
|
|
62
65
|
fireAt: string;
|
|
63
66
|
}>;
|
|
64
67
|
listAgents: (includeGhosts: boolean) => Promise<PinetMeshOpsFollowerAgentRecord[]>;
|
|
68
|
+
searchAgentSessions: (options: AgentSessionSearchOptions) => Promise<AgentSessionSearchInfo[]>;
|
|
65
69
|
}
|
|
66
70
|
export interface PinetMeshOpsDeps {
|
|
67
71
|
getPinetEnabled: () => boolean;
|
|
@@ -100,6 +104,7 @@ export interface PinetMeshOps {
|
|
|
100
104
|
}>;
|
|
101
105
|
listBrokerAgents: () => PinetMeshOpsAgentRecord[];
|
|
102
106
|
listFollowerAgents: (includeGhosts: boolean) => Promise<PinetMeshOpsAgentRecord[]>;
|
|
107
|
+
searchPinetSessions: (options: AgentSessionSearchOptions) => Promise<AgentSessionSearchInfo[]>;
|
|
103
108
|
}
|
|
104
109
|
export declare function parseGitHubRemoteRepo(remoteUrl: string): {
|
|
105
110
|
repoOwner: string;
|
package/dist/pinet-mesh-ops.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { normalizeOutgoingPinetControlMessage } from "./helpers.js";
|
|
4
4
|
import { dispatchBroadcastAgentMessage, dispatchDirectAgentMessage, isBroadcastChannelTarget, } from "./broker/agent-messaging.js";
|
|
5
|
+
import { summarizePinetStableId } from "./pinet-session-formatting.js";
|
|
5
6
|
import { extractTaskAssignmentsFromMessage } from "./task-assignments.js";
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
function prepareOutgoingPinetAgentMessage(body, metadata) {
|
|
@@ -33,6 +34,7 @@ function appendSlackThreadTransferNotice(body, threadId, channel) {
|
|
|
33
34
|
`- channel: ${channel}`,
|
|
34
35
|
`- To report directly in the transferred Slack thread, use slack_send with thread_ts ${threadId}; the channel is already recorded in Pinet.`,
|
|
35
36
|
"- If slack_send says the thread is already owned by another agent, ask the broker to inspect ownership and transfer it again.",
|
|
37
|
+
"- If slack_send says the Pinet broker is unavailable, wait for the broker to reconnect \u2014 do NOT retry via post_channel; direct posting would bypass thread ownership (#855).",
|
|
36
38
|
].join("\n");
|
|
37
39
|
}
|
|
38
40
|
export function parseGitHubRemoteRepo(remoteUrl) {
|
|
@@ -230,6 +232,8 @@ export function createPinetMeshOps(deps) {
|
|
|
230
232
|
name: agent.name,
|
|
231
233
|
id: agent.id,
|
|
232
234
|
pid: agent.pid,
|
|
235
|
+
stableId: agent.stableId ?? null,
|
|
236
|
+
session: summarizePinetStableId(agent.stableId),
|
|
233
237
|
status: agent.status,
|
|
234
238
|
metadata: agent.metadata,
|
|
235
239
|
lastHeartbeat: agent.lastHeartbeat,
|
|
@@ -256,6 +260,7 @@ export function createPinetMeshOps(deps) {
|
|
|
256
260
|
name: agent.name,
|
|
257
261
|
id: agent.id,
|
|
258
262
|
pid: agent.pid,
|
|
263
|
+
session: agent.session ?? null,
|
|
259
264
|
status: agent.status ?? "idle",
|
|
260
265
|
metadata: agent.metadata,
|
|
261
266
|
lastHeartbeat: agent.lastHeartbeat,
|
|
@@ -272,6 +277,20 @@ export function createPinetMeshOps(deps) {
|
|
|
272
277
|
laneId: agent.laneId,
|
|
273
278
|
}));
|
|
274
279
|
}
|
|
280
|
+
async function searchPinetSessions(options) {
|
|
281
|
+
if (deps.getBrokerRole() === "broker") {
|
|
282
|
+
const db = deps.getActiveBrokerDb();
|
|
283
|
+
if (!db) {
|
|
284
|
+
throw new Error("Broker agent identity is unavailable.");
|
|
285
|
+
}
|
|
286
|
+
return db.searchAgentSessions(options);
|
|
287
|
+
}
|
|
288
|
+
const client = deps.getFollowerClient();
|
|
289
|
+
if (!client) {
|
|
290
|
+
throw new Error("Pinet is in an unexpected state.");
|
|
291
|
+
}
|
|
292
|
+
return await client.searchAgentSessions(options);
|
|
293
|
+
}
|
|
275
294
|
return {
|
|
276
295
|
sendPinetAgentMessage,
|
|
277
296
|
sendPinetBroadcastMessage,
|
|
@@ -279,5 +298,6 @@ export function createPinetMeshOps(deps) {
|
|
|
279
298
|
scheduleFollowerWakeup,
|
|
280
299
|
listBrokerAgents,
|
|
281
300
|
listFollowerAgents,
|
|
301
|
+
searchPinetSessions,
|
|
282
302
|
};
|
|
283
303
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentSessionKind, AgentSessionSearchInfo, AgentSessionSummary } from "./broker/types.js";
|
|
2
|
+
export interface ParsedPinetStableId {
|
|
3
|
+
host: string | null;
|
|
4
|
+
kind: AgentSessionKind;
|
|
5
|
+
locator: string;
|
|
6
|
+
hasPath: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function parsePinetStableId(stableId: string | null | undefined): ParsedPinetStableId | null;
|
|
9
|
+
export declare function summarizePinetStableId(stableId: string | null | undefined): AgentSessionSummary | null;
|
|
10
|
+
export declare function getPinetSessionPath(stableId: string | null | undefined): string | null;
|
|
11
|
+
export declare function getPinetSessionFilename(stableId: string | null | undefined): string | null;
|
|
12
|
+
export declare function buildPinetSessionFullDetails(session: AgentSessionSearchInfo): Record<string, unknown>;
|
|
13
|
+
export declare function buildPinetSessionCompactDetails(session: AgentSessionSearchInfo): Record<string, unknown>;
|