|
| 1 | +# AgentEx Header Forwarding Implementation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The AgentEx SDK now supports forwarding custom HTTP headers to agents with a **pass-through by default** approach. Headers are forwarded to agents unless the agent has specific filtering configuration in its manifest. |
| 6 | + |
| 7 | +## Key Features |
| 8 | + |
| 9 | +- **Pass-through by default**: All headers are forwarded unless agent has filtering configuration |
| 10 | +- **Optional filtering**: Agents can opt-in to header filtering via `custom_headers` in manifest |
| 11 | +- **Flexible patterns**: Support for wildcards and case-insensitive matching |
| 12 | +- **Clean API**: Headers passed via `request.headers` structure instead of flat parameters |
| 13 | + |
| 14 | +## Architecture |
| 15 | + |
| 16 | +### Pass-Through by Default Behavior |
| 17 | + |
| 18 | +1. **No manifest config** → All headers forwarded to agent |
| 19 | +2. **Empty allowlist** → No headers forwarded |
| 20 | +3. **Configured allowlist** → Only matching headers forwarded |
| 21 | + |
| 22 | +### Agent Manifest Configuration |
| 23 | + |
| 24 | +```yaml |
| 25 | +# manifest.yaml |
| 26 | +agent: |
| 27 | + name: scallion |
| 28 | + description: Real-time smart search triggers |
| 29 | + |
| 30 | + # Optional: Custom header filtering (omit for pass-through) |
| 31 | + custom_headers: |
| 32 | + strategy: "allowlist" |
| 33 | + allowed_headers: |
| 34 | + - "x-user-email" # User identification |
| 35 | + - "x-user-oauth-credentials" # OAuth for Drive search |
| 36 | + - "x-meeting-id" # Meeting context |
| 37 | + - "x-tenant-*" # Tenant-related headers (wildcard) |
| 38 | + - "authorization" # Standard auth header |
| 39 | +``` |
| 40 | +
|
| 41 | +### Example Agent Configurations |
| 42 | +
|
| 43 | +```yaml |
| 44 | +# Pass-through agent (default behavior) |
| 45 | +agent: |
| 46 | + name: data-processor |
| 47 | + description: Processes data with full header context |
| 48 | + # No custom_headers section = all headers forwarded |
| 49 | +``` |
| 50 | + |
| 51 | +```yaml |
| 52 | +# Filtered agent (OAuth-enabled) |
| 53 | +agent: |
| 54 | + name: scallion |
| 55 | + custom_headers: |
| 56 | + strategy: "allowlist" |
| 57 | + allowed_headers: |
| 58 | + - "x-user-*" # All user headers |
| 59 | + - "authorization" # Standard auth |
| 60 | + - "x-tenant-*" # Tenant context |
| 61 | +``` |
| 62 | +
|
| 63 | +```yaml |
| 64 | +# Restricted agent (minimal headers) |
| 65 | +agent: |
| 66 | + name: calculator |
| 67 | + custom_headers: |
| 68 | + strategy: "allowlist" |
| 69 | + allowed_headers: |
| 70 | + - "x-request-id" # Just for tracing |
| 71 | +``` |
| 72 | +
|
| 73 | +## Files to Modify |
| 74 | +
|
| 75 | +### 1. Manifest Schema Definition |
| 76 | +
|
| 77 | +**File**: `src/agentex/lib/types/agent_configs.py` (or similar) |
| 78 | + |
| 79 | +```python |
| 80 | +# NEW: Agent manifest header configuration |
| 81 | +class CustomHeadersConfig(BaseModel): |
| 82 | + strategy: Literal["allowlist", "blocklist"] = "allowlist" |
| 83 | + allowed_headers: List[str] = [] |
| 84 | + blocked_headers: List[str] = [] |
| 85 | + max_header_size: int = 8192 |
| 86 | + max_headers_count: int = 50 |
| 87 | +
|
| 88 | +class AgentManifestConfig(BaseModel): |
| 89 | + # ... existing fields |
| 90 | + custom_headers: Optional[CustomHeadersConfig] = None |
| 91 | +``` |
| 92 | + |
| 93 | +### 2. Core Service Layer with Header Filtering |
| 94 | + |
| 95 | +**File**: `src/agentex/lib/core/services/adk/acp/acp.py` |
| 96 | + |
| 97 | +```python |
| 98 | +import fnmatch |
| 99 | +from typing import Dict, List |
| 100 | +
|
| 101 | +class ACPService: |
| 102 | + async def _get_agent_header_config(self, agent_name: str | None, agent_id: str | None) -> CustomHeadersConfig: |
| 103 | + """Get agent's header configuration from manifest""" |
| 104 | + # Load agent manifest and return custom_headers config |
| 105 | + # Implementation depends on how manifests are stored/accessed |
| 106 | + |
| 107 | + def _filter_headers(self, extra_headers: Dict[str, str] | None, config: CustomHeadersConfig) -> Dict[str, str]: |
| 108 | + """Filter headers based on agent's configuration""" |
| 109 | + if not extra_headers or not config.allowed_headers: |
| 110 | + return {} |
| 111 | + |
| 112 | + filtered = {} |
| 113 | + for header_name, header_value in extra_headers.items(): |
| 114 | + # Check against allowlist patterns |
| 115 | + for pattern in config.allowed_headers: |
| 116 | + if fnmatch.fnmatch(header_name.lower(), pattern.lower()): |
| 117 | + # Apply size limits |
| 118 | + if len(header_value) <= config.max_header_size: |
| 119 | + filtered[header_name] = header_value |
| 120 | + break |
| 121 | + |
| 122 | + # Respect max headers count |
| 123 | + if len(filtered) >= config.max_headers_count: |
| 124 | + break |
| 125 | + |
| 126 | + return filtered |
| 127 | +
|
| 128 | + async def event_send( |
| 129 | + self, |
| 130 | + content: TaskMessageContent, |
| 131 | + agent_id: str | None = None, |
| 132 | + agent_name: str | None = None, |
| 133 | + task_id: str | None = None, |
| 134 | + task_name: str | None = None, |
| 135 | + trace_id: str | None = None, |
| 136 | + parent_span_id: str | None = None, |
| 137 | + # NEW: Accept extra_headers |
| 138 | + extra_headers: dict[str, str] | None = None, |
| 139 | + ) -> Event: |
| 140 | + # NEW: Get agent's header configuration |
| 141 | + header_config = await self._get_agent_header_config(agent_name, agent_id) |
| 142 | + |
| 143 | + # NEW: Filter headers based on agent's allowlist |
| 144 | + filtered_headers = self._filter_headers(extra_headers, header_config) |
| 145 | + |
| 146 | + # Forward only allowed headers |
| 147 | + if agent_name: |
| 148 | + json_rpc_response = await self._agentex_client.agents.rpc_by_name( |
| 149 | + agent_name=agent_name, |
| 150 | + method="event/send", |
| 151 | + params={ |
| 152 | + "task_id": task_id, |
| 153 | + "content": cast(TaskMessageContentParam, content.model_dump()), |
| 154 | + }, |
| 155 | + extra_headers=filtered_headers, # Only agent-approved headers |
| 156 | + ) |
| 157 | + # ... similar for agent_id case |
| 158 | +
|
| 159 | + async def task_create( |
| 160 | + self, |
| 161 | + name: str | None = None, |
| 162 | + agent_id: str | None = None, |
| 163 | + agent_name: str | None = None, |
| 164 | + params: dict[str, Any] | None = None, |
| 165 | + trace_id: str | None = None, |
| 166 | + parent_span_id: str | None = None, |
| 167 | + # NEW: Accept extra_headers |
| 168 | + extra_headers: dict[str, str] | None = None, |
| 169 | + ) -> Task: |
| 170 | + # NEW: Apply same filtering pattern |
| 171 | + header_config = await self._get_agent_header_config(agent_name, agent_id) |
| 172 | + filtered_headers = self._filter_headers(extra_headers, header_config) |
| 173 | + |
| 174 | + # Forward only allowed headers to agent |
| 175 | + # ... implementation similar to event_send |
| 176 | +``` |
| 177 | + |
| 178 | +### 3. ADK Interface Updates |
| 179 | + |
| 180 | +**Files**: `src/agentex/lib/adk/_modules/acp.py` |
| 181 | + |
| 182 | +```python |
| 183 | +# Add extra_headers parameter to public interfaces |
| 184 | +async def event_send( |
| 185 | + content: TaskMessageContent, |
| 186 | + agent_id: str | None = None, |
| 187 | + agent_name: str | None = None, |
| 188 | + task_id: str | None = None, |
| 189 | + task_name: str | None = None, |
| 190 | + # NEW: Accept extra_headers (will be filtered by agent config) |
| 191 | + extra_headers: dict[str, str] | None = None, |
| 192 | +) -> Event: |
| 193 | + return await _get_acp_service().event_send( |
| 194 | + content=content, |
| 195 | + agent_id=agent_id, |
| 196 | + agent_name=agent_name, |
| 197 | + task_id=task_id, |
| 198 | + task_name=task_name, |
| 199 | + extra_headers=extra_headers, |
| 200 | + ) |
| 201 | +``` |
| 202 | + |
| 203 | +## Implementation Checklist |
| 204 | + |
| 205 | +### Phase 1: Manifest Schema & Parsing |
| 206 | +- [ ] Add `CustomHeadersConfig` to agent manifest schema |
| 207 | +- [ ] Update manifest parsing to include `custom_headers` section |
| 208 | +- [ ] Add validation for header patterns and limits |
| 209 | +- [ ] Update CLI to validate manifest header configurations |
| 210 | + |
| 211 | +### Phase 2: Core Service Layer |
| 212 | +- [ ] Add `_get_agent_header_config()` method to load from manifest |
| 213 | +- [ ] Add `_filter_headers()` method with pattern matching |
| 214 | +- [ ] Add `extra_headers` parameter to `ACPService.event_send()` |
| 215 | +- [ ] Add `extra_headers` parameter to `ACPService.task_create()` |
| 216 | +- [ ] Add `extra_headers` parameter to `ACPService.message_send()` |
| 217 | +- [ ] Add `extra_headers` parameter to `ACPService.task_cancel()` |
| 218 | + |
| 219 | +### Phase 3: ADK Public Interface |
| 220 | +- [ ] Update `adk.acp.event_send()` to accept `extra_headers` |
| 221 | +- [ ] Update `adk.acp.task_create()` to accept `extra_headers` |
| 222 | +- [ ] Update other ADK methods as needed |
| 223 | + |
| 224 | +### Phase 4: Security & Defaults |
| 225 | +- [ ] Implement secure-by-default (no headers unless explicitly allowed) |
| 226 | +- [ ] Add global fallback for agents without `custom_headers` config |
| 227 | +- [ ] Add logging for blocked/allowed headers (for debugging) |
| 228 | +- [ ] Add size and count validation |
| 229 | + |
| 230 | +### Phase 5: Testing & Documentation |
| 231 | +- [ ] Add unit tests for header filtering logic |
| 232 | +- [ ] Add integration tests with real agent manifests |
| 233 | +- [ ] Update agent development guide with header configuration |
| 234 | +- [ ] Add security best practices documentation |
| 235 | + |
| 236 | +## API Usage |
| 237 | + |
| 238 | +### Python SDK |
| 239 | + |
| 240 | +```python |
| 241 | +# New request.headers structure |
| 242 | +task = await agentex_client.acp.create_task( |
| 243 | + name="oauth-task", |
| 244 | + agent_name="data-processor", # Pass-through agent |
| 245 | + params={"content": "Process with full context"}, |
| 246 | + request={ |
| 247 | + "headers": { |
| 248 | + "x-user-email": "[email protected]", |
| 249 | + "x-user-oauth-credentials": oauth_creds_json, |
| 250 | + "x-tenant-id": "tenant-123", |
| 251 | + "authorization": "Bearer token", |
| 252 | + "x-custom-data": "important-context" |
| 253 | + } |
| 254 | + } |
| 255 | +) |
| 256 | +# All headers forwarded to pass-through agent |
| 257 | +
|
| 258 | +# Filtered agent example |
| 259 | +task = await agentex_client.acp.create_task( |
| 260 | + name="calculation", |
| 261 | + agent_name="calculator", # Filtered agent |
| 262 | + params={"expression": "2 + 2"}, |
| 263 | + request={ |
| 264 | + "headers": { |
| 265 | + "x-request-id": "req-123", # ✅ Allowed |
| 266 | + "x-user-email": "[email protected]", # ❌ Filtered out |
| 267 | + "authorization": "Bearer token" # ❌ Filtered out |
| 268 | + } |
| 269 | + } |
| 270 | +) |
| 271 | +# Only x-request-id forwarded to filtered agent |
| 272 | +``` |
| 273 | + |
| 274 | +### Direct HTTP API Usage |
| 275 | + |
| 276 | +```python |
| 277 | +# Enhanced 2b_send_demo.py with headers |
| 278 | +headers = { |
| 279 | + "Content-Type": "application/json", |
| 280 | + "x-user-email": "[email protected]", |
| 281 | + "x-user-oauth-credentials": json.dumps(oauth_creds), |
| 282 | + "x-meeting-id": "standup-2024-01-15", |
| 283 | + "x-internal-secret": "should-be-blocked" # Blocked by manifest config |
| 284 | +} |
| 285 | +
|
| 286 | +# AgentEx service filters headers based on agent's manifest |
| 287 | +response = await client.post(send_event_url, json=event_payload, headers=headers) |
| 288 | +``` |
| 289 | + |
| 290 | +### Agent Implementation |
| 291 | + |
| 292 | +```python |
| 293 | +# Agent can trust that only declared headers are received |
| 294 | +class CustomACP(BaseACPServer): |
| 295 | + async def _handle_jsonrpc(self, request: Request): |
| 296 | + # Only headers from manifest allowlist are present |
| 297 | + user_email = request.headers.get("x-user-email") # ✅ Allowed |
| 298 | + oauth_creds = request.headers.get("x-user-oauth-credentials") # ✅ Allowed |
| 299 | + admin_token = request.headers.get("x-admin-token") # ❌ None (blocked) |
| 300 | + |
| 301 | + return await super()._handle_jsonrpc(request) |
| 302 | +``` |
| 303 | + |
| 304 | +## Security Benefits |
| 305 | + |
| 306 | +### 1. **Secure by Default** |
| 307 | +- No headers forwarded unless explicitly declared in agent manifest |
| 308 | +- Agents can't accidentally receive sensitive headers |
| 309 | + |
| 310 | +### 2. **Clear Intent** |
| 311 | +- Agent manifest documents exactly what context it needs |
| 312 | +- Easy to audit what data each agent accesses |
| 313 | + |
| 314 | +### 3. **Defense in Depth** |
| 315 | +- Application layer + AgentEx service + Agent all participate in security |
| 316 | +- Malicious/buggy applications can't leak headers to wrong agents |
| 317 | + |
| 318 | +### 4. **Principle of Least Privilege** |
| 319 | +- Each agent gets only the headers it explicitly needs |
| 320 | +- System agents can get no custom headers at all |
| 321 | + |
| 322 | +## Example Security Scenarios |
| 323 | + |
| 324 | +```yaml |
| 325 | +# Financial agent - only gets user context, no admin headers |
| 326 | +agent: |
| 327 | + name: financial-advisor |
| 328 | + custom_headers: |
| 329 | + allowed_headers: |
| 330 | + - "x-user-email" |
| 331 | + - "x-user-role" # But NOT x-admin-* headers |
| 332 | +
|
| 333 | +# System monitoring - no user context at all |
| 334 | +agent: |
| 335 | + name: system-monitor |
| 336 | + # No custom_headers = completely isolated from user data |
| 337 | +
|
| 338 | +# OAuth-enabled agents - get user credentials safely |
| 339 | +agent: |
| 340 | + name: document-search |
| 341 | + custom_headers: |
| 342 | + allowed_headers: |
| 343 | + - "x-user-email" |
| 344 | + - "x-user-oauth-credentials" # Only OAuth, no other sensitive data |
| 345 | +``` |
| 346 | + |
| 347 | +## Migration Path |
| 348 | + |
| 349 | +1. **Phase 1**: Deploy AgentEx service changes (backward compatible) |
| 350 | +2. **Phase 2**: Add `custom_headers` to agent manifests that need them |
| 351 | +3. **Phase 3**: Applications can start sending custom headers |
| 352 | +4. **Phase 4**: Remove any legacy header handling in agents |
| 353 | + |
| 354 | +**Backward Compatibility**: Agents without `custom_headers` config get no headers (secure default). |
| 355 | + |
| 356 | +## Impact Assessment |
| 357 | + |
| 358 | +### Existing Systems Compatibility |
| 359 | + |
| 360 | +✅ **Authentication**: No impact - `x-agent-api-key` is handled at FastAPI level, not forwarded as custom header |
| 361 | +✅ **Tracing**: No impact - tracing is service-level, not header-based |
| 362 | +✅ **Existing Agents**: No impact - agents without `custom_headers` config get no custom headers (secure default) |
| 363 | +✅ **HTTP Infrastructure**: No impact - underlying `agents.rpc()` already supports `extra_headers` |
| 364 | + |
| 365 | +### Change Impact |
| 366 | + |
| 367 | +- **Breaking Changes**: None (secure-by-default for agents without config) |
| 368 | +- **Performance**: Minimal (header filtering is fast pattern matching) |
| 369 | +- **Security**: Major improvement (prevents accidental header leakage) |
| 370 | +- **Complexity**: Medium (requires manifest parsing + filtering logic) |
| 371 | +- **Maintainability**: High (clear intent in version-controlled manifests) |
| 372 | + |
| 373 | +This approach provides **security by design** while maintaining clean APIs and clear agent intent declaration. |
0 commit comments