Uh oh!
There was an error while loading. Please reload this page.
fix(objectql): cut the tenant-audit control's scope by the object's tenancy, not the caller's flag (#13491) - #13635
Conversation
…enancy, not the caller's flag Maintainer ruling 2026-08-31 (联案 #13491 + #13497, verbatim「同意」): the tenant-audit control's scope is decided per OBJECT, not by the caller's `isSystem` flag. `isSystem` x a tenant-scoped object is IN scope; `isSystem` x a genuinely global object is out, with #8672's reasoning inheriting per object rather than exempting the `sys_`/`cloud_`/`ai_` namespace wholesale. Two gates narrow by one classification: the blanket `isPlatformNamespaceObject` short-circuit in `resolveSystemInsertOrganization`, and the engine's `bypassTenantAudit` isSystem mute (the #13178 census measured it silencing 135 of 175 write call sites). The classification cannot be read off the schema: `applySystemFields` provisions `organization_id` unconditionally, so 59 of the 84 platform-namespace objects in this tree carry the column, `sys_permission_set` included. The inventory is therefore hand-adjudicated in `packages/objectql/src/tenancy/platform-object-tenancy.ts`, with an `unclassified` verdict that keeps today's behaviour for anything the evidence does not settle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e80bdbbe47b404db1f6290750e5ef03555ecc22e && git checkout e80bdbbe47b404db1f6290750e5ef03555ecc22e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 575ce836d1c990cb82bb4ba8cb507997d4857a74 58f94c112d48ee655fca5c20a060d53d51343768 && git checkout -B drift-repro 575ce836d1c990cb82bb4ba8cb507997d4857a74 && git merge --no-ff 58f94c112d48ee655fca5c20a060d53d51343768
node scripts/docs-audit/affected-docs.mjs --json 575ce836d1c990cb82bb4ba8cb507997d4857a74
|
…ne edit
`content/docs/permissions/system-context.mdx` anchors `objectql/src/engine.ts`
by line number in 15 places, and the tenancy-classification commit shifted every
anchor below its change point. 14 of the 15 are pure line rot — the anchored
statement is byte-identical across the move — and are repaired mechanically by
`node scripts/check-system-context-census.mjs --fix`.
The 15th is not rot: this branch deliberately reworded the construct row 24's
`NON_READ_ANCHORS` needle locates, `if (isSystem && opts.bypassTenantAudit ===
undefined) {`, by adding the classification term. The construct still exists and
the row still describes it, so the needle is re-pointed at the new spelling
rather than left to rot off. ⛔ No row was added and none was retired — the
ledger stays shrink-only, and the gate reports the same population as before the
change: 109 elevation read sites, 27 declared non-read.
Found by the PM, not by the local gate derivation: `dispatch-gates.mjs` does not
name this family, which is a second instance of the standing reading that the
derived family is not the whole farm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5Lzhuangjianguo
commented
Aug 31, 2026
⛔ CI red — root-caused, and it is not this PR's to fix. Standing down on it, not silently.
The failing check
Why it is not this PR'sThis PR touches The actual mechanism — read from the test, not guessedfunctiondiscoverTransports(): string[]{returnwalk(join(REPO_ROOT,'packages')).filter((f)=>readFileSync(f,'utf8').includes('resolveAuthzContext({'))It walks all of ⇒ It slows down as the monorepo grows, and reddens whichever PR happens to be pushing when it crosses the line.
|
zhuangjianguo
commented
Aug 31, 2026
|
zhuangjianguo
commented
Aug 31, 2026
PM verdict: ACCEPT — blocked on #13645 alone, not on anything in this PRRun 33357913478 is now complete at The complete red list is two checks, and they are one failure
Green includes ⭐ The predicted red materialised and the repair held
It reproduced at 26 problems, exactly as predicted, and the repair is confirmed correct by this green. Worth recording precisely because the gate is not in the I also re-verified the one hand-edited Release status⛔ Not released, and not being marked ready. The enqueue bar in this lane is every check green, not the required subset, and two are red. Marking ready or enqueuing now would only re-run into the same timeout and, if it reached the queue, eject and force a rebuild of everything behind it. The single remaining blocker is #13645, which is dispatched. It is not this PR's failure, on measurement rather than assertion: the identical failure hit #13630 eleven minutes earlier on a queue branch whose walked file set is identical to plain ⇒ On #13645 landing, this PR needs no code change — only a re-run of the two red checks against the fixed base. Still watched. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13491
Implements the maintainer's 2026-08-31 re-ruling (联案 #13491 + #13497, verbatim「同意」, option C): the tenant-audit control's scope is cut by the object's tenancy classification, never by the caller's
isSystemflag.Zone 2 — the PM's assumption, FALSIFIED. This is a build, not an unblocking.
The dispatch expected that narrowing the blanket guard would be "mostly deleting a short-circuit", so tenant-scoped
sys_*objects fall through into machinery that already exists. Measured, it is not.applySystemFields(registry.ts) provisions theorganization_idCOLUMN unconditionally — its existence was deliberately decoupled from whether tenancy is on, so sudo writers can always stamp it. AST census of everyObjectSchema.createunderpackages/:managedBy: 'better-auth', plussys_sso_provider'stenancy.enabled: false)The 84 reconcile exactly against the curated
PLATFORM_OBJECTS_BY_PACKAGEregistry (89 names minus the 5cloud-runtime objects defined in the separate repo).sys_permission_set— the object #8672 reasoned about and the ruling names verbatim as the case whose reasoning inherits — is one of the 59. So deleting the short-circuit replaces a wholesale exemption with a wholesale inclusion, which is the same defect mirrored. The derive branch would additionally stamp an organization onto rows that land org-less today, i.e. a change to what a write stores — forbidden by execution point 3.The downstream machinery itself needs no new capability:
resolveSystemInsertOrganizationalready reads the schema, resolves the tenant field, checks the rows, reads the posture and reaches #8844's refusal including the multi-organization branch. What was missing is the classification, and its second half ("有列且有写手填") is a fact about the code, not a runtime fact. That is what this PR builds.What lands
A hand-adjudicated inventory,
packages/objectql/src/tenancy/platform-object-tenancy.ts, and two gates narrowed by that one reading:resolveSystemInsertOrganization'sif (isPlatformNamespaceObject(object)) return undefined;becomesif (isPlatformObjectOutOfTenantAuditScope(object)) return undefined;— an admitted object now reaches the derive/refuse machinery.bypassTenantAuditisSystemmute, which used to silence every elevated write whatever the object was. The tenant-audit: the "write without tenantId" signal is a throttled log warn gated on multi-tenant posture, so it cannot fire in any environment where code is exercised #13178 census measured 135 of 175 write call sites (77%) silenced at it — the control's largest gate, sitting ahead of the condition the control is about. It now mutes only where the object is not tenant-scoped. An explicit caller-suppliedbypassTenantAudit: trueis still honoured; this branch only ever filled a gap.isPlatformNamespaceObjectsurvives as a NAME-SHAPE predicate with a warning against using it for tenancy, and its module header now records the withdrawal rather than the withdrawn doctrine.Diff surface: engine only. The dispatch expected engine + driver. No driver change was needed:
SqlDriver.auditMissingTenantreads the option the engine produces, so narrowing the producer is the whole change. Nothing underpackages/spec/src/**.The one-time inventory (execution point 2)
Admitted — tenant-scoped (7). The admission bar is a citable writer fact: a maintainer-ordered organization repair, a ruled writer that stamps the column, or both.
sys_filemetadata-store.ts); maintainer-ordered backfill, 2026-08-28sys_upload_sessioncontext.tenantIdon insert and update; pre-repair rows ruled forward-stamp-onlysys_approval_requestsys_approval_actionapproval-service.tsstamps itsys_approval_approverapproval-service.tsstamps itsys_automation_runsuspended-run-store.tsresolves and stamps viarecordOrgResolver.organizationOf(...)sys_notification_deliverySqlOutbox.enqueuewrites the derived notification organizationAdjudicated global (1).⚠️ Recorded honestly in the ledger:
sys_permission_set— named verbatim by the ruling as the case whose reasoning inherits.ensure-default-organization.tsDOES stamp org-scoped permission sets, so the object holds both populations. The verdict rests on the maintainer naming it, not on an absence of org-scoped writes.⛔ Cannot determine — 51 objects, listed rather than guessed. Per「判不了的逐个列出回批呈裁,⛔ 不猜」these keep TODAY'S behaviour exactly and return for adjudication:
sys_activitysys_approval_delegationsys_approval_tokensys_attachmentsys_audience_binding_suggestionsys_audit_logsys_business_unitsys_business_unit_membersys_capabilitysys_commentsys_emailsys_email_templatesys_flow_dispatchsys_http_deliverysys_import_jobsys_inbox_messagesys_invitationsys_jobsys_job_queuesys_job_runsys_membersys_metadatasys_metadata_activationsys_metadata_auditsys_metadata_commitsys_metadata_historysys_migrationsys_migration_journalsys_notificationsys_notification_preferencesys_notification_receiptsys_notification_subscriptionsys_notification_templatesys_positionsys_position_permission_setsys_presencesys_record_sharesys_report_schedulesys_saved_reportsys_scim_connection_credentialsys_secretsys_settingsys_setting_auditsys_share_linksys_sharing_rulesys_teamsys_user_permission_setsys_user_positionsys_user_preferencesys_view_definitionsys_webhookPlus the 5
cloud-runtime objects (sys_app,sys_environment,sys_environment_member,sys_package,sys_package_installation), which are defined in the separatecloudrepository and are therefore structurally unclassifiable from this tree.A long unknown list does NOT make the blast radius unknowable, which is why this did not trip the STOP condition:
unclassifiedis byte-identical to today's behaviour, so the set of objects whose behaviour moves is exactly the admitted seven.Three findings inside the unknown list that the adjudicating batch should see first
sys_http_delivery— the rule and the ruling's own evidence disagree, and the disagreement IS a defect. The ruling's evidence ring names it as a tenant-scoped object in the namespace, butEnqueueHttpInputcarries no organization member at all (sql-http-outbox.ts), so no caller can fill the column — 100% of rows land org-less, and the deliberate cross-organization wall onredeliver()is reachable around for every one of them. By the letter of「有列且有写手填」it is not tenant-scoped; by its purpose it plainly is. Admitting it would have refused every enqueue on a walled install, i.e. manufactured an outage out of a missing writer. It is listed instead. (This is measure: 企业版 Middleware A 的organization_idstamper 缺口——「调用点没带租户上下文」是否 ≠「行落 NULL」?(定 #13178 修复族伤害等级的唯一输入) #13497's finding, which that card recommended for PM filing and did not file.)sys_metadatais the sharpest: the org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 ruling (option A) says a non-overridable type's write lands env-wide — a deliberately org-less row.sys_audit_log's writer enumerates legitimate org-less cases too (a record on an object with no organization column). System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844's decision is per OBJECT plus posture; it has no way to say "this ROW is legitimately org-less". Admitting a conditionally-scoped object would refuse its ruled-legitimate writes on a walled posture. Whether that gap gets closed is a design question this card did not resolve.seed-loader.tsandseed-tenancy-backfill.tsboth carry/^(sys_|cloud_|ai_)/, and the module header this PR edits states the three paths must stay in step. Only the runtime path is re-cut here — deliberately, since re-cutting the seed and backfill paths is a decision about rows those paths already wrote. The divergence is now written down at the site rather than left to be discovered.Direction (execution point 3) and the flips
What this adds is refusals and warnings only. No write's target or payload changes, nothing was skipped, disabled, quarantined, allow-listed, deleted or re-baselined, and no behaviour was rewritten to keep a suite green.
Expected reds registered as flips: none materialized. Reported as a measurement, not as an absence of checking — every suite that owns an admitted object was executed and stayed green (below). The reason is structural:
unclassifiedmoves nothing, and the admitted objects' suites either carry an organization or run on a non-walled posture. One prose site DID have to move, and it is the honest half of the same coin:system-write-organization.test.tsjustified its exclusion case as "platform namespace ⇒ deliberately org-less (#8672)", the wholesale reading the ruling withdrew. The assertion is unchanged; the comment now says the verdict isunclassified, pending adjudication.Verification — all at
e486ce8163pnpm --filter @objectstack/objectql test—Test Files 249 passed (249),Tests 4306 passed (4306)pnpm --filter @objectstack/objectql typecheck—Done518 passed, service-messaging301 passed, service-automation1092 passed, plugin-approvals632 passed, plugin-audit323 passed, driver-sql2248 passed | 132 skippednode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack. It grew from 16 families to 39 once the diff took incontent/docs/**andscripts/**— all 39 measured green. Four of them first reportedPREREQUISITE NOT MET(spec:docs,spec:skill-examples,lint:doc-formula-expressions,lint:doc-security-posture): each reads compiled output that a freshly-recreated worktree does not have. Those are NOT MEASURED, not red — so@objectstack/lint,@objectstack/spec,@objectstack/clientand@objectstack/client-reactwere built and all four re-run green,skill-examplesreporting260 prose examples type-check across 3 surface(s).check:dual-build-cjs-loadsstill printsPREREQUISITE NOT MET — this gate reads built output, and some package has no dist/; it needs a whole-tree build and is reported as NOT MEASURED, neither green nor red.check:doc-authoringcaught a real defect on the first run: the ledger'sevidencevalues are RUNTIME strings, and tracker ids in them reach readers who cannot resolve them (maintainer ruling 2026-08-12). The ids moved to adjacent//comments; the gate is green.Ablations — mutation and restore both proven on disk
Both mutate the COMMITTED implementation, restore under
trap ... EXIT INT TERMwith an absolute repo root, and prove the restore by whole-treegit diff HEAD+git status --porcelainplus a HEAD-blob hash match. No rebuild is needed and that is load-bearing: both test files import their subject by relative source path (./engine.js,./tenancy/platform-object-tenancy.js), not through a package exports map, so vitest reads the mutated source directly and the stale-dist false-green does not apply.bc52fde8vs HEAD54c700923 failed / 16 passed); everyglobal/unclassifiedcontrol and all mute pins stayed greenisSystemmutef89275182 failed / 17 passed); the still-muted controls stayed greenEach ablation reddens only its own gate, which is what makes the two pins independent rather than one assertion counted twice.
The system-context census red — caught by the PM, NOT by the local gate derivation
content/docs/permissions/system-context.mdxanchorsobjectql/src/engine.tsby line number in 15 places, and the engine commit's +43/-8 shifted every anchor below its change point. Reproduced at49e2695205:check-system-context-censusexit 1, 26 problems over 145 anchors and 109 census sites.dispatch-gates.mjsdoes not name this family — not at the 16-family derivation and not at the 39-family one.Lint & Repo Gatesruns it directly and the step is merely named after the first command in it, so there is nopnpm check:alias to find. This is a second instance of the standing reading that the derived family is not the whole farm; PR #13569 hit the same gate for the same reason the same night.Pure shift was confirmed before
--fixwas trusted, on both axes the ledger cares about:109 elevation read sites ... 27 declared non-readbefore and after — identical. No elevation read site arrived or vanished.git show origin/main:packages/objectql/src/engine.ts.The 15th is not rot and was not left to
--fix: this branch deliberately reworded the exact construct row 24'sNON_READ_ANCHORSneedle locates —if (isSystem && opts.bypassTenantAudit === undefined) {gained the classification term. The construct still exists and the row still describes it, so the needle was re-pointed at the new spelling before running--fix, since a needle matching zero lines makes the union short by one and--fixwould have refused the whole file as a population change. ⛔ No ledger row was added and none retired — it stays shrink-only. ⛔ Nothing was re-baselined and the gate was not quieted.--fixthen rewrote 15 anchors mechanically; the gate is green. Because the script itself was edited, both self-tests were run:check-system-context-census --self-test: all cases passedandisystem-census --self-testgreen.Re-run at the final commit
e486ce8163, not at the commit where the repair landed — the trap that bit #13569, where a later edit shifted a read a second time after--fixhad corrected it.Docs
The PM swept for pages this reclassification falsifies and found none owed beyond those anchors; my own reading agrees. The census page carries no row claiming
isSystemexempts organization stamping, and theisSystemclaims infield-level-security.mdx/authorization.mdx/execution-context.mdxare about bypassing permission checks, a different control from the tenant-audit one this ruling rescopes. ⛔ Nothing undercontent/docs/releases/was touched.Session for this work: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
Generated by Claude Code
Generated by Claude Code