Skip to content

feat(agentex): forward user api key to agent pods via acp headers#245

Open
cdvillegas wants to merge 8 commits into
mainfrom
chris-villegas/AGX1-240/runtime-delegation-headers
Open

feat(agentex): forward user api key to agent pods via acp headers#245
cdvillegas wants to merge 8 commits into
mainfrom
chris-villegas/AGX1-240/runtime-delegation-headers

Conversation

@cdvillegas
Copy link
Copy Markdown

@cdvillegas cdvillegas commented May 21, 2026

Pull Request Summary

Runtime delegation v1 plumbing: when a user calls scale-agentex with x-api-key and auth succeeds, outbound ACP JSON-RPC calls to agent pods include x-acting-user-api-key (from the inbound request), x-acting-as-agent, and optional x-selected-account-id. Clients cannot spoof the acting headers; they are blocked on ingress and set only server-side.

No agentex-auth changes: validation still uses /v1/authn; the user key is not stored on principal_context. Agent pods must read x-acting-user-api-key and pass it to SGP (SDK PassthroughResolver is a follow-up).

Test Plan

  • Added test_send_message_includes_delegation_headers asserting acting headers on ACP default_headers.
  • Updated AgentACPService unit/use-case fixtures to inject a mock Request with principal_context and headers.

Linear Issue

Resolves AGX1-240

Made with Cursor

Greptile Summary

This PR wires up runtime-delegation v1: when a user authenticates with x-api-key, every outbound ACP JSON-RPC call to agent pods now carries x-acting-user-api-key (and optionally x-selected-account-id) so agents can call downstream services as the user. Client spoofing is prevented by adding x-api-key and the x-acting-* headers to BLOCKED_HEADERS, and delegation is only emitted when auth has already validated a non-agent principal.

  • New delegation_headers.py encapsulates the logic; AgentACPService is injected with the FastAPI Request to read principal_context and inbound headers, and get_headers is unified to merge filtered passthrough → delegation → agent-auth → server x-request-id (last wins).
  • request_utils.py extends the log-scrubbing blacklist with api-key and acting-user patterns; middleware_utils.py replaces full principal_context object logging with scoped user_id/account_id fields to limit sensitive data exposure in logs.
  • Tests cover delegation header presence, absence of raw x-api-key passthrough, and server-side x-request-id override.

Confidence Score: 4/5

Safe to merge with one known open security item: x-selected-account-id is absent from BLOCKED_HEADERS, so an authenticated caller can supply their own account ID on the send_event path and have it forwarded to agent pods.

The delegation header plumbing is well-constructed — x-api-key and x-acting-* are properly blocked, x-request-id is server-generated last so it cannot be overridden, and the principal guard in build_delegation_headers ensures keys are only forwarded for authenticated user requests. The one unresolved gap is x-selected-account-id missing from BLOCKED_HEADERS: when delegation does not fire, a client-supplied x-selected-account-id passes through filter_request_headers into the outbound ACP call, contradicting the PR's stated invariant that acting headers are set only server-side.

agentex/src/domain/services/agent_acp_service.py — BLOCKED_HEADERS needs x-selected-account-id added to close the acting-header spoofing gap on the send_event path.

Important Files Changed

Filename Overview
agentex/src/domain/delegation_headers.py New module implementing build_delegation_headers; correctly guards on principal and api_key presence, but x-selected-account-id is not in BLOCKED_HEADERS so it remains client-spoofable on the send_event path.
agentex/src/domain/services/agent_acp_service.py Adds Request injection, unifies get_headers with correct precedence (x-request-id last so server wins), and blocks x-api-key and x-acting-* from passthrough; x-selected-account-id still absent from BLOCKED_HEADERS.
agentex/src/utils/request_utils.py Adds api-key and acting-user to the log-scrubbing regex blacklist — straightforward security hardening for log redaction.
agentex/src/api/middleware_utils.py Replaces full principal_context object in the auth-success log line with scoped user_id/account_id fields — reduces sensitive data exposure in logs.
agentex/tests/unit/services/test_agent_acp_service.py Adds comprehensive tests for delegation header presence, raw-key non-passthrough, and server x-request-id precedence; fixtures correctly inject mock Request with principal_context.
agentex/tests/fixtures/services.py Adds create_mock_request helper and threads the optional request parameter through create_agent_acp_service factory — clean test infrastructure update.
agentex/tests/unit/use_cases/test_agents_acp_use_case.py Inline mock Request added to the use-case fixture so it compiles with the new AgentACPService constructor — no logic changes.

