Skip to content

feat(rpc): auto-provision per-binding RPC service keys (ADR-0030/0031)#93

Open
wmadden-electric wants to merge 3 commits into
mainfrom
claude/rpc-service-key-slice2
Open

feat(rpc): auto-provision per-binding RPC service keys (ADR-0030/0031)#93
wmadden-electric wants to merge 3 commits into
mainfrom
claude/rpc-service-key-slice2

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What

Turns slice 1's inert RPC enforcement on end to end: the framework now mints a distinct unguessable key per RPC binding at deploy and wires it to both ends automatically. Before this, serve() could reject callers but no keys were ever provisioned, so every RPC endpoint was still open. No authoring change.

Two ADRs, because the mechanism is worth separating from its first use:

  • ADR-0030what: every RPC binding carries a per-binding service key; the client sends it, serve() 401s a caller without one. Its value is a capability token minted into the workspace deploy state — deliberately not an ADR-0029 name-only secret.
  • ADR-0031how, generically: a framework-minted param value is an opaque, branded ProvisionNeed (mirroring secretSource) that core forwards but never reads, resolved against the deploy target's provisions registry (mirroring ADR-0017's node descriptors). The declarer provides a need; the target provides a registered provisioner. Neither side is a magic string, and the link is enforced at deploy.

Why the registry, not a facet

A named facet (autoProvision: 'per-binding-key') would have been an enum typed in core that every future strategy widens — the shape ADR-0018 rejects — and an unchecked reference: on a target that didn't implement it, the param would silently stay empty and the security would silently not apply.

So core carries one field, ConfigParam.provision?: ProvisionNeed, and never another. The rule that keeps it there: does core need to read it to do its job? optional/default pass (coerce() reads them); mint mechanics, size, storage, rotation all fail — they're provisioner policy, invisible to core. "Minted on odd days" and "64 bytes" are never new param fields.

Shape

  • Core owns enumeration, resolution and minting: it resolves each need's brand against the consumer's extension registry, mints the edge's value once, and buildConfig fills the consumer's param through the normal channel (which deleted the target's consumer-side write block and the optional-skip wart).
  • Target registers provisions: Map([[RPC_PEER_KEY, serviceKeyProvisioner]]) — the ServiceKey resource (mint-once, stable in deploy state, Web Crypto) plus the physical landing (env var + JSON-array encoding), which stays the target's per ADR-0019.
  • Fails closed on shapes not designed yet, rather than guessing: a provisioned edge spanning two extensions, an unregistered brand, and a connection declaring more than one provisioned param (one edge mints one value, so a second need would silently take the first's).

Proven live (the DoD)

Deployed examples/storefront-auth (a storefront → auth RPC edge) to Prisma Cloud, on the final registry design:

  • Wired round trip: storefront renders auth.verify() → { ok: true }. ✅
  • Anonymous rejected: POST /rpc/verify with no bearer and with a junk bearer → HTTP 401. ✅
  • Stable / no-op redeploy: Plan: 17 to noop, [servicekey-storefront.auth] noop — keys reused from state, not re-minted. ✅
  • Torn down clean (17 deleted, no orphans).

Tests

Unit + type green across the four touched packages (@internal/core 157, @internal/rpc 32, @internal/prisma-cloud 159, @internal/lowering 100): ProvisionNeed opacity, unregistered-brand / cross-extension / multiple-provisioned-param failures, resolved-edge-mints-once-and-fills-buildConfig, the ServiceKey lifecycle (mint-once, stable-on-redeploy), the serialize wiring (two consumers of one provider → distinct keys, provider set has both), and run()'s address-free re-stash landing exactly on @internal/rpc's RPC_ACCEPTED_KEYS_ENV.

Also registers service-keys.ts in architecture.config.json — it's new in this PR and matched no plane glob, so dependency-cruiser was passing vacuously over it; its authoring/control import boundary is now machine-checked rather than only caught by the bundle probe.

🤖 Generated with Claude Code

…0 slice 2)

Turns slice 1's inert enforcement on end to end. A generic autoProvision
facet on the serviceKey connection param drives the Prisma Cloud target to
mint one stable ServiceKey resource per RPC edge (in application.provision,
graph-wide), wire the consumer's key onto its serviceKey env var and the
provider's accepted set (JSON array via alchemy Output.all/map) onto the
reserved COMPOSER_RPC_ACCEPTED_KEYS var, which run() re-stashes address-free
for serve() to read. Core stays RPC-agnostic — it only carries the facet.

- ServiceKey resource: mint-once, stable across redeploys (S3Credentials
  lifecycle), Web Crypto, value in deploy state — not a PDP secret.
- serviceKeyEdges(): the single faceted-edge enumeration, shared by the
  minter (control.ts) and the serializer (descriptors/compute.ts).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Jul 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@prisma/composer@93
npm i https://pkg.pr.new/@prisma/composer-prisma-cloud@93

commit: a97b8be

@wmadden-electric wmadden-electric enabled auto-merge (squash) July 15, 2026 21:59
wmadden-electric and others added 2 commits July 16, 2026 09:41
…+ target registry

A framework-minted param value declares a branded, opaque ProvisionNeed
(mirroring secretSource) instead of a named autoProvision facet on
ConfigParam. Core carries one field and resolves the need brand against the
consumer extension's `provisions` registry (mirroring ADR-0017 node
descriptors), failing the deploy loudly on an unregistered brand or a
cross-extension edge. The provisioner owns all mint/size/stability/rotation
policy; core never accretes a second field. Supersedes ADR-0030's facet
paragraph; specs the refactor as slice 3 (lands by amending PR #93).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…registry (ADR-0031)

Replaces the autoProvision string facet with a branded, opaque ProvisionNeed
(mirroring secretSource) that core forwards but never reads, resolved against
the CONSUMER extension's `provisions` registry (mirroring ADR-0017 node
descriptors). Core now owns enumeration, resolution and minting: a need whose
brand no extension registers fails the deploy loudly instead of silently
shipping an unprovisioned endpoint, and buildConfig fills the consumer's param
through the normal channel — which deletes the target's consumer-side write.
The provisioner owns all mint/stability policy; core carries one field and
never accretes another.

Fails closed on the shapes not designed yet: a provisioned edge spanning two
extensions, and a connection declaring more than one provisioned param (one
edge mints one value, so a second need would silently take the first's).

Also registers service-keys.ts in architecture.config.json — new in this PR
and matched by no plane glob, so dependency-cruiser was passing vacuously over
it; the authoring/control import boundary is now machine-checked, not just
caught by the bundle probe.

ServiceKey resource ids are unchanged (`servicekey-${edgeId}`), so an existing
deploy finds its keys rather than re-minting them.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wmadden-electric wmadden-electric changed the title feat(rpc): auto-provision per-binding RPC service keys at deploy (ADR-0030 slice 2) feat(rpc): auto-provision per-binding RPC service keys (ADR-0030/0031) Jul 16, 2026
@wmadden-electric wmadden-electric enabled auto-merge (squash) July 16, 2026 08:30
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