Skip to content

feat(connector): peek-driven elicitation — delegated consent for connectors (SP3a)#143

Merged
harry-harish merged 8 commits into
mainfrom
worktree-feat+connector-sp3a-elicitation
Jul 6, 2026
Merged

feat(connector): peek-driven elicitation — delegated consent for connectors (SP3a)#143
harry-harish merged 8 commits into
mainfrom
worktree-feat+connector-sp3a-elicitation

Conversation

@harry-harish

@harry-harish harry-harish commented Jul 6, 2026

Copy link
Copy Markdown
Member

SP3a — peek-driven elicitation (delegated consent for connectors)

Adds a second consent surface for connectors (headless MCP clients like a Slack bot that spawn @peekdev/mcp): peek-mcp elicits delegated consent from the connector over MCP elicitInput, and the connector relays it to its human via chat. Ordinary MCP clients (Claude Code) are unaffected. Part of the connector-platform arc (SP1 #140, SP2 #142).

The loop (end to end)

  1. Connector bootstrap sets SdkBrain.delegateActionConsent: true → the brain runs the action tool-use inline via mcp.callTool('execute_action', …) and stubs sibling tool-uses with is_error.
  2. peek-mcp dispatchActTool sees the connector advertised elicitation.form and calls a defensive elicitConsent helper (capability-probe → race a 120 s timeout → degrade, never throws) for the execute_action tool; on decline/cancel/timeout it short-circuits to { verdict:'deny', result:'denied', approver:'user' } before the host bridge runs the action.
  3. Connector advertises {capabilities:{elicitation:{form:{}}}} + registers onElicit; its ConnectorRuntime posts a Slack consent card and correlates the (conversation-id-less) reverse-request to the active conversation via a single #pendingElicit resolver + #activeConversationId, resolving through handleConsentResponse.

Design boundaries honored

  • Uniform elicitation for execute_action — peek does not classify destructive-vs-safe; the extension SW gate + destructive-override remain the backstop. Ride-along tools do not trigger elicitation.
  • Behavior-neutral for non-elicitation clients (no-capability → proceeds to bridge.request exactly as before).
  • SP2 preserved + gated — the connector-minted consent-card suspend/resume path still works when delegateActionConsent is false (default); the flag selects the path.
  • ≤1 in-flight elicitation (single-user assumption; multi-elicit is an SP3b non-goal).

Tasks (6 commits, each per-task-reviewed)

# Commit What
1 d7d3967 peek-mcp elicitation.ts helper (clone of roots.ts's defensive shape) + tests
2 bbf7f89 wire elicitConsent into dispatchActTool (deny short-circuits before bridge) + tests
3 d1bb8af connector advertises elicitation.form + PeekMcp.onElicit + tests
4 d0327c8 SdkBrain.delegateActionConsent — inline-run + sibling stub (SP2 fallback intact) + tests
5 9c9e160 runtime elicit→Slack correlation + bootstrap delegateActionConsent:true + tests
5-fix f04f0a1 clear stale #pendingElicit in runLoop finally — prevents abandoned-consent wedge + test
ff41e79 changeset (@peekdev/mcp patch)

Verification

Full local CI green at HEAD: build ✅ · typecheck ✅ (16 pkgs + 2 Astro apps) · lint ✅ (biome, 557 files) · test ✅ (connector-core 39, peek-mcp 411/1-skip, connector-slack 7, all other packages green). A whole-branch opus review confirmed the security-critical invariant: a denied elicitation can never reach bridge.request; elicitConsent never throws; no hang on abandoned consent (connector 30 s < peek 120 s + the finally-block deny).

Known limitations (intentional, deferred to SP3b)

  • Single-user assumption is load-bearing. #activeConversationId is last-writer-wins; under concurrent conversations a consent card could be mis-targeted (worst case: a human still has to actively approve a mis-addressed card — a stale response can't resolve it; no silent execution).
  • Audit indistinguishability. An elicit-deny and a local-banner deny are audit-identical (approver:'user', result:'denied'); a dedicated connector-elicit approver + audit-union widening is deferred to SP3b.
  • request_authorization is largely vestigial for connectors (no side-panel; inline-runs through the bridge — no hang/misbehavior).

Design + plan: private repo docs/specs|plans/2026-07-06-peek-connector-sp3a-elicitation-*.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added delegated consent prompts for action execution when the connected client supports elicitation.
    • When consent is approved, actions proceed; when declined (or not approved), the action is blocked.
  • Bug Fixes

    • Prevented pending consent decisions from blocking subsequent actions.
    • Ensured elicitation is skipped for non-action operations while preserving the existing non-elicitation behavior.
  • Tests

    • Added/updated automated coverage for delegated consent gating, approve/deny/timeout/error outcomes, and action execution inline behavior.

harry-harish and others added 7 commits July 6, 2026 20:05
…one of roots.ts)

Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
…chActTool

Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
…andler

Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
… action, keep <=1-per-turn

Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
…n) + enable delegated consent

Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
… prevent wedge

