Skip to content

test(triggers,plugins,services): the authz false-green sweep — five in-lane fixtures now provision the resolver's tables - #18067

Merged
os-project-manager merged 4 commits into
mainfrom
claude/issue-17985-authz-false-green-sweep
Sep 13, 2026
Merged

os-project-manager merged 4 commits into
mainfrom
claude/issue-17985-authz-false-green-sweep

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #17985

Clause-②: no

The card handed this lane a static pointer — 73 test files outside plugin-auth
naming resolveUserAuthzGrants / resolveAuthzContext — and asked for a measurement.
Here is the measurement, the instrument's own blind spot (which it has, and which the
pointer could never have shown), the five in-lane sites it found, and the out-of-lane
ones this lane reports rather than touches.

The population, and how it was derived

Reproduced at the branch point 2f1a6f696, same greps the card used:

reading count
*.test.ts outside plugin-auth naming the resolver 73 — exactly the card's number
control, the same grep inside plugin-auth 5 (the card's 6 counts all *.ts, not just tests)
*.test.ts outside plugin-auth constructing AuthManager 4
union, de-duplicated 77 files across 18 packages

The card's method is "run the candidate packages' suites". That was done — all 18 —
and then extended to every other package this lane owns, because a candidate list built
from a grep cannot name a fixture that drives the resolver without mentioning it (and
one of the five sites below is exactly that). 44 package suites in total, each a full
vitest run --maxWorkers=2, ~34k tests, all green.

The instrument has a blind spot, and it is the one that mattered

Counting refused a read on '...' in the shared log under-reads. Four fixtures route
their refusals through captureExpectedReadRefusals (#10629 / #11081), which withholds
the driver line and asserts the count instead. Against those, a grep of the log reads
a clean zero while every read is still being refused.

A complete census of that channel: 24 captureExpectedReadRefusals call sites in the
repo; 4 of them declare the authz resolver's tables as expected-absent. Two are in
this lane, two are not. Both of this lane's two are real sites of the class, and neither
would have appeared in a log-grep sweep.

That is not a defect in #10629 — the channel names this outcome itself:

a table that started resolving means the fixture now provisions it

How a "site" is told apart from any other refused read

resolve-authz-context.ts issues eight reads with fixed filter shapes and limits, so a
refusal is attributed to the resolver by the (table, filter, limit) triple in the logged
statement — sys_position ... where name in (...) limit 200 is the resolver's; the
where name = ? limit 1 on the same table is not.

The discriminator earns its keep: plugin-security's suite emits 585 refused reads and
0 of them are resolver-class (they are sys_permission_set / sys_position /
sys_capability reads by name, a different fail-soft path), while client's 264
contain 227 that are.

Controls — so the zeros mean something

  1. Positive control, same tree, same command shape. plugin-auth's two known fixtures
    at 2f1a6f696: 7 lines, sys_user_position 2 / sys_user_permission_set 3 /
    sys_position 2 — byte-for-byte the baseline PR test(plugin-auth): register the authz objects two sign-in fixtures drive #17982 measured independently. The
    instrument detects the class when the class is there.
  2. Discrimination control. The same classifier reads 0 resolver-class out of
    plugin-security's 585 and 227 out of client's 264 — it is not "any refusal".
  3. Blind-spot control. The withheld sites were measured through the capture's own
    refusals counter, not through the log.

The five in-lane sites — before and after

Before is at 2f1a6f696; after is this branch. Per file, --maxWorkers=1.

file before after
triggers/trigger-record-change/src/record-change-integration.test.ts 75 (withheld) 0
plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts 21 (withheld) 0
triggers/trigger-record-change/src/before-update-flow-payload-reach.test.ts 10 0
triggers/trigger-record-change/src/reentrant-start-condition.test.ts 10 0
services/service-automation/src/paused-run-visibility.test.ts 5 0

121 refused authz reads, every one of them a grant resolution that answered [] because
the table was missing rather than because the state was empty.

Actual output. The two withheld ones, read off the capture's own counter by a one-off
console.log of noise.refusals (injected, run, restored):

BEFORE  status-mirror-cascade      {"sys_organization":3,"sys_approval_delegation":2,
                                    "sys_user":5,"sys_member":4,"sys_user_position":4,
                                    "sys_user_permission_set":4,"sys_position":4}
AFTER   status-mirror-cascade      {"sys_organization":3,"sys_approval_delegation":2}

BEFORE  record-change-integration  {"sys_organization":8,"sys_user":15,"sys_member":15,
                                    "sys_user_position":15,"sys_user_permission_set":15,
                                    "sys_position":15}
AFTER   record-change-integration  {"sys_organization":9}

The three visible ones, counted from the log:

BEFORE  before-update-flow-payload-reach  total=11  RESOLVER-CLASS=10  other={sys_organization:1}
AFTER   before-update-flow-payload-reach  total=1   RESOLVER-CLASS=0   other={sys_organization:1}
BEFORE  reentrant-start-condition         total=11  RESOLVER-CLASS=10  other={sys_organization:1}
AFTER   reentrant-start-condition         total=1   RESOLVER-CLASS=0   other={sys_organization:1}
BEFORE  paused-run-visibility             total=7   RESOLVER-CLASS=5   other={sys_organization:2}
AFTER   paused-run-visibility             total=2   RESOLVER-CLASS=0   other={sys_organization:2}

Whole-suite, after: plugin-approvals 45 files / 738 tests / 32 refusals, 0 resolver-class ·
trigger-record-change 10 / 101 / 3, 0 resolver-class · service-automation 134 / 1581 / 8,
0 resolver-class. All sys_organization and one sys_metadata_activation — the
probeInstallOrganizations class, not this card's.

The count falls because the read SUCCEEDS

⛔ No log line is silenced, filtered or re-levelled: the diff is five test files, zero
product code. The declared-absent lists shrink — the tables leave them because the
fixture now provisions them, and sys_organization / sys_approval_delegation stay
declared because those probes are still genuinely unprovisioned.

The positive proof, PR #17982's shape: a one-off, non-committed assertion that seeds a
sys_user_position row and then asks the resolver for that principal's grants. Before the
fix it cannot even reach the table; after it, the row comes back and the resolver's answer
carries it.

BEFORE  SqliteError: insert into `sys_user_position` ... - no such table: sys_user_position
        (both files; 1 failed | N passed)

AFTER   ONEOFF-17985 sys_user_position rows = [{"id":"oneoff17985", ...,
          "user_id":"approver","position":"oneoff_role","organization_id":null}]
        ONEOFF-17985 resolved positions = ["oneoff_role","everyone"]

oneoff_role reached grants.positions only because the resolver's sys_user_position
read returned a real row — the leg that answered [] before.

Both legs are anchored on HEAD (which carries the implementation), restored with
git checkout HEAD -- ..., and verified by git hash-object against the HEAD blob plus an
empty git diff HEAD:

RESTORE-PROOF ok packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts:
  HEAD blob cc4d10ec9fbe1fdc316f0bfb37d62ec4d54a1aeb == working tree cc4d10ec9fbe1fdc316f0bfb37d62ec4d54a1aeb
RESTORE-PROOF ok packages/triggers/trigger-record-change/src/record-change-integration.test.ts:
  HEAD blob 7d0efb716f0ec3b6311714012c958185287bd44a == working tree 7d0efb716f0ec3b6311714012c958185287bd44a
RESTORE-PROOF ok: git diff HEAD is EMPTY

Each injection was proved to have landed on disk before the run (marker count grepped, not
inferred from the editor's exit code), and every mutation ran under a trap ... EXIT INT TERM.

The remedy, and the edge it does not add

Each fixture declares the objects locally, with only the columns the reading path
touches, so ⛔ no dependency edge onto plugin-auth or plugin-security is added — the
find-envelope-limb-removal.test.ts precedent PR #17982 applied:

object columns declared why
sys_user id, email id filter; email is the RLS owner-email fallback
sys_member id, user_id, organization_id, role both membership reads
sys_user_position id, user_id, position, organization_id ADR-0057 D4 role assignments
sys_user_permission_set id, user_id, permission_set_id, organization_id user-scoped grants
sys_position id, name, active, organization_id name filter, isRowActive, tenant scope

sys_position_permission_set and sys_permission_set are deliberately absent: the
resolver reaches them only once a sys_position row resolves and a permission-set id is
collected, and none of these fixtures seeds either. The ADR-0091 validity columns are absent
for the reason sys_member lacks them today — isGrantActive reads an absent bound as
unbounded, so declaring them would change no verdict.

Out of lane — REPORTED, not touched

🔴 The population is repo-wide by construction and this lane owns part of it. These are
measured and left alone for routing.

package lane resolver-class refusals shape
@objectstack/client domain:cli 227 visible in the log
@objectstack/runtime domain:cli 0 visible 2 fixtures declare the 5 authz tables as expected-absent, so the refusals are withheld and asserted

client, per file — these seven account for 100% of the package's 227, so there is no
eighth site in it:

src/client.metadata-prefix.test.ts      95
src/client.hono.test.ts                 35
src/client.data-prefix.test.ts          25
src/client.batch-transaction.test.ts    25
src/auth-get-session-envelope.test.ts   21
src/client.environment-scoping.test.ts  20
src/auth-login-register-envelope.test.ts 6

runtime, by file: src/notifications.hono.integration.test.ts and
src/notification-schema-conformance.integration.test.ts, each declaring
['sys_user','sys_member','sys_user_position','sys_user_permission_set','sys_position','sys_setting']
as ABSENT_AUTHZ_TABLES.

Every other swept package reads 0 resolver-class, including core, spec, rest,
mcp, dogfood, verify, cloud-connection, plugin-hono-server, organizations,
plugin-security, plugin-sharing and all 16 services/*, 4 connectors/* and the other
two triggers/*.

Checks

  • 55 gate commands derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
    from the delivered change set at this head — see the report comment on sweep: do harnesses outside plugin-auth drive resolveUserAuthzGrants against a narrower object set? — the #17897 class is a latent FALSE GREEN, not just log noise #17985 for the verdict line.
    check:type-check-debt first answered exit 3 (PREREQUISITE NOT MET, heap OOM), which is
    not a pass; re-run under the --max-old-space-size=6144 ceiling its own script pins, it
    exits 0 with a real verdict (5 ledger entries re-measured, none above its recorded number).
  • pnpm --filter @objectstack/plugin-approvals --filter @objectstack/trigger-record-change --filter @objectstack/service-automation typecheck — exit 0, all three Done.
  • Lint, as a declared narrowing with the three readings it needs: population 6725
    tracked files matching eslint.config.mjs's own **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}
    minus its only global ignores (NEVER_LINTED); 5 files linted, counted from
    --format json, 0 errors / 0 warnings; and the invariance that makes the narrowing a
    measurement rather than a skipped run — this repo's config never enables type-aware
    linting
    (no parserOptions.project, no typed @typescript-eslint rules, stated and
    measured with a positive control at eslint.config.mjs:327), so no edit in this diff can
    move the verdict on a file it does not contain. The full-repo scan is CI's.
  • grep -naP control-character scan over all five edited files — no matches.

No changeset — measured, not assumed

All three packages ship files[] = ["dist","README.md","CHANGELOG.md"]. After a build,
grepping the shipped path for every symbol this diff introduces:

authzResolverObjects       in dist/ -> 0 files   (x3 packages)
status-mirror-cascade      in dist/ -> 0 files
paused-run-visibility      in dist/ -> 0 files
reentrant-start-condition  in dist/ -> 0 files
compiled test files        in dist/ -> 0

POSITIVE CONTROL — a real published symbol per package:
ApprovalService            in plugin-approvals/dist       -> 4 files
RecordChangeTriggerPlugin  in trigger-record-change/dist   -> 4 files
AutomationServicePlugin    in service-automation/dist      -> 4 files

Nothing published moves, so this carries skip-changeset rather than a changeset.

Clause-② re-determination from the delivered diff: no. The diff is five *.test.ts
files. It adds no exported symbol reachable from any published entry (measured above: every
introduced identifier is absent from all three dist/ trees while a real published symbol
is present in four files of each), and no new key on an already-published payload — the
object literals it adds are fixture-local consts consumed only by registry.registerObject
inside the same file.

Acceptance notes

Observations from the sweep, noted and not filed — none is in this card's class, and
each already has a home or no one to hand it to:

  • plugin-security emits 585 refused reads, 0 resolver-class: sys_permission_set
    (471), sys_organization (81), sys_position (24), sys_capability (7),
    sys_audience_binding_suggestion (2). The sys_* ones are reads by name from a
    different fail-soft path; 14 more are a deliberate connection-abort fixture. Carrier: the
    domain:services seat already owns the file surface, but no PR in flight touches it.
  • The probeInstallOrganizations / seed-loader / metadata classes are the bulk of what is
    left repo-wide (rest 362 sys_metadata, dogfood 126 sys_migration, verify 22
    sys_migration, and sys_organization across ~10 packages). That population is [finding] 15 more PASSING @objectstack/runtime tests print the same refused a read on feature — 134 lines after #10380 quiets its two, led by notifications.hono.integration.test.ts (52) #10629's,
    not this card's.
  • trigger-record-change/src/record-change-integration.test.ts emits one refusal on
    sys_metadata_activation that its captureExpectedReadRefusals list does not declare —
    unchanged by this diff, present in both legs.
  • service-automation/src/notify-zero-delivery-visibility.integration.test.ts exits 1 when
    run as a single file under --maxWorkers=1 and passes in the package run. Pre-existing,
    untouched by this diff. Carrier: none identified.

Generated by Claude Code

…cts two fixtures drive

Both fixtures boot a real ObjectKernel over a real better-sqlite3 SqlDriver and
register only their own test objects. Every write/decision then drives core's
`resolveUserAuthzGrants`, whose five reads were refused by the driver because the
tables were never provisioned. `tryFind` classifies a missing table as "not
provisioned" and answers `[]`, so nothing went red: both suites resolved grants
from reads that never happened.

The refusals were invisible to a shared-log sweep because #11081 withholds and
asserts them through `captureExpectedReadRefusals`. Registering the objects
locally — with only the columns the reading path touches, so no dependency edge
onto plugin-auth or plugin-security is added — makes the reads SUCCEED, which is
the outcome that channel's own contract names. The declared-absent lists shrink
to the tables that are still genuinely unprovisioned.

No product code changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
…jects three more fixtures drive

The same defect the previous commit closed, in the three sites the repo-wide
sweep found outside the two the withheld-noise channel was hiding: each boots a
real ObjectKernel over a real better-sqlite3 SqlDriver, registers only its own
test object, and then drives core's `resolveUserAuthzGrants` against tables that
were never provisioned. Measured at 2f1a6f6, per file with --maxWorkers=1:
before-update-flow-payload-reach 10, reentrant-start-condition 10,
paused-run-visibility 5 — five tables each, no line of it visible as a failure
because `tryFind` answers `[]` for a missing table.

Registered locally with only the columns the reading path touches, so no
dependency edge onto plugin-auth or plugin-security is added. No product code
changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
… seam

`objectql.registry` is typed `EngineSchemaRegistryView` here, which declares no
`registerObject`; the file already carries a structural `TestObjectRegistry` for
exactly that and casts to it two lines up. tsc --noEmit TS2339, fixed by using
the same cast.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 13, 2026
@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 6d647858b7c309b1a4b0afdc97d78062a606416fpackageMentionDocs.

Copy link
Copy Markdown
Collaborator

Contract review — Verdict: PASS

Served-tier: claude-opus-5
Implemented-by: claude/issue-17985-authz-false-green-sweep
Reviewed-by: session_01URLHobLUJB9K1ABV6ofdjj

Head reviewed: 820bf5d20f59382feb377a1baa728950fd88133f
Seat: domain:services execution seat, default-judgment tier.

⚠️ Served-tier: is stated even though no carrier is being cleared here, so it reads claude-opus-5 honestly. This card hangs no needs:contract-review on either carrier, so no clearance occurs and C7's at-tier requirement is not engaged — ⛔ this record is not a substitute for one where it is.

Clause ② — re-derived from the DELIVERED diff, ⛔ not carried from the claim

no, and the claim's prediction holds. Derived, not recalled:

probe reading
files changed 5, all *.test.ts · 0 non-test files · 0 package.json
+export lines added across the whole diff 0
files[] of all three packages ['dist','README.md','CHANGELOG.md'] — test sources are never published
any index.ts re-exporting a .test module none

⭐ The dev's own discriminating control is the right shape and I record it because a negative needs one: after a build, every identifier this diff introduces greps to 0 files in each dist/, while the same probe finds a real published symbol (ApprovalService, RecordChangeTriggerPlugin, AutomationServicePlugin) in 4 files of each. The probe can see a published export; it does not see any of this diff's.

check-clause2-carriers.mjs --pair 18067exit 0 (captured before any pipe), run from a worktree at origin/main 6d647858b.

The card's hard prohibition, checked directly

The card said: ⛔ "Do not silence, filter or re-level the log line. The count must fall because the read SUCCEEDS."

Measured on the diff: 0 touches to captureExpectedReadRefusals, ABSENT_AUTHZ_TABLES, logLevel, suppress, or DATABASE_ERROR — neither added nor removed. The fix is local object registration, with a per-column justification and an explicit account of which objects are ⛔ not registered and why (sys_position_permission_set / sys_permission_set are unreachable on this path). ⇒ the reads now succeed; the line was not quietened.

No dependency edge added, as the card required: 0 package.json changes, 0 new plugin-security imports.

Landing state

  • 13 workflow runs, 11 distinct workflow names (= the open-PR reference), 0 non-green, 0 incomplete.
  • Lint & Repo Gates: 180 steps, 0 non-green.
  • check-governed-merges.mjs --pr 180670 of 5 paths hit the register ⇒ NOT governed; ordinary queue landing applies.
  • Red lines: 0 — no packages/spec, no content/docs/releases/**.
  • Gate reconciliation carries a denominator, as it must: 55 derived / 55 run / 0 NOT MEASURED / 0 UNRUN. ⭐ check:type-check-debt first answered exit 3 (PREREQUISITE NOT MET) and was driven to a real exit 0 under the ceiling its own script pins — ⛔ not recorded as a pass.

Noted, not blocking

⚠️ Card #17985 carries pm:retriage: the delivery report stated that "triage's written escalation clause" had fired and the card was therefore p2. No such clause exists — measured, the card reserves grading to triage in as many words. The grade stays priority:p3 until triage rules; the objection and its evidence are on the card. This bears on the card's grade, ⛔ not on this diff, which lands on its own merit.

⚠️ The out-of-lane sites this sweep measured (@objectstack/client 227, @objectstack/runtime 2 pinned-not-closed) are filed as #18070 and routed to domain:cli, so they outlive this card's closure.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants