Skip to content

slack-bot: DM manual permission approvals instead of auto-approving - #1707

Merged
kantord merged 3 commits into
mainfrom
issue-1397
Sep 21, 2026
Merged

kantord merged 3 commits into
mainfrom
issue-1397

Conversation

@kantord

@kantord kantord commented Sep 18, 2026

Copy link
Copy Markdown
Member

Summary

screenshot-2026-09-18_17-22-06
  • Replaces sdk/typescript/examples/slack-bot/src/bridge.ts's hardcoded onPermissionAsk: () => "allow_once" with a real human-in-the-loop approval flow: every ordinary tool-permission ask is DM'd to the Slack user who started the run as a Block Kit card (Allow once / Allow always / Deny), backed by a new PermissionApprovalGateway (src/approvals.ts).
  • The session shows suspended while an ask is pending; the DM card updates in place (still a card, no buttons) once decided, or if the run ends/is cancelled with no answer.
  • Tool args render as a Block Kit table (bold keys, code-styled/raw_number values) rather than raw JSON, since a card block can't nest a table on the Slack side.
  • Correlation is a single Map<askId, ...> (the SDK's askId is already globally unique); resolution goes through the SDK's onPermissionAsk responder Promise directly, never a manual run.resolveAsk call from Slack code.
  • Fails closed (denies) and logs a warning if the approval DM itself can't be sent, instead of hanging the run or silently proceeding.
  • Removes the old one-time DM greeting (app_home_opened) — unrelated noise that reset on every bot restart.

Closes #1397.

Test plan

  • task --dir sdk/typescript/examples/slack-bot typecheck
  • task --dir sdk/typescript/examples/slack-bot lint
  • task --dir sdk/typescript/examples/slack-bot test (75 tests, including an offline mecated --mock --mock-script integration test that drives a real permission ask end-to-end)
  • Manually verified live against a real Slack workspace + mecated (DM approval card, Allow once/Allow always/Deny, session suspend/resume, retracted-card update on run end)

Fully or partially written by an AI agent.

🤖 Generated with Claude Code

…ving

Replaces the hardcoded onPermissionAsk: () => "allow_once" with a real
human-in-the-loop flow: every ordinary tool-permission ask is DM'd to
the Slack user who started the run as a Block Kit card (Allow once /
Allow always / Deny), the session shows suspended while pending, and
the card updates in place (still a card, no more buttons) once decided
or once the run ends/cancels with no answer.

- src/approvals.ts (new): PermissionApprovalGateway drives the DM/card
  lifecycle, correlated purely by the SDK's already-unique askId, and
  resolves the SDK's onPermissionAsk responder Promise directly rather
  than calling run.resolveAsk from Slack code.
- Args render as a Block Kit table (bold keys, code-styled/raw_number
  values) rather than raw JSON, since a table can't nest inside a card
  block Slack-side.
- bridge.ts/agentSessions.ts thread the responder through; the old
  one-time DM greeting (unrelated noise, reset on every bot restart)
  is removed.
- Fails closed (denies) and logs if the approval DM itself can't be
  sent, instead of hanging or silently proceeding.

Closes #1397.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@samuv samuv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clear implementation and thorough test coverage. I left two inline comments about keeping Slack status aligned with the bridge queue and respecting the configured Block Kit length limits.

Comment thread sdk/typescript/examples/slack-bot/src/agentSessions.ts
Comment thread sdk/typescript/examples/slack-bot/src/approvals.ts Outdated
…iew)

Addresses samuv's two review comments on #1707:

- Slack session status (processing/suspended/active) was set from
  agentSessions.ts's runPrompt wrapper, which runs eagerly at call
  time — before the prompt even joins bridge.ts's per-thread queue. A
  second same-thread message could overwrite a still-pending run's
  "suspended" status with "processing" before its own turn arrived,
  and the first run's final "active" write could later clobber the
  second run's real state. Fixes by adding onStart/onSettle lifecycle
  hooks to MecatlBridge.handlePrompt, fired from inside the actual
  queued execution (#runPrompt) instead of by the caller; agentSessions
  now drives status from those hooks. Pinned by a new bridge.test.ts
  case that holds a first run pending on an approval and asserts a
  second same-thread call's onStart never fires before the first's
  onSettle.

- clamp() appended the ellipsis after a full max-length slice,
  producing max+1 characters whenever truncation actually triggered —
  enough to push a card title/subtitle one character past Slack's
  exact 150-char limit, get the block rejected, and have the ask fail
  closed (denied) for a reason unrelated to the ask itself. Fixed to
  slice(0, max - 1) so truncated output is always exactly max
  characters. Pinned by a 151-character boundary test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@samuv samuv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing both original comments. The queue-ordering fix and truncation boundary fix look correct. I left one inline follow-up for a session-creation failure path that currently skips the new settlement hook.

Comment thread sdk/typescript/examples/slack-bot/src/bridge.ts
Follow-up to samuv's second review pass on #1707: #sessionFor ran
before the try/finally that guards onSettle, so onStart had already
fired (Slack status set to "processing") by the time a session-creation
failure (daemon unreachable, etc.) could skip onSettle entirely,
leaving status stuck at "processing" with no run to ever resolve it.

Moves the session lookup inside the same try/finally as the run itself,
so any failure from #sessionFor onward still reaches onSettle. Pinned
by a new bridge.test.ts case pointing MecatlBridge at an unreachable
address and asserting onSettle still fires after onStart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@samuv samuv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The session-creation failure now crosses the same lifecycle boundary as the run, so onSettle reliably restores status. The focused regression test exercises the real bridge failure path, and CI is green. Approved with one non-blocking test-hardening suggestion inline.

() => {
events.push("start");
},
() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: could this callback return a delayed promise, with the test asserting that handlePrompt does not settle until that promise resolves? The synchronous marker proves onSettle is invoked, but it would still pass if the production await onSettle?.() were accidentally removed. An assertion for the resulting active status call in agentSessions.test.ts would also pin the user-visible outcome.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thansk, let me fold this into the next PR!

@kantord
kantord merged commit e14c88d into main Sep 21, 2026
36 checks passed
@kantord
kantord deleted the issue-1397 branch September 21, 2026 08:44
This was referenced Sep 21, 2026
JAORMX added a commit that referenced this pull request Sep 21, 2026
Merge origin/main at 686b5e5.

Incoming commits after the approved cleanup base:
- 79fae4d chore(catalog): refresh models.dev curated subset (#1721)
- 73309df chore(deps): bump anthropic-sdk-go (#1713)
- e14c88d slack-bot: DM manual permission approvals (#1707)
- 5b0db5a feat(sdk): add MCP authorization lifecycle (#1692)
- 39715ed chore(deps): bump SDK npm dependencies (#1715)
- 498971e plan(sdk): harden malformed-success decode errors (#1698)
- 9ac4798 slack-bot: relabel session approval (#1724)
- 8de41cd docs(process): route approved cleanup directly (#1720)
- a711451 release v0.0.39 (#1727)
- 44786d1 fix(sdk): sanitize malformed-success decode errors (#1700)
- 85f97fa release TypeScript SDK v0.3.0 (#1729)
- 6501b59 fix(ci): repair live compaction slot and metadata (#1728)
- 8047188 chore(deps): bump toolhive-core (#1712)
- 686b5e5 docs: add README diagrams (#1731)

Conflict resolutions preserve the MCP authorization lifecycle from main while
applying the approved alpha cleanup: Run and MCP continuation results consume
canonical payload usage only; removed EventCommon usage and deprecated aliases
stay removed. Keep the landed authorization acceptance record and both the
Unreleased cleanup note and released v0.3.0 changelog. Regenerate API reports
and user reference from the merged canonical TypeScript sources.

Co-Authored-By: mecatl <noreply@mecatl.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slack bot: manual permission approval flow

2 participants