Comments Outside Diff (2)

  1. agentex/src/domain/services/agent_acp_service.py, line 83-110 (link)

    P1 security x-api-key missing from BLOCKED_HEADERS, leaking raw key alongside delegation header

    For the EVENT_SEND path, the route handler passes dict(fastapi_request.headers) (all inbound headers) as request_headers. Since x-api-key is not in BLOCKED_HEADERS, filter_request_headers lets it through, so agent pods receive the user's raw API key as x-api-key in addition to the properly-gated x-acting-user-api-key. The PR's stated goal is controlled key forwarding through delegation; this gap means the key is also forwarded verbatim, bypassing that control for every send_event call made by a user principal. Adding "x-api-key" to BLOCKED_HEADERS would close this.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: agentex/src/domain/services/agent_acp_service.py
    Line: 83-110
    
    Comment:
    **`x-api-key` missing from `BLOCKED_HEADERS`, leaking raw key alongside delegation header**
    
    For the `EVENT_SEND` path, the route handler passes `dict(fastapi_request.headers)` (all inbound headers) as `request_headers`. Since `x-api-key` is not in `BLOCKED_HEADERS`, `filter_request_headers` lets it through, so agent pods receive the user's raw API key as `x-api-key` in addition to the properly-gated `x-acting-user-api-key`. The PR's stated goal is controlled key forwarding through delegation; this gap means the key is also forwarded verbatim, bypassing that control for every `send_event` call made by a user principal. Adding `"x-api-key"` to `BLOCKED_HEADERS` would close this.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Cursor Fix in Claude Code Fix in Codex

  2. agentex/src/domain/services/agent_acp_service.py, line 72-81 (link)

    P1 security x-selected-account-id missing from BLOCKED_HEADERS

    The PR blocks x-acting-user-api-key and x-acting-as-agent but omits x-selected-account-id. For send_event calls, request_headers is the full inbound client header map passed through filter_request_headers. When delegation doesn't fire (e.g., the user authenticates via a non-x-api-key mechanism so build_delegation_headers returns {}), the client-supplied x-selected-account-id passes through filtered_request_headers and is forwarded verbatim to the agent pod. This lets an authenticated caller pick an arbitrary account ID for downstream SGP operations — contradicting the PR's stated invariant that acting headers "are blocked on ingress and set only server-side." Adding "x-selected-account-id" to BLOCKED_HEADERS closes this gap.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: agentex/src/domain/services/agent_acp_service.py
    Line: 72-81
    
    Comment:
    **`x-selected-account-id` missing from `BLOCKED_HEADERS`**
    
    The PR blocks `x-acting-user-api-key` and `x-acting-as-agent` but omits `x-selected-account-id`. For `send_event` calls, `request_headers` is the full inbound client header map passed through `filter_request_headers`. When delegation doesn't fire (e.g., the user authenticates via a non-`x-api-key` mechanism so `build_delegation_headers` returns `{}`), the client-supplied `x-selected-account-id` passes through `filtered_request_headers` and is forwarded verbatim to the agent pod. This lets an authenticated caller pick an arbitrary account ID for downstream SGP operations — contradicting the PR's stated invariant that acting headers "are blocked on ingress and set only server-side." Adding `"x-selected-account-id"` to `BLOCKED_HEADERS` closes this gap.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Cursor Fix in Claude Code Fix in Codex

Reviews (4): Last reviewed commit: "refactor(agentex): drop x-acting-as-agen..." | Re-trigger Greptile

Co-authored-by: Cursor <cursoragent@cursor.com>
@cdvillegas cdvillegas requested a review from a team as a code owner May 21, 2026 01:45
cdvillegas and others added 2 commits May 20, 2026 18:50
…merge style

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread agentex/src/domain/services/agent_acp_service.py Outdated
cdvillegas and others added 4 commits May 20, 2026 19:01
…aders

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Collaborator

@danielmillerp danielmillerp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good thanks for fast PR here!!


from typing import Any

HEADER_ACTING_AS_AGENT = "x-acting-as-agent"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment explaining why we are doing this? on first reading it feels weird to send the agent id to the agent haha

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought idk if we need this header (ticket called for it). Eventually agents will be Hydra clients minting OBO tokens with agent id in the claim, so sending it separately as a header feels redundant. I went ahead and removed it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants