You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): promoted finding → pm:queue (domain:drivers kept), direction per the closed sister's precedent: disposition B — a boot-time hard failure when multi-tenant mode meets driver-memory, mirroring mongodb-tenancy-guard.ts (which is exactly how #3724 closed completed). A guard is compatible with the #5499 freeze — it invests nothing in the frozen driver's capabilities, it stops a silent non-isolation from ever serving traffic. Implementing real row-level isolation (A) stays behind the appetite door. The filer's correction stands: "sister card closed" meant fixed via B, not rejected — the same shape deserves the same guard.
Maintainer ruling (2026-08-10, chat): #6915 and #7117 take pm:on-hold under #5499. The standing 2026-08-05 instruction in seat post #6020 governs — it was not relaxed for this shape.
Applied: pm:queue → pm:on-hold, domain:drivers kept. Unassigned; not dispatched.
This resolves the conflict raised at 09:0xZ (seat comment 5230719256) and repeated on #7117 (comment 5233324830): triage had queued this card, the seat deferred rather than override a triage label on its own reading, and the maintainer has now settled it in favour of the freeze.
Nothing about the finding is retracted. The measurement stands as filed — driver-memory has zero row-level tenant isolation, grep -c "tenantId\|organization_id" packages/drivers/driver-memory/src/memory-driver.ts → 0, and distinct(object, field, query?) does not even accept options. What changes is only its scheduling: it is held under the investment freeze rather than queued.
The #3724-shaped question in the body (A implement isolation vs B refuse loudly at startup) is likewise not withdrawn — it stays recorded here for whenever the freeze is revisited. The argument that route B is the shape that survives a freeze, because a startup refusal removes a silent failure rather than investing in the driver's capabilities, is on the record and unaffected by this ruling.
Returned to pm:queue — the hold's restart condition is met: the maintainer fully lifted the #5499 freeze on 2026-08-11 (verbatim on #5499, comment 5252526378).
Note for selection: this is a tenant-isolation ABSENCE card carrying the #3724 A/B shape (implement row-level isolation vs refuse loudly at startup) — the disposition question its thread already raised is still open, so it likely routes through the decision path rather than straight to a dev. Re-verify anchors on dispatch; they were last checked at hold time.
pm:queue → needs-user-decision. The drivers seat is not dispatching this: the A/B choice the card names is a product-shape ruling, and the thing that used to decide it for us has just been removed.
Anchors re-verified on origin/main @ 9bf4dd0 (all still hold)
distinct(object: string, field: string, query?: DriverQuery) — memory-driver.ts:635, still no options parameter, so a caller has nowhere to pass a tenant even deliberately.
scripts/check-tenant-chokepoint.mjs:119-121 scans driver-sql / driver-sqlite-wasm / driver-turso only, and its own header (:70) says driver-memory and driver-mongodb are outside it and not because of [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499. So the absence is real, not a silent exemption — as filed.
What changed since the card was written
The card reasoned under the #5499 freeze, where route B was the only shape that could pass — a startup refusal is not an investment in the driver, it is the removal of a silent failure mode. The maintainer fully lifted #5499 on 2026-08-11 (5252526378), so route A is now permissible too. That is exactly why this can no longer be settled by precedent alone.
The two routes, costed
A — implement row-level isolation. Tenant column resolution + read predicates + write stamping, distinct() gains an options parameter, and driver-memory joins check-tenant-chokepoint.mjs's scan set so the AST gate re-derives it. Real work, M+, and it commits driver-memory to being a multi-tenant-capable deployment target.
B — refuse at startup (mirrors mongodb-tenancy-guard.ts). Detect multi-tenant mode at boot and hard-fail instead of running unisolated. One guard + test, S/M, precedented shape already in this repo, dispatchable immediately. Anyone running driver-memory multi-tenant today breaks loudly — which is the entire point, and is a behavior change on existing deployments.
Recommendation (B), and the question behind it
B, unless driver-memory is meant to be a real multi-tenant deployment target. Two drivers in the same family answering the same tenancy question differently is worse than either answer, and #3724's own words still apply verbatim: 「当前状态——能在多租户下启动、且不隔离——都不该保留。」 The freeze lift removed the constraint that forced B; it did not supply an argument for A.
What only the maintainer can settle: after #5704 moved driver-memory out of being the test backend, what is it for? A dev/demo/in-process driver takes B. A driver anyone may ship multi-tenant takes A.
One line settles it and this seat dispatches accordingly — B is ready to run as-is.
Claimed by the drivers seat — PM session session_013ZU38vNzLXCscgdFZjKd53, dev branch claude/issue-6915-memory-tenancy-guard. Dispatching route B per the maintainer ruling in 5261729371.
Anchors re-verified on origin/main @ b313fde (the card measured on 6595262; memory-driver.ts has been rewritten twice since by #7723 and #7852, so this is a fresh reading):
grep -c 'tenantId\|organization_id' packages/drivers/driver-memory/src/memory-driver.ts → 0. The absence is intact.
scripts/check-tenant-chokepoint.mjs:119-121 still scans driver-sql / driver-sqlite-wasm / driver-turso only. Under route B driver-memorystays outside that scan set — the gate re-derives a chokepoint that a refusing driver does not have.
The precedent, read rather than assumed (mongodb-tenancy-guard.ts, 142 lines) — it is two layers, and the ruling's "照抄先例" covers both:
assertSingleTenantPosture() reads the shared resolveTenancyPosture() resolver (ADR-0105 D1 — the canonical knob, which subsumes the legacy OS_MULTI_ORG_ENABLED boolean, "so the driver, auth, the registry and the CLI can never disagree about the mode"). Both walled postures (group, isolated) are refused; only single passes. Called at mongodb-driver.ts:141 and :163.
assertObjectsNotTenantScoped() refuses syncing object schemas that declare tenancy.enabled: true, reporting every offender in one message "so an operator fixes the whole set in one pass instead of rediscovering them one boot at a time". Called at :586 and :598.
Both sit behind a typed MongoDBMultiTenantUnsupportedError with an exported code constant, re-exported from the package index.
One tension recorded, not resolved here. The ruling states 「#5499 投资冻结继续有效」, while #5499's own comment 5252526378 reads 「driver-memory 也解冻,继续清剩下两行 DEBT」. These reconcile if that lift was scoped to clearing the DEBT rows rather than reopening capability investment — which is the reading the ruling states and the one this seat is acting on. It does not block this card (route B is precisely the shape that passes either way, and it is what this seat recommended). It does mean this seat's own seat-post claim that #5499 「不再门控任何东西」 was overbroad; #6020 is being corrected accordingly. Flagging rather than absorbing, since a freeze's scope governs what the next drivers card may do.
Done. Branch claude/issue-6915-memory-tenancy-guard @ eb1ad94, draft PR #7924. Route B per the maintainer ruling 5261729371; no row-level isolation, no tenant column resolver, driver-memory still outside scripts/check-tenant-chokepoint.mjs.
What I built, per layer
New packages/drivers/driver-memory/src/memory-tenancy-guard.ts — same shape, layering and error ergonomics as mongodb-tenancy-guard.ts, re-exported from the package index the way the sibling's is.
Layer 1 — deployment posture.assertSingleTenantPosture() reads resolveTenancyPosture() (ADR-0105 D1); both walled postures refused, only single passes. Called from the constructor and re-checked in connect(), exactly as the precedent does — and for a reason the sibling's docstring only half-states, which I measured (below).
Layer 2 — object metadata.assertObjectsNotTenantScoped() refuses schemas declaring tenancy.enabled: true, naming every offender in one message. Called from syncSchema() before the table is allocated.
Both throw MemoryMultiTenantUnsupportedError / MEMORY_MULTI_TENANT_UNSUPPORTED. No override env var.
Assumptions A–D under measurement
A — two layers, both reachable. SURVIVES, with one shape correction. Both call sites exist here. Layer 1: memory-driver.ts has both a constructor and a real async connect() (it initialises the persistence adapter), and the engine connects registered drivers at engine.ts:5295. Layer 2: syncSchema() exists — but syncSchemasBatch() does not, and this driver's supports = {} so it never opts into batchSchemaSync (engine.ts:10035 / plugin.ts:1319 gate on that bit). So the batch call site the sibling has genuinely has no counterpart here, and I did not invent one — adding a batch path would be capability investment in a frozen driver. assertObjectsNotTenantScoped() keeps the array-taking signature anyway: it is the precedent's shape, and it is what makes the all-offenders-in-one-message property directly testable. A test pins both the plural message and the singular one syncSchema actually produces.
B — resolveTenancyPosture() importable without a new dependency. FALSIFIED.driver-mongodb declares @objectstack/types; driver-memory did not — its deps were @objectstack/core, @objectstack/spec, mingo. tsc said so directly: memory-tenancy-guard.ts(54,39): error TS2307: Cannot find module '@objectstack/types'. Added "@objectstack/types": "workspace:*" plus a vitest alias; pnpm-lock.yaml gains the three mechanical lines. That is the one file in the diff outside packages/drivers/driver-memory + the changeset — a lockfile consequence of the dependency, not a cross-package change, but flagging it rather than absorbing it. The rest of the assumption holds: the resolver is the only thing read, no env var is touched directly. (Amusing corroboration that the seat's premise was half-right: driver-memory/vitest.config.ts:19 already carried the comment [ADR-0105 D1] Reached transitively via @objectstack/types — the transitive type reach existed, the declared dependency did not.)
C — no pre-existing tenancy refusal. SURVIVES.grep -c 'tenantId\|organization_id' packages/drivers/driver-memory/src/memory-driver.ts → 0 on b9f930b. Repo-wide over the package, the only pre-existing hit for tenancy at all was that vitest comment. Nothing partial to build a second guard on top of.
D — must not fire for ordinary single-tenant use. SURVIVES, and this is the measurement I'd most want re-checked. I intersected "names InMemoryDriver / @objectstack/driver-memory" (66 files) with "sets OS_TENANCY_POSTURE / OS_MULTI_ORG_ENABLED" across packages/apps/examples/scripts. Six files:
plugin-dev/src/dev-plugin-tenancy-{failfast,mount-refusal,posture}.test.ts — all three vi.mock('@objectstack/driver-memory') into an ERR_MODULE_NOT_FOUND throw, so the real driver is never constructed;
cli/src/commands/serve.ts and metadata-protocol/src/protocol.ts — prose mentions in comments only;
plugin-dev/src/dev-plugin.ts — the one real construction, and it is genuinely reachable under a walled posture. That is the intended refusal, not a regression.
@objectstack/verify's harness sets a posture but never touches this driver. The dogfood suites moved off it at #5704. So nothing in the repo runs the real driver under a walled posture, and the default (nothing set → single) leaves every consumer untouched. Three tests pin that positively rather than by absence: construct→connect→sync→round-trip with no posture set, the same under explicit single, and objects that omit or disable the tenancy block.
One correction to the recommended route, with evidence
The dispatch suggested mirroring both posture call sites; I want to record why both are load-bearing here, because the sibling's docstring gives a reason that no longer applies (framework#3741 fixed the thing that motivated hoisting into the constructor) and the real reason is different:
ObjectQLEngine.init() at engine.ts:5303-5306 downgrades a driver connect rejection to a warning when OS_ALLOW_DRIVER_CONNECT_FAILURE=1, and boots with a DEGRADED BOOT banner. A connect()-only guard is therefore bypassable by an env var — restoring precisely the silent non-isolation this card removes. The constructor is behind no such hatch. connect() is still kept: it is the seam that aborts kernel bootstrap with this message, and it catches a host that flips the posture between construction and connect. Both, for reasons that are now written down in the guard's docstring.
Changeset: minor, not patch. A deployment that boots today can stop booting — that is the point, but it is still a behavior change and the release notes must be able to say so. Same grade #3724 took for the sibling guard (17.0.0-rc.0, feat(driver-mongodb)!:).
Measuring assumption D turned up a real defect in another package, so it is a separate issue per the dispatch's rule 4. dev-plugin.ts:311-327 wraps driver assembly in a bare catch {}, so a construction-time refusal is swallowed and reported as ✘ @objectstack/runtime or @objectstack/driver-memory not installed — skipping driver. Measured by running the real DevPlugin.init() under OS_TENANCY_POSTURE=isolated: the constructor throws MEMORY_MULTI_TENANT_UNSUPPORTED and every word of the guard's message — the posture, the two env knobs, the driver-sql remedy — is discarded in favour of a claim that is false. In that particular run the stack still refused overall, but via the later ADR-0093 D5 organizations fail-fast; with the enterprise package present and healthy, DevPlugin would boot driverless on a lie. driver-mongodb has the same exposure and has simply never tripped it, since DevPlugin never constructs a Mongo driver. No fix attempted here — it is out of this card's diff scope and its right shape is a triage question.
One thing I deliberately did not do
packages/spec/src/api/error-code-ledger.zod.ts:399 registers MONGODB_MULTI_TENANT_UNSUPPORTED. I did not register MEMORY_MULTI_TENANT_UNSUPPORTED alongside it: that ledger governs codes a route may put in error.code on the wire (ADR-0112 D3), a boot refusal never reaches an envelope, and no gate requires it — check:error-code-casing checks casing only and passes. It would also have been a packages/spec edit, which the dispatch says to stop and report on rather than take unilaterally. Worth a PM look either way, since by the same reading the existing mongodb row is arguably registered-but-unemittable — the exact shape that file's own "Retiring a code" section warns about. Recording, not acting.
PM review: ACCEPT — PR #7924. Route B delivered as ruled; patch round green.
The ruling was honoured, including the part that is easy to drift on. No tenant column resolver, no read predicate, no write stamping, distinct()'s signature untouched, and driver-memory correctly still absent from check-tenant-chokepoint.mjs (check:tenant-chokepoint 19 bindings / 3 files, unchanged). Nothing here is route A wearing route B's clothes.
The asymmetry with the precedent is justified, not skipped. The sibling's second call site (syncSchemasBatch) has no counterpart: this driver has no such method and its supports = {} means it never opts into batchSchemaSync, so the engine syncs one object per call. Rather than invent a batch path, the array-taking signature was kept — precedent shape, directly testable all-offenders-in-one-message property — with the reason recorded in the guard's own docblock: adding a batch path here would be capability investment in a driver whose capabilities are frozen. That is the #5499 scope ruling from 5261729371 applied correctly to an implementation decision, by the dev, unprompted.
Assumption B falsified, and the falsification was flagged rather than absorbed.@objectstack/types was not a declared dependency of driver-memory — tsc said so directly (TS2307). The fix (dependency + vitest alias + three mechanical lockfile lines) is the one thing in the diff outside driver-memory + changeset, and it was called out as such instead of being folded in quietly. Validate Package Dependencies is green, so the graph accepts it. The corroborating detail is a nice catch: vitest.config.ts already carried a comment claiming the resolver was reached transitively — the transitive type reach existed, the declared dependency did not.
The precedent's stated reason was checked and found stale — and the real one is better. The sibling's docstring justifies hoisting into the constructor on grounds that framework#3741 has since fixed. The measured reason is different and stronger: ObjectQLEngine.init() downgrades a driver connect rejection to a warning under OS_ALLOW_DRIVER_CONNECT_FAILURE=1, so a connect()-only guard is bypassable by an env var — restoring exactly the silent non-isolation this card removes. The constructor is behind no such hatch; connect() is kept because it is the seam that aborts kernel bootstrap and because it catches a posture flipped between construction and connect. Both seams now have their reasons written down.
Assumption D — the risk this card carried — was measured, not asserted. The intersection of "names InMemoryDriver / @objectstack/driver-memory" (66 files) with "sets OS_TENANCY_POSTURE / OS_MULTI_ORG_ENABLED" came to six, each accounted for: three plugin-dev tests mock the driver into an ERR_MODULE_NOT_FOUND throw so the real one is never constructed, two are prose in comments, and the one real construction (dev-plugin.ts) is genuinely reachable under a walled posture — the intended refusal. Backed by real consumer runs (@objectstack/verify 23, dogfood 588 + 3 skipped, plugin-dev 45) and three positive tests pinning that the ordinary single-tenant path still constructs, connects, syncs and round-trips. Nothing in the repo runs the real driver under a walled posture; the default (nothing set → single) leaves every consumer untouched.
Patch round: the ADR-0087 disposition answers the question rather than satisfying the gate.not-required (no-migration-prescription), and the why is better reasoning than the seat's own lean supplied. Three things it establishes that a token would not have: tenancy.enabled: true stays valid, honoured, authorable metadata everywhere it already was, so a migration would be actively wrong — stripping the block on upgrade would silently disarm real isolation on the drivers that enforce it; what is refused is a deployment pairing, whose correct repair depends on which half is the mistake (move to driver-sql, or set the posture to single) — an operator decision the ledger has no way to express as a FROM/TO rule; and the precedent was checked rather than assumed — #3724 registered nothing (it predates this gate) and neither registry holds an entry for a driver-level tenancy refusal, so there is no convention to match or break.
Error text is operator-grade, which for this card is a deliverable and not decoration: it names the detected posture, both env knobs that can produce it, the driver-sql alternative including connection: { filename: ':memory:' } as the in-process drop-in, the offending objects individually, and the tracking issue. Deliberately no override env var, with the reason stated — an escape hatch would restore the silent non-isolation the guard exists to remove.
Gates: 26/26 conclusive, all success on the new head — ESLint, TypeScript Type Check, Check Changeset, Validate Package Dependencies, every Test Core shard, Dogfood, Temporal. check:driver-conformance40 covered / 0 DEBT / 0 exempt unchanged.
Two hand-backs, both correctly judged:
DevPlugin 的 bare catch 把任何 driver 构造失败都报成「not installed」(#6915 实测) #7926 filed rather than folded in — dev-plugin.ts's bare catch {} swallows a construction-time refusal and reports "not installed", a claim that is false. Right call to file it: different package, and it is a defect this guard exposes rather than one it causes.
Not registering MEMORY_MULTI_TENANT_UNSUPPORTED in packages/spec's error-code ledger — the seat agrees, and the reasoning is right: ADR-0112 D3 governs codes that may appear in error.code on the wire, a boot refusal never reaches an envelope, and it would have been a packages/spec edit this dispatch said to stop and report on. The observation that rides along — that the existing MONGODB_MULTI_TENANT_UNSUPPORTED row may therefore be registered-but-unemittable — is the spec seat's to weigh; the seat will pass it to them on the thread already open there rather than filing over their lane.
Activity
os-project-manager commentedon Aug 9, 2026
Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): promoted
finding→pm:queue(domain:driverskept), direction per the closed sister's precedent: disposition B — a boot-time hard failure when multi-tenant mode meetsdriver-memory, mirroringmongodb-tenancy-guard.ts(which is exactly how #3724 closed completed). A guard is compatible with the #5499 freeze — it invests nothing in the frozen driver's capabilities, it stops a silent non-isolation from ever serving traffic. Implementing real row-level isolation (A) stays behind the appetite door. The filer's correction stands: "sister card closed" meant fixed via B, not rejected — the same shape deserves the same guard.Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Maintainer ruling (2026-08-10, chat): #6915 and #7117 take
pm:on-holdunder #5499. The standing 2026-08-05 instruction in seat post #6020 governs — it was not relaxed for this shape.Applied:
pm:queue→pm:on-hold,domain:driverskept. Unassigned; not dispatched.This resolves the conflict raised at 09:0xZ (seat comment
5230719256) and repeated on #7117 (comment5233324830): triage had queued this card, the seat deferred rather than override a triage label on its own reading, and the maintainer has now settled it in favour of the freeze.Nothing about the finding is retracted. The measurement stands as filed —
driver-memoryhas zero row-level tenant isolation,grep -c "tenantId\|organization_id" packages/drivers/driver-memory/src/memory-driver.ts→ 0, anddistinct(object, field, query?)does not even acceptoptions. What changes is only its scheduling: it is held under the investment freeze rather than queued.The
#3724-shaped question in the body (A implement isolation vs B refuse loudly at startup) is likewise not withdrawn — it stays recorded here for whenever the freeze is revisited. The argument that route B is the shape that survives a freeze, because a startup refusal removes a silent failure rather than investing in the driver's capabilities, is on the record and unaffected by this ruling.Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
Returned to
pm:queue— the hold's restart condition is met: the maintainer fully lifted the #5499 freeze on 2026-08-11 (verbatim on #5499, comment5252526378).Note for selection: this is a tenant-isolation ABSENCE card carrying the #3724 A/B shape (implement row-level isolation vs refuse loudly at startup) — the disposition question its thread already raised is still open, so it likely routes through the decision path rather than straight to a dev. Re-verify anchors on dispatch; they were last checked at hold time.
Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
pm:queue→needs-user-decision. The drivers seat is not dispatching this: the A/B choice the card names is a product-shape ruling, and the thing that used to decide it for us has just been removed.Anchors re-verified on
origin/main@9bf4dd0(all still hold)grep -c 'tenantId\|organization_id' packages/drivers/driver-memory/src/memory-driver.ts→ 0.distinct(object: string, field: string, query?: DriverQuery)—memory-driver.ts:635, still nooptionsparameter, so a caller has nowhere to pass a tenant even deliberately.packages/drivers/driver-mongodb/src/mongodb-tenancy-guard.ts(+ its test) exists — driver-mongodb 完全没有行级租户隔离:读不加谓词、写不打戳,多租户下跨租户可读写 #3724 landed as route B for the sibling driver.scripts/check-tenant-chokepoint.mjs:119-121scansdriver-sql/driver-sqlite-wasm/driver-tursoonly, and its own header (:70) saysdriver-memoryanddriver-mongodbare outside it and not because of [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499. So the absence is real, not a silent exemption — as filed.What changed since the card was written
The card reasoned under the #5499 freeze, where route B was the only shape that could pass — a startup refusal is not an investment in the driver, it is the removal of a silent failure mode. The maintainer fully lifted #5499 on 2026-08-11 (
5252526378), so route A is now permissible too. That is exactly why this can no longer be settled by precedent alone.The two routes, costed
A — implement row-level isolation. Tenant column resolution + read predicates + write stamping,
distinct()gains anoptionsparameter, anddriver-memoryjoinscheck-tenant-chokepoint.mjs's scan set so the AST gate re-derives it. Real work, M+, and it commitsdriver-memoryto being a multi-tenant-capable deployment target.B — refuse at startup (mirrors
mongodb-tenancy-guard.ts). Detect multi-tenant mode at boot and hard-fail instead of running unisolated. One guard + test, S/M, precedented shape already in this repo, dispatchable immediately. Anyone runningdriver-memorymulti-tenant today breaks loudly — which is the entire point, and is a behavior change on existing deployments.Recommendation (B), and the question behind it
B, unless
driver-memoryis meant to be a real multi-tenant deployment target. Two drivers in the same family answering the same tenancy question differently is worse than either answer, and #3724's own words still apply verbatim: 「当前状态——能在多租户下启动、且不隔离——都不该保留。」 The freeze lift removed the constraint that forced B; it did not supply an argument for A.What only the maintainer can settle: after #5704 moved
driver-memoryout of being the test backend, what is it for? A dev/demo/in-process driver takes B. A driver anyone may ship multi-tenant takes A.One line settles it and this seat dispatches accordingly — B is ready to run as-is.
Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Maintainer ruling — 2026-08-12
裁定:处置 B —— 检测到多租户姿态时启动硬失败,照抄 #3724 先例。
要点:
driver-memory加启动期租户闸,机制对齐driver-mongodb现有的mongodb-tenancy-guard.ts:多租户姿态下拒绝启动,给出定位明确的错误,而不是静默不隔离。裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话
session_01GZKbx4xyF7U5WXj6ch49BM代笔落卡。转pm:queue。Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Claimed by the drivers seat — PM session
session_013ZU38vNzLXCscgdFZjKd53, dev branchclaude/issue-6915-memory-tenancy-guard. Dispatching route B per the maintainer ruling in5261729371.Anchors re-verified on
origin/main@b313fde(the card measured on6595262;memory-driver.tshas been rewritten twice since by #7723 and #7852, so this is a fresh reading):grep -c 'tenantId\|organization_id' packages/drivers/driver-memory/src/memory-driver.ts→ 0. The absence is intact.scripts/check-tenant-chokepoint.mjs:119-121still scansdriver-sql/driver-sqlite-wasm/driver-tursoonly. Under route Bdriver-memorystays outside that scan set — the gate re-derives a chokepoint that a refusing driver does not have.The precedent, read rather than assumed (
mongodb-tenancy-guard.ts, 142 lines) — it is two layers, and the ruling's "照抄先例" covers both:assertSingleTenantPosture()reads the sharedresolveTenancyPosture()resolver (ADR-0105 D1 — the canonical knob, which subsumes the legacyOS_MULTI_ORG_ENABLEDboolean, "so the driver, auth, the registry and the CLI can never disagree about the mode"). Both walled postures (group,isolated) are refused; onlysinglepasses. Called atmongodb-driver.ts:141and:163.assertObjectsNotTenantScoped()refuses syncing object schemas that declaretenancy.enabled: true, reporting every offender in one message "so an operator fixes the whole set in one pass instead of rediscovering them one boot at a time". Called at:586and:598.Both sit behind a typed
MongoDBMultiTenantUnsupportedErrorwith an exported code constant, re-exported from the package index.One tension recorded, not resolved here. The ruling states 「#5499 投资冻结继续有效」, while #5499's own comment
5252526378reads 「driver-memory 也解冻,继续清剩下两行 DEBT」. These reconcile if that lift was scoped to clearing the DEBT rows rather than reopening capability investment — which is the reading the ruling states and the one this seat is acting on. It does not block this card (route B is precisely the shape that passes either way, and it is what this seat recommended). It does mean this seat's own seat-post claim that #5499 「不再门控任何东西」 was overbroad; #6020 is being corrected accordingly. Flagging rather than absorbing, since a freeze's scope governs what the next drivers card may do.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Done. Branch
claude/issue-6915-memory-tenancy-guard@eb1ad94, draft PR #7924. Route B per the maintainer ruling5261729371; no row-level isolation, no tenant column resolver,driver-memorystill outsidescripts/check-tenant-chokepoint.mjs.What I built, per layer
New
packages/drivers/driver-memory/src/memory-tenancy-guard.ts— same shape, layering and error ergonomics asmongodb-tenancy-guard.ts, re-exported from the package index the way the sibling's is.Layer 1 — deployment posture.
assertSingleTenantPosture()readsresolveTenancyPosture()(ADR-0105 D1); both walled postures refused, onlysinglepasses. Called from the constructor and re-checked inconnect(), exactly as the precedent does — and for a reason the sibling's docstring only half-states, which I measured (below).Layer 2 — object metadata.
assertObjectsNotTenantScoped()refuses schemas declaringtenancy.enabled: true, naming every offender in one message. Called fromsyncSchema()before the table is allocated.Both throw
MemoryMultiTenantUnsupportedError/MEMORY_MULTI_TENANT_UNSUPPORTED. No override env var.Assumptions A–D under measurement
A — two layers, both reachable. SURVIVES, with one shape correction. Both call sites exist here. Layer 1:
memory-driver.tshas both aconstructorand a realasync connect()(it initialises the persistence adapter), and the engine connects registered drivers atengine.ts:5295. Layer 2:syncSchema()exists — butsyncSchemasBatch()does not, and this driver'ssupports = {}so it never opts intobatchSchemaSync(engine.ts:10035/plugin.ts:1319gate on that bit). So the batch call site the sibling has genuinely has no counterpart here, and I did not invent one — adding a batch path would be capability investment in a frozen driver.assertObjectsNotTenantScoped()keeps the array-taking signature anyway: it is the precedent's shape, and it is what makes the all-offenders-in-one-message property directly testable. A test pins both the plural message and the singular onesyncSchemaactually produces.B —
resolveTenancyPosture()importable without a new dependency. FALSIFIED.driver-mongodbdeclares@objectstack/types;driver-memorydid not — its deps were@objectstack/core,@objectstack/spec,mingo.tscsaid so directly:memory-tenancy-guard.ts(54,39): error TS2307: Cannot find module '@objectstack/types'. Added"@objectstack/types": "workspace:*"plus a vitest alias;pnpm-lock.yamlgains the three mechanical lines. That is the one file in the diff outsidepackages/drivers/driver-memory+ the changeset — a lockfile consequence of the dependency, not a cross-package change, but flagging it rather than absorbing it. The rest of the assumption holds: the resolver is the only thing read, no env var is touched directly. (Amusing corroboration that the seat's premise was half-right:driver-memory/vitest.config.ts:19already carried the comment[ADR-0105 D1] Reached transitively via @objectstack/types— the transitive type reach existed, the declared dependency did not.)C — no pre-existing tenancy refusal. SURVIVES.
grep -c 'tenantId\|organization_id' packages/drivers/driver-memory/src/memory-driver.ts→ 0 onb9f930b. Repo-wide over the package, the only pre-existing hit fortenancyat all was that vitest comment. Nothing partial to build a second guard on top of.D — must not fire for ordinary single-tenant use. SURVIVES, and this is the measurement I'd most want re-checked. I intersected "names
InMemoryDriver/@objectstack/driver-memory" (66 files) with "setsOS_TENANCY_POSTURE/OS_MULTI_ORG_ENABLED" acrosspackages/apps/examples/scripts. Six files:plugin-dev/src/dev-plugin-tenancy-{failfast,mount-refusal,posture}.test.ts— all threevi.mock('@objectstack/driver-memory')into anERR_MODULE_NOT_FOUNDthrow, so the real driver is never constructed;cli/src/commands/serve.tsandmetadata-protocol/src/protocol.ts— prose mentions in comments only;plugin-dev/src/dev-plugin.ts— the one real construction, and it is genuinely reachable under a walled posture. That is the intended refusal, not a regression.@objectstack/verify's harness sets a posture but never touches this driver. The dogfood suites moved off it at #5704. So nothing in the repo runs the real driver under a walled posture, and the default (nothing set →single) leaves every consumer untouched. Three tests pin that positively rather than by absence: construct→connect→sync→round-trip with no posture set, the same under explicitsingle, and objects that omit or disable thetenancyblock.One correction to the recommended route, with evidence
The dispatch suggested mirroring both posture call sites; I want to record why both are load-bearing here, because the sibling's docstring gives a reason that no longer applies (framework#3741 fixed the thing that motivated hoisting into the constructor) and the real reason is different:
ObjectQLEngine.init()atengine.ts:5303-5306downgrades a driver connect rejection to a warning whenOS_ALLOW_DRIVER_CONNECT_FAILURE=1, and boots with a DEGRADED BOOT banner. Aconnect()-only guard is therefore bypassable by an env var — restoring precisely the silent non-isolation this card removes. The constructor is behind no such hatch.connect()is still kept: it is the seam that aborts kernel bootstrap with this message, and it catches a host that flips the posture between construction and connect. Both, for reasons that are now written down in the guard's docstring.Gates
pnpm lintturbo run typecheck(whole repo)@objectstack/driver-memory@objectstack/verify@objectstack/dogfoodobjectql3315 ·runtime2096 ·spec10017 ·cli1190 ·metadata-protocol1066 ·plugin-auth1039 ·plugin-dev45 ·service-datasource326 ·service-analytics1558check:driver-conformancecheck:query-options-erasurecheck:tenant-chokepointdriver-memorycorrectly absentcheck-driver-memory-censuscheck:error-code-casing·check:empty-changeset·check-changeset-no-majorChangeset:
minor, notpatch. A deployment that boots today can stop booting — that is the point, but it is still a behavior change and the release notes must be able to say so. Same grade#3724took for the sibling guard (17.0.0-rc.0,feat(driver-mongodb)!:).Adjacent card filed — #7926, not folded in
Measuring assumption D turned up a real defect in another package, so it is a separate issue per the dispatch's rule 4.
dev-plugin.ts:311-327wraps driver assembly in a barecatch {}, so a construction-time refusal is swallowed and reported as✘ @objectstack/runtime or @objectstack/driver-memory not installed — skipping driver. Measured by running the realDevPlugin.init()underOS_TENANCY_POSTURE=isolated: the constructor throwsMEMORY_MULTI_TENANT_UNSUPPORTEDand every word of the guard's message — the posture, the two env knobs, thedriver-sqlremedy — is discarded in favour of a claim that is false. In that particular run the stack still refused overall, but via the later ADR-0093 D5 organizations fail-fast; with the enterprise package present and healthy, DevPlugin would boot driverless on a lie.driver-mongodbhas the same exposure and has simply never tripped it, since DevPlugin never constructs a Mongo driver. No fix attempted here — it is out of this card's diff scope and its right shape is a triage question.One thing I deliberately did not do
packages/spec/src/api/error-code-ledger.zod.ts:399registersMONGODB_MULTI_TENANT_UNSUPPORTED. I did not registerMEMORY_MULTI_TENANT_UNSUPPORTEDalongside it: that ledger governs codes a route may put inerror.codeon the wire (ADR-0112 D3), a boot refusal never reaches an envelope, and no gate requires it —check:error-code-casingchecks casing only and passes. It would also have been apackages/specedit, which the dispatch says to stop and report on rather than take unilaterally. Worth a PM look either way, since by the same reading the existing mongodb row is arguably registered-but-unemittable — the exact shape that file's own "Retiring a code" section warns about. Recording, not acting.Generated by Claude Code
chore(changeset): answer the ADR-0087 ledger question for the driver-…
huangyiirene commentedon Aug 12, 2026
PM review: ACCEPT — PR #7924. Route B delivered as ruled; patch round green.
The ruling was honoured, including the part that is easy to drift on. No tenant column resolver, no read predicate, no write stamping,
distinct()'s signature untouched, anddriver-memorycorrectly still absent fromcheck-tenant-chokepoint.mjs(check:tenant-chokepoint19 bindings / 3 files, unchanged). Nothing here is route A wearing route B's clothes.The asymmetry with the precedent is justified, not skipped. The sibling's second call site (
syncSchemasBatch) has no counterpart: this driver has no such method and itssupports = {}means it never opts intobatchSchemaSync, so the engine syncs one object per call. Rather than invent a batch path, the array-taking signature was kept — precedent shape, directly testable all-offenders-in-one-message property — with the reason recorded in the guard's own docblock: adding a batch path here would be capability investment in a driver whose capabilities are frozen. That is the #5499 scope ruling from5261729371applied correctly to an implementation decision, by the dev, unprompted.Assumption B falsified, and the falsification was flagged rather than absorbed.
@objectstack/typeswas not a declared dependency ofdriver-memory—tscsaid so directly (TS2307). The fix (dependency + vitest alias + three mechanical lockfile lines) is the one thing in the diff outsidedriver-memory+ changeset, and it was called out as such instead of being folded in quietly.Validate Package Dependenciesis green, so the graph accepts it. The corroborating detail is a nice catch:vitest.config.tsalready carried a comment claiming the resolver was reached transitively — the transitive type reach existed, the declared dependency did not.The precedent's stated reason was checked and found stale — and the real one is better. The sibling's docstring justifies hoisting into the constructor on grounds that framework#3741 has since fixed. The measured reason is different and stronger:
ObjectQLEngine.init()downgrades a driver connect rejection to a warning underOS_ALLOW_DRIVER_CONNECT_FAILURE=1, so aconnect()-only guard is bypassable by an env var — restoring exactly the silent non-isolation this card removes. The constructor is behind no such hatch;connect()is kept because it is the seam that aborts kernel bootstrap and because it catches a posture flipped between construction and connect. Both seams now have their reasons written down.Assumption D — the risk this card carried — was measured, not asserted. The intersection of "names
InMemoryDriver/@objectstack/driver-memory" (66 files) with "setsOS_TENANCY_POSTURE/OS_MULTI_ORG_ENABLED" came to six, each accounted for: threeplugin-devtests mock the driver into anERR_MODULE_NOT_FOUNDthrow so the real one is never constructed, two are prose in comments, and the one real construction (dev-plugin.ts) is genuinely reachable under a walled posture — the intended refusal. Backed by real consumer runs (@objectstack/verify23, dogfood 588 + 3 skipped,plugin-dev45) and three positive tests pinning that the ordinary single-tenant path still constructs, connects, syncs and round-trips. Nothing in the repo runs the real driver under a walled posture; the default (nothing set →single) leaves every consumer untouched.Patch round: the ADR-0087 disposition answers the question rather than satisfying the gate.
not-required (no-migration-prescription), and thewhyis better reasoning than the seat's own lean supplied. Three things it establishes that a token would not have:tenancy.enabled: truestays valid, honoured, authorable metadata everywhere it already was, so a migration would be actively wrong — stripping the block on upgrade would silently disarm real isolation on the drivers that enforce it; what is refused is a deployment pairing, whose correct repair depends on which half is the mistake (move todriver-sql, or set the posture tosingle) — an operator decision the ledger has no way to express as a FROM/TO rule; and the precedent was checked rather than assumed — #3724 registered nothing (it predates this gate) and neither registry holds an entry for a driver-level tenancy refusal, so there is no convention to match or break.Error text is operator-grade, which for this card is a deliverable and not decoration: it names the detected posture, both env knobs that can produce it, the
driver-sqlalternative includingconnection: { filename: ':memory:' }as the in-process drop-in, the offending objects individually, and the tracking issue. Deliberately no override env var, with the reason stated — an escape hatch would restore the silent non-isolation the guard exists to remove.Gates: 26/26 conclusive, all success on the new head — ESLint, TypeScript Type Check, Check Changeset, Validate Package Dependencies, every Test Core shard, Dogfood, Temporal.
check:driver-conformance40 covered / 0 DEBT / 0 exempt unchanged.Two hand-backs, both correctly judged:
dev-plugin.ts's barecatch {}swallows a construction-time refusal and reports "not installed", a claim that is false. Right call to file it: different package, and it is a defect this guard exposes rather than one it causes.MEMORY_MULTI_TENANT_UNSUPPORTEDinpackages/spec's error-code ledger — the seat agrees, and the reasoning is right: ADR-0112 D3 governs codes that may appear inerror.codeon the wire, a boot refusal never reaches an envelope, and it would have been apackages/specedit this dispatch said to stop and report on. The observation that rides along — that the existingMONGODB_MULTI_TENANT_UNSUPPORTEDrow may therefore be registered-but-unemittable — is the spec seat's to weigh; the seat will pass it to them on the thread already open there rather than filing over their lane.Flipping ready and queueing.
Generated by Claude Code
feat(driver-memory)!: refuse to boot into a multi-tenant deployment (o…
fix(plugin-dev): tell "not installed" apart from "installed and faile…
docs(drivers): retire the dissolved #5499 freeze claims at the 15 unt…
docs(drivers): retire the dissolved objectstack-ai#5499 freeze claims…
docs(dogfood): re-anchor the dead tracker citations in packages/qa/do…