@pinet/slack-bridge 0.1.0
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/LICENSE +21 -0
- package/README.md +570 -0
- package/dist/activity-log.d.ts +62 -0
- package/dist/activity-log.js +293 -0
- package/dist/agent-completion-runtime.d.ts +17 -0
- package/dist/agent-completion-runtime.js +26 -0
- package/dist/agent-event-runtime.d.ts +13 -0
- package/dist/agent-event-runtime.js +27 -0
- package/dist/agent-prompt-guidance.d.ts +22 -0
- package/dist/agent-prompt-guidance.js +46 -0
- package/dist/broker/adapters/slack.d.ts +74 -0
- package/dist/broker/adapters/slack.js +563 -0
- package/dist/broker/adapters/types.d.ts +1 -0
- package/dist/broker/adapters/types.js +1 -0
- package/dist/broker/agent-messaging.d.ts +1 -0
- package/dist/broker/agent-messaging.js +1 -0
- package/dist/broker/auth.d.ts +1 -0
- package/dist/broker/auth.js +1 -0
- package/dist/broker/client.d.ts +167 -0
- package/dist/broker/client.js +624 -0
- package/dist/broker/control-plane-dashboard.d.ts +98 -0
- package/dist/broker/control-plane-dashboard.js +213 -0
- package/dist/broker/ghost-reaper.d.ts +49 -0
- package/dist/broker/ghost-reaper.js +209 -0
- package/dist/broker/index.d.ts +36 -0
- package/dist/broker/index.js +90 -0
- package/dist/broker/leader.d.ts +1 -0
- package/dist/broker/leader.js +1 -0
- package/dist/broker/maintenance.d.ts +1 -0
- package/dist/broker/maintenance.js +1 -0
- package/dist/broker/message-send.d.ts +1 -0
- package/dist/broker/message-send.js +1 -0
- package/dist/broker/paths.d.ts +1 -0
- package/dist/broker/paths.js +1 -0
- package/dist/broker/raw-tcp-loopback.d.ts +1 -0
- package/dist/broker/raw-tcp-loopback.js +1 -0
- package/dist/broker/router.d.ts +1 -0
- package/dist/broker/router.js +1 -0
- package/dist/broker/schema.d.ts +9 -0
- package/dist/broker/schema.js +63 -0
- package/dist/broker/socket-server.d.ts +120 -0
- package/dist/broker/socket-server.js +1087 -0
- package/dist/broker/types.d.ts +1 -0
- package/dist/broker/types.js +1 -0
- package/dist/broker-delivery.d.ts +10 -0
- package/dist/broker-delivery.js +26 -0
- package/dist/broker-inbound-persistence.d.ts +11 -0
- package/dist/broker-inbound-persistence.js +22 -0
- package/dist/broker-prompt-loader.d.ts +34 -0
- package/dist/broker-prompt-loader.js +185 -0
- package/dist/broker-runtime-access.d.ts +14 -0
- package/dist/broker-runtime-access.js +20 -0
- package/dist/broker-runtime.d.ts +100 -0
- package/dist/broker-runtime.js +533 -0
- package/dist/broker-thread-owner-hints.d.ts +10 -0
- package/dist/broker-thread-owner-hints.js +14 -0
- package/dist/canvases.d.ts +80 -0
- package/dist/canvases.js +221 -0
- package/dist/command-registration-runtime.d.ts +9 -0
- package/dist/command-registration-runtime.js +9 -0
- package/dist/core-tool-guardrails.d.ts +24 -0
- package/dist/core-tool-guardrails.js +66 -0
- package/dist/deploy-manifest.d.ts +27 -0
- package/dist/deploy-manifest.js +197 -0
- package/dist/follower-delivery.d.ts +16 -0
- package/dist/follower-delivery.js +70 -0
- package/dist/follower-runtime.d.ts +61 -0
- package/dist/follower-runtime.js +350 -0
- package/dist/git-metadata.d.ts +35 -0
- package/dist/git-metadata.js +88 -0
- package/dist/guardrails.d.ts +69 -0
- package/dist/guardrails.js +283 -0
- package/dist/helpers.d.ts +550 -0
- package/dist/helpers.js +2711 -0
- package/dist/home-tab.d.ts +27 -0
- package/dist/home-tab.js +242 -0
- package/dist/imessage-tools.d.ts +33 -0
- package/dist/imessage-tools.js +96 -0
- package/dist/inbox-drain-runtime.d.ts +30 -0
- package/dist/inbox-drain-runtime.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1330 -0
- package/dist/persisted-runtime-state.d.ts +52 -0
- package/dist/persisted-runtime-state.js +98 -0
- package/dist/pinet-activity-formatting.d.ts +20 -0
- package/dist/pinet-activity-formatting.js +43 -0
- package/dist/pinet-agent-status.d.ts +36 -0
- package/dist/pinet-agent-status.js +48 -0
- package/dist/pinet-commands.d.ts +70 -0
- package/dist/pinet-commands.js +402 -0
- package/dist/pinet-confirmation-replies.d.ts +7 -0
- package/dist/pinet-confirmation-replies.js +30 -0
- package/dist/pinet-control-plane-dashboard.d.ts +34 -0
- package/dist/pinet-control-plane-dashboard.js +86 -0
- package/dist/pinet-home-tabs.d.ts +36 -0
- package/dist/pinet-home-tabs.js +101 -0
- package/dist/pinet-maintenance-delivery.d.ts +22 -0
- package/dist/pinet-maintenance-delivery.js +45 -0
- package/dist/pinet-mesh-ops.d.ts +97 -0
- package/dist/pinet-mesh-ops.js +266 -0
- package/dist/pinet-registration-gate.d.ts +14 -0
- package/dist/pinet-registration-gate.js +42 -0
- package/dist/pinet-remote-control-acks.d.ts +16 -0
- package/dist/pinet-remote-control-acks.js +46 -0
- package/dist/pinet-remote-control.d.ts +17 -0
- package/dist/pinet-remote-control.js +74 -0
- package/dist/pinet-runtime-composition.d.ts +27 -0
- package/dist/pinet-runtime-composition.js +20 -0
- package/dist/pinet-tools.d.ts +67 -0
- package/dist/pinet-tools.js +1211 -0
- package/dist/prompts/broker/default.md +41 -0
- package/dist/prompts/broker/tmux.md +49 -0
- package/dist/ralph-loop.d.ts +76 -0
- package/dist/ralph-loop.js +524 -0
- package/dist/reaction-triggers.d.ts +25 -0
- package/dist/reaction-triggers.js +171 -0
- package/dist/repo-tool-guardrails.d.ts +20 -0
- package/dist/repo-tool-guardrails.js +81 -0
- package/dist/runtime-agent-context.d.ts +93 -0
- package/dist/runtime-agent-context.js +302 -0
- package/dist/runtime-mode.d.ts +10 -0
- package/dist/runtime-mode.js +39 -0
- package/dist/scheduled-wakeups.d.ts +1 -0
- package/dist/scheduled-wakeups.js +1 -0
- package/dist/session-ui-runtime.d.ts +20 -0
- package/dist/session-ui-runtime.js +163 -0
- package/dist/single-player-runtime.d.ts +71 -0
- package/dist/single-player-runtime.js +436 -0
- package/dist/skins/cosmere.json +1571 -0
- package/dist/skins/foundation.json +1304 -0
- package/dist/slack-access.d.ts +186 -0
- package/dist/slack-access.js +462 -0
- package/dist/slack-api.d.ts +2 -0
- package/dist/slack-api.js +1 -0
- package/dist/slack-block-kit.d.ts +36 -0
- package/dist/slack-block-kit.js +242 -0
- package/dist/slack-export.d.ts +31 -0
- package/dist/slack-export.js +206 -0
- package/dist/slack-message-context.d.ts +15 -0
- package/dist/slack-message-context.js +207 -0
- package/dist/slack-modals.d.ts +13 -0
- package/dist/slack-modals.js +79 -0
- package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
- package/dist/slack-pinet-runtime-adapter.js +72 -0
- package/dist/slack-presence.d.ts +36 -0
- package/dist/slack-presence.js +89 -0
- package/dist/slack-request-runtime.d.ts +8 -0
- package/dist/slack-request-runtime.js +19 -0
- package/dist/slack-runtime-access.d.ts +28 -0
- package/dist/slack-runtime-access.js +107 -0
- package/dist/slack-scope-diagnostics.d.ts +31 -0
- package/dist/slack-scope-diagnostics.js +235 -0
- package/dist/slack-socket-dedup.d.ts +7 -0
- package/dist/slack-socket-dedup.js +135 -0
- package/dist/slack-thread-status.d.ts +41 -0
- package/dist/slack-thread-status.js +156 -0
- package/dist/slack-tool-policy-runtime.d.ts +36 -0
- package/dist/slack-tool-policy-runtime.js +96 -0
- package/dist/slack-tools.d.ts +52 -0
- package/dist/slack-tools.js +2688 -0
- package/dist/slack-turn-guardrails.d.ts +19 -0
- package/dist/slack-turn-guardrails.js +36 -0
- package/dist/slack-upload.d.ts +49 -0
- package/dist/slack-upload.js +224 -0
- package/dist/task-assignments.d.ts +45 -0
- package/dist/task-assignments.js +527 -0
- package/dist/thread-confirmations.d.ts +22 -0
- package/dist/thread-confirmations.js +139 -0
- package/dist/tool-registration-runtime.d.ts +13 -0
- package/dist/tool-registration-runtime.js +13 -0
- package/dist/ttl-cache.d.ts +47 -0
- package/dist/ttl-cache.js +105 -0
- package/manifest.yaml +57 -0
- package/package.json +60 -0
- package/skills/pinet-skin-creator/SKILL.md +109 -0
- package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
- package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
- package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
- package/skills/slack-bridge/SKILL.md +360 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { buildSlackRequest } from "./helpers.js";
|
|
2
|
+
const SCOPE_DRIFT_PROBES = [
|
|
3
|
+
{
|
|
4
|
+
key: "files_read",
|
|
5
|
+
surface: "files",
|
|
6
|
+
method: "files.info",
|
|
7
|
+
body: { file: "F0000000000" },
|
|
8
|
+
expectedScopes: ["files:read"],
|
|
9
|
+
okErrors: ["file_not_found", "invalid_arguments", "channel_canvas_deleted"],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
key: "files_write",
|
|
13
|
+
surface: "files",
|
|
14
|
+
method: "files.completeUploadExternal",
|
|
15
|
+
body: {
|
|
16
|
+
files: [{ id: "F0000000000", title: "scope-drift-probe" }],
|
|
17
|
+
channel_id: "C0000000000",
|
|
18
|
+
},
|
|
19
|
+
expectedScopes: ["files:write"],
|
|
20
|
+
okErrors: ["file_not_found", "channel_not_found", "invalid_arguments"],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: "bookmarks_read",
|
|
24
|
+
surface: "bookmarks",
|
|
25
|
+
method: "bookmarks.list",
|
|
26
|
+
body: { channel_id: "C0000000000" },
|
|
27
|
+
expectedScopes: ["bookmarks:read"],
|
|
28
|
+
okErrors: ["channel_not_found", "invalid_arguments"],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: "bookmarks_write",
|
|
32
|
+
surface: "bookmarks",
|
|
33
|
+
method: "bookmarks.remove",
|
|
34
|
+
body: { channel_id: "C0000000000", bookmark_id: "Bk0000000000" },
|
|
35
|
+
expectedScopes: ["bookmarks:write"],
|
|
36
|
+
okErrors: ["channel_not_found", "not_found", "invalid_arguments"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "pins_read",
|
|
40
|
+
surface: "pins",
|
|
41
|
+
method: "pins.list",
|
|
42
|
+
body: { channel: "C0000000000" },
|
|
43
|
+
expectedScopes: ["pins:read"],
|
|
44
|
+
okErrors: ["channel_not_found", "invalid_arguments"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "pins_write",
|
|
48
|
+
surface: "pins",
|
|
49
|
+
method: "pins.add",
|
|
50
|
+
body: { channel: "C0000000000", timestamp: "0" },
|
|
51
|
+
expectedScopes: ["pins:write"],
|
|
52
|
+
okErrors: [
|
|
53
|
+
"channel_not_found",
|
|
54
|
+
"message_not_found",
|
|
55
|
+
"file_not_found",
|
|
56
|
+
"no_item",
|
|
57
|
+
"invalid_arguments",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
function uniqueSorted(values) {
|
|
62
|
+
return [...new Set(values)].sort();
|
|
63
|
+
}
|
|
64
|
+
function normalizeScopeList(raw) {
|
|
65
|
+
if (typeof raw !== "string") {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return uniqueSorted(raw
|
|
69
|
+
.split(/[\s,]+/)
|
|
70
|
+
.map((scope) => scope.trim())
|
|
71
|
+
.filter(Boolean));
|
|
72
|
+
}
|
|
73
|
+
function summarizeUnavailableErrors(results) {
|
|
74
|
+
const errors = uniqueSorted(results
|
|
75
|
+
.filter((result) => result.status === "unavailable")
|
|
76
|
+
.map((result) => `${result.method}: ${result.error ?? "unknown"}`));
|
|
77
|
+
return errors[0];
|
|
78
|
+
}
|
|
79
|
+
function formatScopeList(scopes) {
|
|
80
|
+
return scopes.join(", ");
|
|
81
|
+
}
|
|
82
|
+
async function readSlackProbeResult(response) {
|
|
83
|
+
try {
|
|
84
|
+
return (await response.json());
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { ok: false, error: `http_${response.status}` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function runSlackScopeProbe(probe, token, fetchImpl) {
|
|
91
|
+
const { url, init } = buildSlackRequest(probe.method, token, probe.body);
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetchImpl(url, init);
|
|
94
|
+
if (response.status === 429) {
|
|
95
|
+
return {
|
|
96
|
+
key: probe.key,
|
|
97
|
+
surface: probe.surface,
|
|
98
|
+
method: probe.method,
|
|
99
|
+
expectedScopes: probe.expectedScopes,
|
|
100
|
+
status: "unavailable",
|
|
101
|
+
missingScopes: [],
|
|
102
|
+
error: "rate_limited",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const payload = await readSlackProbeResult(response);
|
|
106
|
+
if (payload.ok) {
|
|
107
|
+
return {
|
|
108
|
+
key: probe.key,
|
|
109
|
+
surface: probe.surface,
|
|
110
|
+
method: probe.method,
|
|
111
|
+
expectedScopes: probe.expectedScopes,
|
|
112
|
+
status: "ok",
|
|
113
|
+
missingScopes: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const error = typeof payload.error === "string" ? payload.error : `http_${response.status}`;
|
|
117
|
+
if (probe.okErrors.includes(error)) {
|
|
118
|
+
return {
|
|
119
|
+
key: probe.key,
|
|
120
|
+
surface: probe.surface,
|
|
121
|
+
method: probe.method,
|
|
122
|
+
expectedScopes: probe.expectedScopes,
|
|
123
|
+
status: "ok",
|
|
124
|
+
missingScopes: [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (error === "missing_scope" || error === "not_allowed_token_type") {
|
|
128
|
+
const neededScopes = normalizeScopeList(payload.needed);
|
|
129
|
+
const providedScopes = normalizeScopeList(payload.provided);
|
|
130
|
+
return {
|
|
131
|
+
key: probe.key,
|
|
132
|
+
surface: probe.surface,
|
|
133
|
+
method: probe.method,
|
|
134
|
+
expectedScopes: probe.expectedScopes,
|
|
135
|
+
status: "missing",
|
|
136
|
+
missingScopes: neededScopes.length > 0 ? neededScopes : probe.expectedScopes,
|
|
137
|
+
error,
|
|
138
|
+
...(neededScopes.length > 0 ? { neededScopes } : {}),
|
|
139
|
+
...(providedScopes.length > 0 ? { providedScopes } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
key: probe.key,
|
|
144
|
+
surface: probe.surface,
|
|
145
|
+
method: probe.method,
|
|
146
|
+
expectedScopes: probe.expectedScopes,
|
|
147
|
+
status: "unavailable",
|
|
148
|
+
missingScopes: [],
|
|
149
|
+
error,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
key: probe.key,
|
|
155
|
+
surface: probe.surface,
|
|
156
|
+
method: probe.method,
|
|
157
|
+
expectedScopes: probe.expectedScopes,
|
|
158
|
+
status: "unavailable",
|
|
159
|
+
missingScopes: [],
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function createUncheckedSlackScopeDiagnostics() {
|
|
165
|
+
return {
|
|
166
|
+
status: "not_checked",
|
|
167
|
+
checkedAt: null,
|
|
168
|
+
summary: "not checked",
|
|
169
|
+
surfaces: [],
|
|
170
|
+
missingScopes: [],
|
|
171
|
+
results: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
export function createPendingSlackScopeDiagnostics() {
|
|
175
|
+
return {
|
|
176
|
+
status: "pending",
|
|
177
|
+
checkedAt: null,
|
|
178
|
+
summary: "pending",
|
|
179
|
+
surfaces: [],
|
|
180
|
+
missingScopes: [],
|
|
181
|
+
results: [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export async function detectSlackScopeDiagnostics(options) {
|
|
185
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
186
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
187
|
+
const checkedAt = now();
|
|
188
|
+
const results = await Promise.all(SCOPE_DRIFT_PROBES.map((probe) => runSlackScopeProbe(probe, options.token, fetchImpl)));
|
|
189
|
+
const missingScopes = uniqueSorted(results.flatMap((result) => (result.status === "missing" ? result.missingScopes : [])));
|
|
190
|
+
const surfaces = uniqueSorted(results.flatMap((result) => (result.status === "missing" ? [result.surface] : [])));
|
|
191
|
+
if (missingScopes.length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
status: "drift",
|
|
194
|
+
checkedAt,
|
|
195
|
+
summary: `scope drift — missing ${formatScopeList(missingScopes)}`,
|
|
196
|
+
surfaces,
|
|
197
|
+
missingScopes,
|
|
198
|
+
results,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const unavailableError = summarizeUnavailableErrors(results);
|
|
202
|
+
if (unavailableError) {
|
|
203
|
+
return {
|
|
204
|
+
status: "unavailable",
|
|
205
|
+
checkedAt,
|
|
206
|
+
summary: `unavailable (${unavailableError})`,
|
|
207
|
+
surfaces: [],
|
|
208
|
+
missingScopes: [],
|
|
209
|
+
results,
|
|
210
|
+
error: unavailableError,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
status: "healthy",
|
|
215
|
+
checkedAt,
|
|
216
|
+
summary: "healthy",
|
|
217
|
+
surfaces: [],
|
|
218
|
+
missingScopes: [],
|
|
219
|
+
results,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function formatSlackScopeDiagnosticsStatus(diagnostics) {
|
|
223
|
+
return diagnostics.summary;
|
|
224
|
+
}
|
|
225
|
+
export function buildSlackScopeDriftWarning(diagnostics) {
|
|
226
|
+
if (diagnostics.status !== "drift" || diagnostics.missingScopes.length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const surfaceSummary = diagnostics.surfaces.length > 0 ? diagnostics.surfaces.join(", ") : "files";
|
|
230
|
+
return [
|
|
231
|
+
`Slack scope drift detected: missing ${formatScopeList(diagnostics.missingScopes)}.`,
|
|
232
|
+
`Affected Slack surfaces: ${surfaceSummary}.`,
|
|
233
|
+
"Reinstall or reapply the current slack-bridge manifest scopes in Slack, then restart the bridge.",
|
|
234
|
+
].join(" ");
|
|
235
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SLACK_SOCKET_DELIVERY_DEDUP_TTL_MS: number;
|
|
2
|
+
export declare const SLACK_SOCKET_DELIVERY_DEDUP_MAX_SIZE = 10000;
|
|
3
|
+
export declare function extractSlackEventDedupKey(evt: Record<string, unknown>): string | null;
|
|
4
|
+
export declare function extractSlackBlockActionDedupKey(payload: Record<string, unknown>): string | null;
|
|
5
|
+
export declare function extractSlackViewSubmissionDedupKey(payload: Record<string, unknown>): string | null;
|
|
6
|
+
export declare function extractSlackInteractiveDedupKey(payload: Record<string, unknown>): string | null;
|
|
7
|
+
export declare function extractSlackSocketDedupKey(frame: Record<string, unknown>): string | null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { extractSlackInteractivePayloadFromEnvelope } from "./slack-block-kit.js";
|
|
2
|
+
export const SLACK_SOCKET_DELIVERY_DEDUP_TTL_MS = 10 * 60 * 1000;
|
|
3
|
+
export const SLACK_SOCKET_DELIVERY_DEDUP_MAX_SIZE = 10_000;
|
|
4
|
+
function asNonEmptyString(value) {
|
|
5
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
6
|
+
}
|
|
7
|
+
function buildSlackMessageDedupKey(evt) {
|
|
8
|
+
const channel = asNonEmptyString(evt.channel);
|
|
9
|
+
const ts = asNonEmptyString(evt.ts);
|
|
10
|
+
if (!channel || !ts) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return [
|
|
14
|
+
"message",
|
|
15
|
+
channel,
|
|
16
|
+
asNonEmptyString(evt.thread_ts) ?? "",
|
|
17
|
+
ts,
|
|
18
|
+
asNonEmptyString(evt.user) ?? asNonEmptyString(evt.bot_id) ?? "",
|
|
19
|
+
asNonEmptyString(evt.subtype) ?? "",
|
|
20
|
+
].join(":");
|
|
21
|
+
}
|
|
22
|
+
function buildSlackReactionDedupKey(evt) {
|
|
23
|
+
const item = evt.item;
|
|
24
|
+
if (!item || item.type !== "message" || !item.channel || !item.ts) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return [
|
|
28
|
+
"reaction_added",
|
|
29
|
+
item.channel,
|
|
30
|
+
item.ts,
|
|
31
|
+
asNonEmptyString(evt.user) ?? "",
|
|
32
|
+
asNonEmptyString(evt.reaction) ?? "",
|
|
33
|
+
asNonEmptyString(evt.event_ts) ?? "",
|
|
34
|
+
].join(":");
|
|
35
|
+
}
|
|
36
|
+
function buildSlackAssistantThreadDedupKey(kind, evt) {
|
|
37
|
+
const thread = evt.assistant_thread;
|
|
38
|
+
if (!thread?.channel_id || !thread.thread_ts) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return [kind, thread.channel_id, thread.thread_ts, thread.user_id ?? ""].join(":");
|
|
42
|
+
}
|
|
43
|
+
function buildMemberJoinedDedupKey(evt) {
|
|
44
|
+
const channel = asNonEmptyString(evt.channel);
|
|
45
|
+
const user = asNonEmptyString(evt.user);
|
|
46
|
+
if (!channel || !user) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return ["member_joined_channel", channel, user, asNonEmptyString(evt.event_ts) ?? ""].join(":");
|
|
50
|
+
}
|
|
51
|
+
export function extractSlackEventDedupKey(evt) {
|
|
52
|
+
const type = asNonEmptyString(evt.type);
|
|
53
|
+
if (!type) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
switch (type) {
|
|
57
|
+
case "message":
|
|
58
|
+
return buildSlackMessageDedupKey(evt);
|
|
59
|
+
case "reaction_added":
|
|
60
|
+
return buildSlackReactionDedupKey(evt);
|
|
61
|
+
case "assistant_thread_started":
|
|
62
|
+
return buildSlackAssistantThreadDedupKey("assistant_thread_started", evt);
|
|
63
|
+
case "assistant_thread_context_changed":
|
|
64
|
+
return buildSlackAssistantThreadDedupKey("assistant_thread_context_changed", evt);
|
|
65
|
+
case "member_joined_channel":
|
|
66
|
+
return buildMemberJoinedDedupKey(evt);
|
|
67
|
+
default: {
|
|
68
|
+
const eventTs = asNonEmptyString(evt.event_ts);
|
|
69
|
+
return eventTs ? [type, eventTs].join(":") : null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function extractSlackBlockActionDedupKey(payload) {
|
|
74
|
+
if (payload.type !== "block_actions") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const user = payload.user;
|
|
78
|
+
const channel = payload.channel;
|
|
79
|
+
const container = payload.container;
|
|
80
|
+
const view = payload.view;
|
|
81
|
+
const actions = Array.isArray(payload.actions)
|
|
82
|
+
? payload.actions.filter((action) => typeof action === "object" && action !== null)
|
|
83
|
+
: [];
|
|
84
|
+
const actionSignature = actions
|
|
85
|
+
.map((action) => `${action.action_id ?? ""}@${action.action_ts ?? ""}`)
|
|
86
|
+
.sort()
|
|
87
|
+
.join(",");
|
|
88
|
+
if (!actionSignature) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const parts = [
|
|
92
|
+
"block_actions",
|
|
93
|
+
user?.id ?? "",
|
|
94
|
+
channel?.id ?? container?.channel_id ?? "",
|
|
95
|
+
container?.thread_ts ?? "",
|
|
96
|
+
container?.message_ts ?? "",
|
|
97
|
+
];
|
|
98
|
+
if (view?.id || view?.hash) {
|
|
99
|
+
parts.push(view?.id ?? "", view?.hash ?? "");
|
|
100
|
+
}
|
|
101
|
+
parts.push(actionSignature);
|
|
102
|
+
return parts.join(":");
|
|
103
|
+
}
|
|
104
|
+
export function extractSlackViewSubmissionDedupKey(payload) {
|
|
105
|
+
if (payload.type !== "view_submission") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const user = payload.user;
|
|
109
|
+
const view = payload.view;
|
|
110
|
+
if (!view?.id) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return ["view_submission", user?.id ?? "", view.id, view.hash ?? "", view.callback_id ?? ""].join(":");
|
|
114
|
+
}
|
|
115
|
+
export function extractSlackInteractiveDedupKey(payload) {
|
|
116
|
+
if (payload.type === "block_actions") {
|
|
117
|
+
return extractSlackBlockActionDedupKey(payload);
|
|
118
|
+
}
|
|
119
|
+
if (payload.type === "view_submission") {
|
|
120
|
+
return extractSlackViewSubmissionDedupKey(payload);
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
export function extractSlackSocketDedupKey(frame) {
|
|
125
|
+
if (frame.type === "events_api") {
|
|
126
|
+
const payload = frame.payload;
|
|
127
|
+
const eventId = asNonEmptyString(payload?.event_id);
|
|
128
|
+
if (eventId) {
|
|
129
|
+
return `event:${eventId}`;
|
|
130
|
+
}
|
|
131
|
+
return payload?.event ? extractSlackEventDedupKey(payload.event) : null;
|
|
132
|
+
}
|
|
133
|
+
const interactivePayload = extractSlackInteractivePayloadFromEnvelope(frame);
|
|
134
|
+
return interactivePayload ? extractSlackInteractiveDedupKey(interactivePayload) : null;
|
|
135
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SlackCall } from "./slack-access.js";
|
|
2
|
+
export declare const SLACK_THREAD_STATUS_HEARTBEAT_MS = 90000;
|
|
3
|
+
export declare const SLACK_THREAD_LOADING_MESSAGES: readonly ["Schlepping...", "Combobulating...", "Vibing...", "Noodling...", "Percolating...", "Ruminating...", "Consulting the void...", "Asking the electrons...", "Reticulating splines...", "Consulting the rubber duck..."];
|
|
4
|
+
export declare const DEFAULT_SLACK_THREAD_STATUS = "is thinking\u2026";
|
|
5
|
+
export declare const SLACK_THREAD_SAFE_STATUSES: readonly ["is thinking…", "Reading context…", "Calling tool…", "Drafting reply…", "Checking Slack…"];
|
|
6
|
+
export interface SlackThreadStatusInput {
|
|
7
|
+
slack: SlackCall;
|
|
8
|
+
token: string;
|
|
9
|
+
channelId: string;
|
|
10
|
+
threadTs: string;
|
|
11
|
+
status: string;
|
|
12
|
+
loadingMessages?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function setSlackThreadStatus(input: SlackThreadStatusInput): Promise<void>;
|
|
15
|
+
export interface SlackThreadStatusManagerDeps {
|
|
16
|
+
slack: SlackCall;
|
|
17
|
+
getBotToken: () => string;
|
|
18
|
+
formatError: (error: unknown) => string;
|
|
19
|
+
setInterval?: typeof setInterval;
|
|
20
|
+
clearInterval?: typeof clearInterval;
|
|
21
|
+
heartbeatMs?: number;
|
|
22
|
+
logger?: Pick<Console, "error">;
|
|
23
|
+
maxActiveThreads?: number;
|
|
24
|
+
maxConsecutiveFailures?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function normalizeSlackThreadStatus(status: string | undefined): string;
|
|
27
|
+
export declare class SlackThreadStatusManager {
|
|
28
|
+
private readonly deps;
|
|
29
|
+
private readonly active;
|
|
30
|
+
constructor(deps: SlackThreadStatusManagerDeps);
|
|
31
|
+
begin(channelId: string, threadTs: string, status?: string): Promise<void>;
|
|
32
|
+
update(channelId: string, threadTs: string, status: string, options?: {
|
|
33
|
+
startHeartbeat?: boolean;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
clear(channelId: string, threadTs: string): Promise<void>;
|
|
36
|
+
clearAll(): Promise<void>;
|
|
37
|
+
private heartbeat;
|
|
38
|
+
private evictOldestIfNeeded;
|
|
39
|
+
private stopHeartbeat;
|
|
40
|
+
private trySet;
|
|
41
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export const SLACK_THREAD_STATUS_HEARTBEAT_MS = 90_000;
|
|
2
|
+
export const SLACK_THREAD_LOADING_MESSAGES = [
|
|
3
|
+
"Schlepping...",
|
|
4
|
+
"Combobulating...",
|
|
5
|
+
"Vibing...",
|
|
6
|
+
"Noodling...",
|
|
7
|
+
"Percolating...",
|
|
8
|
+
"Ruminating...",
|
|
9
|
+
"Consulting the void...",
|
|
10
|
+
"Asking the electrons...",
|
|
11
|
+
"Reticulating splines...",
|
|
12
|
+
"Consulting the rubber duck...",
|
|
13
|
+
];
|
|
14
|
+
export const DEFAULT_SLACK_THREAD_STATUS = "is thinking…";
|
|
15
|
+
export const SLACK_THREAD_SAFE_STATUSES = [
|
|
16
|
+
DEFAULT_SLACK_THREAD_STATUS,
|
|
17
|
+
"Reading context…",
|
|
18
|
+
"Calling tool…",
|
|
19
|
+
"Drafting reply…",
|
|
20
|
+
"Checking Slack…",
|
|
21
|
+
];
|
|
22
|
+
export async function setSlackThreadStatus(input) {
|
|
23
|
+
await input.slack("assistant.threads.setStatus", input.token, {
|
|
24
|
+
channel_id: input.channelId,
|
|
25
|
+
thread_ts: input.threadTs,
|
|
26
|
+
status: input.status,
|
|
27
|
+
...(input.status.length > 0
|
|
28
|
+
? { loading_messages: [...(input.loadingMessages ?? SLACK_THREAD_LOADING_MESSAGES)] }
|
|
29
|
+
: {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function statusKey(channelId, threadTs) {
|
|
33
|
+
return `${channelId}:${threadTs}`;
|
|
34
|
+
}
|
|
35
|
+
export function normalizeSlackThreadStatus(status) {
|
|
36
|
+
const normalized = (status ?? "")
|
|
37
|
+
.replace(/[\r\n]+/g, " ")
|
|
38
|
+
.replace(/\s+/g, " ")
|
|
39
|
+
.trim();
|
|
40
|
+
if (!normalized)
|
|
41
|
+
return DEFAULT_SLACK_THREAD_STATUS;
|
|
42
|
+
return (SLACK_THREAD_SAFE_STATUSES.find((candidate) => candidate === normalized) ??
|
|
43
|
+
DEFAULT_SLACK_THREAD_STATUS);
|
|
44
|
+
}
|
|
45
|
+
export class SlackThreadStatusManager {
|
|
46
|
+
deps;
|
|
47
|
+
active = new Map();
|
|
48
|
+
constructor(deps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
}
|
|
51
|
+
async begin(channelId, threadTs, status = DEFAULT_SLACK_THREAD_STATUS) {
|
|
52
|
+
await this.update(channelId, threadTs, status, { startHeartbeat: true });
|
|
53
|
+
}
|
|
54
|
+
async update(channelId, threadTs, status, options = {}) {
|
|
55
|
+
const normalized = normalizeSlackThreadStatus(status);
|
|
56
|
+
const key = statusKey(channelId, threadTs);
|
|
57
|
+
let entry = this.active.get(key);
|
|
58
|
+
if (!entry) {
|
|
59
|
+
this.evictOldestIfNeeded();
|
|
60
|
+
entry = {
|
|
61
|
+
channelId,
|
|
62
|
+
threadTs,
|
|
63
|
+
status: normalized,
|
|
64
|
+
timer: null,
|
|
65
|
+
heartbeatInFlight: false,
|
|
66
|
+
consecutiveFailures: 0,
|
|
67
|
+
};
|
|
68
|
+
this.active.set(key, entry);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
entry.status = normalized;
|
|
72
|
+
}
|
|
73
|
+
if (options.startHeartbeat && !entry.timer) {
|
|
74
|
+
const setIntervalFn = this.deps.setInterval ?? setInterval;
|
|
75
|
+
entry.timer = setIntervalFn(() => {
|
|
76
|
+
void this.heartbeat(entry);
|
|
77
|
+
}, this.deps.heartbeatMs ?? SLACK_THREAD_STATUS_HEARTBEAT_MS);
|
|
78
|
+
}
|
|
79
|
+
await this.trySet(entry, normalized);
|
|
80
|
+
}
|
|
81
|
+
async clear(channelId, threadTs) {
|
|
82
|
+
const key = statusKey(channelId, threadTs);
|
|
83
|
+
const entry = this.active.get(key);
|
|
84
|
+
if (entry?.timer) {
|
|
85
|
+
const clearIntervalFn = this.deps.clearInterval ?? clearInterval;
|
|
86
|
+
clearIntervalFn(entry.timer);
|
|
87
|
+
}
|
|
88
|
+
this.active.delete(key);
|
|
89
|
+
await this.trySet({
|
|
90
|
+
channelId,
|
|
91
|
+
threadTs,
|
|
92
|
+
status: "",
|
|
93
|
+
timer: null,
|
|
94
|
+
heartbeatInFlight: false,
|
|
95
|
+
consecutiveFailures: 0,
|
|
96
|
+
}, "");
|
|
97
|
+
}
|
|
98
|
+
async clearAll() {
|
|
99
|
+
const entries = [...this.active.values()];
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
await this.clear(entry.channelId, entry.threadTs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async heartbeat(entry) {
|
|
105
|
+
if (entry.heartbeatInFlight)
|
|
106
|
+
return;
|
|
107
|
+
entry.heartbeatInFlight = true;
|
|
108
|
+
try {
|
|
109
|
+
await this.trySet(entry, entry.status);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
entry.heartbeatInFlight = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
evictOldestIfNeeded() {
|
|
116
|
+
const maxActiveThreads = this.deps.maxActiveThreads ?? 100;
|
|
117
|
+
if (this.active.size < maxActiveThreads)
|
|
118
|
+
return;
|
|
119
|
+
const oldest = this.active.entries().next().value;
|
|
120
|
+
if (!oldest)
|
|
121
|
+
return;
|
|
122
|
+
const [key, entry] = oldest;
|
|
123
|
+
if (entry.timer) {
|
|
124
|
+
const clearIntervalFn = this.deps.clearInterval ?? clearInterval;
|
|
125
|
+
clearIntervalFn(entry.timer);
|
|
126
|
+
}
|
|
127
|
+
this.active.delete(key);
|
|
128
|
+
}
|
|
129
|
+
stopHeartbeat(entry) {
|
|
130
|
+
if (entry.timer) {
|
|
131
|
+
const clearIntervalFn = this.deps.clearInterval ?? clearInterval;
|
|
132
|
+
clearIntervalFn(entry.timer);
|
|
133
|
+
entry.timer = null;
|
|
134
|
+
}
|
|
135
|
+
this.active.delete(statusKey(entry.channelId, entry.threadTs));
|
|
136
|
+
}
|
|
137
|
+
async trySet(entry, status) {
|
|
138
|
+
try {
|
|
139
|
+
await setSlackThreadStatus({
|
|
140
|
+
slack: this.deps.slack,
|
|
141
|
+
token: this.deps.getBotToken(),
|
|
142
|
+
channelId: entry.channelId,
|
|
143
|
+
threadTs: entry.threadTs,
|
|
144
|
+
status,
|
|
145
|
+
});
|
|
146
|
+
entry.consecutiveFailures = 0;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
entry.consecutiveFailures += 1;
|
|
150
|
+
(this.deps.logger ?? console).error(`[slack-bridge] Slack thread status update failed: ${this.deps.formatError(error)}`);
|
|
151
|
+
if (status && entry.consecutiveFailures >= (this.deps.maxConsecutiveFailures ?? 3)) {
|
|
152
|
+
this.stopHeartbeat(entry);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { InboxMessage } from "./helpers.js";
|
|
2
|
+
type SlackToolPolicyMessageRef = Pick<InboxMessage, "threadTs"> & Partial<Pick<InboxMessage, "channel">>;
|
|
3
|
+
import { type SecurityGuardrails } from "./guardrails.js";
|
|
4
|
+
export interface SlackToolPolicyRuntimeDeps {
|
|
5
|
+
getBrokerRole: () => "broker" | "follower" | null;
|
|
6
|
+
getGuardrails: () => SecurityGuardrails;
|
|
7
|
+
requireToolPolicy: (toolName: string, threadTs: string | undefined, action: string) => void;
|
|
8
|
+
formatAction: (action: string) => string;
|
|
9
|
+
formatError: (error: unknown) => string;
|
|
10
|
+
deliverFollowUpMessage: (prompt: string) => boolean;
|
|
11
|
+
beginThreadStatus?: (channel: string, threadTs: string, status: string) => Promise<void>;
|
|
12
|
+
updateThreadStatus?: (channel: string, threadTs: string, status: string) => Promise<void>;
|
|
13
|
+
clearThreadStatus?: (channel: string, threadTs: string) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface SlackToolPolicyRuntime {
|
|
16
|
+
deliverTrackedSlackFollowUpMessage: (options: {
|
|
17
|
+
prompt: string;
|
|
18
|
+
messages: SlackToolPolicyMessageRef[];
|
|
19
|
+
}) => boolean;
|
|
20
|
+
onInput: (event: {
|
|
21
|
+
source?: string;
|
|
22
|
+
text: string;
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
onTurnStart: () => Promise<void>;
|
|
25
|
+
onTurnEnd: () => Promise<void>;
|
|
26
|
+
onAgentEnd: () => Promise<void>;
|
|
27
|
+
onToolCall: (event: {
|
|
28
|
+
toolName: string;
|
|
29
|
+
input: Record<string, unknown>;
|
|
30
|
+
}) => Promise<{
|
|
31
|
+
block: true;
|
|
32
|
+
reason: string;
|
|
33
|
+
} | undefined>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createSlackToolPolicyRuntime(deps: SlackToolPolicyRuntimeDeps): SlackToolPolicyRuntime;
|
|
36
|
+
export {};
|