Skip to content

Sign workflow run mail with the run principal's key - #258

Merged
alexanderguy merged 14 commits into
mainfrom
intr-709-sign-workflow-run-mail-with-the-run-principals-key
Nov 5, 2026
Merged

alexanderguy merged 14 commits into
mainfrom
intr-709-sign-workflow-run-mail-with-the-run-principals-key

Conversation

@alexanderguy

Copy link
Copy Markdown
Member

Summary

  • Each deployment has a run principal from deploy. The run principal holds the deployment's signing key as its one active principal_key row. The sidecar keeps the private key, and the hub stores only the public key and refuses to sign with it.
  • Mail verification, the initialization checks, routing, and the run list read that key. A sidecar replacement retires the key, and the key the replacement reports becomes active. workflow_run.public_key no longer exists. Migrations 0119 to 0122 make principal_key.private_key nullable, give existing deployments run principals, copy each deployment's key to its run principal, and drop the column. Each migration refuses data it cannot carry over faithfully.
  • Behavior changes: invoker allow rows last 24 hours from deploy, not from the first trigger, so a first trigger more than a day after deploy gets none, including an existing deployment that has not fired. A run principal whose run exists cannot be deleted (409). A run sender's mail parks only while its allocated deploy attempt is open. The deploy ack's key must be 32 bytes of lowercase hex. Hubs that run the previous version must stop before the migrations run.

Verification

  • make all passes: lint, type check, the admin UI bundle, and the unit (10319), workflow-deploy (255), and core (1569) test passes.
  • The migration tests replay 0120 to 0122 over seeded data, including the refusal cases and hub transactions that run against 0122.
  • Each commit carries the tests for the behavior it introduces.

Closes INTR-709

A principal whose private key lives outside the hub still needs its
public key on record, so the hub can verify what the principal signs.
Such a row has no private key, and the hub refuses to sign with it.
The anchor lock already serializes first commits, so the principal link
the commit writes onto the run row can mark it. A run principal's
existence then says nothing about whether the run has committed, and
the principal can be created independently of the first trigger.
A stopped or failed deployment deactivates each run's own principal,
found by its run reference rather than by the run row's link, so a run
principal that its run has not linked yet is deactivated too.
A run principal that exists from deploy gives the run one identity for
its whole deployment, ready to hold a key the party running it supplies.
Because the principal exists before the first trigger, an operator can
act on it then: the first trigger keeps grants given to it, honors a
deny among them, and refuses to start a suspended or deactivated one.
The invoker grants a first trigger derives still last a day from the
principal's creation, which is now the deploy, not the first trigger.
Every deploy frame carries an allocation, so the listener that wrote
the key for an ack with no allocation could no longer fire. It is gone
rather than kept as a second key writer that takes no allocation lock.
Recording a key whose private half lives elsewhere needs no cipher, and
the callers that record a sidecar's key hold a transaction but no key
store. Module functions that take the caller's transaction let them
record and retire keys under the caller's own lock.
The sidecar running a deployment holds the private half of its key, so
the run principal that answers for the deployment holds the public half
as a public-only key. The deploy ack records it there and on the anchor,
and existing keys are copied over, so the two copies agree. A
replacement sidecar can report a new key, so the replaced one is
retired. The ack's key must be 32 bytes of lowercase hex, the form the
copy checks, so a malformed key is refused at the wire.
A replaced sidecar's key is retired on its run principal, so reading
the active key there stops a retired key from verifying mail.
Only an allocated deploy attempt's completion or failure settles a run
sender's parked mail. A deploy in flight without one has nothing that
settles it, so mail parked on it could only expire undelivered.
A run principal holds its deployment's only signing key, and its run
names it by reference rather than by a foreign key, so the existing
referenced-principal refusal did not cover it.
@alexanderguy

Copy link
Copy Markdown
Member Author

Self-Review (code-review skill)

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

  • Invoker allow rows count 24 hours from deploy. runInvokerExpiresAt (packages/hub-api/src/run-grant-materialization.ts) counts from the run principal's creation, which is the deploy. Greybeard ruling: the deploy-time count is the agreed design, since workflows are planned to start at deploy; a first-trigger timestamp would add a column and a second time source that only helps a deployment idle for more than a day. The limit fails closed: a late first fire gets fewer permissions, never more, the same state main reaches 24 hours after a first fire.
  • First triggers take the tenant-wide lockTenantRunDelegatedGrants lock. Greybeard ruling: the tenant is the only correct scope that needs no new lock protocol. A grant change does not lock the principal row, and a role change finds the principals it affects only after it writes, so a per-principal lock would need every grant, role, and principal mutation to find and lock its principals first. The lock is taken only before a deployment's first commit, and nothing under it calls the network.

@linear

linear Bot commented Nov 5, 2026

Copy link
Copy Markdown

INTR-709

@alexanderguy
alexanderguy marked this pull request as ready for review November 5, 2026 20:21
@alexanderguy
alexanderguy merged commit 7bb6b04 into main Nov 5, 2026
6 checks passed
@alexanderguy
alexanderguy deleted the intr-709-sign-workflow-run-mail-with-the-run-principals-key branch November 5, 2026 20:43
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