Skip to content

fix(identity): API keys are minted against the minter's active organization (#8287) - #8709

Merged
qq9340100 merged 12 commits into
mainfrom
claude/issue-8287-api-key-organization
Aug 14, 2026
Merged

fix(identity): API keys are minted against the minter's active organization (#8287)#8709
qq9340100 merged 12 commits into
mainfrom
claude/issue-8287-api-key-organization

Conversation

@qq9340100

@qq9340100 qq9340100 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8287

Implements the maintainer's option-2 minimal v1 ruling (comment 5281696102) as one integrated cross-lane PR, per the cross-lane designation (comment 5293093118, 「同意」). The prior dev's STOP report (comment 5287641437) was inherited rather than re-derived; its measurements are re-verified below and two are corrected.

The defect

On OS_TENANCY_POSTURE=isolated, a minted API key read nothing. sys_api_key carried no organization column, so key auth established a user but no active organization — and the isolated Layer 0 wall is organization_id = activeOrganizationId, which with no active organization matches no row. Every org-scoped read answered 200 with total 0 while the console went on offering minting. No cross-tenant leak; the failure was in the other direction.

The column was absent by an inherited rule, not oversight: resolveInjectedSystemColumns injects organization_id into every object except managedBy: 'better-auth' ones, which sys_api_key is — even though better-auth's apiKey plugin is not loaded and the table is hand-rolled ObjectStack. Confirmed at packages/spec/src/data/injected-system-columns.ts (rule 2 returns nothing).

The read side was already wired (also confirmed): resolveApiKeyPrincipal already read an organization into tenantId, and resolveAuthzContext already adopted it. It was reading a column no mint path ever wrote.

What changed, by lane

