Note — OCR audit hygiene: production is clean, smoke traffic isn't
Audience: whoever runs OCR gateway smoke/verification against the AHU AI platform (ahu-ocr-tidyup maintainers + platform ops). Type: convention/runbook note, not a production code change.
Finding
Production OCR audit attribution is correct. backend/src/lib/gateway-headers.ts builds every gateway call's headers through buildGatewayHeaders(), which:
- hardcodes
X-Tenant-Id: ahu-ocrandX-Surface: internal(constants — cannot be blank), - reuses one
X-Request-Idper document/action (minted once at the outermost entry:getRequestContext().requestIdfor HTTP,crypto.randomUUID()for pipeline runs) and propagates it unchanged downstream, - attributes machine work as a named, filterable actor
X-User-Id: system:<stage>when no human verifier is on the request.
Verified against the live observatory (ai_calls, 7-day window):
- 102 / 102 OCR calls with a
system:*actor carrysurface = internal. Zero blank. - Zero OCR trace_id spans more than 5 minutes — i.e. no cross-task trace collisions (contrast the chatbot, whose reused
sessionId-t{turn}ids collided across unrelated conversations hours apart).
So OCR does not replicate the chatbot's trace-collision or blank-attribution problems. Nothing in the OCR engine needs changing.
The one real issue: smoke traffic bypasses the helpers
Every problematic OCR row in the audit stream is manual smoke/verification traffic, not production:
| trace_id | calls | surface | note |
|---|---|---|---|
smoke-p27-azure |
21 | (blank) | hand-typed X-Request-Id, reused across 21 calls |
smoke-p27-job, smoke-p27-classify, smoke-p27-classify3 |
1 each | (blank) | flip-day smoke |
smoke-paddle-facade, p28-scan1, p28-scan2 |
1 each | (blank) | manual scans |
| (blank) | 1 | (blank) | no headers at all |
These were curl -H "X-Request-Id: smoke-p27-azure" … commands run by hand during the 2026-07-07 gateway flip. They aren't committed anywhere. Because they hit the gateway directly instead of going through buildGatewayHeaders, they land in the shared ahu.ai.audit stream with:
- a hardcoded, reused trace id (
smoke-p27-azureon 21 calls) — the same collision pattern we flagged elsewhere, just from a test harness, and - blank
X-Surfaceand blankX-User-Id— so they pool into theahu-ocr/internal/unattributed(and worse, fully-blank-surface) actor buckets that the observatory's security rules evaluate.
Impact is low (it's low-volume, one-off, obviously smoke-*), but it pollutes the actor rollups the security detector reads and could, at higher volume, nudge an outlier rule.
Recommended convention (apply when smoke-testing any engine through the gateway)
Option A — preferred: smoke-test through the real code path. Drive OCR via its own endpoints/pipeline so buildGatewayHeaders runs. You automatically get X-Surface: internal, a unique per-run X-Request-Id, and proper system:<stage> attribution — the traffic is then indistinguishable-in-shape from production and correctly attributed.
Option B — when you must curl the gateway directly: send the full CONVENTIONS §2 header set, and make the test traffic self-identifying and unique:
TRACE="smoke-$(uuidgen)" # unique per run — never a fixed string reused across calls
curl -H "X-Tenant-Id: ahu-ocr" \
-H "X-Surface: internal" \
-H "X-User-Id: system:smoke-ocr-azure" \ # filterable synthetic actor, not blank
-H "X-Request-Id: $TRACE" \
-H "X-Priority: system" \
…
Two rules that matter:
1. Unique trace per logical action (uuidgen), reused only across that action's own fan-out — never a hardcoded literal shared by many calls.
2. Tag synthetic traffic with a system:smoke-* actor (reusing the engine's existing system:<stage> convention) so it is attributed and filterable rather than pooled into unattributed.
Optional complement (observatory side — I can implement directly; not hands-off)
Add a system:smoke% (and smoke-% trace-prefix) exclusion to the observatory's actor rollup used by the security rules, so labeled test traffic is never counted as behavior. This is the same "exclude synthetic/known-noise" move as the 2026-07-09 infra-error-code exclusion already shipped in sec_error_probing. It makes Option B's system:smoke-* tag actively useful and keeps the security surface honest even if a stray hand-run smoke slips through. Say the word and I'll add it (with a regression test) the same way.
Bottom line
- No OCR engine code change required — production attribution and tracing are correct.
- The only litter is manual smoke traffic; fix it by convention (Options A/B), not by touching the engine.
- The durable safeguard is the observatory-side
system:smoke*exclusion, which I can ship on request.