Cleans up operator key management - #59
Conversation
atc964
left a comment
There was a problem hiding this comment.
Verified all three fixes, including reproducing the hang mechanism empirically: aiosqlite's connection is a non-daemon thread, so an unclosed storage after asyncio.run blocks interpreter shutdown — the close_storage-in-finally is the root-cause fix and routing the console script through force_exit_after is sound insurance (rich flushes per print, so the one-time key survives the hard exit even piped). Duplicate-label 409 confirmed at service, HTTP, and CLI layers with an end-to-end smoke on the installed entrypoint; no weakening of the operator-auth surface (deps untouched, no new routes; the 409's key_id disclosure is operator-visible anyway). Suite 1421/0 here.
Three take-or-leave observations: (1) label uniqueness is check-then-mint, so two concurrent operator POSTs with the same label can both land — low stakes here, maybe worth a docstring line; (2) the blanket ValueError->409 in the router will silently 409 any future ValueError from that path — a dedicated exception would be tighter; (3) behavior note: a second unlabeled operator-key request now 409s on the empty/default label where it used to succeed — reads as intended, but flagging since it changes bootstrap scripts that mint twice.
Editable installs need pip install -e . re-run for the entrypoint change, as your body notes. Good cleanup.
Improve operator key CLI management
Problem
ad-seller create-operator-keyhad two rough edges after the operator-auth work landed:asyncio.run(...), and the console-script entrypoint (ad-seller = ...:app) skipped the hardos._exitthat__main__used to bypass telemetry atexit hangs.Operators also had no out-of-band way to list or revoke operator keys without going through the HTTP API (which itself requires an operator credential).
What this PR does
Duplicate active labels
ApiKeyService.create_operator_keyrejects an active (non-revoked, non-expired) operator key with the same label (ValueError, includes existingkey_id).POST /auth/api-keys/operatormaps that to 409 Conflict.CLI exits cleanly
create-operator-key(and the new commands) alwaysclose_storage()in afinally.ad_seller.interfaces.cli.main:main, wrapped withforce_exit_aftersoad-sellerhard-exits like__main__.Bootstrap CLI surface
ad-seller create-operator-keyad-seller list-operator-keys [--include-inactive]ad-seller delete-operator-key --label …/--key-id …delete-operator-keyrefuses buyer keys (useDELETE /auth/api-keys/{key_id}for those).Docs
Updated bootstrap sections in
docs/api/authentication.md,docs/guides/agent-management.md, anddocs/guides/developer-setup.md.Test plan
ad-seller create-operator-key --label "Ops"twice → second fails with clear error; process exitsad-seller list-operator-keysshows the active key;--include-inactiveshows revoked tooad-seller delete-operator-key --label "Ops"then recreate with the same label succeedsPOST /auth/api-keys/operatorwith a duplicate active label returns 409pip install -e .pytest tests/unit/test_operator_auth.py