Commit Graph
2 Commits
Author SHA1 Message Date
Renn F 417b8c5f29 fix(gateway): dm catches A2AAccessDeniedError; circuit breakers handle dict errors
Smoke-7 surfaced: be-qa called dm(recipient='qa-all', ...) — 'qa-all'
is a channel slug, not an agent. A2A enforcement raised
A2AAccessDeniedError. It propagated past dm(), past content_actions,
got caught by FastAPI middleware which renders RobocoError.to_dict()
as {'error': {'code': ..., 'message': ..., 'details': ...}} — a
DICT-shaped 'error' field.

do_server's circuit-breaker check (and flow_server's mirror) did
`payload.get('error') in _CIRCUIT_REJECTION_KINDS` — trying to hash
a dict against a frozenset → `TypeError: unhashable type: 'dict'`.
The agent saw "Error executing tool dm: unhashable type: 'dict'"
and got stuck calling dm in a loop.

Two-layer fix:

1. content_actions.dm now catches A2AAccessDeniedError and returns
   Envelope.not_authorized with the original reason + route_hint as
   remediate. This is the right shape — content tools always emit
   Envelopes; RobocoErrors escaping to the middleware is a bug.

2. Defense-in-depth: do_server._record_and_check_circuit and
   flow_server._record_and_check_circuit now guard against non-string
   error fields. Any future RobocoError-leak that bypasses (1) will
   pass through untouched instead of crashing the tool call.

3 new tests pin the contracts:
- dm A2A denial returns Envelope.not_authorized (not propagated)
- do_server circuit-breaker doesn't crash on dict-shaped errors
2026-05-15 03:03:53 +02:00
Renn F 21007e122f fix(mcp): do_server per-verb circuit breaker mirrors flow_server
Smoke-6 surfaced the gap. main-pm called note(scope='decision') with
context: null 8 times in a row — every one returned incomplete_input
and the agent kept retrying. The flow_server had a breaker (C1) but
do_server didn't, so content-tool rejections went uncapped.

Mirror the flow_server pattern:
- _CIRCUIT_REJECTION_KINDS = {tracing_gap, invalid_state,
  not_authorized, incomplete_input} (same set)
- _record_and_check_circuit posts to the SDK's /verb/attempted on each
  counted rejection
- When the SDK reports open=true, the original rejection envelope is
  REPLACED with the circuit_open envelope so the agent stops retrying

The SDK side (agent_sdk/server.py) already accepts arbitrary verb
names; no changes there. note hits the default cap of 3 retries / 60s
from foundation.agent_loop. After the third incomplete_input the
agent gets circuit_open and the loop ends.

9 new tests pinning the contract.
2026-05-14 05:21:33 +02:00