Skip to content

test(client,runtime): close 337 refused authz-resolver reads — the false-green class on this lane's nine sites - #18082

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-18070-authz-resolver-false-green
Sep 14, 2026
Merged

os-warren merged 3 commits into
mainfrom
claude/issue-18070-authz-resolver-false-green

Conversation

@os-warren

@os-warren os-warren commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18070

The false-green class, closed on the nine sites this lane owns: seven
@objectstack/client fixtures where the refusals were visible in the log,
and two @objectstack/runtime integration fixtures where the symptom was
pinned rather than closed. Test-only: no product code is touched, and no
dependency edge onto plugin-auth / plugin-security is added.

What was wrong

core/src/security/resolve-authz-context.ts reaches five sys_* tables through
tryFind, which classifies a missing table as "not provisioned" and answers
[]. A fixture that boots a real ObjectQL over a real SqlDriver but
registers a narrower object set therefore gets a green it did not earn: the
assertion passes because the read returned empty, not because the state was
empty. A suite in that condition cannot turn red when grant resolution breaks.

The card's counts, RE-MEASURED (not transcribed)

Every number in the card was re-derived on fb29f62ce, classified by the
(table, filter, limit) triple of the eight reads the resolver issues — so
sys_position ... where name in (...) limit 200 counts and where name = ? limit 1 on the same table does not.

file card measured after
client/src/client.metadata-prefix.test.ts 95 95 0
client/src/client.hono.test.ts 35 35 0
client/src/client.data-prefix.test.ts 25 25 0
client/src/client.batch-transaction.test.ts 25 25 0
client/src/auth-get-session-envelope.test.ts 21 21 0
client/src/client.environment-scoping.test.ts 20 20 0
client/src/auth-login-register-envelope.test.ts 6 6 0
sum 227 227 0

Whole-package control, same tree, same command shape: @objectstack/client
emitted 264 refused reads of which 227 were resolver-class; after, 37
of which 0 are. The 37 that remain are sys_metadata 25, sys_setting 7,
sys_metadata_history 3, sys_organization 2 — the probeInstallOrganizations
and boot-metadata classes, a different card. The seven summing to the
whole-package 227 is what proves there is no eighth site.

The instrument under-reads, and here is the proof

