gabriel / musehub public
feat BREAKING feat/223-key-status-revocation #1 / 1
AI Agent gabriel · 2 days ago · Oct 22, 2026 · Diff

feat: GET /api/keys/{key_id}/status + key revocation soft-delete (musehub#223)

Audit: muse verify-commit --check-key-status always reported "unknown" because the endpoint didn't exist (404). Investigation found a deeper gap: revoke_key() hard-deleted the key row, so even with the endpoint built there would be no way to distinguish "revoked" from "never registered."

Design decision (confirmed via AskUserQuestion): add revoked_at soft-delete rather than scoping down to active/unknown-only. Fully satisfies the ticket's acceptance criteria at the cost of auditing every signature- verification query site.

Implementation: - New alembic migration 0083: revoked_at column on musehub_auth_keys, nullable, no backfill needed (NULL = active for all existing rows). - revoke_key() now sets revoked_at instead of deleting the row. - Added revoked_at IS NULL filters everywhere a key is looked up for authentication/signature verification, so a revoked key can never authenticate again (same practical behavior as hard-delete, now auditable): request_signing.py's MSign core auth path, verify_and_authenticate's login/registration fingerprint lookup and dead-identity-recovery existing_keys check, register_agent_identity's fingerprint lookup, /api/auth/challenge's is_new_key check (must agree with verify's fingerprint lookup or a revoked key's owner couldn't re-register), get_keys_for_identity (list_keys), and repair-identity-repo's "most recent key" lookup. - New GET /api/keys/{key_id}/status (public, no auth -- status is non-sensitive and callers verifying a commit may not know the signer's handle): returns {"status": "active"|"revoked"}, 404 for unregistered.

TDD: wrote tests/test_key_status.py first (6 tests: active, revoked, unknown key_id, soft-delete not hard-delete, revoked key cannot authenticate, list_keys excludes revoked). Confirmed genuine RED via muse-dev shelf save/pop isolation across all 6 touched files.

Regression: 214 passed across key-status/auth/identity/authz test files -- no regressions in any authentication path this change touches.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:0ce9daa6cc1a86e370a7c023184881065b392d52dd276b224582d7b936d9644a sha
+41 ~9 symbols
8 changed · 1107 in snapshot files
sha256:487f524622afc477dee06fee3e1ce221b26492ac7a57e054e5fc135beb5277f4 snapshot
+41
symbols added
~9
symbols modified
8
files changed
1107
files in snapshot
0
dead code introduced
Semantic Changes 50 symbols
+ branch_labels variable variable branch_labels L23–23
+ depends_on variable variable depends_on L24–24
+ down_revision variable variable down_revision L22–22
+ downgrade function function downgrade L34–35
+ Sequence import import Sequence L14–14
+ Union import import Union L14–14
+ annotations import import annotations L12–12
+ op import import op L16–16
+ sa import import sa L17–17
+ revision variable variable revision L21–21
+ upgrade function function upgrade L27–31
~ tests/test_key_status.py .py 29 symbols added
+ _COUNTER variable variable _COUNTER L40–40
+ _NOW variable variable _NOW L39–39
+ _key_for function function _key_for L48–50
+ _seed_identity_with_key function async_function _seed_identity_with_key L53–84
+ _uid function function _uid L43–45
+ AsyncClient import import AsyncClient L26–26
+ AsyncSession import import AsyncSession L28–28
+ MusehubAuthKey import import MusehubAuthKey L33–33
+ MusehubIdentity import import MusehubIdentity L34–34
+ _hashlib import import _hashlib L22–22
+ annotations import import annotations L19–19
+ base64 import import base64 L21–21
+ compute_identity_id import import compute_identity_id L31–31
+ compute_key_id import import compute_key_id L31–31
+ datetime import import datetime L23–23
+ encode_pubkey import import encode_pubkey L30–30
+ get_keys_for_identity import import get_keys_for_identity L35–35
+ key_fingerprint import import key_fingerprint L32–32
+ pytest import import pytest L25–25
+ revoke_key import import revoke_key L35–35
+ select import import select L27–27
+ timezone import import timezone L23–23
+ pytestmark variable variable pytestmark L37–37
+ test_ks01_active_key_status function async_function test_ks01_active_key_status L90–97
+ test_ks02_revoked_key_status function async_function test_ks02_revoked_key_status L103–114
+ test_ks03_unknown_key_id_returns_404 function async_function test_ks03_unknown_key_id_returns_404 L120–122
+ test_ks04_revoke_key_soft_deletes function async_function test_ks04_revoke_key_soft_deletes L128–141
+ test_ks05_revoked_key_cannot_authenticate function async_function test_ks05_revoked_key_cannot_authenticate L147–170
+ test_ks06_get_keys_for_identity_excludes_revoked function async_function test_ks06_get_keys_for_identity_excludes_revoked L176–190
~ musehub/api/routes/api/auth.py .py 1 symbol added, 1 symbol modified
+ key_status function async_function key_status L303–328
Files Changed
+2 ~6
1107 in snapshot
← Older Oldest on feat/223-key-status-revocation
All commits
Newer → Latest on feat/223-key-status-revocation

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:0ce9daa6cc1a86e370a7c023184881065b392d52dd276b224582d7b936d9644a --body "your comment"