feat(sdk): deployed-plane DependencyDB read via injected manifest (decision-18 PR2)#139
Merged
Merged
Conversation
…cision-18 PR2)
Light up the deployed-plane branch of ms.DependencyDB so a deployed consumer
reads a producer's exposed table through the SAME
ms.DependencyDB(ctx, ref).Select(...).Result(ctx) API used on the dev plane
(decision-18 §3). The platform ships the authorized dependency set down the
trusted Lambda envelope as a manifest; the SDK looks the producer up, composes
a sanitized dynamic SELECT against the platform-supplied physical relation
name, and runs it on mod.DB's already-vended consumer-role pool inside a READ
ONLY tx. The install-time GRANT SELECT stays the single DB-level authorizer —
the manifest is advisory routing only.
- runtime: LambdaRequest gains Dependencies []DependencyGrant
(json:"dependencies,omitempty"); DependencyGrant is a type alias to
db.DependencyGrant so the ctx seam can store the canonical type without a
db->runtime import cycle. Threaded through InjectParams.
- db: new DependencyGrant{Ref,Tables} (json ref/tables) + WithDependencies /
DependenciesFrom ctx seam, parallel to WithSchema/WithCredential/WithPrefix.
New TxReadOnly helper (db.Tx is read-write today): pgx READ ONLY tx with the
app scope (search_path + ms.app_id) pinned SET LOCAL.
- core/select.go: faithful PORT of api-platform internal/shared/database
QueryDynamicSelect/DynamicSelect/buildDynamicSelect (shape-gate +
pgx.Identifier.Sanitize + $n binding + limit+1 truncation). SDK cannot import
api-platform.
- core/dependency_db.go: replace the inLambda "dev-plane only" hard-reject with
the deployed branch. Fail-closed matrix (decision-18 §5): manifest absent ->
keep today's hard error (rollout gate, keeps #31 at 501 until PR1 ships);
ref absent -> ErrProducerNotFound; table absent -> ErrNotExposed; 42P01/42501
-> ErrDependencyUnavailable.
Conformance tests lock the two cross-repo contracts fail-closed on drift:
(a) golden buildDynamicSelect (SQL text + args) byte-identical to api-platform;
(b) parseProducerRef normalization equals the platform owner/slug manifest-key
reconstruction. Plus the §5 sentinel matrix over a synthetic injected manifest
and an integration test (build tag) for the live read + 42P01.
No migration. gofmt/build/vet/test green.
Merge-collision note: app-module-sdk feat/notify (other session) also edits
internal/runtime/lambda.go + inject.go (adds an envelope field + ctx injection).
This change keeps the LambdaRequest addition minimal + isolated (one alias line
+ one field + one InjectParams field + one InjectResources stash) so its later
rebase is trivial; only one PR should add the field at a time.
PR1 coordination note: the omitempty rollout gate means a manifest-supporting
platform that resolves an EMPTY dependency set (e.g. the sole producer was
uninstalled after install) sends no field -> the SDK sees nil -> the rollout-gate
501, not the 404 §5 expects for that window. Flagged for PR1 to weigh
(non-omitempty empty array or accept the uninstall-window edge).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two safe simplify follow-ups on the deployed-plane DependencyDB path, no behavior change: - TxReadOnly now acquires a *pgxpool.Conn and begins the READ ONLY tx on it (conn.BeginTx), reusing the single applyScope seam for search_path + the ms.app_id RLS GUC instead of a divergent setScopeLocalTx copy. The tenant-scoping SQL now lives in exactly one place (no RLS-drift footgun) and runs in one batched round trip instead of two sequential Execs. setScopeLocalTx is deleted. - Dependency.consumer was always == mod.config.ID; dropped the redundant field and read q.dep.mod.config.ID at the single dev-plane use site. Co-Authored-By: Claude Fable 5 <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.
Problem
On the dev plane a consumer reads a producer's exposed table with
ms.DependencyDB(ctx, ref).Select(...).Result(ctx)over the dispatch read-exposed proxy, authenticated by the module's live dev-tunnel session secret. On the deployed plane that path is dead — a deployed module has no tunnel session, soDependencyDB.result()hard-rejects with theerrors.New("… DependencyDB is dev-plane only …")branch and the consumer route (#31) fails closed with501.DependencyDBwas dev-plane-only; deployed consumers had no blessed read path (decision-18 §0).Approach
The deployed branch resolves the physical relation for the producer ref from the platform-injected manifest and runs the SAME structured read the dev/proxy path runs — but on the module's OWN, already-vended
r_<app8>_<consumer>GRANT-ed pool, inside a READ ONLY transaction (decision-18 §2/§3).GRANT SELECTissued at install; the manifest is advisory routing only — it cannot widen what the role may read. A forged/malicious manifest name →42501→ErrDependencyUnavailable, never an over-read.m<hex>_<table>) is computed platform-side (ids.PhysicalTableName) and shipped down; the SDK never derives it and module-authored code never names it.501until both sides ship). Ref absent →ErrProducerNotFound; table absent →ErrNotExposed;42P01/42501→ErrDependencyUnavailable.Cross-repo contracts locked (both fail-closed)
Two conformance tests pin the cross-repo contracts and fail closed on drift (never over-read):
buildDynamicSelectproduces SQL text + args byte-identical to api-platform'sQueryDynamicSelect(shape-gate +pgx.Identifier.Sanitize+$nbinding +limit+1). The SDK cannot import api-platform, so the builder is a faithful port and the golden test is the lock.parseProducerRefnormalization equals the platform'sowner/slugmanifest-key reconstruction, so a producer resolvable platform-side is resolvable SDK-side and drift surfaces asErrProducerNotFound/ErrNotExposed, never a wrong read.JSON-tag contract with api-platform PR1
Wire contract locked to PR1: envelope field
dependencies(json:"dependencies,omitempty"onruntime.LambdaRequest); manifest entryref(json:"ref") andtables(json:"tables") ondb.DependencyGrant. Note: PR1 (api-platformfeat/deployed-crossread-manifest, worktreeapi-platform-wt-depread) is NOT yet written — that worktree is at main with noDependencyGrant— so these tags are authoritative-from-the-task; PR1 must match them exactly (api-platform declares a LOCAL mirror, never imports the SDK). Design note: SDK'sruntime.DependencyGrantis a type ALIAS todb.DependencyGrant(canonical struct lives indb) — required because the decision mandatesdb.WithDependenciesanddbmust not importruntime(cycle). JSON wire shape is identical regardless of which package declares the struct.COLLISION NOTE
internal/runtime/lambda.go+inject.goare also edited byapp-module-sdk feat/notify(other session), which adds its own envelope field + ctx injection to the SAME files. Sequence the envelope-field addition — only one PR adds theLambdaRequestfield at a time; the later PR does a trivial rebase. This change keeps its addition minimal + isolated (one alias line + one field + oneInjectParamsfield + oneInjectResourcesstash) so that rebase is trivial. Coordinate before either merges.Independent of PR1 at build time
No manifest in ctx (old platform / PR1 not yet shipped) → the deployed branch keeps the hard error, so #31 stays
501until both sides ship. Builds and passes with PR1 absent.No migration
Every table and query already exists on
origin/main; the install GRANT walk is unchanged. Zero migrations.Verification
gofmt -lon all 9 changed files: clean.go build ./...,go vet ./...,go test -count=1 ./...: ALL GREEN (re-run uncached after the amend).go vet -tags integration ./internal/core/compiles; the//go:build integrationdeployed-read test Skips cleanly with no local Postgres.buildDynamicSelect(SQL text + args byte-identical to api-platform), limit clamp/default, shape-gate rejection (7 cases), §5 sentinel matrix (manifest-absent rollout gate, producer-not-in-manifest, table-not-exposed),TestMapDeployedReadError(42P01/42501→Unavailable,25006/non-pg→generic, never nil), filters deterministic-sorted order,TestPlatformRefKeyConformance.origin/main(internal/core/{cron,event,task}.go,registry/exposure_test.go,runtime/lambda_test.go) left untouched.24b5ce4) onfeat/deployed-crossread-read, working tree clean, author+committer EXACTLYSheng Kun Chang <nothingchang@mirrorstack.ai>, body ends withCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>.Release order
PR 1 (api-platform manifest inject) → PR 2 (this — tag a release) → PR 3 (ms-app-modules #31 flip). Each lands on
mainindependently; no stacked-merge footgun. This tip is taggedv0.3.0-depreadfor PR 3 to pin.Part of decision-18 (deployed-plane cross-module read).
🤖 Generated with Claude Code