Completion notes — Audit Hardening (2026-07-07)
Spec: specs/2026-07-07-audit-hardening-design.md · Plan: plans/2026-07-07-audit-hardening.md
Source prompt: https://x056.think.val.id/prompt-ai-ahu-chatbot-audit-hardening.md
All four items shipped, TDD throughout, verified against the installed pins
(agno 2.4.7 + openai 2.16.0) in a dedicated scratch venv — not assumed.
1. Embedder tenant attribution ✅
apps/{internal,public}-agent/dash/embedder.py — gateway branch now passes
client_params={"default_headers": gateway_default_headers("interactive")}.
Verified plumbing: agno 2.4.7's OpenAIEmbedder.client_params merges into
both the sync and async OpenAI client constructors (empirical). Priority
interactive — the instance serves query-time knowledge search while a user
waits (offline reingest shares it; commented). Legacy no-EMBEDDER_BASE_URL
branch stays byte-identical. Tests assert X-Tenant-Id on the constructed
client; the test loader registers a stub dash package + real gateway.py
by file path so the heavy dash/__init__ never runs.
2. 403 → friendly degradation (public) ✅
LlmHttpErrorgainscode(parsed from the JSON error body:
error.code|code) — carriesEXTERNAL_UPSTREAM_FORBIDDENfor
observability.orchestrate.ts: 403 fromLlmHttpError(non-stream) or
ComposeUpstreamError(mid-streamevent: error) → SSE error
"Layanan AI sedang tidak tersedia untuk permintaan ini." Tests assert
noorchestrator failure:leak and no raw code string in the frame.
3. Staff surface degradation ✅ (best-effort within agno, as prompted)
- Not forced: agno agents still don't opt into
X-Queue-Events— a
client-level header would leak foreignevent: queueSSE frames into
openai-python's chunk parser. Documented in
packages/streams/src/friendly-errors.tsfor the future agno upgrade. - Bounded retry already exists beneath agno: openai-python retries twice
with exponential backoff honoringRetry-After(verified on 2.16.0) before
RunErrorsurfaces. Documented, not reimplemented. - New
friendlyAgentErrorMessage()maps 429/rate-limit/503 shapes →
"Sistem sedang sibuk. Silakan coba lagi sesaat lagi." and
403/EXTERNAL_UPSTREAM_FORBIDDEN→ "Layanan AI sedang tidak tersedia untuk
permintaan ini." Applied at bothNativeProviderfailure sites (non-ok run
POST;RunErrorevents). Raw text preserved inconsole.error; unrelated
errors pass through unchanged. Benefits both surfaces (shared package);
staff/app/datawas the target.
4. Per-turn trace_id — FEASIBLE, implemented (the prompt's "attempt" landed)
The finding the prompt asked for: agno 2.4.7 has no per-run header API, but
openai-python re-evaluates the default_headers mapping on every request
build — verified empirically on the pinned openai 2.16.0 (and 2.44.0), for
chat and embeddings. So:
dash/gateway.py:request_id_var(ContextVar) +GatewayHeaders
(lazyMapping: static identity +X-Request-Idwhen the var is set) +
RequestIdMiddleware(pure ASGI, framework-free) seeding the var from the
incomingX-Request-Id.gateway_default_headers()now returns
GatewayHeaders, upgrading all existing call sites (3 OpenAILike models,
knowledge client, embedder) with zero further changes. One
add_middleware(RequestIdMiddleware)line in each agent'sapp/main.py.- Senders:
DataTool(public orchestrator, server-side) forwards the turn's
existing id via a new optionalrequestIdctor arg —
turnRequestId(req) = ${sessionId}-t${turn}shared withbuildLlms, so
the orchestrator↔agent fan-out for one turn joins one trace.
NativeProvider(staff, browser → same-origin proxy → agent) sends the
same derivation; the proxy forwards headers verbatim, no CORS impact. - Guard:
test_openai_client_resolves_request_id_per_requestdrives the real
pinned client's_build_requestacross ContextVar changes — an openai
upgrade that snapshots the mapping fails the image's build-time pytest
instead of silently reverting to random trace ids. - Caveats documented in-code: ContextVar propagates through asyncio tasks
(agno's async run path) and anyioto_thread; a rawThreadPoolExecutor
hop would drop it (header simply omitted → gateway assigns, as today).
No-header requests always reset the var (no cross-request leakage).
Verification
- Workspace: typecheck clean; 139 tests green (streams 16, public-web 103,
internal-web 20);check-conventions.mjsOK. - Agents: 13/13 DB-free tests per app on the pinned venv; mirrors
byte-identical (gateway.py,embedder.py,agents.py,knowledge.py,
tests); bothmain.pycarry the middleware (files are variant-specific,
same addition applied). - Dormancy: gateway env unset ⇒ unchanged behavior; all additions are
additiveX-*headers (ignored by OpenAI/vLLM) or error-message copy.
Interpretations (prompt asked)
- Any 403 from the LLM endpoint gets the friendly copy (code distinguishes
on-prem refusal on the error object; users never see raw codes). - Embedder priority
interactive(query-time dominant use; reingest shares). - Staff "bounded retry honoring Retry-After" = the openai SDK's built-in
retry, documented rather than duplicated at another layer. - NativeProvider's
X-Request-Idalso flows on the public doc-RAG surface
(ai-ahu-document-ragbackend ignores it harmlessly — additive).
Out of scope (unchanged)
Synthesis on-prem move = gateway-side deploy/gateway.yaml decision (Efran).
This repo now degrades cleanly the moment allow_external_upstreams flips off.