Menu

#273 feat(auth): add non-blocking async challenge interface and InMemoryAsyncBackend (#144)

closed
nobody
None
2026-08-08
2026-08-06
Anonymous
No

Originally created by: Maqbool61

Closes [#144]

What this adds

A non-blocking challenge path alongside the existing synchronous one. The caller
issues a challenge and immediately receives a ChallengeHandle token — no blocking,
no prompt shown. Later, when the human has decided via whatever out-of-band channel
the operator has wired, resolve_challenge() settles the handle and returns a normal
AuthResult.

Files changed

  • src/doberman/auth/async_challenge.py — new module: ChallengeHandle, AsyncChallengeBackend
    Protocol, InMemoryAsyncBackend, issue_challenge(), resolve_challenge(),
    active_async_backend() registry lookup
  • src/doberman/engine/registry.py — added ASYNC_CHALLENGE_BACKEND_GROUP constant
  • tests/unit/test_async_challenge.py — 26 tests

Invariants preserved

  • Fail closed: expired, unresolved, or error handles → non-approved AuthResult
  • Single-use, action-bound: _settle() is write-once under lock; action-ID mismatch raises ValueError
  • No weakening: tier selection, TOTP verification, and AuthResult structure unchanged
  • Import-linter: doberman.auth never imports doberman.proxy/hosthooks/dash/turngate; both contracts KEPT
  • Delivery channels out of scope: this module has no knowledge of Slack/email/push

CI

ruff check ✓ · ruff format --check ✓ · lint-imports ✓ (2 contracts kept) · pytest tests/unit/ ✓ (26 new, full suite green)

Related

Tickets: #144
Tickets: #296

Discussion

  • Anonymous

    Anonymous - 2026-08-08

    Ticket changed by: fu351

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-08-08

    Originally posted by: fu351

    Merged. This is a model interface-only slice: nothing in the live decision path references the new module, resolve() reuses the same rate-limited totp.verify() as the sync path instead of reimplementing it weaker, every terminal state denies (expiry, wait-timeout, missing or bad code, action-id mismatch), and test_concurrent_resolvers_only_one_wins actually races threads at a Barrier rather than asserting idempotency on faith.

    One non-blocking nit for the future wiring PR: two concurrent resolvers can both clear the "not yet resolved" check, so a losing bad-code resolve still burns a TOTP lockout attempt. Stricter rather than weaker, so it can wait.

    You picked a hard first issue here and landed it cleanly. If you want another with real depth: [#245] (OpenTelemetry AuditSink, level-4) is a subsystem behind an existing interface, much like this one. We are also on Discord if you want faster feedback loops: https://discord.gg/Sfy5XGNqty

     

    Related

    Tickets: #245


Log in to post a comment.