Skip to content

feat(sdk): deployed-plane DependencyDB read via injected manifest (decision-18 PR2)#139

Merged
I-am-nothing merged 2 commits into
mainfrom
feat/deployed-crossread-read
Jul 4, 2026
Merged

feat(sdk): deployed-plane DependencyDB read via injected manifest (decision-18 PR2)#139
I-am-nothing merged 2 commits into
mainfrom
feat/deployed-crossread-read

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

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, so DependencyDB.result() hard-rejects with the errors.New("… DependencyDB is dev-plane only …") branch and the consumer route (#31) fails closed with 501. DependencyDB was 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).

  • Authority is the Postgres GRANT SELECT issued at install; the manifest is advisory routing only — it cannot widen what the role may read. A forged/malicious manifest name → 42501ErrDependencyUnavailable, never an over-read.
  • The physical name (m<hex>_<table>) is computed platform-side (ids.PhysicalTableName) and shipped down; the SDK never derives it and module-authored code never names it.
  • Manifest absent → keep today's hard error (the safe rollout gate: feat: per-module shared schema (mod_<id>) for cross-app module state #31 stays 501 until both sides ship). Ref absent → ErrProducerNotFound; table absent → ErrNotExposed; 42P01/42501ErrDependencyUnavailable.

Cross-repo contracts locked (both fail-closed)

Two conformance tests pin the cross-repo contracts and fail closed on drift (never over-read):

  • Golden SQL-builder parity — the ported buildDynamicSelect produces SQL text + args byte-identical to api-platform's QueryDynamicSelect (shape-gate + pgx.Identifier.Sanitize + $n binding + limit+1). The SDK cannot import api-platform, so the builder is a faithful port and the golden test is the lock.
  • Ref-normalization conformanceparseProducerRef normalization equals the platform's owner/slug manifest-key reconstruction, so a producer resolvable platform-side is resolvable SDK-side and drift surfaces as ErrProducerNotFound/ErrNotExposed, never a wrong read.

JSON-tag contract with api-platform PR1

Wire contract locked to PR1: envelope field dependencies (json:"dependencies,omitempty" on runtime.LambdaRequest); manifest entry ref (json:"ref") and tables (json:"tables") on db.DependencyGrant. Note: PR1 (api-platform feat/deployed-crossread-manifest, worktree api-platform-wt-depread) is NOT yet written — that worktree is at main with no DependencyGrant — 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's runtime.DependencyGrant is a type ALIAS to db.DependencyGrant (canonical struct lives in db) — required because the decision mandates db.WithDependencies and db must not import runtime (cycle). JSON wire shape is identical regardless of which package declares the struct.

COLLISION NOTE

internal/runtime/lambda.go + inject.go are also edited by app-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 the LambdaRequest field at a time; the later PR does a trivial rebase. This change keeps its addition minimal + isolated (one alias line + one field + one InjectParams field + one InjectResources stash) 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 501 until 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 -l on 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 integration deployed-read test Skips cleanly with no local Postgres.
  • New unit tests execute and pass: golden 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.
  • Pre-existing non-gofmt files on origin/main (internal/core/{cron,event,task}.go, registry/exposure_test.go, runtime/lambda_test.go) left untouched.
  • One unpushed commit (24b5ce4) on feat/deployed-crossread-read, working tree clean, author+committer EXACTLY Sheng Kun Chang <nothingchang@mirrorstack.ai>, body ends with Co-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 main independently; no stacked-merge footgun. This tip is tagged v0.3.0-depread for PR 3 to pin.

Part of decision-18 (deployed-plane cross-module read).

🤖 Generated with Claude Code

I-am-nothing and others added 2 commits July 5, 2026 06:05
…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>
@I-am-nothing I-am-nothing merged commit ad9a855 into main Jul 4, 2026
1 check passed
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