lane file change
domain:metadata platform-objects/src/identity/sys-api-key.object.ts declare active_organization_id, index, list-view columns
domain:metadata platform-objects/src/apps/translations/*.objects.generated.ts regenerated (check:i18n)
domain:identity plugin-auth/src/managed-extension-fields.ts ADR-0105 D7 registration + the drift correction
domain:cli runtime/src/domains/keys.ts mint inherits the active org, membership check, stamping
domain:engine-core core/src/security/api-key.ts, resolve-authz-context.ts canonical read, posture-conditional refusal, ex-member fail-closed
(transport wiring) runtime/src/security/resolve-execution-context.ts, rest/src/rest-server.ts supply the effective tenancy posture

packages/spec is not touched.

⚠️ The column is named active_organization_id, not organization_id — please read this bit

This is the one place I departed from what the card and the prior dev's report assumed, and it is deliberate. The ruling says "the organization is recorded on the sys_api_key row" and does not name the column.

Semantics. This value is not "the organization that owns this row", it is "the organization this credential makes ACTIVE" — which is the ruling's own third clause. sys_session already carries exactly that fact under exactly that name, and both are read into ExecutionContext.tenantId by the one shared resolver. One concept, one name.

Measured consequence, which is the load-bearing half. objectHasOrgIdField (plugin-security security-plugin.ts) tests the registered field set for the literal organization_id, and computeTenantLayer0Filter (tenant-layer.ts) exempts objects without it. Naming the column organization_id would therefore have made sys_api_key itself org-walled, and both walled postures exclude NULL:

  • every pre-existing org-less row would vanish from the console's "My Keys" list for its own owner — and under group those keys still authenticate, so it becomes a live credential nobody can see or revoke;
  • new keys fare little better: a key minted in org A disappears from its owner's list the moment they switch to org B.

That is a fresh instance of the exact silent-empty class this card exists to remove. sys_api_key is an owner-scoped credential table like sys_user / sys_session / sys_account, scoped by the Layer 1 sys_api_key_self policy (user_id == current_user.id); keeping it there is what makes this fix additive rather than a trade. Pinned in plugin-security/src/tenant-layer.test.ts against the real field set, with the counterfactual.

If the maintainer prefers organization_id, it is a one-line rename plus accepting the Layer-0 consequences above.

⚠️ The posture comes from the tenancy service, never from OS_TENANCY_POSTURE

The first push of this branch read the env var via resolveTenancyPosture(). CI's red Build Core exposed that as wrong on two counts, and the second is the one that matters:

  • Mechanically: check:type-source-resolution requires a cross-package type import to resolve to SOURCE, so adding @objectstack/types to core forced a paths rule that collides with core's rootDir: "./src" under the tsup DTS build (TS6059). That gate's own header documents this exact cost.
  • Substantively: OS_TENANCY_POSTURE is what the operator asked for, not what is enforced. Under ADR-0093 D4/D5 a deployment requesting isolated without the enterprise @objectstack/organizations runtime resolves to single and runs with no wall at all — so the env-reading version would have refused org-less API keys on a deployment with no organization boundary to enforce, breaking working automation to uphold a wall that does not exist.

The posture is now an explicit input, resolved via effectiveTenancyPosture() from the kernel's tenancy service — the same source plugin-security reconciles before handing a posture to Layer 0, so admission and the wall can never disagree. core drops the @objectstack/types dependency entirely and both gates go quiet on their own, not by registry widening.

An absent posture disables both posture-conditional refusals, leaving behaviour exactly as before. That is deliberate: it is a question about the deployment, not about the credential, so an unwired transport is never made worse — only less strict. Wired here: the runtime dispatcher/MCP path and the REST data API, the two surfaces the card measured. Four other resolveAuthzContext callers (plugin-sharing, service-storage, service-settings, mcp stdio) are not yet wired and therefore keep today's behaviour — declared rather than silent.

The three follow-through decisions

  1. Ex-member keys fail closed at VERIFY time. No principal, not a degrade to a user-only principal (which would resurrect 200 + total 0). At verify rather than revoke-on-event because membership ends through many paths — better-auth org endpoints, SCIM, a direct sys_member delete, an ADR-0091 window lapsing — and a hook must catch every one or it silently misses. Zero extra queries: resolveUserAuthzGrants has already read sys_member for this user. Pinned by a query-counting test. Scoped to walled postures: under single there is no boundary to cross.
  2. Revocation/audit stamping — NOT implemented here; see "Findings" below. The decision stands; its implementation site turned out to be a fifth lane with a repo-wide blast radius.
  3. Existing org-less keys: posture-conditional refusal, never backfilled. single — no wall, left working. group — the wall derives from the owner's memberships independently of tenantId, so they already work there; left working. isolated — provably dead today, refused at verify time.

⚠️ Residual, carried and visible as instructed: refusing under isolated does break the one behaviour the card measured as workingGET /data/sys_user returning the key owner's own row, which is walled by member-id enumeration rather than the org column. Decided: refuse.

Distinguishability of the refusal — scoped down, and why

The decision asked for "a distinguishable error code". A new error.code must be registered in ERROR_CODE_LEDGER, which lives in packages/spec/src/api/error-code-ledger.zod.ts — off-limits to this seat. So the refusal uses the standard catalog member UNAUTHENTICATED (401), which is also what that ledger's own admission rule prescribes: "If the condition is generic (not found / permission / validation / rate limit), use the standard catalog instead of registering a synonym" — and since #8211 that rule is mechanically enforced, so a synonym might well be refused admission anyway. The behavioural requirement is met in full: the failure is loud at call time (401) instead of 200 + total 0. A machine-readable discriminator rides on ResolvedAuthzContext.authRefusal.reason, deliberately lowercase so it can never be mistaken for a wire code. Filed for the spec seat as #8708.

Verification

Union of derived gates run after the final commit, at 5f129e53f, clean tree — all green:

check:authz-resolver · check:route-envelope · check:cross-package-test-inputs · check:kernel-hook-pairs · check:test-source-alias · check:type-source-resolution · check:changeset-gate-self-tests · check:objectui-changeset · check:i18n · check:query-options-erasure · check:type-check-coverage · check:nul-bytes · check:error-code-casing · check-adr-0087-registration · check-changeset-no-major · check-empty-changeset · check-cross-package-test-inputs

Gates the dispatch prompt did not name, surfaced by re-deriving against the actual changed paths: check:authz-resolver, check:route-envelope, check:cross-package-test-inputs, check:kernel-hook-pairs, check:test-source-alias, check:type-source-resolution.

Build, as CI runs it: 31/31 turbo build tasks green, Build Core included.

Tests at 5f129e53f: core 803 · runtime 2379 · rest 1903 · plugin-auth 1204 · plugin-security 1138 · platform-objects 369 · mcp 185 — 7,981 passed, 0 failed. core has no typecheck script (pre-existing ledger entry); its types are checked by the tsup DTS build, which passes.

Reverse verification (fix committed first, direction predicted before running): restoring the old row.organization_id ?? row.organizationId chain turned the canonical-read pins RED — 4 failed / 15 passed, exactly the spelling and org-read assertions — then restored from the commit and re-confirmed green.

Fixture triage: four pre-existing fixtures spelled the retired column. All four were re-spelled rather than replaced — each merely used the alias, and each assertion still reads a value the mint path really produces. Two lived in runtime, outside the packages this change edits, and were found by sweeping the verifier's consumption radius rather than the edited package.

packages/platform-objects i18n bundles were regenerated after merging origin/main (twice, most recently at 5f129e53f), and #8149's sys_email rows were confirmed still present. Both regenerations were no-ops.

Docs screened

All hand-written docs carrying API-key vocabulary were screened. Accurate and unaffected: api/index.mdx, api/client-sdk.mdx, ai/agents.mdx, ai/connect-mcp.mdx, getting-started/build-with-claude-code.mdx, deployment/environment-variables.mdx. Screened, zero API-key vocabulary: permissions/authentication.mdx, permissions/system-context.mdx. Different subsystem (outbound connector auth, not inbound keys): automation/connectors.mdx, references/integration/connector.mdx.

permissions/authorization.mdx deserves its own line: it states that sys_api_key "deliberately stay[s] public-posture … row scoping is their guard" — still true, and only because of the naming choice above. organization_id would have falsified that sentence. Two pre-existing docs defects found and filed, not fixed here (#8715).

Findings filed, not fixed here

Generated by Claude Code

claude added 3 commits August 14, 2026 14:59
…#8287)

`sys_api_key` carried no organization, so under `OS_TENANCY_POSTURE=isolated`
a minted key authenticated a user with no active organization and the Layer 0
wall (`organization_id = activeOrganizationId`) could match nothing: every
org-scoped read answered `200` with `total 0` while the console went on
offering minting.

The column was absent by an inherited rule, not by oversight —
`resolveInjectedSystemColumns` skips `managedBy: 'better-auth'` objects, and
`sys_api_key` carries that flag even though better-auth's `apiKey` plugin is
not loaded and the table is hand-rolled ObjectStack.

- declare `active_organization_id` on `sys_api_key` (+ index, list columns)
- register it as an ADR-0105 D7 managed extension field, and correct that
  registry's long-standing drift (its comment said every column here is an
  extension field; the set listed one)
- mint (`POST /keys`) inherits the caller's active organization, re-checks
  membership against `sys_member` at mint time, and refuses under a walled
  posture rather than handing back a key that cannot read
- the verifier reads ONE spelling (PD #12), refuses an org-less key under
  `isolated`, and the shared resolver fails an ex-member's key closed using
  the membership set it had already read — zero extra queries

The column is deliberately NOT named `organization_id`: that name would make
`sys_api_key` itself org-walled, hiding pre-existing org-less rows from their
own owners while they keep authenticating under `group`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH
…ts (#8287)

Adding the dependency made `check:test-source-alias` and
`check:type-source-resolution` both fire: a unit test or typecheck that reads a
sibling's built `dist/` is a verdict about a build, not about the code in this
checkout, and the dangerous direction is the one that PASSES. Anchored regex
aliases (array form) so the bare entry cannot swallow the `/node` subpath.

Also adds the changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 14, 2026 5:42pm

Request Review

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/rest, @objectstack/runtime.

40 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/automation/webhooks.mdx (via @objectstack/core)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth, @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/kernel/contracts/cache-service.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/plugin-auth)
  • content/docs/kernel/services.mdx (via @objectstack/core)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/plugin-auth, @objectstack/rest, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/core, packages/runtime)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/system-context.mdx (via packages/rest, packages/runtime)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/plugin-auth, @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects)

5 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/plugin-auth, @objectstack/rest, @objectstack/runtime)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/rest)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v17.mdx (via @objectstack/core, @objectstack/rest, @objectstack/runtime)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 14, 2026
…ot the env (#8287)

Fixes the red `Build Core` at 2b993c9 and, underneath it, a correctness bug
the build failure exposed.

The build break: `check:type-source-resolution` requires a cross-package type
import to resolve to SOURCE, so adding `@objectstack/types` to `core` forced a
`paths` rule — which collides with core's `rootDir: "./src"` under the tsup DTS
build (TS6059). That gate's own header documents this exact cost.

The bug it exposed is the more important half. `resolveTenancyPosture()` reads
`OS_TENANCY_POSTURE`, which is what the operator ASKED for — not what is
ENFORCED. Under ADR-0093 D4/D5 a deployment requesting `isolated` without the
enterprise organizations runtime resolves to `single` and runs with no wall at
all, so the env-reading version would have refused org-less API keys on a
deployment that has no organization boundary to enforce.

The posture is now an explicit input, resolved from the kernel's `tenancy`
service — the same source plugin-security reconciles before handing a posture
to the Layer 0 wall, so admission and the wall can never disagree. `core` drops
the `@objectstack/types` dependency entirely, and both gates go quiet on their
own rather than by registry widening.

An ABSENT posture disables the two posture-conditional refusals, leaving
behaviour exactly as before: that is a question about the deployment, not about
the credential, so an unwired transport is never made worse — only less strict.
Wired here: the runtime dispatcher/MCP path and the REST data API, which are
the surfaces the card measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH
@github-actions github-actions Bot added size/xl and removed size/l labels Aug 14, 2026
#8287)

The tsconfig `paths` and vitest `alias` entries were added to satisfy
check:type-source-resolution / check:test-source-alias when core briefly
depended on @objectstack/types. That dependency is gone — the tenancy posture
now arrives from the kernel's `tenancy` service — so both entries resolve
nothing, and their comments describe a `resolveTenancyPosture` call that no
longer exists.

Left in place they would mislead the next author and re-arm the TS6059
rootDir collision the moment anyone re-added the import. Both gates stay green
without them, because the predicate is the IMPORT, not the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH
claude added 2 commits August 14, 2026 16:23
… dispatch predicates (#8287)

`check:engine-double-contract` went red at 339a1b6: the organization-inheritance
suite added a second engine double to http-dispatcher.keys.test.ts, taking the file
from 1 unguarded double to 2 on both the delete and the update slice while the
shrink-only baseline records 1.

The baseline is NOT raised — that is an explicit gate-weakening action and the
gate's own message rules it out ("pin the new one rather than raising it"). The
new `makeOrgKernel` double now routes both write verbs through
assertEngineUpdateDispatch / assertEngineDeleteDispatch from
@objectstack/metadata-core, so it cannot be looser than ObjectQL itself. The
file's pre-existing makeKernel double keeps its measured DEBT entry untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH
…erResult (#8287)

`check:type-check-debt --re-measure` went red at 339a1b6: @objectstack/runtime's
TEST_DEBT records 227 raw tsc errors and the tree measured 235 (+8). All eight are
TS18048 'res.response' is possibly 'undefined' in the organization-inheritance
suite this PR added — `HttpDispatcherResult.response` is optional, and the runtime
test layer is hidden from tsc at the package level, so `pnpm test` going green said
nothing about them.

The ledger is NOT raised — it is a shrink-only ratchet and raising it is on the
maintainer's floor. A `responseOf()` helper narrows once and throws a named error
when a dispatcher answers no response at all, so the failure stays distinguishable
from a wrong status. Scoped to the new suite: the older suite's nine reads are the
file's share of the frozen number, and pressing that down is a separate improvement
to bank, not a rider on this repair. Re-measured: runtime back to exactly 227.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH
…-key-organization

# Conflicts:
#	packages/core/src/security/resolve-authz-context.test.ts

Copy link
Copy Markdown
Collaborator Author

Gate repair — verification at the final head 538209590

Supersedes the PR body's 5f129e53f verification block, which was taken one commit behind the then-head. The body is deliberately not rewritten: the API round-trip HTML-escapes apostrophes and quotation marks, and pushing ~15k characters of reviewed prose back through it would corrupt it. This comment is the current record; the body's block stands as the implementation-pass history.

Two CI gates were red at 339a1b6ad, both shrink-only ratchets, both moved by the same new test code in packages/runtime. Neither ledger is raised — that is an explicit gate-weakening action on the maintainer's floor, and both gates say so in their own output.

1. check:engine-double-contract (job "ESLint")

The #8287 suite added a second fake ObjectQL engine (makeOrgKernel) to http-dispatcher.keys.test.ts, taking the file to 2 unguarded doubles on both slices where the baseline records 1. The new double now routes both write verbs through the producer's own predicates:

update: async (_obj: string, data: any, options?: any) => {
  assertEngineUpdateDispatch(data, options);
  return {};
},
delete: async (_obj: string, options?: any) => {
  assertEngineDeleteDispatch(options);
  return {};
},

Only the new double is pinned. The file's pre-existing makeKernel double keeps its measured DEBT entry exactly as recorded — pinning that one too would have driven the count to 0 and required deleting two ledger entries, which is a different (and out-of-scope) change. Gate now reports OK — 244 pinned (was 242; +2 is exactly this change).

2. check:type-check-debt --re-measure (job "TypeScript Type Check")

@objectstack/runtime measured 235 against a recorded 227. All +8 were TS18048 'res.response' is possibly 'undefined' in the new suite — HttpDispatcherResult.response is optional, and this package's test layer is hidden from tsc, so a green pnpm test said nothing about them. Typed out with a responseOf() narrowing helper that throws a named error when a dispatcher answers no response at all, keeping that failure distinguishable from a wrong status. Re-measured: runtime back to exactly 227, none above its recorded number.

Note for the next author: check:type-check-coverage and check:type-check-debt are different invocations of the same script, and only the latter re-measures against the ledger. The earlier union ran the former.

Reverse verification — direction predicted before running

Predicted for both: gate RED, suite GREEN — the doubles are dormant and the test layer is invisible to tsc, which is precisely why both escaped a green pnpm test. Observed:

ablation gate suite
unpin the two write verbs RED — both PINNED messages return verbatim green
unwrap the responseOf() calls 236 vs the 227 ceiling → RED green

The second landed at 236, not the predicted 235: the ablation leaves responseOf declared and unused, which is one error the pre-fix tree did not carry. Reported as measured rather than rounded to the prediction. Both restored with git checkout HEAD -- ... from the committed fix, tree confirmed clean after each.

Sibling merge

origin/main carrying #8713 (#8613) was merged at 538209590. Exactly the ruled conflict shape: one content conflict in packages/core/src/security/resolve-authz-context.test.ts; resolve-authz-context.ts and security/index.ts both auto-merged. It was a pure both-sides-appended collision — both describe blocks kept in full, nothing dropped or reconciled; the merged file runs 34 files / 820 tests green, and both sides' exports survive in the security barrel.

Union at 538209590, clean tree — all green

Re-derived with scripts/pm/dispatch-gates.mjs against the branch's actual changed paths, run after the final commit:

check:authz-resolver · check:changeset-gate-self-tests · check:cross-package-test-inputs · check:kernel-hook-pairs · check:objectui-changeset · check:route-envelope · check:test-source-alias · check:type-source-resolution · check:query-options-erasure · check:type-check-coverage · check:type-check-debt --re-measure · check:i18n · check:engine-double-contract · check:nul-bytes · check:error-code-casing · check-adr-0087-registration · check-changeset-no-major · check-empty-changeset · check-cross-package-test-inputs

Build: 70/70 turbo tasks. Tests: core 34 files / 820 tests, runtime 156 files / 2379 tests — 0 failed.

Both ledger files are untouched by this branch — git diff origin/main...HEAD -- scripts/engine-double-contract.baseline.json scripts/check-type-check-coverage.mjs is empty.

Derivation gap, recorded not fixed

check:engine-double-contract is not in the re-derived set — absent by design, handed to human judgement in the script's closing prose. That is already filed as #8632; this instance is attached there as a second measurement rather than duplicated. check:type-check-debt is derived, correctly, with its --re-measure form named. dispatch-gates.mjs is not touched here.


Generated by Claude Code

claude added 2 commits August 14, 2026 17:17
…tead of misreading them (#8287)

`check:where-matcher-conformance` went red once the engine-double gate stopped
aborting the ESLint job ahead of it: `makeOrgKernel`'s find matcher is an
`Object.entries(where).every(...)` body with no combinator branch, so it read
`$or`/`$and` as an ordinary FIELD NAME, compared `row.$or` (undefined) against
the array, matched nothing, and would have handed a suite an empty result set
with nothing erroring — shape (b) in that gate's header.

Fixed by refusal, not by implementing the combinator: the `makeKernel` matcher
160 lines above in this same file already refuses with this exact message, so
this keeps one convention in one file, and refusal is what 140 of the 233
discovered matchers already do. The baseline is NOT touched — third shrink-only
ratchet on this branch, same rule. Gate now reports 233/233 conforming, 141 by
refusing (+1, exactly this matcher).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH

Copy link
Copy Markdown
Collaborator Author

Third gate repaired, and the full CI sequence run — head 52cc65835

Supersedes the previous verification comment. check:where-matcher was red once check:engine-double-contract stopped aborting the ESLint job ahead of it — the repair unmasked it rather than causing it.

The fix: refusal, not implementation

makeOrgKernel's matcher was an Object.entries(where).every(...) body with no combinator branch, so it read $or/$and as an ordinary field name, compared row.$or (undefined) against the array, matched nothing, and would hand a suite an empty result set with nothing erroring — shape (b) in that gate's header.

Chose refusal over implementing the combinator, and the deciding reason is local rather than statistical: the makeKernel matcher 160 lines above in this same file already refuses, with this exact message. Adding a second, differently-shaped answer to the same question in one file is how the next author picks the wrong one. That it is also the well-trodden path (141 of 233 matchers now conform by refusing) is the weaker half of the argument.

return table.filter((r: any) => Object.entries(where).every(([k, v]) => {
  if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`);
  return r[k] === v;
}));

Gate: 233 matcher(s) discovered, 233 answer the combinator battery correctly or refuse it loudly (141 refuse) — +1 refusing, exactly this matcher. Baseline untouched, third shrink-only ratchet on this branch and none raised.

Reverse verification, direction predicted before running — gate RED, suite GREEN, because the suite never hands this matcher a $ key, which is precisely why the defect was silent. Observed: gate exit 1 with 1 silently wrong (141 → 140 refusing); pnpm --filter @objectstack/runtime test 2379 passing in both states. Restored with git checkout HEAD -- ..., tree clean, gate back to exit 0.

Full CI sequence, extracted mechanically and run to completion

Per the coordinator's instruction I stopped relying on the derived union and instead parsed the run steps straight out of the workflow files, then ran all of them in workflow order without stopping at the first failure, so anything masked behind this gate surfaced in this round:

job steps run result
lint (.github/workflows/lint.yml) 48 48 green
typecheck (same file) 34 34 green

82/82 green. Zero failures, and zero NOT MEASURED — every step executed for real in this environment. I specifically checked the ones that could plausibly have passed by skipping (check:required-contexts, check:shard-attestation, check:release-body, check:published-files, check:merge-driver, check:release-notes): each ran its real self-test and assertion battery, none took a degraded or offline path.

That sequence includes both turbo build invocations, turbo run typecheck (122 tasks), check:type-check-debt (33 ledger entries, none above its recorded number), check:authorable-surface, check:i18n, and the two gates repaired earlier on this branch.

Also green at this head: core 34 files / 820 tests, runtime 156 files / 2379 tests.

All three ratchet ledgers untouched by this branch — git diff origin/main...HEAD over engine-double-contract.baseline.json, where-matcher-conformance.baseline.json and check-type-check-coverage.mjs is empty.

origin/main was merged again at 52cc65835 (conflict-free this time) so the run is against current main.

On the derivation gap

check:where-matcher is not in the derived union — and unlike check:engine-double-contract, it is not in the script's judgement-call prose either. grep -c "where-matcher" scripts/pm/dispatch-gates.mjs returns 0, so a dev following that script's own output has no path to this gate at all. Attached to #8632 as a third instance with that measurement; dispatch-gates.mjs is not touched here.


Generated by Claude Code

@qq9340100
qq9340100 marked this pull request as ready for review August 14, 2026 18:27
@qq9340100
qq9340100 added this pull request to the merge queue Aug 14, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31828764827 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m scripts/build-schemas-check-mode.test.ts�[2m > �[22mbuild-schemas.ts — a shallow checkout re-anchors the deletion gate, it does not accuse (#6452)�[2m > �[22man anchor rev no
    �[41m�[1m FAIL �[22m�[49m scripts/build-schemas-check-mode.test.ts�[2m > �[22mbuild-schemas.ts — a shallow checkout re-anchors the deletion gate, it does not accuse (#6452)�[2m > �[22man anchor rev no
    �[41m�[1m FAIL �[22m�[49m scripts/build-schemas-check-mode.test.ts�[2m > �[22mbuild-schemas.ts — a shallow checkout re-anchors the deletion gate, it does not accuse (#6452)�[2m > �[22mwith no upstream
    �[41m�[1m FAIL �[22m�[49m scripts/build-schemas-check-mode.test.ts�[2m > �[22mbuild-schemas.ts — a shallow checkout re-anchors the deletion gate, it does not accuse (#6452)�[2m > �[22mwith no upstream
    �[41m�[1m FAIL �[22m�[49m scripts/build-schemas-check-mode.test.ts�[2m > �[22mbuild-schemas.ts — the output clean spares a sibling generator (#5371)�[2m > �[22mleaves gen:openapi's artifact byte-ident
    �[41m�[1m FAIL �[22m�[49m scripts/build-schemas-check-mode.test.ts�[2m > �[22mbuild-schemas.ts — the output clean spares a sibling generator (#5371)�[2m > �[22m--check does the same — the reported tri
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@qq9340100
qq9340100 added this pull request to the merge queue Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Queue-failure triage → checklist item 3, re-queued once

Answering the triage bot above rather than re-queueing blind. Verdict: not this PR's regression, not a recurring flake — most likely a same-batch semantic conflict. Re-queued exactly once; if it fails again the next reader should NOT re-queue a third time without new evidence.

Item 1 — is the failing test in a package this PR changes? No. The failure is @objectstack/spec#testpackages/spec/scripts/build-schemas-check-mode.test.ts. This PR's 18-file surface contains zero files under packages/spec or scripts/ (git diff --name-only origin/main...52cc65835 | grep -E 'build-schemas|scripts/' → no match). That package is also outside this seat's lane (domain:spec-tooling, spec seat), so a fix here would be cross-seat work regardless.

Item 2 — is it a known recurring flake? Not established, and I checked rather than assuming. The other recent queue failure I sampled (run 31826824389, 18:02) failed on something completely differentpackages/cli test/i18n-section-coverage.test.ts:532, an unexpected objects.showcase_contact._sections.who_is_this.label in the expected set. Different package, different test. So these are not one flake recurring; searching the backlog for this test name also returned no existing issue.

Item 3 — batch interaction. This is where the evidence points. The queue is demonstrably not blocked: merge_group runs succeeded at 18:03 ×2, 18:07, 18:23 and 18:29 ×2 — the last two after this failure at 18:27. And the failing assertions are precisely the shape a batch-mate touching spec artifacts would produce: generated-artifact byte-identity (leaves gen:openapi's artifact byte-identical, --check does the same) and deletion-gate anchoring (a shallow checkout re-anchors the deletion gate, it does not accuse). The queue runs the full suite against the combined batch, which is why PR-side CI — green on all 26 checks at this head — could not have seen it.

Correcting my own first reading, for the record: I initially suspected the queue's checkout shape (shallow / no upstream) made this deterministic for everyone. The interleaved successes above refute that, so it is not shared infrastructure damage and there is nothing to stop-the-bleeding on.

Landing preconditions re-verified at 52cc65835 and unchanged: 26/26 checks green PR-side, and all three shrink-only ratchet ledgers untouched (engine-double-contract.baseline.json, where-matcher-conformance.baseline.json, check-type-check-coverage.mjs), each path confirmed to exist so the empty diffs are real evidence.


Generated by Claude Code

Merged via the queue into main with commit e43d63a Aug 14, 2026
27 checks passed
@qq9340100
qq9340100 deleted the claude/issue-8287-api-key-organization branch August 14, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert)

2 participants