Skip to content

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

Open
kantord wants to merge 3 commits into
mainfrom
issue-1397
Open

kantord wants to merge 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>
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