think
16px
820px

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-chatbot repo. This is a follow-up to prompt-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.7 API (do not guess) whether a model call accepts per-run extra headers / metadata. Check RunResponse / Agent.run(...) / the OpenAILike request 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) into X-User-Id on 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-Surface correct (internal vs public) — the security view slices by it.

Dormancy + verification

  • Dormancy proof: with no new env set / MODEL_GATEWAY_URL unset, confirm request URLs, bodies, and headers minus the additive X-* are byte-identical against today's upstreams.
  • Tests: add/extend unit tests asserting X-User-Id is 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> and ahu-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.