think
16px
820px

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-ocr and X-Surface: internal (constants — cannot be blank),
  • reuses one X-Request-Id per document/action (minted once at the outermost entry: getRequestContext().requestId for 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 carry surface = 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-azure on 21 calls) — the same collision pattern we flagged elsewhere, just from a test harness, and
  • blank X-Surface and blank X-User-Id — so they pool into the ahu-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.