fix(a2a): validate session_id at entry of _handle_session_close before devcontainer cleanup #11098

Merged
HAL9000 merged 7 commits from feature/9250-fix-a2a-session-close into master 2026-06-17 16:38:46 +00:00
8 changed files with 53 additions and 12 deletions
+1
View File
1
@@ -130,3 +130,4 @@ Below are some specific details of individual PR contributions.
* HAL 9000 has contributed the PyYAML security hardening fix (PR #11017 / issue #11012): added `pyyaml>=6.0.3` as an explicit runtime dependency in `pyproject.toml` to mitigate CVE-2025-8045, replacing the previous implicit transitive-only dependency chain that left YAML config loading vulnerable to silent supply-chain breakage from upstream dependency changes.
* HAL 9000 has contributed the plan explain structured alternatives format fix (PR #11090): updated `_build_explain_dict()` in `src/cleveragents/cli/commands/plan.py` to convert the `alternatives_considered` list into structured objects with `index` (1-based), `description`, and `chosen` fields in the `alternatives` output key, aligning the `agents plan explain` output with the spec-required format.
* HAL 9000 has contributed the plan tree JSON/YAML spec-compliant envelope fix (issue #11041): wrapped `agents plan tree` JSON and YAML output in the spec-required command envelope (`command`, `status`, `exit_code`, `data`, `timing`, `messages`), updated BDD step definitions to validate envelope structure, and removed the `@tdd_expected_fail` tag from the previously-failing JSON tree format test (issue #4254).
* HAL 9000 has contributed the a2a session_id validation fix (PR #11098 / issue #9250): moved the session_id validation guard to the top of `_handle_session_close()` in `A2aLocalFacade`, closing the validation bypass path where empty or null session IDs could slip through to devcontainer cleanup when `SessionService` was not wired.
+19 -3
View File
1
@@ -20,11 +20,27 @@ Feature: A2A local facade coverage — uncovered handler and edge-case paths
Then the facade-cov response status should be "ok"
And the facade-cov response data key "status" should equal "closed"
Scenario: Session close with empty session_id and no service
# -------------------------------------------------------------------
# Session close — session_id validation guard (lines 321-324)
# -------------------------------------------------------------------
@tdd_issue @tdd_issue_9250
Scenario: Session close with empty session_id and no service raises ValueError error
Given a facade-cov facade with no services
When I dispatch facade-cov operation "session.close" with params {"session_id": ""}
Then the facade-cov response status should be "ok"
And the facade-cov response data key "status" should equal "closed"
Then the facade-cov response status should be "error"
@tdd_issue @tdd_issue_9250
Scenario: Session close with missing session_id key and no service raises ValueError error
Given a facade-cov facade with no services
When I dispatch facade-cov operation "session.close" with params {}
Then the facade-cov response status should be "error"
@tdd_issue @tdd_issue_9250
Scenario: Session close with empty session_id and wired service raises ValueError error
Given a facade-cov facade with a mock SessionService
When I dispatch facade-cov operation "session.close" with params {"session_id": ""}
Then the facade-cov response status should be "error"
# -------------------------------------------------------------------
# Plan cancel — with service wired (lines 501-502)
+9
View File
@@ -34,6 +34,15 @@ Feature: A2A local facade wiring to live services
When I dispatch wired operation "session.close" with params {}
Then the wired response status should be "error"
# ---------------------------------------------------------------
# Validate session_id at entry — no service scenario (PR #9250)
# ---------------------------------------------------------------
Scenario: session.close without session_id and no service returns error
Given a wired A2aLocalFacade with no services
When I dispatch wired operation "session.close" with params {"session_id": ""}
Then the wired response status should be "error"
# ---------------------------------------------------------------
# Plan lifecycle wiring
# ---------------------------------------------------------------
+2 -2
View File
@@ -42,7 +42,7 @@ Feature: Consolidated Misc
Scenario: Dispatch session.close returns status closed
Given a new A2aLocalFacade with no services
When I dispatch operation "session.close" with params {}
When I dispatch operation "session.close" with params {"session_id": "01M6SM0KESESS0N00000000000"}
Then the response status should be "ok"
And response data key "status" equals "closed"
@@ -563,7 +563,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke A2A session close returns closed status
Given a m6 smoke test runner
And a m6 smoke A2A local facade
When I m6 smoke dispatch "session.close" with params {}
When I m6 smoke dispatch "session.close" with params {"session_id": "01M6SM0KESESS0N00000000000"}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data "status" should equal "closed"
+1 -1
View File
@@ -35,7 +35,7 @@ Feature: M6 autonomy acceptance smoke tests
And the m6 smoke response data should contain key "status"
Scenario: M6 smoke A2A session close returns closed status
When I m6 smoke dispatch "session.close" with params {}
When I m6 smoke dispatch "session.close" with params {"session_id": "01M6SM0KESESS0N00000000000"}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data "status" should equal "closed"
1
@@ -297,6 +297,14 @@ def step_fc_data_no_key(context: Context, key: str) -> None:
)
@then(r"the facade-cov response error should contain 'session_id is required'")
def step_fc_error_contains_session_id(context: Context) -> None:
assert context.fc_response.error is not None, "Expected an error in the response"
assert "session_id is required" in context.fc_response.error.message, (
f"Expected error containing 'session_id is required', got: {context.fc_response.error.message}"
)
@then(r"the facade-cov response error should not be None")
def step_fc_error_not_none(context: Context) -> None:
assert context.fc_response.error is not None, "Expected an error in the response"
+4 -1
View File
@@ -48,8 +48,11 @@ def facade_session() -> None:
resp_create = facade.dispatch(A2aRequest(method="session.create", params={}))
assert resp_create.result is not None, f"Expected ok, got {resp_create.status}"
assert "session_id" in resp_create.result
session_id = resp_create.result["session_id"]
resp_close = facade.dispatch(A2aRequest(method="session.close", params={}))
resp_close = facade.dispatch(
A2aRequest(method="session.close", params={"session_id": session_id})
)
assert resp_close.result is not None
assert resp_close.result["status"] == "closed"
+9 -5
View File
1
@@ -364,6 +364,13 @@ class A2aLocalFacade:
def _handle_session_close(self, params: dict[str, Any]) -> dict[str, Any]:
session_id = params.get("session_id", "")
# Validate session_id before any cleanup or service operations.
# This prevents best-effort devcontainer cleanup from running on
# an invalid or omitted session identifier, which could trigger
# incorrect container lifecycle operations on wrong/unknown sessions.
if not session_id:
raise ValueError("session_id is required")
svc = self._session_service
if svc is None:
# R7-F4 fix: still run container cleanup even without a
@@ -371,8 +378,6 @@ class A2aLocalFacade:
self._cleanup_session_devcontainers(session_id)
return {"status": "closed"}
if not session_id:
raise ValueError("session_id is required")
svc.delete(session_id)
# R7-F4 fix: run container cleanup after session deletion.
@@ -503,10 +508,9 @@ class A2aLocalFacade:
"""Best-effort stop of devcontainers associated with a session.
Failures are logged but never propagated so session close always
succeeds.
succeeds. ``session_id`` is guaranteed non-empty by the entry
validation in :meth:`_handle_session_close`.
"""
if not session_id:
return
try:
from cleveragents.application.services.cleanup_service import (
CleanupService,