Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0f5e415
feat: implement secure token handling via request_state
timof1308 Jan 19, 2026
85ed69f
feat: Add warning for non-primitive state values that may not seriali…
timof1308 Jan 19, 2026
574a4eb
refactor: Introduce `HeaderProvider` type and modularize header gener…
timof1308 Jan 19, 2026
e83ac6a
feat: Add strict mode to `create_session_state_header_provider` to ra…
timof1308 Jan 19, 2026
67b01ad
feat: Introduce `state_header_strict` config for strict state header …
timof1308 Jan 19, 2026
800a95e
applied autoformat.sh
timof1308 Jan 19, 2026
b2ba3b0
feat: Add RFC 7230 compliant header validation and sanitization
timof1308 Jan 19, 2026
e0ab9e0
refactor: add future annotations import to types and internal modules.
timof1308 Jan 19, 2026
81b8ede
refactor: Publicize header name validation, refine header name regex,…
timof1308 Jan 19, 2026
15452b5
applied autoformat.sh
timof1308 Jan 19, 2026
f7babb3
applied ./autoformat.sh
timof1308 Jan 19, 2026
69ee1eb
apply autoformat after rebase
timof1308 Jan 19, 2026
e9136a2
test(mcp): Fix Mock InvocationContext to include request_state
timof1308 Jan 20, 2026
fd71217
Fix test_fast_api.py mocks to match new run_async signature
timof1308 Jan 21, 2026
eb5e17b
apply autoformat.sh
timof1308 Jan 21, 2026
53e119a
fix(mcp): Remove duplicate create_session_state_header_provider
timof1308 Jan 21, 2026
e5d077a
docs: Clarify `ReadOnlyContext.state` property as `MappingProxyType` …
timof1308 Jan 21, 2026
ea546c9
docs: update `ReadOnlyContext.state` to use `MappingProxyType` with a…
timof1308 Jan 21, 2026
aaf58b1
Remove the `_HEADER_NAME_FORBIDDEN` constant
timof1308 Jan 21, 2026
b5cf6fb
remove unused import of `get_mcp_auth_headers`
timof1308 Jan 22, 2026
ad3fcd7
fix(mcp): Restore McpTool.__init__ body and fix method name during re…
timof1308 Mar 10, 2026
8df968e
style: Fix pyink formatting in adk_web_server.py
timof1308 Mar 10, 2026
785b34a
test(cli): update fast api mocks for request_state
timof1308 Mar 16, 2026
e43e32f
chore(headers): align branch-added copyright years
timof1308 Mar 16, 2026
a675aaf
chore(samples): drop unrelated gepa formatting churn
timof1308 Mar 16, 2026
d3ef532
fix(mcp): add CRLF injection protection and remove duplicate code
timof1308 Mar 26, 2026
bc629bd
fix(mcp): enhance CRLF injection protection in header handling
timof1308 Apr 1, 2026
ddeb65e
refactor(mcp): remove dead code from _internal.py and update tests
timof1308 Apr 2, 2026
68180ed
chore: remove unused Mapping import from readonly_context.py
timof1308 Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/google/adk/agents/invocation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ class InvocationContext(BaseModel):
agent_states: dict[str, dict[str, Any]] = Field(default_factory=dict)
"""The state of the agent for this invocation."""

request_state: dict[str, Any] = Field(default_factory=dict)
"""The ephemeral state of the request.

This state is not persisted to the session and is only available for the
current invocation. It is used to pass sensitive information like tokens
that should not be stored in the session state.
"""

end_of_agents: dict[str, bool] = Field(default_factory=dict)
"""The end of agent status for each agent in this invocation."""

Expand Down
16 changes: 14 additions & 2 deletions src/google/adk/agents/readonly_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

from collections import ChainMap
from types import MappingProxyType
from typing import Any
from typing import Optional
Expand Down Expand Up @@ -52,8 +53,19 @@ def agent_name(self) -> str:

@property
def state(self) -> MappingProxyType[str, Any]:
"""The state of the current session. READONLY field."""
return MappingProxyType(self._invocation_context.session.state)
"""The state of the current session. READONLY field.

Note: This property returns a merged view of ephemeral request_state and
persistent session.state using ChainMap. Changes to the underlying
request_state or session.state dictionaries will be reflected through
this view, but direct writes through this property are prevented.
"""
return MappingProxyType(
ChainMap(
self._invocation_context.request_state,
self._invocation_context.session.state,
)
)

@property
def session(self) -> Session:
Expand Down
7 changes: 4 additions & 3 deletions src/google/adk/cli/adk_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ class RunAgentRequest(common.BaseModel):
state_delta: Optional[dict[str, Any]] = None
# for long-running function resume requests (e.g., OAuth callback)
function_call_event_id: Optional[str] = None
request_state: Optional[dict[str, Any]] = None
# for resume long-running functions
invocation_id: Optional[str] = None

Expand Down Expand Up @@ -944,9 +945,7 @@ async def version() -> dict[str, str]:
return {
"version": __version__,
"language": "python",
"language_version": (
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
),
"language_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
}

@app.get("/list-apps")
Expand Down Expand Up @@ -1824,6 +1823,7 @@ async def run_agent(req: RunAgentRequest) -> list[Event]:
new_message=req.new_message,
state_delta=req.state_delta,
invocation_id=req.invocation_id,
request_state=req.request_state,
)
) as agen:
events = [event async for event in agen]
Expand Down Expand Up @@ -1866,6 +1866,7 @@ async def event_generator():
state_delta=req.state_delta,
run_config=RunConfig(streaming_mode=stream_mode),
invocation_id=req.invocation_id,
request_state=req.request_state,
)
) as agen:
try:
Expand Down
39 changes: 33 additions & 6 deletions src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ async def run_async(
invocation_id: Optional[str] = None,
new_message: Optional[types.Content] = None,
state_delta: Optional[dict[str, Any]] = None,
request_state: Optional[dict[str, Any]] = None,
run_config: Optional[RunConfig] = None,
) -> AsyncGenerator[Event, None]:
"""Main entry method to run the agent in this runner.
Expand All @@ -525,6 +526,7 @@ async def run_async(
interrupted invocation.
new_message: A new message to append to the session.
state_delta: Optional state changes to apply to the session.
request_state: Optional ephemeral state for the request.
run_config: The run config for the agent.

Yields:
Expand Down Expand Up @@ -560,18 +562,32 @@ async def _run_with_trace(
is_resumable = (
self.resumability_config and self.resumability_config.is_resumable
)
if not is_resumable and not new_message:
raise ValueError(
'Running an agent requires a new_message or a resumable app. '
f'Session: {session_id}, User: {user_id}'
if invocation_id:
if not is_resumable:
raise ValueError(
f'invocation_id: {invocation_id} is provided but the app is not'
' resumable.'
)
invocation_context = await self._setup_context_for_resumed_invocation(
session=session,
new_message=new_message,
invocation_id=invocation_id,
run_config=run_config,
state_delta=state_delta,
request_state=request_state,
)

if not is_resumable:
elif not is_resumable:
if not new_message:
raise ValueError(
'Running an agent requires a new_message or a resumable app. '
f'Session: {session_id}, User: {user_id}'
)
invocation_context = await self._setup_context_for_new_invocation(
session=session,
new_message=new_message,
run_config=run_config,
state_delta=state_delta,
request_state=request_state,
)
else:
invocation_id = self._resolve_invocation_id(
Expand All @@ -583,6 +599,7 @@ async def _run_with_trace(
new_message=new_message,
run_config=run_config,
state_delta=state_delta,
request_state=request_state,
)
else:
invocation_context = (
Expand All @@ -592,6 +609,7 @@ async def _run_with_trace(
invocation_id=invocation_id,
run_config=run_config,
state_delta=state_delta,
request_state=request_state,
)
)
if invocation_context.end_of_agents.get(
Expand Down Expand Up @@ -1294,6 +1312,7 @@ async def _setup_context_for_new_invocation(
new_message: types.Content,
run_config: RunConfig,
state_delta: Optional[dict[str, Any]],
request_state: Optional[dict[str, Any]] = None,
) -> InvocationContext:
"""Sets up the context for a new invocation.

Expand All @@ -1302,6 +1321,7 @@ async def _setup_context_for_new_invocation(
new_message: The new message to process and append to the session.
run_config: The run config of the agent.
state_delta: Optional state changes to apply to the session.
request_state: Optional ephemeral state for the request.

Returns:
The invocation context for the new invocation.
Expand All @@ -1311,6 +1331,7 @@ async def _setup_context_for_new_invocation(
session,
new_message=new_message,
run_config=run_config,
request_state=request_state,
)
# Step 2: Handle new message, by running callbacks and appending to
# session.
Expand All @@ -1333,6 +1354,7 @@ async def _setup_context_for_resumed_invocation(
invocation_id: Optional[str],
run_config: RunConfig,
state_delta: Optional[dict[str, Any]],
request_state: Optional[dict[str, Any]] = None,
) -> InvocationContext:
"""Sets up the context for a resumed invocation.

Expand All @@ -1342,6 +1364,7 @@ async def _setup_context_for_resumed_invocation(
invocation_id: The invocation id to resume.
run_config: The run config of the agent.
state_delta: Optional state changes to apply to the session.
request_state: Optional ephemeral state for the request.

Returns:
The invocation context for the resumed invocation.
Expand All @@ -1367,6 +1390,7 @@ async def _setup_context_for_resumed_invocation(
new_message=user_message,
run_config=run_config,
invocation_id=invocation_id,
request_state=request_state,
)
# Step 3: Maybe handle new message.
if new_message:
Expand Down Expand Up @@ -1415,6 +1439,7 @@ def _new_invocation_context(
new_message: Optional[types.Content] = None,
live_request_queue: Optional[LiveRequestQueue] = None,
run_config: Optional[RunConfig] = None,
request_state: Optional[dict[str, Any]] = None,
) -> InvocationContext:
"""Creates a new invocation context.

Expand All @@ -1424,6 +1449,7 @@ def _new_invocation_context(
new_message: The new message for the context.
live_request_queue: The live request queue for the context.
run_config: The run config for the context.
request_state: The ephemeral state for the request.

Returns:
The new invocation context.
Expand Down Expand Up @@ -1458,6 +1484,7 @@ def _new_invocation_context(
live_request_queue=live_request_queue,
run_config=run_config,
resumability_config=self.resumability_config,
request_state=request_state if request_state is not None else {},
)

def _new_invocation_context_for_live(
Expand Down
2 changes: 2 additions & 0 deletions src/google/adk/tools/mcp_tool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .mcp_session_manager import StreamableHTTPConnectionParams
from .mcp_tool import MCPTool
from .mcp_tool import McpTool
from .mcp_toolset import create_session_state_header_provider
from .mcp_toolset import MCPToolset
from .mcp_toolset import McpToolset

Expand All @@ -32,6 +33,7 @@
'MCPTool',
'McpToolset',
'MCPToolset',
'create_session_state_header_provider',
'SseConnectionParams',
'StdioConnectionParams',
'StreamableHTTPConnectionParams',
Expand Down
Loading