close
Skip to content

fix(inbox): stop an unattributed sender inheriting owner write authority - #7006

Merged
icecrasher321 merged 2 commits into
stagingfrom
fix/inbox-unattributed-sender-permission
Aug 23, 2026
Merged

fix(inbox): stop an unattributed sender inheriting owner write authority#7006
icecrasher321 merged 2 commits into
stagingfrom
fix/inbox-unattributed-sender-permission

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

What

An inbox message from a sender that matches no workspace member runs as ws.ownerId. That identity also supplies userPermission, which is what tool authorization gates on — so the owner's admin satisfied every check. This caps such a run at read.

Follow-up to #7004, which established that Copilot's function_execute gate was routable around via the workflow tools. Same shape, larger blast radius.

The path

resolveInboxExecutionActor deliberately refuses to name a raw-secret actor for an unmatched sender:

return { executionUserId: ws.ownerId, secretActorUserId: null }

The null guards function_execute's direct mount. Nothing guarded the rest:

  1. executor.ts:218userPermission = workspaceAccess.permission, resolved for ws.ownerIdadmin.
  2. Headless has no browser, so client-routed tools fall back to their server handlers — the comment at tool-executor/executor.ts:57-59 says exactly this, and register-handlers.ts:142,148 wires create_workflow and run_workflow.
  3. All three of create_workflow, edit_workflow, run_workflow carry requiredPermission: 'write', checked at executor.ts:41-53 against that userPermission.
  4. runWorkflowFromCopilot (lib/workflows/application/run-workflow-from-copilot.ts:223) calls executeWorkflow(..., actorUserId, { enforceCredentialAccess: true }).
  5. enforceCredentialAccess makes identifiedCallerUserId = metadata.userId (execution-core.ts:452), so both personalEnvUserId and workspaceEnvUserId become the owner.

Net: an allowlisted external correspondent could reach the owner's workspace and personal secrets by having the agent build and run a workflow — precisely what secretActorUserId: null refuses for a direct mount.

Bounded by mothership_inbox_allowed_sender (webhooks/agentmail/route.ts:304), so an admin deliberately added the address. Not the open internet, but a far larger implicit grant than adding a vendor's address suggests.

Change

One predicate, inboxToolPermission: an attributed message keeps the sender's own permission; an unattributed one is capped at read.

Attributed runs are unchangedexecutionUserId and secretActorUserId are already the same member there, so an emailed request is equivalent to that person acting in the app, and a read-only member still cannot run or edit anything.

Read rather than none because answering an external correspondent from workspace context is the point of the inbox; only mutation and execution are withheld. null stays null — no promotion.

The owner identity itself stays. Billing attribution and workspace reads need a real user; that need is why the fallback exists, and it's now separated from the authority that rode along with it.

Test plan

lib/mothership/inbox/executor.test.ts — 4 passed.

  • caps an external sender at read even when the owner is an adminverified this fails without the fix (reverted the one line, watched it go red, restored).
  • does not lend a read-only member write authority — attributed path unchanged.
  • leaves an external sender with no permission at none rather than promoting to read.
  • Existing member-attribution test extended to assert userPermission.

bun run type-check clean, biome clean.

Not addressed

This caps authority; it does not change the fact that an unattributed run still reads workspace context as the owner, which is the inbox's intended behaviour. If that should narrow too, it's a product decision about what an external correspondent may be told, not an authorization bug.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
Image docs Skipped Skipped Aug 23, 2026 2:06am

Request Review

@cursor

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Authorization change on inbox identity and Copilot tool dispatch; a mistake could either re-open owner secret access or block legitimate member/headless workflow runs.

Overview
Stops an allowlisted external inbox sender from inheriting the workspace owner's write/admin tool authority. Unattributed runs still execute as the owner for billing and workspace reads, but inboxToolPermission now ceilings userPermission at read when secretActorUserId is null (and leaves a true null permission as none). Attributed members keep their own permission, so a read-only member still cannot mutate.

