Skip to content

Commit ac0d339

Browse files
feat: simplify header filtering to use simple allowlist
- Remove CustomHeadersConfig class complexity - Change custom_headers to header_allowlist as simple list[str] | None - Remove unnecessary strategy field (allowlist is the only option) - Update all service methods and tests to use simplified structure - Much cleaner manifest configuration: # Old custom_headers: strategy: allowlist allowed_headers: [...] # New header_allowlist: [...] Breaking change: Agent manifest header configuration simplified
1 parent e1190c8 commit ac0d339

File tree

4 files changed

+69
-93
lines changed

4 files changed

+69
-93
lines changed

src/agentex/lib/core/services/adk/acp/acp.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from agentex.lib.core.tracing.tracer import AsyncTracer
66
from agentex.lib.utils.logging import make_logger
77
from agentex.lib.utils.temporal import heartbeat_if_in_workflow
8-
from agentex.lib.types.agent_configs import CustomHeadersConfig
8+
# No longer need CustomHeadersConfig import
99
from agentex.types.event import Event
1010
from agentex.types.task import Task
1111
from agentex.types.task_message import TaskMessage
@@ -24,64 +24,64 @@ def __init__(
2424
self._agentex_client = agentex_client
2525
self._tracer = tracer
2626

27-
async def _get_agent_header_config(
27+
async def _get_agent_header_allowlist(
2828
self, agent_name: str | None = None, agent_id: str | None = None
29-
) -> CustomHeadersConfig | None:
29+
) -> list[str] | None:
3030
"""
31-
Get agent's header configuration from manifest.
31+
Get agent's header allowlist from manifest.
3232
3333
Returns None for pass-through behavior (all headers forwarded).
34-
Returns CustomHeadersConfig only if agent has specific filtering requirements.
34+
Returns list[str] of allowed header patterns if agent has filtering requirements.
3535
3636
Note: In a production system, this would load the agent's manifest from a registry/database
37-
based on agent_name/agent_id and return the custom_headers configuration. For now, we implement
37+
based on agent_name/agent_id and return the header_allowlist. For now, we implement
3838
pass-through by default as requested.
3939
40-
TODO: Integrate with agent registry to load manifest-based header configurations
40+
TODO: Integrate with agent registry to load manifest-based header allowlists
4141
"""
4242
# Pass-through by default - forward all headers unless agent has specific filtering config
4343
# In production, this would query an agent registry/database:
4444
# 1. Look up agent by name/id
4545
# 2. Get agent's manifest configuration
46-
# 3. Return manifest.agent.custom_headers
46+
# 3. Return manifest.agent.header_allowlist
4747
return None
4848

4949
def _filter_headers(
5050
self,
5151
headers: dict[str, str] | None,
52-
config: CustomHeadersConfig | None
52+
allowlist: list[str] | None
5353
) -> dict[str, str]:
5454
"""
55-
Filter headers based on agent's configuration.
55+
Filter headers based on agent's allowlist.
5656
57-
Pass-through by default: if no config provided, all headers are forwarded.
58-
If config exists, only allowlisted headers are forwarded.
57+
Pass-through by default: if no allowlist provided, all headers are forwarded.
58+
If allowlist exists, only matching headers are forwarded.
5959
6060
Args:
6161
headers: Headers to filter
62-
config: Agent's header configuration (None = pass-through all)
62+
allowlist: Agent's header allowlist (None = pass-through all)
6363
6464
Returns:
6565
Filtered headers dictionary
6666
"""
6767
if not headers:
6868
return {}
6969

70-
# Pass-through behavior: if no config, forward all headers
71-
if config is None:
72-
logger.info("No header filtering config found, passing through all %d headers", len(headers))
70+
# Pass-through behavior: if no allowlist, forward all headers
71+
if allowlist is None:
72+
logger.info("No header allowlist found, passing through all %d headers", len(headers))
7373
return headers
7474

7575
# Apply filtering based on allowlist
76-
if not config.allowed_headers:
77-
logger.info("Empty allowlist in config, blocking all headers")
76+
if not allowlist:
77+
logger.info("Empty allowlist, blocking all headers")
7878
return {}
7979

8080
filtered = {}
8181
for header_name, header_value in headers.items():
8282
# Check against allowlist patterns (case-insensitive)
8383
header_allowed = False
84-
for pattern in config.allowed_headers:
84+
for pattern in allowlist:
8585
if fnmatch.fnmatch(header_name.lower(), pattern.lower()):
8686
header_allowed = True
8787
break
@@ -120,8 +120,8 @@ async def task_create(
120120

121121
# Extract headers from request and filter them
122122
headers = request.get("headers") if request else None
123-
header_config = await self._get_agent_header_config(agent_name, agent_id)
124-
filtered_headers = self._filter_headers(headers, header_config)
123+
header_allowlist = await self._get_agent_header_allowlist(agent_name, agent_id)
124+
filtered_headers = self._filter_headers(headers, header_allowlist)
125125

126126
if agent_name:
127127
json_rpc_response = await self._agentex_client.agents.rpc_by_name(
@@ -178,8 +178,8 @@ async def message_send(
178178

179179
# Extract headers from request and filter them
180180
headers = request.get("headers") if request else None
181-
header_config = await self._get_agent_header_config(agent_name, agent_id)
182-
filtered_headers = self._filter_headers(headers, header_config)
181+
header_allowlist = await self._get_agent_header_allowlist(agent_name, agent_id)
182+
filtered_headers = self._filter_headers(headers, header_allowlist)
183183

184184
if agent_name:
185185
json_rpc_response = await self._agentex_client.agents.rpc_by_name(
@@ -245,8 +245,8 @@ async def event_send(
245245

246246
# Extract headers from request and filter them
247247
headers = request.get("headers") if request else None
248-
header_config = await self._get_agent_header_config(agent_name, agent_id)
249-
filtered_headers = self._filter_headers(headers, header_config)
248+
header_allowlist = await self._get_agent_header_allowlist(agent_name, agent_id)
249+
filtered_headers = self._filter_headers(headers, header_allowlist)
250250

251251
if agent_name:
252252
json_rpc_response = await self._agentex_client.agents.rpc_by_name(
@@ -327,8 +327,8 @@ async def task_cancel(
327327

328328
# Extract headers from request and filter them
329329
headers = request.get("headers") if request else None
330-
header_config = await self._get_agent_header_config(agent_name, agent_id)
331-
filtered_headers = self._filter_headers(headers, header_config)
330+
header_allowlist = await self._get_agent_header_allowlist(agent_name, agent_id)
331+
filtered_headers = self._filter_headers(headers, header_allowlist)
332332

333333
# Build params for the agent (task identification)
334334
params = {}

src/agentex/lib/sdk/config/agent_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pydantic import Field
44

5-
from agentex.lib.types.agent_configs import TemporalConfig, TemporalWorkflowConfig, CustomHeadersConfig
5+
from agentex.lib.types.agent_configs import TemporalConfig, TemporalWorkflowConfig
66
from agentex.lib.types.credentials import CredentialMapping
77
from agentex.lib.utils.logging import make_logger
88
from agentex.lib.utils.model_utils import BaseModel
@@ -28,8 +28,8 @@ class AgentConfig(BaseModel):
2828
temporal: TemporalConfig | None = Field(
2929
default=None, description="Temporal workflow configuration for this agent"
3030
)
31-
custom_headers: CustomHeadersConfig | None = Field(
32-
default=None, description="Configuration for custom HTTP headers that this agent can receive"
31+
header_allowlist: list[str] | None = Field(
32+
default=None, description="List of header patterns this agent is allowed to receive. If not provided, all headers are forwarded."
3333
)
3434

3535
def is_temporal_agent(self) -> bool:

src/agentex/lib/types/agent_configs.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from typing import Literal
21
from pydantic import BaseModel, Field, field_validator, model_validator
32

43

@@ -38,26 +37,7 @@ class TemporalWorkerConfig(BaseModel):
3837
)
3938

4039

41-
class CustomHeadersConfig(BaseModel):
42-
"""
43-
Configuration for custom HTTP header filtering for agents.
44-
45-
When present in agent configuration, only allowlisted headers are forwarded.
46-
When absent, all headers are passed through (pass-through by default).
47-
48-
Attributes:
49-
strategy: Header filtering strategy (currently only 'allowlist' supported)
50-
allowed_headers: List of header patterns that are allowed to be forwarded
51-
"""
52-
53-
strategy: Literal["allowlist"] = Field(
54-
default="allowlist",
55-
description="Header filtering strategy. Only 'allowlist' is currently supported."
56-
)
57-
allowed_headers: list[str] = Field(
58-
default_factory=list,
59-
description="List of header name patterns that are allowed to be forwarded to this agent. Supports wildcards with fnmatch."
60-
)
40+
# Removed CustomHeadersConfig - now using simple list[str] for header_allowlist
6141

6242

6343
class TemporalConfig(BaseModel):

tests/test_header_filtering.py

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,46 @@
44
import fnmatch
55
import pytest
66

7-
from agentex.lib.types.agent_configs import CustomHeadersConfig
7+
# No longer need CustomHeadersConfig - using simple lists now
88

99

10-
class TestCustomHeadersConfig:
11-
"""Test CustomHeadersConfig functionality."""
10+
class TestHeaderAllowlist:
11+
"""Test header allowlist functionality."""
1212

13-
def test_custom_headers_config_creation(self) -> None:
14-
"""Test that CustomHeadersConfig can be created with defaults."""
15-
config = CustomHeadersConfig()
16-
17-
assert config.strategy == "allowlist"
18-
assert config.allowed_headers == []
19-
20-
def test_custom_headers_config_with_headers(self) -> None:
21-
"""Test CustomHeadersConfig with allowed headers."""
22-
config = CustomHeadersConfig(
23-
allowed_headers=["x-user-email", "x-tenant-id"]
24-
)
25-
26-
assert config.strategy == "allowlist"
27-
assert config.allowed_headers == ["x-user-email", "x-tenant-id"]
13+
def test_allowlist_creation(self) -> None:
14+
"""Test that allowlists can be created as simple lists."""
15+
allowlist = ["x-user-email", "x-tenant-id"]
16+
assert len(allowlist) == 2
17+
assert "x-user-email" in allowlist
18+
assert "x-tenant-id" in allowlist
19+
20+
def test_empty_allowlist(self) -> None:
21+
"""Test empty allowlist behavior."""
22+
allowlist: list[str] = []
23+
assert len(allowlist) == 0
2824

2925

3026
def filter_headers_standalone(
3127
headers: dict[str, str] | None,
32-
config: CustomHeadersConfig | None
28+
allowlist: list[str] | None
3329
) -> dict[str, str]:
3430
"""Standalone header filtering function matching the production implementation."""
3531
if not headers:
3632
return {}
3733

38-
# Pass-through behavior: if no config, forward all headers
39-
if config is None:
34+
# Pass-through behavior: if no allowlist, forward all headers
35+
if allowlist is None:
4036
return headers
4137

4238
# Apply filtering based on allowlist
43-
if not config.allowed_headers:
39+
if not allowlist:
4440
return {}
4541

4642
filtered = {}
4743
for header_name, header_value in headers.items():
4844
# Check against allowlist patterns (case-insensitive)
4945
header_allowed = False
50-
for pattern in config.allowed_headers:
46+
for pattern in allowlist:
5147
if fnmatch.fnmatch(header_name.lower(), pattern.lower()):
5248
header_allowed = True
5349
break
@@ -63,42 +59,42 @@ class TestHeaderFiltering:
6359

6460
def test_filter_headers_no_headers(self) -> None:
6561
"""Test header filtering with no input headers."""
66-
config = CustomHeadersConfig(allowed_headers=["x-user-email"])
67-
result = filter_headers_standalone(None, config)
62+
allowlist = ["x-user-email"]
63+
result = filter_headers_standalone(None, allowlist)
6864
assert result == {}
6965

70-
result = filter_headers_standalone({}, config)
66+
result = filter_headers_standalone({}, allowlist)
7167
assert result == {}
7268

7369
def test_filter_headers_pass_through_by_default(self) -> None:
74-
"""Test header filtering with no config (pass-through behavior)."""
70+
"""Test header filtering with no allowlist (pass-through behavior)."""
7571
headers = {
7672
"x-user-email": "[email protected]",
7773
"x-admin-token": "secret",
7874
"authorization": "Bearer token",
7975
"x-custom-header": "value"
8076
}
8177
result = filter_headers_standalone(headers, None)
82-
# All headers should pass through when no config is provided
78+
# All headers should pass through when no allowlist is provided
8379
assert result == headers
8480

8581
def test_filter_headers_empty_allowlist(self) -> None:
86-
"""Test header filtering with empty allowed headers."""
87-
config = CustomHeadersConfig(allowed_headers=[])
82+
"""Test header filtering with empty allowlist."""
83+
allowlist: list[str] = []
8884
headers = {"x-user-email": "[email protected]", "x-admin-token": "secret"}
89-
result = filter_headers_standalone(headers, config)
85+
result = filter_headers_standalone(headers, allowlist)
9086
assert result == {}
9187

9288
def test_filter_headers_allowed_headers(self) -> None:
93-
"""Test header filtering with allowed headers."""
94-
config = CustomHeadersConfig(allowed_headers=["x-user-email", "x-tenant-id"])
89+
"""Test header filtering with allowlist."""
90+
allowlist = ["x-user-email", "x-tenant-id"]
9591
headers = {
9692
"x-user-email": "[email protected]",
9793
"x-tenant-id": "tenant123",
9894
"x-admin-token": "secret", # Should be filtered out
9995
"content-type": "application/json" # Should be filtered out
10096
}
101-
result = filter_headers_standalone(headers, config)
97+
result = filter_headers_standalone(headers, allowlist)
10298

10399
expected = {
104100
"x-user-email": "[email protected]",
@@ -108,14 +104,14 @@ def test_filter_headers_allowed_headers(self) -> None:
108104

109105
def test_filter_headers_case_insensitive_patterns(self) -> None:
110106
"""Test header filtering with case-insensitive pattern matching."""
111-
config = CustomHeadersConfig(allowed_headers=["X-User-Email", "x-tenant-*"])
107+
allowlist = ["X-User-Email", "x-tenant-*"]
112108
headers = {
113109
"x-user-email": "[email protected]", # Should match X-User-Email
114110
"X-TENANT-ID": "tenant123", # Should match x-tenant-*
115111
"x-tenant-name": "acme", # Should match x-tenant-*
116112
"x-admin-token": "secret" # Should be filtered out
117113
}
118-
result = filter_headers_standalone(headers, config)
114+
result = filter_headers_standalone(headers, allowlist)
119115

120116
expected = {
121117
"x-user-email": "[email protected]",
@@ -126,7 +122,7 @@ def test_filter_headers_case_insensitive_patterns(self) -> None:
126122

127123
def test_filter_headers_wildcard_patterns(self) -> None:
128124
"""Test header filtering with wildcard patterns."""
129-
config = CustomHeadersConfig(allowed_headers=["x-user-*", "authorization"])
125+
allowlist = ["x-user-*", "authorization"]
130126
headers = {
131127
"x-user-id": "123",
132128
"x-user-email": "[email protected]",
@@ -135,7 +131,7 @@ def test_filter_headers_wildcard_patterns(self) -> None:
135131
"x-system-info": "blocked", # Should be filtered out
136132
"content-type": "application/json" # Should be filtered out
137133
}
138-
result = filter_headers_standalone(headers, config)
134+
result = filter_headers_standalone(headers, allowlist)
139135

140136
expected = {
141137
"x-user-id": "123",
@@ -147,7 +143,7 @@ def test_filter_headers_wildcard_patterns(self) -> None:
147143

148144
def test_filter_headers_complex_patterns(self) -> None:
149145
"""Test header filtering with complex fnmatch patterns."""
150-
config = CustomHeadersConfig(allowed_headers=["x-tenant-*", "x-user-[abc]*", "auth*"])
146+
allowlist = ["x-tenant-*", "x-user-[abc]*", "auth*"]
151147
headers = {
152148
"x-tenant-id": "tenant1", # Matches x-tenant-*
153149
"x-tenant-name": "acme", # Matches x-tenant-*
@@ -158,7 +154,7 @@ def test_filter_headers_complex_patterns(self) -> None:
158154
"authenticate": "digest", # Matches auth*
159155
"content-type": "json", # No match
160156
}
161-
result = filter_headers_standalone(headers, config)
157+
result = filter_headers_standalone(headers, allowlist)
162158

163159
expected = {
164160
"x-tenant-id": "tenant1",
@@ -172,7 +168,7 @@ def test_filter_headers_complex_patterns(self) -> None:
172168

173169
def test_filter_headers_all_types(self) -> None:
174170
"""Test that any header type can be allowed, not just x- prefixed ones."""
175-
config = CustomHeadersConfig(allowed_headers=["authorization", "accept-language", "custom-*"])
171+
allowlist = ["authorization", "accept-language", "custom-*"]
176172
headers = {
177173
"authorization": "Bearer token",
178174
"accept-language": "en-US",
@@ -181,7 +177,7 @@ def test_filter_headers_all_types(self) -> None:
181177
"content-type": "application/json", # Should be blocked
182178
"x-blocked": "value" # Should be blocked
183179
}
184-
result = filter_headers_standalone(headers, config)
180+
result = filter_headers_standalone(headers, allowlist)
185181

186182
expected = {
187183
"authorization": "Bearer token",

0 commit comments

Comments
 (0)