fix(auth): send X-Agent-Token + X-Agent-Team from all agent->API call sites

The prior fix (6ed4e139) covered the flow/do MCP servers but missed four
other agent->orchestrator call sites that built the header dict by hand
and omitted X-Agent-Token and/or X-Agent-Team. With ROBOCO_AGENT_AUTH_REQUIRED
armed on the NAS, every one 401s:

- agent_sdk/server.py: the session-end post-mortem flush
  (/api/journals/me/entries), A2A persistence + offline fallback
  (/api/a2a/*), and the stopped-without-transition auto-substitute
  (/api/tasks/auto-substitute) — all sent only X-Agent-ID/Role, so each
  401'd 'Missing X-Agent-Token'. Add a shared _agent_headers() helper
  (mirroring flow_server._build_headers) and route all four through it.
- agent_sdk/secretary_driver.py: _headers() sent the token but not the
  team, so the HMAC gate 401'd with signature mismatch (secretary is
  board-team; token signed with team='board', verified with team='').
  Add the team header.
- mcp/git_readonly.py: the read-only git MCP sent only X-Agent-ID/Role
  — no token, no team — so /api/git/* 401'd once auth was armed. Convert
  the static _HEADERS to a _headers() helper with team + token.
- runtime/orchestrator.py: the cell-PM auto-submit self-API call acted
  as a PM with a hand-built {X-Agent-ID, X-Agent-Role} dict — no token,
  no team — 401ing under auth-required. Add _agent_api_headers(uuid,
  role) mirroring _system_api_headers, and use it.

Tests: _agent_headers round-trip (token + team, team-omitted when None),
_agent_api_headers carries a signed PM token + team.
This commit is contained in:
Renn F
2026-07-06 04:38:35 +02:00
parent ba646da07a
commit 53391f2248
6 changed files with 154 additions and 16 deletions
+28 -1
View File
@@ -16,7 +16,11 @@ from roboco.agents_config import verify_agent_token
from roboco.foundation import identity as _foundation
from roboco.models import AgentRole
from roboco.models.permissions import TASK_PERMISSIONS, TaskAction
from roboco.runtime.orchestrator import _SYSTEM_API_HEADERS, _system_api_headers
from roboco.runtime.orchestrator import (
_SYSTEM_API_HEADERS,
_agent_api_headers,
_system_api_headers,
)
def test_system_api_headers_match_the_system_identity() -> None:
@@ -55,3 +59,26 @@ def test_system_api_headers_unsigned_when_secret_unset(
monkeypatch.delenv("ROBOCO_AGENT_AUTH_SECRET", raising=False)
headers = _system_api_headers()
assert headers["X-Agent-Token"] == "UNSIGNED"
def test_agent_api_headers_carry_signed_token_and_team(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# The cell-PM auto-submit self-API call acts as a specific PM. A hand-built
# {X-Agent-ID, X-Agent-Role} dict 401s under ROBOCO_AGENT_AUTH_REQUIRED —
# same F038/F039 gap as the system self-call. _agent_api_headers must carry
# a token signed for that PM's (id, role, team) plus the team header.
monkeypatch.setenv("ROBOCO_AGENT_AUTH_SECRET", secrets.token_hex(32))
be_pm = _foundation.AGENTS["be-pm"]
be_pm_uuid = str(be_pm.uuid)
role = be_pm.role.value # "cell_pm"
team = be_pm.team.value # "backend"
headers = _agent_api_headers(be_pm_uuid, role)
assert headers["X-Agent-ID"] == be_pm_uuid
assert headers["X-Agent-Role"] == role
assert headers["X-Agent-Team"] == team
token = headers["X-Agent-Token"]
assert token and token != "UNSIGNED"
assert verify_agent_token(token, be_pm_uuid, role, team)