Task — engineering-spec@1

"Metering api_calls emit at auth verify_jwt — WalQueue push on success"

doneTASK-TEN-205
module ten · class product · priority p0 · created 2026-07-26 · shipped null
depends on TASK-TEN-004, TASK-TEN-204 · blocks none

TASK-TEN-205: api_calls metering emit at verify_jwt

Summary

Wire the second live metering emit path: after a successful JWT-gated request (verify_jwt → handler → 2xx/3xx), push one MeteringEvent with axis = api_calls, quantity = 1 onto a process-local WalQueue / InMemoryRecorder. Failed auth never bills.

Problem

TASK-TEN-004 §1 #6 requires auth-middleware api_calls emit. TEN-204 shipped ai_tokens at cost_reconcile only. Auth middleware has the tenant on the response path and is the single inheritance point for every JWT-gated route.

Proposed Solution

  1. Add cyberos-metering dep to cyberos-auth (alongside existing cyberos-ten).
  2. services/auth/src/metering_emit.rs mirroring ai-gateway: OnceLock WalQueue + InMemoryRecorder; emit_api_call(tenant_id, idempotency_key, method, path).
  3. In verify_jwt, after next.run(request), if response.status().is_success(), call emit with idempotency_key = format!("{jti}:{method}:{path}:{uuid}") (unique per request; jti alone is session-scoped).
  4. Never emit on early 401 paths (missing/invalid/revoked JWT).
  5. WAL overflow / lock poison: log; never change the response.
  6. Unit + integration tests for emit helper; middleware success-path covered by helper contract + a unit test that documents the call site.

Alternatives Considered

Success Metrics

Scope

In scope

Out of scope / Non-Goals

Dependencies

AI Authorship Disclosure

Generated then reviewed against as-built auth middleware + TEN-204 emit (2026-07-26).

Acceptance Criteria

  1. Success emitemit_api_call with qty=1 axis api_calls records once.
  2. Idempotency — duplicate same key → recorder length unchanged.
  3. Non-blocking — overflow does not panic / does not alter return contract.
  4. Auth fail — early unauthorized path does not call emit (documented + unit coverage of helper not invoked when status would be 401).
  5. source_service"auth".
  6. extra — includes method and path.
  7. DepCargo.toml lists cyberos-metering.
  8. verify_jwt hook — success response path calls emit (source citation).

Verification

cd services
cargo test -p cyberos-auth --lib metering_emit -- --test-threads=1
cargo test -p cyberos-auth --test metering_api_calls_emit_test -- --test-threads=1
bash .cyberos/cuo/gates/run-gates.sh

Failure Modes

FailureDetectionOutcomeRecovery
WAL overflowWalErrorlog; request OKDrain later
Lock poisonlock errlog; request OKRestart
Bad quantityvalidateskipN/A (qty=1)
Missing jtiUUID fallbackstill emit
Double middlewareunique keytwo events if two successOK
Path PIIpath onlyno query stringStrip query
Test isolationunique keys
Dep missingcargobuild failAdd dep
Emit before next.runcode reviewfail ACMove after
5xx billedis_success falseno emitOK

End of TASK-TEN-205.

Audit

§1 — Verdict summary

Residual api_calls emit at verify_jwt success path. 8 ACs, 10 failure modes, mirrors TEN-204 shape; overage explicitly Out of scope.

§2 — Findings (all resolved)

ISS-001 — Idempotency key weak if jti-only

Resolved: key includes jti+method+path+uuid; AC #2.

ISS-002 — Emit on 5xx would bill failures

Resolved: is_success() only; failure table.

ISS-003 — Hot-path latency

Resolved: post-response emit; non-blocking overflow AC #3.

ISS-004 — Overage creep

Resolved: Out of scope.

ISS-005 — Query string PII in path

Resolved: strip query; failure row.

ISS-006 — Pattern drift from TEN-204

Resolved: Proposed Solution mirrors ai-gateway metering_emit.

§3 — Resolution

All 6 mechanical concerns addressed. Score = 10/10.


End of TASK-TEN-205 audit.