Integration prompt — ai-ahu-chatbot: populate X-User-Id so audit/security can attribute per-person
Paste into a Claude Code session inside the
ai-ahu-chatbotrepo. This is a follow-up toprompt-ai-ahu-chatbot.md+prompt-ai-ahu-chatbot-audit-hardening.md(both DONE + LIVE). The gateway wire contract is unchanged — this prompt only closes the identity-attribution gap.
Why (read first — this is the whole point)
The platform now has a per-actor security layer in the observatory + dashboard: it flags token-egress outliers, jailbreak/probing patterns (error-rate), volume outliers, off-hours activity, and bulk body-reading — per actor, where an actor is engine / surface / user_id.
Right now user_id is blank on almost all chatbot traffic, so every chatbot actor collapses to ahu-chatbot/public/unattributed and ahu-chatbot/internal/unattributed. The security rules still fire, but they can only point at a surface, not a person. The moment this engine sends a stable X-User-Id, the same rules resolve to individual users — that is the difference between "the public surface is being probed" and "user X is probing."
No gateway change is required. The gateway already reads X-User-Id off every request (proxy/handler.go, jobs/api.go, all façades) and writes it straight into the audit event's user_id. This is a pure client-side fill-in and is dormant-safe: with MODEL_GATEWAY_URL unset the current upstreams ignore the header.
Scope — every gateway egress path must carry an identity
There are three call paths out of this repo. All three must set X-User-Id:
1. Agno agents (apps/{internal,public}-agent/dash/agents.py)
prompt-ai-ahu-chatbot.md §1 added the static X-Tenant-Id/X-Surface headers to the OpenAILike models in _resolve_model() / _build_model_from_slot(), but left the per-run user_id as a # TODO(gateway) pending an agno API check.
- Verify against the installed
agno==2.4.7API (do not guess) whether a model call accepts per-run extra headers / metadata. CheckRunResponse/Agent.run(...)/ theOpenAILikerequest hook for a per-invocation header seam. - If per-run headers are supported: thread the run's
user_id(the same identifier the orchestrator already tracks per conversation/session) intoX-User-Idon each call. - If agno 2.4.7 genuinely cannot do per-run headers: fall back to a stable per-session id set at model-construction time for that session's agent instance (a documented, acceptable landing — per-session grouping is far better than fully unattributed). Do not invent a random-per-call id — that destroys grouping and would make every call look like a distinct actor.
2. Embedders (apps/{internal,public}-agent/dash/embedder.py)
prompt-ai-ahu-chatbot-audit-hardening.md §1 flagged that build_embedder() constructs OpenAIEmbedder with no identity headers at all — query-time RAG retrieval is audited as tenant unknown. If that fix already added X-Tenant-Id/X-Surface, extend it to also carry X-User-Id (the same session/user id used in §1). If it isn't done yet, do the full identity header set here. Retrieval embeddings are part of a user's request footprint — they must attribute to the same actor as the chat call they serve.
3. Orchestrator direct calls (apps/public-web/src/lib/orchestrator, and any internal-web equivalent)
These already send headers (they're the wire-correct path from the original audit). Confirm X-User-Id is populated from the request's authenticated user / session id, not omitted. Public citizens are typically anonymous — use a stable per-session id (e.g. the existing chat session id) so a single abusive session groups into one actor rather than smearing across "unattributed."
Identity rules (apply uniformly)
- Authenticated internal staff → their real staff/user id. This is the highest-value case: it makes the internal surface fully attributable.
- Anonymous public citizens → a stable per-session id (reuse the existing session/conversation identifier; do not mint a fresh one). Never random-per-call.
- Never put PII (name, email, NIK) in
X-User-Id— it lands in the audit trail. An opaque stable id is correct. - Keep
X-Surfacecorrect (internalvspublic) — the security view slices by it.
Dormancy + verification
- Dormancy proof: with no new env set /
MODEL_GATEWAY_URLunset, confirm request URLs, bodies, and headers minus the additiveX-*are byte-identical against today's upstreams. - Tests: add/extend unit tests asserting
X-User-Idis present and stable across calls within one session, and that anonymous public traffic gets a stable (non-random) per-session id. - Post-flip check: run one internal chat + one public chat, then in the observatory dashboard's Security view confirm the actors table now shows
ahu-chatbot/internal/<staff-id>andahu-chatbot/public/<session-id>instead of.../unattributed.
Report back
Files changed; the agno 2.4.7 per-run-header finding (supported? which seam? or the documented fallback to per-session); confirmation that embedder + orchestrator paths carry the same id; and anything in the identity contract that needed interpretation.