Also closes the matching hole in headless Copilot: client-routed tools like run_workflow have no catalog requiredPermission (the browser path uses the caller's session). The server fallback now requires write before dispatching a registered handler, so a capped run cannot build/run workflows that would resolve the owner's workspace and personal secrets.

Reviewed by Cursor Bugbot for commit 6a0ce48. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR separates the identity used for inbox execution and billing from the authority granted to inbox tools.

  • Caps unattributed inbox senders at read permission without promoting senders whose resolved permission is absent.
  • Adds a write-permission gate to server-side fallbacks for client-routed workflow execution tools.
  • Extends executor and inbox tests for attributed, read-only, unattributed, and missing-permission cases.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/mothership/inbox/executor.ts Separates owner execution identity from tool authority by capping unattributed inbox runs at read permission.
apps/sim/lib/copilot/tool-executor/executor.ts Applies a write requirement when client-routed tools execute through registered server-side handlers in headless mode.
apps/sim/lib/mothership/inbox/executor.test.ts Covers member permissions, the unattributed-sender read ceiling, and preservation of absent permission.
apps/sim/lib/copilot/tool-executor/executor.test.ts Verifies that headless workflow fallback execution accepts write authority and rejects read or missing authority.

Sequence Diagram

sequenceDiagram
  participant Sender as Inbox sender
  participant Inbox as Inbox executor
  participant Lifecycle as Headless Copilot
  participant Tool as Tool executor
  participant Workflow as Workflow handler
  Inbox->>Inbox: Resolve execution and secret actors
  alt Attributed workspace member
    Inbox->>Lifecycle: Member identity and own permission
  else Unattributed sender
    Inbox->>Lifecycle: Owner execution identity and read ceiling
  end
  Lifecycle->>Tool: Execute requested tool with userPermission
  Tool->>Tool: Check catalog or fallback write requirement
  alt Permission satisfies requirement
    Tool->>Workflow: Invoke registered handler
  else Read or missing permission
    Tool-->>Lifecycle: Permission denied
  end
Loading

Reviews (2): Last reviewed commit: "fix(copilot): bar the headless client-to..." | Re-trigger Greptile

Comment thread apps/sim/lib/mothership/inbox/executor.ts
icecrasher321 and others added 2 commits August 22, 2026 19:05
resolveInboxExecutionActor refuses to name a raw-secret actor when the sender
matches no workspace member, then hands the run ws.ownerId for everything else.
That identity also supplies userPermission, which is what executeTool gates on,
so the owner's admin satisfied every requiredPermission check.

In headless mode the client-routed workflow tools fall back to their registered
server handlers (see the comment in tool-executor/executor.ts), so create_workflow,
edit_workflow and run_workflow — all requiredPermission 'write' — were reachable.
runWorkflowFromCopilot then executes with enforceCredentialAccess and the owner as
actor, which resolves the owner's workspace and personal secrets. An allowlisted
external correspondent could therefore reach, through a workflow it had the agent
build and run, exactly what the null secret actor refuses for a direct mount.

Cap the run's tool permission at read when no member owns the message. An
attributed message is unchanged and still uses the sender's own permission, so a
read-only member emailing the inbox still cannot run or edit anything. Read rather
than none because answering an external correspondent from workspace context is
the point of the inbox; only mutation and execution are withheld.

The owner identity itself stays: billing attribution and workspace reads need a
real user. This separates that need from the authority that came with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Client-routed tools carry no catalog requiredPermission because the browser runs
them through the workflow APIs, which authorize the caller's own session. The
headless fallback in executeTool has no session and runs under the request's
principal instead, with nothing standing in for that check.

So the read cap from the previous commit did not reach run_workflow,
run_workflow_until_block, run_block or run_from_block: all four are route
'client' with no requiredPermission, unlike create_workflow and edit_workflow.
An unattributed inbox sender could therefore still run an existing workflow,
which executes with enforceCredentialAccess under the workspace owner and
resolves the owner's workspace and personal secrets.

Derive the requirement at the gate instead: a client-routed tool taking the
headless fallback requires write. Interactive callers never reach this branch,
so the browser path is unaffected. The catalog itself is generated from the
copilot contracts repo and cannot carry this rule, which only applies to the
fallback.

Also corrects the inboxToolPermission doc, which claimed run_workflow gates on
requiredPermission 'write'. It does not; it is gated here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@icecrasher321
icecrasher321 force-pushed the fix/inbox-unattributed-sender-permission branch from cf7a29d to 6a0ce48 Compare August 23, 2026 02:06
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6a0ce48. Configure here.

@icecrasher321
icecrasher321 merged commit 33feaa1 into staging Aug 23, 2026
30 checks passed
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