mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(mcp): send X-Agent-Token + X-Agent-Team from flow/do servers
flow_server._build_headers and do_server._build_headers constructed only X-Agent-ID/Role/Correlation-ID, omitting X-Agent-Token and X-Agent-Team (unlike ApiClient._get_agent_headers used by the other MCP servers). Latent since the gateway refactor — surfaced when ROBOCO_AGENT_AUTH_REQUIRED=true was armed on the NAS, 401-ing every flow/do verb with 'Missing X-Agent-Token header'. Add both headers (mirroring ApiClient) so the HMAC gate passes. Tests assert the headers are now injected.
This commit is contained in:
+13
-1
@@ -23,6 +23,8 @@ import httpx
|
||||
import structlog
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from roboco.agents_config import get_agent_team
|
||||
|
||||
ORCHESTRATOR_URL = os.environ.get(
|
||||
"ROBOCO_ORCHESTRATOR_URL",
|
||||
"http://roboco-orchestrator:8000",
|
||||
@@ -226,11 +228,21 @@ def _build_headers() -> dict[str, str]:
|
||||
orchestrator's middleware can bind it to structlog and the audit row,
|
||||
and the envelope echoes it back to the agent.
|
||||
"""
|
||||
return {
|
||||
# X-Agent-Token + X-Agent-Team must travel with every do verb or the
|
||||
# API's ROBOCO_AGENT_AUTH_REQUIRED gate 401s — mirrors flow_server and
|
||||
# the ApiClient header path used by the other MCP servers.
|
||||
headers = {
|
||||
"X-Agent-ID": AGENT_ID,
|
||||
"X-Agent-Role": AGENT_ROLE,
|
||||
"X-Correlation-ID": str(uuid.uuid4()),
|
||||
}
|
||||
team = get_agent_team(AGENT_ID)
|
||||
if team:
|
||||
headers["X-Agent-Team"] = team
|
||||
token = os.environ.get("ROBOCO_AGENT_TOKEN")
|
||||
if token:
|
||||
headers["X-Agent-Token"] = token
|
||||
return headers
|
||||
|
||||
|
||||
def _post(path: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
@@ -25,6 +25,7 @@ import structlog
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import BeforeValidator
|
||||
|
||||
from roboco.agents_config import get_agent_team
|
||||
from roboco.foundation.policy.content.validators import coerce_str_list
|
||||
|
||||
# A ``list[str]`` field that tolerates the Claude SDK's XML-ish tool-input
|
||||
@@ -260,11 +261,22 @@ def _build_headers() -> dict[str, str]:
|
||||
every log line and the audit row carry the same id and the agent
|
||||
receives it back on the envelope.
|
||||
"""
|
||||
return {
|
||||
# X-Agent-Token (HMAC over id:role:team, issued by the orchestrator at
|
||||
# spawn) and X-Agent-Team must travel with every flow verb or the API's
|
||||
# ROBOCO_AGENT_AUTH_REQUIRED gate 401s with "Missing X-Agent-Token" —
|
||||
# the same headers ApiClient injects for the other MCP servers.
|
||||
headers = {
|
||||
"X-Agent-ID": AGENT_ID,
|
||||
"X-Agent-Role": AGENT_ROLE,
|
||||
"X-Correlation-ID": str(uuid.uuid4()),
|
||||
}
|
||||
team = get_agent_team(AGENT_ID)
|
||||
if team:
|
||||
headers["X-Agent-Team"] = team
|
||||
token = os.environ.get("ROBOCO_AGENT_TOKEN")
|
||||
if token:
|
||||
headers["X-Agent-Token"] = token
|
||||
return headers
|
||||
|
||||
|
||||
def _post(path: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
@@ -89,6 +89,36 @@ def test_note_with_scope_reflect(do_module: Any) -> None:
|
||||
assert kwargs["json"]["scope"] == "reflect"
|
||||
|
||||
|
||||
def test_build_headers_carries_auth_token_and_team(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""do verbs must carry X-Agent-Token + X-Agent-Team or the API's
|
||||
ROBOCO_AGENT_AUTH_REQUIRED gate 401s with "Missing X-Agent-Token" —
|
||||
regression: the manual header dict omitted both, latent until auth was
|
||||
armed on the NAS deploy."""
|
||||
import importlib
|
||||
|
||||
be_dev_1 = "00000000-0000-0000-0001-000000000001" # role=developer, team=backend
|
||||
manifest = Path(tempfile.mkdtemp()) / "tool-manifest.json"
|
||||
manifest.write_text(json.dumps({**_DO_TEST_MANIFEST, "agent_id": be_dev_1}))
|
||||
monkeypatch.setenv("ROBOCO_AGENT_ID", be_dev_1)
|
||||
monkeypatch.setenv("ROBOCO_AGENT_ROLE", "developer")
|
||||
monkeypatch.setenv("ROBOCO_AGENT_TOKEN", "test-hmac-token")
|
||||
monkeypatch.setenv("ROBOCO_ORCHESTRATOR_URL", "http://test-orchestrator:8000")
|
||||
monkeypatch.setenv("ROBOCO_TOOL_MANIFEST_PATH", str(manifest))
|
||||
|
||||
import roboco.mcp.do_server as srv
|
||||
|
||||
importlib.reload(srv)
|
||||
headers = srv._build_headers()
|
||||
|
||||
assert headers["X-Agent-ID"] == be_dev_1
|
||||
assert headers["X-Agent-Role"] == "developer"
|
||||
assert headers["X-Agent-Team"] == "backend"
|
||||
assert headers["X-Agent-Token"] == "test-hmac-token"
|
||||
assert "X-Correlation-ID" in headers
|
||||
|
||||
|
||||
def test_dm_posts_all_fields(do_module: Any) -> None:
|
||||
fake_client = MagicMock()
|
||||
fake_client.__enter__.return_value = fake_client
|
||||
|
||||
@@ -134,6 +134,34 @@ def test_give_me_work_posts_to_orchestrator(flow_module: types.ModuleType) -> No
|
||||
assert kwargs["headers"]["X-Agent-Role"] == "developer"
|
||||
|
||||
|
||||
def test_build_headers_carries_auth_token_and_team(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""flow verbs must carry X-Agent-Token + X-Agent-Team or the API's
|
||||
ROBOCO_AGENT_AUTH_REQUIRED gate 401s with "Missing X-Agent-Token" —
|
||||
regression: the manual header dict omitted both, latent until auth was
|
||||
armed on the NAS deploy."""
|
||||
be_dev_1 = "00000000-0000-0000-0001-000000000001" # role=developer, team=backend
|
||||
manifest = tmp_path / "tool-manifest.json"
|
||||
manifest.write_text(json.dumps({**_FULL_MANIFEST, "agent_id": be_dev_1}))
|
||||
monkeypatch.setenv("ROBOCO_AGENT_ID", be_dev_1)
|
||||
monkeypatch.setenv("ROBOCO_AGENT_ROLE", "developer")
|
||||
monkeypatch.setenv("ROBOCO_AGENT_TOKEN", "test-hmac-token")
|
||||
monkeypatch.setenv("ROBOCO_ORCHESTRATOR_URL", "http://test-orchestrator:8000")
|
||||
monkeypatch.setenv("ROBOCO_TOOL_MANIFEST_PATH", str(manifest))
|
||||
|
||||
import roboco.mcp.flow_server as srv
|
||||
|
||||
importlib.reload(srv)
|
||||
headers = srv._build_headers()
|
||||
|
||||
assert headers["X-Agent-ID"] == be_dev_1
|
||||
assert headers["X-Agent-Role"] == "developer"
|
||||
assert headers["X-Agent-Team"] == "backend"
|
||||
assert headers["X-Agent-Token"] == "test-hmac-token"
|
||||
assert "X-Correlation-ID" in headers
|
||||
|
||||
|
||||
def test_i_will_work_on_passes_plan(flow_module: types.ModuleType) -> None:
|
||||
fake_client = _make_fake_client({"status": "in_progress"})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user