Skip to content

fix(approvals): an allowed share covers the bot's reworded retry - #907

Closed
sx4im wants to merge 1 commit into
CopilotKit:mainfrom
sx4im:fix/889-share-allow-once-retry
Closed

sx4im wants to merge 1 commit into
CopilotKit:mainfrom
sx4im:fix/889-share-allow-once-retry

Conversation

@sx4im

@sx4im sx4im commented Oct 30, 2026

Copy link
Copy Markdown

What this changes

A private share (e.g. a Gmail reply held for approval) asks once, and "allow once" is meant to cover the Bot's next attempt for thirty minutes. The check matched the exact content bytes, so when the Bot reworded its reply after your approval it opened a fresh request you had already answered, looping until someone finally denied it. The approval now covers the share itself: same Bot, same audience, same conversation, thirty minutes. A different audience still asks.

Where it runs

No new state, no listeners, no schedules. The lookup reads the existing approvals ledger; every replica decides the same way off the same rows.

  • New state that outlives a request? None.
  • What happens on the second replica? Identical: the match is computed from the stored approval rows.
  • Anything serialised? Nothing new.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act.
  • New refusals and new failures each write a row. No new refusal paths.
  • Nothing new is trusted from the client that the server can resolve itself.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

bun test server/tests/proactive.test.ts: 13 pass, 0 fail. New tests: "an allow-once covers the bot's reworded retry to the same audience" fails before the fix (retry opened a second request) and passes after; "an allow-once for another audience still asks again" guards the boundary. tsc --noEmit clean on server. DB-backed suites need TEST_DATABASE_URL, not available here; the pure check suite covers this change.

Fixes #889

The private-share check matched a recent allow-once on the exact content digest, so when the Bot reworded its send after approval it opened a fresh request the person had already answered, looping until someone denied it. The match is now the share itself: same Bot, audience, conversation, within thirty minutes.

Co-authored-by: Saim Shafique <128062316+sx4im@users.noreply.github.com>
@davidmckayv

Copy link
Copy Markdown
Contributor

Thanks for picking up #889. The loop is real, but we can't take this approach, because it would let one approval cover messages the person never saw.

Matching on Bot, scope and conversation instead of the action's digest means "Allow once" covers anything sent through that tool in the conversation for 30 minutes. For connector sends without a recipient field we recognise, the scope falls back to the tool itself (share-target.ts). Gmail's reply tool is one of those: recipient_email and thread_id aren't recipient keys, which is why #889 shows external:composio-gmail/GMAIL_REPLY_TO_THREAD. So after one allowed Gmail reply, a Bot steered by an email it reads could send any private content to any address, without asking. That check is the always-on guard against sharing private information, so an approval has to stay bound to what the person was shown.

We'd welcome a PR that does what #889 suggests: resume the held call with the content the person approved, rather than letting a reworded retry inherit the approval.

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.

A held private-share for a tool write (Gmail reply) never shows a card, and retries can't be approved

2 participants