The two @objectstack/runtime sites route their refusals through
captureExpectedReadRefusals (#10629 / #11081), which withholds the driver
line
. Measured on fb29f62ce: grep -c "refused a read on" over a full run of
either file reads a clean 0, while the capture's own counter reads

notifications.hono.integration            refusals {"sys_user":10,"sys_member":10,"sys_user_position":10,
                                                    "sys_user_permission_set":10,"sys_position":10,"sys_setting":2}
                                          engineFrames identical   →  52 total, 50 resolver-class
notification-schema-conformance           refusals {"sys_user":12,"sys_member":12,"sys_user_position":12,
                                                    "sys_user_permission_set":12,"sys_position":12,"sys_setting":3}
                                          engineFrames identical   →  63 total, 60 resolver-class

After: {"sys_setting":2} and {"sys_setting":3}110 resolver-class
refusals closed
, and sys_setting, which is not resolver-class, deliberately
left exactly as it was.

Total across this PR: 337 refused authz-resolver reads closed (227 visible +
110 withheld).

The count falls because the read SUCCEEDS

Nothing is silenced, filtered or re-levelled. Proven positively by a one-off
probe that seeded one row per table and printed what the read returns —
injected, run, then restored under a trap and verified byte-exact with
git hash-object against each path's HEAD blob:

[#18070 PROBE ROWS] sys_user                [{"id":"probe-user",...,"email":"probe@example.com"}]
[#18070 PROBE ROWS] sys_member              [{"id":"probe-mem",...,"user_id":"probe-user","role":"admin"}]
[#18070 PROBE ROWS] sys_user_position       [{"id":"probe-up",...,"position":"everyone"}]
[#18070 PROBE ROWS] sys_user_permission_set [{"id":"probe-ups",...,"permission_set_id":"probe-ps"}]
[#18070 PROBE ROWS] sys_position            [{"id":"probe-pos",...,"name":"everyone","active":true}]

RESTORED packages/runtime/src/notifications.hono.integration.test.ts             blob=9f7158bc9... == HEAD-BLOB-of-the-same-path
RESTORED packages/runtime/src/notification-schema-conformance.integration.test.ts blob=299cd3b37... == HEAD-BLOB-of-the-same-path
RESTORED packages/client/src/client.environment-scoping.test.ts                   blob=2aa383b08... == HEAD-BLOB-of-the-same-path
RESTORE VERIFIED: git diff HEAD empty over all three probed paths

A by-product worth naming: seeding those rows made the new runtime assertion go
RED (expected [ { id: 'probe-user', …(6) } ] to deeply equal []). That is the
assertion reading real state rather than a stub.

⭐ The runtime half: a passing assertion had to move, deliberately

This is the review's sticking point, and it should be.

Both runtime fixtures declared ABSENT_AUTHZ_TABLES and asserted
noise.silentChannels(ALWAYS_READ_AUTHZ_TABLES) — an assertion that each of the
five reads was still being refused. That pins the symptom. Closing the read
necessarily falsifies it, and leaving it in place would leave a pin asserting a
number that no longer describes reality.

It is replaced, not deleted. What it asserted about behaviour — "these five
reads really happen on this path" — is now asserted in the direction the fix
runs, by expectResolverAuthzReadsSucceed(): each read SUCCEEDS and answers
[] because the state is empty rather than because the table is missing. Same
call sites, same -t-safety (per authed test in the hono file; in afterAll
for the conformance file, with the shutdown moved into a finally so the old
invariant — a failure here can never leave the kernel running — survives the
reordering the live-engine read forces).

ABSENT_AUTHZ_TABLES shrinks to ['sys_setting'], which is the shared capture
module's own prescribed repair: "a table that started resolving means the
fixture now provisions it"
. ⭐ Shrinking that list is also what keeps the
capture from becoming a mute: captureDriver forwards an unrecognised
refusal straight to console.warn, so a regression that stops provisioning one
of the five is now LOUD as well as red — where, while the five were declared,
the same regression would have been withheld and merely counted.

Ablation — the new pin can fail

Registration deleted, on-disk landing proven by anchor counts before/after
(rt2=1 cl1=1rt2=0 cl1=0, one ABLATED marker each), restored under a
trap and verified byte-exact against HEAD:

ablated result
notification-schema-conformance.integration.test.ts Test Files 1 failed — while all 8 Tests pass. The failure is the new afterAll assertion, i.e. the pin catches exactly the regression it exists for. The five driver refusal lines are also visible in the output, confirming the loudness claim above.
client.environment-scoping.test.ts refusals back to 20 resolver-class — and the suite still passes (5/5). That is the false green itself, reproduced on demand.

Scope and fences

  • resolve-authz-context.ts lives in packages/core/src/security/ (domain:engine) and is not touched. Nothing needed it.
  • packages/spec is not touched. The only new dependency on it is a import type { ServiceObject } — a type already re-exported from @objectstack/spec/data, which both packages already depend on.
  • Objects are registered locally, with only the columns the reading path touches (id is not declared anywhere — the registry supplies the primary key). sys_position_permission_set and sys_permission_set are deliberately absent: the resolver reaches them only after a sys_position row resolves, and measurement confirms neither appears in any of these files' refusals before or after.
  • Changeset: skip-changeset, measured rather than assumed. Both packages publish files: ["dist","README.md","CHANGELOG.md"]; grep -rl for the new symbols (AUTHZ_RESOLVER_OBJECTS, expectResolverAuthzReadsSucceed) over every one of those paths returns zero hits, against a positive control (ObjectStackClient, createRestApiPlugin) that hits dist/. No published artefact moves.

Acceptance notes

Out of scope, observed while measuring, filed as nothing:

Verification

See the report comment on #18070 for the full gate table and exit codes.

Authored by Claude Code in session session_01TbSMtGzMrtPwh925wDEZd5 — kept as prose
because this body was EDITED after creation, and the edit channel appends its own
footer block: a session-URL footer sent on an edit ends up with the platform's bare
one beneath it, two footers where the form allows one.


Generated by Claude Code

…sing fixtures

WIP — the seven `@objectstack/client` sites from #18070 now register the
`sys_*` objects `resolveUserAuthzGrants` reads, locally and with only the
columns that reading path touches.

Claude-Session: https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5
Co-authored-by: Claude <noreply@anthropic.com>
WIP — the two `packages/runtime` integration fixtures now provision the five
authz tables locally, and the `silentChannels` pins that asserted the
refusals are replaced by an assertion that the same reads succeed.

Claude-Session: https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5
Co-authored-by: Claude <noreply@anthropic.com>
@os-warren os-warren added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 13, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c54d8d67b831fe106a1f02a9fdcd88764ff81592packageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 13, 2026
@os-warren
os-warren marked this pull request as ready for review September 14, 2026 00:22
@os-warren
os-warren enabled auto-merge September 14, 2026 00:23
@os-warren
os-warren added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit b3d6918 Sep 14, 2026
54 checks passed
@os-warren
os-warren deleted the claude/issue-18070-authz-resolver-false-green branch September 14, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants