Conversation
…v workflow Introduces docs/planning/TARGET_ARCHITECTURE.md — the optimal steady-state design Trinity should converge toward (PostgreSQL, actor model coordination, async-first agent communication, fleet observability, GuardAgent security). Updates CLAUDE.md, DEVELOPMENT_WORKFLOW.md, and the groom/roadmap/sprint playbooks to distinguish current architecture (what is built today) from target architecture (where decisions should point), and to use target architecture alignment as a ranking signal during backlog grooming and issue prioritization. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): Encrypt Slack bot tokens at rest (#453) The SLACK-001 public-link Slack integration (`db/slack.py`) was the last holdout still storing bot tokens as plaintext in SQLite, violating Architectural Invariant #12. Telegram (`telegram_bindings.bot_token_encrypted`), WhatsApp (`whatsapp_bindings.auth_token_encrypted`), and SLACK-002 (`slack_workspaces.bot_token`) all already encrypt via `services.credential_encryption` (AES-256-GCM, JSON envelope). This brings SLACK-001 in line with that pattern. Additionally, `slack_workspaces.bot_token` was rolled out with lazy encryption (encrypt-on-write + plaintext fallback at `slack_channels.py:47-49`) which left two sources of plaintext on disk: - Rows written before the encryption rollout - Rows copied from `slack_link_connections` by `_migrate_slack_channel_agents` A one-shot migration walks both tables on startup and re-encrypts any plaintext `xoxb-*` rows. Idempotent at row level (skip JSON envelopes) and at migration level (schema_migrations runner). src/backend/db/slack.py - Add `_get_encryption_service` / `_encrypt_token` / `_decrypt_token` (exact copy of the pattern in `db/slack_channels.py`, `db/telegram_channels.py`, `db/whatsapp_channels.py`) - Encrypt at `create_slack_connection` (write site) - Decrypt at `_row_to_connection` (read site, with `xoxb-*` plaintext fallback so runtime works pre-migration on legacy rows) - Caller-facing API surface unchanged — `slack_bot_token` field still contains plaintext in the returned dict src/backend/db/migrations.py - New `_migrate_slack_bot_token_encryption` registered in MIGRATIONS list - Walks BOTH `slack_link_connections.slack_bot_token` AND `slack_workspaces.bot_token`, encrypts plaintext rows in place - Hard-fail on missing CREDENTIAL_ENCRYPTION_KEY (matches the implicit pattern of every other consumer of CredentialEncryptionService) - Skips already-encrypted rows (signature: starts with `{` not `xoxb-`) - Defensive: skips silently if a table doesn't exist docs/memory/architecture.md - Reword Invariant #12 to acknowledge channel/subscription tokens as a documented exception (persisted but mandatorily encrypted), with the full list of tables under that rule - Add `slack_link_connections` DDL block; update `slack_workspaces` block to clarify the column-name vs content-type distinction tests/unit/test_slack_token_encryption.py (NEW, 14 tests) - TestRoundTrip: write encrypts, read decrypts, raw DB value is JSON envelope - TestPlaintextFallback: legacy `xoxb-*` row returns token + warning logged; corrupt envelope returns None + error logged - TestEncryptionHelpers: encrypt+decrypt isolation; encrypt raises ValueError on missing key; decrypt returns None on missing key - TestMigration: encrypts plaintext in both tables, skips encrypted, idempotent on second run, hard-fails without key, no-op on missing/empty tables Live verification on running backend: - Migration ran on startup: 1 row in each table re-encrypted (the real `ability.ai` workspace data) - On-disk now `{"version": 1, "algorithm": "AES-256-GCM", ...}` envelopes - `SlackOperations.get_slack_connection` returns the original `xoxb-...` plaintext via decrypt — caller-facing API surface unchanged Out of scope (filed separately): - Encryption tests for slack_channels.py + telegram_channels.py + whatsapp_channels.py (all shipped without dedicated test coverage): tracked in #664 - Renaming `bot_token` → `bot_token_encrypted` columns: cosmetic, would require a real schema migration; current naming works behind the service-layer encapsulation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(req): annotate SLACK-001 bot token as encrypted at rest (#453) SLACK-002 (line 809) noted "bot_token encrypted"; SLACK-001 (line 781) didn't, even after #453 brought slack_link_connections.slack_bot_token under the same AES-256-GCM regime. Mirror the annotation for parity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Pavlo <pash@pashs-MacBook-Pro.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
56 checks across 10 categories covering static file checks, YAML/JSON schema validation, security scanning, and AI-evaluable logical checks (skill coherence, CLAUDE.md quality, cross-file consistency). Serves as the canonical check list for the compatibility validation API and MCP tool. Closes-adjacent: #668 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…NULL status row (#669) (#676) `GET /api/monitoring/status` (the endpoint MCP `get_fleet_health` calls) was returning 500 whenever any `agent_health_checks` row had `status = NULL`. Root cause: the build loop did AgentHealthSummary(status=check.get("status", "unknown"), ...) `dict.get(key, default)` only returns the default when the key is *missing*. A row with the key present but value `None` returned `None`, which Pydantic v2 rejected because `AgentHealthSummary.status: str` is required. The sibling per-agent endpoint dodged this by triggering a fresh health check when no aggregate row existed, which is why `get_agent_health` worked while `get_fleet_health` 500'd in the production fleet that triggered #669. Fix: - Extract `_build_agent_summary(name, check)` and `_coerce_status(raw)` so NULL/missing/non-str status degrades to `"unknown"` consistently. Same for NULL `error_message` (would explode `.split("; ")`). - Wrap the aggregator in try/except returning a structured "unknown" payload rather than 500 (issue ask #1). Future schema drift surfaces as data, not as an outage. - Reconcile `docs/user-docs/operations/monitoring.md` — the listed `/api/monitoring/fleet-health` path doesn't exist; correct to `/api/monitoring/status`. Tests: 7 new unit tests in `tests/unit/test_fleet_status_resilience.py` covering NULL status, missing status key, missing check row, NULL error_message, non-string status, and sort-key tolerance. Does NOT fix the underlying scheduler stoppage (8 of 9 agents going months without a health-check refresh) — that root cause needs production logs and is split out as a separate ticket. Closes #669 (symptom) Refs #675 (scheduler stoppage follow-up) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#640 debugging (#662) After diagnosing #640 against a running agent with an npx-launched stdio MCP (@upstash/context7-mcp@latest), the issue body's "MCP child inherits fd > 2" theory does not hold: Node.js spawn already isolates the MCP child to fd 0/1/2 on socketpairs. The remaining wire-corruption mechanism (some descendant outside claude's pgid acquiring agent-server's pipe write-end via setsid+dup) needs a live production trace to root-cause — single-session repro budget here isn't enough to manifest the failure (issue says ≥100 turns / 18min). What this commit does ship: make the next production failure usable by surfacing the diagnostic data the existing #618/#639 mitigations already had at hand but discarded. read_stdout (chat + headless paths) - JSONDecodeError no longer silently `pass`-swallowed. - Track count + capture first sanitised, length-capped (300 char) sample. - Surface in completion log line as parse_failures=N + WARNING with sample text when N > 0. Lets operators distinguish wire corruption (#640) from reader-leak-past-claude-exit (#520/#618) in production logs. _classify_empty_result - New parse_failure_count / parse_failure_sample kwargs (defaults preserve legacy callers — chat path doesn't wire it; backward compat covered by test_default_parse_failure_args_preserve_legacy_callers). - Detail string now includes parse_failures + raw_messages type histogram (top 6 types) + first malformed line. The histogram tells operators whether the reader caught most of the stream or stalled near the start. _kill_orphan_pipe_writers (#618) - Was logging orphan count only. - Now captures cmdline / ppid / pgid per orphan BEFORE SIGKILL (after the kill /proc/{pid} is gone) and emits one INFO line per pid, capped at 10 lines + count-only summary, so log volume stays bounded under a runaway MCP fan-out. - First pass at identifying which package consistently leaks. Issue body's "npm setsid" hypothesis is testable now without re-instrumenting. 5 new tests (33 total, all green): - test_parse_failure_count_surfaces_in_detail - test_parse_failure_sample_appended_when_present - test_parse_failure_sample_omitted_when_count_is_zero - test_raw_messages_type_summary_in_detail - test_default_parse_failure_args_preserve_legacy_callers Does not close #640. The wire-interleaving root cause remains open — but the diagnostic surface is now sufficient to identify it from a single production failure rather than needing to re-instrument and wait for the next occurrence. Issue: #640 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… late agent reply (#671) (#681) When an operator cancels a running execution mid-flight, two writers race on the same `schedule_executions` row: Writer A — terminate handler (`routers/chat.py:~1841`) writes status = CANCELLED. Writer B — TaskExecutionService success branch (`services/task_execution_service.py:498`) writes status = SUCCESS once the agent's HTTP reply lands. Claude Code typically catches the cancel signal, emits a graceful final message, and exits 0 — so the agent reports "completed successfully" and B lands well after A. Pre-fix CAS (RELIABILITY-005, db/schedules.py): SUCCESS writes were unconditional ("agent's own completion result always wins"). When A landed first and B landed second, B silently clobbered the CANCELLED status with SUCCESS. Effects in production: - schedule's `next_run_at` advanced as if the run had succeeded, suppressing recovery on the next cron tick (silent skip) - cost telemetry counted the partial run as billable success - on-call had no signal that deliverables were incomplete - for agents with side effects (Slack post, sheet rows, CRM), wrongly- green status hid incomplete work from operators Reporter saw two consecutive incidents on bdr-agent / `Daily Lead Outreach` ending with status=success despite the operator cancelling and no Slack ping / sheet rows / final deliverable being produced. Fix: narrow the CAS carve-out so SUCCESS writes are blocked when the row is already CANCELLED, but still win over RUNNING / QUEUED / PENDING_RETRY / SKIPPED and over a phantom-stale FAILED (preserves the #378 invariant — real completions still beat misfired Phase-3 cleanup). - SUCCESS over RUNNING — wins (happy path) - SUCCESS over phantom FAILED — wins (#378 invariant preserved) - SUCCESS over CANCELLED — blocked (#671) - FAILED/CANCELLED over any terminal — blocked (RELIABILITY-005, unchanged) Tests: 5 unit tests in tests/unit/test_cancelled_not_overwritten.py covering each transition above. The exact prod race repro (`test_success_blocked_when_row_already_cancelled`) fails pre-fix, passes post-fix. Live-verified against running stack: cancel write ok: True after cancel: ('cancelled',) late-success write ok: False after late-success: ('cancelled', None, None) # response/cost NOT recorded Defense-in-depth (plumb cancel signal into the agent task-runner so its reply carries `status=cancelled` and its log says "cancelled by user" instead of "completed successfully") tracked separately as a follow-up. Closes #671 (minimum CAS guard) Refs #679 (defense-in-depth follow-up) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…244) (#684) Implements Slack's documented multi-connection Socket Mode pattern in adapters/transports/slack_socket.py. Slack's edge fans out events across active connections, so when one half-closes, siblings keep absorbing traffic — eliminating the 430 ms reconnect gap absorbed by the watchdog. Empirical basis: 7-day production ops report on ability-services showed 68 disconnect/reconnect cycles, 100% recovered by the watchdog (#278), all ending in identical "Cannot write to closing transport" — Slack's edge half-closing without a WebSocket close frame. Slack's own docs and support team recommend running multiple concurrent connections (up to 10 per app) as the architectural answer to this exact pattern. Changes - N concurrent SocketModeClient instances (default 2; range 1-10) via SLACK_SOCKET_CONNECTION_COUNT env var, clamped fail-safe on parse error - _ClientCtx dataclass per client (own watchdog task, own backoff counter) - Envelope-ID dedup ring (OrderedDict cap 1024 + asyncio.Lock) defends against possible cross-connection duplicate delivery; INFO log on hit so we measure whether Slack ever actually dual-delivers - Per-client log prefix [c=N] so ops can attribute disconnects per client - is_connected returns "any client healthy" (permissive) + new connected_count property exposes degraded mode - stop() iterates all clients/watchdogs (cleanup correctness) - Parallel start via asyncio.gather keeps boot at ~10s ceiling - Env-var WARN no longer echoes raw value (prevents accidental token leak if operator pastes app token into wrong env var) Tests - 56/56 passing (28 existing watchdog + 28 new multi-connection) - New test_slack_multi_connection.py covers env-var bounds, is_connected/connected_count semantics, dedup ring (skip + concurrent + FIFO eviction), per-client backoff isolation, partial startup, N=1 backward compat, stop() cleanup Deferred - #683 — wrap connect_to_new_endpoint() in asyncio.wait_for to prevent watchdog stall (pre-existing watchdog hole, blast-radius reduced by this PR's per-client isolation) Verified - Single-worker uvicorn confirmed (docker-compose.yml line 82, no --workers flag; line 230 comment confirms intent), so per-process dedup ring is correct - Architectural Invariants preserved (Channel Adapter ABC unchanged; no new endpoints; no new persistent storage; no Invariant #12 regression) - /review found 0 critical, 2 informational; C1 applied - /cso --diff: 0 critical / 0 high / 0 medium / 0 low Fixes #244 Co-authored-by: Pavlo <pash@pashs-MacBook-Pro.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add 6 missing deploying/ spokes: local-development, single-server, public-access, upgrading, backup-and-restore, monitoring - Add 9 UI screenshots and wire into 10 existing feature docs - Update deploying-trinity.md hub with spoke navigation table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… failures while open (#687) (#688) record_failure() was resetting last_failure_time on every call, including when the circuit was already open. Continuous probe failures (cleanup re-verify, scheduler dispatches) kept the cooldown timer near zero, making the half-open transition unreachable and leaving the circuit permanently open until backend restart. Fix: only update last_failure_time when state != "open", so the 30s cooldown starts from when the circuit first opens and is not disturbed by subsequent failures. Fixes #687 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
`config.REDIS_URL` is the canonical Redis URL gate (#589 / PR #643) — it raises at import if creds are missing. Three services bypassed that gate by reading the env var directly with an unauthenticated localhost fallback, both at the factory site and in the constructor default: redis_url = os.getenv("REDIS_URL", "redis://redis:6379") def __init__(self, redis_url: str = "redis://redis:6379"): In docker-compose these branches don't fire — REDIS_URL is always populated. But test/CI paths and ad-hoc debug shells silently fall back to unauth localhost, then hit NOAUTH at runtime instead of the clear startup-time error #589 establishes. Changes: - slot_service / ssh_service: factories drop the os.getenv fallback; constructors take Optional[str] = None and lazy-import config.REDIS_URL when called with no arg. - capacity_manager: same pattern, lazy-import in __init__. - tests/unit/conftest.py: setdefault REDIS_URL with creds before any backend import — unit tests don't share the parent conftest (norecursedirs = ..) so the env wasn't being primed. - tests/unit/test_redis_url_no_fallback.py: lint-style regression test that greps src/backend/services/ for `os.getenv("REDIS_URL"` and `"redis://redis:6379"` and fails if either resurfaces. Verified: - 30 unit tests pass in trinity-backend container (test_redis_url_no_fallback + test_capacity_manager + test_config_fail_fast) - 14 ssh_service tests + 9 redis-url-related tests pass on host venv - Live backend bootstraps cleanly: SlotService / SshService / CapacityManager all resolve REDIS_URL via config - Lint test catches regression: stashing the slot_service fix flips both assertions to FAIL with offending line:number Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): close TOCTOU race in webhook rate limiter (#644) Pre-fix path issued a separate GET then INCR. N concurrent callers could all observe count < WEBHOOK_RATE_LIMIT before any of them incremented, slipping past the 429 and pushing the actual call rate to limit + N. Switched to INCR-then-compare (Redis INCR is atomic): increment unconditionally, then 429 the caller whose post-increment count crosses the threshold. Trade-off: blocked requests still tick the counter, slightly extending cool-down for an over-limit token. Acceptable for a rate-limiter — we only stop accepting work, we don't unwind. Tests: - tests/unit/test_webhook_rate_limit_toctou.py — pins INCR-first semantics. The structural assertion (r.get() not called) reliably catches a partial revert that re-adds the GET; the wide-window race belongs in integration tests against real Redis. - tests/integration/test_webhook_rate_limit.py — adds concurrent burst test alongside the existing #589 sequential coverage. Verified live in trinity-backend with real Redis: - pre-fix: 15/20 succeeded under 20-thread burst (limit 10) — race reproduced. - post-fix: exactly 10/20 succeeded — limit holds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(webhooks): unblock trigger endpoint — schedule model + audit signature (#647 follow-up) While verifying #644 against a live stack, found two additional facade gaps that #648 (the WEBHOOK-001 delegation fix) didn't catch — both crash trigger_webhook before the rate-limiter even runs to completion: 1. `Schedule` pydantic model never carried `webhook_enabled` / `webhook_token` fields. The DB columns exist, but the row mapper discarded them, so `if not schedule.webhook_enabled:` raised AttributeError on every trigger call. 2. `webhooks.py:trigger_webhook` called `platform_audit_service.log()` with `actor_type="system"`. The service derives actor_type internally from actor_user / actor_agent_name / mcp_scope and has no such kwarg; every accepted webhook 500'd in the audit step. Both are tiny: - Add the two fields to `Schedule` (db_models.py). - Pull them through `_row_to_schedule` (db/schedules.py). - Drop the bogus actor_type kwarg, pass actor_ip instead — webhook callers are unauthenticated so caller IP is the only attributable signal. With these, the integration test in tests/integration/test_webhook_rate_limit.py now exercises the full HTTP path end-to-end. Live verification against the running backend: 15-way concurrent burst → 10 × 202 + 5 × 429, limit holds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both copy actions on /api-keys (Copy Config, Copy key icon) called navigator.clipboard.writeText() with no fallback and swallowed every rejection in console.error, so users in modal-focus or non-secure contexts saw nothing on the clipboard and no error. - Add `utils/clipboard.js` with a textarea + execCommand fallback, returning a boolean for caller-driven UX. - Wire ApiKeys.vue's copyApiKey / copyMcpConfig through the helper. Visual "Copied!" / green-check state only fires on confirmed success; failure shows an alert telling the user to copy manually. - Add e2e spec exercising both buttons with a granted clipboard-read permission. Verified in a real browser (admin login, /api-keys, create key, click both buttons): clipboard contained the expected MCP JSON and raw `trinity_mcp_*` key respectively; no console errors. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…694) `GET /api/monitoring/status` returned 500 TypeError for non-admin users because routers/monitoring.py called the helper with the pre-refactor two-arg signature `(email, agent_names)`. Every other call site was updated to `(current_user)` — only this one was missed. Adds a unit regression test that pins the canonical helper signature and spy-verifies the router calls it with exactly one positional User arg. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…theses falsified (#693) * test(harness): repro scaffolding for #640 — controlled stdio MCP leaks Adds tests/harness/640/ — three files implementing a deterministic repro harness for the open root cause behind the reader-thread / wire-corruption failure family (#640, manifesting in #678, #630, #618, #548, #586). Background: PR #662's author tried 1.5h with `@upstash/context7-mcp` — issue body says ≥100 turns / 18min are needed to manifest. Hunting for a naturally-leaky package is unreliable. Instead this harness builds a controlled experiment: a minimal stdio MCP server with switchable leak variants, each testing one hypothesis from the issue body and #662's empirical notes. Files: - noisy_mcp_server.py — stdlib-only stdio MCP server with --leak knob: none : control / baseline stderr-flood : MCP child stderr noise setsid-child : grandchild that escapes pgid (#618 family) and retains protocol-pipe write end proc-fd-write : raw writes to /proc/self/fd/1 interleaved with MCP frames delayed-stdout : partial-line writes that race the reader at line boundary npm-wrapper : real-world npx-style boilerplate emitted to stdout BEFORE protocol handshake — most likely production culprit - run_repro.py — driver that hits an agent's chat API for N turns and measures null-cost / null-response rate (the observable symptom from #678). Exits 1 if rate exceeds --null-cost-fail-rate (default 5%) so the harness can also serve as a CI regression gate once a fix lands. - README.md — runbook: agent setup, .mcp.json wiring, expected output, caveats. Documents that parse_failures-counter assertions wait on PR #662 merging. Smoke-tested: - Simulator's CLI parses --help. - Sample initialize + tools/list session round-trips clean JSON responses with --leak=none, sidecar log captures protocol activity. - npm-wrapper variant emits the boilerplate BEFORE the JSON-RPC reply as designed. This commit is scaffolding only — actual variant characterization against a running stack is a follow-up. Each variant takes ~10-15 min of Sonnet wall-time at 50 turns, so running the full 6-variant matrix is a budgeted exercise rather than something to do in one CI pass. Refs #640 Refs #662 (parse_failures instrumentation prerequisite) Refs #678 (production manifestation that motivated this work) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(harness): document negative results for #640 — 6 hypotheses falsified Adds the results section to tests/harness/640/README.md after running 2 variants empirically (npm-wrapper, setsid-child) plus 4 hypotheses ruled out via static inspection of claude-cli's cli.js and Linux kernel behaviour. Also fixes a driver bug where cost was read from top-level `cost_usd` instead of `metadata.cost_usd` (real cost lives under metadata; the chat endpoint nests observability fields there). The harness as designed cannot reproduce #640 because every leak path it can exercise is on the MCP protocol pipe (claude-side), not on the agent-server claude-stdout pipe (which is where the wire corruption in #640 actually manifests). Claude+SDK isolate MCP child stdio correctly; Linux refuses /proc/*/fd/* bypass with ENXIO; Claude has no stray stdout writes in stream-json hot path. Negative results preserved so future #640 hunts don't re-walk the same paths. Real next step is landing PR #662 and getting prod-data- driven evidence on which package actually leaks. Refs #640 Refs #662 (diagnostic instrumentation prerequisite) Refs #678 (production manifestation) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tools (#699) (#703) * feat(voice): agent workspace page with canvas panel and Gemini panel tools (#699) Adds a full-page voice workspace at /agents/:name/workspace with a split layout: orb + controls on the left, an agent-controlled canvas panel on the right. Introduces four in-process panel tools (show_markdown, update_panel, append_to_panel, clear_panel) that let Gemini write structured content to the canvas during a voice conversation without delegating to the agent container. Panel state is polled at 300ms via a new GET /voice/{session_id}/panel endpoint. Workspace mode is gated on a new voice_available feature flag (GEMINI_API_KEY + VOICE_ENABLED) and surfaced via a BETA-badged button in AgentHeader. Security: panel content is DOMPurify-sanitised before v-html rendering; panel endpoint has session ownership checks; append_to_panel caps accumulated content at 512 KB to bound per-session memory. Tests: 7 new panel-tool unit tests (test_voice_tools.py); 5 new panel endpoint ownership tests (test_voice_auth.py). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(voice): add VOICE-008 to requirements + voice API table in architecture Adds VOICE-008 (Voice Workspace / #699) requirements entry and Phase 4 roadmap entry. Adds voice API endpoint table to architecture.md (was entirely absent) and updates feature-flags description to mention voice_available. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
) (#706) #704: Voice sessions written to Redis (dual-write with in-memory) so a WebSocket worker can auth-check a session created on a different Uvicorn process. VoiceService.create/get/remove_session are now async; Redis key TTL = VOICE_MAX_DURATION + 60 (360s); Redis failure at /voice/start raises loudly rather than silently producing an intermittently-failing session ID. #705: on_tool_call callback passed legacy actor_type=/actor_id=/actor_email= kwargs that don't exist on platform_audit_service.log(), causing a TypeError that silently swallowed every voice tool-call audit record. Fixed to use actor_user=types.SimpleNamespace(id=..., email=...) matching the _resolve_actor contract; wrapped in asyncio.create_task so the audit write doesn't block. Tests: +7 Redis fallback tests (TestRedisSessionFallback in test_voice_tools.py), +1 audit attribution source-inspection test (TestVoiceAuditAttribution in test_voice_auth.py). Catalog updated: 45 voice unit tests total. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nel (#707) (#709) - updated_at change-detection gate in fetchPanel() prevents 3x/sec Vue re-renders and stops empty state from overwriting content when session ends - in-flight guard (panelFetching flag) prevents overlapping 300ms requests - panel content preserved on session end; reset on new session start - replace v-html+sanitizedHtml computed with ref+renderHtmlPanel(): DOMPurify.sanitize(html, {ADD_TAGS:['script']}) + _execScripts() re-clones script nodes as live DOM so Chart.js new Chart() calls execute correctly - Chart.js 4.4.0 pre-loaded via injectChartJs() on mount (CDN, id-guarded) - WORKSPACE_PANEL_INSTRUCTIONS updated: document Chart.js pre-loaded rule Fixes #707 Co-authored-by: Claude <noreply@anthropic.com>
…factor ApiKeys.vue deleted; /api-keys now redirects to /settings?tab=mcp-keys. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#302) (#700) Splits the 2,600-line Settings page into 5 logical tabs (General, Access, Integrations, MCP Keys, Agents) with URL ?tab= deep-linking and ROLE-001 role-gated visibility. Absorbs the standalone /api-keys page into the new MCP Keys tab; preserves bookmarks via a permanent SPA redirect. Built test-first via Canon TDD against a 12-behavior list documented at docs/planning/302-settings-test-list.md. 14 Playwright tests pass; 13 match the test-list items 1:1 plus 1 regression test pinning the non-admin admin-403-bounce fix surfaced during /review. Behavior - /settings ?tab=<id> deep links to any tab. Unknown ?tab= falls back to the user's default tab. Browser back/forward navigates tab history. - Tab visibility gates by role: admin sees all 5 tabs; non-admin sees only MCP Keys (matches today's /api-keys page being non-admin). - Default tab: General for admin, MCP Keys for non-admin. - /api-keys redirects to /settings?tab=mcp-keys (static literal target, no open-redirect surface). - NavBar "Keys" link removed; Settings link now visible to all auth users (was admin-only). Implementation - New components/settings/McpKeysTab.vue extracted from views/ApiKeys.vue (deleted). Same auth posture preserved verbatim. - New composables/useRole.js mirrors backend ROLE-001 hierarchy (user < operator < creator < admin) for client-side UI gating. - New authStore.role getter sourced from /api/users/me; new fetchUserProfile() action populates role on login + session restore. - 13 existing Settings sections wrapped with v-if matching their tab. - watch(isAdmin, ..., { immediate: true }) guards admin-only data fetches so non-admin users don't trigger 403 → router.push('/') bounce — this was the bug surfaced by /review and fixed pre-merge. Security - Backend require_admin/require_role in routers/settings.py UNCHANGED. UI hiding is convenience, not the security boundary. A non-admin who edits localStorage.user.role = 'admin' sees all 5 tabs but every admin endpoint still returns 403 — UI bypass yields zero capability gain. - /api-keys redirect target is a hardcoded literal — no user input. - /review: 0 critical (after C1 fix), 5 informational. - /cso --diff: 0 critical / 0 high / 0 medium / 0 low. Acceptance criteria status - [x] Tabbed nav with ?tab= URL query param - [x] 12+ sections organized into 5 logical tabs - [x] MCP API Keys absorbed into Settings - [~] "Each tab a separate Vue component" — only McpKeysTab.vue extracted; the other 4 tabs remain inline v-if sections in Settings.vue. Follow-up issue worth filing for full extraction (state, methods, computed props need to move per-tab). - [x] NavBar simplified (Keys link removed) - [x] Non-admin users still access MCP key management - [x] No functionality lost — covered by behavior 11 regression test Test plan - 14/14 settings-tabs.spec.js pass (13 list-driven + 1 regression) - Existing smoke.spec.js updated (no longer asserts Keys link) - 3 unrelated session-tab.spec.js failures pre-exist on dev (unaffected) Out of scope (intentionally deferred) - Full tab-as-component extraction (4 remaining) - v-show vs v-if for modal-state preservation across tab switches - Vitest unit-test infra (frontend has Playwright e2e only) Fixes #302 Co-authored-by: Pavlo <pash@pashs-MacBook-Pro.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#726) Fixes three independent regressions in the voice workspace: 1. **test_voice_auth.py collection failure** — `_stub_docker_service()` was missing `docker_client` and four other attrs that `services/__init__.py` imports; adding a template_service stub also prevents a cascade from migration code that imports `services.credential_encryption`. Set `SECRET_KEY` env var early so the real config and `test_voice_tools.py` stub both sign/verify JWTs with the same key (avoids 4001 on ownership tests). 2. **test_voice_tools.py import error** — `services.gemini_voice` stub left in `sys.modules` by `test_voice_auth.py` blocked the real import of `GeminiVoiceService`. One-line eviction before the import fixes it. 3. **Chart.js CDN → bundle** — replace the dynamic CDN script injection with a proper `import Chart from 'chart.js/auto'` + `window.Chart = Chart` in `AgentWorkspace.vue`. The CDN approach was unreliable under load; the bundled path is deterministic. `fetchPanel` null-guard updated so panel content is never overwritten by an empty response after session end. Also exposes `VOICE_MODEL` env var in docker-compose and updates the default model identifier to `models/gemini-3.1-flash-live-preview`. All 45 tests in test_voice_auth.py + test_voice_tools.py pass. Closes #723 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
) (#727) * docs(validate-pr): add infrastructure change check to align with DEVELOPMENT_WORKFLOW.md Adds Step 4.7 to flag docker-compose/Dockerfile changes without justification, matching the Red Flags checklist in docs/DEVELOPMENT_WORKFLOW.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(schedules): restore auth parity on GET webhook status endpoint (#724) GET /{name}/schedules/{id}/webhook used AuthorizedAgent which calls db.get_agent_owner() and 404s for agents not in the ownership table. POST and DELETE already use name:str + can_user_access_agent; align GET to match so all three webhook endpoints behave consistently. Fixes #724 Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… files pass (#725) (#729) * fix(tests): patch sys.modules stubs in monitoring router and skill service user agent tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): add get_agent_default_resources stub to readiness probe test (#725) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): recover real fastapi in monitoring router loader (#725) test_inject_assigned_credentials.py permanently overwrites sys.modules['fastapi'] with a Mock at collection time via sys.modules.update(). When collected after that file (alphabetically 'i' < 'm'), _load_monitoring_router() exec'd monitoring.py with a mocked APIRouter, causing @router.get() to return a Mock instead of the original async function. asyncio.run() then raised TypeError on the Mock. Fix: briefly evict the polluted fastapi entry, re-import from disk to get the real module, restore the Mock, then include the real fastapi in the patch.dict context used during exec_module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…spin (#730) `safe_close_pipes` deadlocks on Python's BufferedReader internal lock when a subscription token is expired (no claude child ever spawns, reader threads stay alive). The deadlock wedges `asyncio.run(_drain_reader_threads(...))` in the executor thread for up to 7200 s at 87–91% CPU. Add `_drain_bounded()` in `claude_code.py`: runs the drain inside a daemon thread with a `threading.Event` + 90 s `done.wait()` budget. Replaces all 4 `asyncio.run(_drain_reader_threads(...))` call sites. `subprocess_pgroup.py` is untouched to minimise regression risk. 4 unit tests in `tests/unit/test_drain_bounded.py`. Fixes #728 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up to PR #700 (issue #302). Adds 6 new Playwright tests and a manual test runbook to lift coverage from "navigation works" to "regression-safe + integration". New e2e tests (in src/frontend/e2e/settings-tabs.spec.js): F1. Every section appears under its expected tab — replaces the limited behavior 11 (4 of 13 sections) with full 13-section regression. Uses exact:true match to disambiguate "API Keys" from "MCP API Keys". F2. Parametric deep links — 4 ?tab= IDs not already covered by behavior 2 (general, access, integrations, agents). Round out the 5-ID matrix. F3. MCP Keys CRUD integration — drives create + revoke through the UI, verifies state via API queries. Uses CLEANUP_PREFIX 'test-302-e2e- cleanup' for the test key name. afterEach hook + DELETE response assertion cleans up; manual recovery one-liner documented in the file header in case the hook somehow misses (zero stray rows observed in local runs). F4. Admin login fetches /api/users/me — pins the new fetchUserProfile() wiring from auth.js so a future refactor can't silently break role- based UI gating. F5. Re-click active tab does not push duplicate history — guards against regressing the early-return guard in selectTab(). F6. Non-admin does not see MCP Server URL section in MCP Keys tab — confirms the inner v-if="isAdmin" gate (independent of the tab-level v-if) works. Test infra changes - Whole spec file marked test.describe.configure({ mode: 'serial' }) because the CRUD test creates real keys and parallel workers race against the McpKeysTab list re-fetch + ensureDefaultKey side effects in headless mode. Total runtime: ~12s serial vs ~5s parallel — acceptable. - New cleanupTestMcpKeys helper used by afterEach hook. - New CLEANUP_PREFIX constant exported in the file header comment with the docker-exec recovery one-liner. Manual test plan (docs/testing/302-settings-tabbed-layout-manual-test- plan.md): 7-section runbook covering everything the e2e suite covers plus the things only humans can verify (visual UX, real non-admin session via DevTools localStorage spoof, /api-keys bookmark redirect demo). ~25 minutes for full pass. Verified - 23/23 settings-tabs.spec.js pass in 12.2s - 0 stray test rows in mcp_api_keys after a full run - No backend, docker, or CI changes — this is purely test-infrastructure hardening for a frontend-only feature Refs #302 Co-authored-by: Pavlo <pash@pashs-MacBook-Pro.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… failures (#708) (#719) When ALL initial Socket Mode connect attempts fail at backend boot (transient DNS slowness, edge throttle, etc. exceeding the 10s connect ceiling), `start()` now spawns a recovery supervisor task that retries in the background with the watchdog's exponential backoff (60→120→240→ 300s cap) until at least one client connects, then graduates to the per-client watchdog model. Pre-fix behavior: silent permanent-offline state until manual restart. The watchdog model assumed at-least-one-connection, leaving no path out of "zero contexts" once initial gather failed. Behavior matrix: - All initial succeed → no supervisor, watchdogs run (no overhead change) - Partial succeed → no supervisor, degraded mode unchanged - All initial fail (transient) → supervisor retries, exits on recovery - All initial fail (bad creds) → supervisor retries forever; backend HTTP stays fully responsive; ERROR "STARTUP UNREACHABLE" log fires after 3 consecutive failures for operator paging - Token format invalid (no xapp- prefix) → existing early-return path preserved, no supervisor (permanent error must not spin) - stop() called mid-supervisor → supervisor cancelled cleanly, await propagates CancelledError, no zombie task main.py: stop nilling _slack_transport when initial connect fails so the supervisor task isn't orphaned. Test coverage: 10 new unit tests (TestStartupRecoverySupervisor) + 1 flipped existing (test_start_aborts_when_all_clients_fail → test_start_spawns_supervisor_when_all_clients_fail). 65/65 pass. End-to-end smoke verified against running backend with extra_hosts DNS poisoning + bad-credentials scenarios; backend HTTP confirmed responsive throughout. Co-authored-by: Pavlo <pash@pashs-MacBook-Pro.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…changeme propagation paths (#692) - docker-compose.yml: TRINITY_PASSWORD now reads ADMIN_PASSWORD directly - docker-compose.prod.yml: switch to fail-loud ${ADMIN_PASSWORD:?...} on both backend and mcp-server - .env.example: collapse duplicate FRONTEND_URL; add GOOGLE_API_KEY, LOG_*, TRINITY_DATA_PATH, HOST_TEMPLATES_PATH - src/mcp-server/src/server.ts: drop || "changeme" fallback; throw on startup when MCP_REQUIRE_API_KEY=false and no usable credential - scripts/deploy/gcp-deploy.sh: refuse to deploy if ADMIN_PASSWORD is unset or literally "changeme" - deploy.config.example: drop "changeme" default - docs: update mcp-orchestration flow, single-server deploy guide, TEST_REPORT historical note, feature-flows index Default MCP_REQUIRE_API_KEY=true mode is unaffected. Closes #692 Co-Authored-By: AndriiPasternak31 <andriipasternak31@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves all 49 failures + 2 collection errors in `uv run pytest tests/unit/`. Final state: 754 passed, 1 skipped, 0 failed across 3 consecutive runs. Production schema/migrations and backend code are untouched. Failure groups fixed: - A (14): missing backend deps in tests/requirements-test.txt - B (4): test_telegram_webhook_backfill missing platform_audit_service stub - C (2): test_fleet_sync_audit S7 partial UNIQUE index prevents seeding duplicate-binding state - D (28): test_file_upload cascading pollution from test_backlog tmp_db + test_slack_watchdog sys.modules stub - E (4+1): test_git_status_dual_ahead_behind inverted args + agent_server package shadow - F (1): test_agent_server_auto_sync same package shadow as E Also adds slackify-markdown>=0.2.0 floor for supply-chain consistency. Co-Authored-By: Andrii Pasternak <andriipasternak31@gmail.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#712) Adds 11 tables, 14 columns, and ~22 indexes that existed only in migrations.py to schema.py, restoring it as a faithful reference for the database (Architectural Invariant #3). Mechanical and additive. Migrations are append-only history. Existing databases unaffected — every new statement uses IF NOT EXISTS. Tables added: subscription_credentials, agent_notifications, subscription_rate_limit_events, slack_workspaces, slack_channel_agents, slack_active_threads, telegram_bindings, telegram_chat_links, telegram_group_configs, whatsapp_bindings, whatsapp_chat_links. Columns added: agent_ownership (full_capabilities, max_backlog_depth, voice_system_prompt), schedule_executions (source_user_id, source_user_email, source_agent_name, source_mcp_key_id, source_mcp_key_name, claude_session_id, queued_at, backlog_metadata, fan_out_id), agent_schedules (webhook_token, webhook_enabled). Indexes added: subscription, notification, rate-limit, multi-agent slack, telegram, whatsapp, plus partial indexes for execution backlog (idx_executions_queued), retry (idx_executions_pending_retry), fan-out (idx_executions_fan_out), webhook tokens (idx_schedules_webhook_token), and proactive sharing (idx_agent_sharing_proactive). Verified: name-only and strict DDL parity scripts both pass with zero missing and zero different entries. test_migrations.py 17/17. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports the metrics skill from feature/704-705-voice-bugs to dev. Provides velocity, cycle time, bug ratio, and backlog health reporting via GitHub Issues + project board data. Includes the 2026-05-07 baseline report generated during initial development. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dependabot had no target-branch configured, so all version-update PRs opened against main — violating the feature → dev → main SDLC and accumulating merge conflicts against dev-first changes. Set target-branch: dev on all five ecosystems. Takes effect when this lands on main (next release cut); the config change itself triggers an immediate Dependabot re-run, which recreates the dropped PRs against dev. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
) (#1138) Google retired gemini-2.0-flash (404 NOT_FOUND), breaking Telegram voice transcription entirely and silently degrading image-gen prompt refinement. The model id was hardcoded in both services with no config escape hatch. - Add GEMINI_TEXT_MODEL / GEMINI_TRANSCRIPTION_MODEL to config.py (default gemini-3.5-flash, live-verified for text + inline audio; 2.5-flash was rejected: it carries an announced Oct 2026 shutdown) - or-coalesce + non-empty compose defaults + commented .env.example entries, mirroring the VOICE_MODEL pattern (#1076) so an empty env var can't shadow the default - Wire both vars through docker-compose.yml AND docker-compose.prod.yml (#1039 packaging-gap class) - Explicit retired-model log hints on 404 in both call paths so the next retirement is a same-day config fix, not a user-reported bug - Regression-guard test: text_refinement model must not be gemini-2.0*/1.x - Feature flows synced (image-generation, telegram-integration, agent-avatars) Agent-server model allowlist still offers retired models — tracked separately in #1137 (different deploy surface, needs base-image rebuild). Fixes #1130 Co-authored-by: Eugene Vyborov <eugene@beingluminous.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- agent-guardrails.md: Guardrails tab renamed to Settings (#1108) — GuardrailsPanel now section #1 inside components/settings/SettingsPanel.vue, ?tab=guardrails deep-link alias, refreshed AgentDetail wiring + Key Files - agent-monitoring.md: rewrite stale Frontend Layer that still documented the deleted /monitoring route and views/Monitoring.vue as live; now describes the redirect + admin-gated Health tab (MonitoringPanel.vue, #1109) - agent-overview-dashboard.md: note the Health section empty state when monitoring is off (6df68c9) - feature-flows.md: add Recent Updates rows for #1130 and #1108 Refs #1108, #1109, #1130 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the accreted architecture doc with a deduplicated rewrite: each cross-cutting feature (capacity, circuit breakers, heartbeat, soft-delete/retention, session tab, VoIP, canary, ...) now has exactly one canonical block; catalogs and endpoint tables point to it instead of repeating behavior. Content-equivalent (495 lines vs 1192 removed — redundancy and changelog narration only); the #868 tenant-boundary warning is preserved explicitly. Previous version archived at docs/archive/memory/architecture-2026-06-11.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bulk clear actions for the Operating Room queue and agent notifications:
- POST /api/operator-queue/bulk-cancel — cancels only the ids the operator
actually saw (race-safe vs the 5s sync loop); per-id accessible-agent
scoping; returns {cancelled, skipped}
- POST /api/operator-queue/clear-resolved — hides terminal rows
(acknowledged/cancelled/expired) via a new cleared_at column. NOT a
DELETE: the sync loop re-creates DB-missing items whose agent-file entry
still says pending (always true for expired, and for cancelled inside
the propagation window). Deletion deferred to the retention sweep (#1142)
- POST /api/notifications/dismiss-all — pending+acknowledged → dismissed
in one statement, same accessible-set the list endpoint uses
- Sync write-back now propagates cancelled/expired status into agent queue
files (in-place flips only) so agents stop waiting on cleared items
- Respond-vs-bulk-cancel race now returns 409 instead of a silent 200
that never recorded the response
- One audit-log entry + one WS event (operator_queue_cleared /
notifications_cleared) per bulk op; stores refetch on receipt
- Operations.vue: per-tab Clear All button (hidden when empty) with
blast-radius confirmation; Resolved feed now shows cancelled/expired
- Tests: tests/test_ops_clear_all.py (20 tests — scoping isolation,
tri-state accessible-set, idempotency, 409 marker, hide-not-delete)
Closes #1017
Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…1137) (#1144) Follow-up from #1130. The agent-server model endpoints still advertised Gemini models Google has retired, so a gemini-cli runtime agent could select one and get a 404 from Google at runtime. - GET /api/model `available_models`: replace the gemini-2.0-flash entry with the live set — gemini-3-pro, gemini-3-flash, gemini-2.5-pro, gemini-2.5-flash; update the note accordingly. - PUT /api/model `valid_models` allowlist: drop gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash; keep only the four live models. Update the 400 error hint. The `startswith("gemini-")` forward-compat fallback is retained intentionally. - gemini_runtime.py pricing table: comment the 2.0-flash / 2.0-flash-lite entries as historical — kept only so cost calc of older executions still resolves, not offered in the lists. Model set sourced from the runtime's own GEMINI_PRICING table (default gemini-3-flash), not the issue's parenthetical guesses. Base image rebuilt (trinity-agent-base:0.6.0/latest); new lists verified inside the image. Related to #1137 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion timeout (#1094) (#1147) * fix(agent-server): distinct error for stall-watchdog kill vs max-duration timeout (#1094) The per-tool no-output stall watchdog and the max-duration budget are two distinct termination paths, but the terminal 504 always claimed "Task execution timed out after {effective_timeout} seconds" regardless of which fired. A 300s stall-kill at ~531s wall-clock was therefore persisted as "timed out after 2400 seconds", misleading operators into bumping the execution timeout — the wrong knob (the 300s no-output threshold is what actually fired). The watchdog already distinguished the two causes for its log line but discarded that for the persisted error. Now: - `HeadlessRunContext` carries `termination_reason` ("stall_no_output" | "max_duration") and `stalled_tool`, set in the watchdog's `except subprocess.TimeoutExpired` before re-raising. - The orchestrator builds a reason-specific 504. Stall returns a structured dict detail `{message, termination_reason, stalled_tool}`; max-duration returns `{message, termination_reason}`. The backend's existing HTTPError handler (task_execution_service.py:1019-1021) reads `detail["message"]`, so the persisted `schedule_executions.error` now reflects the real cause and the structured reason rides along for the UI/API — no DB migration. - The "timed out after Ns" label is now reserved for genuine budget exhaustion (elapsed ≈ N), as the issue asked. Out of scope (per issue): a per-task override of the 300s stall threshold, and a dedicated termination_reason DB column. Tests: tests/unit/test_1094_stall_termination_reason.py asserts both ctx outcomes via the #970 fake-Popen harness; full #970 wait-loop suite still green (11 passed). Base image rebuilt (trinity-agent-base:0.6.0/latest); fix verified inside the image. Related to #1094 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): make #1094 stall test self-contained (CI collection error) The regression-diff CI runs `cd tests; pytest unit/` and collects each changed test file in isolation, where `tests/unit` is not on sys.path — so `from test_headless_executor_970_timeout import ...` raised ModuleNotFoundError at collection (reported as a new [E] under HEAD). It only passed locally because I ran it next to the #970 file. Inline the small fake-Popen harness (ctx/_FakePopen/_FakePipe/ patched_loop/_install_popen) instead of importing from the sibling test module. Verified CI-style: `pytest unit/test_1094_...py` from tests/ under pytest-randomly now passes (2), and 11 pass alongside the #970 file. Related to #1094 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-server): structured termination_reason on outer-timeout path too (#1094 review) Review follow-up: the inner subprocess.TimeoutExpired max-duration branch returned a structured detail dict carrying termination_reason, but the sibling outer asyncio.TimeoutError safety-net raise — same semantic cause (budget exhausted) — still emitted a plain string. A consumer filtering executions on termination_reason="max_duration" would miss budget timeouts that fired via the outer net. Make the two exits symmetric. Related to #1094 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(default OFF) (#1121) (#1123) * fix(monitoring): wire fleet-health loop to lifespan, persist enabled (default OFF) (#1121) The MON-001 fleet-monitoring loop was never started at backend boot — the only path that started it was an admin calling POST /api/monitoring/enable. Every trinity-backend restart silently killed the loop and left it off until a human re-enabled it; the persisted monitoring_config was never consulted on startup. The documented "30s monitoring loop is authoritative" invariant was silently false on any restarted instance. - main.py lifespan(): delayed (+12s) resume that reads the persisted monitoring_config and starts the loop only when enabled — survives restarts. Best-effort stop added to shutdown for parity with the other loops. - MonitoringConfig.enabled now defaults to OFF and is the single source of truth (was dead code defaulting to True). enable/disable and PUT /config persist the flag and reconcile the running loop. - Negative/zero *_check_interval rejected via a Pydantic validator (422 on request body; persisted bad config falls back to DEFAULT). Loop also clamps its sleep to >=1s as a belt-and-suspenders guard against a flood loop. - routers/monitoring.py: shared load_persisted_monitoring_config() + _persist_monitoring_enabled() helpers (one reader, no drift). - architecture.md: monitoring loop now described as lifespan-wired and gated by the persisted default-off setting; coordinated the #307 heartbeat "stays authoritative" wording. - test_monitoring.py: test_update_config_validation now asserts 422 for non-positive intervals (it previously asserted the unvalidated 200). 7-day health-check retention (#772) unchanged; longer retention is a follow-up Enterprise knob (#1049) per the issue. Verified end-to-end on the live stack: enable -> restart -> loop resumes (fleet checks every ~30s); disable -> restart -> no loop; bad intervals -> 422. Related to #1121 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(monitoring): address review — partial-PUT preserves enabled, inline start/stop (#1121) Two issues from PR review of the enable/disable/PUT reconcile path: 1. Partial `PUT /config` silently disabled monitoring. Every MonitoringConfig field has a default, so a body omitting `enabled` deserialized to `enabled=False` and the reconcile stopped a running loop on a routine interval tweak. Now only treat `enabled` as authoritative when the client explicitly sent it (`model_fields_set`); otherwise preserve the persisted run-state. 2. enable→disable race left the loop running against persisted `enabled=False`: enable scheduled the start as a background task, so a racing disable could no-op (is_running still False) before the start ran. start()/stop() only create/cancel the loop task and don't block, so enable/disable/PUT now reconcile inline (awaited) — runtime state matches the persisted flag before the response returns. Dropped the now-unused background_tasks params. Verified live: enable -> is_running true immediately; partial PUT keeps enabled+loop; explicit enabled:false PUT stops the loop; left clean (disabled). Related to #1121 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Bump vulnerable npm dependencies flagged by Dependabot: - vitest 2.1.8 -> 3.2.6 in tests/git-sync (GHSA-5xrq-8626-4rwp, critical — Vitest UI arbitrary file read/exec; alert #150) - js-cookie -> 3.0.8 via overrides in tests/git-sync (GHSA-qjx8-664m-686j, high — prototype hijack in assign(); alert #127). Transitive via js-beautify; pinned with an overrides entry. - fast-uri -> 3.1.2 via root overrides (GHSA-v39h-62p7-jpjc #121 + GHSA-q3j6-qgpj-74h6 #113, high — host confusion / path traversal). Transitive via ajv; pinned to ^3.1.2 to stay within ajv's expected 3.x range rather than jumping to the 4.x major. git-sync vitest suite passes (10/10) on the new versions. Related to #1140 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1126) (#1128) Backend (the confirmed root cause): every container-creation site passed `cpu_count` to docker-py, which maps to the Windows-only HostConfig.CpuCount and leaves NanoCpus=0 on Linux — so NO agent ever got a CPU cgroup limit (created, recreated, or system). Memory was unaffected (`mem_limit` is the correct Linux param). Replaced with `nano_cpus = cores * 1e9` (Linux CFS quota) at all three sites: crud.py (create), lifecycle.py (recreate), system_agent_service.py (system agent). Frontend: - ResourceModal: pre-select the EFFECTIVE value (`cpu ?? current_cpu`, `memory ?? current_memory`) instead of the DB override (null when unset), so the dialog reflects what the agent actually runs with; the null option becomes an explicit "Inherit default". Adds a load-error banner. - useAgentSettings: `loadResourceLimits` records `error` (surfaced in the modal) instead of silently swallowing; `updateResourceLimits` returns a success boolean. - AgentHeader: show the configured core count + max memory alongside the live CPU/MEM gauges while running (was only shown when stopped). - AgentDetail.saveResourceLimits: gate the restart on the container actually reaching a stopped state (poll via fetchAgent, 30s cap) instead of a fixed 1s sleep, skip restart if the save failed, and report restart failures to the user instead of leaving the agent stopped silently. Verified live: PUT cpu + restart → HostConfig.NanoCpus = cores*1e9 (0 → 1e9 for 1 core, → 2e9 for 2 cores), label trinity.cpu and the cgroup agree, memory unchanged. UI files compile (vue/compiler-sfc). Optional capacity-pressure hint: deferred (issue marks it drop-if-not-easy). Related to #1126 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ci): point Dependabot version updates at dev, not main
…e-by-default (#1129) (#1132) * feat(security): fleet-wide default for require-verified-email, secure-by-default (#1129) Promote the per-agent `require_email` access-policy flag (#311) to an admin-controlled fleet-wide default that defaults ON, so new agents are secure-by-default instead of opt-in per agent. Mirrors the RES-001 agent-defaults/resources pattern. - settings_service: AGENT_DEFAULT_REQUIRE_EMAIL_KEY + get_agent_default_require_email() (default ON; resolved from system_settings, code fallback so fresh installs are secure without a data migration). - routers/settings: GET/PUT /api/settings/agent-defaults/access-policy (admin-only) + AgentDefaultAccessPolicyUpdate model. - register_agent_owner (db/agents + database facade): new require_email param, written explicitly in the INSERT (same SQLite-baked-default reasoning as #665's execution_timeout_seconds). Defaults False so the system agent is unaffected. - crud: seed require_email from the platform default at user-agent creation. - Settings UI: admin toggle in the Platform section, consistent with Default Model. - No schema change (column + system_settings already exist); existing agents are never rewritten — the default is read only at creation time. Verified live: default GET = ON; new agent seeds require_email ON; flip default OFF → next new agent OFF; existing agent unaffected by the flip; per-agent override still works; UI toggle renders + reflects the default. Throwaway agents deleted, default restored ON. Related to #1129 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): audit-log the agent-default access-policy change (#1129 review) PUT /api/settings/agent-defaults/access-policy wrote the fleet-wide require_email default via db.set_setting() with no audit trail, while every other settings write in the router (API-key, GitHub PAT, generic /{key}) emits AuditEventType.CONFIGURATION. Flipping a security default must leave a trace — add the same platform_audit_service.log() call. Verified live: flipping the default writes a `settings_change` audit row with {setting: agent_default_require_email, require_email: <new>}. Related to #1129 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
…small for build scratch (#1098) (#1124) Agent containers mount /tmp as a 100 MB RAM-backed tmpfs (noexec,nosuid). Heavy scratch — pip/npm install, compiling C extensions, ML wheels (torch/transformers) — overflows it with "No space left on device" even though the home volume has hundreds of GB free, and noexec breaks compile-and-exec-from-tmp. This blocks a core class of agent setup work. Fix: set a default TMPDIR=/home/developer/.tmp on the disk-backed, exec-capable home volume. pip/npm/most build tooling honor TMPDIR, so this redirects scratch onto real disk and dodges noexec in one move — while keeping /tmp's hardened noexec,nosuid posture unchanged. - New constants AGENT_TMPFS_MOUNT + AGENT_DEFAULT_TMPDIR in the low-dep capabilities.py (single source of truth). Wired into all three provisioning paths so create / recreate / system-agent can't drift: crud.py (create), lifecycle.py (recreate, setdefault so a template/user TMPDIR carried on the container wins), system_agent_service.py. - startup.sh creates the dir (idempotent, chmod 700) before its own pip install, so existing agents pick it up on restart/recreate and tools honoring TMPDIR land on disk. - architecture.md: note the TMPDIR redirect alongside the /tmp tmpfs claim. Requires a base-image rebuild (startup.sh change) — start.sh / build-base-image.sh handles it; env + startup.sh ship together. Verified e2e on a fresh agent (rebuilt base image): container env carries TMPDIR=/home/developer/.tmp; startup.sh auto-creates the dir (700, developer-owned) on the 600 GB-free home disk; tempfile.mkdtemp() lands there; 150 MB scratch succeeds via TMPDIR but caps at exactly 100 MB on /tmp ("No space left"); /tmp stays noexec,nosuid,size=100m. Related to #1098 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
…timeline views (#1150) (#1152) * feat(analytics): loop executions as a distinct Loops bucket/color in timeline views (#1150) - Remap triggered_by=loop from Scheduled to a new Loops bucket in _TRIGGER_BUCKETS / _BUCKET_ORDER (read-time bucketing — historical loop rows remap automatically, no migration) - BUCKET_COLORS: Loops = fuchsia-400, a deliberate accent so loop bursts read distinctly from Scheduled (cyan) and Agent-to-agent (teal) - ExecutionsPanel: fuchsia loop trigger chip (light+dark) + loop option in the trigger filter dropdown - routers/executions.py: add loop to _VALID_TRIGGERS — without it the new dropdown option would be a silent no-op (router nulls unknown values) - StackedBarChart: slate fallback for buckets missing from the colors map (stale bundle vs newer backend renders gray, not invisible) - Tests: Loops bucket assertions + dropdown/_VALID_TRIGGERS drift guard Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: add Loops bucket to agent-overview-dashboard flow (#1150) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: sync executions-dashboard, overview, and run-agent-loop flows with Loops bucket (#1150) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Eugene Vyborov <eugene@beingluminous.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Two new pages: sequential agent loops (automation/agent-loops.md) and outbound VoIP telephony (advanced/voip-telephony.md). Updated 17 pages for the unified Operations nav, Agent Detail Overview tab + tab overflow, Guardrails-to-Settings rename, dispatch circuit breaker, fleet executions dashboard, per-schedule analytics, idempotency keys, heartbeat liveness, operator-queue MCP tools, and the fleet-wide require-verified-email default. MCP tool count corrected to 80; fixed stale routes, broken links, and a leaked internal codename. Refs #1106 #1056 #1109 #1107 #1108 #852 #868 #525 #526 #307 #1101 #1129 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n sweeper (#1153) (#1154) The periodic cgroup orphan sweeper (#817) SIGKILLed operator `docker exec` sessions within 0–30s — the same class of actor (an operator/platform process maintaining the agent) that the allowlist already deliberately protects for SSH. A long `docker exec agent-X sh -c '...sleep...'` died with exit 137; weeks of FAISS index-build failures were misdiagnosed as OOM/capacity. Root cause: a `docker exec` entry process matches none of the allowlist sources (no execution id, no env tag, no shared pgid, not sshd) and was reaped as an orphan — yet it is exactly the SSH-session case. Fix: - orphan_allowlist: new `_docker_exec_session_pids()` — inside the container PID namespace a `docker exec` entry has **PPID 0** (real parent is outside the namespace); genuinely-leaked orphans reparent to **PID 1**, never 0, so PPID 0 is an unspoofable "operator exec session" signal. Allowlist every PPID-0 process (excluding PID 1, already hard-protected) + its descendant tree. Wired into `resolve_allowlist`. When the session exits, leftover children reparent to PID 1 and fall out → reaped next sweep (#817 guarantee preserved). - orphan_sweep: each real kill now logs at **WARNING** with `pid` + a length-capped `cmd=` (was INFO). One such line turns the weeks-long "which process keeps dying?" misdiagnosis into a 5-minute fix. dry_run stays INFO. cmdline capped at 200 chars. - docs: TRINITY_COMPATIBLE_AGENT_GUIDE gains a "Long-Running & Background Processes (the orphan sweeper)" section — the `persistent-processes.allow` escape hatch and "run maintenance as a platform execution" guidance. Tests: tests/unit/test_1153_docker_exec_allowlist.py (5) — PPID-0 entry + descendants protected, PID 1 (and its children) excluded, reparented orphan not protected, resolve_allowlist end-to-end, post-exit child reaped. #912 suite still green (13 passed together). Live-verified on a fresh trinity-agent-base:0.6.0 agent: a 30s `docker exec` loop survived a real mid-flight sweep and exited 0; agent-server + sshd spared under the production sweep_pid; a planted PPID-1 orphan was reaped. Related to #1153 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt loops into a usage guide Verified each abilities plugin against its actual skill directories (README lags the repo): create-agent 13 wizards (+doctor), agent-dev 19 skills (claim/close renames, git-sync, backlog dev cycle, pipelines), trinity 6 skills (+loop, create-dashboard, deploy-new-instance), dev-methodology 24 skills. Loops documentation: - automation/agent-loops.md: new Common Patterns section (iterative refinement, agentic retry, bounded polling, backlog draining) and Writing Good Until-Mode Loops guidance (verifiable sentinels, caps, per-run timeouts, stall watching); /trinity:loop entry point - abilities/trinity-plugin.md: documents /trinity:loop usage, modifiers, and when-to-use-what vs chat/fan-out/schedules Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t` (#1162) The CLI shipped an obfuscated third-party API credential in trinity_cli/_notify.py, used to email an access request during `trinity init` when no instance URL was given. Any installed copy (PyPI/Homebrew) could recover and use the key. Delete _notify.py and replace the no-URL branch of `trinity init` with plain instructions to email access@ability.ai directly — the CLI now carries no credential and makes no outbound send. The credential has been revoked in the provider console. Refs #1158 Co-authored-by: Eugene Vyborov <eugene@beingluminous.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…avoid duplicate commit sha The build-stamped VERSION is "X.Y.Z+g<sha>" (#993), and the NavBar chip (#926) appends git_commit_short again, rendering e.g. "v0.6.0+gcccac44d · cccac44". Add displayVersion to useBuildInfo (version with the "+..." suffix stripped) and use it in the NavBar chip, build-info modal, and Settings build panel — commit stays its own field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skills updated in trinity-dev: /review (plan completion audit, merge-base diffing, evidence-gated findings), /cso (calibration + refuter verification), /autoplan (user-challenge gate, full-depth contract), /release (landing verification); workflow doc reconciled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ped (#1143) (#1145) `stores/notifications.js:fetchPendingCount()` read `count` off `GET /api/notifications?status=pending&limit=1`, where `count` is `len(returned page)` — so the polled NavBar badge clamped to 1 every 60s regardless of how many pending notifications existed. - Add `GET /api/notifications/count?status=pending` — returns the true total scoped to the caller's accessible agents (mirrors the per-agent `/api/agents/{name}/notifications/count`). Registered before the `/{notification_id}` catch-all (Invariant #4). - `db.count_pending_notifications` gains an `agent_names` list filter (fleet count over accessible agents); empty list → 0, not "all" (avoids invalid `IN ()`). - Point `fetchPendingCount()` at the new endpoint. Verified live: a fleet with 32 pending now reports 32 (old limit=1 query still returns 1); `/{id}` route unshadowed (404 on unknown id); invalid/unsupported status → 400; unauth → 401. Related to #1143 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
…shop videos - Restructure Quick Start around the abilities plugin marketplace; CLI moves to "Other ways to deploy" (still fully documented) - Update Abilities section to the current plugin inventory (four-step lifecycle, trinity plugin's 6 skills, doctor wizard, accurate skill counts) - Add AGENTS.md: task router, key facts, and verification steps for AI agents; referenced from the top of the README (discovery by reference) - Point agents to docs/user-docs/README.md as the detailed docs index - Add workshop video links (watch-more list + Thursday archive), Cornelius repo link; fix workshops URL (workshopsgt -> workshops) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… to main Reconciles divergence before the 0.6.1 release cut. README kept at dev's newer version (main's copy was stale and carried a workshops-link typo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> # Conflicts: # README.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…release gate The dev→main PR's unit-suite regression gate surfaced four test-only breaks (invisible on dev pushes, where the base/head matrix doesn't run): - test_1073_voip_media_stream_ticket: config stub missing VOIP_MAX_CALL_DURATION (added by #1091) — the import error aborted the entire head-side collection - test_telegram_webhook_backfill: models/settings_service stubs missing the #1129 names (AgentDefaultAccessPolicyUpdate, AGENT_DEFAULT_REQUIRE_EMAIL*) - test_image_generation_service: config mock missing GEMINI_TEXT_MODEL (#1130 made the text model configurable; MagicMock repr leaked into the URL) - test_admin_recovery: patch DB_PATH via the get_db_connection globals each ops module actually captured, so sibling tests that replace the db package in sys.modules can't redirect the fixture's tmp DB Full unit suite verified locally under all three CI seeds (12345/67890/99999): 2124 passed; only the two pre-existing test_orphaned_execution_recovery failures remain, which also fail on base/main, so the diff gate ignores them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v0.6.1 — cumulative changes since v0.6.0 (108 commits, 34 issues).
Full notes:
docs/releases/v0.6.1.md(ships in this PR).Trinity v0.6.1
Released: 2026-06-12 · Tag:
v0.6.1Diff:
v0.6.0...v0.6.1· 108 commits · 34 issues · 6 contributorsA UI/UX-and-reliability release. Headline themes: the Operations area consolidation (Health + Ops + Executions in one tabbed view, #1109), the Agent Detail Overview dashboard (#1107), sequential agent loops shipped end-to-end (MCP tool #740 + web UI #1106), VoIP telephony Phase 1 — agents place outbound phone calls over Twilio Media Streams + Gemini Live (#1056), a fleet-wide secure-by-default access policy (#1129), and a model-catalog refresh adding Opus 4.8 and retiring EOL Gemini models (#1080, #1130, #1137).
This document lists every issue shipped in the release (all 34
status-in-devissues closed on merge), grouped by type.Features (14)
Fixes (15)
<Parameter>)Refactors (5)
Other changes
trinity init— security fix; merging this release tomaintriggerspublish-cli.yml(path trigger onsrc/cli/**), publishing a patchedtrinity-clito PyPI that purges the revoked credential (release(cli): cut patch trinity-cli release to purge revoked credential from published packages #1163)main: GitHub Actions version bumps (checkout v6, cache v5, download-artifact v8, github-script v9, tailscale), Docker base-image bumps, npm patch/minor groupsUpgrade notes
timeout_secondsoverride removed (demotion PR 1: drop per-task timeout_seconds override (→ envelope deadline) #1068): task dispatch no longer accepts a per-task timeout; the agent'sexecution_timeout_seconds(envelope deadline) governs. Schedule-leveltimeout_secondsis unaffected.require_email = ON(Make 'require verified email' access policy a fleet-wide default config (secure-by-default) #1129): fleet-wide default applies to newly created agents only; existing agents keep their current access policy. Admins can change the default in Settings → Agent Defaults.TMPDIRnow defaults to the home volume (fix: agent /tmp is a 100MB noexec tmpfs — starves pip/npm/build scratch; default TMPDIR to home volume #1098): containers pick this up on next recreate/restart; no action needed./monitoring,/executions,/operating-room,/eventsnow redirect (query-preserving) to/operations?tab=.... Update any bookmarks or external links.Contributors: Eugene Vyborov, dolho, dependabot[bot], andrii.pasternak, obasilakis, oleksandr-korin
Closes #18
Closes #307
Closes #548
Closes #740
Closes #868
Closes #951
Closes #993
Closes #1017
Closes #1023
Closes #1026
Closes #1037
Closes #1042
Closes #1056
Closes #1068
Closes #1073
Closes #1076
Closes #1080
Closes #1094
Closes #1098
Closes #1101
Closes #1106
Closes #1107
Closes #1108
Closes #1109
Closes #1114
Closes #1121
Closes #1126
Closes #1129
Closes #1130
Closes #1137
Closes #1140
Closes #1143
Closes #1150
Closes #1153
Pre-release checklist passed 2026-06-12. Ready for squash-merge.
v0.6.1publish-cli.yml(fires —src/cli/**changed, PR fix(cli): remove embedded credential from trinity init #1162) andsync-docs-to-vertex.yml(fires —docs/user-docs/**changed)