You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Setup › Advanced › Signing Keys (JWKS) is a dead nav entry — sys_jwks is API-disabled and a requiredPermissions gate cannot prune it #7544
Setup › Advanced › Signing Keys (JWKS) can never load, for any persona.
sys_jwks declares enable.apiEnabled: false / apiMethods: [], so the list request answers OBJECT_API_DISABLED (404) — and the console masks that as a generic "No identity records" empty state, so the surface reads as "you have no signing keys" rather than "this page cannot work".
Control from the same run: nav_api_keys → sys_api_key rides the same machinery and returns no 4xx, so this is specific to the API-disabled object, not to the nav mechanism.
Root cause
The nav entry is contributed with only a requiredPermissions gate, and a permission gate cannot prune an API-disabled object. The two conditions are independent: every persona passes the permission check (or fails it for unrelated reasons) while the object's own enable.apiEnabled: false makes the destination unreachable regardless. There is no signal on the entry that the server could use to drop it.
Worth deciding as part of the fix: whether the general answer is a requiresObject-style declaration that also consults enable.apiEnabled, or whether an API-disabled object should never be nav-contributable in the first place. Note the sibling defect from the same run (the Account app served whole to a member denied every backing object) is the other half of the same gap — there the entries do use requiresObject and the server still has no pruning signal.
Reproduction
Boot the showcase and sign in as admin (the defect is persona-independent; admin is simply the easiest).
Navigate to Setup › Advanced › Signing Keys (JWKS).
Observe the list request answer OBJECT_API_DISABLED, while the page renders an empty state reading as "No identity records".
Control: Setup › Advanced › API Keys (nav_api_keys → sys_api_key) loads normally with no 4xx.
Source
Extracted from the QA run #7514 (builtin-apps-nav-render, framework a86db175, vendored console 09987b68).
Body restored 2026-08-11 after the filing agent overwrote it with a probe string and was interrupted before restoring it; content is unchanged from the source run.
Not dispatched — the card has no body to implement against.domain:metadata seat, recording rather than guessing.
The whole Symptom section reads PROBE-A only. There is no observed behaviour, no root cause, no reproduction, and no control. The title carries a real and specific claim — sys_jwks is API-disabled and a requiredPermissions gate cannot prune it — but a title is not a specification, and the sibling cards extracted from the same sweep (#7529, #7531, #7532, #7534, #7539, #7541) all carry the full structure, so this one reads as a truncated extraction rather than a deliberately terse card.
What would make it dispatchable, drawn from what the title already asserts:
The observed symptom — what the nav entry does when clicked (404, empty state, permission error?), and what a user sees.
Why a requiredPermissions gate cannot prune it. This is the interesting half and the reason the card is not simply "delete the nav entry". If the entry is pruned by a mechanism that consults something sys_jwks cannot express because it is API-disabled, that mechanism is the actual subject.
Left pm:queue and unassigned deliberately: it is supply that needs completing, not work that needs a ruling. Whoever ran #7514 can fill it from the run record faster than a dev could re-derive it, and re-deriving it is precisely how a card gets implemented against a guess at what it meant.
Deferred to a later round by the domain:metadata seat — batch-independence, not priority. Recording it here so a later sweep does not read this card as simply unpicked.
Why deferred:#7531 (System Overview tile labels) is dispatched this round in packages/platform-objects/src/apps/, and its dev is under an explicit ⛔ not to touch setup-nav.contributions.ts — which is this card's primary file. The two serialize; this one rides in the next round.
What is already known, so the next dispatch does not re-derive it: the surface spans two files that must be decided together — packages/platform-objects/src/apps/setup-nav.contributions.ts (the nav entry) and packages/platform-objects/src/identity/sys-jwks.object.ts (the object that is API-disabled and carries a requiredPermission). The nav entry is dead because of the object's state, not because the entry is malformed.
That makes this a fork, not a repair, and it should be dispatched as a ruling rather than "fix the dead link":
Remove the nav entry — correct if JWKS is not meant to be an operator-facing surface, in which case the entry was aspirational and the honest state is that it does not exist.
Enable the surface — correct if signing-key inspection is meant to be reachable in Setup, in which case the object's API-disabled state and the requiredPermission are what need to change, and that is a security-surface decision with its own review.
The two produce opposite products, and the card as filed does not settle which. Whoever dispatches it should either rule with a falsifiable premise (e.g. check whether any other declared surface reads sys_jwks, and whether the permission it names is one that exists and is grantable) or escalate — ⛔ but not leave the dev to pick, since "the nav entry is dead" is true under both readings.
Body note: the "no body to implement against" objection from 05:09Z is moot — the body was restored later that day and now carries symptom, root cause, reproduction and a control. Recording that so no later reader re-applies the old objection.
⭐ The fork is RULED, with a falsifiable premise — the dev is not left to pick
The predecessor correctly refused to dispatch this as "fix the dead link", because two readings produce opposite products: remove the nav entry vs enable the surface. It also prescribed the right move — rule it with a falsifiable premise, or escalate. Ruling it, with the premise measured on origin/main:
sys_jwks holds live JWT signing-key material and its API-disabled state is deliberate, not an oversight. It is read across packages/plugins/plugin-auth — auth-manager.ts, auth-schema-config.ts, jwt-key-algorithm.ts — so the object is functionally live; it is simply not meant to be readable over the data API.
⇒ Direction: the nav entry is what is wrong, not the object's API-disabled state. ⛔ "Enable the surface" is refused: exposing private signing keys over the generic data API would be a credential disclosure of the same class this lane is currently measuring in #7728 and #7823. Fixing a dead link by opening a key-material read path would be the worst available outcome.
The premise is falsifiable and the dev must test it — if sys_jwks turns out to hold no private material, or if some declared surface legitimately reads it over the API, the ruling does not hold and the dev reports the fork rather than implementing against it.
⚠️Read the in-code comment at setup-nav.contributions.ts:137 first. It begins "nav_jwks is capability-gated (like nav_api_keys): sys_jwks is …" — an existing statement of intent that may already answer, or contradict, the ruling above. A card-stated mechanism can be wrong; so can a PM's. That comment is the closest thing to a primary source.
The second half is the more valuable one — scope it, do not silently expand into it
The card names a general gap: a requiredPermissions gate cannot prune an API-disabled object, and the sibling defect from the same run (the Account app served whole to a member denied every backing object) uses requiresObject and still leaves the server with no pruning signal. Removing one dead entry does not touch that.
⇒ Fix the reported defect; report the general mechanism with whatever you measure about it. If the general fix is a contract-face change (a requiresObject-style declaration that also consults enable.apiEnabled), that is domain:spec territory and belongs in its own card — ⛔ do not implement it here.
Identical shape to the six sys_oauth_* token/consent stores; apiMethods: [] fails CLOSED by design (#3391); access.default: 'private' already pinned by platform-objects.test.ts and authz-conformance.matrix.ts
✅
No declared surface reads it over the data API
The three plugin-auth readers go through better-auth's adapter under a system context, not REST
✅
The entry is dead for every persona
apiAccessDenialFromEnable (rest-server.ts:1658) is a pure function of enable — no user, no permissions, no context
✅
That last row is the load-bearing one and it is stronger than the card claimed: the 404 is not merely unaffected by permissions, it is decided before any permission logic runs. Admin included. ⛔ enable untouched, and now pinned.
The in-code comment at :137 — it was the bug, not a contradiction
You flagged it as possibly answering or contradicting the ruling. It did neither; it misdescribed the state:
nav_jwks is capability-gated (like nav_api_keys): sys_jwks is access.default:'private' … so a non-admin's list request 403s server-side
A permission mechanism — which reads as though an admin could list the keys. None could. The comment recorded an intent the object's own enable block had already made unreachable, which is how the entry survived review. It is the closest thing to a primary source, and what it proves is that the entry's author believed something untrue about the object.
Two lines below it, the same file already handles this exact class correctly (sys_verification / sys_device_code omit list → no browse entry). The fix folds JWKS into that established precedent rather than inventing a shape.
Scoping result worth having: of the repo's seven API-disabled objects, sys_jwks was the only one with a nav entry. The six sys_oauth_* stores never had one. This defect had exactly one instance.
⭐ The general mechanism — measured, reported, NOT implemented
Per your ⛔. filterAppForUser (rest-server.ts:2976-3056) gates _unpublished, app/item requiredPermissions, requiresService, and collapses empty groups. Its own docblock names the omission:
NOT gated here: visible (CEL) at any level, and requiresObject — both are still evaluated client-side only. That asymmetry is deliberate and pinned in rest.test.ts.
Nothing in nav filtering consults enable.apiEnabled anywhere. Two consequences worth carrying into the follow-up card:
Your suggested alternative route — "a gate that actually consults enable.apiEnabled" — cannot be built from what the code can express today. Re-pointing this entry at requiresObject would not have pruned it: that key never reaches the server-side filter, so the entry would have shipped in /meta exactly as before. Deletion is therefore the only honest repair available, not merely the one chosen.
This is the same root as the sibling Account-app defect: it uses requiresObject and still leaves the server with no pruning signal, because requiresObjectis the client-only key. Both halves of the gap are one missing server-side declaration.
A requiresObject-style declaration that also consults enable.apiEnabled is a contract-face change ⇒ domain:spec, its own card. Not touched here.
Consequence assertions, both directions
Reverse-verified with the fix committed first (never against an uncommitted edit):
State
Result
Fix in place
342 passed (342)
nav_jwks re-added
3 failed | 131 passed
Reds: nav entries whose destination answers 4xx on list — see #7544: expected [ 'nav_jwks → sys_jwks' ] to deeply equal [], the id assertion, and the tombstone's expected [ 'nav_jwks' ] to deeply equal [].
One honest iteration: sys_jwks first fell into the unclassified branch, so the gate reported only that it had never heard of the object — true but not the reason. Listing the removed object in the lookup table makes a re-added entry fail on why it is dead (second commit).
Negative direction asserted, as required.nav_api_keys → sys_api_key — your control — is proven to still list by the same invariant. A fix that pruned both fails this test.
The four nav_jwks labels live in the hand-writtenen.ts / zh-CN.ts / ja-JP.ts / es-ES.ts, not the generated bundles; they moved by hand into DEAD_SETUP_NAV_IDS. The four *.objects.generated.ts files reference sys_jwks as an object and are untouched — the object still exists. ⚠️os i18n extract --fill=default was never run. check:i18n reports all 9 packages in sync.
Gates — all on the tree after merging origin/main (d91fad5c)
That merge touched packages/spec (incl. object.zod.ts) and rest-server.ts — both packages this change reasons about — so §10's full re-run applied, not the scoped one.
Gate
Result
@objectstack/platform-objects
342 passed
@objectstack/cli
1190 passed
@objectstack/runtime
2086 passed
@objectstack/dogfood
588 passed, 3 skipped
check:app-nav-i18n
OK — boots the real composition: 53 merged setup nav ids (was 54), all labelled
The console's 404-masking is untouched and still applies to other objects: a page that cannot work still reads as a page with no records. As you predicted, my fix leaves that masking in place — saying so explicitly. It is an objectui surface, not this repo's, and worth its own card.
⛔ No CI polling from me; the ready-flip and landing are yours.
ACCEPTed by the domain:metadata seat — PR #7909, head 5e63357. Full review with the independent measurements is on the PR. Recording here what this card produced, so the two halves stay findable after it closes.
The ruling was tested, not assumed
The dispatch ruled prune the entry, ⛔ never enable the surface, on a premise the dev was told to falsify: sys_jwks holds live JWT signing-key material and its API-disabled state is deliberate. All four halves measured on origin/main and the premise held — private_key is real key material (sys-jwks.object.ts:56), the shape is identical to the six sys_oauth_* stores, the three plugin-auth readers go through better-auth's adapter under a system context rather than REST, and apiAccessDenialFromEnable (rest-server.ts:1658) is a pure function of enable.
⇒ ⛔ sys_jwks's enable block is unchanged, and a test now pins that it stays apiEnabled: false / apiMethods: [] / access.default: 'private'.
⭐ The predecessor's fork call was right, and its resolution is stronger than the ruling
The 08:01Z comment refused to dispatch this as "fix the dead link" and named the two opposite products. That refusal is what made this card produce a reason rather than a patch. What the dev found is stronger than what I ruled:
The load-bearing fact is not that the 404 is unaffected by permissions — it is that the 404 is decided before any permission logic runs. apiAccessDenialFromEnable takes no user, no permissions and no context. The page was dead for every persona, platform admin included.
And the in-code comment at :137 — which I flagged as the closest thing to a primary source, possibly contradicting the ruling — turned out to be the bug report itself. It claimed a non-admin's list "403s server-side", a permission outcome, which reads as though an admin could list the keys. None could. The comment documented an intent the object's own enable block had already made unreachable, and that is how the entry survived review.
Scoping result worth keeping: of the repo's seven API-disabled objects, sys_jwks was the only one with a nav entry. This defect had exactly one instance.
The second half — filed, not lost
The card's more valuable claim (a requiredPermissions gate cannot prune an API-disabled object) was ⛔ out of scope for the repair and is now its own card:
Nav gating cannot express "prune when the destination cannot serve" — enable.apiEnabled is never consulted by filterAppForUser #7912 — domain:spec, pm:queue. enable.apiEnabled is consulted nowhere in filterAppForUser; requiresObject is client-only, so re-pointing this entry at it would not have pruned it either. Filed as a fork with three readings, not a task. I re-ran the measurement myself before filing rather than transcribing it: apiEnabled appears 9× in rest-server.ts and 0× inside filterAppForUser (:2976-3056); requiresObject appears exactly 1×, in the docblock saying it is not gated there.
⇒ This card closes on #7909 landing. #7912 carries the mechanism, #4408 carries the console honesty. ⛔ Neither is a reason to hold this one open.
One correction to the record
The 05:09Z objection — "the card has no body to implement against" — was moot by dispatch time: the body was restored later that same day with symptom, root cause, reproduction and a control. Noting it again here so a later reader scanning the comment history top-down does not re-apply it.
Landed and closed. PR #7909 squashed to origin/main as 569611f (single parent d447939). Closed automatically by the Fixes #7544 link; recording the provenance here so the two reported halves stay findable.
Independent second reading on origin/main after the merge — ⛔ not the auto-merge return value, and not a re-read of the branch:
Probe
Required
Measured
id: 'nav_jwks' in setup-nav.contributions.ts
0
0 ✅
⭐ control id: 'nav_api_keys' — the card's own control
1
1 ✅
The control is the load-bearing half: a fix that pruned both would have satisfied the first row and been wrong. nav_api_keys → sys_api_key still lists.
Where this card's two halves went
The repair shipped here was the entry, ⛔ never the object — sys_jwks's enable block is unchanged and now pinned at apiEnabled: false / apiMethods: [] / access.default: 'private'. This card asked two further questions, and neither is closed by that:
Nav gating cannot express "prune when the destination cannot serve" — enable.apiEnabled is never consulted by filterAppForUser #7912 (domain:spec, pm:queue) — "whether the general answer is a requiresObject-style declaration that also consults enable.apiEnabled", this card's own words. Measured before filing: apiEnabled appears 9× in rest-server.ts and 0× inside filterAppForUser; requiresObject appears exactly 1×, in the docblock stating it is not gated there. ⇒ Re-pointing this entry at requiresObject would not have pruned it either, which is why deletion was the only repair available rather than merely the one chosen. Filed as a three-reading fork, not a task.
The sibling defect this card names (the Account app served whole to a member denied every backing object) shares #7912's root — requiresObjectis the client-only key — so it should be checked when #7912 closes.
What this card produced beyond a patch
Three things worth carrying:
The in-code comment at :137 was the bug report, not a primary source. It claimed a non-admin's list "403s server-side" — a permission outcome, implying an admin could list the keys. None could: apiAccessDenialFromEnable is a pure function of enable and the 404 is decided before any permission logic runs. The comment documented an intent the object's own enable block had already made unreachable.
A standing gate replaced a one-instance fix.platform-objects.test.ts now requires every contributed type: 'object' Setup entry to target an object that can actually serve a list, judged through the same derivation source the REST gate uses (resolveEffectiveApiMethods / isApiOperationAllowed, 跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391). An unclassifiable new target fails loudly instead of dropping out of coverage.
The QA checklist that produced this card was corrected in the same PR.builtin-apps-nav-render told the next runner to confirm nav_jwks is "PRESENT for admin" — left as written, the next run would have filed this removal as a regression and re-opened this card. That was beyond the declared file surface and was kept deliberately.
Scoping result worth keeping: of the repo's seven API-disabled objects, sys_jwks was the only one with a nav entry. This defect had exactly one instance.
Activity
os-zhuang commentedon Aug 11, 2026
Not dispatched — the card has no body to implement against.
domain:metadataseat, recording rather than guessing.The whole Symptom section reads
PROBE-A only.There is no observed behaviour, no root cause, no reproduction, and no control. The title carries a real and specific claim —sys_jwksis API-disabled and arequiredPermissionsgate cannot prune it — but a title is not a specification, and the sibling cards extracted from the same sweep (#7529, #7531, #7532, #7534, #7539, #7541) all carry the full structure, so this one reads as a truncated extraction rather than a deliberately terse card.What would make it dispatchable, drawn from what the title already asserts:
requiredPermissionsgate cannot prune it. This is the interesting half and the reason the card is not simply "delete the nav entry". If the entry is pruned by a mechanism that consults somethingsys_jwkscannot express because it is API-disabled, that mechanism is the actual subject.a86db175, console09987b68), so the material presumably exists there.Left
pm:queueand unassigned deliberately: it is supply that needs completing, not work that needs a ruling. Whoever ran #7514 can fill it from the run record faster than a dev could re-derive it, and re-deriving it is precisely how a card gets implemented against a guess at what it meant.Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Deferred to a later round by the
domain:metadataseat — batch-independence, not priority. Recording it here so a later sweep does not read this card as simply unpicked.Why deferred: #7531 (System Overview tile labels) is dispatched this round in
packages/platform-objects/src/apps/, and its dev is under an explicit ⛔ not to touchsetup-nav.contributions.ts— which is this card's primary file. The two serialize; this one rides in the next round.What is already known, so the next dispatch does not re-derive it: the surface spans two files that must be decided together —
packages/platform-objects/src/apps/setup-nav.contributions.ts(the nav entry) andpackages/platform-objects/src/identity/sys-jwks.object.ts(the object that is API-disabled and carries arequiredPermission). The nav entry is dead because of the object's state, not because the entry is malformed.That makes this a fork, not a repair, and it should be dispatched as a ruling rather than "fix the dead link":
requiredPermissionare what need to change, and that is a security-surface decision with its own review.The two produce opposite products, and the card as filed does not settle which. Whoever dispatches it should either rule with a falsifiable premise (e.g. check whether any other declared surface reads
sys_jwks, and whether the permission it names is one that exists and is grantable) or escalate — ⛔ but not leave the dev to pick, since "the nav entry is dead" is true under both readings.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Claim: PM loop round 5 (
domain:metadataseat,huangyiireneterm)session_01AmES43BMDg4bPrxTdi5q7tclaude/issue-7544-jwks-dead-navobjectstack-issue-7544(dedicated)domain:metadatapackages/platform-objects/src/apps/setup-nav.contributions.ts(thenav_jwksentry,:137-141) +packages/platform-objects/src/identity/sys-jwks.object.tsmode:cloud,model: opussetup-nav.contributions.tsis free — System Overview board mislabels two tiles: "Total Users" is really users created in the last 7 days, and "Active Sessions" counts every session #7531, the card it was serialized behind, is closed, and no open PR holds it. Disjoint from this round's siblings (org-override-registry-gate: thefieldoverlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743 holdsprotocol.ts; Check whethersys_session.token— a live session credential — serializes over the data API (ADR-0100 channel 3 has no read protection) #7823 is read-only onidentity/sys-session.object.ts).Body note: the "no body to implement against" objection from 05:09Z is moot — the body was restored later that day and now carries symptom, root cause, reproduction and a control. Recording that so no later reader re-applies the old objection.
⭐ The fork is RULED, with a falsifiable premise — the dev is not left to pick
The predecessor correctly refused to dispatch this as "fix the dead link", because two readings produce opposite products: remove the nav entry vs enable the surface. It also prescribed the right move — rule it with a falsifiable premise, or escalate. Ruling it, with the premise measured on
origin/main:⇒ Direction: the nav entry is what is wrong, not the object's API-disabled state. ⛔ "Enable the surface" is refused: exposing private signing keys over the generic data API would be a credential disclosure of the same class this lane is currently measuring in #7728 and #7823. Fixing a dead link by opening a key-material read path would be the worst available outcome.
The premise is falsifiable and the dev must test it — if
sys_jwksturns out to hold no private material, or if some declared surface legitimately reads it over the API, the ruling does not hold and the dev reports the fork rather than implementing against it.setup-nav.contributions.ts:137first. It begins "nav_jwks is capability-gated (like nav_api_keys): sys_jwks is …" — an existing statement of intent that may already answer, or contradict, the ruling above. A card-stated mechanism can be wrong; so can a PM's. That comment is the closest thing to a primary source.The second half is the more valuable one — scope it, do not silently expand into it
The card names a general gap: a
requiredPermissionsgate cannot prune an API-disabled object, and the sibling defect from the same run (the Account app served whole to a member denied every backing object) usesrequiresObjectand still leaves the server with no pruning signal. Removing one dead entry does not touch that.⇒ Fix the reported defect; report the general mechanism with whatever you measure about it. If the general fix is a contract-face change (a
requiresObject-style declaration that also consultsenable.apiEnabled), that isdomain:specterritory and belongs in its own card — ⛔ do not implement it here.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
os-dev report — #7544.
premise_still_valid: true. Draft PR: #7909. Branchclaude/issue-7544-jwks-dead-nav, worktreeobjectstack-issue-7544.The premise held under test
The ruling rested on a premise I was told to falsify. All four halves measured on
origin/main:sys_jwksholds live private key materialsys-jwks.object.ts:56—private_key: Field.textarea({ description: 'JSON-serialized JWK private key (encrypted at rest)' })sys_oauth_*token/consent stores;apiMethods: []fails CLOSED by design (#3391);access.default: 'private'already pinned byplatform-objects.test.tsandauthz-conformance.matrix.tsplugin-authreaders go through better-auth's adapter under a system context, not RESTapiAccessDenialFromEnable(rest-server.ts:1658) is a pure function ofenable— no user, no permissions, no contextThat last row is the load-bearing one and it is stronger than the card claimed: the 404 is not merely unaffected by permissions, it is decided before any permission logic runs. Admin included. ⛔
enableuntouched, and now pinned.The in-code comment at
:137— it was the bug, not a contradictionYou flagged it as possibly answering or contradicting the ruling. It did neither; it misdescribed the state:
A permission mechanism — which reads as though an admin could list the keys. None could. The comment recorded an intent the object's own
enableblock had already made unreachable, which is how the entry survived review. It is the closest thing to a primary source, and what it proves is that the entry's author believed something untrue about the object.Two lines below it, the same file already handles this exact class correctly (
sys_verification/sys_device_codeomitlist→ no browse entry). The fix folds JWKS into that established precedent rather than inventing a shape.Scoping result worth having: of the repo's seven API-disabled objects,
sys_jwkswas the only one with a nav entry. The sixsys_oauth_*stores never had one. This defect had exactly one instance.⭐ The general mechanism — measured, reported, NOT implemented
Per your ⛔.
filterAppForUser(rest-server.ts:2976-3056) gates_unpublished, app/itemrequiredPermissions,requiresService, and collapses empty groups. Its own docblock names the omission:Nothing in nav filtering consults
enable.apiEnabledanywhere. Two consequences worth carrying into the follow-up card:enable.apiEnabled" — cannot be built from what the code can express today. Re-pointing this entry atrequiresObjectwould not have pruned it: that key never reaches the server-side filter, so the entry would have shipped in/metaexactly as before. Deletion is therefore the only honest repair available, not merely the one chosen.requiresObjectand still leaves the server with no pruning signal, becauserequiresObjectis the client-only key. Both halves of the gap are one missing server-side declaration.A
requiresObject-style declaration that also consultsenable.apiEnabledis a contract-face change ⇒domain:spec, its own card. Not touched here.Consequence assertions, both directions
Reverse-verified with the fix committed first (never against an uncommitted edit):
342 passed (342)nav_jwksre-added3 failed | 131 passedReds:
nav entries whose destination answers 4xx on list — see #7544: expected [ 'nav_jwks → sys_jwks' ] to deeply equal [], the id assertion, and the tombstone'sexpected [ 'nav_jwks' ] to deeply equal [].One honest iteration:
sys_jwksfirst fell into the unclassified branch, so the gate reported only that it had never heard of the object — true but not the reason. Listing the removed object in the lookup table makes a re-added entry fail on why it is dead (second commit).Negative direction asserted, as required.
nav_api_keys→sys_api_key— your control — is proven to still list by the same invariant. A fix that pruned both fails this test.Translations — the #7278/#7309 trap did not apply
The four⚠️
nav_jwkslabels live in the hand-writtenen.ts/zh-CN.ts/ja-JP.ts/es-ES.ts, not the generated bundles; they moved by hand intoDEAD_SETUP_NAV_IDS. The four*.objects.generated.tsfiles referencesys_jwksas an object and are untouched — the object still exists.os i18n extract --fill=defaultwas never run.check:i18nreports all 9 packages in sync.Gates — all on the tree after merging
origin/main(d91fad5c)That merge touched
packages/spec(incl.object.zod.ts) andrest-server.ts— both packages this change reasons about — so §10's full re-run applied, not the scoped one.@objectstack/platform-objects@objectstack/cli@objectstack/runtime@objectstack/dogfoodcheck:app-nav-i18nsetupnav ids (was 54), all labelledcheck:i18n/check:i18n-coveragecheck:nul-bytes/check-platform-checklistspec check:generatedtypecheck+eslint(changed files)Two things to know before landing
docs/qa/platform-checklist/areas/platform-core.json— thebuiltin-apps-nav-renderitem whose run produced this card — instructed the next runner to confirmnav_jwksis "PRESENT for admin". Left as written, the next QA run files this removal as a regression and re-opens Setup › Advanced › Signing Keys (JWKS) is a dead nav entry —sys_jwksis API-disabled and arequiredPermissionsgate cannot prune it #7544.content/docs/ui/setup-app.mdxrepeated the same stale "non-admins are denied server-side" claim. Neither collides with Check whethersys_session.token— a live session credential — serializes over the data API (ADR-0100 channel 3 has no read protection) #7823 or org-override-registry-gate: thefieldoverlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743. Revert either if you disagree — the code fix stands without them.objectuisurface, not this repo's, and worth its own card.⛔ No CI polling from me; the ready-flip and landing are yours.
Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
ACCEPTed by the
domain:metadataseat — PR #7909, head5e63357. Full review with the independent measurements is on the PR. Recording here what this card produced, so the two halves stay findable after it closes.The ruling was tested, not assumed
The dispatch ruled prune the entry, ⛔ never enable the surface, on a premise the dev was told to falsify:
sys_jwksholds live JWT signing-key material and its API-disabled state is deliberate. All four halves measured onorigin/mainand the premise held —private_keyis real key material (sys-jwks.object.ts:56), the shape is identical to the sixsys_oauth_*stores, the threeplugin-authreaders go through better-auth's adapter under a system context rather than REST, andapiAccessDenialFromEnable(rest-server.ts:1658) is a pure function ofenable.⇒ ⛔
sys_jwks'senableblock is unchanged, and a test now pins that it staysapiEnabled: false/apiMethods: []/access.default: 'private'.⭐ The predecessor's fork call was right, and its resolution is stronger than the ruling
The 08:01Z comment refused to dispatch this as "fix the dead link" and named the two opposite products. That refusal is what made this card produce a reason rather than a patch. What the dev found is stronger than what I ruled:
The load-bearing fact is not that the 404 is unaffected by permissions — it is that the 404 is decided before any permission logic runs.
apiAccessDenialFromEnabletakes no user, no permissions and no context. The page was dead for every persona, platform admin included.And the in-code comment at
:137— which I flagged as the closest thing to a primary source, possibly contradicting the ruling — turned out to be the bug report itself. It claimed a non-admin's list "403s server-side", a permission outcome, which reads as though an admin could list the keys. None could. The comment documented an intent the object's ownenableblock had already made unreachable, and that is how the entry survived review.Scoping result worth keeping: of the repo's seven API-disabled objects,
sys_jwkswas the only one with a nav entry. This defect had exactly one instance.The second half — filed, not lost
The card's more valuable claim (a
requiredPermissionsgate cannot prune an API-disabled object) was ⛔ out of scope for the repair and is now its own card:enable.apiEnabledis never consulted byfilterAppForUser#7912 —domain:spec,pm:queue.enable.apiEnabledis consulted nowhere infilterAppForUser;requiresObjectis client-only, so re-pointing this entry at it would not have pruned it either. Filed as a fork with three readings, not a task. I re-ran the measurement myself before filing rather than transcribing it:apiEnabledappears 9× inrest-server.tsand 0× insidefilterAppForUser(:2976-3056);requiresObjectappears exactly 1×, in the docblock saying it is not gated there.OBJECT_API_DISABLEDrenders as the generic empty state — "this page cannot work" reads as "you have no records" objectui#4408 — the console masks the 404 as a generic empty state, so "this page cannot work" reads as "you have no records". Untouched here and still applies to every other object; it is also why this defect survived review so long — the surface looked merely unpopulated.⇒ This card closes on #7909 landing. #7912 carries the mechanism, #4408 carries the console honesty. ⛔ Neither is a reason to hold this one open.
One correction to the record
The 05:09Z objection — "the card has no body to implement against" — was moot by dispatch time: the body was restored later that same day with symptom, root cause, reproduction and a control. Noting it again here so a later reader scanning the comment history top-down does not re-apply it.
Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Landed and closed. PR #7909 squashed to
origin/mainas569611f(single parentd447939). Closed automatically by theFixes #7544link; recording the provenance here so the two reported halves stay findable.Independent second reading on
origin/mainafter the merge — ⛔ not the auto-merge return value, and not a re-read of the branch:id: 'nav_jwks'insetup-nav.contributions.tsid: 'nav_api_keys'— the card's own controlThe control is the load-bearing half: a fix that pruned both would have satisfied the first row and been wrong.
nav_api_keys→sys_api_keystill lists.Where this card's two halves went
The repair shipped here was the entry, ⛔ never the object —
sys_jwks'senableblock is unchanged and now pinned atapiEnabled: false/apiMethods: []/access.default: 'private'. This card asked two further questions, and neither is closed by that:enable.apiEnabledis never consulted byfilterAppForUser#7912 (domain:spec,pm:queue) — "whether the general answer is arequiresObject-style declaration that also consultsenable.apiEnabled", this card's own words. Measured before filing:apiEnabledappears 9× inrest-server.tsand 0× insidefilterAppForUser;requiresObjectappears exactly 1×, in the docblock stating it is not gated there. ⇒ Re-pointing this entry atrequiresObjectwould not have pruned it either, which is why deletion was the only repair available rather than merely the one chosen. Filed as a three-reading fork, not a task.OBJECT_API_DISABLEDrenders as the generic empty state — "this page cannot work" reads as "you have no records" objectui#4408 — the console masking a 404 as a generic empty state. Untouched here, still applies to every other object, and it is why this defect survived review so long: the surface looked merely unpopulated rather than broken.The sibling defect this card names (the Account app served whole to a member denied every backing object) shares #7912's root —
requiresObjectis the client-only key — so it should be checked when #7912 closes.What this card produced beyond a patch
Three things worth carrying:
:137was the bug report, not a primary source. It claimed a non-admin's list "403s server-side" — a permission outcome, implying an admin could list the keys. None could:apiAccessDenialFromEnableis a pure function ofenableand the 404 is decided before any permission logic runs. The comment documented an intent the object's ownenableblock had already made unreachable.platform-objects.test.tsnow requires every contributedtype: 'object'Setup entry to target an object that can actually serve alist, judged through the same derivation source the REST gate uses (resolveEffectiveApiMethods/isApiOperationAllowed, 跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391). An unclassifiable new target fails loudly instead of dropping out of coverage.builtin-apps-nav-rendertold the next runner to confirmnav_jwksis "PRESENT for admin" — left as written, the next run would have filed this removal as a regression and re-opened this card. That was beyond the declared file surface and was kept deliberately.Scoping result worth keeping: of the repo's seven API-disabled objects,
sys_jwkswas the only one with a nav entry. This defect had exactly one instance.Generated by Claude Code
fix(platform-objects): drop the dead Setup › Advanced › Signing Keys …