feat(reports): agent-published structured reports via MCP + dashboard (#918)#1365
Conversation
…#918) Agents publish structured telemetry/domain reports (KPI, table, markdown, timeline, JSON) surfaced on the dashboard without reading chat. Three-surface clone of agent_activities: router/reports.py -> services/report_service.py (create + thin WS broadcast) -> db/reports.py; MCP `report` tool; Vue renderers. - Self-gated create (agent-scoped key must match path agent, mirrors authorize_heartbeat); 256 KB payload cap (413); per-agent rate limit (429). - Thin agent_report WS trigger carries only {agent_name, report_id, report_type, created_at}; frontend refetches payload via access-gated REST. - List = metadata (ReportSummary), detail = full payload; fleet access via accessible_agent_names + _narrow_to_agent. - Retention sweep (agent_reports_retention_days, default 90, 0=off); dual-track migration (SQLite agent_reports_table + Alembic 0006_agent_reports). - CASCADE on agent delete (AGENT_REFS) + re-key on rename + canary L-03 orphan scan, closing the cross-tenant disclosure under name-reuse found by /cso. Tests: test_918_agent_reports_db / _report_broadcast / _report_endpoint, plus cascade/rename coverage in test_agent_soft_delete (30 passed locally). Closes #918 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Engineering review — clean ✅ Reviewed the full +2494 / 41-file diff across all three sync surfaces Backend — three-layer split intact; agent self-gating mirrors Schema — dual-track DDL agrees across MCP — agent resolved server-side (no spoofing); 403/413/422/429 mapped. Frontend — split agent/fleet stores; access-gated refetch on thin trigger; Two things that looked like nits are actually consistent with house patterns and All 18 CI checks green. Ready for reviewer sign-off (SOC 2 separation of duties). |
…e-918 # Conflicts: # docs/memory/feature-flows.md
…xample (#918) The rate-limit knob was read via os.getenv in routers/reports.py but not forwarded to the backend container, so the .env lever was inert in prod (the #1056/#1039 packaging-gap class — prod compose launches standalone with no env_file). Mirror the VOIP_CALL_RATE_LIMIT precedent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vybe
left a comment
There was a problem hiding this comment.
Approved via /validate-pr after resolving the one blocker.
Validation was clean across the board (dual-track migration correct — Alembic 0006_agent_reports chains to 0005 head; XSS-safe — only ReportMarkdown.vue uses v-html, routed through DOMPurify; self-gated create mirroring authorize_heartbeat; thin WS trigger carries no payload; 413/429 caps; retention + delete/rename/canary-L03 cascade wired) EXCEPT one packaging gap: REPORT_RATE_LIMIT was read via os.getenv but not forwarded to the container (#1056/#1039 class — inert in prod).
Fixed on-branch in commit f28d3c3: wired REPORT_RATE_LIMIT into docker-compose.yml + docker-compose.prod.yml backend.environment + .env.example, matching the VOIP_CALL_RATE_LIMIT precedent. Also merged dev (now carrying #1364+#1363) and resolved the feature-flows.md index conflict (kept all three rows); client.ts/server.ts cleanly carry both #905 git tools and #918 report tools.
Summary
Implements #918 — agents publish structured reports (telemetry / domain results) that surface on the dashboard without anyone reading chat. A three-surface clone of the
agent_activitiespattern:routers/reports.py→services/report_service.py(create + thin WS broadcast) →db/reports.py; an MCPreporttool; and per-display_hintVue renderers (KPI tiles, table, markdown, timeline, JSON).Closes #918.
What's included
Backend (Router → Service → DB, Invariant #1)
POST /api/agents/{name}/reports— self-gated create:AuthorizedAgentpluscurrent_user.agent_name == namefor agent-scoped callers (an agent can only report as itself, mirroringauthorize_heartbeat). Payload capped at 256 KB → 413; strictReportCreatevalidation; per-agent rate limit (REPORT_RATE_LIMIT/60s via sharedrate_limiter.py, fail-open) → 429.GET /api/reports+/api/reports/stats(fleet,accessible_agent_names+_narrow_to_agent),GET /api/agents/{name}/reports(list =ReportSummary, no payload),GET /api/reports/{id}(full payload, lazy-loaded on expand),DELETE.agent_reportonSCOPE_ALL/wscarries only{agent_name, report_id, report_type, created_at}— nevertitle/payload(can be sensitive); the store refetches via the access-gated REST endpoints (thenotificationspattern). Guarded bytest_918_report_broadcast.py.cleanup_service._sweep_retention_772prunes pastagent_reports_retention_days(default 90,0disables) via chunkedprune_agent_reports.Schema (Invariant #3, dual-track #1183)
agent_reportstable indb/schema.py+db/tables.py; SQLite migration (agent_reports_tableinmigrations.py) and Alembic0006_agent_reports.MCP (third surface, Invariant #13)
reporttool (tools/reports.ts+client.createReport); agent resolved from auth context (self-only), backend self-gates.Frontend (Store = domain, Invariant #6)
stores/reports.js,ReportsPanel.vue(Agent Detail) +ReportsPanelFleet.vue(Operations), renderers undercomponents/reports/selected bydisplay_hint→report_typeprefix → JSON fallback. OnlyReportMarkdown.vueusesv-html, routed through DOMPurify (H-005).Security (/cso --diff)
A
/cso --diffaudit found one HIGH — cross-tenant report disclosure under agent name reuse (agent_reportswas missing from the delete-cascadeAGENT_REFSand therename_agentUPDATE list, so orphaned rows became readable by a new owner of the reused name). Fixed in this PR:AgentRef("agent_reports", "agent_name", Policy.CASCADE)indb/agent_cleanup.py(also turns thetest_agent_cleanup_parityguard green),agent_reportsonrename_agent(db/agent_settings/metadata.py),ORPHAN_SCAN_TABLES(canary/snapshot.py).Full report + JSON committed under
docs/security-reports/cso-diff-2026-06-27-918.*. Rest of the surface verified clean (parameterized SQL, layered authz, IDOR 404s, byte-cap, thin WS). Two pre-existing out-of-scope items flagged as follow-ups (live-rename structural guard;nevermined_payment_logKEEP reuse).Docs
architecture.md(Agent Reports subsystem + table DDL + endpoints),requirements.md,feature-flows.mdindex +feature-flows/agent-reports.md.Testing
test_918_agent_reports_db.py,test_918_report_broadcast.py,test_918_report_endpoint.py, plus cascade/rename coverage added totest_agent_soft_delete.pyand thetest_agent_cleanup_parityguard — 30 passed locally.🤖 Generated with Claude Code