Skip to content

feat(reports): agent-published structured reports via MCP + dashboard (#918)#1365

Merged
vybe merged 3 commits into
devfrom
AndriiPasternak31/issue-918
Jun 27, 2026
Merged

feat(reports): agent-published structured reports via MCP + dashboard (#918)#1365
vybe merged 3 commits into
devfrom
AndriiPasternak31/issue-918

Conversation

@AndriiPasternak31

Copy link
Copy Markdown
Contributor

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_activities pattern: routers/reports.pyservices/report_service.py (create + thin WS broadcast) → db/reports.py; an MCP report tool; and per-display_hint Vue renderers (KPI tiles, table, markdown, timeline, JSON).

Closes #918.

What's included

Backend (Router → Service → DB, Invariant #1)

  • POST /api/agents/{name}/reportsself-gated create: AuthorizedAgent plus current_user.agent_name == name for agent-scoped callers (an agent can only report as itself, mirroring authorize_heartbeat). Payload capped at 256 KB → 413; strict ReportCreate validation; per-agent rate limit (REPORT_RATE_LIMIT/60s via shared rate_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.
  • Thin WS trigger: agent_report on SCOPE_ALL /ws carries only {agent_name, report_id, report_type, created_at} — never title/payload (can be sensitive); the store refetches via the access-gated REST endpoints (the notifications pattern). Guarded by test_918_report_broadcast.py.
  • Retention: cleanup_service._sweep_retention_772 prunes past agent_reports_retention_days (default 90, 0 disables) via chunked prune_agent_reports.

Schema (Invariant #3, dual-track #1183)

  • New agent_reports table in db/schema.py + db/tables.py; SQLite migration (agent_reports_table in migrations.py) and Alembic 0006_agent_reports.

MCP (third surface, Invariant #13)

  • report tool (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 under components/reports/ selected by display_hintreport_type prefix → JSON fallback. Only ReportMarkdown.vue uses v-html, routed through DOMPurify (H-005).

Security (/cso --diff)

A /cso --diff audit found one HIGH — cross-tenant report disclosure under agent name reuse (agent_reports was missing from the delete-cascade AGENT_REFS and the rename_agent UPDATE 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) in db/agent_cleanup.py (also turns the test_agent_cleanup_parity guard green),
  • re-key agent_reports on rename_agent (db/agent_settings/metadata.py),
  • added to the canary L-03 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_log KEEP reuse).

Docs

architecture.md (Agent Reports subsystem + table DDL + endpoints), requirements.md, feature-flows.md index + 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 to test_agent_soft_delete.py and the test_agent_cleanup_parity guard — 30 passed locally.

Draft pending an independent reviewer (SOC 2 separation of duties) and a /verify-local full-stack run before marking ready.

🤖 Generated with Claude Code

…#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>
@AndriiPasternak31
AndriiPasternak31 marked this pull request as ready for review June 27, 2026 18:48
@AndriiPasternak31 AndriiPasternak31 self-assigned this Jun 27, 2026
@AndriiPasternak31
AndriiPasternak31 requested review from dolho and vybe June 27, 2026 18:48
@AndriiPasternak31

Copy link
Copy Markdown
Contributor Author

Engineering review — clean ✅

Reviewed the full +2494 / 41-file diff across all three sync surfaces
(backend · MCP · frontend). No blocking findings; no code changes recommended.

Backend — three-layer split intact; agent self-gating mirrors
authorize_heartbeat; parameterized SQLAlchemy Core (SQLite+PG); iso_cutoff()
windows (Inv #16); chunked retention prune on idx_agent_reports_created;
delete_report scoped by (agent_name, id); thin WS trigger carries no
title/payload (guarded by test_918_report_broadcast.py); rate limit +
256 KiB cap + strict ReportCreate validation.

Schema — dual-track DDL agrees across schema.py, tables.py, the SQLite
agent_reports_table migration, and Alembic 0006_agent_reports. Cascade +
rename re-key + canary orphan scan + cleanup retention all wired.

MCP — agent resolved server-side (no spoofing); 403/413/422/429 mapped.

Frontend — split agent/fleet stores; access-gated refetch on thin trigger;
lazy payload load; all HTTP via api.js (Inv #7).

Two things that looked like nits are actually consistent with house patterns and
were dismissed: unescaped LIKE search matches db/schedules.py:3238;
no-debounce WS refetch matches the notifications/loops stores (and the fleet
store adds an if (loading) return dedup guard).

All 18 CI checks green. Ready for reviewer sign-off (SOC 2 separation of duties).

Eugene Vyborov and others added 2 commits June 27, 2026 22:09
…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 vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@vybe
vybe merged commit 859f66c into dev Jun 27, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants