Repository navigation
API key restore: the owner can restore a key that an administrator forced password reset revoked (the compromise default of #23287 ruling C′), though that ruling's changeset names the administrator as the one who restores #23993
Description
Filing gate ②: a security question one ruling leaves open, measured against what the runtime does. reach: every key revoked by POST …/admin/set-user-password with the default revokeSessionsAndKeys: true.
Filed by the domain:engine seat 2 (#20966) · os-tesla · session_01TQLy3Yq6vF4SDpTXdr3Uwi, from the #23944 dev's out_of_scope_findings (PR #23991). ⛔ Not a claim. Grade, lane and whether this needs the maintainer are triage's.
The standing record
- auth-gate parity: a request carrying an API key beside a session cookie held by ADR-0069 is refused at REST but admitted by the five raw-host route families — one cross-seam answer is owed (session versus user) #23287 ruling C′ item 2. As the code records it (
packages/plugins/plugin-auth/src/admin-user-endpoints.ts~:690): an administrator's forced password reset revokes the user's sessions and API keys by default, "because the act records no reason and the ruling sets the default for a suspected compromise — the case where a surviving key is the hole the reset was meant to close." - Its changeset (
.changeset/pre/23287-api-key-account-posture.md:36): "Revoked keys stay revoked after the password change; an administrator can restore a key a routine reset revoked with the Restore API Key action, or the owner mints a replacement."
What the runtime does (measured by the #23944 dev at 2054a0888)
restore_api_keyisPATCH /api/v1/data/sys_api_key/{id}with{ revoked: false }, the same door as revoke.- Under
member_default, the owner holdsupdateon their ownsys_api_keyrows (the RLS policysys_api_key_self,user_id == current_user.id).revokedis the one column that policy lets them write (ADR-0092 D2 whitelist). - So the owner can restore their own key whatever revoked it, including a forced reset.
sys_api_keyrecords no reason for a revocation, so the door cannot tell a routine reset from a compromise response.- The live CI dogfood
api-key-owner-revokedrives an owner restore of an owner-revoked key. No test drives an owner restore of a reset-revoked key; the evaluator reading of the permission sets says it is admitted.
The question
Is it intended that the owner can restore a key revoked by an administrator's forced reset? The default exists for the compromise case, where the key may have leaked. The changeset's sentence reads as if only an administrator restores and the owner re-issues.
If it is not intended, the fix needs the row to say why it was revoked: a revoked_reason or revoked_by, as sys_session.revoke_reason already has admin_password_reset (#23477). The owner's restore door then refuses that reason. That is a field and a permission change: Clause-②, and maybe a decision card.
Not changed by PR #23991: that PR rewords the API_KEY_REVOKED message to say what the runtime does today ("its owner or a platform administrator can restore it"). If this card restricts the owner, that sentence moves with it.
Dedupe
- MCP
search_issueson this repository, closed included:revoked API key from forced password reset revokeSessionsAndKeys can be restored by owner restore_api_key compromise→ API key refusal API_KEY_REVOKED says "a revoked key never works again", but the declared restore_api_key action brings the same key back to 200 #23944 (the message), platform-objects: sys_session.revoke_reason's description is the field's published vocabulary and lacks admin_password_reset, the value PR #23440's forced-reset revocation writes #23477 (closed;sys_session.revoke_reasonvocabulary), api-key-ui-lifecycle: after #7727 an ordinary member still cannot revoke their OWN API key — 403 PERMISSION_DENIED, and the key keeps authenticating #8053 / api-key-ui-lifecycle: API keys cannot be revoked through any product route (405) — object declares revoke/restore as PATCH but disables the PATCH method #7727 (closed). None asks this. - Dedupe words: owner restore reset-revoked api key; revokeSessionsAndKeys restore; api key revoke reason.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
security·bug·priority:p1·target:v18·domain:engine·area:access·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T17:53ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
.changeset/pre/23287-api-key-account-posture.md:36) names who acts after a forced reset: "an administrator can restore a key a routine reset revoked …, or the owner mints a replacement". An owner restore of a reset-revoked key is therefore not the ruled behaviour. The forced reset's default exists for the compromise case, where the surviving key is the hole.security+target:v18: the measured door reopens a key that a compromise response closed. Not P0: the reset also revokes the user's sessions, and only the legitimate owner's later session can restore.domain:engine:sys_api_key(packages/platform-objects) and the key door (packages/core/src/security/api-key.ts), the lane of API key refusal API_KEY_REVOKED says "a revoked key never works again", but the declared restore_api_key action brings the same key back to 200 #23944.plugin-auth's reset endpoint, which would stamp the reason, is declared on the claim.sys_session.revoke_reason'sadmin_password_reset(platform-objects: sys_session.revoke_reason's description is the field's published vocabulary and lacks admin_password_reset, the value PR #23440's forced-reset revocation writes #23477). The owner's restore refuses that reason; an administrator's restore stays. That is a field plus a permission change, so the claim declaresClause-②. If the claimant finds the ruling text does not settle it, returnneeds_decision.API_KEY_REVOKEDto the current behaviour. That sentence moves with this card.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 1 · 2026-10-16T18:51Z · on triage's first grade 6221482817 (the #23287 ruling C′ changeset text settles who restores a reset-revoked key)
Session:
session_01TQLy3Yq6vF4SDpTXdr3UwiAccount:
os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23993-reset-revoked-key-restoreWorktree:
objectstack-issue-23993Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
packages/platform-objects/src/identity/sys-api-key.object.ts: a revocation-reason field, and the guard that refuses the OWNER's restore of a key a forced reset revoked while an administrator's restore stays;enregenerated, translated leaves hand-written per AGENTS.md);packages/core/src/security/api-key.ts: only theAPI_KEY_REVOKEDmessage, so it names who can restore a key revoked for that reason;packages/plugins/plugin-auth/src/admin-user-endpoints.ts: only the forced reset's key revocation, which stamps the reason (declared todomain:services);content/docs/api/error-catalog.mdx: only theAPI_KEY_REVOKEDentry, if the message changes (declared todomain:devx);.changeset/23993-reset-revoked-key-restore.md.⛔ No
packages/spec/**and no new permission set: the guard rides the object's own declared metadata and the existing ADR-0092 D2 owner whitelist. Stop on a breach and explain it in the report.Container & model:
M,mode:subagent,model: default(dispatch-gates --tieratfd9664ffa: no path-derived mandate). A security card. It owes one review atCONTRACT_REVIEW_TIERbefore the queue, for the narrowing face.Clause-②: yes (narrowing)
Responsibility:
sys_api_key(this repository's code) lets the owner clearrevokedon any of their keys, whatever revoked it | #23287 ruling C′: the forced reset revokes keys by default for a suspected compromise, and its changeset names the administrator as the restorer and re-issue as the owner's road | every key a forced reset revoked: the owner, whose account may be the compromised one, can reopen the key the reset closedThread-read: 6221482817
Serial constraints cleared:
sys-api-key.object.ts,core/src/security/api-key.ts,plugin-auth/src/admin-user-endpoints.ts,error-catalog.mdxor theplatform-objectstranslation bundles.4ffdebe18) reworded theAPI_KEY_REVOKEDmessage, and that sentence moves with this card. PR feat(platform-objects,service-messaging): Setup lists show their own empty state; API Keys gains Create Key #23924 (platform-objects: Setup's Invitations, API Keys, OAuth Applications and three notification objects declare no viewemptyState, so their lists show generic fallbacks; declare each (and API Keys' Create key action), moved from objectui#12607 #23852, this seat's,8f4260fe6) addedsys_api_key's Create Key action and empty states.Read 2026-10-16T18:51Z.
On
Clause-②: yes (narrowing): the row gains a field (a widening of the published object), and the owner's restore of a reset-revoked key is refused (a narrowing of the object write door's accept set). Both are read, perclause2-line.mjs's arm grammar.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23993,
"status": "blocked",
"branch": "claude/issue-23993-reset-revoked-key-restore",
"pr": null,
"session": "session_01TQLy3Yq6vF4SDpTXdr3Uwi — subagent run; the parent PM's session id (claim 6222506660)",
"premise_still_valid": true,
"summary": "Section 1 stop: the guard cannot be built inside the claimed file surface. Nothing in sys_api_key's declared metadata can tell a platform administrator from the owner: object-level validations bind record and previous only, so every declarative spelling either cannot be evaluated or refuses the administrator too (measured, see tests). And the ADR-0092 D2 guard clamps the administrator as it clamps the owner, so no data-API write can clear a reason column. The seam that can do it is a beforeUpdate hook on sys_api_key in plugin-auth, beside the D2 guard, asking hasPlatformAdminStanding. The ruling text settles the behaviour (triage's reading holds), so this is blocked on the surface, not needs_decision. Branch pushed empty at fd9664f (the claim's landing marker); no commit, no PR, no code change.",
"readings": {
"a1_whitelist": "HOLDS, and it clamps the administrator too. Registered in plugin-auth auth-plugin.ts:1549-1558 (kernel:ready) from MANAGED_EXTENSION_EDITABLE_FIELDS (managed-extension-fields.ts:197, sys_api_key = {revoked}). Enforced by identity-write-guard.ts guardUpdate (:185, beforeUpdate priority 10, :273) on every write where isUserContextWrite (:98) holds, i.e. session.userId set and not isSystem. There is no administrator carve-out, so a platform administrator's data-API write is clamped exactly like the owner's. A revoked_reason column is unwritable through the data door by both: stripped when sent beside revoked, 403 when sent alone. Declared readonly (ADR-0092 D4), the engine's static readonly strip removes it even earlier. Only a system-context writer or a hook can set or clear it, so the administrator's Restore API Key action cannot clear the reason, and the one place that keeps it in step with revoked has to be a hook. Also: MANAGED_EXTENSION_FIELDS.sys_api_key (managed-extension-fields.ts:103) declares EVERY column of this object, so a new column belongs there as well, which is outside the surface too.",
"a2_guard": "FALSIFIED inside the surface. Object-level validations (script / cross_field / conditional) bind record and previous only: checkPredicate evaluates ruleScope(record, previous) (rule-validator.ts:4402; ruleScope :3309-3316). opts.currentUser is read at exactly one site, the per-option visibleWhen gate (:3486), and the #18783 permissions binding (current_user.can) is read only there too. revoked is a boolean with no options; field readonlyWhen binds no caller and strips instead of refusing; access.update is a per-verb capability list. So no key in sys-api-key.object.ts can tell an administrator from the owner. The seat that can: a beforeUpdate hook on sys_api_key. HookContext carries session.userId, session.isSystem and previous (the pre-image), but no permissions (engine.ts:6300 buildSession). It asks hasPlatformAdminStanding(engine, session.userId) (@objectstack/core resolve-authz-context.ts:1706): the ADR-0068 D2 PLATFORM_ADMIN rung, which is an unscoped admin_full_access grant held now, or the verified OS_PLATFORM_OWNER_EMAIL anchor. It is fail-closed. Never positions[] (platform-admin-gate.ts, #15136). The #23944 reading is consistent: admin_full_access (wildcard, modifyAllRecords) is the only grant that updates other users' rows; member_default and organization_admin reach their own rows through sys_api_key_self (plugin-security default-permission-sets.ts:552). Refusal code: PERMISSION_DENIED 403, already in the ledger and already what this table answers. A new code would need packages/spec api/error-code-ledger.zod.ts, which the claim excludes.",
"a3_writers_census": "Writers of sys_api_key.revoked at fd9664f, in packages/ and examples/: (1) the forced reset, forced-reset-revocation.ts:157 revokeSessionsAndApiKeys, system context, writes {id, revoked: true}. admin-user-endpoints.ts:744 only calls it, so the stamp's measured producer is forced-reset-revocation.ts, not the claimed admin-user-endpoints.ts. (2) the revoke_api_key row action and any user-context PATCH or import update with revoked: true (the owner through member_default + sys_api_key_self, an administrator through admin_full_access), all on the same engine path and hooks. (3) restore_api_key and any user-context revoked: false. (4) the mint, runtime/src/domains/keys.ts:297, a system insert with revoked: false and no reason, which is truthful. (5) core/src/security/api-key.ts:438, a system stamp of last_used_at / expires_at only, which never touches revoked. No other writer. The tracking hook needs three rules: stamp the reason only on the forced reset; clear it on every un-revoke the guard admits; and never clear it when an already-revoked row is revoked again. Otherwise the owner launders the reason with {revoked: true} and then restores.",
"a4_preexisting_rows": "The reason-less forced-reset revocation landed in 0f05907 (2026-10-14, PR #23440). It is published only in npm @objectstack/plugin-auth@18.0.0-next.2 (dist-tag next, published 2026-10-16 03:04 UTC). latest is 17.7.0 (published 2026-10-06), which does not carry it. So the population is keys revoked by a forced reset on a deployment running 18.0.0-next.2, or a main build since 2026-10-14: prerelease-only and under three days old. Those rows stay owner-restorable after the fix. No backfill is proposed: the reset's own audit row (user.admin_password_set) carries counts only (revokedApiKeys), never key ids, so it cannot tell a reason-less revoked row from an owner revoke. Not raised as a decision.",
"a5_message": "HOLDS. lifecycleRefusalOf (api-key.ts:299) reads the row from ql.find('sys_api_key', { where: { key }, limit: 1 }) (:687) with no field projection, so a revoked_reason column arrives on the row with no other change. The message can branch on it with code, status, order and carried fields unchanged. Not done (stop).",
"seam_needed": "The amended surface for the PM (all plugin-auth, domain:services), in addition to the claimed files: (a) packages/plugins/plugin-auth/src/forced-reset-revocation.ts, the measured stamp producer; it replaces admin-user-endpoints.ts in the surface, which needs no change. (b) A new packages/plugins/plugin-auth/src/api-key-restore-guard.ts plus its test: ONE beforeUpdate hook on sys_api_key, at a priority after the D2 guard (10), that refuses a user-context revoked: false on a row whose previous.revoked_reason is admin_password_reset unless hasPlatformAdminStanding. It answers 403 PERMISSION_DENIED with a message naming the reason and the road (ask a platform administrator to restore it, or create a new key). The same hook keeps revoked_reason in step under the three census rules. (c) packages/plugins/plugin-auth/src/auth-plugin.ts: register it in kernel:ready beside registerIdentityWriteGuard (:1573). (d) packages/plugins/plugin-auth/src/managed-extension-fields.ts: declare revoked_reason in MANAGED_EXTENSION_FIELDS.sys_api_key, and NOT in the editable map. Still to measure in that round: a beforeUpdate hook's write of a readonly column on a user-context update persists (the engine tracks hook-written keys apart from caller keys), and per-row previous on a multi-row update. Gate delta for (a)-(d) against the PM's dispatch list: check:auth-mount-ledger, check:durability-log-level and check:route-envelope (dispatch-gates --commands with explicit paths, 52 commands). --tier: no path-derived mandate."
},
"tests": "No code change, so no suite was owed and no pin was written. Red/green legs per pin: NOT MEASURED, reason: Section 1 stop before any implementation. Falsification probe for assumption 2: a throwaway vitest file in packages/objectql/src/validation, run once and deleted, never committed (git status --porcelain empty afterwards, HEAD fd9664f). It ran after
pnpm turbo run build --filter=@objectstack/objectql^... --concurrency=2(lock VERDICT command-exit 0, 13 tasks successful). Command: pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/validation/zz-probe-23993.test.ts (lock VERDICT command-exit 0, 1 file, 1 test). It calls evaluateValidationRules on update with previous {revoked: true, revoked_reason: admin_password_reset} and payload {revoked: false}. Readings: (1) a rule naming current_user, ADMIN restores: REFUSED 400 VALIDATION_FAILED rule_violation, constraint reason unevaluable, fault 'Unknown variable: current_user'. (2) The same rule, OWNER restores: the same refusal. (3) A caller-blind rule (previous.revoked_reason == admin_password_reset and record.revoked == false), ADMIN restores: REFUSED 400 VALIDATION_FAILED rule_violation. (4) CONTROL: the caller-blind rule, OWNER restores a plain-revoked key (reason null): ADMITTED.","gates": "Not applicable: no diff.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon this worktree exited 2 and refused, with 'this branch changes nothing against origin/main (merge base fd9664f) — nothing to derive'. --ran reconciliation: not applicable. No PR, so there are no check-runs to cite. Hypothetical-surface reading for the seam paths: listed under readings.seam_needed.","mcp_calls": "0 — none",
"api_writes": "1 — the os-dev-report comment on #23993 (POST /repos//issues/23993/comments) through scripts/pm/post-stamped.mjs on its auto route. Plus one git push of the empty branch (the write-route probe), which is not a REST write. No pr_create, no label write.",
"open_questions": [
{
"question": "Where does the owner-vs-administrator restore guard live, given that sys_api_key's declared metadata cannot bind the caller (readings.a2_guard)? Answering it amends the claim's file surface.",
"options": [
"A: a beforeUpdate hook on sys_api_key in plugin-auth, beside the ADR-0092 D2 guard, using hasPlatformAdminStanding. It is the one place that refuses the owner's restore of a reset-revoked key (403 PERMISSION_DENIED, naming the reason and the road) and keeps revoked_reason in step. Surface: readings.seam_needed (a)-(d). Business need (measured): the administrator keeps the road the ruling's changeset names, the Restore API Key action on the data door, and the one stamp producer is the forced reset. Long-term: the caller-aware rule sits where the caller and the pre-image both exist, next to the module that already owns which sys_api_key columns a caller may write; one hook, no second door. AI error-proofing: a loud, located refusal; the field stays readonly and unwritable through the data API; no consumer tolerance. Startup scope: no new route, permission set, role or error code; one hook module. Cost: about 4 plugin-auth files beyond the claim; S-M.",
"B: a caller-blind invariant in sys_api_key's validations (an un-revoked key may not carry admin_password_reset), plus a new platform-admin-only, system-context restore route that clears the reason. Business need: there is no measured caller for a second restore door; the ruling names the existing action. Long-term: two doors for one act. AI error-proofing: the invariant is visible in metadata (good), but the Restore API Key action then refuses the administrator on reset-revoked keys unless the action is split per row. Startup scope: a new public route, with its closed query-parameter set, docs and pins. Cost: M-L.",
"C: split sys_api_key_self (plugin-security default-permission-sets.ts:552) into select and update policies, with update requiring revoked_reason != admin_password_reset. Business need: covers the owner, but it still needs the A-style hook to clear the reason on an administrator restore. Without the hook, a restored key would be hidden from its owner's own revoke. Long-term: it edits a default permission set shipped to every deployment. AI error-proofing: an RLS refusal says neither the reason nor the road. Startup scope: widest blast radius. Cost: M."
],
"recommendation": "A, on all four axes. It keeps the administrator's existing road and the ruled behaviour, the refusal is loud and located, it adds no route, permission set or code, and it is the single place where the reason is kept in step. The decision is the PM's (a surface amendment plus a domain:services declaration for the plugin-auth files), not the maintainer's: the ruling text already settles who restores."
}
],
"out_of_scope_findings": []
}
objectstack-fleet commentedon Oct 16, 2026
Claim amendment to 6222506660 ·
domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T19:19Z. ⛔ Not a new claim; same session, same branchclaude/issue-23993-reset-revoked-key-restore. This answers the dev'sblockedreport (its open question, A).Clause-②: yes (narrowing)
sys_api_key's declared metadata cannot tell an administrator from the owner. Object-level validations bindrecordandpreviousonly, and a probe measured that a caller-naming rule faults while a caller-blind rule refuses the administrator too.beforeUpdatehook onsys_api_keyinplugin-auth, beside the D2 guard, askinghasPlatformAdminStanding(the ADR-0068 D2 platform-admin rung, fail-closed).domain:services, declared in this act):packages/plugins/plugin-auth/src/forced-reset-revocation.ts: the measured producer of the forced reset'srevoked: true; it stamps the reason. It replacesadmin-user-endpoints.ts, which needs no change.packages/plugins/plugin-auth/src/api-key-restore-guard.tsand its test: ONE hook. It refuses a user-contextrevoked: falseon a row whoseprevious.revoked_reasonisadmin_password_reset, unless the caller has platform-admin standing, with403 PERMISSION_DENIEDnaming the reason and the road. It also keepsrevoked_reasonin step: stamped only by the forced reset, cleared on every admitted un-revoke, and never cleared when an already-revoked row is revoked again.packages/plugins/plugin-auth/src/auth-plugin.ts: only its registration inkernel:ready, besideregisterIdentityWriteGuard.packages/plugins/plugin-auth/src/managed-extension-fields.ts:revoked_reasoninMANAGED_EXTENSION_FIELDS.sys_api_key, and ⛔ NOT in the editable map.sys-api-key.object.tsfield (declaredreadonly, ADR-0092 D4), theplatform-objectstranslations, thecoreAPI_KEY_REVOKEDmessage, the error-catalog line, tests and one changeset.admin-user-endpoints.ts.@objectstack/plugin-auth@18.0.0-next.2, andmainbuilds since 2026-10-14). No backfill: the reset's audit row records counts, not key ids.packages/spec/**, no new route, permission set, role or error code.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23993,
"status": "done",
"branch": "claude/issue-23993-reset-revoked-key-restore",
"pr": "#24044",
"session": "session_01TQLy3Yq6vF4SDpTXdr3Uwi — subagent run; the parent PM's session id (claim 6222506660, amendment 6222961246)",
"premise_still_valid": true,
"summary": "Built route A on the amended surface. sys_api_key gains a readonly select revoked_reason (one option, admin_password_reset). The forced reset's revocation (forced-reset-revocation.ts) stamps it beside revoked: true. A new plugin-auth hook, api-key-restore-guard.ts, is ONE beforeUpdate hook on sys_api_key at priority 15, after D2 (10), registered in auth-plugin.ts kernel:ready beside registerIdentityWriteGuard. It refuses a user-context un-revoke of a reset-revoked row with 403 PERMISSION_DENIED (naming the platform administrator and POST /api/v1/keys) unless hasPlatformAdminStanding; it clears the reason on every un-revoke it admits; and it never touches the reason on a revoke. The core API_KEY_REVOKED message branches on the reason, with code, status and fields unchanged, and the error catalog matches. Draft PR #24044 is at f782520, assignee os-tesla; CI is in progress. One deviation: a row on content/docs/permissions/system-context.mdx that the census gate requires (see deviations).",
"readings": {
"m1_hook_write_of_readonly_column": "PERSISTS. Throwaway probe on a real ObjectQL over SQLite, user context ({userId}, no isSystem), hook at priority 15, never committed. By id, the hook wrote 'a' to a readonly select and 'a' persisted. The caller forged 'a' and the hook wrote null: null persisted and the forgery was not handed back. The engine credits hook-written keys past the post-hook static strip (hookWrittenKeys). Confirmed again on the real chain by pin 2 (the administrator's restore clears the reason) and by the system-context pin.",
"m2_multi_row_previous": "PER ROW. In the same probe, a multi:true user-context update matched two rows stored tag b and a. The per-row previous the hook saw was r1=b and r2=a, and the hook's in-place null landed on both rows. On the real chain: an owner batch over {reset-revoked, plain} is refused whole and neither row moves; an owner batch of plain keys is admitted; an administrator batch clears the reason on every row. The clear is keyed on the payload alone (an un-revoke), so the written key set is row-invariant and the #14099 divergence refusal cannot fire.",
"hook_rules_vs_census": "Rule 1, refuse: pin 1, the re-revoke-then-restore half of pin 4, and the multi owner batch. Rule 2, stamp only on the reset: pin 5; a key revoked before the reset keeps null. Rule 3, clear on every admitted un-revoke: pin 2, the multi administrator batch, and the system un-revoke. Rule 4, never clear on a re-revoke: pin 4, including a forged revoked_reason: null, which the readonly strip withholds. Rule 5, system context never refused: the system-context pin (an un-revoke lands and clears; a last_used_at stamp leaves the reason). The mint is an insert, which this hook never sees, and the forced reset is a revoke.",
"admin_vs_owner": "hasPlatformAdminStanding(engine, session.userId), from @objectstack/core: the ADR-0068 D2 PLATFORM_ADMIN rung, which fails closed. The test administrator holds the unscoped admin_full_access grant that bootstrapPlatformAdmin writes, and resolves as a transport would ({userId, permissions: ['admin_full_access']}). The owner resolves member_default and its sys_api_key_self row scope through the real SecurityPlugin."
},
"tests": "Red legs: node scripts/ablation-replace.mjs in wrap mode against the committed fix 39730a7, every leg run under os-verify-lock (VERDICT command-exit 0), with an outer trap restoring from HEAD by absolute path. Each leg's anchor went 1 -> 0, its blob changed, and the restore was proven blob == HEAD with git diff HEAD empty. L1 dropped
throw restoreRefused(): 3 failed of 11 (pin 1, pin 4, the multi owner batch). L2 dropped the clear: 3 of 11 (pin 2, the multi administrator batch, the system un-revoke). L3 dropped the stamp: 7 of 68 across api-key-restore-guard.test.ts and admin-user-endpoints.test.ts, including pin 5 in both files. L4 forced the core message branch to false: 1 of 22 (pin 6, core). L5 removed the auth-plugin registration: 1 of 11 run with -t kernel:ready (the registration pin). The CONTROL (pin 3) stayed green on every leg. Full suites: plugin-auth in two shards, 76 + 75 files, 1753 + 1202 passed, 10 skipped; platform-objects 73 files, 1136 passed (sys-api-key-single-declaration's column set gained revoked_reason); core api-key-lifecycle / api-key / api-key-account-posture 61 passed. Re-run on the merged head 17ca3f0: 125 + 61 + 144 passed. Typecheck of core, platform-objects and plugin-auth: exit 0, and check:test-typecheck OK on each (the plugin-auth ledger is unchanged at 10 files / 88 errors / 23 signatures). Probes for m1 and m2 were throwaway and never committed. test:repo was not run locally; it is CI's.","gates": "dispatch-gates --commands --repo objectstack-ai/objectstack (no paths) at final head f782520 derived 102 commands. Each was run with its exit captured before any pipe. --ran: 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN, and every exit was 0. That includes check:auth-mount-ledger, check:durability-log-level, check:route-envelope, check:i18n (10 packages in sync), check:i18n-stale-fill, check:system-context-census, check:changeset-no-major, check:adr-0087-registration, check:empty-changeset, check:merge-driver, check:dual-build-cjs-loads and spec check:skill-examples. The last two exited 3 (prerequisite not met) on an earlier pass and 0 after the CLI and client closures were built. The four roster families marked under these paths and the ten declared wide-population families were also run locally, all exit 0: check-changeset-fixed, check:authz-resolver, error-code-casing, filter-alias-parity, init-service-contract, live-db-isolation, meta-type-normalized, optional-error-sink, resume-authority-declared, runner-env-posture, settings-bind-window, startup-registry-verdict, verify-stand-in, wildcard-fallthrough. PR #24044 check-runs at report time: 22 in_progress, 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped. The required contexts Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core and Governed Surface Queue Guard were not yet concluded. NOT MEASURED: CI's verdicts (reason: in_progress at report time; CI convergence is the PM's).",
"deviations": [
"content/docs/permissions/system-context.mdx: one row (26b), outside the amended surface. check:system-context-census failed on the new hook: [file-without-a-row] api-key-restore-guard.ts holds 1 elevation read site (isUserContextWrite). Rule 5 needs the isSystem read, and the page holds every such site, so without the row the required Lint & Repo Gates goes red. The census was green after the row. It needs the seat's declaration to domain:devx, as error-catalog.mdx had.",
"The stamp's producer is forced-reset-revocation.ts, as the amendment states. admin-user-endpoints.test.ts (a test, inside 'tests') changed only its DEFAULT ON expectation, which is pin 5's unit leg.",
"origin/main was merged once before the PR opened (17ca3f0, through 2b27460, no overlap with these paths). The closure was rebuilt and the targeted suites were re-run on the merged head.",
"Gate record: a first gate run, at 17ca3f0, was still finishing when the final run at f782520 began, and its last 4 lines (type-check-debt, vendor-version-stamps, watch-hint-literal, where-matcher, all exit 0) landed as lines 2-5 of the final record. Those 4 lines were removed by position before --ran. The cleaned record equals the derived list in order and set, and the final run ran each of those 4 itself (exit 0)."
],
"mcp_calls": "0 — none",
"api_writes": "4 across both rounds, all through the fleet relay as objectstack-fleet[bot] (scripts/pm/). Round 1: POST /repos//issues/23993/comments (the blocked report, 6222928785). Round 2: pr_create, which is two requests, POST /repos/objectstack-ai/objectstack/pulls (draft #24044) and POST /repos//issues/24044/assignees (os-tesla); and POST /repos//issues/23993/comments (this report). Also several git pushes of the branch, which are not REST writes. No label writes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed (PR Acceptance notes): a key the owner revoked BEFORE a forced reset is not stamped (the reset scans revoked: false), so its owner can still restore it after the reset. The reason stays truthful, and whether the reset should stamp already-revoked keys is not decided here.",
"carrier: none · noted, not filed (PR Acceptance notes): the Restore API Key action still renders for the owner on a reset-revoked key and the server refuses it with the road; a visible predicate would be UI courtesy only."
]
}
objectstack-fleet commentedon Oct 16, 2026
Claim amendment 2 to 6222506660 ·
domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T20:57Z. ⛔ Not a new claim; same branch, PR #24044.content/docs/permissions/system-context.mdx, one row (26b) only, declared todomain:devx. The newapi-key-restore-guard.tsreadsisSystemonce, throughisUserContextWrite, so that a system-context write is never refused.check:system-context-censusholds every elevation read site on that page, and it fails[file-without-a-row]without the row.Clause-②: yes (narrowing)stands. ACONTRACT_REVIEW_TIERreview follows before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #24044 comment 6225034761 ·
domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T21:28Z. TheCONTRACT_REVIEW_TIERrecord 6224996826 (PASS) is the review of the narrowing face. Landing next: ready, then auto-merge through the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landing record: PR #24044 →
e246f06119·domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T22:01Zmerged: true, merge commite246f06119, through the merge queueorigin/maine246f06119isorigin/main's tip.plugin-authcarriesapi-key-restore-guard.ts(API_KEY_RESTORE_GUARD_PRIORITY), andauth-plugin.tsregisters it.sys_api_keydeclaresrevoked_reason..changeset/23993-reset-revoked-key-restore.mdis present (minorforplatform-objects,plugin-authandcore,Clause-②: yes (narrowing))gh-readonly-queue/main/*pr-24044*head remainsState change: this card closed
completedthroughFixes #23993;pm:dispatchedremoved in this act. No other card closed with it (the body carries no other closing verb).Carried: the six out-of-scope notes in the ACCEPT 6225034761 stay noted, with no carrier. The ADR-0092 D2 prose drift (import carries the caller's context) goes to the maintainer in the round report.
Generated by Claude Code