think
16px
820px

Close the last X-User-Id gaps — internal staff + embeddings — design

Date: 2026-07-09
Status: spec
Source: integration prompt prompt-ai-ahu-chatbot-identity-internal-embed.md.
Follow-up to e12ae68 (per-actor X-User-Id + per-turn trace id).

Investigation (grounded in live ai_calls, last 3 days)

The prompt cites two gaps: internal/chat 66 blank vs 2 real, and 16 blank embeds.
Querying the actual rows:

  • internal/chat blanks are HISTORICAL. All 66 use the pre-nonce
    ${uuid}-t0 trace format and stop at 2026-07-08 08:50 — before the e12ae68
    dash-proxy X-User-Id injection deployed (~09:43). No blank internal/chat rows
    exist after the deploy; post-deploy internal chat resolves to the staff HMAC
    (e.g. the eval actor staff-604287292ba92ba0). The internal-agent registers
    only [dash, reasoning_dash] (one data agent) — there is no second "guidance"
    agent, and the only engine=ahu-chatbot, surface=internal egress is the data
    agent via dash-proxy (fixed) + the eval worker (fixed by 0ce6a72). Gap 1 is
    already closed;
    this spec verifies it, no code change.
  • embed blanks use gateway-assigned ULID traces (no incoming X-Request-Id) →
    they run outside any request context = offline knowledge loads.

The embed call-paths (Gap 2)

Path Human on request? Fix
search_knowledge_base at query time (in a run) yes already carries the run actor via user_id_var (RequestIdMiddleware) — no change
save_query tool (in a run) yes same — no change
admin console knowledge writes (app/api/knowledge.py insert) yes (admin) internal-web must forward the admin id — it wasn't
dash/scripts/load_knowledge.py (offline script) no tag system:reingest

reload-schema rebuilds only the preloaded TEXT context (no embedding); the
agent does not embed at boot. So the only offline embed path is load_knowledge.

Design (reuse the e12ae68 seam)

  • Offline reingest → machine actor. Add SYSTEM_REINGEST_ACTOR = "system:reingest" + seed_reingest_actor() to dash/gateway.py;
    load_knowledge.py calls it before its inserts so the embeddings attribute to
    system:reingest (honest machine actor, mirrors the OCR engine's
    system:<stage>) instead of blank.
  • Admin knowledge writes → the admin. The 4 internal-web routes
    (admin/knowledge/{tables,business,content,seed-templates}) already have the
    verified admin session (requireMutationSession); they now send
    X-User-Id: opaqueStaffId(auth.user.email) to the agent, so the write's
    embedding attributes to the admin (the agent's RequestIdMiddleware seeds
    user_id_var from it). Same non-PII HMAC source as everywhere else — no new
    identity source.
  • Query-time embeds already inherit the request actor via the existing
    ContextVar — unchanged.

Dormancy

Additive X-* only; with the gateway unflipped the upstream ignores them. The
seed_reingest_actor() helper only sets a ContextVar; it changes nothing until
an embed call reads gateway_default_headers().

Tests

  • gateway.py: seed_reingest_actor()gateway_default_headers() emits
    X-User-Id: system:reingest.
  • internal-web: the knowledge-write routes forward X-User-Id = opaqueStaffId(email).

Verification (post-deploy)

Re-run the attribution table: internal/chat dominated by staff HMAC ids;
embed splits into real-actor (admin writes) + system:reingest (batch), no PII.