@pinet/slack-bridge 0.2.2 → 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 +295 -455
- package/dist/broker/socket-server.d.ts +1 -0
- package/dist/broker/socket-server.js +53 -0
- package/dist/index.js +1 -0
- package/dist/pinet-mesh-ops.js +1 -0
- package/dist/slack-tools.d.ts +8 -0
- package/dist/slack-tools.js +26 -49
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,157 +1,185 @@
|
|
|
1
1
|
# slack-bridge (Pinet)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect pi coding agents to Slack. Pinet provides multi-agent coordination, thread routing, and inbox tools through Socket Mode.
|
|
4
4
|
|
|
5
|
-
## Install
|
|
5
|
+
## Install Pinet
|
|
6
6
|
|
|
7
|
-
Install the latest
|
|
7
|
+
Install the latest version:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pi install npm:@pinet/slack-bridge
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Pin a specific version:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
pi install npm:@pinet/slack-bridge@0.
|
|
16
|
+
pi install npm:@pinet/slack-bridge@0.2.2
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
For
|
|
19
|
+
For direct npm installation:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
npm install @pinet/slack-bridge
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## What you need
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
- a Slack workspace where you can install apps
|
|
28
|
+
- Node.js 22 or later
|
|
29
|
+
- pi installed on your system
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
- `pi.extensions` points at the built extension entrypoint, `./dist/index.js`.
|
|
31
|
-
- `pi.skills` points at the bundled skill directory, `./skills`.
|
|
32
|
-
- No `pi.image` or `pi.video` preview is declared yet because this package does
|
|
33
|
-
not currently include a reviewed gallery image/video asset.
|
|
31
|
+
## Set up your Slack app
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
Slack app manifest, built `dist/` files, bundled `skills/`, and LICENSE. Verify
|
|
37
|
-
that locally with:
|
|
33
|
+
### Create the app
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
workflow's default dry-run/readiness path for validation; do not publish, tag, or
|
|
47
|
-
bump versions without explicit maintainer release approval.
|
|
48
|
-
|
|
49
|
-
## Prerequisites
|
|
50
|
-
|
|
51
|
-
- A Slack workspace where you have permission to install apps
|
|
52
|
-
- Node.js 22+ (uses native `fetch` and `WebSocket`)
|
|
53
|
-
- [pi](https://github.com/badlogic/pi-mono) installed
|
|
54
|
-
|
|
55
|
-
## Slack App Setup
|
|
35
|
+
1. Go to [api.slack.com/apps](https://api.slack.com/apps)
|
|
36
|
+
2. Select 'Create New App'
|
|
37
|
+
3. Choose 'From a manifest'
|
|
38
|
+
4. Select your workspace
|
|
39
|
+
5. Paste the contents of [`manifest.yaml`](./manifest.yaml)
|
|
40
|
+
6. If you want a different Slack command, change `features.slash_commands[0].command` before creating and set `slackCommandName` or `slackCommandNames` in `settings.json`
|
|
41
|
+
7. Select 'Create'
|
|
56
42
|
|
|
57
|
-
|
|
43
|
+
The manifest configures Socket Mode, the assistant view, bot scopes, event subscriptions, and slash commands automatically.
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
2. Choose **From a manifest**
|
|
61
|
-
3. Select your workspace
|
|
62
|
-
4. Paste the contents of [`manifest.yaml`](./manifest.yaml) from this directory
|
|
63
|
-
5. If the Slack app is not named Pinet, change `features.slash_commands[0].command` before creating the app (for example, Oathgate uses `/oathgate` instead of the packaged `/pinet` default)
|
|
64
|
-
6. Click **Create**
|
|
45
|
+
### Get your tokens
|
|
65
46
|
|
|
66
|
-
|
|
47
|
+
Generate two tokens:
|
|
67
48
|
|
|
68
|
-
|
|
49
|
+
| Token | Where to find it | Format |
|
|
50
|
+
| --------------- | ------------------------------------------------------------------------------- | ------------ |
|
|
51
|
+
| App-Level Token | Basic Information → App-Level Tokens → Generate (add `connections:write` scope) | `xapp-1-...` |
|
|
52
|
+
| Bot Token | OAuth & Permissions → Install to Workspace → Bot User OAuth Token | `xoxb-...` |
|
|
69
53
|
|
|
70
|
-
|
|
54
|
+
### Required bot scopes
|
|
71
55
|
|
|
72
|
-
|
|
73
|
-
| ------------------- | -------------------------------------------------------------------------------- | ------------ |
|
|
74
|
-
| **App-Level Token** | Basic Information → App-Level Tokens → Generate (with `connections:write` scope) | `xapp-1-...` |
|
|
75
|
-
| **Bot Token** | OAuth & Permissions → Install to Workspace → Bot User OAuth Token | `xoxb-...` |
|
|
76
|
-
|
|
77
|
-
### 3. Required bot scopes
|
|
78
|
-
|
|
79
|
-
These are included in the manifest, but for reference:
|
|
56
|
+
The manifest includes these scopes:
|
|
80
57
|
|
|
81
58
|
```
|
|
82
59
|
app_mentions:read assistant:write bookmarks:read
|
|
83
60
|
bookmarks:write canvases:read canvases:write
|
|
84
61
|
channels:history channels:read chat:write
|
|
85
|
-
commands files:read files:write
|
|
86
|
-
groups:read im:history
|
|
87
|
-
im:write pins:read
|
|
88
|
-
reactions:read reactions:write
|
|
62
|
+
commands files:read files:write
|
|
63
|
+
groups:history groups:read im:history
|
|
64
|
+
im:read im:write pins:read
|
|
65
|
+
pins:write reactions:read reactions:write
|
|
66
|
+
users:read
|
|
89
67
|
```
|
|
90
68
|
|
|
91
|
-
`commands`
|
|
69
|
+
The `commands` scope enables slash commands. The `files:read` scope is needed because Slack uses `files.info` for canvas comment pagination.
|
|
92
70
|
|
|
93
|
-
|
|
71
|
+
## Configure Pinet
|
|
94
72
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Add your tokens to `~/.pi/agent/settings.json`:
|
|
73
|
+
Add tokens, runtime mode, and access rules to `~/.pi/agent/settings.json`:
|
|
98
74
|
|
|
99
75
|
```json
|
|
100
76
|
{
|
|
101
77
|
"slack-bridge": {
|
|
102
78
|
"botToken": "xoxb-your-bot-token",
|
|
103
|
-
"appToken": "xapp-your-app-token"
|
|
79
|
+
"appToken": "xapp-your-app-token",
|
|
80
|
+
"runtimeMode": "single",
|
|
81
|
+
"allowedUsers": ["U_YOUR_USER_ID"]
|
|
104
82
|
}
|
|
105
83
|
}
|
|
106
84
|
```
|
|
107
85
|
|
|
108
|
-
|
|
86
|
+
Pinet stays off unless you set `runtimeMode`, `autoConnect`, or `autoFollow`. Start pi after you configure access.
|
|
109
87
|
|
|
110
|
-
###
|
|
88
|
+
### Use environment variables instead
|
|
111
89
|
|
|
112
90
|
```bash
|
|
113
91
|
export SLACK_BOT_TOKEN="xoxb-..."
|
|
114
92
|
export SLACK_APP_TOKEN="xapp-..."
|
|
115
93
|
```
|
|
116
94
|
|
|
117
|
-
Settings in `settings.json`
|
|
95
|
+
Settings in `settings.json` override environment variables.
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
## Control who can use Pinet
|
|
120
98
|
|
|
121
|
-
|
|
99
|
+
Slack access is default-deny. Configure one of these:
|
|
100
|
+
|
|
101
|
+
- `allowedUsers`: list specific Slack user IDs
|
|
102
|
+
- `allowAllWorkspaceUsers: true`: allow everyone in the workspace
|
|
103
|
+
|
|
104
|
+
Example with specific users:
|
|
122
105
|
|
|
123
106
|
```json
|
|
124
107
|
{
|
|
125
108
|
"slack-bridge": {
|
|
126
|
-
"
|
|
109
|
+
"botToken": "xoxb-...",
|
|
110
|
+
"appToken": "xapp-...",
|
|
111
|
+
"allowedUsers": ["U_USER_ID_1", "U_USER_ID_2"]
|
|
127
112
|
}
|
|
128
113
|
}
|
|
129
114
|
```
|
|
130
115
|
|
|
116
|
+
Find user IDs by selecting a user's profile in Slack and choosing 'Copy member ID'.
|
|
117
|
+
|
|
118
|
+
## Optional settings
|
|
119
|
+
|
|
120
|
+
### Mesh authentication
|
|
121
|
+
|
|
122
|
+
Shared-secret authentication is optional. Configure it with settings or environment variables:
|
|
123
|
+
|
|
124
|
+
Settings:
|
|
125
|
+
|
|
131
126
|
```json
|
|
132
127
|
{
|
|
133
128
|
"slack-bridge": {
|
|
134
|
-
"
|
|
129
|
+
"meshSecret": "your-shared-secret"
|
|
135
130
|
}
|
|
136
131
|
}
|
|
137
132
|
```
|
|
138
133
|
|
|
134
|
+
Or use a file:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"slack-bridge": {
|
|
139
|
+
"meshSecretPath": "/path/to/secret.txt"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Environment variables:
|
|
145
|
+
|
|
139
146
|
```bash
|
|
140
|
-
export PINET_MESH_SECRET="shared-secret"
|
|
147
|
+
export PINET_MESH_SECRET="your-shared-secret"
|
|
141
148
|
# or
|
|
142
|
-
export PINET_MESH_SECRET_PATH="
|
|
149
|
+
export PINET_MESH_SECRET_PATH="/path/to/secret.txt"
|
|
143
150
|
```
|
|
144
151
|
|
|
145
|
-
|
|
152
|
+
How it works:
|
|
153
|
+
|
|
154
|
+
- settings override environment variables
|
|
155
|
+
- inline secrets override file paths
|
|
156
|
+
- if nothing is set, mesh auth is disabled
|
|
157
|
+
- brokers create the secret file if it does not exist
|
|
158
|
+
- followers need an existing file or will show an error
|
|
146
159
|
|
|
147
|
-
|
|
148
|
-
- Inline secrets win over secret paths. If `meshSecret` or `PINET_MESH_SECRET` is set, the corresponding `*Path` value is ignored.
|
|
149
|
-
- If all four values are unset, broker/follower mesh auth is disabled.
|
|
150
|
-
- A broker started with `meshSecretPath` creates the secret file if it does not exist yet.
|
|
151
|
-
- A follower started with `meshSecretPath` does **not** create the file. If the configured file is missing, follow fails with a clear error telling you to point at an existing file, provide `meshSecret` directly, or leave both unset to disable shared-secret auth.
|
|
152
|
-
- A follower configured for mesh auth will fail closed against an older/no-auth broker with a clear compatibility error. It will **not** silently retry as an unauthenticated follower.
|
|
160
|
+
### Require mentions in channels
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
Make Pinet respond only when mentioned in specific channels:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"slack-bridge": {
|
|
167
|
+
"ingressGuard": {
|
|
168
|
+
"requireMention": {
|
|
169
|
+
"channels": ["C_CHANNEL_ID"],
|
|
170
|
+
"mixedParticipantThreads": {
|
|
171
|
+
"enabled": true,
|
|
172
|
+
"trustedUsers": ["U_TRUSTED_USER"]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
This is separate from `allowedUsers`. Authorization decides who can use Pinet. The guard decides when a mention is needed.
|
|
181
|
+
|
|
182
|
+
### All configuration options
|
|
155
183
|
|
|
156
184
|
```json
|
|
157
185
|
{
|
|
@@ -159,25 +187,32 @@ Behavior and precedence:
|
|
|
159
187
|
"botToken": "xoxb-...",
|
|
160
188
|
"appToken": "xapp-...",
|
|
161
189
|
"runtimeMode": "single",
|
|
162
|
-
"allowedUsers": ["
|
|
190
|
+
"allowedUsers": ["U_USER_ID"],
|
|
191
|
+
"allowAllWorkspaceUsers": false,
|
|
163
192
|
"ingressGuard": {
|
|
164
193
|
"requireMention": {
|
|
165
|
-
"channels": ["
|
|
194
|
+
"channels": ["C_CHANNEL_ID"],
|
|
166
195
|
"mixedParticipantThreads": {
|
|
167
196
|
"enabled": true,
|
|
168
|
-
"trustedUsers": ["
|
|
197
|
+
"trustedUsers": ["U_USER_ID"]
|
|
169
198
|
}
|
|
170
199
|
}
|
|
171
200
|
},
|
|
172
|
-
"defaultChannel": "
|
|
201
|
+
"defaultChannel": "C_CHANNEL_ID",
|
|
173
202
|
"logChannel": "#pinet-logs",
|
|
174
203
|
"logLevel": "actions",
|
|
175
|
-
"
|
|
204
|
+
"autoConnect": false,
|
|
205
|
+
"autoFollow": false,
|
|
176
206
|
"ralphLoopIntervalMs": 300000,
|
|
177
207
|
"ralphSnoozeAfterEmptyCycles": 0,
|
|
178
208
|
"ralphSnoozeDurationMs": 1800000,
|
|
179
|
-
"meshSecretPath": "/
|
|
180
|
-
"suggestedPrompts": [
|
|
209
|
+
"meshSecretPath": "/path/to/secret",
|
|
210
|
+
"suggestedPrompts": [
|
|
211
|
+
{
|
|
212
|
+
"title": "Status",
|
|
213
|
+
"message": "What are you working on?"
|
|
214
|
+
}
|
|
215
|
+
],
|
|
181
216
|
"security": {
|
|
182
217
|
"readOnly": false,
|
|
183
218
|
"requireConfirmation": ["slack:create_channel"],
|
|
@@ -187,450 +222,255 @@ Behavior and precedence:
|
|
|
187
222
|
}
|
|
188
223
|
```
|
|
189
224
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `logChannel` | no | Channel for broker activity logs |
|
|
206
|
-
| `logLevel` | no | `"errors"`, `"actions"` (default), or `"verbose"` |
|
|
207
|
-
| `runtimeMode` | no | Explicit startup mode: `"off"`, `"single"`, `"broker"`, or `"follower"` |
|
|
208
|
-
| `autoConnect` | no | Legacy compatibility alias for `runtimeMode: "single"` |
|
|
209
|
-
| `autoFollow` | no | Legacy compatibility alias for follower startup when a broker socket exists |
|
|
210
|
-
| `ralphLoopIntervalMs` | no | Broker RALPH maintenance cadence in milliseconds; defaults to `300000` (5 minutes), valid range `1000`-`2147483647` |
|
|
211
|
-
| `ralphSnoozeAfterEmptyCycles` | no | Broker RALPH auto-snooze trigger after N empty cycles; defaults to `0` (disabled), valid range `0`-`100` |
|
|
212
|
-
| `ralphSnoozeDurationMs` | no | Broker RALPH auto-snooze duration in milliseconds; defaults to `1800000` (30 minutes), valid range `60000`-`86400000` |
|
|
213
|
-
| `skinTheme` | no | Pinet presentation skin selected at broker startup/reload (`default`, `foundation`, `cosmere`, or free-form) |
|
|
214
|
-
| `slackCommandName` | no | Slack web app slash command name for `agents list`; defaults to `/pinet`, or `/oathgate` for Oathgate/Cosmere skins |
|
|
215
|
-
| `slackCommandNames` | no | Optional list of accepted/deployed Slack slash command aliases when one app needs multiple command names |
|
|
216
|
-
| `meshSecret` | no | Optional inline Pinet shared secret; overrides `meshSecretPath` and env fallbacks |
|
|
217
|
-
| `meshSecretPath` | no | Optional path to a shared-secret file; broker creates it if missing, followers require an existing file |
|
|
218
|
-
| `suggestedPrompts` | no | Prompts shown when a user opens a new conversation |
|
|
219
|
-
| `security.readOnly` | no | Runtime-block write-capable tools for Slack-triggered turns, including core tools like `bash`, `edit`, and `write` |
|
|
220
|
-
| `security.requireConfirmation` | no | Runtime-require Slack approval before matching tools execute; core tools need a specific Slack thread context |
|
|
221
|
-
| `security.blockedTools` | no | Runtime-block matching tools for Slack-triggered turns, including core tools |
|
|
222
|
-
|
|
223
|
-
## Scope carrier model (compatibility-first)
|
|
224
|
-
|
|
225
|
-
Slack/Pinet now threads a first-class runtime `scope` carrier through shared message contracts and runtime metadata.
|
|
226
|
-
|
|
227
|
-
For this first slice:
|
|
228
|
-
|
|
229
|
-
- **workspace/install scope** is carried as compatibility-first metadata for Slack
|
|
230
|
-
- **instance scope** is also carried as a first-class compatibility carrier
|
|
231
|
-
- today’s single-workspace deployments use one default compatibility scope
|
|
232
|
-
- a missing or empty Slack `teamId` stays **unknown** — the bridge does not invent a fake workspace ID
|
|
233
|
-
- these carriers are metadata only in this slice; enforcement and multi-install behavior land later in `#547` / `#550`
|
|
234
|
-
|
|
235
|
-
## Usage
|
|
236
|
-
|
|
237
|
-
Once configured, Pinet appears in Slack's sidebar. Users open it, type a message, and the pi agent responds.
|
|
225
|
+
| Setting | Description | Default |
|
|
226
|
+
| ------------------------ | ------------------------------------------------------ | -------------------- |
|
|
227
|
+
| `botToken` | Bot User OAuth Token (required) | none |
|
|
228
|
+
| `appToken` | App-Level Token for Socket Mode (required) | none |
|
|
229
|
+
| `runtimeMode` | How Pinet runs (`off`, `single`, `broker`, `follower`) | `off` |
|
|
230
|
+
| `allowedUsers` | Slack user IDs who can use Pinet | none |
|
|
231
|
+
| `allowAllWorkspaceUsers` | Allow all workspace members | `false` |
|
|
232
|
+
| `defaultChannel` | Where to post updates | none |
|
|
233
|
+
| `logChannel` | Where to post logs | none |
|
|
234
|
+
| `logLevel` | What to log (`errors`, `actions`, `verbose`) | `actions` |
|
|
235
|
+
| `autoConnect` | Start as a single instance when `runtimeMode` is unset | `false` |
|
|
236
|
+
| `autoFollow` | Start as follower if broker exists | `false` |
|
|
237
|
+
| `ralphLoopIntervalMs` | How often to check for stalls (milliseconds) | `300000` (5 minutes) |
|
|
238
|
+
| `meshSecret` | Shared secret for mesh auth | none |
|
|
239
|
+
| `meshSecretPath` | File containing shared secret | none |
|
|
238
240
|
|
|
239
|
-
|
|
240
|
-
User opens Pinet in Slack sidebar
|
|
241
|
-
└─► types a message
|
|
242
|
-
└─► 👀 reaction appears (thinking)
|
|
243
|
-
└─► message queued for pi agent
|
|
244
|
-
└─► agent responds via slack_send
|
|
245
|
-
└─► 👀 removed, reply appears in thread
|
|
246
|
-
```
|
|
241
|
+
## Using Pinet
|
|
247
242
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
### Reaction triggers
|
|
251
|
-
|
|
252
|
-
Slack emoji reactions are ignored by default: they do not enqueue Pinet work, trigger reviews, steer agents, interrupt owners, or cause broker/worker replies. To opt in deliberately, configure `reactionCommands` for the exact emoji aliases that should become structured Pinet requests from the reacted-to Slack message. Even configured reactions are accepted only inside an already authorized Pinet thread (for example a thread with a current Pinet owner, or persisted Slack assistant-thread context). Reaction authorization is deny-by-default: it requires an explicit broker-backed authorization gate, and a thread the adapter has merely seen or cached never qualifies on its own. Reactions in ordinary, uninvoked Slack channel threads remain no-op — even from authorized users — and they do not enqueue work, persist thread state, claim ownership, or receive a Slack ACK. Messages and interactive events from users outside the allowlist also never mint known-thread state that could later admit reactions or replies. Pinet adds ✅ only when it accepts an opt-in reaction-triggered request. If it cannot process an accepted opted-in reaction, it adds ❌; check broker logs for the underlying Slack/API error. When Slack cannot return the reacted message text, Pinet can still route configured reactions when the message timestamp itself identifies an already authorized thread; otherwise it ignores the reaction safely.
|
|
253
|
-
|
|
254
|
-
### Available tools
|
|
255
|
-
|
|
256
|
-
Slack-bridge uses progressive disclosure to keep the per-turn tool surface
|
|
257
|
-
small:
|
|
258
|
-
|
|
259
|
-
| Tool | Description |
|
|
260
|
-
| ------------- | --------------------------------------------------------------------------- |
|
|
261
|
-
| `slack_inbox` | Hot-path inbox drain for pending incoming Slack messages |
|
|
262
|
-
| `slack_send` | Hot-path reply tool for Slack assistant threads |
|
|
263
|
-
| `slack` | Dispatcher for all non-hot Slack actions; call `action: "help"` for schemas |
|
|
264
|
-
|
|
265
|
-
Cold Slack actions live behind the `slack` dispatcher:
|
|
266
|
-
|
|
267
|
-
| Dispatcher action | Description |
|
|
268
|
-
| ---------------------- | --------------------------------------------------------------------------------- |
|
|
269
|
-
| `react` | Add an emoji reaction to a message |
|
|
270
|
-
| `read` | Read messages from a thread |
|
|
271
|
-
| `upload` | Upload files, snippets, or diffs into Slack |
|
|
272
|
-
| `file` | Download Slack-hosted files to a controlled local temp cache by file ID |
|
|
273
|
-
| `schedule` | Schedule a message for later delivery |
|
|
274
|
-
| `post_channel` | Post to a channel (by name or ID) |
|
|
275
|
-
| `delete` | Delete a bot-posted message or an entire thread |
|
|
276
|
-
| `read_channel` | Read channel history or a thread in a channel |
|
|
277
|
-
| `create_channel` | Create a new Slack channel |
|
|
278
|
-
| `project_create` | Create a project channel + RFC canvas + bot invite in one call |
|
|
279
|
-
| `pin` | Pin or unpin a message |
|
|
280
|
-
| `bookmark` | Add, list, or remove channel bookmarks |
|
|
281
|
-
| `export` | Export a thread as markdown, plain text, or JSON |
|
|
282
|
-
| `presence` | Check if users are active, away, or in DND |
|
|
283
|
-
| `canvas_comments_read` | Read comments attached to a verified canvas by canvas ID or channel canvas lookup |
|
|
284
|
-
| `canvas_create` | Create a standalone or channel canvas |
|
|
285
|
-
| `canvas_update` | Append, prepend, or replace canvas content |
|
|
286
|
-
| `modal_open` | Open a modal from a trigger interaction |
|
|
287
|
-
| `modal_push` | Push a new step onto a modal stack |
|
|
288
|
-
| `modal_update` | Update an existing open modal |
|
|
289
|
-
| `confirm_action` | Request user confirmation before a dangerous action |
|
|
290
|
-
|
|
291
|
-
Use `slack` with `action: "help"` for the action catalogue, or
|
|
292
|
-
`action: "help", args: { "topic": "canvas_update" }` for a specific JSON
|
|
293
|
-
schema and example invocations. Dispatcher responses use a consistent
|
|
294
|
-
`{ "status", "data", "errors", "warnings" }` envelope. Guardrails match
|
|
295
|
-
cold Slack actions as `slack:<action>` (for example `slack:upload` or
|
|
296
|
-
`slack:canvas_update`); legacy `slack_<action>` patterns are accepted during
|
|
297
|
-
migration.
|
|
298
|
-
|
|
299
|
-
#### Tool and workflow usage notes
|
|
300
|
-
|
|
301
|
-
- **Reply where the work arrived.** Use `slack_send` for assistant-thread
|
|
302
|
-
replies. If a task was delivered in a Slack thread, acknowledge briefly,
|
|
303
|
-
do the work, report blockers immediately, and finish with the outcome. If
|
|
304
|
-
you know only a channel/thread pair, use dispatcher action `post_channel`
|
|
305
|
-
with `channel` and optional `thread_ts` instead.
|
|
306
|
-
- **Channel posting is explicit.** `post_channel` posts to a named channel or
|
|
307
|
-
channel ID. When `channel` is omitted, it first resolves a provided
|
|
308
|
-
`thread_ts` to a tracked thread channel, then falls back to `defaultChannel`
|
|
309
|
-
from settings. `slack_send` is intentionally narrower and resolves the
|
|
310
|
-
current tracked assistant thread/DM context.
|
|
311
|
-
- **Rich messages use Block Kit JSON.** Pass `blocks` directly to
|
|
312
|
-
`slack_send` or `post_channel`; keep `text` as the notification/fallback.
|
|
313
|
-
Block Kit builder tools are not registered by this package. Load the bundled
|
|
314
|
-
`slack-bridge` skill for copyable status-report, button, code, and diff
|
|
315
|
-
templates. The package also bundles `pinet-skin-creator` for safely drafting
|
|
316
|
-
or reviewing curated Pinet skin descriptors and character/status-vocabulary
|
|
317
|
-
pools before changing runtime skin wiring.
|
|
318
|
-
- **Modal helpers are patterns, not hot tools.** Use dispatcher actions
|
|
319
|
-
`modal_open`, `modal_push`, and `modal_update` with Slack view JSON. Open or
|
|
320
|
-
push immediately after receiving a fresh `trigger_id`; Slack trigger IDs
|
|
321
|
-
expire quickly. Include `thread_ts` when submissions should route back to an
|
|
322
|
-
original assistant thread.
|
|
323
|
-
- **Uploads are for bulky artifacts.** Use `upload` for logs, screenshots,
|
|
324
|
-
long diffs, and generated files instead of large inline messages. Inline
|
|
325
|
-
uploads require `filename`; path uploads are guarded and must stay within the
|
|
326
|
-
current working directory or system temp directory. `slack_send` also accepts
|
|
327
|
-
`files: [{ path, filename?, title?, filetype? }]` so one assistant-thread
|
|
328
|
-
reply can contain both text and local binary attachments in the same Slack
|
|
329
|
-
file upload message. Slack external file uploads cannot include Block Kit in
|
|
330
|
-
that same message, so omit `blocks` when sending files or send a separate
|
|
331
|
-
block-only reply.
|
|
332
|
-
- **Inbound Slack files are fetched explicitly.** Incoming file-share messages
|
|
333
|
-
preserve safe `slackFiles` metadata such as file ID, name, type, size, and
|
|
334
|
-
permalink, but private Slack download URLs are not exposed in normal tool
|
|
335
|
-
output. To inspect raw content, call dispatcher action `file` with
|
|
336
|
-
`op: "download"`, `file_id`, and optionally `thread_ts`, `message_ts`, and
|
|
337
|
-
`channel`. The bot fetches the file with Slack bot auth, stores it under the
|
|
338
|
-
system temp `pi-slack-files` cache with best-effort TTL cleanup, and returns a
|
|
339
|
-
descriptor containing the local path, filename, type, size, SHA-256, expiry,
|
|
340
|
-
and residual privacy risks.
|
|
341
|
-
- **Upload host egress note.** The second upload leg goes to Slack file upload
|
|
342
|
-
hosts (`files.slack.com`/`uploads.slack.com`) for the raw payload. In
|
|
343
|
-
environments with restricted egress this can fail with `403` (proxy
|
|
344
|
-
allowlist) or DNS errors after `files.getUploadURLExternal`; verify the proxy
|
|
345
|
-
allowlist first, or route through an environment that can reach those hosts.
|
|
346
|
-
- **Upload metadata note.** Slack snippet uploads attempt to use inferred
|
|
347
|
-
`snippet_type` values for inline content and retry with plain upload metadata
|
|
348
|
-
when Slack returns `invalid_arguments`, preserving syntax highlighting for
|
|
349
|
-
supported types while avoiding hard failures on unsupported snippet types.
|
|
350
|
-
- **Canvases are long-lived docs.** `canvas_create` creates standalone or
|
|
351
|
-
channel canvases. If Slack rejects channel tab creation with
|
|
352
|
-
`canvas_tab_creation_failed`, it falls back to a standalone canvas attached to
|
|
353
|
-
the channel, attempts to bookmark the canvas URL, and returns the fallback
|
|
354
|
-
`canvas_id` for future `canvas_update` calls. `canvas_update` can append,
|
|
355
|
-
prepend, replace the whole canvas, or replace a matched section;
|
|
356
|
-
`canvas_comments_read` is read-only and limited to verified canvas targets.
|
|
357
|
-
- **Scheduling, pins, and bookmarks are durable affordances.** Use `schedule`
|
|
358
|
-
for delayed reminders instead of waiting; use `pin` for important thread
|
|
359
|
-
messages; use `bookmark` for persistent channel-header links to repos,
|
|
360
|
-
dashboards, docs, or runbooks.
|
|
361
|
-
- **Presence helps choose timing.** Use `presence` before pinging humans when
|
|
362
|
-
active/away/DND status affects routing or whether to schedule a follow-up.
|
|
363
|
-
- **Destructive actions stay constrained.** `delete` can remove only messages
|
|
364
|
-
posted by the current bot and every delete call requires `confirm: true`.
|
|
365
|
-
Whole-thread deletion additionally requires `thread: true` and succeeds only
|
|
366
|
-
when every message in the target thread belongs to the current bot. Prefer
|
|
367
|
-
asking for explicit approval before destructive cleanup.
|
|
368
|
-
- **Confirm guarded actions in the same thread.** If guardrails require
|
|
369
|
-
confirmation, call `confirm_action` with the target `thread_ts`, exact tool
|
|
370
|
-
name, and the exact action string required by the guarded tool. The safest
|
|
371
|
-
flow is: attempt the guarded call, copy the `requires confirmation for action
|
|
372
|
-
...` string from the error, request confirmation, wait for the user's approval
|
|
373
|
-
via `slack_inbox`, then retry the guarded call unchanged. Batched
|
|
374
|
-
multi-thread Slack turns cannot satisfy a single-thread confirmation.
|
|
375
|
-
- **Plain emoji reactions are not tasks.** Slack emoji reactions are ignored
|
|
376
|
-
unless `reactionCommands` explicitly opts that emoji into structured
|
|
377
|
-
reaction-trigger handling and the reacted message belongs to an already
|
|
378
|
-
authorized Pinet thread. If an opt-in reaction-triggered request or a Block
|
|
379
|
-
Kit/modal interaction payload arrives through `slack_inbox` with metadata,
|
|
380
|
-
treat it as a user instruction tied to the referenced Slack thread or
|
|
381
|
-
message.
|
|
382
|
-
|
|
383
|
-
#### Common dispatcher examples
|
|
384
|
-
|
|
385
|
-
Reply in the current Slack assistant thread with Block Kit:
|
|
243
|
+
### In Slack
|
|
386
244
|
|
|
387
|
-
|
|
388
|
-
{
|
|
389
|
-
"text": "Deploy complete — branch main, checks passed.",
|
|
390
|
-
"blocks": [
|
|
391
|
-
{
|
|
392
|
-
"type": "section",
|
|
393
|
-
"fields": [
|
|
394
|
-
{ "type": "mrkdwn", "text": "*Branch*\n`main`" },
|
|
395
|
-
{ "type": "mrkdwn", "text": "*Checks*\n✅ lint/typecheck/test" }
|
|
396
|
-
]
|
|
397
|
-
}
|
|
398
|
-
]
|
|
399
|
-
}
|
|
400
|
-
```
|
|
245
|
+
Talk to Pinet:
|
|
401
246
|
|
|
402
|
-
|
|
247
|
+
- Direct message: open a DM with Pinet
|
|
248
|
+
- In channels: mention `@pinet` (or your app name)
|
|
249
|
+
- Slack slash command: type `/pinet agents list` or `/pinet agents list all`
|
|
403
250
|
|
|
404
|
-
|
|
405
|
-
{
|
|
406
|
-
"action": "post_channel",
|
|
407
|
-
"args": {
|
|
408
|
-
"channel": "#pinet-logs",
|
|
409
|
-
"thread_ts": "1712345678.000100",
|
|
410
|
-
"text": "PR #123 is ready for review."
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
```
|
|
251
|
+
### Pi commands
|
|
414
252
|
|
|
415
|
-
|
|
253
|
+
Run these inside pi.
|
|
416
254
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
255
|
+
Main commands:
|
|
256
|
+
|
|
257
|
+
- `/pinet` - show available commands
|
|
258
|
+
- `/pinet status` - show current Pinet status
|
|
259
|
+
- `/pinet logs` - show recent broker activity logs
|
|
260
|
+
- `/pinet rename [name]` - rename this agent
|
|
261
|
+
- `/pinet free` - mark this agent idle
|
|
262
|
+
|
|
263
|
+
Coordinator commands:
|
|
264
|
+
|
|
265
|
+
- `/pinet start` or `/pinet broker` - become the broker
|
|
266
|
+
- `/pinet follow` - become a follower
|
|
267
|
+
- `/pinet unfollow` - disconnect from broker
|
|
268
|
+
- `/pinet reload <agent>` - ask another agent to reload
|
|
269
|
+
- `/pinet exit <agent>` - ask another agent to exit
|
|
270
|
+
- `/pinet snooze [duration|off|status]` - quiet empty RALPH cycles
|
|
271
|
+
- `/pinet subtree [start|status|spawn|stop]` - manage subtree broker mode
|
|
429
272
|
|
|
430
|
-
|
|
431
|
-
|
|
273
|
+
### From pi
|
|
274
|
+
|
|
275
|
+
Use the Pinet dispatcher for agent coordination:
|
|
432
276
|
|
|
433
277
|
```json
|
|
434
278
|
{
|
|
435
|
-
"action": "
|
|
279
|
+
"action": "send",
|
|
436
280
|
"args": {
|
|
437
|
-
"
|
|
438
|
-
"
|
|
439
|
-
"action": "channel=#pinet-logs | thread_ts=1712345678.000100 | ts=1712345678.000200 | thread=false"
|
|
281
|
+
"to": "@worker",
|
|
282
|
+
"message": "Please review PR #123"
|
|
440
283
|
}
|
|
441
284
|
}
|
|
442
285
|
```
|
|
443
286
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
The `canvas_comments_read` dispatcher action is intentionally narrow:
|
|
447
|
-
|
|
448
|
-
- it validates the target with `canvases.sections.lookup` before reading comment pages via `files.info`
|
|
449
|
-
- it needs `files:read` because Slack exposes canvas comments through the file API surface
|
|
450
|
-
- it will **not** inspect generic Slack files, non-canvas file comments, or full canvas body/history
|
|
451
|
-
|
|
452
|
-
### Slash commands
|
|
287
|
+
Common actions:
|
|
453
288
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
| `/pinet logs` | Show recent broker activity log entries |
|
|
460
|
-
| `/<app> agents list [all]` | Slack-native broker roster, workload, task, and lane view |
|
|
289
|
+
- `send` - send a message to an agent or broker-only channel
|
|
290
|
+
- `read` - read this agent's inbox
|
|
291
|
+
- `schedule` - schedule a future wake-up
|
|
292
|
+
- `free` - mark this agent idle
|
|
293
|
+
- `help` - discover actions and schemas
|
|
461
294
|
|
|
462
|
-
|
|
295
|
+
Use `slack_send` for hot-path Slack replies. Use the `slack` dispatcher for uploads, canvases, pins, bookmarks, and other Slack actions.
|
|
463
296
|
|
|
464
|
-
|
|
297
|
+
## Architecture
|
|
465
298
|
|
|
466
|
-
|
|
467
|
-
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
468
|
-
| `off` | Slack bridge is loaded, but **no Slack Socket Mode ingress** and no coordination runtime are started. |
|
|
469
|
-
| `single` | One local Pi session owns Slack ingress and local thread/inbox ownership only. No broker DB/socket/client, no RALPH/control plane, no mesh auth, no multi-agent surface. |
|
|
470
|
-
| `broker` | The session runs the broker coordination runtime. |
|
|
471
|
-
| `follower` | The session connects to an existing broker as a worker runtime. |
|
|
299
|
+
### Broker and followers
|
|
472
300
|
|
|
473
|
-
|
|
301
|
+
Pinet can run as:
|
|
474
302
|
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
-
|
|
478
|
-
- explicit `runtimeMode` wins over the legacy flags.
|
|
479
|
-
- `/pinet start` and `/pinet follow` still switch the live session into broker/follower runtimes explicitly.
|
|
303
|
+
- single - one instance handles everything
|
|
304
|
+
- broker - coordinates and routes messages
|
|
305
|
+
- follower - receives work from the broker
|
|
480
306
|
|
|
481
|
-
|
|
307
|
+
The broker:
|
|
482
308
|
|
|
483
|
-
|
|
309
|
+
- watches Slack for messages
|
|
310
|
+
- assigns work to agents
|
|
311
|
+
- tracks who owns what
|
|
312
|
+
- syncs state across followers
|
|
484
313
|
|
|
485
|
-
|
|
486
|
-
- `scope.instance` models the current broker/runtime instance scope.
|
|
487
|
-
- in the first slice, both stay **compatibility-first**: today’s singleton runtime gets one default compatibility scope
|
|
488
|
-
- if Slack omits `team_id`, the carrier keeps the workspace id **unknown** instead of inventing a fake one
|
|
489
|
-
- this slice is metadata/plumbing only: it does **not** change routing, enforcement, or multi-install orchestration yet
|
|
314
|
+
Followers:
|
|
490
315
|
|
|
491
|
-
|
|
316
|
+
- connect to the broker
|
|
317
|
+
- receive assigned work
|
|
318
|
+
- stay in sync automatically
|
|
492
319
|
|
|
493
|
-
|
|
320
|
+
### RALPH maintenance loop
|
|
494
321
|
|
|
495
|
-
|
|
322
|
+
RALPH keeps broker state healthy. It:
|
|
496
323
|
|
|
497
|
-
|
|
324
|
+
- runs every 5 minutes by default
|
|
325
|
+
- checks worker presence
|
|
326
|
+
- releases stale claims held by unavailable workers
|
|
327
|
+
- observes pending backlog while broker maintenance handles assignment
|
|
328
|
+
- triggers wake-ups
|
|
498
329
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
**Broker** (one per mesh — coordinates routing and health):
|
|
502
|
-
|
|
503
|
-
```
|
|
504
|
-
/pinet start
|
|
505
|
-
```
|
|
330
|
+
Configure RALPH:
|
|
506
331
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
332
|
+
```json
|
|
333
|
+
{
|
|
334
|
+
"slack-bridge": {
|
|
335
|
+
"ralphLoopIntervalMs": 120000,
|
|
336
|
+
"ralphSnoozeAfterEmptyCycles": 3,
|
|
337
|
+
"ralphSnoozeDurationMs": 1800000
|
|
338
|
+
}
|
|
339
|
+
}
|
|
511
340
|
```
|
|
512
341
|
|
|
513
|
-
|
|
342
|
+
### Inbox and threading
|
|
514
343
|
|
|
515
|
-
|
|
344
|
+
Pinet maintains an inbox for each agent. Messages are:
|
|
516
345
|
|
|
517
|
-
|
|
346
|
+
- routed based on thread ownership
|
|
347
|
+
- queued when agents are busy
|
|
348
|
+
- marked read when processed
|
|
349
|
+
- preserved across restarts
|
|
518
350
|
|
|
519
|
-
|
|
520
|
-
2. user-local override: `~/.pi/agent/slack-bridge/tmux.md`
|
|
521
|
-
3. packaged default: `dist/prompts/broker/tmux.md`
|
|
351
|
+
Thread ownership ensures continuity. Once an agent owns a thread, it keeps receiving those messages.
|
|
522
352
|
|
|
523
|
-
|
|
353
|
+
## Troubleshooting
|
|
524
354
|
|
|
525
|
-
|
|
355
|
+
### Socket Mode connection issues
|
|
526
356
|
|
|
527
|
-
|
|
357
|
+
If you see 'WebSocket error' or connection failures:
|
|
528
358
|
|
|
529
|
-
|
|
359
|
+
1. Check your app token is valid
|
|
360
|
+
2. Verify Socket Mode is enabled in your Slack app
|
|
361
|
+
3. Check network connectivity
|
|
362
|
+
4. Look for rate limiting (Slack allows 10 connections per app)
|
|
530
363
|
|
|
531
|
-
|
|
532
|
-
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
533
|
-
| `pinet` | Pinet dispatcher with token-efficient `action`-based routing (`help`, `send`, `read`, `free`, `snooze`, `schedule`, `agents`, `sessions`, `lanes`, `ports`, `spawn`, `reload`, `exit`) |
|
|
364
|
+
### Permission errors
|
|
534
365
|
|
|
535
|
-
|
|
366
|
+
If Pinet cannot perform actions:
|
|
536
367
|
|
|
537
|
-
|
|
368
|
+
1. Check the bot is in the channel (invite with `/invite @pinet`)
|
|
369
|
+
2. Verify bot scopes match the manifest
|
|
370
|
+
3. Reinstall the app to update permissions
|
|
371
|
+
4. Check `allowedUsers` includes the right user IDs
|
|
538
372
|
|
|
539
|
-
|
|
373
|
+
### Messages not received
|
|
540
374
|
|
|
541
|
-
|
|
375
|
+
If Pinet does not respond:
|
|
542
376
|
|
|
543
|
-
|
|
377
|
+
1. Check Socket Mode shows 'Connected' in Slack app settings
|
|
378
|
+
2. Verify event subscriptions are enabled
|
|
379
|
+
3. Check `allowedUsers` or `allowAllWorkspaceUsers`
|
|
380
|
+
4. Look in the log channel for errors
|
|
381
|
+
5. Try `/pinet status` to check if Pinet is running
|
|
544
382
|
|
|
545
|
-
|
|
383
|
+
### Stalled agents
|
|
546
384
|
|
|
547
|
-
|
|
385
|
+
If work gets stuck:
|
|
548
386
|
|
|
549
|
-
|
|
387
|
+
1. Check `/pinet status` for current state.
|
|
388
|
+
2. Check `/pinet logs` for repeated failures.
|
|
389
|
+
3. Wait for RALPH to run automatically.
|
|
390
|
+
4. Reduce `ralphLoopIntervalMs` for faster recovery if needed.
|
|
550
391
|
|
|
551
|
-
|
|
392
|
+
## Package information
|
|
552
393
|
|
|
553
|
-
|
|
394
|
+
### Publishing metadata
|
|
554
395
|
|
|
555
|
-
|
|
396
|
+
The package declares pi metadata in [`package.json`](./package.json):
|
|
556
397
|
|
|
557
|
-
|
|
398
|
+
- `keywords` includes `pi-package` for gallery discovery
|
|
399
|
+
- `pi.extensions` points to `./dist/index.js`
|
|
400
|
+
- `pi.skills` points to bundled skills
|
|
401
|
+
- No preview assets yet
|
|
558
402
|
|
|
559
|
-
|
|
403
|
+
Check the package contents:
|
|
560
404
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
| `/pinet unfollow` | Disconnect from the broker |
|
|
566
|
-
| `/pinet reload <agent>` | Ask another agent to reload |
|
|
567
|
-
| `/pinet exit <agent>` | Ask another agent to exit |
|
|
568
|
-
| `/pinet free` | Mark this agent as idle |
|
|
569
|
-
| `/pinet snooze [duration/off/status]` | Quiet empty RALPH cycles while preserving human-triggered wake/route behavior |
|
|
570
|
-
| `/pinet subtree [start/status/spawn/stop]` | Run this worker as a local subtree broker for child followers |
|
|
405
|
+
```bash
|
|
406
|
+
cd slack-bridge
|
|
407
|
+
npm pack --dry-run
|
|
408
|
+
```
|
|
571
409
|
|
|
572
|
-
###
|
|
410
|
+
### Development
|
|
573
411
|
|
|
574
|
-
|
|
412
|
+
Build the package:
|
|
575
413
|
|
|
576
|
-
|
|
414
|
+
```bash
|
|
415
|
+
cd slack-bridge
|
|
416
|
+
pnpm build
|
|
417
|
+
```
|
|
577
418
|
|
|
578
|
-
|
|
579
|
-
- `foundation` / `foundation/space` / `space` — JSON descriptor with curated institutional sci-fi characters, full-name aliases, and archive, relay, frontier, and crisis-room flavor.
|
|
580
|
-
- `cosmere` / `cosmere-inspired` / `oathgate` — JSON descriptor with curated/prebaked 1–3 word identities, static emoji, and whimsical Mistborn/Stormlight/Emberdark-inspired agents, spren, artifacts, places, and jokes while avoiding exact third-party character names.
|
|
419
|
+
Run tests:
|
|
581
420
|
|
|
582
|
-
|
|
421
|
+
```bash
|
|
422
|
+
pnpm test
|
|
423
|
+
```
|
|
583
424
|
|
|
584
|
-
|
|
425
|
+
Deploy the Slack manifest:
|
|
585
426
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
- Thread ownership is first-responder-wins — the first agent to reply claims the thread
|
|
427
|
+
```bash
|
|
428
|
+
pnpm deploy:slack
|
|
429
|
+
```
|
|
590
430
|
|
|
591
431
|
## Security
|
|
592
432
|
|
|
593
|
-
|
|
594
|
-
- **Tool guardrails**: `security.readOnly`, `security.requireConfirmation`, and `security.blockedTools` are runtime-enforced for Slack-triggered turns, including core tools such as `bash`, `edit`, and `write`
|
|
595
|
-
- **Guardrail posture**: If Slack/Pinet access is enabled for admitted users and `security.readOnly`, `security.blockedTools`, and `security.requireConfirmation` are all effectively empty (`readOnly !== true` and both arrays are absent or empty), the bridge emits a startup/runtime warning and `/pinet status` shows `Guardrails: empty (warn-first posture; behavior unchanged)`. This is visibility-only: it does **not** auto-enable `readOnly`, block startup, or require an acknowledgement flow.
|
|
596
|
-
- **Mesh authentication**: Optional. Configure `meshSecret` or `meshSecretPath` (or `PINET_MESH_SECRET` / `PINET_MESH_SECRET_PATH`) to require a shared secret; leave them unset to disable shared-secret auth. Configured followers fail closed on missing secret files or older/no-auth brokers rather than silently downgrading.
|
|
597
|
-
|
|
598
|
-
Find Slack user IDs: click a user's profile → **More** → **Copy member ID**.
|
|
433
|
+
### Default-deny access
|
|
599
434
|
|
|
600
|
-
|
|
435
|
+
Pinet requires explicit configuration to allow users. Without `allowedUsers` or `allowAllWorkspaceUsers`, nobody can use it.
|
|
601
436
|
|
|
602
|
-
|
|
437
|
+
### Token safety
|
|
603
438
|
|
|
604
|
-
|
|
439
|
+
- Never commit tokens to git
|
|
440
|
+
- Use environment variables in production
|
|
441
|
+
- Rotate tokens regularly
|
|
442
|
+
- Use separate apps for development and production
|
|
605
443
|
|
|
606
|
-
|
|
607
|
-
pnpm run build
|
|
608
|
-
```
|
|
444
|
+
### Confirmation for dangerous actions
|
|
609
445
|
|
|
610
|
-
|
|
446
|
+
Configure confirmation for sensitive operations:
|
|
611
447
|
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
```bash
|
|
621
|
-
pnpm deploy:slack
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"slack-bridge": {
|
|
451
|
+
"security": {
|
|
452
|
+
"requireConfirmation": ["slack:create_channel", "slack:upload", "slack:delete"]
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
622
456
|
```
|
|
623
457
|
|
|
624
|
-
|
|
458
|
+
### Read-only mode
|
|
625
459
|
|
|
626
|
-
|
|
460
|
+
Prevent all modifications:
|
|
627
461
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
-
|
|
631
|
-
|
|
632
|
-
|
|
462
|
+
```json
|
|
463
|
+
{
|
|
464
|
+
"slack-bridge": {
|
|
465
|
+
"security": {
|
|
466
|
+
"readOnly": true
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
633
471
|
|
|
634
|
-
##
|
|
472
|
+
## Support
|
|
635
473
|
|
|
636
|
-
|
|
474
|
+
- [GitHub repository](https://github.com/gugu91/extensions)
|
|
475
|
+
- [Architecture documentation](../plans/)
|
|
476
|
+
- Check the log channel in Slack for runtime issues
|
|
@@ -1078,6 +1078,14 @@ export class BrokerSocketServer {
|
|
|
1078
1078
|
}
|
|
1079
1079
|
// ─── Adapter capability handler ───────────────────────
|
|
1080
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
|
+
}
|
|
1081
1089
|
const params = req.params ?? {};
|
|
1082
1090
|
const adapterName = typeof params.adapter === "string"
|
|
1083
1091
|
? params.adapter.trim()
|
|
@@ -1097,6 +1105,12 @@ export class BrokerSocketServer {
|
|
|
1097
1105
|
return await this.dispatchAdapterCapability(req.id, adapterName, capability, capabilityParams, state);
|
|
1098
1106
|
}
|
|
1099
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
|
+
}
|
|
1100
1114
|
const params = req.params ?? {};
|
|
1101
1115
|
const method = typeof params.method === "string" ? params.method.trim() : "";
|
|
1102
1116
|
if (!method) {
|
|
@@ -1112,6 +1126,15 @@ export class BrokerSocketServer {
|
|
|
1112
1126
|
if (!adapter?.invokeCapability) {
|
|
1113
1127
|
return rpcError(id, RPC_METHOD_NOT_FOUND, `Adapter ${adapterName} does not implement capability ${capability}`);
|
|
1114
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
|
+
}
|
|
1115
1138
|
try {
|
|
1116
1139
|
const response = await adapter.invokeCapability({ capability, params: capabilityParams });
|
|
1117
1140
|
this.applyAdapterCapabilityEffects(adapterName, response, state);
|
|
@@ -1122,6 +1145,36 @@ export class BrokerSocketServer {
|
|
|
1122
1145
|
return rpcError(id, RPC_INTERNAL_ERROR, `${errorPrefix}: ${message}`);
|
|
1123
1146
|
}
|
|
1124
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
|
+
}
|
|
1125
1178
|
applyAdapterCapabilityEffects(adapterName, response, state) {
|
|
1126
1179
|
if (!state.agentId)
|
|
1127
1180
|
return;
|
package/dist/index.js
CHANGED
|
@@ -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 = {
|
package/dist/pinet-mesh-ops.js
CHANGED
|
@@ -34,6 +34,7 @@ function appendSlackThreadTransferNotice(body, threadId, channel) {
|
|
|
34
34
|
`- channel: ${channel}`,
|
|
35
35
|
`- To report directly in the transferred Slack thread, use slack_send with thread_ts ${threadId}; the channel is already recorded in Pinet.`,
|
|
36
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).",
|
|
37
38
|
].join("\n");
|
|
38
39
|
}
|
|
39
40
|
export function parseGitHubRemoteRepo(remoteUrl) {
|
package/dist/slack-tools.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ export interface SlackPinetDeliveryResult {
|
|
|
26
26
|
source: string;
|
|
27
27
|
}
|
|
28
28
|
export interface SlackPinetDeliveryPort {
|
|
29
|
+
/**
|
|
30
|
+
* True when Pinet is configured/enabled for this session, regardless of
|
|
31
|
+
* live broker connectivity. When true, threaded Slack replies MUST route
|
|
32
|
+
* through the broker; direct Slack fallback is refused even if the broker
|
|
33
|
+
* is momentarily unavailable, to preserve broker-enforced thread
|
|
34
|
+
* ownership (see gugu91/extensions#855).
|
|
35
|
+
*/
|
|
36
|
+
isEnabled: () => boolean;
|
|
29
37
|
isAvailable: () => boolean;
|
|
30
38
|
sendSlackMessage: (input: SlackPinetDeliveryInput) => Promise<SlackPinetDeliveryResult>;
|
|
31
39
|
}
|
package/dist/slack-tools.js
CHANGED
|
@@ -180,25 +180,6 @@ function isAbortError(error) {
|
|
|
180
180
|
function getErrorMessage(error) {
|
|
181
181
|
return error instanceof Error ? error.message : String(error);
|
|
182
182
|
}
|
|
183
|
-
function isPinetDeliveryFallbackError(error) {
|
|
184
|
-
const lower = getErrorMessage(error).toLowerCase();
|
|
185
|
-
if (lower.includes("already owned"))
|
|
186
|
-
return false;
|
|
187
|
-
return (lower.includes("not running") ||
|
|
188
|
-
lower.includes("unexpected state") ||
|
|
189
|
-
lower.includes("unavailable") ||
|
|
190
|
-
lower.includes("not connected") ||
|
|
191
|
-
lower.includes("disconnected") ||
|
|
192
|
-
lower.includes("timeout") ||
|
|
193
|
-
lower.includes("timed out") ||
|
|
194
|
-
lower.includes("econn") ||
|
|
195
|
-
lower.includes("socket") ||
|
|
196
|
-
lower.includes("no transport source") ||
|
|
197
|
-
lower.includes("no transport channel") ||
|
|
198
|
-
lower.includes("only allows local file paths") ||
|
|
199
|
-
lower.includes("no adapter") ||
|
|
200
|
-
lower.includes("identity is unavailable"));
|
|
201
|
-
}
|
|
202
183
|
function classifySlackDispatcherError(error) {
|
|
203
184
|
const message = getErrorMessage(error);
|
|
204
185
|
const lower = message.toLowerCase();
|
|
@@ -423,6 +404,7 @@ function buildSlackSendPromptGuidelines() {
|
|
|
423
404
|
return [
|
|
424
405
|
"Use slack_send for replies in the current Slack assistant thread; always reply where the task came from.",
|
|
425
406
|
"For rich Block Kit JSON examples or modal/canvas patterns, load the slack-bridge skill instead of relying on tool schemas.",
|
|
407
|
+
"If slack_send fails with 'broker is unavailable' or 'already owned by another agent', do NOT retry via post_channel or a different tool \u2014 report the blocker in the same thread (or to the broker) and wait for ownership to be transferred; direct posting would bypass thread ownership (#855).",
|
|
426
408
|
];
|
|
427
409
|
}
|
|
428
410
|
function getSlackCanvasSummary(markdown) {
|
|
@@ -624,32 +606,31 @@ export function registerSlackTools(pi, deps) {
|
|
|
624
606
|
}
|
|
625
607
|
async function deliverSlackMessage(input) {
|
|
626
608
|
const blocks = input.blocks ? normalizeSlackBlocksInput(input.blocks) : undefined;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
609
|
+
// Threaded Slack replies routed through Pinet MUST NOT fall back to direct
|
|
610
|
+
// Slack when the broker is unavailable or the delivery errors: direct
|
|
611
|
+
// Slack posting bypasses broker thread-ownership enforcement and lets any
|
|
612
|
+
// worker with a valid bot token take over a Slack thread it does not own.
|
|
613
|
+
// See gugu91/extensions#855. Single-player mode (isEnabled === false) and
|
|
614
|
+
// top-level channel posts (no threadTs) remain unaffected.
|
|
615
|
+
if (input.threadTs && pinetDelivery?.isEnabled()) {
|
|
616
|
+
if (!pinetDelivery.isAvailable()) {
|
|
617
|
+
throw new Error("Cannot post to Slack thread: Pinet broker is unavailable and direct Slack fallback would bypass thread-ownership enforcement. Wait for the broker to reconnect, or ask the broker to transfer this thread.");
|
|
618
|
+
}
|
|
619
|
+
const result = await pinetDelivery.sendSlackMessage({
|
|
620
|
+
threadId: input.threadTs,
|
|
621
|
+
channel: input.channel,
|
|
622
|
+
text: input.text,
|
|
623
|
+
...(blocks ? { blocks } : {}),
|
|
624
|
+
...(input.files ? { files: input.files } : {}),
|
|
625
|
+
});
|
|
626
|
+
return {
|
|
627
|
+
threadTs: input.threadTs,
|
|
628
|
+
channel: result.channel,
|
|
629
|
+
blocksCount: blocks?.length ?? 0,
|
|
630
|
+
delivery: "pinet",
|
|
631
|
+
adapter: result.adapter,
|
|
632
|
+
messageId: result.messageId,
|
|
633
|
+
};
|
|
653
634
|
}
|
|
654
635
|
if (input.files && input.files.length > 0) {
|
|
655
636
|
if (blocks && blocks.length > 0) {
|
|
@@ -676,7 +657,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
676
657
|
channel: input.channel,
|
|
677
658
|
blocksCount: blocks?.length ?? 0,
|
|
678
659
|
delivery: "slack",
|
|
679
|
-
...(fallbackReason ? { fallbackReason } : {}),
|
|
680
660
|
};
|
|
681
661
|
}
|
|
682
662
|
const body = {
|
|
@@ -700,7 +680,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
700
680
|
channel: input.channel,
|
|
701
681
|
blocksCount: blocks?.length ?? 0,
|
|
702
682
|
delivery: "slack",
|
|
703
|
-
...(fallbackReason ? { fallbackReason } : {}),
|
|
704
683
|
};
|
|
705
684
|
}
|
|
706
685
|
const slackActionRegistry = new Map();
|
|
@@ -1479,7 +1458,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
1479
1458
|
filesCount: Array.isArray(params.files) ? params.files.length : 0,
|
|
1480
1459
|
...(delivery.adapter ? { adapter: delivery.adapter } : {}),
|
|
1481
1460
|
...(delivery.messageId ? { messageId: delivery.messageId } : {}),
|
|
1482
|
-
...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
|
|
1483
1461
|
},
|
|
1484
1462
|
};
|
|
1485
1463
|
},
|
|
@@ -2194,7 +2172,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
2194
2172
|
filesCount: Array.isArray(params.files) ? params.files.length : 0,
|
|
2195
2173
|
...(delivery.adapter ? { adapter: delivery.adapter } : {}),
|
|
2196
2174
|
...(delivery.messageId ? { messageId: delivery.messageId } : {}),
|
|
2197
|
-
...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
|
|
2198
2175
|
},
|
|
2199
2176
|
};
|
|
2200
2177
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinet/slack-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi package for Pinet Slack assistant integration — multi-agent broker, thread routing, and inbox tools",
|
|
6
6
|
"author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"test": "vitest run"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@pinet/broker-core": "0.2.
|
|
53
|
-
"@pinet/imessage-bridge": "0.2.
|
|
54
|
-
"@pinet/pinet-core": "0.2.
|
|
55
|
-
"@pinet/transport-core": "0.2.
|
|
52
|
+
"@pinet/broker-core": "0.2.4",
|
|
53
|
+
"@pinet/imessage-bridge": "0.2.4",
|
|
54
|
+
"@pinet/pinet-core": "0.2.4",
|
|
55
|
+
"@pinet/transport-core": "0.2.4",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49"
|
|
57
57
|
},
|
|
58
58
|
"types": "./dist/index.d.ts",
|