Skip to content

Check a send grant on every outbound mail - #259

Merged
alexanderguy merged 15 commits into
mainfrom
intr-710-check-a-send-grant-on-every-outbound-mail
Nov 7, 2026
Merged

alexanderguy merged 15 commits into
mainfrom
intr-710-check-a-send-grant-on-every-outbound-mail

Conversation

@alexanderguy

Copy link
Copy Markdown
Member

Summary

  • The hub checks the sender's mail.send grants for each recipient before it routes, holds or relays mail from a sidecar, before it stores the sidecar's audit copy, and before the REST run-mail routes start a run. A refused relay recipient is dropped and logged; a refused REST mail gets 403 mail_send_denied. A recipient has principal, asset, tenant and domain coordinates, and any matching deny narrows a broader allow.
  • Every run principal gets mail.send:*:* at its first trigger, and the owner and admin system roles get mail.send:tenant:<tenant id>. Migration 0124_mail_send_backfill gives the same rows to existing deployments and to holders of workflow-run manage. Stop the old hub before the migration runs.
  • The grants API refuses a mail.send grant that can never decide a send, and a workflow grant requirement cannot use the reserved mail.send namespace. Coordinate grants rank by their coordinate pattern alone, so a deny with action * now also overrides a mail.accept allow at the same coordinate.

Verification

  • make all exits 0 on the branch tip.
  • make build, make lint and make test pass on each of the 15 commits.
  • Tests cover allow, deny and no grant on the relay and REST paths, the database-backed authorizer and the migration.
  • With the production authorizer, a real-route run's reply passes the relay check, an operator deny on the run refuses the reply end to end, and a held mail from a run that ended while it waited still goes on re-drive.

Closes INTR-710

The capability walk emitted mail.send:<domain> for each mail trigger,
but no runtime code read it, and its name claimed a send authority
that the hub never checked.
A deny whose action or namespace-wide pattern was spelled more broadly
than an allow of the same coordinate ranked below it, so the deny
refused nothing. Every grant here already matches the grammar's one
action, so its spelling says nothing about what the grant covers.
The probe gate refuses a requirement in any reserved namespace with the
reason reserved_namespace_grant_requirements, in place of
mail_accept_grant_requirements, and names each namespace in the refusal
text, so one reason covers every namespace the table reserves.
The correspondent minter named a recipient through its signing key and
fell back to an exact address match, so the same recipient could
resolve or not by key state and address spelling. The minter and the
sender key resolver now read one resolver that ignores both.
@linear

linear Bot commented Nov 7, 2026

Copy link
Copy Markdown

INTR-710

@alexanderguy

Copy link
Copy Markdown
Member Author

Self-Review (code-review skill)

The branch carries the following intentional exception, authorized by a Greybeard ruling:

  • The relay runs the send check twice for each relayed mail, once for the relay frame and once for the delivered: true audit copy (packages/hub-sessions/src/ws/sidecar-handler.ts). The two frames share no key, so one decision for both needs a wire protocol change. Each frame fails closed on its own. Greybeard ruled this a waiver: the cost is one extra authorizer query per relayed mail, and the protocol change is out of scope. INTR-728 tracks one decision per relayed mail.

@alexanderguy
alexanderguy merged commit 633b581 into main Nov 7, 2026
6 checks passed
@alexanderguy
alexanderguy deleted the intr-710-check-a-send-grant-on-every-outbound-mail branch November 7, 2026 20:10
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