@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.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +570 -0
  3. package/dist/activity-log.d.ts +62 -0
  4. package/dist/activity-log.js +293 -0
  5. package/dist/agent-completion-runtime.d.ts +17 -0
  6. package/dist/agent-completion-runtime.js +26 -0
  7. package/dist/agent-event-runtime.d.ts +13 -0
  8. package/dist/agent-event-runtime.js +27 -0
  9. package/dist/agent-prompt-guidance.d.ts +22 -0
  10. package/dist/agent-prompt-guidance.js +46 -0
  11. package/dist/broker/adapters/slack.d.ts +74 -0
  12. package/dist/broker/adapters/slack.js +563 -0
  13. package/dist/broker/adapters/types.d.ts +1 -0
  14. package/dist/broker/adapters/types.js +1 -0
  15. package/dist/broker/agent-messaging.d.ts +1 -0
  16. package/dist/broker/agent-messaging.js +1 -0
  17. package/dist/broker/auth.d.ts +1 -0
  18. package/dist/broker/auth.js +1 -0
  19. package/dist/broker/client.d.ts +167 -0
  20. package/dist/broker/client.js +624 -0
  21. package/dist/broker/control-plane-dashboard.d.ts +98 -0
  22. package/dist/broker/control-plane-dashboard.js +213 -0
  23. package/dist/broker/ghost-reaper.d.ts +49 -0
  24. package/dist/broker/ghost-reaper.js +209 -0
  25. package/dist/broker/index.d.ts +36 -0
  26. package/dist/broker/index.js +90 -0
  27. package/dist/broker/leader.d.ts +1 -0
  28. package/dist/broker/leader.js +1 -0
  29. package/dist/broker/maintenance.d.ts +1 -0
  30. package/dist/broker/maintenance.js +1 -0
  31. package/dist/broker/message-send.d.ts +1 -0
  32. package/dist/broker/message-send.js +1 -0
  33. package/dist/broker/paths.d.ts +1 -0
  34. package/dist/broker/paths.js +1 -0
  35. package/dist/broker/raw-tcp-loopback.d.ts +1 -0
  36. package/dist/broker/raw-tcp-loopback.js +1 -0
  37. package/dist/broker/router.d.ts +1 -0
  38. package/dist/broker/router.js +1 -0
  39. package/dist/broker/schema.d.ts +9 -0
  40. package/dist/broker/schema.js +63 -0
  41. package/dist/broker/socket-server.d.ts +120 -0
  42. package/dist/broker/socket-server.js +1087 -0
  43. package/dist/broker/types.d.ts +1 -0
  44. package/dist/broker/types.js +1 -0
  45. package/dist/broker-delivery.d.ts +10 -0
  46. package/dist/broker-delivery.js +26 -0
  47. package/dist/broker-inbound-persistence.d.ts +11 -0
  48. package/dist/broker-inbound-persistence.js +22 -0
  49. package/dist/broker-prompt-loader.d.ts +34 -0
  50. package/dist/broker-prompt-loader.js +185 -0
  51. package/dist/broker-runtime-access.d.ts +14 -0
  52. package/dist/broker-runtime-access.js +20 -0
  53. package/dist/broker-runtime.d.ts +100 -0
  54. package/dist/broker-runtime.js +533 -0
  55. package/dist/broker-thread-owner-hints.d.ts +10 -0
  56. package/dist/broker-thread-owner-hints.js +14 -0
  57. package/dist/canvases.d.ts +80 -0
  58. package/dist/canvases.js +221 -0
  59. package/dist/command-registration-runtime.d.ts +9 -0
  60. package/dist/command-registration-runtime.js +9 -0
  61. package/dist/core-tool-guardrails.d.ts +24 -0
  62. package/dist/core-tool-guardrails.js +66 -0
  63. package/dist/deploy-manifest.d.ts +27 -0
  64. package/dist/deploy-manifest.js +197 -0
  65. package/dist/follower-delivery.d.ts +16 -0
  66. package/dist/follower-delivery.js +70 -0
  67. package/dist/follower-runtime.d.ts +61 -0
  68. package/dist/follower-runtime.js +350 -0
  69. package/dist/git-metadata.d.ts +35 -0
  70. package/dist/git-metadata.js +88 -0
  71. package/dist/guardrails.d.ts +69 -0
  72. package/dist/guardrails.js +283 -0
  73. package/dist/helpers.d.ts +550 -0
  74. package/dist/helpers.js +2711 -0
  75. package/dist/home-tab.d.ts +27 -0
  76. package/dist/home-tab.js +242 -0
  77. package/dist/imessage-tools.d.ts +33 -0
  78. package/dist/imessage-tools.js +96 -0
  79. package/dist/inbox-drain-runtime.d.ts +30 -0
  80. package/dist/inbox-drain-runtime.js +71 -0
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +1330 -0
  83. package/dist/persisted-runtime-state.d.ts +52 -0
  84. package/dist/persisted-runtime-state.js +98 -0
  85. package/dist/pinet-activity-formatting.d.ts +20 -0
  86. package/dist/pinet-activity-formatting.js +43 -0
  87. package/dist/pinet-agent-status.d.ts +36 -0
  88. package/dist/pinet-agent-status.js +48 -0
  89. package/dist/pinet-commands.d.ts +70 -0
  90. package/dist/pinet-commands.js +402 -0
  91. package/dist/pinet-confirmation-replies.d.ts +7 -0
  92. package/dist/pinet-confirmation-replies.js +30 -0
  93. package/dist/pinet-control-plane-dashboard.d.ts +34 -0
  94. package/dist/pinet-control-plane-dashboard.js +86 -0
  95. package/dist/pinet-home-tabs.d.ts +36 -0
  96. package/dist/pinet-home-tabs.js +101 -0
  97. package/dist/pinet-maintenance-delivery.d.ts +22 -0
  98. package/dist/pinet-maintenance-delivery.js +45 -0
  99. package/dist/pinet-mesh-ops.d.ts +97 -0
  100. package/dist/pinet-mesh-ops.js +266 -0
  101. package/dist/pinet-registration-gate.d.ts +14 -0
  102. package/dist/pinet-registration-gate.js +42 -0
  103. package/dist/pinet-remote-control-acks.d.ts +16 -0
  104. package/dist/pinet-remote-control-acks.js +46 -0
  105. package/dist/pinet-remote-control.d.ts +17 -0
  106. package/dist/pinet-remote-control.js +74 -0
  107. package/dist/pinet-runtime-composition.d.ts +27 -0
  108. package/dist/pinet-runtime-composition.js +20 -0
  109. package/dist/pinet-tools.d.ts +67 -0
  110. package/dist/pinet-tools.js +1211 -0
  111. package/dist/prompts/broker/default.md +41 -0
  112. package/dist/prompts/broker/tmux.md +49 -0
  113. package/dist/ralph-loop.d.ts +76 -0
  114. package/dist/ralph-loop.js +524 -0
  115. package/dist/reaction-triggers.d.ts +25 -0
  116. package/dist/reaction-triggers.js +171 -0
  117. package/dist/repo-tool-guardrails.d.ts +20 -0
  118. package/dist/repo-tool-guardrails.js +81 -0
  119. package/dist/runtime-agent-context.d.ts +93 -0
  120. package/dist/runtime-agent-context.js +302 -0
  121. package/dist/runtime-mode.d.ts +10 -0
  122. package/dist/runtime-mode.js +39 -0
  123. package/dist/scheduled-wakeups.d.ts +1 -0
  124. package/dist/scheduled-wakeups.js +1 -0
  125. package/dist/session-ui-runtime.d.ts +20 -0
  126. package/dist/session-ui-runtime.js +163 -0
  127. package/dist/single-player-runtime.d.ts +71 -0
  128. package/dist/single-player-runtime.js +436 -0
  129. package/dist/skins/cosmere.json +1571 -0
  130. package/dist/skins/foundation.json +1304 -0
  131. package/dist/slack-access.d.ts +186 -0
  132. package/dist/slack-access.js +462 -0
  133. package/dist/slack-api.d.ts +2 -0
  134. package/dist/slack-api.js +1 -0
  135. package/dist/slack-block-kit.d.ts +36 -0
  136. package/dist/slack-block-kit.js +242 -0
  137. package/dist/slack-export.d.ts +31 -0
  138. package/dist/slack-export.js +206 -0
  139. package/dist/slack-message-context.d.ts +15 -0
  140. package/dist/slack-message-context.js +207 -0
  141. package/dist/slack-modals.d.ts +13 -0
  142. package/dist/slack-modals.js +79 -0
  143. package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
  144. package/dist/slack-pinet-runtime-adapter.js +72 -0
  145. package/dist/slack-presence.d.ts +36 -0
  146. package/dist/slack-presence.js +89 -0
  147. package/dist/slack-request-runtime.d.ts +8 -0
  148. package/dist/slack-request-runtime.js +19 -0
  149. package/dist/slack-runtime-access.d.ts +28 -0
  150. package/dist/slack-runtime-access.js +107 -0
  151. package/dist/slack-scope-diagnostics.d.ts +31 -0
  152. package/dist/slack-scope-diagnostics.js +235 -0
  153. package/dist/slack-socket-dedup.d.ts +7 -0
  154. package/dist/slack-socket-dedup.js +135 -0
  155. package/dist/slack-thread-status.d.ts +41 -0
  156. package/dist/slack-thread-status.js +156 -0
  157. package/dist/slack-tool-policy-runtime.d.ts +36 -0
  158. package/dist/slack-tool-policy-runtime.js +96 -0
  159. package/dist/slack-tools.d.ts +52 -0
  160. package/dist/slack-tools.js +2688 -0
  161. package/dist/slack-turn-guardrails.d.ts +19 -0
  162. package/dist/slack-turn-guardrails.js +36 -0
  163. package/dist/slack-upload.d.ts +49 -0
  164. package/dist/slack-upload.js +224 -0
  165. package/dist/task-assignments.d.ts +45 -0
  166. package/dist/task-assignments.js +527 -0
  167. package/dist/thread-confirmations.d.ts +22 -0
  168. package/dist/thread-confirmations.js +139 -0
  169. package/dist/tool-registration-runtime.d.ts +13 -0
  170. package/dist/tool-registration-runtime.js +13 -0
  171. package/dist/ttl-cache.d.ts +47 -0
  172. package/dist/ttl-cache.js +105 -0
  173. package/manifest.yaml +57 -0
  174. package/package.json +60 -0
  175. package/skills/pinet-skin-creator/SKILL.md +109 -0
  176. package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
  177. package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
  178. package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
  179. package/skills/slack-bridge/SKILL.md +360 -0