When brain.runTurn rejected/timed out while a delegated elicit was still
awaiting a human answer, #pendingElicit was never cleared. Every subsequent
elicit then hit the serialisation guard ('if (this.#pendingElicit) return
cancel') and silently denied all future elicitations until process restart.

The fix: in runLoop's inner finally (the same block that clears
#activeConversationId), check whether #pendingElicit belongs to the ending
turn's conversationId and, if so, resolve it with 'deny' and set
#pendingElicit to undefined. The normal handleConsentResponse path clears
#pendingElicit before resolving, so this never double-resolves.

Adds a regression test that parks runTurn, fires an elicit, then rejects
the turn to simulate a timeout — asserts (a) the abandoned elicit resolves
to 'decline' and (b) a subsequent elicit is NOT blocked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
… patch)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@harry-harish, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c9bd33b-880a-42a5-a760-763e0fe988bb

📥 Commits

Reviewing files that changed from the base of the PR and between ff41e79 and 5fd6e2c.

📒 Files selected for processing (4)
  • packages/connector-core/src/runtime.test.ts
  • packages/connector-core/src/runtime.ts
  • packages/peek-mcp/src/mcp/elicitation.test.ts
  • packages/peek-mcp/src/mcp/elicitation.ts
📝 Walkthrough

Walkthrough

Adds delegated consent elicitation for execute_action: peek-mcp now probes client capability and can elicit approval, the server short-circuits denied actions, connector-core wires MCP elicitation and runtime handling, and SdkBrain can execute actions inline when enabled.

Changes

Delegated consent elicitation

Layer / File(s) Summary
peek-mcp elicitation helper module
packages/peek-mcp/src/mcp/elicitation.ts, packages/peek-mcp/src/mcp/elicitation.test.ts, .changeset/peek-elicit-delegated-consent.md
Adds elicitConsent/buildElicitMessage, capability gating, timeout/error-to-deny handling, unit tests, and the patch changeset note.
execute_action server dispatch wiring
packages/peek-mcp/src/mcp/server.ts, packages/peek-mcp/test/server.elicit.test.ts
dispatchActTool now optionally elicits consent before bridge dispatch, execute_action enables it, and tests cover accept/decline/no-capability/read-tool paths.
PeekMcp client elicitation capability and onElicit wiring
packages/connector-core/src/mcp.ts, packages/connector-core/src/mcp.test.ts
PeekMcp advertises elicitation.form, exposes capability/test seams, and maps onElicit through ElicitRequestSchema.
ConnectorRuntime delegated consent state and flow
packages/connector-core/src/runtime.ts, packages/connector-core/src/runtime.test.ts
Runtime tracks the active conversation and pending elicitation, posts consent requests, resolves responses, and clears abandoned pending elicits.
SdkBrain inline action execution mode
packages/connector-core/src/sdk-brain.ts, packages/connector-core/src/sdk-brain.test.ts, packages/connector-slack/src/index.ts
Adds delegateActionConsent, changes action handling to inline execution when enabled, and turns it on for Slack.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • Cubenest/rrweb-stack#140: Modifies the same connector-core consent/runtime path with delegated elicit-on-execute_action handling.
  • Cubenest/rrweb-stack#142: Also changes packages/connector-core/src/sdk-brain.ts action and sibling tool-use handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: peek-driven delegated consent elicitation for connectors in SP3a.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-feat+connector-sp3a-elicitation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@harry-harish

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@harry-harish

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@harry-harish

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@harry-harish

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@harry-harish

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/connector-core/src/mcp.ts (1)

67-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test seam exposed on the public class surface.

