claude-slack-channel-bots 0.6.6 → 0.6.7
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 +43 -8
- package/package.json +2 -3
- package/skills/setup-slack-channel-bots/SKILL.md +37 -88
- package/src/agent-director-client.ts +91 -1
- package/src/agent-director-startup.ts +183 -3
- package/src/lib.ts +8 -0
- package/src/permission-action-id.ts +24 -30
- package/src/permission-click-handler.ts +60 -109
- package/src/permission-poller.ts +247 -80
- package/src/registry.ts +46 -45
- package/src/server.ts +41 -25
- package/src/session-manager.ts +105 -2
- package/src/trust-bootstrap.ts +132 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ See the sections below for manual configuration details if you prefer not to use
|
|
|
38
38
|
|
|
39
39
|
- [Bun](https://bun.sh) `>= 1.0.21` (agent-director minimum)
|
|
40
40
|
- [Claude Code](https://claude.ai/code) installed and authenticated
|
|
41
|
-
- [`agent-director`](https://github.com/gabemahoney/agent-director) **runtime dependency** — pulled in transitively when you install this package. Hard required at server boot: CSCB refuses to start if the library is missing, the host platform is unsupported, Bun is too old, or the installed version is below `MIN_AD_VERSION` (`^0.
|
|
41
|
+
- [`agent-director`](https://github.com/gabemahoney/agent-director) **runtime dependency** — pulled in transitively when you install this package. Hard required at server boot: CSCB refuses to start if the library is missing, the host platform is unsupported, Bun is too old, or the installed version is below `MIN_AD_VERSION` (`^0.6.3`). agent-director itself requires [tmux](https://github.com/tmux/tmux) on the operator's PATH; CSCB no longer probes for it directly.
|
|
42
42
|
- Slack workspace admin access (to create and configure the Slack app)
|
|
43
43
|
- **cozempic** (optional) — Python 3.10+ and `pip install cozempic` — used by JSONL path resolution helpers retained for downstream callers.
|
|
44
44
|
|
|
@@ -54,7 +54,7 @@ See the sections below for manual configuration details if you prefer not to use
|
|
|
54
54
|
|
|
55
55
|
If the host is unsupported, the SR-5.1 startup gate exits non-zero at server boot with a typed error from agent-director (`ErrPlatformPackageMissing` or `ErrUnsupportedPlatform`) and writes the failure to `~/.claude/channels/slack/startup-errors.log` and stderr. See [Startup errors](#startup-errors) below.
|
|
56
56
|
|
|
57
|
-
> **Note on agent-director versions.** v0.4.1 is a zombie release (the published tarball is missing `dist/` and cannot be imported). v0.4.2 lacks the `MakeTemplateParams.overwrite` field CSCB needs for the boot-time template refresh. v0.5.4 and earlier lack `allow_pending` on `readPane`/`sendKeys`, causing `ErrSpawnNotInteractive` during dev-channels dialog approval on freshly-spawned bots. CSCB pins `^0.
|
|
57
|
+
> **Note on agent-director versions.** v0.4.1 is a zombie release (the published tarball is missing `dist/` and cannot be imported). v0.4.2 lacks the `MakeTemplateParams.overwrite` field CSCB needs for the boot-time template refresh. v0.5.4 and earlier lack `allow_pending` on `readPane`/`sendKeys`, causing `ErrSpawnNotInteractive` during dev-channels dialog approval on freshly-spawned bots. v0.6.0 shipped a stale TS shim whose `Client` dropped `getPermission`, whose `buildDecide()` dropped `--request-token`, and whose error catalog omitted `ErrInvalidFlags` / `ErrPermissionRequestNotFound` / `ErrAmbiguousRequest` — each silently breaks the disambiguation relay. v0.6.1–0.6.2 still lack the full `permission_requests` plural projection + composite-key disambiguation surface CSCB depends on for concurrent open requests. CSCB pins `^0.6.3` to get past all of these.
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
@@ -439,6 +439,9 @@ Classes you may see:
|
|
|
439
439
|
- `ad-cli-not-executable` — the resolved agent-director CLI binary exists but is not executable. Run `chmod +x` on the binary referenced in the log line.
|
|
440
440
|
- `ad-version-probe` — `agent-director` was loaded but the `version()` probe failed (subprocess invocation, platform binary, etc.).
|
|
441
441
|
- `ad-version-stale` — installed `agent-director` is below the minimum version this CSCB requires. Run `bun add agent-director@^<minimum>`.
|
|
442
|
+
- `ad-shim-missing-get-permission` — the installed `agent-director` TS shim's `Client` does not expose `getPermission`. The npm-published package is out of sync with its bundled binary. Reinstall and confirm the resolved version actually ships the method.
|
|
443
|
+
- `ad-shim-catalog-incomplete` — the installed `agent-director` TS error catalog is missing one or more of `ErrInvalidFlags`, `ErrPermissionRequestNotFound`, `ErrAmbiguousRequest`. The log line lists the missing names. Same remediation as `ad-shim-missing-get-permission`.
|
|
444
|
+
- `ad-shim-decide-drops-token` — the installed `agent-director` dist does not include `--request-token` in its bundled JS, meaning `buildDecide()` would resolve permission clicks against the wrong row. Reinstall and confirm `buildDecide` carries the flag.
|
|
442
445
|
- `ad-call-timeout` — an agent-director verb call exceeded the configured `callTimeoutMs` (default 30 s). Investigate the subprocess or increase the timeout.
|
|
443
446
|
- `ad-same-user` — `~/.agent-director/state.db` is owned by a different UID than the CSCB process. Reinstall agent-director as the correct user or remove the mismatched file.
|
|
444
447
|
- `ad-same-user-stat` — Non-ENOENT stat error on the state DB (permissions, I/O). Investigate the file before re-launching.
|
|
@@ -501,11 +504,7 @@ This gracefully exits the managed Claude Code sessions, stops and restarts the s
|
|
|
501
504
|
For operators upgrading from a pre-`agent-director` install:
|
|
502
505
|
|
|
503
506
|
1. **Install the new CSCB**: `bun remove claude-director` (if present) and `bun install -g claude-slack-channel-bots@^<new>`. The `agent-director` library is pulled in transitively — no separate install step.
|
|
504
|
-
2. **Delete any old relay hooks**
|
|
505
|
-
```sh
|
|
506
|
-
rm -f ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
507
|
-
```
|
|
508
|
-
Also remove their entries from `~/.claude/settings.json` if you wired them in by hand previously.
|
|
507
|
+
2. **Delete any old relay hooks** — see [Upgrading from pre-Epic-2 (v0.5.x → v0.6.x)](#upgrading-from-pre-epic-2-v05x--v06x) for the cleanup commands.
|
|
509
508
|
3. **Configure agent-director's `find-missing` sweep**. CSCB does NOT call `client.findMissing(...)`; reconciling stuck rows is the operator's responsibility. Add a cron entry (or systemd timer) that runs `agent-director find-missing` on a cadence that matches your tolerance — e.g. every minute on a busy host:
|
|
510
509
|
```cron
|
|
511
510
|
* * * * * /usr/local/bin/agent-director find-missing --timeout 30s
|
|
@@ -518,5 +517,41 @@ For operators upgrading from a pre-`agent-director` install:
|
|
|
518
517
|
6. **Optional cleanup**: `~/.claude/channels/slack/sessions.json` and `sessions.json.last` are no longer read or written. CSCB ignores them; you can safely `rm` them after a successful boot.
|
|
519
518
|
7. **`tmux` is no longer a CSCB-direct prereq** but is still required transitively via agent-director — keep it installed.
|
|
520
519
|
|
|
521
|
-
After step 1, every CSCB bot is spawned through `client.spawn(...)` with `relay_mode='on'`. The green/red Slack button UX is byte-identical to the pre-migration behavior; the action_id shape changes from `perm_(allow|deny)_<uuid>` to `perm_(allow|deny)_cscb_<channelId>_<
|
|
520
|
+
After step 1, every CSCB bot is spawned through `client.spawn(...)` with `relay_mode='on'`. The green/red Slack button UX is byte-identical to the pre-migration behavior; the action_id shape changes from `perm_(allow|deny)_<uuid>` to `perm_(allow|deny)_cscb_<channelId>_<request_token>` (where `<request_token>` is a UUIDv4 minted by agent-director) but this is invisible to end users.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Upgrading from pre-Epic-2 (v0.5.x → v0.6.x)
|
|
525
|
+
|
|
526
|
+
If you installed CSCB before v0.6.0 you may have legacy artifacts on disk that are no longer needed. Clean them up manually — automatic postinstall migration is tracked under idea b.irf and not yet implemented.
|
|
527
|
+
|
|
528
|
+
### 1. Remove old relay hook files
|
|
529
|
+
|
|
530
|
+
The `.sh` relay hooks are no longer shipped by CSCB. Delete them if present:
|
|
531
|
+
|
|
532
|
+
```sh
|
|
533
|
+
rm -f ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### 2. Remove orphan settings.json hook entries
|
|
537
|
+
|
|
538
|
+
If you wired the hooks into `~/.claude/settings.json` by hand, remove the stale entries. Use this `jq` filter to check whether any are present:
|
|
539
|
+
|
|
540
|
+
```sh
|
|
541
|
+
jq '
|
|
542
|
+
(.hooks.PermissionRequest // [] | map(select(.hooks[]?.command | strings | test("\\.sh$")))),
|
|
543
|
+
(.hooks.PreToolUse // [] | map(select(.matcher == "AskUserQuestion" and (.hooks[]?.command | strings | test("\\.sh$")))))
|
|
544
|
+
' ~/.claude/settings.json 2>/dev/null
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Any non-empty arrays in the output are orphan entries. Remove:
|
|
548
|
+
|
|
549
|
+
- Any object inside `hooks.PermissionRequest` whose `hooks[].command` ends in `permission-relay.sh`.
|
|
550
|
+
- Any object inside `hooks.PreToolUse` with `"matcher": "AskUserQuestion"` whose `hooks[].command` ends in `ask-relay.sh`.
|
|
551
|
+
|
|
552
|
+
The modern permission relay runs automatically via agent-director — no `PermissionRequest` or `PreToolUse` hook entries for `.sh` files are needed.
|
|
553
|
+
|
|
554
|
+
### 3. Note on automated migration
|
|
555
|
+
|
|
556
|
+
A postinstall step that performs this cleanup automatically is tracked under idea b.irf. Until that lands, the steps above are manual.
|
|
522
557
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slack-channel-bots",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Multi-session Slack-to-Claude bridge — run multiple Claude Code bots across Slack channels via Socket Mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"src/*.ts",
|
|
11
11
|
"!src/*.test.ts",
|
|
12
|
-
"hooks/",
|
|
13
12
|
"scripts/fixup-bun-cache.ts",
|
|
14
13
|
"slack-app-manifest.yml",
|
|
15
14
|
"README.md",
|
|
@@ -30,7 +29,7 @@
|
|
|
30
29
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
31
30
|
"@slack/socket-mode": "^2.0.0",
|
|
32
31
|
"@slack/web-api": "^7.0.0",
|
|
33
|
-
"agent-director": "^0.
|
|
32
|
+
"agent-director": "^0.6.3"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"@types/bun": "^1.0.0",
|
|
@@ -343,104 +343,53 @@ Do not modify `access.json` during setup unless the user asks to.
|
|
|
343
343
|
|
|
344
344
|
---
|
|
345
345
|
|
|
346
|
-
### Step 7 —
|
|
346
|
+
### Step 7 — Verify agent-director is installed
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
The modern permission relay runs automatically via agent-director — no
|
|
349
|
+
operator action is required for permission-relay or ask-handling.
|
|
350
|
+
`AskUserQuestion` is denied at the agent-director template level (SR-3.1).
|
|
349
351
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**If either hook is missing:**
|
|
355
|
-
|
|
356
|
-
Provide the copy and chmod commands. The source path depends on how the
|
|
357
|
-
package was installed. Try to detect it:
|
|
358
|
-
|
|
359
|
-
```bash
|
|
360
|
-
# Attempt to find the package hooks directory
|
|
361
|
-
bun pm ls -g 2>/dev/null | grep -o "claude-slack-channel-bots.*" | head -1 || true
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
Show the user the exact commands:
|
|
365
|
-
|
|
366
|
-
```bash
|
|
367
|
-
mkdir -p ~/.claude/hooks
|
|
368
|
-
|
|
369
|
-
# Copy from the installed package (adjust path if needed):
|
|
370
|
-
cp "$(npm root -g)/claude-slack-channel-bots/hooks/permission-relay.sh" ~/.claude/hooks/
|
|
371
|
-
cp "$(npm root -g)/claude-slack-channel-bots/hooks/ask-relay.sh" ~/.claude/hooks/
|
|
372
|
-
chmod +x ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
Or, to keep them in sync with future package updates, symlink instead:
|
|
352
|
+
Check that agent-director is installed and the `slack-channel-bot` template
|
|
353
|
+
is registered:
|
|
376
354
|
|
|
377
355
|
```bash
|
|
378
|
-
|
|
379
|
-
ln -sf "$HOOKS_DIR/permission-relay.sh" ~/.claude/hooks/permission-relay.sh
|
|
380
|
-
ln -sf "$HOOKS_DIR/ask-relay.sh" ~/.claude/hooks/ask-relay.sh
|
|
356
|
+
agent-director --version
|
|
381
357
|
```
|
|
382
358
|
|
|
383
|
-
|
|
384
|
-
|
|
359
|
+
The `slack-channel-bot` template is registered automatically at CSCB startup
|
|
360
|
+
via `client.makeTemplate(...)`. Confirm the template exists after a successful
|
|
361
|
+
`claude-slack-channel-bots start` by checking the agent-director template
|
|
362
|
+
registry. If the startup log shows `ad-template-install` in
|
|
363
|
+
`$STATE_DIR/startup-errors.log`, the template registration
|
|
364
|
+
failed — investigate that error before proceeding.
|
|
385
365
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
chmod +x ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
390
|
-
```
|
|
366
|
+
If `agent-director` is not on `PATH`, postinstall did not complete successfully.
|
|
367
|
+
Re-run `bun postinstall.ts` from the installed package directory, or reinstall
|
|
368
|
+
CSCB with `bun install -g claude-slack-channel-bots`.
|
|
391
369
|
|
|
392
370
|
---
|
|
393
371
|
|
|
394
|
-
### Step 8 — Check
|
|
395
|
-
|
|
396
|
-
Read `~/.claude/settings.json` and check whether the `PermissionRequest` and
|
|
397
|
-
`PreToolUse` hook entries for the relay scripts are present.
|
|
372
|
+
### Step 8 — Check settings.json for orphan legacy hook entries
|
|
398
373
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
{ "type": "command", "command": "~/.claude/hooks/permission-relay.sh" }
|
|
404
|
-
```
|
|
374
|
+
Operators upgrading from pre-Epic-2 (v0.5.x) may have `PermissionRequest` or
|
|
375
|
+
`PreToolUse` `AskUserQuestion` entries in `~/.claude/settings.json` that point
|
|
376
|
+
at the old `.sh` files. These are harmless but stale — the `.sh` files no
|
|
377
|
+
longer exist and agent-director owns the relay machinery now.
|
|
405
378
|
|
|
406
|
-
|
|
379
|
+
Check for orphan entries:
|
|
407
380
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
**If either entry is missing OR exists but is missing `"timeout": 2000000`**,
|
|
414
|
-
show the user the exact JSON to add or fix. The timeout is critical — without
|
|
415
|
-
it Claude Code uses a short default timeout, kills the hook before the
|
|
416
|
-
long-poll completes, and falls back to TUI approval. This is the complete
|
|
417
|
-
block for both entries:
|
|
418
|
-
|
|
419
|
-
```jsonc
|
|
420
|
-
"PermissionRequest": [
|
|
421
|
-
{
|
|
422
|
-
"matcher": ".*",
|
|
423
|
-
"timeout": 2000000,
|
|
424
|
-
"hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-relay.sh" }]
|
|
425
|
-
}
|
|
426
|
-
],
|
|
427
|
-
"PreToolUse": [
|
|
428
|
-
{
|
|
429
|
-
"matcher": "AskUserQuestion",
|
|
430
|
-
"timeout": 2000000,
|
|
431
|
-
"hooks": [{ "type": "command", "command": "~/.claude/hooks/ask-relay.sh" }]
|
|
432
|
-
}
|
|
433
|
-
]
|
|
381
|
+
```bash
|
|
382
|
+
jq '
|
|
383
|
+
(.hooks.PermissionRequest // [] | map(select(.hooks[]?.command | strings | test("\\.sh$")))),
|
|
384
|
+
(.hooks.PreToolUse // [] | map(select(.matcher == "AskUserQuestion" and (.hooks[]?.command | strings | test("\\.sh$")))))
|
|
385
|
+
' ~/.claude/settings.json 2>/dev/null
|
|
434
386
|
```
|
|
435
387
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
Offer to write the missing entries automatically to `~/.claude/settings.json`.
|
|
443
|
-
If the user agrees, make the targeted edits, preserving all existing content.
|
|
388
|
+
If either output array is non-empty, orphan entries are present. Show the user
|
|
389
|
+
the relevant block from `settings.json` and advise manual removal of any
|
|
390
|
+
`PermissionRequest` entries whose `hooks[].command` ends in `permission-relay.sh`,
|
|
391
|
+
and any `PreToolUse` entries with `matcher: "AskUserQuestion"` whose
|
|
392
|
+
`hooks[].command` ends in `ask-relay.sh`.
|
|
444
393
|
|
|
445
394
|
---
|
|
446
395
|
|
|
@@ -450,13 +399,13 @@ Print a final summary of what was checked and configured:
|
|
|
450
399
|
|
|
451
400
|
- Environment variables: set / missing
|
|
452
401
|
- Token format: valid / invalid
|
|
453
|
-
- config.json:
|
|
402
|
+
- ~/.claude/channels/slack/config.json: present and valid (N routes) / missing or skeleton
|
|
454
403
|
- append_system_prompt_file: configured / skipped
|
|
455
404
|
- access.json: present / missing
|
|
456
|
-
-
|
|
457
|
-
-
|
|
458
|
-
- settings.json PermissionRequest
|
|
459
|
-
- settings.json PreToolUse
|
|
405
|
+
- agent-director: installed (version X) / missing
|
|
406
|
+
- slack-channel-bot template: registered (confirmed at startup) / unconfirmed
|
|
407
|
+
- settings.json orphan PermissionRequest (.sh) hooks: none / found (needs cleanup)
|
|
408
|
+
- settings.json orphan PreToolUse AskUserQuestion (.sh) hooks: none / found (needs cleanup)
|
|
460
409
|
|
|
461
410
|
Remind the user:
|
|
462
411
|
|
|
@@ -27,7 +27,13 @@
|
|
|
27
27
|
import { readFileSync } from 'node:fs'
|
|
28
28
|
import { dirname, join } from 'node:path'
|
|
29
29
|
import { fileURLToPath } from 'node:url'
|
|
30
|
-
import { Client } from 'agent-director'
|
|
30
|
+
import { AgentDirectorError, Client } from 'agent-director'
|
|
31
|
+
import type {
|
|
32
|
+
DecideParams as ADDecideParams,
|
|
33
|
+
DecideResult,
|
|
34
|
+
GetPermissionParams as ADGetPermissionParams,
|
|
35
|
+
GetPermissionResult as ADGetPermissionResult,
|
|
36
|
+
} from 'agent-director'
|
|
31
37
|
|
|
32
38
|
// ---------------------------------------------------------------------------
|
|
33
39
|
// Constants
|
|
@@ -131,3 +137,87 @@ export function resetClientForTests(): void {
|
|
|
131
137
|
export function setClientForTests(client: Client): void {
|
|
132
138
|
singleton = client
|
|
133
139
|
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// decide-wire wrapper (SR-4.1, SR-7.2)
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* CSCB decide params — extends `DecideParams` with the `request_token` field
|
|
147
|
+
* (SR-7.2). The snake-case JSON field flows through the subprocess-CLI
|
|
148
|
+
* transport without further marshaling on this side.
|
|
149
|
+
*
|
|
150
|
+
* Typing `request_token` as a required field at the wrapper boundary is how
|
|
151
|
+
* SR-4.1 (unconditional pass-through) is enforced statically — every CSCB
|
|
152
|
+
* decide call site must construct one of these.
|
|
153
|
+
*/
|
|
154
|
+
export interface DecideParamsWithToken extends ADDecideParams {
|
|
155
|
+
request_token: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Always-include-token decide wrapper. MIN_AD_VERSION is now pinned at
|
|
160
|
+
* `^0.6.0`, which carries `request_token` on `DecideParams` natively. The
|
|
161
|
+
* structural cast is retained because, until the lockfile is refreshed to
|
|
162
|
+
* resolve the bumped pin, the imported `DecideParams` type still comes from
|
|
163
|
+
* the locked `0.5.6` package — same lockfile-lag pattern as
|
|
164
|
+
* `GetResultWithPermissionRequests`.
|
|
165
|
+
*
|
|
166
|
+
* Routing the single CSCB decide call site through this function gives the
|
|
167
|
+
* codebase one definitive serializer for the decide wire shape (SR-7.2).
|
|
168
|
+
*/
|
|
169
|
+
export async function decideWithToken(
|
|
170
|
+
client: Pick<Client, 'decide'>,
|
|
171
|
+
params: DecideParamsWithToken,
|
|
172
|
+
): Promise<DecideResult> {
|
|
173
|
+
return client.decide(params as unknown as ADDecideParams)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// get-permission wrapper (SR-7.1)
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Params + result for AD's `get-permission` verb (shipped in `^0.6.1`+).
|
|
182
|
+
* Re-exported from `agent-director` so the local consumers
|
|
183
|
+
* (`permission-poller`, `permission-click-handler`, tests) reference one
|
|
184
|
+
* source of truth. `decision` / `decision_reason` / `decided_at` are
|
|
185
|
+
* optional + nullable on AD's type because the same row shape covers both
|
|
186
|
+
* open and closed states; `classifyVerdict` collapses any non-canonical
|
|
187
|
+
* combination into the SR-5.2 fail-closed generic-deny path.
|
|
188
|
+
*/
|
|
189
|
+
export type GetPermissionParams = ADGetPermissionParams
|
|
190
|
+
export type GetPermissionResult = ADGetPermissionResult
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Local sentinel matcher for AD's `ErrPermissionRequestNotFound`. Matches on
|
|
194
|
+
* `errName` so both the typed class (`^0.6.0`+ exposes it) and the stub's
|
|
195
|
+
* AgentDirectorError-shape resolve cleanly. The errName-based predicate keeps
|
|
196
|
+
* working with the typed class when the lockfile catches up to the bumped
|
|
197
|
+
* pin (same pattern as `ErrInvalidFlags`).
|
|
198
|
+
*/
|
|
199
|
+
export function isErrPermissionRequestNotFound(err: unknown): boolean {
|
|
200
|
+
return err instanceof AgentDirectorError && err.errName === 'ErrPermissionRequestNotFound'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `get-permission` wrapper (SR-7.1). MIN_AD_VERSION is pinned at `^0.6.0`,
|
|
205
|
+
* which carries the verb; the SR-6.1 startup gate is the compatibility
|
|
206
|
+
* boundary that keeps stale-version installs from reaching this path. The
|
|
207
|
+
* wrapper takes a structural client (rather than `Pick<Client, 'getPermission'>`)
|
|
208
|
+
* because the locked `0.5.6` types do not yet expose the method shape, and
|
|
209
|
+
* fails loudly at runtime when the verb is missing as defense-in-depth for
|
|
210
|
+
* the dev/test edge (e.g. stub clients that omit it on purpose).
|
|
211
|
+
*/
|
|
212
|
+
export async function getPermission(
|
|
213
|
+
client: { getPermission?: (params: GetPermissionParams) => Promise<GetPermissionResult> },
|
|
214
|
+
params: GetPermissionParams,
|
|
215
|
+
): Promise<GetPermissionResult> {
|
|
216
|
+
if (typeof client.getPermission !== 'function') {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'agent-director-client: getPermission verb unavailable — ' +
|
|
219
|
+
'confirm installed agent-director version meets the MIN_AD_VERSION pin',
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
return client.getPermission(params)
|
|
223
|
+
}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* agent-director-startup.ts — SR-5.1 startup gate for the agent-director
|
|
3
3
|
* library dependency.
|
|
4
4
|
*
|
|
5
|
-
* Runs the
|
|
6
|
-
* work:
|
|
5
|
+
* Runs the boot sequence the SRD requires before any other CSCB work:
|
|
7
6
|
*
|
|
8
7
|
* 1. import { Client } from 'agent-director' — covered at module load
|
|
9
8
|
* time; module-not-found surfaces as a top-level import error caught
|
|
@@ -14,6 +13,12 @@
|
|
|
14
13
|
* ErrBunVersionTooOld; other throws surface verbatim.
|
|
15
14
|
* 3. await client.version({}) — strip leading 'v',
|
|
16
15
|
* compare to MIN_AD_VERSION via semverGte.
|
|
16
|
+
* 3.5. API surface probes (SR-6.1 publish-skew defense) — short-circuit,
|
|
17
|
+
* run in order: probeGetPermission (ad-shim-missing-get-permission) →
|
|
18
|
+
* probeErrorCatalog (ad-shim-catalog-incomplete) → probeDecideArgv
|
|
19
|
+
* (ad-shim-decide-drops-token). These exist because a passing version
|
|
20
|
+
* check does not prove the npm-shipped TS shim matches the bundled Go
|
|
21
|
+
* binary; each missing surface silently breaks the permission relay.
|
|
17
22
|
* 4. stat ~/.agent-director/state.db — compare st_uid to
|
|
18
23
|
* geteuid(). ENOENT passes (the row is created on first verb call);
|
|
19
24
|
* other stat errors are fatal.
|
|
@@ -27,6 +32,7 @@
|
|
|
27
32
|
|
|
28
33
|
import * as fs from 'node:fs'
|
|
29
34
|
import * as os from 'node:os'
|
|
35
|
+
import { fileURLToPath } from 'node:url'
|
|
30
36
|
import { join } from 'node:path'
|
|
31
37
|
|
|
32
38
|
import {
|
|
@@ -57,6 +63,54 @@ const AD_STATE_DB_PATH = join(os.homedir(), '.agent-director', 'state.db')
|
|
|
57
63
|
/** Supported platforms surfaced in operator-facing remediation text. */
|
|
58
64
|
const SUPPORTED_PLATFORMS = ['linux-x64', 'darwin-arm64'] as const
|
|
59
65
|
|
|
66
|
+
/**
|
|
67
|
+
* err_names CSCB's runtime branches on. The structural-drift indicator we
|
|
68
|
+
* guard against is whether the `agent-director` dist file ships a class
|
|
69
|
+
* declaration for each — i.e. matches `class <Name> extends ...`. The class
|
|
70
|
+
* declaration is the defining symptom of a shim that's caught up to the
|
|
71
|
+
* bundled Go binary: when one is missing, `instanceof Err<Name>` predicates
|
|
72
|
+
* silently downgrade and the runtime `errorFromEnvelope` falls back to a
|
|
73
|
+
* base `AgentDirectorError` instead of the typed subclass. A bare-identifier
|
|
74
|
+
* match in the dist source is too loose because the shipped shim also
|
|
75
|
+
* carries a metadata array (`{ name: "ErrPermissionRequestNotFound", ... }`)
|
|
76
|
+
* that mentions the names even when the class is absent — so the regex
|
|
77
|
+
* must anchor on `class <Name>\s`.
|
|
78
|
+
*/
|
|
79
|
+
const REQUIRED_ERR_NAMES = [
|
|
80
|
+
'ErrInvalidFlags',
|
|
81
|
+
'ErrPermissionRequestNotFound',
|
|
82
|
+
'ErrAmbiguousRequest',
|
|
83
|
+
] as const
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Private helper: resolve + read the agent-director dist source (memoized)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Source-text cache for the resolved `agent-director` dist entry. Probes 2
|
|
91
|
+
* and 3 both grep the same file; reading it once keeps boot fast and avoids
|
|
92
|
+
* surprising operators with duplicate I/O failures.
|
|
93
|
+
*/
|
|
94
|
+
let cachedAdDistSource: string | Error | null = null
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve `agent-director` via `import.meta.resolve`, convert the file:// URL
|
|
98
|
+
* to a filesystem path, and read the bundled JS as UTF-8. Memoized for the
|
|
99
|
+
* life of the process. Returns the cached `Error` on a previous failure so
|
|
100
|
+
* each probe can surface a uniform `detail` field.
|
|
101
|
+
*/
|
|
102
|
+
function readAdDistSource(): string | Error {
|
|
103
|
+
if (cachedAdDistSource !== null) return cachedAdDistSource
|
|
104
|
+
try {
|
|
105
|
+
const resolved = import.meta.resolve('agent-director')
|
|
106
|
+
const distPath = fileURLToPath(resolved)
|
|
107
|
+
cachedAdDistSource = fs.readFileSync(distPath, 'utf-8')
|
|
108
|
+
} catch (err) {
|
|
109
|
+
cachedAdDistSource = err instanceof Error ? err : new Error(String(err))
|
|
110
|
+
}
|
|
111
|
+
return cachedAdDistSource
|
|
112
|
+
}
|
|
113
|
+
|
|
60
114
|
// ---------------------------------------------------------------------------
|
|
61
115
|
// Pure helper: semver-gte for the three-segment versions AD ships
|
|
62
116
|
// ---------------------------------------------------------------------------
|
|
@@ -114,6 +168,33 @@ export interface StartupGateDeps {
|
|
|
114
168
|
recordStartupError: typeof recordStartupError
|
|
115
169
|
/** Process exit hook (terminates by default). */
|
|
116
170
|
exit: (code: number) => never
|
|
171
|
+
/**
|
|
172
|
+
* Probe 1 — verify the wrapper Client exposes the `getPermission` method.
|
|
173
|
+
* Production default checks `typeof client.getPermission === 'function'`.
|
|
174
|
+
* Tests inject a constant boolean.
|
|
175
|
+
*/
|
|
176
|
+
probeGetPermission: (client: unknown) => boolean
|
|
177
|
+
/**
|
|
178
|
+
* Probe 2 — verify the AD error catalog includes the err_names CSCB
|
|
179
|
+
* branches on. Production default reads the resolved `agent-director`
|
|
180
|
+
* dist file and matches `class <Name>\s` for each required name — the
|
|
181
|
+
* class-declaration site is the structural symptom of a shim that's
|
|
182
|
+
* caught up to the bundled Go binary. A behavioral `errorFromEnvelope`
|
|
183
|
+
* round-trip is unusable (the helper falls back to a base
|
|
184
|
+
* `AgentDirectorError` with `.errName` copied verbatim from the input),
|
|
185
|
+
* and a bare-identifier match false-positives on the dist's metadata
|
|
186
|
+
* array that names err_names even when the class is missing.
|
|
187
|
+
*/
|
|
188
|
+
probeErrorCatalog: () => { ok: true } | { ok: false; missing: string[] }
|
|
189
|
+
/**
|
|
190
|
+
* Probe 3 — verify the wrapper Client's `decide()` argv carries
|
|
191
|
+
* `--request-token`. Production default reads the resolved
|
|
192
|
+
* `agent-director` dist file and greps for the literal `request-token` —
|
|
193
|
+
* a static-file probe (no subprocess, no DB write) chosen over the
|
|
194
|
+
* subprocess fallback per the bug requirements. See the production
|
|
195
|
+
* defaults block for the resolution + read.
|
|
196
|
+
*/
|
|
197
|
+
probeDecideArgv: (client: unknown) => Promise<{ ok: true } | { ok: false; detail: string }>
|
|
117
198
|
}
|
|
118
199
|
|
|
119
200
|
// ---------------------------------------------------------------------------
|
|
@@ -138,6 +219,55 @@ const prodDeps: StartupGateDeps = {
|
|
|
138
219
|
geteuid: () => process.geteuid?.(),
|
|
139
220
|
recordStartupError,
|
|
140
221
|
exit: (code) => process.exit(code),
|
|
222
|
+
probeGetPermission: (client) =>
|
|
223
|
+
typeof (client as { getPermission?: unknown }).getPermission === 'function',
|
|
224
|
+
probeErrorCatalog: () => {
|
|
225
|
+
// Static-file probe: the runtime `errorFromEnvelope` helper falls back
|
|
226
|
+
// to constructing a base `AgentDirectorError` (with `.errName` copied
|
|
227
|
+
// verbatim from the input) when err_name is absent from `ERROR_TABLE`,
|
|
228
|
+
// so a behavioral round-trip cannot tell catalog-present from
|
|
229
|
+
// catalog-absent. A bare-identifier regex is also too loose: the shim
|
|
230
|
+
// carries a metadata array whose `name: "Err..."` literals match every
|
|
231
|
+
// err_name regardless of whether the class is declared. Anchor instead
|
|
232
|
+
// on `class <Name>\s` — the class declaration site is emitted once per
|
|
233
|
+
// typed error and is the defining symptom of a caught-up shim.
|
|
234
|
+
const src = readAdDistSource()
|
|
235
|
+
if (src instanceof Error) {
|
|
236
|
+
// Surface this as "all required names missing" with a diagnostic
|
|
237
|
+
// detail in the first slot, so the operator sees a complete picture.
|
|
238
|
+
return { ok: false, missing: [`<read-failed: ${src.message}>`, ...REQUIRED_ERR_NAMES] }
|
|
239
|
+
}
|
|
240
|
+
const missing: string[] = []
|
|
241
|
+
for (const name of REQUIRED_ERR_NAMES) {
|
|
242
|
+
const re = new RegExp(`class\\s+${name}\\s`)
|
|
243
|
+
if (!re.test(src)) {
|
|
244
|
+
missing.push(name)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return missing.length === 0 ? { ok: true } : { ok: false, missing }
|
|
248
|
+
},
|
|
249
|
+
probeDecideArgv: async (_client) => {
|
|
250
|
+
// Static-file probe: grep the resolved agent-director dist for the
|
|
251
|
+
// literal `--request-token` flag. Chosen over the subprocess fallback
|
|
252
|
+
// because it's faster, touches no AD state, and a stale shipped shim
|
|
253
|
+
// is exactly the file-content condition we're detecting.
|
|
254
|
+
const src = readAdDistSource()
|
|
255
|
+
if (src instanceof Error) {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
detail: `failed to read agent-director dist for argv probe: ${src.message}`,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (src.includes('--request-token')) {
|
|
262
|
+
return { ok: true }
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
detail:
|
|
267
|
+
`agent-director dist does not include the literal '--request-token' — ` +
|
|
268
|
+
`buildDecide() is dropping the field.`,
|
|
269
|
+
}
|
|
270
|
+
},
|
|
141
271
|
}
|
|
142
272
|
|
|
143
273
|
function mergeDeps(overrides?: Partial<StartupGateDeps>): StartupGateDeps {
|
|
@@ -154,7 +284,7 @@ function mergeDeps(overrides?: Partial<StartupGateDeps>): StartupGateDeps {
|
|
|
154
284
|
*/
|
|
155
285
|
export type StartupGateOutcome =
|
|
156
286
|
| { ok: true; client: unknown; adVersion: string }
|
|
157
|
-
| { ok: false; phase: 'construct' | 'version' | 'same-user' | 'unexpected'; classLabel: string; message: string }
|
|
287
|
+
| { ok: false; phase: 'construct' | 'version' | 'api-surface' | 'same-user' | 'unexpected'; classLabel: string; message: string }
|
|
158
288
|
|
|
159
289
|
/**
|
|
160
290
|
* Run the SR-5.1 startup sequence without exiting. The dispatcher
|
|
@@ -280,6 +410,56 @@ export async function runStartupGate(
|
|
|
280
410
|
}
|
|
281
411
|
}
|
|
282
412
|
|
|
413
|
+
// Step 3.5: API surface probes. The version gate above confirms the AD
|
|
414
|
+
// binary meets MIN_AD_VERSION, but published agent-director npm packages
|
|
415
|
+
// have shipped a stale TS shim that drops methods (getPermission), drops
|
|
416
|
+
// CLI flags (--request-token in buildDecide), and misses err_names in the
|
|
417
|
+
// catalog. Each of those silently breaks CSCB at click-handling time. The
|
|
418
|
+
// probes run 1 → 2 → 3 and short-circuit on first failure so one operator
|
|
419
|
+
// message corresponds to one fix.
|
|
420
|
+
if (!d.probeGetPermission(client)) {
|
|
421
|
+
d.closeClient(client)
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
phase: 'api-surface',
|
|
425
|
+
classLabel: 'ad-shim-missing-get-permission',
|
|
426
|
+
message:
|
|
427
|
+
`agent-director Client is missing the 'getPermission' method. ` +
|
|
428
|
+
`The installed shim is stale relative to the AD binary (${adVersion}). ` +
|
|
429
|
+
`Run: bun add agent-director@^${MIN_AD_VERSION} (and confirm the resolved version actually ships getPermission).`,
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const catalogProbe = d.probeErrorCatalog()
|
|
434
|
+
if (!catalogProbe.ok) {
|
|
435
|
+
d.closeClient(client)
|
|
436
|
+
return {
|
|
437
|
+
ok: false,
|
|
438
|
+
phase: 'api-surface',
|
|
439
|
+
classLabel: 'ad-shim-catalog-incomplete',
|
|
440
|
+
message:
|
|
441
|
+
`agent-director TS error catalog is missing required err_names: ` +
|
|
442
|
+
`${catalogProbe.missing.join(', ')}. ` +
|
|
443
|
+
`Envelopes with these names would surface as the base AgentDirectorError ` +
|
|
444
|
+
`instead of typed subclasses, breaking CSCB's instanceof Err<Name> branches. ` +
|
|
445
|
+
`Run: bun add agent-director@^${MIN_AD_VERSION} (confirm the resolved package ships the full catalog).`,
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const argvProbe = await d.probeDecideArgv(client)
|
|
450
|
+
if (!argvProbe.ok) {
|
|
451
|
+
d.closeClient(client)
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
phase: 'api-surface',
|
|
455
|
+
classLabel: 'ad-shim-decide-drops-token',
|
|
456
|
+
message:
|
|
457
|
+
`agent-director shim's decide() does not pass --request-token to the CLI: ${argvProbe.detail} ` +
|
|
458
|
+
`Permission clicks would resolve against the wrong row. ` +
|
|
459
|
+
`Run: bun add agent-director@^${MIN_AD_VERSION} (confirm the resolved package's buildDecide includes the flag).`,
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
283
463
|
// Step 4: same-user check on ~/.agent-director/state.db.
|
|
284
464
|
let expectedUid = d.geteuid()
|
|
285
465
|
if (expectedUid === undefined || expectedUid === -1) {
|
package/src/lib.ts
CHANGED
|
@@ -14,6 +14,14 @@ import { resolve } from 'path'
|
|
|
14
14
|
// Constants (re-exported so server.ts and tests share the same values)
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
|
|
17
|
+
// True iff the standalone GET SSE stream entry "_GET_stream" is present in the
|
|
18
|
+
// MCP SDK transport's internal _streamMapping — used by handleMessage to detect
|
|
19
|
+
// silent-drop conditions before forwarding a Slack message.
|
|
20
|
+
export function hasGetStreamKey(transport: unknown): boolean {
|
|
21
|
+
const mapping = (transport as any)?._streamMapping
|
|
22
|
+
return typeof mapping?.has === 'function' && mapping.has('_GET_stream')
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
export const MAX_PENDING = 3
|
|
18
26
|
export const MAX_PAIRING_REPLIES = 2
|
|
19
27
|
export const PAIRING_EXPIRY_MS = 60 * 60 * 1000 // 1 hour
|