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}-t0trace format and stop at 2026-07-08 08:50 — before thee12ae68
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 actorstaff-604287292ba92ba0). The internal-agent registers
only[dash, reasoning_dash](one data agent) — there is no second "guidance"
agent, and the onlyengine=ahu-chatbot, surface=internalegress is the data
agent via dash-proxy (fixed) + the eval worker (fixed by0ce6a72). 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()todash/gateway.py;
load_knowledge.pycalls 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'sRequestIdMiddlewareseeds
user_id_varfrom 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.