Skip to content

Require operator credentials on the admin surface (REST + MCP) - #39

Open
therevoltingx wants to merge 2 commits into
mainfrom
fix/enforces-operator-key
Open

Require operator credentials on the admin surface (REST + MCP)#39
therevoltingx wants to merge 2 commits into
mainfrom
fix/enforces-operator-key

Conversation

@therevoltingx

Copy link
Copy Markdown
Contributor

Require operator credentials on the admin surface (REST + MCP)

Problem

Nearly all admin endpoints were publicly accessible: anyone who could reach the
server could mint API keys (POST /auth/api-keys), read the event log, rewrite
the rate card, change buyer-agent trust levels, mutate packages, trigger
inventory syncs, and push/distribute deals. There was also no way to
distinguish an operator credential from a buyer credential — ApiKeyRecord had
no role concept — so the existing auth dependencies couldn't have gated these
routes correctly even if applied.

What this PR does

1. API keys now carry a role

  • New ApiKeyRole enum (buyer | operator) on ApiKeyRecord, the create
    request/response, and ApiKeyInfo.
  • Defaults to buyer, so pre-existing stored keys deserialize as buyers — old
    keys are never silently promoted.
  • POST /auth/api-keys accepts "role": "operator", so an existing operator
    can mint additional operator keys (invalid roles → 400).

2. Operator enforcement

  • New require_operator_key dependency: anonymous/invalid/revoked/expired
    → 401, valid buyer key → 403, operator key → allowed.
  • Applied to the REST operator surface:
    • /auth/api-keys (create, list, get, revoke)
    • /events, /events/{id}
    • PUT /api/v1/rate-card
    • POST /api/v1/inventory-sync/trigger
    • GET /gam/orders, GET /gam/report (previously optional auth)
    • Registry mutations: discover, trust update, delete
    • Package mutations: POST/PUT/DELETE /packages, /packages/assemble,
      /packages/sync
    • POST /api/v1/curators, POST /api/v1/deals/push,
      POST /api/v1/deals/distribute
  • Buyer-facing reads stay public (GET /packages, GET /registry/agents,
    GET /api/v1/rate-card, media-kit search, etc.).
  • Applied to 18 admin MCP tools (key management, set_agent_trust,
    register_buyer_agent, sync_inventory, create_package,
    update_rate_card, set_publisher_identity, set_approval_gates,
    approve_or_reject, deal push/distribute, GAM reporting, list_sessions,
    inbound queue, buyer activity). Over HTTP transports the tool validates an
    operator key from Authorization: Bearer / X-Api-Key
    (mcp-remote: --header "Authorization: Bearer <key>"). Local stdio access
    is trusted, same model as the CLI.

3. Bootstrap

  • New CLI command: ad-seller create-operator-key [--label ...] [--expires-in-days N]. Mints an operator key by writing directly to storage
    (no network surface), prints the key exactly once. This is how the first
    operator key is created; subsequent keys go through the now-gated REST/MCP
    surface.
  • The AgentCore runtime's internal tool-auth key is now minted in-process via
    ApiKeyService instead of POSTing to the now-gated endpoint.

Bug fix found along the way

app.dependency_overrides never worked for router-mounted routes: main.py
appends router routes directly and set dependency_overrides_provider
afterward, but APIRoute.__init__ had already compiled the request handler
with a None provider, so every test override in the codebase was a silent
no-op. The routes' handlers are now rebuilt after wiring (using FastAPI's
request_response, which also sets up the dependency exit stacks). Three
tests in test_trust_tier_verification.py were unknowingly relying on their
override not applying and were adjusted to drop it where they genuinely
need real key validation.

Tests

  • New tests/unit/test_operator_auth.py (21 tests): legacy records default to
    buyer role, 401/403/allowed matrix across the REST surface, operator keys
    minting buyer + operator keys, CLI bootstrap, and MCP gating (stdio allowed;
    HTTP without key / with buyer key denied, with operator key allowed).
  • Existing tests that hit gated routes anonymously now bypass the operator
    dependency via app.dependency_overrides (they test catalog/flow behavior,
    not auth).
  • Full suite: 1310 unit + 39 integration passing.

@therevoltingx
therevoltingx marked this pull request as draft July 29, 2026 21:31
storage = await get_storage()
service = ApiKeyService(storage)
return await service.create_key(
ApiKeyCreateRequest(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem right to overload the api key, think a new object should be created instead of having all these non-sensical field values

@therevoltingx
therevoltingx marked this pull request as ready for review July 29, 2026 21:51
@therevoltingx
therevoltingx requested a review from atc964 July 29, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant