feat(rpc): auto-provision per-binding RPC service keys (ADR-0030/0031)#93
Open
wmadden-electric wants to merge 3 commits into
Open
feat(rpc): auto-provision per-binding RPC service keys (ADR-0030/0031)#93wmadden-electric wants to merge 3 commits into
wmadden-electric wants to merge 3 commits into
Conversation
…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>
commit: |
…+ 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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.ProvisionNeed(mirroringsecretSource) that core forwards but never reads, resolved against the deploy target'sprovisionsregistry (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/defaultpass (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
buildConfigfills the consumer's param through the normal channel (which deleted the target's consumer-side write block and the optional-skip wart).provisions: Map([[RPC_PEER_KEY, serviceKeyProvisioner]])— theServiceKeyresource (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.Proven live (the DoD)
Deployed
examples/storefront-auth(astorefront → authRPC edge) to Prisma Cloud, on the final registry design:auth.verify() → { ok: true }. ✅POST /rpc/verifywith no bearer and with a junk bearer → HTTP 401. ✅Plan: 17 to noop,[servicekey-storefront.auth] noop— keys reused from state, not re-minted. ✅Tests
Unit + type green across the four touched packages (
@internal/core157,@internal/rpc32,@internal/prisma-cloud159,@internal/lowering100):ProvisionNeedopacity, unregistered-brand / cross-extension / multiple-provisioned-param failures, resolved-edge-mints-once-and-fills-buildConfig, theServiceKeylifecycle (mint-once, stable-on-redeploy), the serialize wiring (two consumers of one provider → distinct keys, provider set has both), andrun()'s address-free re-stash landing exactly on@internal/rpc'sRPC_ACCEPTED_KEYS_ENV.Also registers
service-keys.tsinarchitecture.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