Skip to content

Production-readiness pass: observability, store-backed approvals, retention, shutdown, config validation - #17

Open
shadowhunter-92 wants to merge 5 commits into
mainfrom
prod/readiness-pass
Open

Production-readiness pass: observability, store-backed approvals, retention, shutdown, config validation#17
shadowhunter-92 wants to merge 5 commits into
mainfrom
prod/readiness-pass

Conversation

@shadowhunter-92

Copy link
Copy Markdown
Owner

What this PR does

Closes the gaps flagged in docs/ROADMAP.md as demand-gated / known limitations. The maintainer's own roadmap listed these as the work needed before the project is production-ready; this PR ships all of it.

Test suite: 157 passing, 7 skipped (was 136; +21 new tests). Skips need external resources (Postgres / redis).

The 8 things this PR ships

1. Observability (was roadmap #1)

  • /metrics Prometheus endpoint: call counter (agentbridge_calls_total{src,dst,capability,decision}), latency histograms (agentbridge_call_duration_seconds, agentbridge_translate_duration_seconds), audit-entry gauge, per-agent budget gauges, pending-approvals gauge, HTTP request counter + duration histogram, rate-limit-hit counter, auth-failure counter.
  • OpenTelemetry tracing (optional, lazy-initialized): set OTEL_EXPORTER_OTLP_ENDPOINT to ship spans. Gateway opens a span around every route_call.
  • Structured JSON logging (AGENTBRIDGE_LOG_JSON=1, auto-on in production / k8s): one JSON object per line with ts, level, logger, msg, request_id, plus any extra= fields.
  • Correlation IDs: every request gets an X-Request-ID (echoed in the response), surfaced in logs via a ContextVar.

2. Store-backed ApprovalQueue (was roadmap #2 — last piece of in-process state)

  • Approvals now live in the durable store (InMemory / SQLite / Postgres) instead of in-process state. Multi-worker safe — no instance pinning required for approval traffic.
  • New store.consume_approval() atomically transitions approved -> consumed, so two workers can't double-consume a one-shot grant.
  • Schema additions: approvals table in SQLite + Postgres with indexes on status and agent_id.

3. JWKS auto-fetch for OIDC (was roadmap #3)

  • When no static signing key is configured, the verifier fetches <issuer>/.well-known/openid-configuration to discover jwks_uri, then fetches + caches JWKS keys (TTL 15min, refresh on kid miss).
  • Explicit AGENTBRIDGE_OIDC_JWKS_URL also supported.

4. Audit retention + legal hold

  • POST /control/audit/checkpoint — sign the current audit head with Ed25519 so a third party can later prove the log wasn't truncated before this point.
  • POST /control/audit/retention
    • {"action":"truncate","seq":N} removes entries with seq < N (safe after a checkpoint).
    • {"action":"legal_hold","on":true} freezes truncation (subsequent truncate attempts return 409).
  • New store.truncate_audit_before(seq) on all 3 backends.

5. Graceful shutdown + split health probes

  • lifespan installs SIGTERM/SIGINT handlers; flips readiness to False; drains in-flight requests up to AGENTBRIDGE_SHUTDOWN_GRACE (default 10s); then closes.
  • /health — liveness (always 200, even during drain, so k8s doesn't restart the pod mid-shutdown).
  • /ready — readiness (503 during drain OR if the governance store is unreachable).
  • /version — build info (version, Python, store type).
  • CLI passes graceful-shutdown timeout through to uvicorn.

6. Retry/backoff on transient store errors

  • append_audit_chained and mutate_budget now retry on SQLite database is locked / psycopg OperationalError (up to 4 attempts, exponential + jitter, capped at 0.5s). Permanent errors bubble immediately.

7. Config validation at startup

  • New src/config.py validates env vars before any state is created.
  • Production requires AGENTBRIDGE_ADMIN_KEY and AGENTBRIDGE_DB; rate-limit and shutdown-grace values are range-checked; OIDC issuer must be a URL; psycopg must be importable when a postgres URL is configured.
  • Errors raise ConfigError (fail-fast at boot, not first request).
  • Warnings emitted when admin key is missing/short, when in-memory store is used, when OIDC has no signing key.

8. Production-safety polish

  • FastAPI docs (/docs, /redoc) suppressed when AGENTBRIDGE_ENV=production unless AGENTBRIDGE_DOCS=1.
  • CLI serve improvements: --workers N, --log-level, disables uvicorn's noisy access log (we have our own structured middleware).
  • prometheus-client added as a runtime dependency; [otel] optional extra added for OpenTelemetry.

File changes (17 files, +2072/-214)

New files (5):

  • src/observability/__init__.py — Prometheus metrics + OpenTelemetry tracing
  • src/observability/logging.py — structured JSON logging with correlation IDs
  • src/config.py — startup config validation (fail-fast)
  • src/governance/resilience.py — retry/backoff on transient store errors
  • tests/test_production_readiness.py — 21 new tests

Modified files (12):

  • src/api/control_plane.py — middleware rewrite, /metrics + /ready + /version, lifespan, validate_config, audit retention endpoints, AUTH_FAILURES / approvals gauges
  • src/api/auth_oidc.py — JWKS auto-discovery + cache + key rotation on kid miss
  • src/governance/store.py — approval table + methods, consume_approval, truncate_audit_before, retry on atomic ops
  • src/governance/approvals.py — store-backed rewrite
  • src/governance/audit.pyset_legal_hold, truncate_before
  • src/governance/gateway.py — span + metrics instrumentation
  • src/protocols/registry.py — translation-duration metric
  • src/cli.py--workers, --log-level, graceful shutdown
  • pyproject.tomlprometheus-client runtime dep, [otel] extra
  • CHANGELOG.md, docs/ROADMAP.md, docs/ENTERPRISE.md — updated to reflect what's now done

Test results

157 passed, 7 skipped in ~10s

Skips:

  • 6 Postgres integration tests (need AGENTBRIDGE_TEST_PG=postgres://...)
  • 1 conformance test (needs redis)

New test coverage in tests/test_production_readiness.py:

  • Store-backed approvals: persistence, one-shot consume, idempotent approve/deny
  • Audit retention + legal hold + checkpoint signing + verification
  • Prometheus metrics rendering
  • Structured JSON logging
  • Config validation (5 scenarios: clean dev, prod-missing-key, prod-missing-db, bad rate-limit, bad OIDC issuer)
  • Retry/backoff (3 scenarios: succeeds-after-transient, gives-up-after-max, no-swallow-permanent)
  • /health + /ready + /version + /metrics endpoints
  • Request-ID echo
  • Full audit-retention HTTP round-trip

What's NOT in this PR (still genuinely demand-gated)

  • Streaming/multi-turn canonical model (would change wire bytes; needs live-agent re-validation)
  • SIEM push connectors (the export + checkpoint primitives are there; the shippers are a small future addition)
  • Engine/mesh consolidation (same — changes wire bytes)
  • ANP support (correctly deferred to identity/discovery plane)
  • Managed hosting / SOC 2 (operations, not code)

Reviewer notes

  • The PR is large (~2K LOC) but each file's changes are tightly scoped to its feature.
  • Suggested review order: src/observability/src/governance/resilience.pysrc/governance/store.py (approvals + truncate) → src/governance/approvals.py + audit.pysrc/config.pysrc/api/control_plane.py (the biggest single file) → src/api/auth_oidc.py → tests.
  • All existing tests pass unchanged (no behavioral regression on existing endpoints).
  • The ApprovalQueue(store=store) change in control_plane.py is the only line that changes existing runtime behavior — approvals now persist across restarts.

Disclosure

This PR was produced with AI assistance (Claude) under human direction. The author reviewed every change, ran the test suite, and verified the end-to-end smoke test (see CHANGELOG for the smoke-test checklist).

Production Readiness Bot and others added 2 commits June 30, 2026 01:31
…ention, shutdown, config validation

Closes the gaps flagged in docs/ROADMAP.md as "demand-gated" / "known limitations":
observability (#1), store-backed ApprovalQueue (#2), JWKS auto-fetch for OIDC (#3),
audit retention + legal hold, graceful shutdown + split health probes, retry/backoff
on transient store errors, config validation at startup.

Tests: 157 passing, 7 skipped (was 136; +21 new in tests/test_production_readiness.py).
Skips are the 6 Postgres integration tests (need AGENTBRIDGE_TEST_PG) and 1 conformance
test that needs redis.

See CHANGELOG.md and docs/ENTERPRISE.md for the full feature list and production
deployment checklist.
CI-blocker:
- requirements.txt: add prometheus-client (CI installs from it; /metrics tests were red).

Correctness:
- cli.py: graceful-shutdown timeout to uvicorn in SECONDS (was *1000 -> ~2.8h).
- audit.py: truncated chain stays verifiable (verify_chain require_genesis + auto-detect);
  drop false 'truncate pseudo-entry' docstring claim.
- resilience.py: only retry sqlite3.OperationalError, not DatabaseError (parent of
  IntegrityError/ProgrammingError) -> permanent errors fail fast.
- auth_oidc.py: JWKS resolves the cryptography key object directly (no brittle JWK->PEM
  __import__ dance); + an end-to-end JWKS round-trip test.

Performance / safety:
- gateway.py: AuditLog.count() (O(1)) instead of copying the whole audit list per call.
- control_plane.py: HTTP metrics label by route TEMPLATE, not raw path (cardinality fix).

Hygiene:
- observability: real __version__ in build-info metric (was hardcoded 1.0.0).
- normalized 8 files back to mode 644; strengthened retention + permanent-error tests;
  Windows-safe temp-db cleanup in the approvals test.
@shadowhunter-92

Copy link
Copy Markdown
Owner Author

Reviewed file-by-file and fixed all findings (commit d4a2a4bCI now green on 3.11 + 3.12):

  • requirements.txt was missing prometheus-client (CI red) → added
  • CLI graceful-shutdown timeout was ×1000 (~2.8h) → seconds
  • audit truncation broke verify_chain/verify_durablerequire_genesis + auto-detect; truncated chain stays verifiable
  • JWKS resolution was fragile/untested → rewritten to cache the key object + a round-trip test
  • gateway copied the whole audit log per call to count it → AuditLog.count() (O(1))
  • resilience retried permanent errors (caught sqlite3.DatabaseError) → OperationalError only
  • HTTP metrics labeled by raw path (cardinality bomb) → route template
  • tests strengthened; 8 files normalized to mode 644

Decision: parking, not merging. Solid, green, mergeable work — but it's demand-gated production infra (observability / audit-retention / JWKS) and there are no production users yet. Keeping this branch ready; will merge the moment a design-partner needs metrics or audit-retention. main stays lean.

…ce, deployment)

The pass added the features + updated CHANGELOG/ROADMAP/ENTERPRISE but left the README and
endpoint docs stale. Now documented:
- README: new 'Production & operations' section (observability/metrics, /health vs /ready,
  audit retention + signed checkpoints, OIDC JWKS, fail-fast config, graceful shutdown,
  --workers); quick-start lists /ready + /metrics.
- API_REFERENCE: /ready, /version, /metrics; /control/audit/checkpoint + /retention (audit:export).
- DEPLOYMENT: new env vars (JWKS, ENV, LOG_JSON, SLOW_LOG, SHUTDOWN_GRACE, OTEL) + k8s probe note.
… flaky CI hang)

The threaded concurrency test could deadlock under CI scheduling jitter: a slow/absent worker
left the others waiting on a no-timeout threading.Barrier forever, and _run's per-thread joins
summed past pytest-timeout -> a blunt 90s kill (intermittent red on 3.12).
- Barrier now has a generous 30s timeout (self-heals; on CI workers arrive in <1s, so it never
  fires in the normal case but bounds a true stall well under pytest-timeout).
- _run bounds the whole run with one shared deadline instead of cumulative per-thread joins.
shadowhunter-92 added a commit that referenced this pull request Jun 30, 2026
…ang)

Same flaky deadlock that just went red on PR #17's 3.12 run exists here on main: a slow/absent
worker left the others on a no-timeout threading.Barrier forever, and _run's per-thread joins
summed past pytest-timeout -> blunt 90s kill. Barrier now has a generous 30s self-heal timeout
(workers arrive in <1s on CI, so it never fires normally but bounds a true stall); _run uses one
shared deadline instead of cumulative per-thread joins.
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