__setRequestHandlerForTest monkey-patches the client's setRequestHandler and is reachable by any consumer of the compiled package, not just tests. Given this class gates a consent-sensitive flow, consider a constructor-injectable client/factory instead of a production method that can override request dispatch. Low practical risk since only first-party connectors currently import this package.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connector-core/src/mcp.ts` around lines 67 - 79, The test-only
request handler override on the MCP client is exposed on the public class
surface, so move this seam out of the production API. Update the Mcp client
construction path in mcp.ts to accept an injectable client or factory for tests
instead of keeping __setRequestHandlerForTest as an instance method, and keep
the request-dispatch override isolated to test helpers. Preserve the existing
capabilities() behavior and ensure only test code can swap the underlying
setRequestHandler implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/connector-core/src/runtime.ts`:
- Around line 24-32: The global `#activeConversationId` in Runtime is being shared
across overlapping turns, so one conversation can overwrite another’s state and
cause handleElicit to target the wrong thread or cancel the wrong turn. Update
Runtime to track active state per conversation (or serialize runLoop so only one
turn is active at a time), and make runLoop and handleElicit read/write
conversation-scoped state instead of a single slot. Also ensure the cleanup in
runLoop’s finally only clears the matching conversation’s state, using the
existing `#pendingElicit` and start/onMessage flow as the main entry points.

In `@packages/peek-mcp/src/mcp/elicitation.ts`:
- Around line 16-19: The `elicitInput` call in `server.elicitInput` is not
receiving the request timeout, so the SDK still uses its default budget instead
of the module’s `withTimeout(...)` window. Update the `elicitInput` parameter
shape in `elicitation.ts` to accept `timeoutMs`, and thread it through the call
site as `{ timeout: timeoutMs }` so the SDK and bridge share the same timeout
budget.

---

Nitpick comments:
In `@packages/connector-core/src/mcp.ts`:
- Around line 67-79: The test-only request handler override on the MCP client is
exposed on the public class surface, so move this seam out of the production
API. Update the Mcp client construction path in mcp.ts to accept an injectable
client or factory for tests instead of keeping __setRequestHandlerForTest as an
instance method, and keep the request-dispatch override isolated to test
helpers. Preserve the existing capabilities() behavior and ensure only test code
can swap the underlying setRequestHandler implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ad98da25-02c8-4a00-9742-0bd8cabab2fd

📥 Commits

Reviewing files that changed from the base of the PR and between 15f8823 and ff41e79.

📒 Files selected for processing (12)
  • .changeset/peek-elicit-delegated-consent.md
  • packages/connector-core/src/mcp.test.ts
  • packages/connector-core/src/mcp.ts
  • packages/connector-core/src/runtime.test.ts
  • packages/connector-core/src/runtime.ts
  • packages/connector-core/src/sdk-brain.test.ts
  • packages/connector-core/src/sdk-brain.ts
  • packages/connector-slack/src/index.ts
  • packages/peek-mcp/src/mcp/elicitation.test.ts
  • packages/peek-mcp/src/mcp/elicitation.ts
  • packages/peek-mcp/src/mcp/server.ts
  • packages/peek-mcp/test/server.elicit.test.ts

Comment thread packages/connector-core/src/runtime.ts
Comment thread packages/peek-mcp/src/mcp/elicitation.ts Outdated
…ize turn processing

Finding 1 (CR major): ElicitCapableServer.elicitInput now accepts an optional
second `options?: { timeout?: number }` arg (structurally loose, matching
roots.ts's minimal-shape convention). elicitConsent passes `{ timeout: timeoutMs }`
at the call site so the SDK and the outer withTimeout race on the same budget;
a valid-but-slow approval between ~60s (SDK default) and 120s is honored rather
than folded to `reason:'error'`. New test asserts the options.timeout value is
threaded through.

Finding 2 (CR major): ConnectorRuntime.onMessage routes each turn's body through
a private `#turnChain` promise chain so turns process strictly one at a time.
handleConsentResponse is NOT routed through the chain (consent responses must
remain able to resolve a #pendingElicit while a turn is parked awaiting consent
— serializing them would deadlock). Existing #activeConversationId / #pendingElicit
/ finally-clear-on-conversation-match SP2 suspend/resume path unchanged. Two new
tests: (a) second turn does not start until first completes; (b) elicit fired
during turn 1 targets conversation 1, not 2. Consent-deadlock trace: turn awaits
Promise inside #turnChain → consent arrives on separate handleConsentResponse call
→ resolves #pendingElicit → turn resumes → #turnChain unblocks.

All tests green: @peekdev/mcp 412 passed (40 files), @peekdev/connector-core 41 passed (5 files).
Both packages typecheck clean under exactOptionalPropertyTypes + noUncheckedIndexedAccess.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
@harry-harish

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@harry-harish harry-harish merged commit 521cc05 into main Jul 6, 2026
7 checks passed
@harry-harish harry-harish deleted the worktree-feat+connector-sp3a-elicitation branch July 6, 2026 16:54
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.

1 participant