@@ -0,0 +1,221 @@
1
+ function normalizeOptionalString(value) {
2
+ const trimmed = value?.trim();
3
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
4
+ }
5
+ function buildDocumentContent(markdown) {
6
+ if (markdown == null || markdown.length === 0)
7
+ return undefined;
8
+ return { type: "markdown", markdown };
9
+ }
10
+ function asRecord(value) {
11
+ return typeof value === "object" && value !== null ? value : null;
12
+ }
13
+ function asString(value) {
14
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
15
+ }
16
+ function asNumber(value) {
17
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
18
+ }
19
+ function asStringifiedNumber(value) {
20
+ if (typeof value === "string" && value.trim().length > 0) {
21
+ return value.trim();
22
+ }
23
+ if (typeof value === "number" && Number.isFinite(value)) {
24
+ return String(value);
25
+ }
26
+ return undefined;
27
+ }
28
+ function asStringArray(value) {
29
+ if (!Array.isArray(value))
30
+ return undefined;
31
+ const strings = value.map(asString).filter((item) => Boolean(item));
32
+ return strings.length > 0 ? strings : undefined;
33
+ }
34
+ export function normalizeSlackCanvasCreateKind(kind) {
35
+ const normalized = kind?.trim().toLowerCase();
36
+ if (!normalized)
37
+ return "standalone";
38
+ if (normalized === "standalone" || normalized === "channel") {
39
+ return normalized;
40
+ }
41
+ throw new Error("Unsupported canvas kind. Use 'standalone' or 'channel'.");
42
+ }
43
+ export function normalizeSlackCanvasUpdateMode(mode) {
44
+ const normalized = mode?.trim().toLowerCase();
45
+ if (!normalized)
46
+ return "append";
47
+ if (normalized === "append" || normalized === "prepend" || normalized === "replace") {
48
+ return normalized;
49
+ }
50
+ throw new Error("Unsupported canvas update mode. Use 'append', 'prepend', or 'replace'.");
51
+ }
52
+ export function normalizeSlackCanvasSectionType(type) {
53
+ const normalized = type?.trim().toLowerCase();
54
+ if (!normalized)
55
+ return undefined;
56
+ if (normalized === "h1" ||
57
+ normalized === "h2" ||
58
+ normalized === "h3" ||
59
+ normalized === "any_header") {
60
+ return normalized;
61
+ }
62
+ throw new Error("Unsupported canvas section type. Use 'h1', 'h2', 'h3', or 'any_header'.");
63
+ }
64
+ export function normalizeSlackCanvasCommentsLimit(limit) {
65
+ if (limit == null)
66
+ return 20;
67
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
68
+ throw new Error("Canvas comment reads require limit to be an integer between 1 and 200.");
69
+ }
70
+ return limit;
71
+ }
72
+ export function buildSlackCanvasCreateRequest(input) {
73
+ const kind = normalizeSlackCanvasCreateKind(input.kind);
74
+ const channelId = normalizeOptionalString(input.channelId);
75
+ const title = normalizeOptionalString(input.title);
76
+ const documentContent = buildDocumentContent(input.markdown);
77
+ if (kind === "channel") {
78
+ if (!channelId) {
79
+ throw new Error("Channel canvases require a channel.");
80
+ }
81
+ return {
82
+ kind,
83
+ method: "conversations.canvases.create",
84
+ body: {
85
+ ...(title ? { title } : {}),
86
+ ...(documentContent ? { document_content: documentContent } : {}),
87
+ channel_id: channelId,
88
+ },
89
+ };
90
+ }
91
+ return {
92
+ kind,
93
+ method: "canvases.create",
94
+ body: {
95
+ ...(title ? { title } : {}),
96
+ ...(documentContent ? { document_content: documentContent } : {}),
97
+ ...(channelId ? { channel_id: channelId } : {}),
98
+ },
99
+ };
100
+ }
101
+ export function buildSlackCanvasEditRequest(input) {
102
+ const canvasId = normalizeOptionalString(input.canvasId);
103
+ if (!canvasId) {
104
+ throw new Error("Canvas updates require a canvas ID.");
105
+ }
106
+ if (input.markdown.length === 0) {
107
+ throw new Error("Canvas updates require markdown content.");
108
+ }
109
+ const sectionId = normalizeOptionalString(input.sectionId);
110
+ const mode = normalizeSlackCanvasUpdateMode(input.mode);
111
+ const operation = mode === "append" ? "insert_at_end" : mode === "prepend" ? "insert_at_start" : "replace";
112
+ return {
113
+ canvas_id: canvasId,
114
+ changes: [
115
+ {
116
+ operation,
117
+ document_content: { type: "markdown", markdown: input.markdown },
118
+ ...(sectionId ? { section_id: sectionId } : {}),
119
+ },
120
+ ],
121
+ };
122
+ }
123
+ export function buildSlackCanvasSectionsLookupRequest(input) {
124
+ const canvasId = normalizeOptionalString(input.canvasId);
125
+ if (!canvasId) {
126
+ throw new Error("Canvas section lookups require a canvas ID.");
127
+ }
128
+ const containsText = normalizeOptionalString(input.containsText);
129
+ if (!containsText) {
130
+ throw new Error("Canvas section lookups require text to match.");
131
+ }
132
+ const sectionType = normalizeSlackCanvasSectionType(input.sectionType);
133
+ return {
134
+ canvas_id: canvasId,
135
+ criteria: {
136
+ contains_text: containsText,
137
+ ...(sectionType ? { section_types: [sectionType] } : {}),
138
+ },
139
+ };
140
+ }
141
+ export function pickSlackCanvasSectionId(sections, sectionIndex) {
142
+ const matches = (sections ?? [])
143
+ .map((section) => normalizeOptionalString(section.id))
144
+ .filter((id) => Boolean(id));
145
+ if (matches.length === 0) {
146
+ throw new Error("No canvas sections matched the lookup.");
147
+ }
148
+ if (sectionIndex != null) {
149
+ if (!Number.isInteger(sectionIndex) || sectionIndex < 1) {
150
+ throw new Error("section_index must be a positive integer.");
151
+ }
152
+ const selected = matches[sectionIndex - 1];
153
+ if (!selected) {
154
+ throw new Error(`Canvas section lookup matched ${matches.length} sections; section_index ${sectionIndex} is out of range.`);
155
+ }
156
+ return selected;
157
+ }
158
+ if (matches.length > 1) {
159
+ throw new Error(`Canvas section lookup matched ${matches.length} sections. Provide section_index to choose one result or narrow the lookup.`);
160
+ }
161
+ return matches[0];
162
+ }
163
+ export function extractSlackChannelCanvasId(response) {
164
+ const channel = asRecord(response.channel);
165
+ const properties = asRecord(channel?.properties);
166
+ if (!properties)
167
+ return null;
168
+ const directCanvas = asString(properties.canvas);
169
+ if (directCanvas)
170
+ return directCanvas;
171
+ const canvasRecord = asRecord(properties.canvas);
172
+ const canvasId = asString(canvasRecord?.id) ?? asString(canvasRecord?.canvas_id);
173
+ if (canvasId)
174
+ return canvasId;
175
+ const channelSolutions = asRecord(properties.channel_solutions);
176
+ const canvasIds = asStringArray(channelSolutions?.canvas_ids);
177
+ if (canvasIds?.[0])
178
+ return canvasIds[0];
179
+ return null;
180
+ }
181
+ function extractSlackCanvasCommentText(comment) {
182
+ return (asString(comment.comment) ??
183
+ asString(comment.comment_text) ??
184
+ asString(comment.text) ??
185
+ asString(comment.plain_text) ??
186
+ "(no comment text exposed by Slack)");
187
+ }
188
+ export function extractSlackCanvasCommentsPage(response, fallbackCanvasId) {
189
+ const file = asRecord(response.file) ?? {};
190
+ const comments = Array.isArray(response.comments)
191
+ ? response.comments
192
+ : [];
193
+ const paging = asRecord(response.paging);
194
+ const responseMetadata = asRecord(response.response_metadata);
195
+ const canvasId = asString(file.id) ?? normalizeOptionalString(fallbackCanvasId);
196
+ if (!canvasId) {
197
+ throw new Error("Slack did not return a canvas/file id for this canvas comment read.");
198
+ }
199
+ const parsedComments = comments.map((comment) => ({
200
+ ...(asStringifiedNumber(comment.id) ? { id: asStringifiedNumber(comment.id) } : {}),
201
+ ...(asString(comment.user) ? { userId: asString(comment.user) } : {}),
202
+ ...(asStringifiedNumber(comment.created ?? comment.timestamp ?? comment.ts)
203
+ ? { createdTs: asStringifiedNumber(comment.created ?? comment.timestamp ?? comment.ts) }
204
+ : {}),
205
+ text: extractSlackCanvasCommentText(comment),
206
+ }));
207
+ const nextCursor = asString(responseMetadata?.next_cursor);
208
+ const totalFromPaging = asNumber(paging?.total);
209
+ const commentCount = asNumber(file.comments_count) ?? totalFromPaging ?? parsedComments.length;
210
+ return {
211
+ canvasId,
212
+ ...(asString(file.title) ? { title: asString(file.title) } : {}),
213
+ ...(asString(file.permalink) ? { permalink: asString(file.permalink) } : {}),
214
+ commentsCount: commentCount,
215
+ returnedCount: parsedComments.length,
216
+ ...(asNumber(paging?.page) ? { page: asNumber(paging?.page) } : {}),
217
+ ...(asNumber(paging?.pages) ? { pages: asNumber(paging?.pages) } : {}),
218
+ comments: parsedComments,
219
+ ...(nextCursor ? { nextCursor } : {}),
220
+ };
221
+ }
@@ -0,0 +1,9 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { type PinetCommandsDeps } from "./pinet-commands.js";
3
+ export interface CommandRegistrationRuntimeDeps {
4
+ pinetCommands: PinetCommandsDeps;
5
+ }
6
+ export interface CommandRegistrationRuntime {
7
+ register: (pi: ExtensionAPI) => void;
8
+ }
9
+ export declare function createCommandRegistrationRuntime(deps: CommandRegistrationRuntimeDeps): CommandRegistrationRuntime;
@@ -0,0 +1,9 @@
1
+ import { registerPinetCommands } from "./pinet-commands.js";
2
+ export function createCommandRegistrationRuntime(deps) {
3
+ function register(pi) {
4
+ registerPinetCommands(pi, deps.pinetCommands);
5
+ }
6
+ return {
7
+ register,
8
+ };
9
+ }
@@ -0,0 +1,24 @@
1
+ import { type SecurityGuardrails } from "./guardrails.js";
2
+ export interface SlackToolPolicyTurn {
3
+ threadTs: string | undefined;
4
+ threadCount: number;
5
+ }
6
+ export declare const GUARDED_CORE_TOOLS: Set<string>;
7
+ export declare function isGuardedCoreTool(toolName: string): boolean;
8
+ export declare function getGuardrailToolName(toolName: string): string;
9
+ export declare function summarizeCoreToolAction(event: {
10
+ toolName: string;
11
+ input: Record<string, unknown>;
12
+ }): string;
13
+ export declare function evaluateSlackOriginCoreToolPolicy(options: {
14
+ turn: SlackToolPolicyTurn | null;
15
+ toolName: string;
16
+ input: Record<string, unknown>;
17
+ guardrails: SecurityGuardrails;
18
+ requireToolPolicy: (toolName: string, threadTs: string | undefined, action: string) => void;
19
+ formatAction: (action: string) => string;
20
+ formatError: (error: unknown) => string;
21
+ }): {
22
+ block: true;
23
+ reason: string;
24
+ } | undefined;
@@ -0,0 +1,66 @@
1
+ import { isToolBlocked, toolNeedsConfirmation } from "./guardrails.js";
2
+ export const GUARDED_CORE_TOOLS = new Set(["bash", "read", "edit", "write", "grep", "find", "ls"]);
3
+ export function isGuardedCoreTool(toolName) {
4
+ return GUARDED_CORE_TOOLS.has(toolName);
5
+ }
6
+ export function getGuardrailToolName(toolName) {
7
+ return toolName === "grep" ? "rg" : toolName;
8
+ }
9
+ export function summarizeCoreToolAction(event) {
10
+ const input = event.input;
11
+ switch (event.toolName) {
12
+ case "bash":
13
+ return `command=${String(input.command ?? "")}`;
14
+ case "read":
15
+ return `path=${String(input.path ?? "")} | offset=${String(input.offset ?? "")} | limit=${String(input.limit ?? "")}`;
16
+ case "edit":
17
+ return `path=${String(input.path ?? "")} | edits=${Array.isArray(input.edits) ? input.edits.length : 0}`;
18
+ case "write": {
19
+ const content = typeof input.content === "string" ? input.content : "";
20
+ return `path=${String(input.path ?? "")} | content_length=${content.length}`;
21
+ }
22
+ case "grep":
23
+ return `pattern=${String(input.pattern ?? "")} | path=${String(input.path ?? "")} | glob=${String(input.glob ?? "")}`;
24
+ case "find":
25
+ return `pattern=${String(input.pattern ?? "")} | path=${String(input.path ?? "")} | limit=${String(input.limit ?? "")}`;
26
+ case "ls":
27
+ return `path=${String(input.path ?? "")} | limit=${String(input.limit ?? "")}`;
28
+ default:
29
+ return JSON.stringify(input);
30
+ }
31
+ }
32
+ export function evaluateSlackOriginCoreToolPolicy(options) {
33
+ const { turn, toolName, input, guardrails, requireToolPolicy, formatAction, formatError } = options;
34
+ if (!turn || !isGuardedCoreTool(toolName)) {
35
+ return undefined;
36
+ }
37
+ const guardrailToolName = getGuardrailToolName(toolName);
38
+ if (isToolBlocked(guardrailToolName, guardrails)) {
39
+ return {
40
+ block: true,
41
+ reason: `Tool "${guardrailToolName}" is blocked by Slack security guardrails.`,
42
+ };
43
+ }
44
+ if (!toolNeedsConfirmation(guardrailToolName, guardrails)) {
45
+ return undefined;
46
+ }
47
+ const action = summarizeCoreToolAction({ toolName, input });
48
+ if (!turn.threadTs) {
49
+ return {
50
+ block: true,
51
+ reason: turn.threadCount > 1
52
+ ? `Tool "${guardrailToolName}" requires Slack confirmation for action ${formatAction(action)}, but this Slack-triggered turn currently batches ${turn.threadCount} threads. Process one Slack thread at a time before using that tool.`
53
+ : `Tool "${guardrailToolName}" requires Slack confirmation for action ${formatAction(action)}, but there is no tracked Slack thread available for this turn. Retry from a specific Slack thread and call slack with action "confirm_action" there first.`,
54
+ };
55
+ }
56
+ try {
57
+ requireToolPolicy(guardrailToolName, turn.threadTs, action);
58
+ return undefined;
59
+ }
60
+ catch (error) {
61
+ return {
62
+ block: true,
63
+ reason: formatError(error),
64
+ };
65
+ }
66
+ }
@@ -0,0 +1,27 @@
1
+ export interface SlackBridgeSettings {
2
+ appId?: string;
3
+ appToken?: string;
4
+ appConfigToken?: string;
5
+ }
6
+ export interface ManifestScopeChanges {
7
+ addedBotScopes: string[];
8
+ removedBotScopes: string[];
9
+ addedUserScopes: string[];
10
+ removedUserScopes: string[];
11
+ }
12
+ export interface ResolvedDeployConfig {
13
+ manifestPath: string;
14
+ appId?: string;
15
+ appConfigToken?: string;
16
+ appToken?: string;
17
+ }
18
+ export interface DeployResult {
19
+ appId: string;
20
+ scopeChanges: ManifestScopeChanges;
21
+ }
22
+ export declare function diffManifestScopes(beforeManifest: Record<string, unknown> | null | undefined, afterManifest: Record<string, unknown> | null | undefined): ManifestScopeChanges;
23
+ export declare function formatScopeChangeSummary(changes: ManifestScopeChanges): string[];
24
+ export declare function resolveDeployConfig(settings: SlackBridgeSettings, env: NodeJS.ProcessEnv, cwd?: string): ResolvedDeployConfig;
25
+ export declare function getDeployConfigError(config: ResolvedDeployConfig): string | null;
26
+ export declare function deploySlackManifest(config: ResolvedDeployConfig): Promise<DeployResult>;
27
+ export declare function run(): Promise<void>;
@@ -0,0 +1,197 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { execFile as execFileCallback } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { pathToFileURL } from "node:url";
6
+ const execFile = promisify(execFileCallback);
7
+ class SlackMethodError extends Error {
8
+ method;
9
+ slackError;
10
+ details;
11
+ constructor(method, slackError, details = []) {
12
+ const detailMessage = details
13
+ .map((detail) => {
14
+ const pointer = detail.pointer ? `${detail.pointer}: ` : "";
15
+ return `- ${pointer}${detail.message ?? "unknown error"}`;
16
+ })
17
+ .join("\n");
18
+ super(detailMessage
19
+ ? `Slack ${method}: ${slackError}\n${detailMessage}`
20
+ : `Slack ${method}: ${slackError}`);
21
+ this.name = "SlackMethodError";
22
+ this.method = method;
23
+ this.slackError = slackError;
24
+ this.details = details;
25
+ }
26
+ }
27
+ function loadSettings(settingsPath) {
28
+ const resolvedPath = settingsPath ??
29
+ path.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".pi", "agent", "settings.json");
30
+ try {
31
+ const content = fs.readFileSync(resolvedPath, "utf-8");
32
+ const parsed = JSON.parse(content);
33
+ return (parsed["slack-bridge"] ??
34
+ {});
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ function buildSlackRequest(method, token, body) {
41
+ const headers = {
42
+ Authorization: `Bearer ${token}`,
43
+ };
44
+ let serialized;
45
+ if (body) {
46
+ headers["Content-Type"] = "application/json; charset=utf-8";
47
+ serialized = JSON.stringify(body);
48
+ }
49
+ return {
50
+ url: `https://slack.com/api/${method}`,
51
+ init: {
52
+ method: "POST",
53
+ headers,
54
+ ...(serialized ? { body: serialized } : {}),
55
+ },
56
+ };
57
+ }
58
+ function asRecord(value) {
59
+ return typeof value === "object" && value !== null ? value : null;
60
+ }
61
+ function readScopeList(manifest, scopeType) {
62
+ const oauthConfig = asRecord(manifest?.oauth_config);
63
+ const scopes = asRecord(oauthConfig?.scopes);
64
+ const raw = scopes?.[scopeType];
65
+ if (!Array.isArray(raw)) {
66
+ return [];
67
+ }
68
+ return [
69
+ ...new Set(raw.filter((item) => typeof item === "string" && item.length > 0)),
70
+ ].sort();
71
+ }
72
+ export function diffManifestScopes(beforeManifest, afterManifest) {
73
+ const beforeBot = new Set(readScopeList(beforeManifest, "bot"));
74
+ const afterBot = new Set(readScopeList(afterManifest, "bot"));
75
+ const beforeUser = new Set(readScopeList(beforeManifest, "user"));
76
+ const afterUser = new Set(readScopeList(afterManifest, "user"));
77
+ const diff = (next, prev) => [...next].filter((scope) => !prev.has(scope)).sort();
78
+ return {
79
+ addedBotScopes: diff(afterBot, beforeBot),
80
+ removedBotScopes: diff(beforeBot, afterBot),
81
+ addedUserScopes: diff(afterUser, beforeUser),
82
+ removedUserScopes: diff(beforeUser, afterUser),
83
+ };
84
+ }
85
+ export function formatScopeChangeSummary(changes) {
86
+ const lines = [];
87
+ if (changes.addedBotScopes.length > 0) {
88
+ lines.push(`Bot scopes added: ${changes.addedBotScopes.join(", ")}`);
89
+ }
90
+ if (changes.removedBotScopes.length > 0) {
91
+ lines.push(`Bot scopes removed: ${changes.removedBotScopes.join(", ")}`);
92
+ }
93
+ if (changes.addedUserScopes.length > 0) {
94
+ lines.push(`User scopes added: ${changes.addedUserScopes.join(", ")}`);
95
+ }
96
+ if (changes.removedUserScopes.length > 0) {
97
+ lines.push(`User scopes removed: ${changes.removedUserScopes.join(", ")}`);
98
+ }
99
+ return lines.length > 0 ? lines : ["No scope changes."];
100
+ }
101
+ export function resolveDeployConfig(settings, env, cwd = process.cwd()) {
102
+ return {
103
+ manifestPath: path.join(cwd, "slack-bridge", "manifest.yaml"),
104
+ appId: settings.appId ?? env.SLACK_APP_ID,
105
+ appConfigToken: settings.appConfigToken ?? env.SLACK_APP_CONFIG_TOKEN ?? env.SLACK_CONFIG_TOKEN,
106
+ appToken: settings.appToken ?? env.SLACK_APP_TOKEN,
107
+ };
108
+ }
109
+ export function getDeployConfigError(config) {
110
+ if (!fs.existsSync(config.manifestPath)) {
111
+ return `Slack manifest not found at ${config.manifestPath}`;
112
+ }
113
+ const messages = [];
114
+ if (!config.appId) {
115
+ messages.push("Missing Slack app ID. Set slack-bridge.appId in ~/.pi/agent/settings.json or SLACK_APP_ID.");
116
+ }
117
+ if (!config.appConfigToken) {
118
+ const appTokenNote = config.appToken
119
+ ? " Note: slack-bridge.appToken / SLACK_APP_TOKEN is a Socket Mode xapp token and cannot be used with apps.manifest.update."
120
+ : "";
121
+ messages.push("Missing Slack app configuration token. Set slack-bridge.appConfigToken in ~/.pi/agent/settings.json or SLACK_APP_CONFIG_TOKEN (or SLACK_CONFIG_TOKEN)." +
122
+ appTokenNote);
123
+ }
124
+ return messages.length > 0 ? messages.join(" ") : null;
125
+ }
126
+ async function parseManifestYaml(manifestPath) {
127
+ try {
128
+ const program = [
129
+ "require 'yaml'",
130
+ "require 'json'",
131
+ "data = YAML.load_file(ARGV[0])",
132
+ "puts JSON.generate(data)",
133
+ ].join("; ");
134
+ const { stdout } = await execFile("ruby", ["-e", program, manifestPath]);
135
+ return JSON.parse(stdout);
136
+ }
137
+ catch (error) {
138
+ throw new Error(`Failed to parse ${manifestPath} via Ruby YAML parser. Ensure Ruby is installed and the manifest is valid YAML. ${String(error)}`);
139
+ }
140
+ }
141
+ async function callSlackMethod(method, token, body) {
142
+ const { url, init } = buildSlackRequest(method, token, body);
143
+ const response = await fetch(url, init);
144
+ const payload = (await response.json());
145
+ if (!response.ok || payload.ok !== true) {
146
+ throw new SlackMethodError(method, payload.error ?? `http_${response.status}`, payload.errors ?? []);
147
+ }
148
+ return payload;
149
+ }
150
+ async function exportRemoteManifest(appId, token) {
151
+ const payload = await callSlackMethod("apps.manifest.export", token, { app_id: appId });
152
+ return payload.manifest;
153
+ }
154
+ async function validateManifest(manifest, token) {
155
+ await callSlackMethod("apps.manifest.validate", token, {
156
+ manifest: JSON.stringify(manifest),
157
+ });
158
+ }
159
+ async function updateManifest(appId, manifest, token) {
160
+ await callSlackMethod("apps.manifest.update", token, {
161
+ app_id: appId,
162
+ manifest: JSON.stringify(manifest),
163
+ });
164
+ }
165
+ export async function deploySlackManifest(config) {
166
+ const configError = getDeployConfigError(config);
167
+ if (configError) {
168
+ throw new Error(configError);
169
+ }
170
+ const appId = config.appId;
171
+ const appConfigToken = config.appConfigToken;
172
+ const manifest = await parseManifestYaml(config.manifestPath);
173
+ const previousManifest = await exportRemoteManifest(appId, appConfigToken);
174
+ await validateManifest(manifest, appConfigToken);
175
+ await updateManifest(appId, manifest, appConfigToken);
176
+ const updatedManifest = await exportRemoteManifest(appId, appConfigToken);
177
+ return {
178
+ appId,
179
+ scopeChanges: diffManifestScopes(previousManifest, updatedManifest),
180
+ };
181
+ }
182
+ export async function run() {
183
+ const settingsPath = process.env.PI_SETTINGS_PATH;
184
+ const settings = loadSettings(settingsPath);
185
+ const config = resolveDeployConfig(settings, process.env);
186
+ const result = await deploySlackManifest(config);
187
+ console.log(`Updated Slack app ${result.appId} from ${config.manifestPath}`);
188
+ for (const line of formatScopeChangeSummary(result.scopeChanges)) {
189
+ console.log(line);
190
+ }
191
+ }
192
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
193
+ run().catch((error) => {
194
+ console.error(error instanceof Error ? error.message : String(error));
195
+ process.exitCode = 1;
196
+ });
197
+ }
@@ -0,0 +1,16 @@
1
+ export interface FollowerDeliveryState {
2
+ queuedButUndeliveredIds: Set<number>;
3
+ deliveredAwaitingAckIds: Set<number>;
4
+ ackInFlightIds: Set<number>;
5
+ }
6
+ export declare function createFollowerDeliveryState(): FollowerDeliveryState;
7
+ export declare function resetFollowerDeliveryState(state: FollowerDeliveryState): void;
8
+ export declare function isFollowerInboxIdTracked(state: FollowerDeliveryState, inboxId: number): boolean;
9
+ export declare function queueFollowerInboxIds(state: FollowerDeliveryState, inboxIds: Iterable<number>): void;
10
+ export declare function markFollowerInboxIdsDelivered(state: FollowerDeliveryState, inboxIds: Iterable<number>): void;
11
+ export declare function hasDeliveredFollowerInboxIds(state: FollowerDeliveryState): boolean;
12
+ export declare function takeFollowerAckBatch(state: FollowerDeliveryState): number[];
13
+ export declare function markFollowerAckBatchSucceeded(state: FollowerDeliveryState, inboxIds: Iterable<number>): void;
14
+ export declare function markFollowerAckBatchFailed(state: FollowerDeliveryState, inboxIds: Iterable<number>): void;
15
+ export declare function drainFollowerAckBatches(state: FollowerDeliveryState, ackBatch: (inboxIds: number[]) => Promise<void>): Promise<void>;
16
+ export declare function getFollowerShutdownAckIds(state: FollowerDeliveryState): number[];
@@ -0,0 +1,70 @@
1
+ export function createFollowerDeliveryState() {
2
+ return {
3
+ queuedButUndeliveredIds: new Set(),
4
+ deliveredAwaitingAckIds: new Set(),
5
+ ackInFlightIds: new Set(),
6
+ };
7
+ }
8
+ export function resetFollowerDeliveryState(state) {
9
+ state.queuedButUndeliveredIds.clear();
10
+ state.deliveredAwaitingAckIds.clear();
11
+ state.ackInFlightIds.clear();
12
+ }
13
+ export function isFollowerInboxIdTracked(state, inboxId) {
14
+ return (state.queuedButUndeliveredIds.has(inboxId) ||
15
+ state.deliveredAwaitingAckIds.has(inboxId) ||
16
+ state.ackInFlightIds.has(inboxId));
17
+ }
18
+ export function queueFollowerInboxIds(state, inboxIds) {
19
+ for (const inboxId of inboxIds) {
20
+ state.queuedButUndeliveredIds.add(inboxId);
21
+ }
22
+ }
23
+ export function markFollowerInboxIdsDelivered(state, inboxIds) {
24
+ for (const inboxId of inboxIds) {
25
+ state.queuedButUndeliveredIds.delete(inboxId);
26
+ state.deliveredAwaitingAckIds.add(inboxId);
27
+ }
28
+ }
29
+ export function hasDeliveredFollowerInboxIds(state) {
30
+ return state.deliveredAwaitingAckIds.size > 0;
31
+ }
32
+ export function takeFollowerAckBatch(state) {
33
+ const batch = [...state.deliveredAwaitingAckIds];
34
+ for (const inboxId of batch) {
35
+ state.deliveredAwaitingAckIds.delete(inboxId);
36
+ state.ackInFlightIds.add(inboxId);
37
+ }
38
+ return batch;
39
+ }
40
+ export function markFollowerAckBatchSucceeded(state, inboxIds) {
41
+ for (const inboxId of inboxIds) {
42
+ state.ackInFlightIds.delete(inboxId);
43
+ }
44
+ }
45
+ export function markFollowerAckBatchFailed(state, inboxIds) {
46
+ for (const inboxId of inboxIds) {
47
+ if (state.ackInFlightIds.delete(inboxId)) {
48
+ state.deliveredAwaitingAckIds.add(inboxId);
49
+ }
50
+ }
51
+ }
52
+ export async function drainFollowerAckBatches(state, ackBatch) {
53
+ while (true) {
54
+ const batch = takeFollowerAckBatch(state);
55
+ if (batch.length === 0) {
56
+ return;
57
+ }
58
+ try {
59
+ await ackBatch(batch);
60
+ markFollowerAckBatchSucceeded(state, batch);
61
+ }
62
+ catch {
63
+ markFollowerAckBatchFailed(state, batch);
64
+ return;
65
+ }
66
+ }
67
+ }
68
+ export function getFollowerShutdownAckIds(state) {
69
+ return [...state.deliveredAwaitingAckIds];
70
+ }