Skip to content

feat(driver-memory)!: refuse to boot into a multi-tenant deployment (#6915) - #7924

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-6915-memory-tenancy-guard
Aug 12, 2026
Merged

feat(driver-memory)!: refuse to boot into a multi-tenant deployment (#6915)#7924
huangyiirene merged 2 commits into
mainfrom
claude/issue-6915-memory-tenancy-guard

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6915

Route B per the maintainer ruling (5261729371): a startup hard-fail when the deployment is multi-tenant, mirroring driver-mongodb's mongodb-tenancy-guard.ts. ⛔ No row-level isolation (route A) — that stays behind the #5499 investment freeze. driver-memory stays out of scripts/check-tenant-chokepoint.mjs's scan set.

What landed

New packages/drivers/driver-memory/src/memory-tenancy-guard.ts, same shape and layering as the sibling:

Precedent (driver-mongodb)This PR (driver-memory)
Typed error + code constantMongoDBMultiTenantUnsupportedError / MONGODB_MULTI_TENANT_UNSUPPORTEDMemoryMultiTenantUnsupportedError / MEMORY_MULTI_TENANT_UNSUPPORTED
Posture layerassertSingleTenantPosture()resolveTenancyPosture() (ADR-0105 D1)same
— call sitesconstructor + connect()constructor + connect()
Object layerassertObjectsNotTenantScoped()same
— call sitessyncSchema + syncSchemasBatchsyncSchema only (no batch path here — see below)
Re-exported from indexyesyes

Both walled postures (group, isolated) are refused; only single passes. There is deliberately no override env var — an escape hatch would restore exactly the silent non-isolation this removes.

Two things the precedent reading needed correcting on

@objectstack/types was not already reachable.driver-mongodb declares it; driver-memory did not. Added as a workspace:* dependency (+ a vitest alias, + the mechanical pnpm-lock.yaml line). resolveTenancyPosture() is still the only knob read — no env var is touched directly.

Both call sites of the posture layer are load-bearing, for a reason the sibling's docstring only half-states.connect() alone is not enough: ObjectQLEngine.init() downgrades a driver connect rejection to a warning under OS_ALLOW_DRIVER_CONNECT_FAILURE=1 (engine.ts:5303-5306), which would boot the deployment unisolated again. The constructor is behind no such hatch. connect() is kept because it is the seam that aborts kernel bootstrap with this message (framework#3741) and it catches a host that flips the posture between construction and connect.

No syncSchemasBatch. This driver does not advertise supports.batchSchemaSync, so the engine syncs one object per call. assertObjectsNotTenantScoped() keeps the array-taking signature anyway — it is the precedent's shape and it keeps the all-offenders-in-one-message property directly testable — but adding a batch path would be capability investment in a frozen driver.

Gates

GateResult
pnpm lintclean
turbo run typecheck (whole repo)126/126
@objectstack/driver-memory24 files / 732 tests (20 new)
@objectstack/verify5 / 23
@objectstack/dogfood92 passed + 1 skipped / 588 passed + 3 skipped
objectql · runtime · spec · cli · metadata-protocol · plugin-auth · plugin-dev · service-datasource · service-analyticsall green (19 794 tests total)
check:driver-conformance40 covered / 0 DEBT / 0 exempt — unchanged
check:query-options-erasure67 non-test sites in 17 files, no files added — baseline untouched
check:tenant-chokepoint19 bindings / 3 files — unchanged (driver-memory correctly absent)
check-driver-memory-censusOK — 2 ruled consumers
check:error-code-casing, check:empty-changeset, check-changeset-no-majorclean

Not breaking the single-tenant path

This is where the card could do damage, so it was measured rather than assumed. Intersecting "names InMemoryDriver" with "sets OS_TENANCY_POSTURE / OS_MULTI_ORG_ENABLED" across packages/apps/examples/scripts yields six files: three plugin-dev tests that vi.mock the driver into ERR_MODULE_NOT_FOUND (never constructed), two that only mention it in prose, and dev-plugin.ts itself. Nothing in the repo runs the real driver under a walled posture. Three tests pin the clean path explicitly: construct → connect → sync → round-trip with no posture set, the same under an explicit single, and objects that omit or disable the tenancy block.

Changeset

minor, not patch — a deployment that boots today can stop booting. Same grade the sibling guard took.


Generated by Claude Code

…6915)
`InMemoryDriver` implements no row-level tenant isolation — it never reads
`DriverOptions.tenantId`, so reads carry no tenant predicate and writes are
not stamped with a tenant column (`distinct()` does not even accept a
`DriverOptions`). Everything above the driver assumes tenant isolation is a
platform guarantee, so a multi-tenant deployment backed by this driver did
not fail — it served cross-tenant reads, updates and deletes SILENTLY.
Route B of #6915, mirroring the guard #3724 landed on driver-mongodb:
* `assertSingleTenantPosture()` reads `resolveTenancyPosture()` (ADR-0105
D1) and refuses both walled postures. Called from the constructor and
re-checked in `connect()`. Both seams are load-bearing: `connect()` is
what `ObjectQLEngine.init()` turns into a boot-aborting
`DriverConnectError`, while the constructor is the seam no escape hatch
reaches — `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` downgrades a connect
rejection to a warning and would boot the deployment unisolated again.
* `assertObjectsNotTenantScoped()` refuses object schemas declaring
`tenancy.enabled: true`, naming every offender in one message. Called
from `syncSchema()` before the table is allocated.
Both throw `MemoryMultiTenantUnsupportedError`
(`code === 'MEMORY_MULTI_TENANT_UNSUPPORTED'`) with a message naming the
detected signal, the knobs that produced it, and the multi-tenant
alternative. No override env var: an escape hatch would restore exactly the
silent non-isolation this removes.
Route A (real row-level isolation) stays behind the #5499 investment freeze
per the maintainer ruling of 2026-08-12 — a startup refusal is not an
investment in the driver's capabilities, it is the removal of a silent
failure mode. `driver-memory` stays outside
`scripts/check-tenant-chokepoint.mjs`'s scan set: a driver that refuses
multi-tenant has no read-side chokepoint to re-derive.
`@objectstack/types` becomes a dependency of the package — the posture
resolver is the only correct way to read the mode, and it was not previously
reachable from here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7aqcZXdqX6yPMtbQWfASy
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 5:22am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-memory.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via @objectstack/driver-memory)
  • content/docs/deployment/vercel.mdx(via @objectstack/driver-memory)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-memory)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/driver-memory)
  • content/docs/permissions/authentication.mdx(via @objectstack/driver-memory)
  • content/docs/plugins/index.mdx(via @objectstack/driver-memory)
  • content/docs/plugins/packages.mdx(via @objectstack/driver-memory)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/driver-memory)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/driver-memory)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…memory tenancy guard (#6915)
`no-migration-prescription`: the guard retires no authorable surface, and
`tenancy.enabled: true` stays valid, honoured metadata everywhere the SQL
family enforces it — so `objectstack migrate meta` has nothing to rewrite,
and rewriting would silently disarm a real isolation declaration on the
deployments that do enforce it. What the guard refuses is a DEPLOYMENT
pairing, whose repair (switch drivers, or set the posture to `single`)
depends on a fact only the operator holds and no ledger entry can state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7aqcZXdqX6yPMtbQWfASy
@huangyiirene
huangyiirene marked this pull request as ready for review August 12, 2026 05:38
@huangyiirene
huangyiirene added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 45d5bd2Aug 12, 2026
27 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-6915-memory-tenancy-guard branch August 12, 2026 05:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

driver-memory 完全没有行级租户隔离(#3724 的未修姊妹面):多租户下静默不隔离

2 participants

@huangyiirene@claude