Repository navigation
[security] driver-turso remote face: find / count / update / delete / create apply no tenant scope (only distinct() refuses), where the local face scopes every door by DriverOptions.tenantId #21226
Description
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·security·priority:p1·domain:engine·area:access·pm:blocked. Measure the reach first; the fix reuses #21185's remote stampTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T18:54Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only.Blocked-by: #21185
Why p1. On the remote face, a tenant-isolation boundary that ADR-0131 D8 assigns to every driver is not applied at the named doors. The source states the gap itself for one door. Whether the engine's Layer 0 already fences these reads and writes end to end is not measured.
- Raise rule: if step (1) of the card shows a cross-organization answer at a public door, the claim raises this card to p0 (the rule [security] driver upsert: a tenant-scoped upsert keyed on a globally-unique business column can merge into, and re-parent, another tenant's row #21185 carries) and says so here.
- Drop rule: if step (1) shows that Layer 0 holds and step (2) shows one organization per remote database, this is defence in depth, and the claim drops the card to p2 and says so here.
Routing.
driver-tursoisdomain:engine.Why blocked. PR #21225 (#21185, p0, in flight; read at this write) edits the same two files and adds the remote face's organization stamp for one door. This card's
createarm reuses that stamp. ⛔ No second copy.- ⛔ Not folded into PR fix(driver-sql,driver-turso)!: refuse an upsert whose conflict lands on another organization's row (#21185) #21225: the p0 fix lands first and is not widened by a measure-first card.
Direction (the card's own, accepted):
- Measure first, through public doors, as the card lists.
- Then every remote door named on the card receives the caller's tenant scope through the one path the local face already uses (
applyTenantScope, and the stamp forcreate). A remoteupdate/deletenarrows by the tenant as well as by the key. - A door the remote transport cannot scope refuses, as
distinct()already does. ⛔ It never answers unscoped.
Pins: each named door, on both faces, as a member of one organization against another organization's rows, refused or empty; a same-organization call answers as before (the control).
Generated by Claude Code
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue, at 2026-10-01T20:07Z.domain:engine#1·session_017xfMoEjKUuSh2xYB8sCozp. ⛔ Not a claim.- The blocked transition this releases: triage's grade 5938367865,
Blocked-by: #21185. It is the latestpm:blockedtransition on this card. No merged PR has named this card since it was written. - The blocker closed: [security] driver upsert: a tenant-scoped upsert keyed on a globally-unique business column can merge into, and re-parent, another tenant's row #21185 landed as
95e24b009(PR fix(driver-sql,driver-turso)!: refuse an upsert whose conflict lands on another organization's row (#21185) #21225; landing record 5939580489). The squash carries the remote face's organization stamp that this card'screatearm reuses.TursoDriver.upsertcallsinjectTenantOnInserton entry, andRemoteTransport.upserttakes afence. Both are present at95e24b009and absent at its parent. - Re-derived on
mainat95e24b009; no new blocker. The in-flight sibling is driver-sql on MySQL: create() answers the insert id (0) instead of the inserted record, so sign-up answers 400 FAILED_TO_CREATE_USER, the dev admin seed fails and no user can sign in #21227 (this seat). It editsdriver-sql'screate/bulkCreateinsql-driver.tsand is not expected to touchremote-transport.tsorturso-driver.ts. A dispatch here names it as a serial neighbour; it is not a blocker. - Triage's raise and drop rules (5938367865) stand for the claim: p0 if step (1) shows a cross-organization answer at a public door; p2 if Layer 0 holds and one organization per remote database is measured.
Generated by Claude Code
- The blocked transition this releases: triage's grade 5938367865,
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_017xfMoEjKUuSh2xYB8sCozp
Account:huangyiirene(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21226-remote-doors-tenant-scope
Worktree:objectstack-issue-21226
Domain:domain:engine
Seat:domain:engine#1(seat post #6367)
File surface:packages/drivers/driver-turso/src/turso-driver.ts(the remote branches offind,findOne,count,update,delete,updateMany,deleteManyandcreate),packages/drivers/driver-turso/src/remote-transport.ts(those doors' statements), pins indriver-turso(local/remote parity), and adriver-tursochangeset. If the ADR-0087 gate asks forregistered, one semantic migration entry underpackages/spec/src/migrations/entries/semantic/plus its generatedregistry.tsregion and'@objectstack/spec': patch, by the precedent of #21185's answer A (5937974892). Step (1) of the card, the reach measurement, comes before any edit.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tierat95e24b00: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no (narrowing)
Thread-read: 5939605478
Serial constraints cleared: read at 2026-10-01T20:10Z againstorigin/main95e24b00. #21185 (the remoteupsertdoor and its organization stamp) has landed as95e24b009; this card reuses that stamp. #21178 (buildWhereSQL, landed862f12c0) and #21163 (landedc6b68891) are in. In flight: #21227 (this seat,driver-sqlcreate/bulkCreateinsql-driver.ts), file-disjoint unlessTursoDriver.create's local branch is touched there, in which case the second lander mergesmain. #20595 stage 1 (PR #21233) ispackages/metadata-protocolonly.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (amendment after the dev's step-1 reading: the grade is raised to p0 by triage's raise rule (5938367865); same session, branch and file surface)
Session:session_017xfMoEjKUuSh2xYB8sCozp
Account:huangyiirene(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21226-remote-doors-tenant-scope
Worktree:objectstack-issue-21226
Domain:domain:engine
Seat:domain:engine#1(seat post #6367)
File surface: as the claim (5939650239), unchanged.
Container & model:M,mode:subagent,model: opus(unchanged)
Clause-②: no (narrowing)
Thread-read: 5939650239
Serial constraints cleared: as the claim (5939650239), unchanged.Step 1, the reach, at 2026-10-01T20:23Z (classes and positions only; the measured detail stays with the seat). The dev measured through the REST data doors with a real engine and security plugin, over a remote-mode
TursoDriveron the libSQL stub, with the local face as the control:- Under the walled tenancy postures, Layer 0 holds at every public data door on the remote face: no cross-organization answer. The one divergence is
create: the remote face does not stamp the caller's organization, which the local face does. That is a divergence, not a disclosure. - Under the posture in which Layer 0 is inert, the remote face answers across organizations at the public read, update and delete doors, where the local face narrows through the driver's tenant scope. That is a cross-organization answer at a public door.
- Not a public door: an elevated engine caller that carries a tenant (which Layer 0 skips by construction) reaches other organizations' rows on the remote face in every posture, where the local face narrows.
Step 2: one organization per remote database is true of the cloud default topology (ADR-0002, ADR-0095), but nothing in the repository guarantees it for a self-hosted remote primary.
Rule applied: the drop rule does not hold (step 2 is not one organization per database by construction). The raise rule holds, because step 1 shows a cross-organization answer at a public door, so this card is p0 from this act on. The fix proceeds on the same branch, unchanged in direction. If triage reads the inert-Layer-0 posture as never holding more than one organization, triage re-grades; this seat applied the rule as written.
Generated by Claude Code
- Under the walled tenancy postures, Layer 0 holds at every public data door on the remote face: no cross-organization answer. The one divergence is
- addedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removedpriority:p1High: required for production / M2High: required for production / M2
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21226,
"status": "done",
"branch": "claude/issue-21226-remote-doors-tenant-scope",
"pr": "#21245",
"session": "session_017xfMoEjKUuSh2xYB8sCozp — dispatched subagent; the parent's harness-stamped id",
"premise_still_valid": true,
"summary": "GitHub copy, classes and positions only, per the dispatch's security clause; the measured detail went to the PM in-session. Premise held at base 95e24b0 (H3 confirmed in source). Step 1, measured at the REST data doors over a real engine with the security layer: under the walled postures Layer 0 held at every data door on both faces, and the remote create landed rows with no organization. In the posture in which Layer 0 is inert, and for an elevated engine caller carrying its organization, the remote face answered across organizations where the local face did not; the seat raised the card to p0. Step 2: the cloud topology is one database per environment (ADR-0002), but nothing keeps a walled multi-organization posture off a remote libSQL primary, so this is not decidable as one organization per database by construction. Fix: TursoDriver.remoteTenantScope compiles the local chokepoint SqlDriver.applyTenantScope on a connection-less Knex probe, and RemoteTransport ANDs that fragment onto each door's WHERE (find, findOne, count, aggregate, update and its read-back, delete, bulkUpdate, bulkDelete, updateMany, deleteMany). The remote create stamps via injectTenantOnInsert, and bulkCreate goes through create. distinct still refuses. upsert was already conformant. A probe shape the face cannot read refuses with INTERNAL_ERROR/500. aggregate, bulkUpdate and bulkDelete are a bounded in-place fix: same defect class, same files, same helper, same gate families. Clause-② no (narrowing); the ADR-0087 disposition is registered driver-remote-doors-tenant-scoped (semantic entry plus regenerated registry.ts, '@objectstack/spec': patch); the seat tells the spec seat. Files: .changeset/21226-remote-doors-tenant-scope.md, driver-turso src/remote-transport.ts, src/turso-driver.ts, src/turso-local-remote-tenant-scope-parity.test.ts (new), spec src/migrations/entries/semantic/18.driver-remote-doors-tenant-scoped.ts (new), src/migrations/registry.ts. 883 changed lines.",
"tests": "Head 71d3bcb (origin/main merged in at ef96c9e; gen:migration-registry then reproduced registry.ts byte-identical). Results: pnpm --filter @objectstack/driver-turso test exit 0 (87 files, 2349 passed, 33 skipped, none in touched files); driver-turso typecheck exit 0 (the pin file is in the program per --listFiles); spec check:generated exit 0 before and after; spec vitest src/migrations 169 passed; spec tsc --noEmit exit 0. check:driver-conformance before the first edit and after the last commit: OK — 50 covered cells, 0 DEBT, 0 exempt, both readings. check:tenant-chokepoint: green before and after. dispatch-gates --commands derived 90 families; all 90 exit 0 on 71d3bcb; --ran reconciles 90 run, 0 NOT MEASURED. On the earlier head 10fa4bc, check:dual-build-cjs-loads was PREREQUISITE NOT MET and measured on the rerun. Lint, narrowed: eslint --no-inline-config --format json over the 5 changed source files: 5 in population, 0 errors, 0 warnings; no type-aware linting is configured, so untouched files cannot move. Reverse verification on e47eee6: with both source files restored from base 17 failed / 19 passed of 36 (all remote-face door pins, the refusal pin and the parity pin red; local face and remote controls green); restored from HEAD (blob hashes equal, git diff HEAD empty) 36/36 passed. The subject resolves from src (relative import), so no dist rebuild was involved in the ablation.",
"mcp_calls": "0",
"api_writes": "3 — each through the fleet-write relay (one repository_dispatch POST per write): POST /repos/objectstack-ai/objectstack/pulls (draft PR 21245); POST /repos//issues/21245/assignees (huangyiirene, via label-write.mjs); POST /repos//issues/21226/comments (this os-dev-report). Plus git pushes (not REST).",
"open_questions": [
{
"question": "Should the remote distinct() carry the same compiled scope instead of refusing a tenant-scoped call?",
"options": [
"A: scope it through remoteTenantScope (converts a refusal into an answer, a widening; Clause-② would move)",
"B: keep the refusal (this PR)"
],
"recommendation": "B for this PR, because the dispatch fixes the line at narrowing and the refusal is honest. A is a cheap follow-up, if the seat wants parity for distinct, decided on the four axes."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the remote face's write doors do not call auditMissingTenant, which the local face's write doors do; a warning-log divergence, not the scope (Acceptance notes in PR 21245)"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (amendment after the dev's report 5941091195: the file surface records three more doors in the same two files, the
packages/specsemantic entry the claim allowed, and the seat's answer ondistinct; same session and branch, PR #21245)
Session:session_017xfMoEjKUuSh2xYB8sCozp
Account:huangyiirene(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21226-remote-doors-tenant-scope
Worktree:objectstack-issue-21226
Domain:domain:engine
Seat:domain:engine#1(seat post #6367)
File surface: as the claim (5939650239), with these recorded:- The same defect at three more doors, fixed in place: the remote
aggregate,bulkUpdateandbulkDelete. They are in the same two files (turso-driver.ts,remote-transport.ts), use the same helper and fall under the same gate families. No file is added. The seat accepts them as a bounded in-place fix of the same class. packages/spec: the ADR-0087 gate asked forregistered driver-remote-doors-tenant-scoped. So: one semantic entrypackages/spec/src/migrations/entries/semantic/18.driver-remote-doors-tenant-scoped.ts, its generatedregistry.tsregion, and'@objectstack/spec': patch, by [security] driver upsert: a tenant-scoped upsert keyed on a globally-unique business column can merge into, and re-parent, another tenant's row #21185's precedent. The claim allowed this. The spec seat is told on its seat post.distinct(the dev's open question): the seat answers B, sodistinctkeeps refusing a tenant-scoped call on the remote face. The card does not name it, and triage's direction names the refusal as the conformant answer for a door left unscoped. Scoping it turns a refusal into an answer, which is a widening that would need its ownClause-②line, and no caller of a tenant-scoped remotedistinctwas measured. If one appears, it is a follow-up on the same helper.
Container & model:M,mode:subagent,model: opus(unchanged)
Clause-②: no (narrowing)
Thread-read: 5941091195
Serial constraints cleared: read at 2026-10-01T21:36Z. The branch mergedorigin/mainef96c9ede(which carries [security] driver upsert: a tenant-scoped upsert keyed on a globally-unique business column can merge into, and re-parent, another tenant's row #21185 and PR fix(spec): filter-text-operator-declared-type-refused's control excludes JSON-stored fields and names the JSON-column door #21234'sregistry.tschange); the regenerated registry is byte-identical. No in-flight claim touchesdriver-turso.
Generated by Claude Code
- The same defect at three more doors, fixed in place: the remote
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT — PR #21245 at head
71d3bcb35eaee6a8b26fc742a8423eb5738f31badomain:engine#1·session_017xfMoEjKUuSh2xYB8sCozp· read at 2026-10-01T21:57Z. Judged against GitHub. Security card: classes and positions only.- Shape: draft, base
main. The first body line isFixes #21226, and there is no other closing keyword. No path is governed; 883 changed lines. The net diff is 6 files:- the remote face (
turso-driver.ts,remote-transport.ts); - a local/remote parity pin file;
- the ADR-0087 semantic entry
18.driver-remote-doors-tenant-scopedand its generatedregistry.tsregion; - the changeset:
driver-tursominor,specpatch, a!title,Clause-②: no (narrowing),adr-0087: registered driver-remote-doors-tenant-scoped.
- the remote face (
- The fix: the remote face asks the local chokepoint (
SqlDriver.applyTenantScope) for its predicate and ANDs it onto each statement, so the scope rule is not copied.- Scoped:
find,findOne,count,aggregate,update,delete,updateMany,deleteMany,bulkUpdate,bulkDelete. - Stamped:
create(andbulkCreatethrough it). - A scope it cannot read is refused, never sent unscoped.
- An unscoped call sends the statement byte-identical to before.
- Scoped:
distinctkeeps its refusal; the seat answered B (amendment 5941143098).- Reach and grade: step 1 found a cross-organization answer at a public door, in the posture in which the engine's wall is inert, and at an elevated engine caller. So the card was raised to p0 (5939869353). After the fix, the same measurement shows no local/remote difference at any measured door, in all three postures.
- Verification: the pin file's 36 tests go 17 red with the fix reverted, every remote door included, and 36 green restored. Gates: 90 derived, 90 run, all exit 0.
check:driver-conformanceandcheck:tenant-chokepointread the same before and after. - Contract review: PASS at
CONTRACT_REVIEW_TIERon this head, record 5941377496.- The invariant, the mechanism, the binding order, the refusal and the byte-identical unscoped path are right.
- The three in-place doors, the spec entry and the
distinctanswer are accepted. - No committed door-level pin, because
restdoes not depend ondriver-turso. The driver-door pins hold the invariant, and the seat holds the step-1 door measurement.
- CI on this head, read at the time above: every check is success or a roster skip (
Build Docs,Console Pin Gate,Packed-tarball smoke (opt-in)incheck-expected-skips.mjs), the required contexts included. - Spec seat: told on its seat post (5941154030).
- Out-of-scope findings:
- The remote write doors do not call
auditMissingTenant(a once-per-door warning, orthogonal to the scope): Acceptance notes, no card. - The remote
createlanding a row with no organization under the walled postures: closed by this PR (the stamp). - The reviewer's observation that one source docblock names the wall-less posture by its documented name rather than by role: dropped — no change. The gap is closed in the same commit, and the source named it at
distinctbefore this PR.
- The remote write doors do not call
Next:
pr_ready, thenautomerge_enable, as two relay acts.Fixescloses this card at merge.
Generated by Claude Code
- Shape: draft, base
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded: PR #21245 →
4b59a381aonmain, verified at 2026-10-01T22:28Z.domain:engine#1·session_017xfMoEjKUuSh2xYB8sCozp. Security card: classes and positions only.- The squash has one parent (
d150c303) and is an ancestor oforigin/main. - Present at the squash and absent at its parent:
remoteTenantScopeinpackages/drivers/driver-turso/src/turso-driver.ts(14, parent 0);scopedWhereSQLinremote-transport.ts(7, parent 0);- the parity pin
turso-local-remote-tenant-scope-parity.test.ts; - the semantic entry
18.driver-remote-doors-tenant-scoped.ts; .changeset/21226-remote-doors-tenant-scope.md.
- Records it landed on: contract review PASS 5941377496 at
71d3bcb3, and ACCEPT 5941476750 on this card. The p0 raise is 5939869353; the surface amendment and thedistinctanswer are 5941143098. Fixes #21226closed this card ascompleted. This act stripspm:dispatchedand clears the assignee. No other card was named by a closing keyword.- With [security] driver upsert: a tenant-scoped upsert keyed on a globally-unique business column can merge into, and re-parent, another tenant's row #21185 (
95e24b009) and this card, the remote face ofTursoDriverapplies the caller's tenant scope at every door that reads rows or picks rows to write, stamps the organization on insert, and refuses atdistinct.
Generated by Claude Code
- The squash has one parent (
Filing gate: ① a defect with named landing sites, under the reach exception for possible data disclosure.⚠️ Classes and positions only. The claim's first step is measuring the reach (see below), before any fix.
Filed by
domain:engine#1(seat post #6367,session_017xfMoEjKUuSh2xYB8sCozp) from #21185'sos-dev-report,out_of_scope_findings[0]. Reader who acts: triage grades and routes;driver-tursois this lane's. ⛔ Not a claim.The positions (read at
origin/main)packages/drivers/driver-turso/src/turso-driver.ts: the remote branches offind,findOne,count,update,delete,updateManyanddeleteManyhandRemoteTransportnoDriverOptions, sooptions.tenantIdnever reaches the remote statement. The local face routes every one of these doors throughSqlDriver.applyTenantScope.packages/drivers/driver-turso/src/remote-transport.ts: the remoteupdateanddeletecompile aWHEREon the primary key only.createdoes not stamp the caller's organization (injectTenantOnInsert). PR fix(driver-sql,driver-turso)!: refuse an upsert whose conflict lands on another organization's row (#21185) #21225 ([security] driver upsert: a tenant-scoped upsert keyed on a globally-unique business column can merge into, and re-parent, another tenant's row #21185) adds that stamp for theupsertdoor only.distinct()arm ofturso-driver.tsrefuses a tenant-scoped call because "no remote read here applies [the tenant scope], so an answer would list every organization's values". The other doors answer instead of refusing.Contract
ADR-0131 D8 (accepted): the engine 「threads the same value to Layer 0 (
computeTenantLayer0Filter) and to every driver」.Seam:
spec:DriverOptions.tenantId → runtime:TursoDriver remote branches (find, count, update, delete, create) | consumer: the remote libSQL transport.What decides the real reach (measure first)
GET/PATCH/DELETE /api/v1/data/:objectas a member of one organization, on a remote-mode datasource holding another organization's rows.If (1) shows a cross-organization answer at a public door, this card is p0 by the same rule #21185 carries. If (1) holds and (2) is one organization per database, it is defence in depth, and the grade drops accordingly.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed: "driver-turso remote transport tenant scope tenantId ignored find update delete create organization remote face". 21 hits, none on this gap. The nearest:SqlDrivermethods the remote face does not override answer from the placeholder:memory:Knex database —introspectSchema()returns no tables,distinct()a 500,findWithWindowFunctions()a raw SQLite error #20055 (closed): the inherited methods answering from the placeholder database, wheredistinct()gained its refusal.upsertdoor, being fixed in PR fix(driver-sql,driver-turso)!: refuse an upsert whose conflict lands on another organization's row (#21185) #21225.Dedupe words:
remote face tenant scope·TursoDriver tenantId ignored·remote create organization stamp·RemoteTransport update where idGenerated by Claude Code