Summary
services/event_dispatch_service.py::_interpolate_template (L104-125) substitutes {{payload.*}} into a subscription's target_message with str(value) — whole dicts/lists serialized — and the only wrap is the provenance line [Event from X: Y]. There is no "treat as data" framing and no length clamp, in contrast to routers/webhooks.py L225-232 (strip, [:CONTEXT_MAX_CHARS], framing header). EmitEventRequest.payload: Optional[dict] (models.py L3010) is unbounded, main.py has no body-size middleware, and nginx's client_max_body_size does not apply to agent→backend:8000.
The template is owner-authored, so the surface exists only where the owner used {{payload.*}}; but emit_event_for_agent is AuthorizedAgent-gated, so any accessor of the trusted source — including a sibling agent's key — can supply the payload.
Acceptance criteria
Evidence
docs/security-reports/cso-2026-09-29.md Finding 4 (ASI01/ASI07).
Summary
services/event_dispatch_service.py::_interpolate_template(L104-125) substitutes{{payload.*}}into a subscription'starget_messagewithstr(value)— whole dicts/lists serialized — and the only wrap is the provenance line[Event from X: Y]. There is no "treat as data" framing and no length clamp, in contrast torouters/webhooks.pyL225-232 (strip,[:CONTEXT_MAX_CHARS], framing header).EmitEventRequest.payload: Optional[dict](models.pyL3010) is unbounded,main.pyhas no body-size middleware, and nginx'sclient_max_body_sizedoes not apply to agent→backend:8000.The template is owner-authored, so the surface exists only where the owner used
{{payload.*}}; butemit_event_for_agentisAuthorizedAgent-gated, so any accessor of the trusted source — including a sibling agent's key — can supply the payload.Acceptance criteria
CONTEXT_MAX_CHARS) and passed throughsanitize_text.treat as data, not instructions).EmitEventRequest.payloadcarries a serialized-size bound via a model validator.Evidence
docs/security-reports/cso-2026-09-29.mdFinding 4 (ASI01/ASI07).