Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/memory/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ to the main agent for processing via `task_execution_service.execute_task(trigge
| GET | `/api/agents/{name}/access-requests` | List pending access requests |
| POST | `/api/agents/{name}/access-requests/{id}/decide` | Approve (auto-shares + fires fire-and-forget approval notification back on the requester's originating channel for telegram/slack/whatsapp, #951) or reject |

### Schedules (12 endpoints)
### Schedules (13 endpoints)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/agents/{name}/schedules` | List schedules |
Expand All @@ -648,10 +648,13 @@ to the main agent for processing via `task_execution_service.execute_task(trigge
| POST | `/api/agents/{name}/schedules/{id}/disable` | Disable schedule |
| POST | `/api/agents/{name}/schedules/{id}/trigger` | Manual trigger |
| GET | `/api/agents/{name}/schedules/{id}/executions` | Execution history |
| GET | `/api/agents/{name}/schedules/{id}/analytics` | Per-schedule analytics — counts, success rate, duration p50/p95/p99, cost total, tool-call top-5 by total duration, daily timeline. `?window_hours=` ∈ {24, 168, 720}, default 168 (#868) |
| POST | `/api/agents/{name}/schedules/{id}/webhook` | Generate/rotate webhook token (WEBHOOK-001) |
| GET | `/api/agents/{name}/schedules/{id}/webhook` | Get webhook status and URL (WEBHOOK-001) |
| DELETE | `/api/agents/{name}/schedules/{id}/webhook` | Revoke webhook token (WEBHOOK-001) |

**Analytics endpoint (#868):** Percentiles computed Python-side via `statistics.quantiles` over the newest 5,000 success rows (`sampled: true, sample_size: 5000` reported back when cap is hit); counts and the daily timeline use the full unsampled rowset. UTC day buckets via `substr(started_at, 1, 10)` then Python gap-fill so chart x-axis is continuous. Tenant boundary lives in the DB layer (`db.schedules.get_schedule_analytics(schedule_id, hours, agent_name=name)`) — `AuthorizedAgent` only validates the path-param agent name, not that `schedule_id` belongs to it. Soft-deleted schedules return 404; the audit/billing surface (cross-trigger per-agent rollup with soft-deleted schedules included) is the deferred #18 endpoint.

### Webhook Triggers (WEBHOOK-001)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
Expand Down
1 change: 1 addition & 0 deletions docs/memory/feature-flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| 2026-05-29 | #950 | deploy-local deferred hardening — `is_trinity_compatible()` now **requires** a non-empty, UTF-8-readable `CLAUDE.md` (blocking 400 `NOT_TRINITY_COMPATIBLE`, previously a non-fatal warning; binary/non-UTF-8 yields a clean 400 not a 500). New `collect_mcp_credential_warnings()` scans `.mcp.json.template`/`.mcp.json` for `${VAR}` refs absent from the post-merge `.env` and not platform-injected (static allowlist mirroring `crud.py`), returning them as advisory `DeployLocalResponse.warnings[]` (also added to the MCP `deploy_local_agent` response type). Docs reconciled: `credentials` request field + `MAX_DEPLOY_CREDENTIALS`, dedicated credential-merge step, `DEPLOYED_TEMPLATES_DIR_UNWRITABLE`/`WORKSPACE_PREPOP_FAILED` codes, `require_role("creator")`. 3 unit-test files. | [local-agent-deploy.md](feature-flows/local-agent-deploy.md), [template-processing.md](feature-flows/template-processing.md) |
| 2026-05-25 | #914 | MCP `chat_with_agent` gateway-timeout receipt — `TrinityClient.chat()` wraps the backend fetch in an `AbortController` bounded by `MCP_CHAT_TIMEOUT_MS` (default 25000ms, under the typical 30-60s MCP gateway ceiling). On abort, `findRecentMcpExecution` queries `/api/agents/{name}/executions` and returns `{status:"queued_timeout", agent, execution_id, message}` so callers poll `get_execution_result` instead of triggering Trinity's concurrent-duplicate guard on retry. New `pickRecentMcpExecution` exported pure helper + 9 `node:test` cases. Live-verified through the FastMCP JSON-RPC transport. Companion to #418 (per-agent execution_timeout enforcement) and the MCP-client surface of #408/#428's long-running dispatch family. | [mcp-orchestration.md](feature-flows/mcp-orchestration.md) |
| 2026-05-25 | #912 | fix(orphan-sweep): drain-time cgroup sweep now forwards an allowlist of in-flight execution pids/pgids so concurrent legitimate claude subprocesses don't get SIGKILLed when a sibling task drains in the same agent cgroup. Single canonical source via `ProcessRegistry.active_execution_pids(exclude_execution_id=…)` — used by the periodic orphan sweeper (#817), `ProcessRegistry.terminate()`, and the new `subprocess_pgroup._active_execution_pids_for_drain()` helper. Fixes the silent SIGKILL of multi-minute tasks visible as "exit code -9 / 0 tool calls / 0 turns" whenever any other task finished in the same container. 8 unit tests + in-container behavioural check. | [execution-termination.md](feature-flows/execution-termination.md), [parallel-headless-execution.md](feature-flows/parallel-headless-execution.md) |
| 2026-05-22 | #868 | feat(analytics): per-schedule execution analytics — `GET /api/agents/{name}/schedules/{id}/analytics` (24h / 7d / 30d) returns counts, success rate, duration p50/p95/p99 (Python `statistics.quantiles`, capped 5000-row pool), cost total, tool-call top-5 weighted by total wall time, UTC daily timeline with gap-fill; `ScheduleAnalyticsCard.vue` (pure-CSS, no Chart.js) inline in `SchedulesPanel.vue` expanded row, threshold-ladder stat tiles, sampled badge; tenant boundary in DB layer (`agent_name` required); per-agent rollup deferred to #18, per-chat-session deferred. 12 unit tests. | [scheduling.md](feature-flows/scheduling.md) |
| 2026-05-20 | #740 | feat: `run_agent_loop` MCP tool + backend loop service — sequential bounded task execution with `{{run}}`/`{{previous_response}}` substitution, optional `stop_signal` early exit, graceful stop. New `agent_loops` + `agent_loop_runs` tables, `loop_id` column on `schedule_executions` for timeline tagging. Cleanup-service startup hook flips orphaned loops to `interrupted`. 16 unit tests. | [run-agent-loop.md](feature-flows/run-agent-loop.md) |
| 2026-05-18 | #887 | fix(read-only): guard moved to base image (`/opt/trinity/hooks/`, root-owned 0555); MultiEdit bypass fixed; fail-closed via `run_hook()`; lifecycle always syncs config on start (stale-volume fix); config file protected by `path_deny` + `bash_deny` in guardrails-baseline.json; 18 unit tests | [read-only-mode.md](feature-flows/read-only-mode.md) |
| 2026-05-18 | #888 | write_user_memory MCP tool — per-user memory write with server-side email resolution, fixing PII cross-user memory leak | [write-user-memory.md](feature-flows/write-user-memory.md) |
Expand Down
1 change: 1 addition & 0 deletions docs/memory/feature-flows/scheduling.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ executions?limit=100 AuthorizedAgent dependency SELECT (spe
| POST | `/api/agents/{name}/schedules/{id}/disable` | Disable | schedules.py:218 | AuthorizedAgent |
| POST | `/api/agents/{name}/schedules/{id}/trigger` | Manual trigger | schedules.py:236 | AuthorizedAgent |
| GET | `/api/agents/{name}/schedules/{id}/executions` | Execution history | schedules.py:265 | AuthorizedAgent |
| GET | `/api/agents/{name}/schedules/{id}/analytics` | Per-schedule analytics (#868) — counts, success rate, duration p50/p95/p99, cost total, tool-call top-5 by total duration, daily timeline; `?window_hours=` ∈ {24, 168, 720}, default 168 | schedules.py:255 | AuthorizedAgent |
| GET | `/api/agents/{name}/executions` | All agent executions (summary) | schedules.py:435 | AuthorizedAgent |
| GET | `/api/agents/{name}/executions/{id}` | Get specific execution (full) | schedules.py:462 | AuthorizedAgent |
| GET | `/api/agents/{name}/executions/{id}/log` | Get execution log | schedules.py:309 | AuthorizedAgent |
Expand Down
80 changes: 80 additions & 0 deletions docs/security-reports/cso-diff-2026-05-22-868.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CSO Diff Audit — #868 Per-Schedule Execution Analytics

**Date:** 2026-05-22
**Mode:** `/cso --diff` (daily gate 8/10, branch changes only)
**Branch:** `feature/868-schedule-execution-analytics` → `dev`
**Scope:** 8 files / +395 net LOC

## Surface delta

- **+1 authenticated endpoint** — `GET /api/agents/{name}/schedules/{schedule_id}/analytics`
- **+1 frontend component** — `ScheduleAnalyticsCard.vue` (read-only fetch)
- **+0** webhooks, internal endpoints, background jobs, WebSocket channels, Docker config, network rules, CI workflows
- **+0** Python deps (`statistics`, `collections.defaultdict` are stdlib); no Node deps

## Findings

**Zero findings.** No critical, high, or medium that survives the 8/10 confidence gate.

## Per-phase walk-through

| Phase | Status | Notes |
|---|---|---|
| **0 Architecture** | ✓ | Diff adds a read-only analytics surface on top of `schedule_executions`. Inherits existing trust boundaries. |
| **1 Attack surface** | ✓ | One new GET endpoint, auth-gated by `AuthorizedAgent`. |
| **2 Secrets archaeology** | ✓ | Grep over diff for `sk-`, `ghp_`, `AKIA`, `xox`, `@gmail/yahoo/abilityai.` matches only pre-existing context (`webhook token` doc rows, `import secrets` stdlib, `get_agent_token_stats` symbol). No real secrets / PII / internal URLs. |
| **3 Supply chain** | ✓ | Zero new deps. |
| **4 CI/CD** | ✓ | No workflow files touched. |
| **5 Infrastructure** | ✓ | No Dockerfile / `docker-compose.yml` / Redis / network changes. |
| **6 Webhooks & integration** | ✓ | No webhooks added. No `verify=False`. No new internal endpoints. |
| **7 LLM & AI security** | ✓ | Endpoint takes no string flowing into a prompt. Frontend has zero `v-html`. JSON parsing is bounded + try/except per row. Rowset cap (5000) and window cap (`{24,168,720}`) prevent unbounded compute. |
| **8 Skill supply chain** | ✓ | No `.claude/skills/` files touched. |
| **9 OWASP Top 10** | ✓ | Detail below. |
| **10 STRIDE** | ✓ | Read-only endpoint, existing trust fabric covers all six. |
| **11 Data classification** | ✓ | Returns aggregates (counts, percentiles, costs, tool names) derived from existing data. No new sensitivity. |
| **12 FP filtering** | ✓ | One pattern considered (no rate limiting on the new endpoint) excluded per hard rule #1 (DoS / rate limiting unless LLM cost amplification — N/A here, SQLite reads only). |

## OWASP Top 10 detail (diff surface only)

| Cat | Verdict | Evidence |
|---|---|---|
| **A01 Access Control** | ✓ Clean | `AuthorizedAgent` on path-param + DB-layer `schedule.agent_name != agent_name → None` (tested via `TestCrossTenant::test_schedule_id_belonging_to_other_agent_returns_none`). Cross-tenant `schedule_id` collapses to 404 alongside missing / soft-deleted. |
| **A02 Crypto Failures** | ✓ Clean | No crypto / JWT / SSH key generation added. |
| **A03 Injection** | ✓ Clean | All SQL parameterized (`?` placeholders, tuple bindings at `db/schedules.py:1530, 1545`). No `subprocess` / `os.system` / `os.popen` introduced. No `v-html` in the new Vue component. |
| **A04 Insecure Design** | ✓ Clean | Window pill server-validated (`_ANALYTICS_VALID_WINDOWS = {24, 168, 720}` → 422). Rowset cap `_PERCENTILE_ROWSET_CAP = 5000` bounds percentile / JSON-parse cost. Per-IP rate limiting not added — out of diff scope; existing auth-layer limits apply. |
| **A05 Misconfiguration** | ✓ Clean | No CORS / CSP / debug-mode changes. |
| **A07 Auth Failures** | ✓ Clean | Inherits existing `AuthorizedAgent` (JWT or MCP API key); no new auth surface. |
| **A08 Integrity** | ✓ Clean | JSON deserialization bounded by `try/except (json.JSONDecodeError, TypeError)` + per-element `isinstance` guards (`db/schedules.py:1577–1605`). `tool_calls` is internal-write data sanitized at write time via `routers/chat.py:357` (`sanitize_execution_log`). |
| **A09 Logging** | ✓ Clean | WARN on malformed JSON parse includes `schedule_id` + exception (which by stdlib design does **not** echo raw content — `json.JSONDecodeError.__str__` is "Expecting value: line 1 column 1 (char 0)", `TypeError` from `json.loads(non-str)` does not echo the value). Read-side audit log not in architectural pattern (`platform_audit_service` writes on mutations only). |
| **A10 SSRF** | ✓ Clean | No outbound URL construction; no user-controlled URLs. |

## Active verification of the load-bearing security gate

The cross-tenant boundary check is the most security-critical addition. Confirmed:

1. **Code path** (`db/schedules.py:1519–1522`):
```python
schedule = self.get_schedule(schedule_id)
if not schedule or schedule.agent_name != agent_name:
return None
```
2. **Soft-delete coupling**: `get_schedule(schedule_id)` filters `deleted_at IS NULL` (`db/schedules.py:292`) — soft-deleted schedules return `None` before the tenant compare.
3. **Router collapse** (`routers/schedules.py:298–304`): missing / soft-deleted / cross-tenant all → 404, no existence leak.
4. **Test pin** (`tests/unit/test_schedule_analytics.py::TestCrossTenant::test_schedule_id_belonging_to_other_agent_returns_none`): caller has access to `agent-A`, requests analytics for `agent-B`'s schedule → asserts `None`.

## XSS active verification (frontend)

`ScheduleAnalyticsCard.vue` renders these user-derivable strings:
- `tool.name` (from `schedule_executions.tool_calls` JSON, written by sanitized agent runtime) — via `{{ }}` text interpolation. Vue 3 auto-escapes.
- `bucket.date` (server-emitted ISO date) — via `:title=` v-bind, which Vue auto-escapes for HTML attributes.
- Cost / counts — `Number.toFixed()` output, no string user input.

No `v-html` directive anywhere in the new component.

## Trend tracking

Compared against `cso-diff-2026-05-12-678.md` and `cso-diff-2026-05-12.md`. No persistent or recurring fingerprints overlap with this diff (different file surfaces).

## Bottom line

Branch is clean for merge from a security posture standpoint. The /autoplan-gated cross-tenant boundary, parameterized SQL, JSON-decode defensiveness, route-order independence (4-segment path vs 3), and zero `v-html` together cover the diff's risk surface.
6 changes: 6 additions & 0 deletions src/backend/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,12 @@ def has_cached_dashboard(self, agent_name: str) -> bool:
def get_agent_execution_stats(self, agent_name: str, hours: int = 24):
return self._schedule_ops.get_agent_execution_stats(agent_name, hours)

def get_schedule_analytics(self, schedule_id: str, hours: int,
agent_name: str):
return self._schedule_ops.get_schedule_analytics(
schedule_id, hours, agent_name,
)

def get_agent_token_stats(self, agent_name: str):
return self._schedule_ops.get_agent_token_stats(agent_name)

Expand Down
Loading
Loading