Fix: Add missing Docker labels to system agent container#1
Merged
Conversation
The system agent was missing required Docker labels (trinity.ssh-port, trinity.cpu, trinity.memory, trinity.created), causing port allocation conflicts when creating new agents. Without trinity.ssh-port label: - get_agent_status_from_container() returns port=0 - get_next_available_port() filters out port 0 - System agent's port (2290) appears available - New agents fail with 'port already allocated' error This fix adds all required labels to match the standard agent creation pattern in routers/agents.py, ensuring proper port tracking and conflict prevention.
vybe
added a commit
that referenced
this pull request
Dec 27, 2025
Bug #1: Terminal session lost when switching tabs - Changed v-if to v-show for terminal tab content in AgentDetail.vue - Keeps terminal component mounted, preserving WebSocket connection Bug #2: MCP deploy_local_agent only copied CLAUDE.md - Updated startup.sh to copy ALL template files instead of hardcoded list - Now includes template.yaml and custom directories (src/, lib/, etc.) - Added .trinity-initialized marker to prevent re-copying on restart 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6 tasks
This was referenced Apr 20, 2026
Closed
4 tasks
dolho
added a commit
that referenced
this pull request
Apr 23, 2026
Move the pre-check exec logic out of routers/internal.py into a dedicated services/pre_check_service.py. Aligns with Invariant #1 (Router → Service → DB): the router becomes a 5-line passthrough, all business logic (path constant, two exec calls, stdout capping, contract dict) lives in the service. Same shape as services/slot_service.py, services/task_execution_service.py, services/monitoring_service.py — testable in isolation, discoverable via grep, won't trip /validate-architecture later. Behavior unchanged. 13/13 scheduler unit tests still pass; live endpoint hit returns same contract dict. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dolho
added a commit
that referenced
this pull request
Apr 24, 2026
Move the pre-check exec logic out of routers/internal.py into a dedicated services/pre_check_service.py. Aligns with Invariant #1 (Router → Service → DB): the router becomes a 5-line passthrough, all business logic (path constant, two exec calls, stdout capping, contract dict) lives in the service. Same shape as services/slot_service.py, services/task_execution_service.py, services/monitoring_service.py — testable in isolation, discoverable via grep, won't trip /validate-architecture later. Behavior unchanged. 13/13 scheduler unit tests still pass; live endpoint hit returns same contract dict. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
4 tasks
vybe
pushed a commit
that referenced
this pull request
Apr 27, 2026
… (#501) Replaces raw `DELETE FROM agent_git_config` SQL in routers/git.py with the existing `db.delete_git_config()` method (already used at line 435 of the same file for the init-failure rollback path). Restores Architectural Invariant #1 (Three-Layer Backend) for this router. No behavior change — identical SQL, identical parameter binding. Closes #451 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dolho
added a commit
that referenced
this pull request
Apr 27, 2026
Move the pre-check exec logic out of routers/internal.py into a dedicated services/pre_check_service.py. Aligns with Invariant #1 (Router → Service → DB): the router becomes a 5-line passthrough, all business logic (path constant, two exec calls, stdout capping, contract dict) lives in the service. Same shape as services/slot_service.py, services/task_execution_service.py, services/monitoring_service.py — testable in isolation, discoverable via grep, won't trip /validate-architecture later. Behavior unchanged. 13/13 scheduler unit tests still pass; live endpoint hit returns same contract dict. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
pushed a commit
that referenced
this pull request
Apr 28, 2026
* feat(scheduler): agent-owned pre-check hook (#454) New optional contract: agents implement POST /api/pre-check in their container; scheduler calls it before firing a cron-triggered chat. Endpoint absent or any error → fire as usual (fail-open). fire=false records a skipped execution. fire=true with a message overrides the schedule.message for that invocation. - docker/base-image/agent_server/routers/pre_check.py: new router that dynamically loads /home/developer/.trinity/pre-check.py (template- supplied) and calls its check() function - agent-server main.py: mount pre_check_router - scheduler/agent_client.py: pre_check() method with fail-open semantics on 404/5xx/timeout/malformed-response - scheduler/service.py: _run_pre_check + pre-check branch in _execute_schedule_with_lock (cron only; manual triggers bypass) - tests/scheduler_tests/test_pre_check.py: 12 tests covering client- and service-level behavior; 161/161 scheduler suite passes Zero schema change — reuses existing ExecutionStatus.SKIPPED and create_skipped_execution. Closes the "wake agent on every cron tick" cost gap noted in docs/planning/PR_REVIEWER_AGENT.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(#454): scheduler pre-check feature flow + arch + requirements - feature-flows/scheduler-pre-check.md: new flow doc with contract, fail-open semantics, error table, testing summary - architecture.md: add /api/pre-check to agent-server endpoint list and pre-check note to Scheduler Service row - requirements.md: SCHED-COND-001 entry under §10 (Scheduling & Execution) - feature-flows.md: index row - docs/planning/PR_REVIEWER_AGENT.md: design doc from which this feature was extracted — committed for traceability Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: address PR #455 review feedback - pre_check.py: asyncio.get_event_loop() → get_running_loop() (deprecated in 3.10+) - pre_check.py: oversized message override no longer dropped silently — response now carries message_truncated="override dropped: N bytes exceeds 32000 cap" so scheduler/operator can see what happened; log escalated to ERROR with size+limit details - pre_check.py: module-level docstring expanded to note the security scope of check() (full Python interpreter access, same sandbox as chat tools — operators should review .trinity/pre-check.py like any executable template file) and the intentional no-cache behavior - tests/unit/test_pre_check_router.py: 15 new router/unit tests covering oversized-message drop path and non-dict return → 500 (both previously only exercised by inspection). Uses importlib to load pre_check.py directly, avoiding python-multipart requirement from sibling routers - feature-flows/scheduler-pre-check.md: document truncation behavior, security scope expectation, and updated test summary (12 scheduler + 15 router = 176 total passing) Lock-scope concern noted in review is not an issue: the skip path returns from _execute_schedule_with_lock, and the outer _execute_schedule holds the lock in a try/finally that covers the return. No leak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(#454): docker exec instead of agent-server HTTP endpoint Review feedback on #455 flagged that the HTTP-endpoint design introduced a new system edge (scheduler → agent-server direct) and a novel code- loading pattern (importlib in a router). Both broke with Trinity's established convention that all "run something in an agent container" flows go through `services/docker_service.execute_command_in_container` — the same primitive used by: - services/git_service.py (persistent-state allowlist, #384 S3) - services/ssh_service.py (key provisioning) - services/agent_service/terminal.py (web SSH) - routers/system_agent.py (admin exec) - adapters/message_router.py (Slack file ingest) - routers/voice.py, monitoring_service.py This commit swaps the design accordingly. Changes: - Delete docker/base-image/agent_server/routers/pre_check.py and its router registration. No new HTTP surface on agent-server. - Delete tests/unit/test_pre_check_router.py (router is gone). - Add src/backend/routers/internal.py → POST /api/internal/agents/{name}/pre-check. Runs the template-shipped `.trinity/pre-check.py` via execute_command_in_container. Two-step: `test -f` for existence, then `python3 .../pre-check.py`. Returns {hook_present, exit_code, stdout, stderr}. Gated by existing X-Internal-Secret header (C-003). - Rewrite src/scheduler/service.py::_run_pre_check to call the backend endpoint (scheduler no longer opens a direct edge to agent-server). Translates backend response to the same {fire, message, reason} shape so _execute_schedule_with_lock is unchanged. - Delete AgentClient.pre_check from src/scheduler/agent_client.py. - Rewrite tests/scheduler_tests/test_pre_check.py to mock the backend HTTP (httpx.AsyncClient) instead of the agent HTTP client. 13 tests covering translation: hook absent → None, non-zero exit → None, empty stdout → skip, non-empty stdout → fire with override, 404, 5xx, connection error, malformed JSON. - Update docs/memory/architecture.md §Agent Containers and §Background Services, docs/memory/requirements.md SCHED-COND-001, and docs/memory/feature-flows/scheduler-pre-check.md to reflect the new topology. feature-flows.md index updated. Template side (dolho/pr-reviewer-agent commit 4330d2a): .trinity/ pre-check.py rewritten as a standalone shebanged script that prints the chat prompt to stdout or exits 0 empty. Benefits: - Preserves "scheduler → backend → agent" topology (Invariant #11). - Uses Trinity's dominant pattern for agent-container command execution. - No importlib, no module caching, no pydantic contract — exit code + stdout is unambiguous. - Smaller code footprint: internal endpoint is ~50 lines, scheduler translation is ~50 lines, template script is ~50 lines. Test coverage: 162/162 scheduler suite passing (up from 161 — new case for malformed backend JSON). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(#454): make pre-check hook language-agnostic Drop the `python3` invocation from the backend's pre-check exec and rename the convention path from `~/.trinity/pre-check.py` to `~/.trinity/pre-check`. Trinity now execs the file directly — the interpreter is selected by the file's shebang line. Templates can ship Python, bash, node, or a compiled binary; Trinity stops caring about language. `test -f` (not `-x`) is kept for the existence check so a present-but-non-executable file surfaces as an exec failure (exit 126) in the operator log instead of silently falling through to the backward-compat "no hook" path. Docs (architecture / feature-flow / requirements / index) updated to reflect the new contract. Scheduler-side translation tests are unaffected — they assert the JSON contract, not the exec command string. 13/13 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(#454): extract pre_check_service from internal router Move the pre-check exec logic out of routers/internal.py into a dedicated services/pre_check_service.py. Aligns with Invariant #1 (Router → Service → DB): the router becomes a 5-line passthrough, all business logic (path constant, two exec calls, stdout capping, contract dict) lives in the service. Same shape as services/slot_service.py, services/task_execution_service.py, services/monitoring_service.py — testable in isolation, discoverable via grep, won't trip /validate-architecture later. Behavior unchanged. 13/13 scheduler unit tests still pass; live endpoint hit returns same contract dict. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 29, 2026
vybe
added a commit
that referenced
this pull request
Apr 30, 2026
* feat(webhooks): agent schedule webhook triggers (WEBHOOK-001, #291)
Add public webhook URLs so external systems (CI/CD, CRMs, monitoring) can
trigger agent schedule executions via a simple HTTP POST with no Trinity
account required — authenticated by a 256-bit opaque token embedded in the URL.
Changes:
- New public router POST /api/webhooks/{token}: rate-limited (10/60s per
token), audit-logged, 202 Accepted, delegates to existing scheduler trigger
- JWT-auth CRUD: POST/GET/DELETE /api/agents/{name}/schedules/{id}/webhook
- DB migration: webhook_token (TEXT UNIQUE), webhook_enabled (INTEGER DEFAULT 0)
on agent_schedules; partial unique index for O(1) token lookup
- Scheduler updated to accept triggered_by param in JSON body so executions
record triggered_by="webhook" correctly
- Webhook context field framed as data to reduce prompt injection surface
- 12 integration tests in tests/test_webhook_triggers.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): patch 4 Dependabot alerts — happy-dom + vite (#486)
Bumps two dev-only dependencies to patched versions. Production is
unaffected (happy-dom is test-only; vite only runs in local dev).
- src/frontend: vite ^6.0.6 → ^6.4.2 (closes Dependabot #55, CVE-2026-39363)
- tests/git-sync: happy-dom ^15.11.7 → ^20.9.0 (closes #83/#84/#85:
VM context escape RCE, ESM code exec, fetch cookie leakage)
Verified: frontend `vite build` clean, all 10 git-sync vitest tests pass
under happy-dom 20.9.0. No new critical/high alerts introduced.
Closes #485
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(webhooks): add WEBHOOK-001 to requirements and architecture (fixes #484 review)
Add missing documentation for the webhook trigger feature:
- requirements.md: WEBHOOK-001 entry with description, key features, DB changes,
API endpoints, security model, and feature flow link
- architecture.md: webhooks.py listed in Routers table; Schedules table expanded
from 9 to 12 endpoints with the 3 webhook management endpoints; new Webhook
Triggers section documenting the public POST /api/webhooks/{token} endpoint;
webhook_token and webhook_enabled columns added to agent_schedules schema block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#488): add status-in-dev label + PR-merge automation (#489)
Close the gap between "PR merged to dev" and "released to main".
- New GH Action `issue-status-on-merge.yml`: on PR merge to dev,
parse Fixes/Closes/Resolves #N from PR body+title, add
`status-in-dev`, remove `status-in-progress`.
- `/release` skill: read `gh issue list --label status-in-dev` as
the authoritative shipping list for release notes; include
`Closes #N` in the release PR body so issues auto-close on merge
to main.
- `DEVELOPMENT_WORKFLOW.md`: SDLC is now Todo → In Progress →
In Dev → Done, each stage mapped 1:1 to commit-graph location.
Fixes #488
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(channels): file upload Phase 2 — workspace delivery hardening (#487) (#494)
Phase 2 of #354 polishes the shared channel-agnostic file delivery path
in `message_router._handle_file_uploads`. Phase 1 (#355) added Telegram
extraction/download/validation; the actual workspace write path was
introduced for Slack inbound (#222). This change hardens the shared path
for both channels:
- New `_sanitize_filename` helper: NFKC unicode normalize → basename →
safe-chars regex → empty/dotfile fallback to `file_{id}` → 200-char
truncation preserving extension → collision dedup with `-1`, `-2`, …
- Spec injection format: `[File uploaded by {uploader}]: {name} ({size})
saved to {path}`. Uploader is the verified email when present
(Issue #311), else `adapter.get_source_identifier(message)`.
- All-writes-failed handling: when every workspace write attempt fails,
the router replies on the channel with an explicit error and skips
agent execution (#487 AC6). Validation rejections (size/MIME/download
errors) still surface in the description block as before.
- Audit log entries gain an `uploader` field.
Per-session upload directory (`/home/developer/uploads/{session_id}/`)
preserved — keeps user uploads isolated and ephemeral, matches the
existing #222 model.
Tests: +17 unit tests across `TestFilenameSanitization` (12),
`TestFileDeliveryFormat` (2), `TestFileDeliveryFailures` (3). 28/28
passing in `tests/unit/test_file_upload.py`.
Docs: `telegram-integration.md` Phase 2 section + revision row;
`slack-file-sharing.md` flow / router / errors / security sections
updated for the shared change; `feature-flows.md` index row.
Closes #487
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(webhooks): import Request in schedules router (#495)
The WEBHOOK-001 commits (c630931 / 8fdf736) added `request: Request`
parameters to `generate_webhook` and `get_webhook_status` without
importing `Request` from fastapi. Backend module import fails with
NameError on startup, blocking all dev deploys.
Integration tests in tests/test_webhook_triggers.py exercise these
endpoints but never caught the bug because the backend never starts —
test setup fails before any test runs.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(backlog): repair drain spawn — lazy-import target after #95 (#496) (#500)
services/backlog_service.py:240 lazy-imported _execute_task_background
from routers.chat, but #95 deleted that function. Every backlog drain
attempt failed with ImportError; the exception was swallowed at
backlog_service.py:218-228, so BACKLOG-001 (#260) was silently dead.
Live observation: 23 drain failures / 24h on a fan-out workload, only
surface signal was the per-execution `error` column.
Why it shipped silently: the unit happy-path test patched
sys.modules["routers.chat"] with a SimpleNamespace stub of whatever
attribute name it expected, masking the production breakage.
Changes:
- Lazy-import _run_async_task_with_persistence (the post-#95
replacement) and adjust the call shape (drop release_slot, drop
orphaned task_activity_id; the unified executor handles both).
- Capture self-task fields (is_self_task, self_task_activity_id,
inject_result) at enqueue time and rehydrate on drain so
SELF-EXEC-001 (#264) survives backlog overflow.
- Emit a stable log token `backlog_drain_spawn_failed` so log-based
detection (Vector / dashboards) can catch import drift or similar
spawn-time regressions at fleet scale rather than per-row.
- AST-based regression guard in tests/unit/test_backlog.py:
TestLazyImportTarget parses routers/chat.py and asserts the import
target exists; paired test asserts the lazy-import string matches
the validated allow-list. Catches both directions of drift without
booting the backend.
- Update happy-path test to use the new symbol and kwarg surface;
add self-task enqueue+drain round-trip tests.
Closes #496
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(announce): add Twitter/X support via API v2 + OAuth 1.0a
Bumps announce skill to v1.6. Adds a Python helper (scripts/post_twitter.py)
that reads tweet text from stdin and posts via Twitter API v2 using OAuth 1.0a
User Context — same exit-0/1 + structured-JSON contract as the existing
Discord/Slack/Telegram send paths so the sequential-only and no-blind-retry
rules apply uniformly. Credentials live in .env (gitignored) under
ANNOUNCE_TWITTER_* keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chat): sync /task long-polls on backlog at capacity (#498) (#515)
Sync parallel `/task` calls (parallel=true, async=false) at capacity used
to fail terminally with HTTP 429 — they never touched the BACKLOG-001
backlog because the spill block was nested under `if request.async_mode:`.
Observed in production: ~40% terminal-failure rate from one MCP fan-out
caller (214 capacity rejections / 24h, 0 enqueues from 541 dispatches).
Sync calls now spill to the same backlog the async path uses and long-poll
on the open HTTP connection until the queued execution reaches a terminal
status, then return the result inline. True 429 only when the backlog is
also full. Total connection hold capped at 2 × effective_timeout.
Implementation:
- New `services/sync_waiter.py` owns the in-process registry and the
`signal_sync_waiter` / `wait_for_sync_terminal` primitives. Wait combines
an asyncio.Future (set by the drain finally block) with a 5s DB-poll
fallback that covers terminal flips routed outside the drain
(corrupt-metadata, expire_stale, cleanup recovery).
- `routers/chat.py` sync branch now mirrors the async branch:
pre-acquires the slot, on at-capacity calls `backlog.enqueue()` then
`wait_for_sync_terminal()`, returns the inline result on wake.
- `_run_async_task_with_persistence` wraps its body in try/finally and
signals any registered sync waiter with the rich TaskExecutionResult
plus chat_session_id. No-op when no waiter is registered (the common
async fire-and-forget path).
Tests (`tests/unit/test_chat_sync_backlog.py`, 13 new):
- Signal / wait / poll-fallback / timeout / cleanup / concurrent waiters
- Regression test pins TERMINAL_TASK_STATUSES to the enum so a new
TaskExecutionStatus value forces a deliberate update (caught a missing
SKIPPED entry pre-merge)
Trade-off (Policy B): worst-case connection hold doubles to
2 × effective_timeout when the request is queued. Honest envelope —
the caller chose to wait. Documented in the architecture diagram of
`persistent-task-backlog.md`.
Companion issue #505 covers the orchestration-education gap (MCP tool
description + platform prompt) so agents pick the right tool for the
job rather than relying on the platform absorbing every misuse.
Closes #498
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(groom): document SDLC stages and add status-label/board reconciliation
Adds SDLC context (Todo → In Progress → In Dev → Done) so grooming respects
in-flight work, and a Step 1b that reconciles status-* labels with board
columns (labels are authoritative).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent): classify signal-killed claude exits as 504, not fake auth failure (#517)
External signal terminations of the claude subprocess (timeout SIGKILL,
OOM-kill, parent SIGTERM, operator cancel) used to fall through to the
auth-fallback heuristics and surface as a misleading "Subscription token
may be expired" 503. Same shape as #361 (max-turns), different exit path.
Adds _classify_signal_exit() consulted before the auth heuristics: matches
Python-native signal exits (return_code < 0) and shell-encoded forms
(130/137/143 for SIGINT/SIGKILL/SIGTERM) and raises HTTP 504 with a clear
"killed by SIGKILL/SIGTERM/SIGINT — likely timeout, OOM, or operator
cancel" message. Tightens the zero-token heuristic with return_code > 0
so signal exits cannot reach it.
The bug became routinely reproducible after #61 (PR #326) added
backend-driven terminate_execution_on_agent() — every timeout now
produces a signal-killed claude subprocess on the agent side, which the
old heuristic block misclassified. Also de-risks PR #508 (auth-class
auto-switch): without this fix, every timeout would trigger an
unnecessary subscription rotation.
Backend's task_execution_service.py only flags AUTH on 503; 504 falls
through to the generic FAILED path. No backend changes required.
Closes #516
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(sprint): align skill with DEVELOPMENT_WORKFLOW.md (#519)
Four divergences between the /sprint playbook and the SDLC documented in
docs/DEVELOPMENT_WORKFLOW.md:
- Step 3 used `gh issue edit --add-label status-in-progress` directly,
bypassing .github/workflows/claim.yml and skipping self-assignment.
Now posts `/claim` as an issue comment, which is the workflow's single
source of truth for the In Progress transition.
- Step 8 invoked pytest directly via `cd tests && source .venv/bin/activate
&& python -m pytest …`. Now defers to `/test-runner [feature]`, with a
documented fallback for brand-new files outside the runner's catalog.
- Step 10 commit + PR body used `closes #N`. Workflow §1 specifies
`Fixes #N`; both auto-close on GitHub but the doc is the contract.
- Step 11 final report didn't mention the post-merge automation. Now
warns that issue-status-on-merge.yml owns the
status-in-progress → status-in-dev transition, so operators don't
manually edit labels post-merge.
Non-breaking: argument signature, automation level (gated), state
dependencies, and pipeline overview unchanged. Net +19/-11.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent): classify clean-exit empty-result as 502, not silent success (#520) (#521)
* fix(agent): classify clean-exit empty-result as 502, not silent success (#520)
Sibling of #516/#517 on the return_code == 0 path. When the claude
subprocess exits 0 but the final {"type":"result"} JSON line is dropped
before the reader thread captures it (typical cause: a child subprocess
inherited stdout, kept the pipe open past claude exit, the reader thread
leaked, the pgroup unwind closed the pipe), metadata.cost_usd and
metadata.duration_ms stay None. The success path used to return HTTP 200
anyway — agent-server logged "completed successfully" while backend
silently reaped the execution as an orphan minutes later, masking the
real failure with a misleading "completed on agent but recovered by
watchdog" message.
Adds _classify_empty_result(metadata, raw_message_count) consulted after
the return_code != 0 block (#516 + auth heuristics) and before response
building. When both cost_usd and duration_ms are None, raises HTTP 502
with diagnostic context (tools, turns, raw_messages, cause hint).
Backend's task_execution_service.py:542 only flags AUTH on 503, so 502
falls through to the generic FAILED path with the helpful detail
preserved — no backend changes needed.
The two-field check is conservative: single-field nullability could be a
Claude format quirk; both-None is a strong signal that the terminal
result message never arrived. Test coverage pins the scope so a future
edit can't silently broaden it.
Changes:
- docker/base-image/agent_server/services/claude_code.py — new
_classify_empty_result() helper next to _classify_signal_exit; call
site between the return_code != 0 block and response building.
- tests/unit/test_empty_result_classification.py — 9 new tests, all
pass. Covers both-None → 502, populated metadata → None,
single-field-only → None (Claude format quirk tolerance), zero-cost
and zero-duration → None (is None vs falsy), missing metadata → None.
- docs/memory/feature-flows/parallel-headless-execution.md — changelog
entry under Recent Updates.
- docs/memory/feature-flows/task-execution-service.md — row in
error-translation table + new Empty-Result Pre-Check paragraph.
Requires base-image rebuild after merge:
./scripts/deploy/build-base-image.sh
Closes #520
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(feature-flows): index entry for agent error classification (#516, #520)
Combined Recent Updates entry covering the matching pair of agent-side
error-classification fixes that shipped this week — _classify_signal_exit
(#516, PR #517) and _classify_empty_result (#520, PR #521). Both touch
docker/base-image/agent_server/services/claude_code.py and share the
"agent surfaces the right HTTP status so backend records FAILED with a
useful detail" theme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent): off-load synchronous terminate cleanup off the event loop (#523)
The async terminate_execution endpoint and the outer asyncio.TimeoutError
handlers in execute_claude_code and execute_headless_task were calling
registry.terminate() / _terminate_process_group() / _safe_close_pipes()
synchronously. Those helpers do up to 7s of process.wait() (SIGINT grace
+ SIGKILL grace), which blocks the asyncio event loop for the entire
window. While blocked, agent-server cannot serve /health, the backend
circuit breaker opens, and UI fan-out hangs for 5+ minutes per page.
This is the actual user-visible mechanism behind the #523 "agent-server
wedge" symptom, not the FD-inheritance / leaked-reader-thread theory in
the original report (see issue comment for the corrected diagnosis —
the FD_CLOEXEC fix as written would not have helped because dup2 strips
CLOEXEC during the child's stdout setup, and the existing post-#407
killpg + safe_close path actually works in the vast majority of cases).
Wrap the three call sites in loop.run_in_executor(None, ...) so the
blocking process.wait() runs on a thread-pool worker. Pipe-inheritance
fragility remains a slow-burn cleanup item to be filed separately.
- routers/chat.py: terminate_execution dispatches registry.terminate to
the default executor
- services/claude_code.py: outer-timeout cleanup in both async paths
off-loads _terminate_process_group + _safe_close_pipes
- tests/unit/test_terminate_async_executor.py: regression test asserts
registry.terminate runs on a non-event-loop thread and the event-loop
yield stays sub-50ms while terminate is in flight
Fixes #523
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(planning): add Tier 2.6 hardening + actor-model destination roadmap
Records the architectural critique from 2026-04-26 review:
- Tier 2.6 (Sprint D′): #524 state machine contract, #525 idempotency
keys, #526 dispatch circuit breaker. Closes the three contract-level
gaps that survive even after Sprint D's plumbing consolidation.
- Future considerations: 7 unranked recommendations (durable
ProcessRegistry, retry-in-funnel, synchronous terminate ack, dual
streams, fairness, EventBus backpressure, lifecycle contract doc).
- Target architecture section: names the actor model as the destination
(mailbox + journal + processor), maps existing components to the
concepts they already implement, defines a 4-phase gated transition
roadmap, and gates Phase 2 (agent-to-agent experiment) on a one-page
message-envelope + journal-format postcard.
Issues: #524, #525, #526 created and added to project board (Epic
#411 Orchestration Invariants, Theme Reliability).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(planning): mark #291 (WEBHOOK-001) shipped, Sprint C now 5/5
#291 closed 2026-04-24, shipped via PR #484 (token-in-URL trigger
through TaskExecutionService) with follow-up fix PR #493. The plan
doc still listed it as the next item to pick up; align it with the
ground truth and re-aim "what to do next" at #428 (after #306 soak)
plus Tier 2.6 hardening (#524/#525/#526) in parallel.
* refactor(capacity): consolidate three queue/slot primitives into CapacityManager (#428) (#527)
* refactor(capacity): consolidate ExecutionQueue + SlotService + BacklogService into CapacityManager (#428)
Single public facade for agent execution capacity. Composes SlotService
(Redis ZSET counter) and BacklogService (SQL persistent overflow) as
private internals; owns the in-memory overflow store (Redis LIST, depth 3,
lifted from the deleted ExecutionQueue).
Why:
- 7 caller sites now go through one API instead of orchestrating three.
- Each new trigger type (retry, webhook, self-exec, fan-out) gets one path
for capacity, not a choice between three primitives.
- Unblocks #429 (CLEANUP-COLLAPSE) and the actor-model destination by
reducing the surface a single capacity store has to expose.
API:
capacity.acquire(agent, exec_id, max_concurrent, *,
overflow_policy='reject'|'queue_in_memory'|'queue_persistent',
overflow_payload=PersistentTaskPayload(...))
capacity.release(agent, exec_id) # idempotent
capacity.release_if_matches(agent, eid) # TOCTOU-safe (watchdog)
capacity.get_status(agent, max_concurrent)
capacity.reclaim_stale(agent_timeouts) # called by cleanup_service
capacity.force_release(agent) # emergency
capacity.cancel_all_overflow(agent, reason) # agent deletion
capacity.run_maintenance(max_age_hours) # 60s tick from main.py
Wire format unchanged: same Redis keys (agent:slots:*, agent:queue:*),
same SQL columns (schedule_executions.queued_at, backlog_metadata).
In-flight executions unaffected; clean revert path.
Deviations from issue spec (user-approved):
- No feature flag — single runtime path. dev-soak + clean revert is the
rollback mechanism, simpler than a per-agent DB column + flag check at
every call site.
- ExecutionQueue deleted in this PR rather than separate cleanup PR.
SlotService and BacklogService kept as private internals (well-factored,
one job each).
Soak deviation: shipped after 5 days of #306 soak rather than the planned
14 days. Mitigated by additive-style refactor (no wire-format change).
Files:
- NEW services/capacity_manager.py (~480 LOC)
- DELETE services/execution_queue.py (~360 LOC)
- 7 caller migrations: routers/chat.py (4 sites), routers/agents.py (2),
routers/agent_config.py (1), services/cleanup_service.py (4),
services/task_execution_service.py (1), services/agent_service/queue.py (3),
main.py (1, callback wiring is now internal).
- NEW tests/unit/test_capacity_manager.py — 21 tests covering acquire/release
for all three overflow policies, drain wiring, status, force_release,
reclaim_stale, cancel_all_overflow.
- UPDATE tests/test_watchdog_unit.py — 11 mock decorator pairs collapsed to
single get_capacity_manager mock.
Tests: 21 new + 35 watchdog + 33 backlog = 89 green for affected surface.
Fixes #428
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(feature-flows): add capacity-management.md, deprecate predecessor flows (#428)
- NEW capacity-management.md — public surface, overflow policies, end-to-end
/chat and /task flows, storage map, maintenance & recovery, what-replaced-what.
- DEPRECATE notes on the three predecessor flows with redirects:
- execution-queue.md (ExecutionQueue deleted)
- parallel-capacity.md (SlotService internalized)
- persistent-task-backlog.md (BacklogService internalized)
- "Now uses CapacityManager" notes on four downstream flows:
- task-execution-service.md, parallel-headless-execution.md,
cleanup-service.md, execution-termination.md
- Index: Recent Updates row + Core Agent Features row for capacity-management.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(requirements): note BACKLOG-001 is now internal to CapacityManager (#428)
Section 10.8 (Persistent Task Backlog) — replace direct SlotService callback
reference with the unified CapacityManager facade. Status bumped with the
2026-04-26 internalization date and #428 cross-ref.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent): drain pipe before close to preserve final result line (#531) (#532)
* fix(agent): drain pipe before close to preserve final result line (#531)
drain_reader_threads previously called safe_close_pipes() immediately
after terminate_process_group(), discarding the kernel pipe buffer before
the reader thread could drain it. On long agentic tasks the final
{"type":"result"} JSON line (cost, duration, answer) was in that buffer
at the moment of close, causing the reader to raise ValueError and
metadata.cost_usd / duration_ms to remain None — triggering the HTTP 502
"Execution completed without a result message" classification from #521.
Fix: reorder so grandchildren are killed first, then the reader is given
post_kill_grace=30s to drain naturally (grandchildren dead → kernel
delivers EOF once the buffer is consumed → reader returns '' and exits
cleanly). safe_close_pipes() is now a true last resort — only called when
the reader is still alive after 30s, which indicates a genuine wedge, not
unfinished backlog drain.
Also extends _classify_empty_result to derive num_turns from raw_messages
when metadata.num_turns is None (result line lost), so the 502 detail
reports an honest turn count instead of always showing 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(tests): update test catalog for #531 drain_reader_threads fix
- Add test_subprocess_pgroup.py and test_empty_result_classification.py
to Test Categories (Operations & Observability, unit section)
- Add 2026-04-27 Recent Test Additions entry with description of the
pipe-drain ordering regression tests and raw_messages fallback tests
- Update unit test count: 165 → 170; total: 2,257 → 2,262
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(feature-flows): document drain_reader_threads pipe-ordering fix (#531)
Update parallel-headless-execution.md with the root cause fix for
the "Execution completed without a result message" HTTP 502: the old
drain_reader_threads sequence closed the pipe before the reader could
drain the kernel buffer (including the final result JSON line). New
sequence: kill grandchildren → natural drain (post_kill_grace=30s) →
force-close only as last resort.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(tests): update orphaned-recovery mocks to CapacityManager (#533) (#534)
Replace stale services.slot_service sys-mock with services.capacity_manager
so all four recovery scenario tests pass after the #428 consolidation.
Assertions updated from release_slot → release to match the new API.
Fixes #533
Co-authored-by: Claude <noreply@anthropic.com>
* docs(skills): align validate-pr with DEVELOPMENT_WORKFLOW.md
Add quick triage block, base branch check, PR size warning, type-docs
label, Base Branch/PR Size rows in report table, and review pipeline
matrix linking /review and /cso --diff with their complementary roles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(validate-architecture): stale-citation filter + dedupe guard (#511) (#513)
* fix(validate-architecture): add stale-citation filter + issue dedupe guard (#511)
The /validate-architecture skill produced false-positive issue #479 by:
1. citing file paths the report's snapshot saw, but `main` no longer has
(process engine deleted in #430 the same day);
2. running `gh issue create` with no check for existing open issues with
the same finding fingerprint.
Two targeted edits to .claude/skills/validate-architecture/SKILL.md:
- New Step 2c "Filter Stale Citations" — `git ls-files --error-unmatch`
every cited path before report. Drop ghosts. Downgrade FAIL → PASS
when an invariant has zero remaining real citations.
- Modified Step 4 — fingerprint = sorted invariant numbers; query
open `automated,priority-p1` issues with `--search "in:body
validate-architecture fingerprint=<fp>"`; comment on existing
issue instead of creating duplicate. Issue body now stamps the
current commit SHA for evidence binding.
Closes #511.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(validate-architecture): clarify dedupe branching, distinct fingerprint marker, quote paths (#511)
Follow-up to review feedback on PR #513. Three small skill-prose
hardenings:
- I2 (LLM-driven flow control): the dedupe branch previously relied on
`if [ -n "$EXISTING" ]; then ...; exit 0; fi` followed by a separate
create block. `exit 0` halts a bash subshell, not an LLM walking the
markdown — a future runner could execute both blocks. Replace with
explicit "Path A — COMMENT, then STOP" / "Path B — CREATE" prose
branching and an explicit DO-NOT note.
- I4 (fingerprint collision): replace free-text body search
`validate-architecture fingerprint=$FP` with HTML-comment marker
`<!-- validate-architecture::fingerprint=$FP -->` plus a quoted-phrase
search. Self-evidently programmatic; won't collide with prose.
- I3 (path quoting): the Step 2c example now uses `"$path"` and a note
about shell metachars, so implementers don't strip the quotes.
- Add concurrency caveat documenting that the dedupe is best-effort,
not atomic (no GitHub primitive provides this).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(config): clean stale Auth0 / AUDIT_URL, document SMTP/SendGrid/FRONTEND_URL (#481) (#509)
- Remove dead Auth0 env vars + build args from docker-compose.prod.yml
and docker/frontend/Dockerfile.prod (Auth0 removed 2026-01-01). The
build-arg fallbacks were also leaking a real Auth0 domain + client ID
into a public repo.
- Drop AUDIT_URL from .env.example (audit-logger service no longer exists;
no Python references it).
- Add FRONTEND_URL to .env.example (required in prod for OAuth post-auth
redirects in slack_service.py / public_links.py and SSH host
auto-detection in ssh_service.py).
- Document SMTP_HOST/PORT/USER/PASSWORD and SENDGRID_API_KEY in
.env.example so the advertised EMAIL_PROVIDER=smtp/sendgrid modes are
actually configurable from the template.
Closes #481
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(auth): require auth on /api/docs endpoints (#452) (#507)
Add Depends(get_current_user) to the three handlers in
src/backend/routers/docs.py so the file no longer violates
Architectural Invariant #8.
Note: this router is not currently registered in main.py, so
the endpoints are not reachable on the running API. The fix is
applied to the file as written so the invariant validator stops
flagging it and so the file is correct if it is ever remounted.
Closes #452
* fix(chat): wrap long unbroken strings in chat bubbles (#457) (#502)
Long URLs, tokens, base64 blobs, and other unbroken strings in agent
chat responses were overflowing their 85% bubble and forcing horizontal
scroll on the entire Chat tab.
Root cause: ChatBubble.vue capped the bubble width but never told
inner content how to handle unbreakable strings. Inline <code> and
the user-text <p> had no overflow-wrap; <pre> defaulted to white-space:
pre with no overflow-x: auto override.
Fix (CSS-only, all 3 render branches — user / self-task / assistant):
- min-w-0 on outer wrapper, overflow-hidden on inner bubble
- break-words on user text and prose container
- prose-pre:overflow-x-auto + prose-pre:max-w-full so code blocks
scroll inside the bubble instead of expanding it
- prose-code:break-words for long inline tokens
- prose-a:break-words for long URLs in markdown links
Verified visually: before/after static test page shows BEFORE leaks
content well past the bubble border; AFTER wraps cleanly with no
regression on normal markdown (headings, lists, links, short code).
* fix(schedules): add missing Request import for webhook endpoints (#493)
Regression from c630931 (WEBHOOK-001 / #291): `routers/schedules.py`
uses `Request` as a type annotation on the `generate_webhook` and
`trigger_webhook` handlers but never imports it, so the module fails
to load and the backend won't start with a NameError.
Minimal fix: add `Request` to the existing `from fastapi import …`
line (line 11). No behavioral change — the annotation was already
intended.
Surfaced while dev-testing FILES-001 (PR #491). uvicorn reload pulled
in the dev branch state and blew up.
Co-authored-by: Pavlo <pash@pashs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(git): route orphan cleanup through db.delete_git_config (#451) (#501)
Replaces raw `DELETE FROM agent_git_config` SQL in routers/git.py with the
existing `db.delete_git_config()` method (already used at line 435 of the same
file for the init-failure rollback path). Restores Architectural Invariant #1
(Three-Layer Backend) for this router. No behavior change — identical SQL,
identical parameter binding.
Closes #451
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(subscription): auto-switch on first failure + auth-class triggers (#441) (#508)
Drop the 2-consecutive-429 gate in `subscription_auto_switch` so a single
subscription failure now triggers a switch — the 2h skip-list on
alternative selection (already pinned by #444 / #476 regression tests) is
sufficient as the lone thrash guard. Broaden the trigger surface to also
fire on auth-class failures (401/403/credit balance/expired OAuth token,
etc.), classified via a centralized `AUTH_INDICATORS` list, so a broken
subscription auto-recovers instead of failing every execution until
manual intervention. Flip the `auto_switch_subscriptions` default to
"true" — operators can still opt out, but the safe behavior is now the
default. Backward-compat shim `handle_rate_limit_error` preserved for
existing 429 callers.
- services/subscription_auto_switch.py: new `handle_subscription_failure`
with `failure_kind` dispatch ("rate_limit" | "auth"); new
`is_auth_failure` classifier; default flipped; notification + log
wording adapts per kind; old shim retained.
- services/task_execution_service.py: 503 / auth-classified errors now
also call the switch path alongside 429.
- routers/chat.py (sync): same broadening on the interactive chat
surface; auth path returns 503+retry hint mirroring the 429 UX.
- routers/subscriptions.py: GET `/auto-switch` default also flipped to
"true" so the UI toggle and runtime gate read the same value.
- scheduler/service.py: dedupe two inline `auth_indicators` copies into
a single module-level constant; cross-reference the canonical list in
backend (cross-container import not viable).
- tests/unit/test_subscription_auto_switch_pingpong.py: new
TestIsAuthFailure + TestSingleEventThreshold classes (8 new tests, all
pingpong + #476 aging tests still green).
- tests/test_subscription_auto_switch.py: flip default-off → default-on.
- docs: SUB-003 feature flow + requirements doc reflect the new
threshold, broadened scope, and on-by-default behavior.
Closes #441
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(feature-flows): clean up #95 drift missed by #500 (#496) (#503)
* fix(backlog): repair drain spawn after #95 rename (#496)
`services/backlog_service.py:_spawn_drain` was lazy-importing
`_execute_task_background` from `routers.chat`, but #95 (PR #316) deleted
that function and replaced it with `_run_async_task_with_persistence`.
Every backlog drain raised `ImportError`, was caught at line 218-228, and
silently marked queued executions FAILED — leaving BACKLOG-001 (#260)
non-functional whenever an agent hit capacity.
Rewire the lazy import to the new helper and adjust the call shape:
- drop `task_activity_id` (not in new signature; chat router already
passes None at enqueue)
- drop `release_slot=True` (the wrapper passes `slot_already_held=True`
to TaskExecutionService, which manages release in its finally block)
- derive `is_self_task` from x_source_agent vs agent_name
- pass `self_task_activity_id=None` (queued items don't carry one;
separate gap, not in scope here)
Add `tests/test_backlog_drain_unit.py` with five regression checks:
two AST-based contract tests that pin the function name and signature
in `routers/chat.py` (would have caught the original break), and three
runtime spy tests covering the kwarg shape `_spawn_drain` forwards. The
existing `tests/unit/test_backlog.py::test_drain_happy_path_spawns_background`
is updated to match the new contract.
Sync the BACKLOG-001 and TaskExecutionService feature-flow docs to
reference the renamed helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(feature-flows): sync index + parallel-capacity for #496
- Add #496 entry to feature-flows.md Recent Updates.
- Fix two more stale `release_slot=True` references in
parallel-capacity.md left over from #95 — the param never existed
on `_run_async_task_with_persistence` (slot release happens inside
TaskExecutionService via slot_already_held=True).
Other stale `release_slot=True` references in
authenticated-chat-tab.md and parallel-headless-execution.md are
deeper drift (separate flows, not touched by #496) — leave for a
follow-up doc-cleanup pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(catalog): register test_backlog_drain_unit.py (#496)
Adds the new BACKLOG-001 regression test file to tests/registry.json
so it shows up in the catalog alongside test_event_bus.py and
unit/test_backlog.py.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: drop redundant test_backlog_drain_unit.py
PR #500 (which superseded the original #496 fix scope) shipped
equivalent contract coverage with a more robust setup:
- `TestLazyImportTarget` (AST guard for the lazy-import target)
- `test_drain_threads_self_task_fields` (round-trip via real
BacklogService against sqlite)
The local file used sys.modules stubs which were strictly weaker.
Keeping it would only add maintenance burden for duplicate coverage,
so drop the file and its registry entry. Net effect on PR #503 is
that it becomes a small, focused docs-cleanup PR (parallel-capacity.md
and task-execution-service.md drift from #95, plus the missing
Recent Updates entry for #496).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(deploy): align scripts, configs, and docs with production operating patterns (#504)
* docs(generate-user-docs): add hub-and-spokes deployment structure and ops-pattern import
Restructure skill to produce guides/deploying/ as a hub plus six spokes
(local-development, single-server, public-access, upgrading,
backup-and-restore, monitoring) instead of one flat deploy guide.
Add an "operational guide" template (When to Run → Pre-flight →
Procedure → Verify → Rollback) for procedural docs that don't fit the
feature-shaped dual-audience template, plus verbatim-reuse snippets for
the load-bearing rules: never down/up, rebuild platform services only,
six-probe verification, resource-thresholds table, alpine cp backup.
Add Step 2h to draw operational patterns from the private ops runbook
under ../trinity-ops/, with explicit safe/forbidden import lists and a
sshpass→localhost rewrite rule.
Strengthen Step 2e to cross-check .env.example keys against each
compose's environment block — docs must not promise behavior the
chosen compose can't deliver.
Add public-safety greps in Step 7 (sshpass, trinity-ops, tailnet, real
IPs, instance-dir refs) so leaked private detail blocks completion.
Tracks issue #504.
* fix(deploy): align scripts, configs, and docs with production operating patterns (#504)
Fixes the first-run blocker (agent creation fails silently without base
image) and removes references to the removed audit-logger service that
caused verify-platform.sh and validate.sh to always fail.
Scripts:
- start.sh: detect missing base image and auto-build on first run; use
`docker compose stop` in help text (not `down`, which destroys agents)
- verify-platform.sh: full rewrite — remove trinity-audit-logger and port
8001 audit checks; fix frontend from port 3000 → 80; check scheduler
health at :8001; add MCP/Vector probes; fix login hint
- validate.sh: remove non-existent `deployment/` dir, `QUICK_START.md`,
and `src/audit-logger/audit_logger.py` from required paths; fix port 3000
Config:
- docker-compose.yml: wire 5 missing env vars into backend (PUBLIC_CHAT_URL,
FRONTEND_URL, EXTRA_CORS_ORIGINS, SLACK_SIGNING_SECRET, SSH_HOST)
- .env.example: remove stale AUDIT_URL; annotate prod-only / overlay-only
vars (SLACK_SIGNING_SECRET, PUBLIC_CHAT_URL, FRONTEND_URL, SSH_HOST,
TRINITY_GIT_BASE_URL) so users know scope before setting them
Docs:
- deploying-trinity.md: add explicit build-base-image.sh step; fix
/trinity:connect to use MCP API key flow (not username/password); add
Upgrading, Health Verification, Resource Thresholds, and Common Recovery
Patterns sections from ops runbook; use `docker compose` (v2 syntax)
- setup.md: remove false claim that start.sh builds the base image; correct
admin account creation (env var driven, not wizard); clarify wizard path
(used only when ADMIN_PASSWORD is unset)
New file:
- quickstart.sh: interactive one-command setup (checks Docker, generates
secrets, sets ADMIN_PASSWORD, builds base image, starts services, verifies)
Skill:
- generate-user-docs: add deployment config reading rules (read scripts
literally; cross-check env vars vs compose; never claim "auto" unless code
proves it); add operational guide template (pre-flight/steps/verify/rollback);
resolve conflict preserving the hub+spokes guide structure from branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(generate-user-docs): remove private repo name from SKILL.md
Replace explicit `trinity-ops` repo references with generic path aliases
(`../ops-runbook/`) so the private repo name is not embedded in this
public repository.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(reliability): CAS guards on execution status writes + state machine doc (#524) (#541)
Closes the FAILED→SUCCESS and SUCCESS→FAILED races that were patched by
#378 re-verify logic without eliminating the root cause.
Changes:
- update_execution_status: SUCCESS writes are unconditional (agent wins);
non-success terminal writes blocked when row already terminal
- mark_stale_executions_failed / mark_no_session_executions_failed: inner
UPDATE gains AND status='running' to close the SELECT→UPDATE TOCTOU window
- _recover_execution: routes through mark_execution_failed_by_watchdog
(already CAS-guarded) instead of bare update_execution_status
- TaskExecutionStatus: state machine, transitions, and authorized writers
documented in docstring; PENDING_RETRY added to enum
- Remove now-dead _STALE_SLOT_ERROR_PATTERN constant
Full projector architecture (ExecutionStateProjector, agent event emission,
projected_status shadow column) deferred — agents have no Redis access and
the restart-recovery design needs more thought before those land.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(public-chat): build context before storing user message to prevent duplication (#539) (#540)
* fix(public-chat): build context before storing user message to prevent duplication (#539)
In the public chat endpoint, the user message was persisted to the database
before build_public_chat_context read from it, causing the current message
to appear twice in every agent prompt — once in "Previous conversation:"
and once in "Current message:". Reordering the calls so context is built
first (from prior history only) then the user message is stored eliminates
the duplicate on every turn.
Adds unit tests that document both the old broken order (two occurrences)
and the corrected order (one occurrence), guarding against regression.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(feature-flows): update public-agent-links with #539 context ordering fix
- Correct PUB-005 data flow: build_public_chat_context before add_public_chat_message
- Update backend implementation step ordering to match fixed code
- Add revision history entry for the bug fix
- Add #539 entry to feature-flows.md index
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agent-runtime): guard content_block isinstance in process_stream_line (#542) (#543)
Prevents AttributeError crash when Claude Code stream-json emits a
string element inside a message content array. Guards both
process_stream_line (real-time path) and parse_stream_json_output
(batch path) using the same isinstance(block, dict) pattern already
used by the error_content loop.
Fixes #542
Co-authored-by: Claude <noreply@anthropic.com>
* docs(#411): Phase 1 canary harness design + catalog Phase 1 subset additions (#544)
* docs(#411): Phase 1 canary harness design + catalog Phase 1 subset additions
- New design doc at docs/planning/CANARY_HARNESS_PHASE_1.md scoping the
AC-required infrastructure (snapshot collector, canary_violations table,
canary agent template, fleet, alerts) for the three required invariants
(S-01, E-02, L-03).
- Catalog Phase 1 subset expanded 10 → 12: adds S-03 (slot TTL ≥ exec
timeout, catches #226) and E-05 (dispatched rows have session, catches
#106), since both bugs are cited in the catalog motivation but had no
Phase 1 detector.
* docs(#411): scope fleet to strict minimum for AC's 3 invariants
* docs(#411): expand design doc to cover full Phase 1 (12 invariants, snapshot format)
* feat(settings): add Remove buttons for stored API keys + Slack (#459) (#483)
Settings page lets admins save/test Anthropic API Key, GitHub PAT, and
Slack OAuth credentials, but exposed no UI to clear them once stored.
Only workaround was calling DELETE endpoints directly or editing the DB.
Adds Remove buttons next to Save in each row, conditionally rendered
when the value lives in settings DB (source === 'settings'). Env-var
fallbacks stay uneditable from UI. Confirm dialog before deletion
(reuses ConfirmDialog component + pattern from ApiKeys.vue).
Backend DELETE endpoints already existed — no backend work:
- DELETE /api/settings/api-keys/anthropic
- DELETE /api/settings/api-keys/github
- DELETE /api/settings/slack
Audit of other Settings sections: Trinity Prompt has clearPrompt,
Skills Library blanks via deleteSetting, MCP URL has resetMcpUrl,
GitHub Templates/Email Whitelist have inline remove. Agent Quotas are
config values, not secrets.
Closes #459.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(migrations): swallow duplicate-column race on cold start (#456) (#537)
* fix(migrations): swallow duplicate-column race on cold start (#456)
`_migrate_sync_health` (#389) used a check-then-act PRAGMA → ALTER
pattern that is not atomic across uvicorn workers. On cold start with
`--workers 2`, both workers passed the PRAGMA before either committed
the ALTER, and the loser crashed its child process with
`sqlite3.OperationalError: duplicate column name: auto_sync_enabled`.
Fix:
- Add `_safe_add_column` helper that swallows the duplicate-column
OperationalError (treats it as success — another worker won the race).
Future migrations should route ALTER TABLE ADD COLUMN through it.
- Refactor `_migrate_sync_health` to use the helper for both column
additions and switch the bare `CREATE TABLE` to `CREATE TABLE IF NOT
EXISTS` (atomic in SQLite).
Tests:
- `test_safe_add_column_swallows_duplicate_column_race` — drives the
exact production race via a PRAGMA-lying cursor proxy.
- `test_safe_add_column_propagates_other_errors` — non-duplicate errors
still raise.
- `test_safe_add_column_returns_true_when_added` — happy path.
- `test_migrate_sync_health_idempotent_under_race` — `_migrate_sync_health`
is now safe to re-run on already-migrated schemas, including under
the simulated race.
The other ~50 ALTER ADD COLUMN sites are untouched: they're already
applied on production DBs (run_all_migrations short-circuits via the
schema_migrations tracking table). The race only bites new migrations
on first cold-start; the helper is available for them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(migrations): route all ALTER ADD COLUMN through _safe_add_column (#456)
Mechanical sweep of every check-then-act `PRAGMA table_info` →
`ALTER TABLE ADD COLUMN` site through the `_safe_add_column` helper, so
new migrations on a fresh cold-start with `--workers N` are race-safe by
default — not just `_migrate_sync_health` (the originally reported case).
22 migrations refactored. Bare `try/except Exception` swallows in
`_migrate_chat_messages_source_column` and
`_migrate_agent_ownership_voice_prompt` are also replaced with the
helper, which catches only the duplicate-column error instead of every
exception class.
Verification:
- Schema dump (init_schema + run_all_migrations on fresh DB) is
byte-identical before and after the sweep — every column type,
default, FK, and index preserved.
- run_all_migrations is idempotent across runs and across fresh
connections (2nd/3rd runs print no add/create lines).
- 6-worker concurrent stress test (`threading.Barrier`-coordinated)
completes without any worker crashing; final schema is intact.
- tests/unit/test_migrations_concurrent.py +
tests/unit/test_migrations.py + tests/unit/test_guardrails.py:
72 pass, 0 fail.
`tests/unit/test_guardrails.py::test_migration_is_idempotent` updated
to also exec the `_safe_add_column` helper into its isolated
namespace, since the migration now delegates to it.
The two remaining bare `CREATE TABLE` calls in the file
(`_migrate_agent_sharing_table`, `_migrate_agent_skills_table`) are
one-time DROP+CREATE data-recreation migrations that already shipped
on every existing install; they are out of scope for this sweep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(git): UI Push no longer commits runtime state (#462)
Expands the platform .gitignore deny-list to cover all runtime files
(.env, .mcp.json, .credentials.enc, instance dirs, content/, Claude
Code state, temp files). Adds idempotent migration that updates
existing agents on next Push and calls `git rm --cached` for files
that are now tracked but newly ignored.
Closes #462
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(frontend): semantic status color tokens (#67) (#553)
Introduce 5 semantic status tokens (`status-success/warning/danger/info/urgent`)
in tailwind.config.js as direct aliases of the green/yellow/red/blue/orange
palettes, then migrate 9 frontend files (4 components, 2 panel-local helpers,
1 composable, 1 utility) from raw color classes to the new tokens. Visual
output is byte-equivalent — tokens compile to identical RGB values.
Add CI safety net: `npm run check:tokens` script verifies token-palette
equivalence and catches typo'd token references in source. Wired into a new
frontend-build.yml workflow that runs `npm ci → check:tokens → build` on PRs
touching `src/frontend/**`.
Drive-by fix: rename postcss.config.js → postcss.config.mjs to fix Node
ESM/CJS interop for local `npm run build` (production Docker build was
masking the issue).
70+ raw-color files remain for follow-up sweep (per autoplan phasing).
Fixes #67
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(files): FILES-001 outbound file sharing — MVP + Phase 1 hardening (#491)
* feat(files): FILES-001 outbound file sharing MVP (Steps 1-6)
Implements outbound file sharing per docs/drafts/amazing-file-outbound.md:
- Schema + migration (agent_shared_files table with FK cascade)
- Per-agent opt-in toggle + Docker publish volume (agent-{name}-public)
- Internal share endpoint with path/MIME/size/quota validation
- Public download endpoint (/api/files/{id}?sig=...) with token auth
- share_file MCP tool (agent-scoped)
- SharingPanel UI: toggle, list, revoke, copy URL
Live-verified on Slack: agent→share_file→URL→download end-to-end.
Unit tests: 33 passed (migration, mixin, mount-match).
Known limitations + production readiness plan:
docs/drafts/amazing-file-outbound-production-readiness.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(files): FILES-001 — requirements, architecture, feature-flow doc
- requirements.md §13.10 new entry marking FILES-001 Implemented (2026-04-24)
- architecture.md: add files.ts MCP module, agent_shared_files_service,
routers/files.py, the 5 new API endpoints + dedicated section, and the
agent_shared_files table schema + operational notes
- feature-flows.md: Recent Updates entry + Documented Flows index
- feature-flows/file-sharing-outbound.md: new full vertical-slice doc
(UI → store → router → service → DB → download) matching the template
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* security(files): cap filename length at 255 chars (C2)
ShareFileRequest.filename and ShareFileMcpRequest.filename get
Field(max_length=255, min_length=1). display_name same cap.
Prevents 10KB+ filename edge cases from agent or attacker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* security(files): disk-space pre-check before write (C3)
New check_disk_space() helper using shutil.disk_usage('/data').
Refuses writes when /data has less than size_bytes + 500MB free
(HTTP 507 Insufficient Storage). Called before persisting.
Protects shared /data mount — SQLite DB, Vector logs, and log
archives live there too; letting an agent fill the disk causes
platform-wide outage, not just a failed share.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cleanup): purge expired and old-revoked shared files (C4 / Step 7)
Adds delete_expired_and_revoked(revoke_grace_hours=24) to the DB ops
class (returns stored_filename list for disk unlink) + facade forward
+ wired into cleanup_service.py's 5-min tick.
Per cycle:
- SELECT rows where expires_at < now OR revoked_at < now - 24h
- DELETE them from DB
- unlink each /data/agent-files/{stored_filename}
- bumps CleanupReport.shared_files_purged
The 24h grace on revoked rows keeps them queryable for incident
diagnosis right after revocation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* security(files): dedicated rate-limit bucket for downloads (C5)
/api/files/{id} now uses _check_file_download_rate_limit which keys
redis by file_downloads:{ip} instead of sharing the public_link_lookups
bucket used by /api/public/chat and friends. Limits unchanged (60/min
per IP).
Prevents heavy download traffic from starving the rate-limit quota for
public chat or other /api/public/* endpoints on the same IP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(files): HEAD handler mirroring GET validation (C6)
Link previewers (Slackbot, Twitterbot, Discordbot, facebookexternalhit)
HEAD-probe URLs before GET. Our endpoint was 405-ing those.
Extracted _validate_download_request() helper from GET; new HEAD
handler reuses it and returns Response(200) with the same headers
(Content-Disposition, nosniff, no-store, Content-Length) but no body,
no download counter bump, no audit row. Follows RFC 7231 §4.3.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* security(files): tighten list endpoint to owner+admin only (C7)
GET /api/agents/{name}/shared-files previously used can_user_access_agent
(owner/admin/shared). But the list response includes full download URLs
with signed tokens — so anyone able to see the list can reuse every
share. That's the same capability as share_file + revoke, both of which
already require can_user_share_agent.
Change to can_user_share_agent (owner + admin), 403 otherwise.
DELETE was already owner-only; no change needed there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(prompt): agent nudge for share_file MCP tool (C8)
Add a new 'Sharing Files with Users' section to the system-wide
PLATFORM_INSTRUCTIONS between Collaboration and Operator Communication.
Tells every agent:
- write files to /home/developer/public/
- call share_file MCP tool with the relative filename
- return the URL as-is
This means new agents discover the capability without the user
needing to name the tool explicitly. Applies immediately to every
agent via compose_system_prompt() — no image rebuild needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pr-491): address validation findings — PII redaction + scope drift
PR #491 /validate-pr flagged two issues:
1. CRITICAL: pavshulin@gmail.com in two draft docs' Owner fields
(amazing-file-outbound.md, amazing-file-outbound-production-readiness.md).
Public repo + CLAUDE.md forbids real user emails.
→ Replaced with @pavshulin (GitHub handle).
2. WARNING: .claude/settings.json committed as new file — personal
Claude Code permission allowlist unrelated to FILES-001 scope.
→ Merged 8 allowlist entries into .claude/settings.local.json
(gitignored per .gitignore:67). Removed .claude/settings.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: fix test-ordering contamination from FILES-001 mixin fixture
Two adjustments surfaced by running the full unit suite:
1. test_file_sharing_mixin.py registered `sys.modules['db']` as a plain
module (no `__path__`), which poisoned `from db.X import Y` lookups in
sibling tests (e.g. test_fleet_sync_audit did `from db.schedules ...`
and hit `'db' is not a package`). Now we give our stub a `__path__`
pointing at the real db directory, and restore `sys.modules['db']` on
fixture teardown so no leakage remains.
2. test_start_agent_skip_inject.py didn't mock the new
`check_public_folder_mount_matches` import added by FILES-001 in
services/agent_service/lifecycle.py. The Mock container lacked
iterable `attrs["Mounts"]`, blowing up with TypeError. Stubbed the
whole `file_sharing` submodule and bound the check on `_mod`
per-test to return True by default.
Full unit suite now matches dev baseline: 17 pre-existing failures,
701 passing (+33 over dev, all from FILES-001).
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>
* fix(channels): deliver images as vision content blocks via stream-json (#562) (#566)
Replaces the broken base64 data-URI-in-text approach (where Claude Code
received images as opaque markdown strings) with proper vision content
blocks fed via --input-format stream-json stdin. Images sent through
Telegram (and other channel adapters) are now visible to the agent.
- message_router: _handle_file_uploads returns 4-tuple (added image_data);
image MIME files collected as {media_type, data} dicts instead of embedded
- task_execution_service: execute_task() accepts images param, forwards in payload
- agent_server models: ParallelTaskRequest.images field added
- agent_server chat router: passes images to runtime.execute_headless()
- claude_code: adds --input-format stream-json and builds JSON content-block
stdin payload when images present; stdout/stderr threads start before stdin
write to prevent pipe deadlock; write moved into executor (not event loop)
- runtime_adapter ABC + GeminiRuntime: images param added to prevent TypeError
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(frontend): add state, brand, accent token families (#555) (#561)
Extends the design-system token system from #67 with three additional families
for colors that don't fit the status taxonomy:
state-* agent operating modes (autonomous, locked)
brand-* third-party product identity (claude, gemini)
accent-* decorative highlights named after the literal color so future
accents (accent-green, etc.) join cleanly
New tokens (all alias full Tailwind palettes, identical visual output):
state-autonomous → amber (AutonomyToggle AUTO mode)
state-locked → rose (ReadOnlyToggle ON mode)
brand-claude → orange (RuntimeBadge for Claude Code)
brand-gemini → blue (RuntimeBadge for Gemini CLI)
accent-purple → purple (DashboardPanel widget badges)
Also extends scripts/check-design-tokens.mjs to validate the new families
via a KNOWN_FAMILIES map; the reference scanner now flags typos within any
of the four families (status/state/brand/accent), not just status-*.
Migrates the 4 components blocked by #67's status-only scope:
- RuntimeBadge.vue → brand-claude, brand-gemini
- AutonomyToggle.vue → state-autonomous
- ReadOnlyToggle.vue → state-locked
- DashboardPanel.vue → accent-purple slot in getStatusColors
Fixes #555
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(scheduler): agent-owned pre-check hook (#454) (#455)
* feat(scheduler): agent-owned pre-check hook (#454)
New optional contract: agents implement POST /api/pre-check in their
container; scheduler calls it before firing a cron-triggered chat.
Endpoint absent or any error → fire as usual (fail-open). fire=false
records a skipped execution. fire=true with a message overrides the
schedule.message for that invocation.
- docker/base-image/agent_server/routers/pre_check.py: new router that
dynamically loads /home/developer/.trinity/pre-check.py (template-
supplied) and calls its check() function
- agent-server main.py: mount pre_check_router
- scheduler/agent_client.py: pre_check() method with fail-open semantics
on 404/5xx/timeout/malformed-response
- scheduler/service.py: _run_pre_check + pre-check branch in
_execute_schedule_with_lock (cron only; manual triggers bypass)
- tests/scheduler_tests/test_pre_check.py: 12 tests covering client-
and service-level behavior; 161/161 scheduler suite passes
Zero schema change — reuses existing ExecutionStatus.SKIPPED and
create_skipped_execution. Closes the "wake agent on every cron tick"
cost gap noted in docs/planning/PR_REVIEWER_AGENT.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#454): scheduler pre-check feature flow + arch + requirements
- feature-flows/scheduler-pre-check.md: new flow doc with contract,
fail-open semantics, error table, testing summary
- architecture.md: add /api/pre-check to agent-server endpoint list
and pre-check note to Scheduler Service row
- requirements.md: SCHED-COND-001 entry under §10 (Scheduling & Execution)
- feature-flows.md: index row
- docs/planning/PR_REVIEWER_AGENT.md: design doc from which this
feature was extracted — committed for traceability
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: address PR #455 review feedback
- pre_check.py: asyncio.get_event_loop() → get_running_loop() (deprecated in 3.10+)
- pre_check.py: oversized message override no longer dropped silently —
response now carries message_truncated="override dropped: N bytes exceeds
32000 cap" so scheduler/operator can see what happened; log escalated to
ERROR with size+limit details
- pre_check.py: module-level docstring expanded to note the security scope
of check() (full Python interpreter access, same sandbox as chat tools —
operators should review .trinity/pre-check.py like any executable template
file) and the intentional no-cache behavior
- tests/unit/test_pre_check_router.py: 15 new router/unit tests covering
oversized-message drop path and non-dict return → 500 (both previously
only exercised by inspection). Uses importlib to load pre_check.py
directly, avoiding python-multipart requirement from sibling routers
- feature-flows/scheduler-pre-check.md: document truncation behavior,
security scope expectation, and updated test summary (12 scheduler +
15 router = 176 total passing)
Lock-scope concern noted in review is not an issue: the skip path returns
from _execute_schedule_with_lock, and the outer _execute_schedule holds
the lock in a try/finally that covers the return. No leak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(#454): docker exec instead of agent-server HTTP endpoint
Review feedback on #455 flagged that the HTTP-endpoint design introduced
a new system edge (scheduler → agent-server direct) and a novel code-
loading pattern (importlib in a router). Both broke with Trinity's
established convention that all "run something in an agent container"
flows go through `services/docker_service.execute_command_in_container`
— the same primitive used by:
- services/git_service.py (persistent-state allowlist, #384 S3)
- services/ssh_service.py (key provisioning)
- services/agent_service/terminal.py (web SSH)
- routers/system_agent.py (admin exec)
- adapters/message_router.py (Slack file ingest)
- routers/voice.py, monitoring_service.py
This commit swaps the design accordingly.
Changes:
- Delete docker/base-image/agent_server/routers/pre_check.py and its
router registration. No new HTTP surface on agent-server.
- Delete tests/unit/test_pre_check_router.py (router is gone).
- Add src/backend/routers/internal.py →
POST /api/internal/agents/{name}/pre-check. Runs the template-shipped
`.trinity/pre-check.py` via execute_command_in_container. Two-step:
`test -f` for existence, then `python3 .../pre-check.py`. Returns
{hook_present, exit_code, stdout, stderr}. Gated by existing
X-Internal-Secret header (C-003).
- Rewrite src/scheduler/service.py::_run_pre_check to call the backend
endpoint (scheduler no longer opens a direct edge to agent-server).
Translates …
5 tasks
5 tasks
11 tasks
vybe
pushed a commit
that referenced
this pull request
May 4, 2026
* fix(security): split Docker compose into platform and agent networks (#589) Redis at 172.28.0.0/16 was reachable from any agent container. AISEC scan 3aad5469 demonstrated end-to-end exfiltration / cross-user task injection from a legitimately deployed agent. Network segmentation is the strongest control — agents now physically cannot route to Redis. Topology: - trinity-platform (172.29.0.0/16, NEW) — Redis, scheduler, vector - trinity-agent (172.28.0.0/16, name preserved) — frontend, agents - Backend / mcp-server / otel-collector / cloudflared straddle both Agent-creation sites in services/agent_service/* and system_agent_service.py need zero changes because the agent-network external name is preserved. Dev: bind Redis host port to 127.0.0.1:6379 (was 0.0.0.0). Tests connect from the dev machine; LAN cannot. Auth lands in the next commit. Refs #589 — acceptance criterion #3 (network segment separation). * fix(security): mandatory Redis auth, ACL users, auth-aware healthcheck (#589) Both compose files now enforce two passwords (REDIS_PASSWORD admin / REDIS_BACKEND_PASSWORD runtime) with the fail-on-missing :? form. docker compose refuses to render without them. Per-user ACL via inline --user flags. Additive (start from zero, allow only what the runtime needs) — never +@ALL -X, which lets newly added dangerous commands through. backend + scheduler get standard data families plus scripting/transactions/pubsub minus -@dangerous, which covers FLUSHALL, CONFIG, SHUTDOWN, MIGRATE, REPLICAOF, MONITOR. Verified at runtime against redis:7-alpine: PING/SET/GET work for the backend user, FLUSHALL and CONFIG GET return NOPERM, unauth requests return NOAUTH. REDIS_URL on backend + scheduler now embeds the backend ACL user. mcp-server: REDIS_URL and depends_on:redis dropped in prod compose (zero Redis imports in src/mcp-server/). Healthcheck pings as the backend ACL user so a typo'd ACL keeps redis unhealthy and gates dependent services. depends_on:redis switches to service_healthy so backend/scheduler don't race the ACL load. Refs #589 — acceptance criteria #1, #2, #5. * fix(scheduler-test-rig): mirror Redis auth posture (#589) Without this, scheduler container fails fast on startup against the rig because src/scheduler/config.py requires creds in REDIS_URL after #589. No ACL or network split here — this is a 2-service standalone debugging rig, not the production posture. * fix(security): fail-fast on REDIS_URL missing credentials (#589) Backend (src/backend/config.py) and scheduler (src/scheduler/config.py) now raise RuntimeError at import time if REDIS_URL is unset or lacks credentials. Removed the splicing fallback in backend config that papered over an unauth REDIS_URL by joining REDIS_PASSWORD into the URL — single source of truth (compose) eliminates silent drift. Tests that import backend modules need a creds-bearing REDIS_URL in their environment; tests/conftest.py will set a dummy one in the test commit. Refs #589 — acceptance criterion #5. * fix(webhooks): use REDIS_URL for rate-limit client (#589) Webhooks rate-limit was the one Redis client that bypassed REDIS_URL — it used redis.Redis(host="redis", port=6379) and would silently fail-open under requirepass. Switching to redis.from_url(REDIS_URL) picks up the credentialed URL like every other client. Also: distinguish auth/ACL errors (logged at ERROR with exception class) from transient errors (WARN). Fail-open behavior preserved so a Redis blip doesn't 500 legitimate webhooks, but a misconfigured deploy now surfaces in alerts instead of via a webhook abuse incident. Drops the now-unused REDIS_HOST/REDIS_PORT env reads. * feat(deploy): auto-generate Redis passwords on fresh installs (#589) start.sh ensure_redis_passwords matches the existing CREDENTIAL_ENCRYPTION_KEY pattern, with one safety guard: - Fresh install (no redis-data volume) → generate both passwords with openssl rand -hex 24 and append to .env. One-command boot keeps working. - Existing volume + missing password → refuse with a loud error pointing at docs/migrations/REDIS_AUTH.md. Re-keying a populated Redis would lock the backend out of its own data; ops needs to follow the explicit upgrade path. Idempotent — second run is a no-op when both passwords are already set. * docs(security): add Redis auth migration guide + architecture notes (#589) - docs/migrations/REDIS_AUTH.md: operator upgrade guide. Covers fresh installs (auto-generated by start.sh), live upgrades (down --remove-orphans + docker network rm + add passwords), production, and verification commands. - docs/memory/architecture.md: new "Network Topology (Issue #589)" section above Container Security. Documents the two-network split, service membership table, the "agents NEVER on platform network" rule, and the three Redis ACL users + their access patterns. * test(security): network isolation, ACL, fail-fast, webhook rate-limit (#589) tests/conftest.py: top-level autouse env stub for backend imports. Backend config now raises at import-time if REDIS_URL lacks credentials; without this, every test that transitively imports backend modules breaks. Real Redis tests under tests/security/ override via their own conftest from .env. Adds the `integration` marker. tests/unit/test_config_fail_fast.py (new): backend refuses to import without creds-bearing REDIS_URL. 3 cases — missing env, unauth URL, URL with creds. tests/security/test_redis_network_isolation.py (new): 5 integration tests covering acceptance criteria #1-#3: - agent-network container has no route to redis (BLOCKED) - unauth client gets NOAUTH on platform network - backend ACL user can PING with creds - backend ACL user FLUSHALL → NOPERM (no admin) - backend ACL user CONFIG GET → NOPERM (no requirepass leak) tests/security/conftest.py (new): session-scoped fixture loads real .env values for the integration tests; skips the suite if missing. tests/integration/test_webhook_rate_limit.py (new): regression for the from_url switch in webhooks.py. Self-contained — creates agent + schedule + webhook token inline, hits 11×, expects 429 on the 11th. Catches the silent fail-open if Redis auth ever regresses. tests/run-integration.sh (new): pytest -m integration runner. Excluded from run-smoke.sh per the smoke runner's ~30s no-Docker contract. * docs(security): detach agents before network rm (#589) Trinity-managed agent containers are created via the Docker SDK outside compose, so they store the agent network's UUID, not its name. After `docker network rm trinity-agent-network` (step 3 of the upgrade procedure), any later `docker start <agent>` fails: Error response from daemon: failed to set up container networking: network <old-uuid> not found Compose-managed services don't hit this — they're recreated with fresh network refs on `up`. Agent containers aren't, so they keep the stale UUID until disconnected. Add an explicit detach loop as step 2, before the network removal. Verified against a populated install with one running and four stopped agents: all five reattach cleanly to the new network on next start. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): CSO OBS-1/2/3 follow-ups — webhook rate-limit + healthcheck hardening (#589) Resolves three observations from the CSO audit (docs/security-reports/cso-2026-05-04-589-diff.md): OBS-1 — webhook rate-limit fail-open + connection-per-request DoS amplifier: * Added in-process secondary rate limiter (3x primary, per-worker) in src/backend/routers/webhooks.py. Bounds blast radius during a Redis outage without breaking the documented fail-open philosophy. * Cached the Redis client at module level under threading.Lock with double-checked init. _check_webhook_rate_limit resets the cache on inner exceptions so stale connections rebuild cleanly. Without caching, a flood would open a fresh TCP per request and exhaust Redis maxclients — turning the rate limiter into the DoS amplifier. OBS-2 — tightened _TOKEN_RE from {20,60} to {43} matching secrets.token_urlsafe(32) (verified against db/schedules.py:524). OBS-3 — switched all three compose healthchecks from `redis-cli -a $$PASS` to `REDISCLI_AUTH="$$PASS" redis-cli` so the password no longer appears in /proc/<pid>/cmdline. Additional #589 hardening (caught while resolving OBS-1): * src/backend/config.py + src/scheduler/config.py: tightened the REDIS_URL credential check from `"@" in url` substring to urlparse validation. Catches redis://@redis:6379, redis://user@redis:6379, etc. * src/scheduler/main.py: redact password from REDIS_URL before logging (was leaking via Vector log aggregator). Tests: * tests/unit/test_webhook_rate_limit_inprocess.py — 7 new tests covering cap, window expiry, token isolation, runtime-error fallback, regex shape, cache hit, cache reset. * tests/unit/test_config_fail_fast.py — 4 new parametrized cases for malformed-credential URL rejection. * 15/15 unit tests pass. * Live Redis healthcheck verified — trinity-redis reports healthy with the new REDISCLI_AUTH form; `redis-cli ping` returns PONG. Also adds .gstack/ to .gitignore so future skill artifacts stay local. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
pushed a commit
that referenced
this pull request
Jun 23, 2026
…erts, SSH, binary) (#1305) * feat(credentials): curated credential file-type injection — SA keys, certs, SSH, binary (enterprise#11) Widens CRED-002 injection from the fixed 3-path exact allowlist (.env/.credentials.enc/.mcp.json) to a curated set of credential file *types*, without reopening the arbitrary-path RCE surface (#183/#590/#598). - New services/credential_paths.py — single-source policy: ALLOW (.config/gcloud/**, .kube/config, *.pem/*.key/*.crt/*.cert/*.p12/*.pfx, .ssh/id_*, + existing exact set) with deny-precedence over anything executed/sourced at startup (shell rc, CLAUDE.md/AGENTS.md/.claude/**, .mcp.json.template, .ssh/authorized_keys/config, .git*, bin/**) and `..`/absolute traversal. Vendored byte-identically into the agent image (Invariant #5) with a parity test. - Agent-server hardening: the inject + update file loops now enforce the policy AND a resolve-under-home traversal guard the original write path lacked; parent-dir creation + chmod 0o600 preserved. New GET /api/credentials/list for export discovery. - Binary-safe: inject carries files_b64 (base64); agent writes via write_bytes. .credentials.enc gains a v2 {files, files_b64} envelope (legacy flat archives still decrypt); encrypt/decrypt stay flat for the single-secret callers (SIEM/2FA/SSO). - Export now captures the FULL injected set (via /list) + binary, not just the 2 defaults. - Three surfaces in sync (Invariant #13): MCP inject_credentials gains files_b64; frontend CredentialsPanel gains a file-upload affordance (text vs base64 auto-detected). - Tests: allowlist test now exercises the REAL policy (newly-allowed + still-blocked), + credential_paths parity test + binary archive round-trip test. 61 pass. - docs/memory/architecture.md: credential-path policy documented. Related to Abilityai/trinity-enterprise#11. Loosens a deliberately-tight boundary — run /cso on the diff before merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(credentials): close /cso findings on the injection widening (#11 review) Security review of the widening surfaced one HIGH regression + hardening items; all fixed here. #1 (HIGH, RCE): the #598 .mcp.json content-validation guard was bypassable via the new files_b64 (binary) channel — validate_mcp_config only checked `files`, so `files_b64={".mcp.json": base64(<stdio-command MCP server>)}` skipped it and configured an RCE MCP server on the target agent. Fix: .mcp.json may only arrive as TEXT (files), where it is validated; rejected in files_b64 at the backend inject router AND the agent-server write helper. #2 (defense-in-depth): import/auto-import wrote decrypted archives via the agent-server /inject layer only. Added validate_credential_set() (curated path policy + .mcp.json content + no-binary-.mcp.json) on the backend import boundary so enforcement is dual-layer as the issue mandates. (Archives are AES-GCM with the server key, so a forged archive wasn't practical — but the layer belongs.) #3: .ssh/ is now locked to id_* only — a stray *.key/*.pem under .ssh is no longer accepted (policy was previously broader than the "SSH keys = id_*" intent). #4 (noted): .config/gcloud/** can hold a google-auth executable credential_source; only honored under non-default GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1. Documented in credential_paths.py. +6 regression tests (169 pass). CSO report: docs/security-reports/cso-2026-06-22-11-diff.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(credentials): exclude vendor dirs from cert globs + accurate export count (#11 live test) Found while testing PR #1305 against a real local instance: 1. Over-capture: the broad *.pem/*.key/*.crt globs matched bundled CA files (e.g. .local/.../site-packages/certifi/cacert.pem), so export's /list walk swept vendored cert material into .credentials.enc. Added node_modules, site-packages, .local, .venv/venv, .cache, go/pkg to the deny-list (both root and nested forms) so cert globs only catch real credential files. 2. export's files_exported count re-read just the 2 default files (reported 1 while the archive actually held 5). export_to_agent now returns the true captured count; dropped the redundant stale read. Verified end-to-end on a live agent: allowed types inject (text+binary, 0600, parent dirs), blocked paths 400 (incl. .ssh non-id_*, .mcp.json-via-files_b64, weaponized .mcp.json text), and binary round-trips through export→import with matching sha256. +5 regression tests (72 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dolho
added a commit
that referenced
this pull request
Jun 26, 2026
…l lock (#19 review) Addresses the open review finding: the dialog had Esc-to-close but no focus management. Now on open it moves focus into the dialog (first focusable, else the panel), traps Tab within it (wrap-around), locks background scroll, and restores focus to the opener (Configure/Manage button) + the prior body overflow on close. Findings #1 (status-label truncation) and #2 (redundant dialog gating) were already addressed in earlier commits. ConfirmDialog.vue shares the same gap; a shared modal primitive applying this to both is the cleaner longer-term fix. Verified: 3 SFCs compile, check:tokens passes, clean npm ci + vite build green.
This was referenced Jul 2, 2026
vybe
pushed a commit
that referenced
this pull request
Jul 3, 2026
* feat(ui): reframe Sharing tab to external-client sharing via channels Part of the Access/Sharing redesign (Epic trinity-enterprise#16). The Access tab (#17) already owns Trinity operators; this scopes the Sharing tab to the operator → external-client surface. - Google-Docs-style "Share this agent" framing; operator language removed (operators live on the Access tab). - External access policy collapsed into one **Restricted ↔ Open** segmented control over require_email/open_access (Restricted = approval-gated, Open = anyone verified; identity proof always on for external sharing). - Pending requests kept, reframed as external clients awaiting approval. - Channels rendered as compact collapsible summary rows (new ChannelDisclosure.vue). Detailed config stays reachable inside the expanded row as a non-regressing interim seam — #19 replaces the body with a modal dialog. No channel functionality removed. - Outbound file sharing + public links nudged into a separate "Distribution" section (distribution, not client access). Frontend-only; no API changes. SharingPanel prop/emit contract unchanged. Verified: both SFCs compile (vue/compiler-sfc), design-token check passes. Related to Abilityai/trinity-enterprise#18 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ui): move channel config into dialogs + VoIP surface (#19) Builds on the Sharing-tab reframe (#18). Replaces the inline collapsible channel panels with compact summary rows that open the existing config panels in a modal — the tab stays a clean summary instead of a wall of forms. - New ChannelConfigDialog.vue — Teleport modal (backdrop + Esc close, scrollable body) matching the ConfirmDialog idiom. - New ChannelConfigRow.vue — summary row showing connected/off + a brief status (channel/handle/number), a Configure/Manage button that opens the dialog, and a lightweight status fetch (shared `api` client) that refetches on dialog close so the row reflects edits. The 4 channel panels render untouched in the dialog slot — no loss of functionality. - SharingPanel wires Slack/Telegram/WhatsApp/VoIP through ChannelConfigRow with per-channel status derivations. VoIP (previously backend-only) now has its Sharing UI surface, gated on `voip_available`. - Distribution (public links, file sharing) keeps the collapsible disclosure — out of scope for the channel-dialog change. Frontend-only; no API changes. Verified: 3 SFCs compile (vue/compiler-sfc), design-token check passes, and Vite transforms all modules on the live dev server (HTTP 200). Related to Abilityai/trinity-enterprise#19 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): mount channel config dialog only when open (no row layout collapse) A closed ChannelConfigDialog should collapse to Teleport comment nodes, but under some dev-server HMR states its slotted panel rendered inline as a flex child of the channel row, squeezing the label (esp. WhatsApp, whose panel is the widest) to zero width. Gate the dialog with v-if="dialogOpen" in ChannelConfigRow so the closed dialog — and its slotted panel — never participates in the row's flex layout, and mount the panel on demand. Make the dialog's Escape handler bind immediately so it still works when mounted already-open. Related to Abilityai/trinity-enterprise#19 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): truncate long channel status labels + collapse redundant dialog gating Addresses code-review findings on PR #1348: - [Medium] The connected-status label was a flex child without `min-w-0`, so a long label (e.g. WhatsApp `whatsapp:+14155238886`) overflowed the row instead of clipping. Add `min-w-0` to the label and `shrink-0` to the status dots and the "· setup needed" span. - [Low] Collapse the redundant triple-gating to a single mechanism: the dialog is mounted only via the parent `v-if="dialogOpen"`, so drop the now always-true `:open` prop and ChannelConfigDialog's internal `v-if="open"` (Escape binds for the component's lifetime via onMounted/onUnmounted). (Skipped the a11y note — focus-trap/scroll-lock would be new behavior beyond the existing ConfirmDialog parity; not a regression.) Related to Abilityai/trinity-enterprise#19 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): modal a11y for ChannelConfigDialog — focus mgmt, trap, scroll lock (#19 review) Addresses the open review finding: the dialog had Esc-to-close but no focus management. Now on open it moves focus into the dialog (first focusable, else the panel), traps Tab within it (wrap-around), locks background scroll, and restores focus to the opener (Configure/Manage button) + the prior body overflow on close. Findings #1 (status-label truncation) and #2 (redundant dialog gating) were already addressed in earlier commits. ConfirmDialog.vue shares the same gap; a shared modal primitive applying this to both is the cleaner longer-term fix. Verified: 3 SFCs compile, check:tokens passes, clean npm ci + vite build green. * ci: retrigger checks against dev base --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
12 tasks
vybe
added a commit
that referenced
this pull request
Jul 8, 2026
* fix(agent): make agent /tmp tmpfs size configurable via AGENT_TMP_SIZE (#1231) (#1233)
Agent containers mounted /tmp as a hardcoded 100 MB noexec,nosuid RAM-backed
tmpfs. It fills easily — e.g. `gh` CLI install artifacts (~38 MB) that hardcode
/tmp and bypass the #1098 TMPDIR redirect — after which every /tmp write fails
with "No space left on device", including git's commit scratch, so autonomous
scheduled runs "complete" but silently fail to persist. The size being a
literal meant operators couldn't tune it without a code change + base-image
rebuild.
- capabilities.py: AGENT_TMPFS_MOUNT size now read from AGENT_TMP_SIZE (env on
the backend service, which builds the agent mount spec), default 512m,
validated `^\d+[mg]$` with empty/invalid → default. noexec,nosuid stay
hardcoded — only size is configurable, and it stays bounded (counts against
the agent memory cgroup). Single source of truth, so create (crud.py) and
recreate (lifecycle.py) can't drift.
- Wire AGENT_TMP_SIZE=${AGENT_TMP_SIZE:-512m} on the backend service in both
docker-compose.yml and docker-compose.prod.yml; document in .env.example.
- architecture.md Container Security: note the now-configurable size.
- tests/unit/test_1231_agent_tmp_size.py: default, valid m/g, case-fold,
invalid→default, and the security flags are never dropped.
Mount specs are creation-time: existing agents pick up a new size on recreate,
not restart. Builds on #1098 (TMPDIR redirect) — closes the gap for tools that
hardcode /tmp. The agent-side git-sync silent-persist-failure is a separate
issue in the abilities repo, per the ticket.
Related to #1231
Co-authored-by: Eugene Vyborov <1073874+vybe@users.noreply.github.com>
* feat(ui): per-schedule performance scorecards on Agent Detail (#1115) (#1149)
Surface per-schedule performance on the Overview tab and the Schedules tab,
both from a SINGLE compact aggregate (no N per-schedule round-trips) — extends
#1107 (Overview) and generalises #868 (per-schedule deep analytics).
Backend:
- db `get_agent_schedules_summary(agent, hours)` — one rollup row per
non-deleted schedule (zero-run schedules included): terminal success_rate
(success / (success + failed[incl. error]); None when zero terminal),
NULL-skipping avg_duration_ms, cost_total, context_avg, tool_call_total
(parsed over newest 5,000 rows agent-wide, tool_calls_sampled flag), and
last-run outcome. Cheap grouped SQL; iso_cutoff window (Invariant #16).
- GET /api/agents/{name}/schedules/analytics-summary?window=7d|14d|30d
(AuthorizedAgent). Declared BEFORE /{schedule_id} in routers/schedules.py
so the literal segment isn't captured as a schedule_id (Invariant #4) —
putting it in analytics.py would be shadowed (schedules_router mounts first).
- models: ScheduleSummaryRow + AgentSchedulesSummaryResponse (Invariant #14).
Frontend (single fetch, two consumers — Invariant #7):
- executions.js fetchSchedulesSummary, cached per ${name}:${window} like
fetchAgentAnalytics.
- OverviewPanel: "Schedules performance" section, honors the existing 7/14/30d
window selector, each row deep-links to the Schedules tab; hidden at zero.
- SchedulesPanel: inline mini-stats per row (success rate, avg duration, runs,
last-run dot) — badge style, no new chart/modal — from the same call.
Tests: tests/unit/test_1115_schedules_summary.py (6) — terminal success rate,
NULL-skip avg, tool-call total, zero-run-still-appears, soft-delete excluded,
out-of-window excluded. Full analytics suites green (30 passed). Frontend
prod build clean; endpoint verified live across 7/14/30d windows.
Related to #1115
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Eugene Vyborov <1073874+vybe@users.noreply.github.com>
* feat(ui): in-app bug reporting from the floating Help widget (#1116) (#1283)
* docs(readme): document the Trinity Ops Agent and PostgreSQL backend/migration (#1290)
Adds a top-of-README callout recommending PostgreSQL for production (SQLite remains the zero-config dev default, opt-in via DATABASE_URL, #300), links the public Trinity Ops Agent (trinity-ops-public) for instance operations, and documents migrating existing SQLite instances via its /migrate-to-postgres skill. Also adds a Database section, a DATABASE_URL env row, and an ops-agent entry in the docs index.
Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(reliability): unify the SUB-003 auth-class failure classifier into one shared module (#1088) (#1297)
The `is_auth_failure` + `AUTH_INDICATORS` + `NON_AUTH_KILL_MARKERS` (#904)
logic was duplicated inline in `subscription_auto_switch.py` and
`scheduler/service.py`, kept in sync by a hand-written "keep these lists in
sync" comment — exactly how the #904 kill-marker bug class re-appears.
Consolidate into one canonical module:
- New `src/backend/services/failure_classifier.py` — canonical, pure-stdlib
classifier (55 lines). `subscription_auto_switch.py` now re-exports
`is_auth_failure` unchanged, so existing importers
(`routers/chat.py`, `services/task_execution_service.py`) and their test
patch targets keep working.
- New `src/scheduler/failure_classifier.py` — byte-identical vendored mirror.
The scheduler runs in a separate container and cannot import
`backend.services`; it uses the classifier for log-labelling only (picks the
`logger.error` wording, never gates a switch). The agent-runtime classifier
in `error_classifier` is intentionally NOT merged — it diverges semantically
and stays kill-safe by `_classify_signal_exit` precedence (D4).
- Byte-identity is enforced by
`tests/unit/test_904_sigkill_no_false_auth.py::TestBackendSchedulerParity`;
the re-export is pinned by `TestBackendReExportGuard`. No hand-sync.
Pure structural refactor, no behavioral change (verified by SHA-256 equality
of the two copies and line-for-line comparison vs the deleted code). The test
rewrite also drops the prior `exec(compile(...))` source-slicing in favour of
`importlib` path-loading, removing the only injection primitive in scope.
Tests: 19/19 pass in `test_904_sigkill_no_false_auth.py`.
CSO --diff: CLEAR (docs/security-reports/cso-diff-2026-06-21.md).
Refs #1088
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(orchestration): pull-pilot routing for agent→agent MCP chat behind default-OFF flag (#946) (#1293)
* feat(orchestration): pull-pilot routing for agent→agent MCP chat behind default-OFF flag (#946)
Phase 2 PoC for pull/work-stealing (Epic #1045, umbrella #1081). When
MCP_AGENT_CHAT_PULL_ENABLED is ON, a sequential agent→agent (scope='agent',
non-self) chat_with_agent is routed by the MCP server through the durable async
/task path instead of the synchronous held /chat; the caller gets an immediate
{accepted|queued, execution_id} receipt and polls get_execution_result.
scope='user', self-tasks, and parallel=true are unchanged. Default OFF — flag
flip + MCP routing revert is the whole rollback.
- config.py: canonical MCP_AGENT_CHAT_PULL_ENABLED registry entry (both services
read the SAME env key, so a single-.env deploy can't drift).
- settings.py: surface mcp_agent_chat_pull_enabled in /api/settings/feature-flags
(auth-gated, observability-only — not a UI surface).
- chat.py: release the idempotency claim on the two /task dispatch-breaker-open
(CircuitOpen) deny paths, mirroring /chat and CapacityFull (T5 fix) — without
it a breaker-open reject silently blocks same-key retries for 24h.
- mcp-server: scope-based pull routing + D8 dispatch-mode idempotency token so a
flag flip can't replay a wrong-shape snapshot across endpoints; startup log of
the routing mode for the soak's control/treatment window.
- tests: chat.test.ts (routing fork + key behavior), test_946_task_idempotency_on_deny.py
(deny-path claim release), feature-flag exposure tests.
- docs: ACTOR_MODEL_POSTCARD (#945 resolved), PULL_PILOT_946_SOAK harness +
go/no-go record, CSO diff audit (CLEAR), TARGET_ARCHITECTURE/architecture updates.
Refs #946
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(feature-flows): document pull-pilot routing (#946) + AGENT_TMP_SIZE tmpfs (#1231)
Sync feature-flow docs with recent changes:
- agent-to-agent-collaboration.md: new Pull-Pilot Routing (#946) section —
flag-gated MCP routing fork to the durable async /task path, poll-for-result
receipt contract, D8 idempotency route token, feature-flag exposure, and the
T5 /task dispatch-breaker-open deny-path claim release.
- container-capabilities.md: refresh stale tmpfs facts — agent /tmp size is now
operator-configurable via AGENT_TMP_SIZE (default 512m, noexec,nosuid fixed),
plus the TMPDIR=/home/developer/.tmp heavy-scratch redirect (#1098).
- feature-flows.md: add Recent Updates index rows for #946 and #1231.
Refs #946
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>
* feat(agent): runtime data_paths with portable export/import (#1169) (#1294)
* feat(agent): runtime data_paths with portable export/import (#1169)
Declare an agent's runtime data (SQLite DBs, datasets) under data/ on the
already-durable home volume — no separate volume, no platform schema change.
- template.yaml `data_paths:` surfaced by template_service (github + local)
and materialized at creation by crud.py -> git_service.materialize_data_paths:
writes ~/.trinity/data-paths.yaml and appends data/ + entries to the agent's
own .gitignore (idempotent). Opt-in; empty list is a no-op.
- S4 persistent-state and data_paths now share one extracted heredoc/list
primitive (materialize_trinity_yaml_list / _read_trinity_yaml_list).
- New routers/agent_data.py: POST /data/export (stream | base64, 413 over cap,
manifest-only tar when data/ missing) and POST /data/import (proxies to the
agent-server restore primitive; data/** allowlist + traversal guard;
Idempotency-Key). Both serialized per agent by a cross-worker Redis op lock.
- MCP tools export_agent_data / import_agent_data (Invariant #13).
- Validation checks DP-001..DP-005 in agent-validation-spec.
- Docs: architecture, requirements, feature-flows index + agent-data-volumes
flow, agent guide; CSO diff audit report (0 critical/high).
Tests: ~30 unit + TestClient tests (export/import endpoints, allowlist,
gitignore, template surface). PR2 (scheduled snapshots + pre-snapshot
quiesce hook + retention/cascade) deferred.
Closes #1169
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(agent-data): satisfy sys.modules pollution lint in #1169 tests
The two new data_paths test files copied the baselined `patch.dict` +
bare `del sys.modules[...]` loader from test_persistent_state_allowlist.py,
which the sys.modules pollution lint flags as NEW (non-baselined) violations.
Adopt the blessed snapshot/restore exception (precedent:
test_telegram_webhook_backfill.py): declare a top-level
`_STUBBED_MODULE_NAMES` list + an autouse `_restore_sys_modules` fixture, and
install the stubs / evict the cached module directly (the fixture owns
restoration). Removes the bare `del sys.modules[...]` entirely rather than
hiding it. Drop the now-unused `patch` import from the gitignore test.
Lint passes (no new violations); both files' 16 tests still green; no
cross-file leakage.
Refs #1169
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(feature-flows): sync recent changes — #1231 tmpfs, #1115/#1231 index rows
Fix container-capabilities.md for the now-configurable agent /tmp tmpfs
(#1231): default 100m → 512m via AGENT_TMP_SIZE, and correct the stale
full_capabilities ternary excerpts to the shared AGENT_TMPFS_MOUNT constant
(noexec,nosuid always applied, both modes). Add Recent Updates index rows for
the per-schedule performance scorecards (#1115) and the tmpfs-size fix (#1231);
the #1169 and #1116 rows already shipped in-commit.
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>
* fix(security): authenticate the in-container agent server on the shared agent network (#1159) (#1292)
* fix(security): authenticate the in-container agent server on the shared agent network (#1159)
The in-container agent server (:8000) had zero inbound auth on
trinity-agent-network: any agent could read a sibling's .env secrets or
run arbitrary Claude on it. Every backend->agent call now carries a
per-agent X-Trinity-Agent-Token = HMAC-SHA256(AGENT_AUTH_SECRET,
"trinity-agent-auth:v1:"+name), verified by a pure-ASGI middleware on all
HTTP and WebSocket routes (constant-time compare; only /health exempt).
- Derive-don't-store: the master AGENT_AUTH_SECRET lives only in the
backend env (auto-generated by start.sh like SECRET_KEY); each container
receives only its own token, so a compromised agent cannot compute a
sibling's. Fail-closed -- derive raises on an empty secret.
- Callers route through services/agent_auth.py (agent_httpx_client /
build_agent_auth_headers / merge_auth_headers); a static guard test
fails any new raw agent-{name}:8000 caller that bypasses them.
- Removed the dead, unauthenticated /ws/chat route (ran arbitrary Claude)
and the agent server's wildcard CORS (internal-only).
- Grace path for old images: empty TRINITY_AGENT_AUTH_TOKEN -> allow;
check_agent_auth_token_env_matches forces a one-pass recreate to inject.
- Retired the unused src/scheduler/agent_client.py.
Tests: unit (matcher, middleware, header guard, derivation) + security
isolation test. CSO diff audit in docs/security-reports/cso-diff-2026-06-20.md.
Closes #1159
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(feature-flows): document agent-server authentication (#1159) + sync recent flows
Add a feature-flow doc for the in-container agent-server inbound auth shipped
in this PR, and sync the index with two recent merged changes.
- New feature-flows/agent-server-authentication.md: end-to-end trace of the
derived X-Trinity-Agent-Token (HMAC over AGENT_AUTH_SECRET), the pure-ASGI
middleware enforcing it on every HTTP/WS route, fail-closed vs grace path,
recreate reconciliation, and the migrated callers + static guard. Added to
the Authentication & Security catalog in the index.
- container-capabilities.md: refresh the stale /tmp tmpfs size (was a hardcoded
100m) to the configurable AGENT_TMP_SIZE (default 512m, #1231).
- Recent Updates rows for #1159, #1231, and the previously-missing #1115.
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>
* docs(voip): genericize moved-issue reference in feature-flow
#1039 (configurable data-retention) moved to the private enterprise
tracker; replace the now-private issue number in voip-telephony.md with a
generic description so the public doc doesn't deep-link a private issue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(agents): server-side compatibility validation with auto-fix (#668)
Run ~100 best-practice checks (11 categories) against a running agent's
workspace, surfaced non-blocking in the Overview tab with one-click
auto-fix for the 10 gitignore checks, plus an MCP tool.
- services/compatibility/ package (spec/collector/static_checks/ai_checks/fixes):
ONE docker exec -> in-container python -> JSON snapshot (secret files
existence-only, size/binary caps); pure STATIC checks (HARD-only) +
category-batched AI checks (Haiku, iterate-expected, fail-open, capped at
SOFT, secret-redacted); runtime-aware (claude-only checks skipped for
Codex/Gemini).
- GET/POST endpoints (read AuthorizedAgentByName; fix OwnedAgentByName, gitignore
only, per-agent Redis lock, atomic write, uncommitted until next sync;
include_ai path rate-limited). agent_compatibility_results table (dual-track
SQLite + Alembic) persists the latest snapshot; cascade/rename via AGENT_REFS.
- CompatibilityPanel.vue (two-phase fetch, grouped checklist, per-check fix,
re-run) in OverviewPanel; get_agent_compatibility_report MCP tool.
- 35 fixture-driven unit tests; spec sync-tested against docs/agent-validation-spec.md.
Persistence departs from the issue's "no DB table" note so AI verdicts show
without re-spend + enable fleet aggregation (see requirements section 41).
Fixes #668
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(compat): remove polynomial-ReDoS in secret-assignment regex (#668)
CodeQL py/polynomial-redos (high): `_ASSIGN_RE` captured the value as
`[ \t]*(.+?)[ \t]*$`. The lazy `.+?` and the surrounding `[ \t]*` can both
match a tab, giving polynomial backtracking when `_redact()` runs the pattern
over up to 48 KB of agent-supplied file text.
Capture the value greedily to end-of-line (`(.*)$`) and let the callers
strip — both `_looks_placeholder()` callers already `.strip()`, so secret
detection and redaction are behaviourally identical (verified: `=`, `:`,
`export`, and indented forms still match). 35 unit tests pass.
* feat(sso): OSS gated surface for enterprise SSO (OIDC) (#32)
Companion to trinity-enterprise#36. OSS carries only the entitlement-gated
surface; all SSO logic lives in the private submodule.
- Login.vue: "Sign in with <IdP>" buttons (shown only when the `sso` feature is
entitled and a provider is enabled), plus OIDC callback-fragment handling
(`/login#sso=ok|mfa|error`) — reuses the existing 2FA challenge UI when the
IdP login still requires a local second factor.
- stores/auth.js: completeSsoLogin() (reuses _finalizeLogin / _setMfaChallenge)
+ fetchSsoProviders() (empty in OSS-only builds — endpoint 404s).
- Settings.vue: admin-gated "SSO" tab → SsoPanel.vue (provider CRUD + test +
policy). Gated by enterpriseStore.isEntitled('sso'), same as the 2FA tab.
- Bump enterprise submodule to the SSO module commit.
- docs: architecture enterprise-modules row + requirements §40 (SSO/OIDC).
No new backend dependency (python-jose + httpx already in the image) and no
OSS Python changes — the mint/whitelist/mfa seams already exist.
Stacked on feat/5-2fa-totp (reuses the OSS mfa_gate + 2FA challenge surface).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(sso): bump enterprise submodule to OIDC hardening (#32 review)
Pulls in the email_verified / issuer-pinning / login-CSRF fixes
(trinity-enterprise 87c8f97). OSS gated surface unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(planning): note incubating goal-directed direction; reword voip flow
Add an "Incubating Directions (Not Yet Decided)" section to
TARGET_ARCHITECTURE.md capturing the goal-directed control-surface idea
(Objective + policies + roster + externally-measured evals), explicitly
bounded by CLAUDE.md §8 and sequenced after the pull migration + #300.
Incubating in trinity-enterprise#27.
Reword the voip-telephony flow note to drop a stale #1039 reference in
favor of describing the LOG_* data-retention no-op class directly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(security): add CSO 2026-06-21 posture report
Routine /cso full-audit posture report (Phases 0–14, daily 8/10 gate).
Follows the docs/security-reports/ convention; no real secrets reproduced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(user-docs): video library + per-page links, v0.6.1 What's New, sync dev features
- Add videos.md (35 published videos, newest-first by topic) and a README Watch section
- Add 'Watch' callouts to 32 feature pages linking the most relevant, newest videos
- Add user-facing whats-new/v0.6.1.md (translated from release notes; no issue numbers)
- Document dev-only features: agent runtimes (Claude Code/Codex/Gemini CLI, #1187),
agent data paths + export/import (#1169), compatibility validation (#668),
in-app bug reporting (#1116), subscription hot-reload (#1089), pull-pilot routing (#946),
configurable AGENT_TMP_SIZE (#1231), Postgres migration-runner groundwork (#1160)
- Index agent-runtimes, agent-data, and the previously-orphaned agent-session pages
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): first-run operator intake + admin email login (abilityai/trinity-enterprise#38, #82)
Capture an optional operator email/company at first-run setup with an explicit,
unchecked-by-default opt-in to "occasionally receive important security & product
updates", submitted once to a new /v1/operator-intake endpoint on #1116's
Cloudflare intake app. The same email binds as the admin's sign-in identity so
the operator can log in with email + password — no verification email is sent (a
fresh install has no Resend key; the email is bound, not code-verified). The
code-based email second factor stays Phase 2 on the existing mfa_gate seam.
- backend: operator_intake_service (fire-and-forget, at-most-once via a
system_settings marker, DO_NOT_TRACK aware, owns installation_id); setup
endpoint captures profile + binds admin email; authenticate_user resolves the
admin by username OR registered email (password guard blocks code-only users);
PUT /api/users/me/email for the existing-admin transition
- frontend: SetupPassword email/company + consent checkbox; Login "username or
email" field; Settings -> General "Admin sign-in email" card
- config: OPERATOR_INTAKE_ENABLED / OPERATOR_INTAKE_URL (+ .env.example)
- docs: requirements section 43, architecture catalog, first-time-setup feature flow
- tests: 16 unit tests (intake idempotency/guards, email-login resolution, setup)
Fixes abilityai/trinity-enterprise#38
Fixes #82
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(feature-flows): index row for first-run intake + admin email login (trinity-enterprise#38, #82)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(setup): de-ambiguate email regex to clear CodeQL polynomial-ReDoS (#82)
The _EMAIL_RE pattern duplicated into setup.py and users.py had two
[^@\s]+ atoms around the literal \. that both also match '.', giving the
engine many ways to place the dot and backtracking polynomially on
user-controlled email input (CodeQL alerts #211, #212).
Constrain only the final segment to [^@\s.]+ (no dot) so the trailing \.
can align with exactly one position -> linear matching. Behaviour is
unchanged: multi-subdomain addresses still validate; an 80k-char
pathological input now resolves in ~2ms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(credentials): curated credential file-type injection (SA keys, certs, SSH, binary) (#1305)
* feat(credentials): curated credential file-type injection — SA keys, certs, SSH, binary (enterprise#11)
Widens CRED-002 injection from the fixed 3-path exact allowlist
(.env/.credentials.enc/.mcp.json) to a curated set of credential file *types*,
without reopening the arbitrary-path RCE surface (#183/#590/#598).
- New services/credential_paths.py — single-source policy: ALLOW (.config/gcloud/**,
.kube/config, *.pem/*.key/*.crt/*.cert/*.p12/*.pfx, .ssh/id_*, + existing exact set)
with deny-precedence over anything executed/sourced at startup (shell rc,
CLAUDE.md/AGENTS.md/.claude/**, .mcp.json.template, .ssh/authorized_keys/config,
.git*, bin/**) and `..`/absolute traversal. Vendored byte-identically into the
agent image (Invariant #5) with a parity test.
- Agent-server hardening: the inject + update file loops now enforce the policy AND
a resolve-under-home traversal guard the original write path lacked; parent-dir
creation + chmod 0o600 preserved. New GET /api/credentials/list for export discovery.
- Binary-safe: inject carries files_b64 (base64); agent writes via write_bytes.
.credentials.enc gains a v2 {files, files_b64} envelope (legacy flat archives still
decrypt); encrypt/decrypt stay flat for the single-secret callers (SIEM/2FA/SSO).
- Export now captures the FULL injected set (via /list) + binary, not just the 2 defaults.
- Three surfaces in sync (Invariant #13): MCP inject_credentials gains files_b64;
frontend CredentialsPanel gains a file-upload affordance (text vs base64 auto-detected).
- Tests: allowlist test now exercises the REAL policy (newly-allowed + still-blocked),
+ credential_paths parity test + binary archive round-trip test. 61 pass.
- docs/memory/architecture.md: credential-path policy documented.
Related to Abilityai/trinity-enterprise#11. Loosens a deliberately-tight boundary —
run /cso on the diff before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(credentials): close /cso findings on the injection widening (#11 review)
Security review of the widening surfaced one HIGH regression + hardening items;
all fixed here.
#1 (HIGH, RCE): the #598 .mcp.json content-validation guard was bypassable via
the new files_b64 (binary) channel — validate_mcp_config only checked `files`,
so `files_b64={".mcp.json": base64(<stdio-command MCP server>)}` skipped it and
configured an RCE MCP server on the target agent. Fix: .mcp.json may only arrive
as TEXT (files), where it is validated; rejected in files_b64 at the backend
inject router AND the agent-server write helper.
#2 (defense-in-depth): import/auto-import wrote decrypted archives via the
agent-server /inject layer only. Added validate_credential_set() (curated path
policy + .mcp.json content + no-binary-.mcp.json) on the backend import boundary
so enforcement is dual-layer as the issue mandates. (Archives are AES-GCM with
the server key, so a forged archive wasn't practical — but the layer belongs.)
#3: .ssh/ is now locked to id_* only — a stray *.key/*.pem under .ssh is no
longer accepted (policy was previously broader than the "SSH keys = id_*" intent).
#4 (noted): .config/gcloud/** can hold a google-auth executable credential_source;
only honored under non-default GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1.
Documented in credential_paths.py.
+6 regression tests (169 pass). CSO report: docs/security-reports/cso-2026-06-22-11-diff.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(credentials): exclude vendor dirs from cert globs + accurate export count (#11 live test)
Found while testing PR #1305 against a real local instance:
1. Over-capture: the broad *.pem/*.key/*.crt globs matched bundled CA files
(e.g. .local/.../site-packages/certifi/cacert.pem), so export's /list walk
swept vendored cert material into .credentials.enc. Added node_modules,
site-packages, .local, .venv/venv, .cache, go/pkg to the deny-list (both
root and nested forms) so cert globs only catch real credential files.
2. export's files_exported count re-read just the 2 default files (reported 1
while the archive actually held 5). export_to_agent now returns the true
captured count; dropped the redundant stale read.
Verified end-to-end on a live agent: allowed types inject (text+binary, 0600,
parent dirs), blocked paths 400 (incl. .ssh non-id_*, .mcp.json-via-files_b64,
weaponized .mcp.json text), and binary round-trips through export→import with
matching sha256. +5 regression tests (72 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(security): annotate CSO 2026-06-21 findings as remediated; drop stale voip hunk
Two review-driven fixes ahead of the v0.7.0 cut:
- Annotate the CSO posture report (.md + .json) with post-audit remediation
status. The report audited `main` pre-cut and listed F1/F2/F3 as open
VERIFIED findings; they are already remediated on `dev` and ship in v0.7.0:
- F1 (unauth agent-server) -> #1159 X-Trinity-Agent-Token middleware
- F2 (fastmcp -> hono/undici) -> #1255, #1289; fastmcp ^4.3.0
- F3 (form-data CRLF via axios) -> #1254
- F4/F5/F8 exploit path closed by #1159 (auth gate)
Adds a top-of-report banner, per-row status tags, per-finding notes, and a
machine-readable `remediation_status` block in the JSON. Avoids publishing a
stale "open CRITICAL + exploit" to a PUBLIC repo without its fix context.
- Drop the voip-telephony.md reword: it is superseded by already-merged #1301,
which made the identical `#1039` -> `LOG_*` change on `dev`. Restoring to
merge-base removes the redundant/conflicting hunk from this PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: set version to 0.7.0
* feat: streamline first-time setup wizard (abilityai/trinity-enterprise#49)
Drop the log-copied setup token, require an admin email, and rebuild the
first-run page as a welcoming animated welcome screen.
Backend (routers/setup.py, main.py):
- Remove the setup-token machinery entirely (ensure_setup_token /
clear_setup_token / Redis-shared token + the main.py startup emission).
Setup no longer depends on Redis — the admin write goes straight to SQLite.
- Make admin email REQUIRED (sign-in identity): missing -> 422 at the model
layer; blank/typo -> 400, validated before any write so setup never
half-completes. Password complexity (OWASP ASVS 2.1) still enforced.
- get_setup_status keeps setup_available:true for frontend back-compat.
Frontend (SetupPassword.vue):
- Full redesign: dark branded hero with an animated orbiting fleet
constellation (Trinity mark core + agent nodes on three rings), split
layout (stacks on mobile), prefers-reduced-motion aware.
- No setup-token field; email required; order email -> password (+confirm)
-> company -> updates opt-in. Removed the Redis-wait panel + polling.
Security tradeoff (chosen: accept + document): removing the token leaves the
unauthenticated first-run window with no proof-of-control. Documented as an
operator responsibility (deploy behind a tunnel/VPN until setup completes) in
docs/DEPLOYMENT.md Security Recommendations; endpoint still self-disables
after first success. See docs/security-reports/cso-diff-2026-06-23.md (F1).
Docs: DEPLOYMENT.md security note, architecture.md, requirements.md
(§15.2/§43), feature-flows/first-time-setup.md.
Tests: remove obsolete test_1165_setup_token_shared.py; update test_setup.py
(no token, email required) and test_setup_operator_profile.py (email
required, model-layer + blank/invalid rejection). 7 operator-profile unit
tests pass; new contract verified live (422/400 negative paths).
Fixes abilityai/trinity-enterprise#49
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(setup): full-bleed setup screen via normal flow, not position:fixed
The redesigned first-run page used `position:fixed; inset:0` for its root.
On wider viewports this left a band of the light `#app` (bg-gray-100)
background showing through on the right/bottom — a fixed root is clipped to
the nearest transformed/contained ancestor instead of the viewport, so its
coverage isn't guaranteed.
Switch the root to the original component's proven normal-flow approach
(`position:relative; width:100%; min-height:100vh`), which fills the
full-width `#app`, and make the decorative aurora/grid `position:absolute`
within it. Verified covering the full viewport at 2560x1440 (light mode, the
repro case) and stacking correctly at 430px.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tests): defer routers.setup import so unit collection can't be corrupted
The CI backend-unit regression gate runs `cd tests && pytest unit/` (the whole
unit suite). test_setup_operator_profile.py imported `routers.setup` at module
(collection) time; that import — pulling in database/dependencies/services and
their many `utils.*` leaves — failed/perturbed sys.modules during collection and
INTERRUPTED the entire `unit/` collection (head collected ~2 of 2734 → the diff
gate flagged it as a new failure).
Defer the `import routers.setup` to a cached `_get_setup()` accessor used inside
the tests, so module collection imports only stdlib/pytest/fastapi/pydantic and
can never corrupt the suite. `_get_setup()` also spec-preloads the backend
`utils.*` leaves (helpers/errors/credential_sanitizer/password_validation/
url_validation/image_optimize) the same way conftest preloads `utils.helpers`,
without touching `sys.modules["utils"]`, so the import resolves cleanly at run
time regardless of harness utils state.
Verified with the exact CI command (`cd tests && pytest unit/ --co`): the full
suite now collects 2734 items with no interruption, and the 7 setup tests pass.
No conftest changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tests): drop sys.modules preload — plain lazy import (passes #762 lint)
The previous commit's `_get_setup()` spec-preloaded backend utils leaves via
`sys.modules[...] = …` / `.pop`, which tests/lint_sys_modules.py (#762) bans
outside conftest. It's also unnecessary: in the backend-unit gate
(`cd tests && pytest unit/`), tests/unit/conftest.py already installs
src/backend/utils as the canonical `utils` package, so a plain lazy
`import routers.setup` resolves the backend `utils.*` leaves natively.
Simplify `_get_setup()` to a cached plain lazy import — no sys.modules
mutation. Verified: lint clean (no new violations), `pytest unit/ --co`
collects 2734 with no interruption, and the 7 setup tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tests): update #858 guards for setup-token removal (#49)
Removing the setup token (trinity-enterprise#49) deleted
`routers/setup.py::ensure_setup_token` and the lifespan token emission, so two
#858 regression guards asserted gone behavior and failed in the backend-unit
gate:
- test_ensure_setup_token_logs_token_via_logger_warning
- test_lifespan_emits_setup_token_via_logger_before_event_bus
The #858 invariant itself is intact: the lifespan still emits the first-run
notice via `logger.warning` (not print), after setup_logging() and before
event_bus.start(). Replace the token-specific guard with one that matches the
new FIRST-TIME SETUP warning by content + ordering, drop the now-obsolete
ensure_setup_token guard, and remove the unused BACKEND_SETUP constant. The
Dockerfile PYTHONUNBUFFERED parity checks and the no-print-in-lifespan guard are
unchanged. 4 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(whatsapp): deliver ChannelResponse.files as Twilio MediaUrl (#1315)
WhatsApp agents can now send files to users. send_response delivers
ChannelResponse.files as Twilio MediaUrl attachments (one message per file,
text first), reaching parity with the Slack adapter.
- New create_share_from_bytes() persists in-memory bytes through the FILES-001
pipeline (MIME-blocklist/quota/disk/DB) and mints a public ?sig= URL; both it
and create_share now share the extracted _persist_and_register helper.
- Per-agent file_sharing_enabled gate; 1h share TTL (cleanup reaper purges).
- Caps (image/audio/video ~5MB, documents ~16MB) on the detected MIME; graceful
text-link fallback when public_chat_url is unset/non-HTTPS, the MIME is
unsupported, or the file is oversized — never silently dropped.
- Per-file isolation: a rejected/failed file never aborts the text or siblings.
- 42 unit tests; requirements.md + whatsapp-integration.md updated.
Fixes #1315
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(whatsapp): webhook routes to backend, not frontend (#1281) (#1316)
The WhatsApp panel's deployment-prerequisite notice told operators to route
/api/whatsapp/webhook/* to the "frontend service". That path is a backend
FastAPI route (Twilio HMAC-verified); pointing tunnel ingress at the static
SPA silently drops inbound messages. Corrected to the backend service
(http://backend:8000), matching the cited PUBLIC_EXTERNAL_ACCESS_SETUP.md.
Related to #1281
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ui): loading skeletons for Dashboard graph & timeline (#1266) (#1312)
The initial fleet/metrics load can take 20s+ on 10+ agent fleets (#1265);
until now the Dashboard rendered the "No agents" empty state (or blank
timeline) during that wait, so the UI looked frozen/broken.
- New reusable `SkeletonLoader.vue` (dark-mode aware, accessible
role=status/aria-busy, reserves space to avoid layout shift) with `rows`
(timeline/list) and `nodes` (collaboration graph) variants.
- `stores/network.js`: add `loading` (defaults true so the first paint is a
skeleton, not the empty state) + `loadError` (distinct failed-load state),
toggled in `fetchAgents` (finally-cleared so a failure never shows an
infinite skeleton).
- `Dashboard.vue`: graph canvas and timeline now render skeleton → error →
empty → content off those flags. Loading shows immediately on nav; error
states offer a Retry (reuses `refreshAll`).
Frontend-only; pairs with the backend perf work in #1265. `vite build` passes.
Related to #1266
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(db): set SQLite end-of-support to September 1, 2026 + Postgres migration notes (#1278) (#1314)
Records the firm SQLite end-of-support date and the SQLite → PostgreSQL
migration announcement/guidance. Documentation/decision only — SQLite code
removal stays with the migration work (#300/#1183/#746).
- docs/migrations/SQLITE_TO_POSTGRES.md (new): authoritative guide — EOL date,
what changes and when, switching a fresh deployment (DATABASE_URL + postgres
profile), migrating an existing deployment (backup-first; no turnkey data-copy
tool yet — honest cutover options), verification, and release-notes copy.
- docs/releases/v0.6.2.md (new, draft): EOL announcement section linking the
guide, seeding the next release notes.
- docs/planning/TARGET_ARCHITECTURE.md + docs/memory/architecture.md (Invariant #3):
reference the EOL date so it's discoverable outside the release.
- Cross-links the in-repo reminder companion (#1279).
Related to #1278
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: stop disclosing enterprise functionality in public docs (trinity-enterprise#45) (#1311)
* docs: stop disclosing enterprise functionality in public docs (trinity-enterprise#45)
The public repo documented the full design, feature catalog, and gating
strategy of the paid enterprise tier — a free blueprint of what we monetize
and how it's built. This removes that competitive content and keeps only the
generic open-core seam public.
- Delete 4 strategy/design docs (OSS_ENTERPRISE_SPLIT_RESEARCH,
ENTERPRISE_ARCHITECTURE, feature-flows/enterprise-modules, ENTERPRISE_LOCAL_DEV)
- architecture.md "Enterprise Modules" table -> neutral seam pointer
(no paid-feature catalog, no enterprise_* table DDL, no per-module detail)
- requirements.md §35 -> abstract EntitlementService seam (drop the enumerated
module list + dead links to the deleted strategy docs)
- audit-trail.md: neutralize the lone enterprise-pillar mention
- CLAUDE.md: standing rule — enterprise designs live only in trinity-enterprise
- CI: enterprise-docs-guard.yml fails the build if live public docs reintroduce
paid-feature / private-schema tokens
Content is preserved (relocated to the private trinity-enterprise repo, see the
companion PR). Git-history scrub of the deleted files + point-in-time historical
docs (archive/, releases/, security-reports/) tracked as a follow-up.
Related to Abilityai/trinity-enterprise#45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(enterprise-docs-guard): add least-privilege permissions block
Clears CodeQL actions/missing-workflow-permissions (medium). The guard only
checks out and greps, so contents: read is sufficient.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(releases): add 0.7.0 release notes
* fix(#1115): port get_agent_schedules_summary to SQLAlchemy Core (Postgres-safe)
The #300 SQLAlchemy migration dropped the get_db_connection import from
db/schedules.py but left get_agent_schedules_summary (#1115) calling it,
so the /schedules/analytics-summary endpoint raised NameError at runtime.
Surfaced for the first time by the v0.7.0 release-PR full-suite run (dev
pushes only lint).
Port the method to get_engine() Core queries like its siblings, and
replace the SQLite-only bare-column-with-MAX last-run query with a
portable ROW_NUMBER() window so it works on PostgreSQL too.
Also refresh the test_login_rate_limit_split config stub, which went
stale when auth.py grew a PUBLIC_ACCESS_REQUESTS_ENABLED dependency
(trinity-enterprise#10) — 8 collection errors under HEAD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(voip): per-agent VoIP config panel + persisted voice (abilityai/trinity-enterprise#28)
Add the missing per-agent VoIP config UI (agent Settings/Sharing tab) and a
persisted per-agent Gemini voice. Shipped as plain OSS gated on the existing
voip_available platform flag — NOT entitlement-gated (a UI gate over a
money-spending OSS backend would be cosmetic; deliberate simplification of the
issue's original "entitlement-gated" framing).
Backend:
- agent_ownership.voice_name (default Kore) via dual-track migration
(SQLite db/migrations.py + Alembic 0004 + schema.py/tables.py). db
get/set_voice_name with read-path fallback to Kore for unset/invalid values.
- GET/PUT /api/agents/{name}/voice/name (PUT owner-only, validated against
GEMINI_VOICE_NAMES). _get_voice_name and voip_service now read the persisted
voice instead of the two hardcoded "Kore" sites.
- PUT /api/agents/{name}/voip/enabled toggle (owner-only, 404 when no binding);
create_binding upsert no longer forces enabled=1 so re-saving credentials
preserves a disabled state (call path already refuses disabled bindings).
Frontend:
- VoipChannelPanel.vue (modeled on WhatsAppChannelPanel) mounted in SharingPanel
under voip_available; shared src/constants/voices.js (drift-guarded vs backend);
AgentWorkspace picker defaults to the persisted voice; sessions store surfaces
voip_available.
Tests: tests/unit/test_28_voip_voice_config.py — voice fallback/roundtrip/
invalid->default, enable toggle + re-PUT-preserves-disabled (H3), and the
frontend/backend voice-list drift guard. Schema-parity + voip-db guards green.
Refs abilityai/trinity-enterprise#28
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(voip): HTTP-level endpoint tests for /voice/name + /voip/enabled (#28 review I1)
Pre-landing /review flagged that the new endpoints were covered only at the DB
layer. Add FastAPI TestClient tests (mount real routers, override auth deps, stub
db/voip_service) asserting:
- PUT /voice/name: owner-gated (403), 400 on unknown voice, empty clears to
default, valid voice persists; GET returns voice_name + available_voices.
- PUT /voip/enabled: owner-gated (403), 404 when no binding / when voip flag off,
200 reflecting state with no auth_token leaked.
Also capture a durable learning (docs/memory/learnings.md): the schema-parity
test is blind to db/tables.py drift — a missing Column there passes parity but
breaks at runtime; guard it with a db-accessor unit test that executes a live
select on the new column.
Refs abilityai/trinity-enterprise#28
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(docs): agent-readable zero-to-value onboarding (#1280) (#1336)
* refactor(docs): agent-readable zero-to-value onboarding (#1280)
Make the repo's entry points machine-first so an autonomous agent can self-orient and reach a useful result without a human translating context.
- AGENTS.md: add a "Using this file" machine-contract header (declares it the authoritative agent entry point, states the AGENTS/CLAUDE/README boundary, explains how to traverse). Rebuild Route-by-task with an explicit "Done when" zero-to-value signal per persona.
- CLAUDE.md: cross-link to AGENTS.md and frame CLAUDE.md as the contributor working agreement (auto-loaded by Claude Code), not the agent landing page.
- Fixes from a context-free agent onboarding test (AC#5 validation): AGENTS.md deploy verify no longer assumes an undefined $TOKEN (leads with `trinity agents list`, shows token derivation); deploy section states the running-instance prerequisite; README CLI example adds the `trinity agents list` verify step; docs/CLI.md leads with `pip install trinity-cli` (PyPI) and marks `-e src/cli/` as the from-source/dev variant.
Validated by an agent performing a zero-to-value deploy task using only repo files, no human context: self-oriented in 2 hops (README -> AGENTS.md) to a correct deploy+verify answer; friction items above are its findings, folded back in.
Repo-root AGENTS.md is hand-authored; the CLAUDE.md->AGENTS.md mirror (#1187) is per-agent-container (startup.sh), so these edits are conflict-free.
Related to #1280
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(templates): machine-readable starter-template catalog (#1280)
Affordance sweep for agent self-selection. Previously an agent had to `ls`
config/agent-templates/ (24 dirs, 7 of them test fixtures) and open each
template.yaml to find a starting point.
- config/agent-templates/README.md: catalog grouping the 17 real templates
(single-purpose: scout/sage/scribe/demo-*/trinity-system; the dd-* due-
diligence suite) with one-line affordances, how-to-use, and an explicit
"not starting points" list for the test/canary fixtures
- AGENTS.md: link the catalog from the Deploy-an-agent section so the
zero-to-value path is "pick a ready-made template", not "author from scratch"
Related to #1280
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tests): un-quarantine and fix the 15 unmasked unit failures (#1103) (#1338)
Removes the @pytest.mark.skip quarantines added in #300 and fixes the
underlying issues. Each group fixed at root cause, not by matching assertions
to current behavior.
Environmental (git identity):
- tests/unit/conftest.py: set GIT_AUTHOR/COMMITTER_NAME/EMAIL process-wide so
in-test `git commit` works on a CI runner with no global git identity
("Author identity unknown"). Fixes test_reset_preserve_state_guardrails (3)
and the test_git_pull_branch end-to-end setups.
Test-setup bug (production code was correct):
- test_git_pull_branch.py: the repos did `git push -u origin main` but `git init`
defaults to `master` (no init.defaultBranch), so origin/main never existed and
_get_pull_branch correctly fell back to the working branch — the assertions
expecting "main" failed. Force `git init -b main` (local + bare). Fixes all 5
(TestGetPullBranch 2 + TestGitPullFromMainEndToEnd 3).
Test-isolation bug (assertions were correct):
- test_orphaned_execution_recovery.py: shared module-level mocks were reset with
plain reset_mock(), which keeps return_value/side_effect — so one test's
get_agent_container.side_effect bled into later tests under random ordering,
skewing recovery counts ("assert 3 == 2"). Reset with
reset_mock(return_value=True, side_effect=True). Stable across 5 seeds.
Real lint findings:
- docker/base-image/startup.sh: shellcheck now exits 0. Converted the 4 fragile
file-iteration loops to `find -print0 | while read` (SC2010/SC2045/SC2044),
hardened 8 `cd` with `|| exit 1` (SC2164), split the SC2155 export, and
documented-disabled SC2001 on the two regex `sed` lines that ${//} can't
express. `bash -n` clean. Un-skips test_startup_sh_shellcheck_clean.
backlog (3) and 929 (1) were already un-quarantined on dev (db_harness schema),
so no change needed there.
Verified: the 11 un-skipped tests pass across multiple random seeds; full unit
suite shows no regressions from these changes (the unrelated pre-existing
test_1115_schedules_summary / test_admin_email_login failures fail on dev too).
Related to #1103
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(deps-dev): bump happy-dom (#1326)
Bumps the patch-and-minor group in /tests/git-sync with 1 update: [happy-dom](https://github.com/capricorn86/happy-dom).
Updates `happy-dom` from 20.10.5 to 20.10.6
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.10.5...v20.10.6)
---
updated-dependencies:
- dependency-name: happy-dom
dependency-version: 20.10.6
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: patch-and-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps-dev): bump @types/node in /src/mcp-server (#1327)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.3 to 26.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)
---
updated-dependencies:
- dependency-name: "@types/node"
dependency-version: 26.0.0
dependency-type: direct:development
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump fastmcp (#1325)
Bumps the patch-and-minor group in /src/mcp-server with 1 update: [fastmcp](https://github.com/punkpeye/fastmcp).
Updates `fastmcp` from 4.3.0 to 4.3.2
- [Release notes](https://github.com/punkpeye/fastmcp/releases)
- [Commits](https://github.com/punkpeye/fastmcp/compare/v4.3.0...v4.3.2)
---
updated-dependencies:
- dependency-name: fastmcp
dependency-version: 4.3.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: patch-and-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump the patch-and-minor group (#1329)
Bumps the patch-and-minor group in /src/frontend with 5 updates:
| Package | From | To |
| --- | --- | --- |
| [axios](https://github.com/axios/axios) | `1.18.0` | `1.18.1` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.5.0` | `10.5.1` |
| [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup) | `4.62.0` | `4.62.2` |
| [@rollup/rollup-linux-arm64-musl](https://github.com/rollup/rollup) | `4.62.0` | `4.62.2` |
Updates `axios` from 1.18.0 to 1.18.1
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.0...v1.18.1)
Updates `@playwright/test` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.0...v1.61.1)
Updates `autoprefixer` from 10.5.0 to 10.5.1
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.5.0...10.5.1)
Updates `@rollup/rollup-darwin-arm64` from 4.62.0 to 4.62.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.62.0...v4.62.2)
Updates `@rollup/rollup-linux-arm64-musl` from 4.62.0 to 4.62.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.62.0...v4.62.2)
---
updated-dependencies:
- dependency-name: axios
dependency-version: 1.18.1
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: patch-and-minor
- dependency-name: "@playwright/test"
dependency-version: 1.61.1
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: patch-and-minor
- dependency-name: autoprefixer
dependency-version: 10.5.1
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: patch-and-minor
- dependency-name: "@rollup/rollup-darwin-arm64"
dependency-version: 4.62.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: patch-and-minor
- dependency-name: "@rollup/rollup-linux-arm64-musl"
dependency-version: 4.62.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: patch-and-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(executions): propagate cancelled terminal status end-to-end (#679) (#1333)
Defense-in-depth follow-up to #671. Make the agent task-runner aware that an
operator cancel happened and surface a third terminal outcome — `cancelled` —
alongside success/failed, so a cancel is never recorded as a billable success
or an agent failure.
Agent server:
- ProcessRegistry records a `_terminated[execution_id]` marker on a successful
SIGINT send; `was_terminated()` (read-only, 300s lazy TTL, cleared on
register) lets the sync chat handler and async result callback relabel a
graceful-exit-0 / SIGKILL->504 turn as cancelled.
- `record_task_finish` accepts a neutral finish (success=None): a cancel
neither resets nor increments the dispatch-breaker failure counter (#526).
Backend:
- 3-way status map (success->SUCCESS, cancelled->CANCELLED, else->FAILED) in
the async callback (routers/agents.py) and the sync applier
(task_execution_service). An auth/rate terminal is never reclassified as a
cancellation — guarded at the backend trust boundary too (CSO finding 2).
- Consumers (message_router, chat, paid, public, validation_service) treat
cancelled as non-delivery; paid no longer settles on cancel (money bug).
- terminate writes CANCELLED only when it actually stopped a running turn; on
already-finished the agent's real terminal stands (Issue 7).
Tests: 9 new unit suites (64 cases) + execution-termination integration
additions; 85 unit tests pass locally. CSO diff audit: CLEAR.
Refs #679
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ui): unify Chat + Session into one Chat tab with a session-mode toggle (#1112) (#1340)
Collapse the redundant Chat + Session tabs on Agent Detail into a single "Chat"
tab carrying a "Session mode" toggle (default ON), keeping the legacy stateless
surface as a first-class user-selectable mode rather than dead code.
- AgentDetail.vue: single `{ id: 'chat' }` tab (drop the separate Session entry).
New `chatMode` ref ('session'|'legacy', default 'session') persisted per-user in
localStorage['trinity.chatMode']. `sessionAvailable` = feature flag on AND
runtime has --resume (not Codex); `effectiveChatMode` forces legacy when the
Session surface is unavailable and hides the toggle. The toggle swaps
SessionPanel ↔ ChatPanel in-place (v-if). isFullscreenTab keys on the single
'chat' id; `?tab=session` aliases to 'chat' (hinting session mode).
- Execution-resume: ExecutionDetail "continue as chat" (?tab=chat&resumeSessionId)
forces legacy ChatPanel (which owns resume) via a transient, non-persisted
routeForcedMode — without rewriting the user's saved preference.
- No backend change (session_tab_enabled already exists). MobileAdmin unaffected:
its openChat is a self-contained mobile chat overlay, not an AgentDetail
deep-link, so there is nothing to repoint.
- docs: architecture Session Tab block + requirements §5.8 note.
Related to #1112
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(access): Access tab — manage Trinity operators per agent (trinity-enterprise#17) (#1317)
New Access tab on Agent Detail that manages Trinity operators (platform users)
with access to an agent, distinct from the Sharing tab's external channel
clients. Draws the operator-vs-client line on the read path.
Backend:
- db.get_agent_operator_access(): outer-joins agent_sharing × users on the
grantee email (lower-cased, engine-based → PG+SQLite). Resolved → active
operator (username/role/last_active); unresolved → pending invite.
- GET /api/agents/{name}/access (AgentOperatorAccess model). Add/remove reuse
the existing /share + /share/{email}.
Frontend:
- AccessPanel.vue: operator roster (status + role badges, last-active), add by
email, remove. Access tab wired into AgentDetail (owner-gated).
- SharingPanel.vue: Team Sharing allow-list removed (moves to Access); dead
share-management script + stale "Team Sharing below" copy cleaned/repointed.
- stores/agents.js: getAgentAccess().
Tests: active-vs-pending classification + agent scoping. vite build passes.
Note: the strict client-vs-operator split (non-user emails → a dedicated client
roster) is deferred to the Sharing-side redesign (#18/#20); removing them here
now would orphan those grants, so all allow-list entries stay visible on Access.
Related to Abilityai/trinity-enterprise#17
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ui): slim the Overview executions-by-type bars
The "Executions by type" stacked bars rendered full-width with a 1px
gap, so a busy agent's week read as a solid wall of color. Cap each
bar at 56px and center it inside its (still full-width) hover column,
and soften the top corner. The column stays flex-1 so spacing/tooltips
are unchanged and wider windows (14d/30d) thin naturally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(voip): add Gacrux to the Gemini Live voice picker
Adds the "Gacrux — Mature" prebuilt voice to the per-agent VoIP voice
selector (and the shared AgentWorkspace per-session picker, which reads
the same list). Updated in lockstep across the three mirrored sources so
the frontend↔backend parity test stays green:
- src/frontend/src/constants/voices.js — single frontend source of truth
- src/backend/config.py GEMINI_VOICE_NAMES — write-validation allowlist +
read-path fallback
- tests/unit/test_28_voip_voice_config.py — hardcoded parity tuple
Follow-up to #1323 (per-agent VoIP config panel + persisted voice).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ui): reframe Sharing tab to external-client sharing via channels (#1347)
Part of the Access/Sharing redesign (Epic trinity-enterprise#16). The Access
tab (#17) already owns Trinity operators; this scopes the Sharing tab to the
operator → external-client surface.
- Google-Docs-style "Share this agent" framing; operator language removed
(operators live on the Access tab).
- External access policy collapsed into one **Restricted ↔ Open** segmented
control over require_email/open_access (Restricted = approval-gated, Open =
anyone verified; identity proof always on for external sharing).
- Pending requests kept, reframed as external clients awaiting approval.
- Channels rendered as compact collapsible summary rows (new
ChannelDisclosure.vue). Detailed config stays reachable inside the expanded
row as a non-regressing interim seam — #19 replaces the body with a modal
dialog. No channel functionality removed.
- Outbound file sharing + public links nudged into a separate "Distribution"
section (distribution, not client access).
Frontend-only; no API changes. SharingPanel prop/emit contract unchanged.
Verified: both SFCs compile (vue/compiler-sfc), design-token check passes.
Related to abilityai/trinity-enterprise#18
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(architecture): trim architecture.md under the 150k-char context limit (#1344)
architecture.md had grown to ~156.7k chars (on dev), past the 150k
soft limit Claude Code warns about when auto-loading it each session
(it's `@`-imported by CLAUDE.md). Over the limit the file risks silent
truncation and eats a large slice of the context window every session.
Compressed the densest Cross-Cutting Subsystem narratives, the migration
and non-root-container invariants (#3/#17), a few catalog/endpoint rows,
and the longest frontend UI prose — preferring summary + pointer where a
dedicated `feature-flows/` doc already owns the deep detail (the doc's own
editorial rule). Result: 156.7k → 149.3k chars (~4.8% smaller).
No facts dropped: every issue tag, field name, default, and error-string
is preserved; protected SQLite DDL (tracked by /validate-schema) untouched;
heading/code-fence/table counts unchanged; all 12 added flow-doc links
resolve.
Related to the over-limit warning surfaced in Claude Code.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps): bump js-yaml from 4.2.0 to 5.1.0 in /src/frontend (#1330)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 5.1.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...5.1.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 5.1.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(ci): guard the Alembic (Postgres) track in schema-parity (#1342) (#1345)
* fix(ci): guard the Alembic (Postgres) track in schema-parity (#1342)
The schema-parity required check validated only the SQLite track
(migrations.py ↔ schema.py). A schema change that ships the SQLite
migration but omits the Alembic revision under
src/backend/migrations/versions/ passed every required check green yet
broke PostgreSQL — init_database() runs alembic_runner.upgrade_to_head(),
which applies revision files only and does not autogenerate from
tables.py. Two PRs reached "green CI but PG-broken" and had to be held by
hand.
Add a cross-track guard, folded into the existing required schema-parity
job (no new required-check to manage):
- scripts/ci/check_alembic_parity.py — fails a PR that ADDS schema DDL to
db/{migrations,schema,tables}.py without a net-new revision file under
src/backend/migrations/versions/. Pure stdlib, PR-only (diffs base...head).
- Heuristic / false-positive guard: the signal is a DDL keyword on an
*added, non-comment* line (SQL: CREATE/ALTER/ADD COLUMN/…; SQLAlchemy:
Column(/Table(/Index(/…). Comment edits, data-only and down migrations
carry no DDL keyword, so they don't trip it. Documented in the script
docstring and the workflow header.
- tests/unit/test_alembic_parity_guard.py — 20 tests incl. the acceptance
fixtures (SQLite-only change fails; dual-tracked passes; comment/data-only
pass). Wired into the parity pytest run.
Also notes the enforcement in architecture.md Invariant #3.
Verified locally: 24 tests pass; end-to-end smoke across clean / SQLite-only
(exit 1) / paired-revision (exit 0) scenarios behaves correctly.
Related to #1342
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): tighten Alembic guard to require a new MIGRATIONS entry (#1342)
Verifying against real repo history surfaced a false positive: the
migration-runner refactor #1263 (_atomic_rebuild table rebuilds) re-emits
CREATE TABLE / CREATE INDEX for *existing* tables in a rename-swap but adds
no actual schema and no new MIGRATIONS entry — yet the original "any added
DDL keyword" heuristic flagged it, violating AC #4 (non-schema edits must
not trip).
Tighten the signal to two conjuncts: a schema change must (1) register a
net-new ("name", _migrate_fn) entry in the MIGRATIONS list AND (2) carry a
DDL keyword. Runner refactors / table rebuilds add no entry → exempt;
data-only new migrations carry no DDL → exempt; real column/table adds do
both → caught.
Validated against real commits:
• #740 agent_loops, #526 agent_ownership column → FAIL (correctly blocked)
• #668 compat, voice_name (both shipped an Alembic revision) → PASS
• #1263 runner refactor → PASS (false positive fixed)
A full post-Alembic history scan finds 0 outstanding missing revisions, so
no backfill is owed; the pre-Alembic columns are already in 0001_baseline.
28 tests pass (added MIGRATIONS-entry detection + the #1263 rebuild case).
Related to #1342
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>
* docs(dependab…
6 tasks
5 tasks
vybe
pushed a commit
that referenced
this pull request
Jul 9, 2026
…118 Part A) (#1555) * feat(mcp): make per-agent MCP connector OSS-core (trinity-enterprise#118 Part A) Relocate the per-agent MCP connector (ent#46/#55/#51, shipped v0.8.0 entitlement- gated as `mcp_connector`) from the private enterprise submodule into OSS core, and drop the entitlement gate front and back. Decision (Eugene, 2026-07-09): sharing agents via individual MCP connectors is a platform-adoption surface, not a paid module. Backend (router → service → db, Invariants #1/#2/#14): - routers/connector.py — /api/agents/{name}/connector* (config, mint/regenerate/ revoke key, playbooks), mounted unconditionally in main.py; no requires_entitlement. - services/connector_service.py — snippet builder + playbook resolution. - db/connector.py (ConnectorOperations) — config CRUD + scoped-key mint/revoke into mcp_api_keys (scope='connector'); facade delegators on database.py. - models.py — ConnectorConfigUpdate/Status/KeySecret/Playbook/ClientSnippet. Schema: enterprise_connectors table re-homed onto OSS dual-track (db/tables.py, db/schema.py, db/migrations.py:enterprise_connectors_table + Alembic 0015_enterprise_connectors). Name kept so existing enterprise installs adopt their data with zero migration (CREATE TABLE IF NOT EXISTS, no duplicate-table drift). Delete/rename cascade via an enterprise_connectors AGENT_REF. Frontend: ConnectorChannelPanel un-gated in SharingPanel.vue (dropped the isEntitled('mcp_connector') v-if + the now-unused enterprise store wiring). The MCP proxy tools (connector.ts), the connector-scope auth fence (dependencies.py), and ExposedToolsPanel.vue were already OSS and edition-agnostic. `mcp_connector` is removed from the entitlement registry by the paired enterprise PR (deletes register_module). Docs: requirements mcp.md §7.5 + feature-flows/mcp-connector.md + architecture/index. Tests: tests/unit/test_118_mcp_connector_oss.py (11) — service helpers, config CRUD, key mint/regenerate/revoke, scope='connector' validate contract. Part B (email-auth onboarding, #848) deferred pending design sign-off. Related to trinity-enterprise#118 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(submodule): bump enterprise to post-#121 so the connector isn't double-mounted (#118) #1555 makes the per-agent MCP connector OSS-core. The enterprise submodule was pinned at 630cca9e, which still ships backend/mcp_connector/ and registers it via register_enterprise() — so an enterprise build would double-mount the connector router alongside the new OSS-core one. Bump the pin to f5d69be6 (enterprise main post trinity-enterprise#121), where the private module is removed. This forward-integrates two already-on-enterprise-main commits into the pin: - trinity-enterprise#121 — removes backend/mcp_connector/ (the intended pair) - trinity-enterprise#120 — ENTERPRISE_LOCAL_DEV docs (docs only) - trinity-enterprise#106 — client-portal umbrella (already on enterprise main) OSS-only CI is unaffected (submodule is update=none / "boots without enterprise"). 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>
Merged
3 tasks
10 tasks
2 tasks
dolho
added a commit
that referenced
this pull request
Jul 17, 2026
…ent#180)
An exposed agent's card advertises every `template.yaml capabilities[]` tag, and
the well-known discovery route is unauthenticated by design — so today that full
capability list is world-readable for any exposed agent. This lets an operator
choose what the outside is told.
**A disclosure control, and the code says so.** The card's `skills[]` is
advertisement: inbound `message/send` dispatches free-form text via
`execute_task(triggered_by="a2a")`, with no per-skill routing. Filtering changes
what an orchestrator SEES, never what it may ASK for. That's stated in the
requirement, the seam docstring and the filter itself, because a filter
operators mistake for an invocation gate is a control that looks like security
and isn't. A real boundary (constraining a2a-triggered runs via
allowed_tools/guardrails) is separate work with its own threat model.
Extends the existing ent#157 seam rather than adding a module — same shape as
the inbound allow-list, second provider:
provider.exposed_skills(agent_name) -> Optional[List[str]]
- No provider (OSS) → identity function; the card is byte-identical to before,
by construction. The enterprise module owns the config, storage and UI.
- `None` = no opinion = advertise all: the unconfigured default, so exposure
(already opt-in, default OFF) keeps every existing card unchanged on upgrade.
- `[]` ≠ `None`: an explicit "advertise nothing".
- Stale ids are inert — the selection only subtracts; `template.yaml` stays the
source of truth for what exists.
- Fail-open on provider error (advertise all + WARNING): consistent with the
seam's availability bias and the advertise-all default. Honest only *because*
this isn't a security boundary — failing closed would silently empty a card
and break discovery invisibly.
Both card surfaces (public well-known + authenticated per-agent) go through one
router helper, so they can't disagree and a future third surface gets the filter
by default rather than by remembering. `generate_a2a_card` stays pure — the
provider lookup lives in the helper.
Writing the tests found a real gap: a provider returning a str (a defect) would
iterate into single characters, match no id, and silently empty the card —
fail-CLOSED, the opposite of the contract, and invisible. Malformed returns now
take the same fail-open path as a raised error.
Requirements §32.4 written before the code (CLAUDE.md rule #1); public docs
describe the generic seam only, per the standing enterprise-docs rule.
Related to trinity-enterprise#180
dolho
added a commit
that referenced
this pull request
Jul 17, 2026
…ent#180)
An exposed agent's card advertises every `template.yaml capabilities[]` tag, and
the well-known discovery route is unauthenticated by design — so today that full
capability list is world-readable for any exposed agent. This lets an operator
choose what the outside is told.
**A disclosure control, and the code says so.** The card's `skills[]` is
advertisement: inbound `message/send` dispatches free-form text via
`execute_task(triggered_by="a2a")`, with no per-skill routing. Filtering changes
what an orchestrator SEES, never what it may ASK for. That's stated in the
requirement, the seam docstring and the filter itself, because a filter
operators mistake for an invocation gate is a control that looks like security
and isn't. A real boundary (constraining a2a-triggered runs via
allowed_tools/guardrails) is separate work with its own threat model.
Extends the existing ent#157 seam rather than adding a module — same shape as
the inbound allow-list, second provider:
provider.exposed_skills(agent_name) -> Optional[List[str]]
- No provider (OSS) → identity function; the card is byte-identical to before,
by construction. The enterprise module owns the config, storage and UI.
- `None` = no opinion = advertise all: the unconfigured default, so exposure
(already opt-in, default OFF) keeps every existing card unchanged on upgrade.
- `[]` ≠ `None`: an explicit "advertise nothing".
- Stale ids are inert — the selection only subtracts; `template.yaml` stays the
source of truth for what exists.
- Fail-open on provider error (advertise all + WARNING): consistent with the
seam's availability bias and the advertise-all default. Honest only *because*
this isn't a security boundary — failing closed would silently empty a card
and break discovery invisibly.
Both card surfaces (public well-known + authenticated per-agent) go through one
router helper, so they can't disagree and a future third surface gets the filter
by default rather than by remembering. `generate_a2a_card` stays pure — the
provider lookup lives in the helper.
Writing the tests found a real gap: a provider returning a str (a defect) would
iterate into single characters, match no id, and silently empty the card —
fail-CLOSED, the opposite of the contract, and invisible. Malformed returns now
take the same fail-open path as a raised error.
Requirements §32.4 written before the code (CLAUDE.md rule #1); public docs
describe the generic seam only, per the standing enterprise-docs rule.
Related to trinity-enterprise#180
dolho
added a commit
that referenced
this pull request
Jul 17, 2026
…ent#180)
An exposed agent's card advertises every `template.yaml capabilities[]` tag, and
the well-known discovery route is unauthenticated by design — so today that full
capability list is world-readable for any exposed agent. This lets an operator
choose what the outside is told.
**A disclosure control, and the code says so.** The card's `skills[]` is
advertisement: inbound `message/send` dispatches free-form text via
`execute_task(triggered_by="a2a")`, with no per-skill routing. Filtering changes
what an orchestrator SEES, never what it may ASK for. That's stated in the
requirement, the seam docstring and the filter itself, because a filter
operators mistake for an invocation gate is a control that looks like security
and isn't. A real boundary (constraining a2a-triggered runs via
allowed_tools/guardrails) is separate work with its own threat model.
Extends the existing ent#157 seam rather than adding a module — same shape as
the inbound allow-list, second provider:
provider.exposed_skills(agent_name) -> Optional[List[str]]
- No provider (OSS) → identity function; the card is byte-identical to before,
by construction. The enterprise module owns the config, storage and UI.
- `None` = no opinion = advertise all: the unconfigured default, so exposure
(already opt-in, default OFF) keeps every existing card unchanged on upgrade.
- `[]` ≠ `None`: an explicit "advertise nothing".
- Stale ids are inert — the selection only subtracts; `template.yaml` stays the
source of truth for what exists.
- Fail-open on provider error (advertise all + WARNING): consistent with the
seam's availability bias and the advertise-all default. Honest only *because*
this isn't a security boundary — failing closed would silently empty a card
and break discovery invisibly.
Both card surfaces (public well-known + authenticated per-agent) go through one
router helper, so they can't disagree and a future third surface gets the filter
by default rather than by remembering. `generate_a2a_card` stays pure — the
provider lookup lives in the helper.
Writing the tests found a real gap: a provider returning a str (a defect) would
iterate into single characters, match no id, and silently empty the card —
fail-CLOSED, the opposite of the contract, and invisible. Malformed returns now
take the same fail-open path as a raised error.
Requirements §32.4 written before the code (CLAUDE.md rule #1); public docs
describe the generic seam only, per the standing enterprise-docs rule.
Related to trinity-enterprise#180
7 tasks
4 tasks
vybe
pushed a commit
that referenced
this pull request
Jul 17, 2026
…#1676) * feat(agents): editable display label with an immutable slug (ent#181) An agent had exactly one name — its slug — so "rename" meant the heavyweight identity change: `PUT /rename` stops the container, rewrites ~20 tables, clears every per-agent Redis keyspace, renames avatar files, and STILL leaves the agent's volumes under the old base, because Docker can rename neither a volume nor its immutable `trinity.agent-name` label. That path is the root of #1664/#1665/#1667/#1669/#1671. Yet "call it Marketing Bot" is the common case, and it should not touch any of that. Adds a display label that is rendered, never resolved. The slug stays the identity everything machine-facing keys on; the label is presentation. - `agent_ownership.display_label TEXT`, nullable — NULL means "render the slug", so no backfill and every existing agent is unchanged until someone sets one; clearing reverts to the slug rather than blanking a name. Dual-track migration (Invariant #3): SQLite `agent_ownership_display_label` + Alembic 0025, plus schema.py / tables.py. - `DisplayLabelMixin` (Invariant #2 — new setting, new mixin), with a batched reader for the fleet list so the hottest endpoint stays one query, and the four facade pass-throughs (the #1666 lesson: mocked tests can't see a facade gap). - `GET`/`PUT /api/agents/{name}/label`, owner-only. Read never coerces `label` to the slug — the UI must tell "no label" from "label equals slug". WS `agent_label_changed` broadcast. - Frontend: one resolution helper (`utils/agentName.js`) — no per-site `label || name`, which would show one agent under two names (§1.3.1 FR-3). The header pencil edits the label and shows the slug as secondary text (FR-4); list/tile surfaces render the label with the slug in the tooltip. The store goes through the shared axios client (Invariant #7). - The slug rename is demoted, not removed (FR-5): a separate "Rename the id instead…" affordance with copy stating what it does (restart, re-key, volumes stay under the old id) — owners who need it keep it; it stops being the default gesture. Requirements §1.3.1 written before the code (rule #1). Verified end-to-end on the live stack: label set → slug untouched (container intact, `/api/agents/{slug}` still 200, nothing restarted), blank and null both clear back to the slug. 28 tests. Decision (maintainer): OSS-core; demote the slug rename; label everywhere. Closes trinity-enterprise#181 * fix(agents): carry the label on the detail endpoint + make the WS broadcast live (ent#181) Self-review (/review) caught two consistency gaps I'd introduced. 1. `GET /api/agents/{name}` — the endpoint AgentHeader loads on page open — enriched the agent dict with owner/is_owner/can_share but NOT display_label. So on a fresh load or refresh the header showed the SLUG; the label only appeared after an edit round-tripped through the store. Reproduced live (detail returned display_label=None for a labelled agent), fixed by adding the one read, re-verified live. §1.3.1 FR-3 — the surfaces must agree. 2. The `agent_label_changed` broadcast was dead: it set only `type`, but the frontend WS client switches on `event` (both are the convention, per agent_started et al.), and no handler existed — so a label change on one client never reached others. Added `event` + the `data` shape the other agent_* events use, and a handler that updates the cached agent (the slug never moves, so it's a pure re-render). Endpoint test updated to assert the new broadcast shape.
vybe
pushed a commit
that referenced
this pull request
Jul 17, 2026
… terminal (#1578) (#1680) * docs(requirements): system-emitted task completion events (#1578) Rule #1 — document the new capability before implementing it. Adds §17.2a beside EVT-001: backend emits agent.task.completed/failed at every CAS-won terminal, matching-sub gated, reserved namespace + recursion-break, EVT-001 dispatch (best-effort to a running subscriber), inert by default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(events): event_dispatch_service — extracted delivery + system emit helper (#1578) New services/event_dispatch_service.py: - trigger_subscription / _interpolate_template / _get_internal_token moved verbatim out of routers/event_subscriptions.py so a service (TES) can reuse the EVT-001 dispatch primitive without importing a router (Invariant #1). - emit_task_terminal_event + spawn_task_terminal_event (#1578): the single shared helper the backend fires at every CAS-won terminal — matching-sub gated (empty => no row, no dispatch), recursion-break on triggered_by='event', flat interpolable payload incl. fan_out_id/loop_id, fail-open. - trigger_subscription stamps the reserved-namespace loopback with X-Event-Trigger so the spawned task persists triggered_by='event'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(events): reserved-namespace guards + recursion-break wiring (#1578) - event_subscriptions.py: import the extracted event_dispatch_service (delete the moved defs); reject agent emits into agent.task.* on both emit routes (400); block reserved-namespace self-subscription on create AND update (400); dispatch via event_dispatch_service.trigger_subscription. - chat.py: read X-Event-Trigger on /task; a reserved-namespace dispatch persists triggered_by='event' (threaded via triggered_by_override) so the emit helper suppresses that task's own terminal event — the recursion-break. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(events): emit agent.task.* at every task_execution terminal (#1578) - apply_result SUCCESS won path -> agent.task.completed (sanitized_resp, cost, duration); covers both #1083 paths (inline sync + async result-callback both converge here). - apply_result FAILURE won path -> agent.task.failed (envelope.status carries failed/cancelled; salvage cost). - _write_terminal_and_gate gains agent_name and emits agent.task.failed on won — covering the timeout / budget-exhausted / unexpected-exception (+ inline circuit-open/capacity/ephemeral) terminals that never reach apply_result, the exact wedge case the feature exists for. agent_name threaded from all 4 execute_task call sites. All emits are CAS-won only, fire-and-forget, fail-open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(events): emit agent.task.failed at the #1083 lease-reaper terminals (#1578) _sweep_stale_slots' two per-execution CAS-won sites (async lease expired with no result callback) now emit agent.task.failed — waking a subscribed orchestrator on the exact wedge the feature exists for. Fire-and-forget + fail-open; the bulk watchdog sweeps stay a documented residual (no per-row context). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(events): emit agent.task.* at the pull-sink terminal (#1578) apply_task_result's CAS-won path now emits agent.task.completed/failed. Dark until a pull pilot is enabled (#1081) but wired so a pilot doesn't silently regress report-back. CAS-won only (replay/late report short-circuits or loses the CAS), fire-and-forget + fail-open. Coordinate with obasilakis (his file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(mcp): note reserved agent.task.* namespace in events.ts (#1578) Invariant #13 doc-only: emit_event notes agent.task.* is reserved/backend-emitted (rejected if emitted); subscribe_to_event notes you can subscribe to a source agent's agent.task.completed/failed for an automatic report-back task instead of polling, with the payload shape and the no-self-subscription rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(events): unit coverage for system-emitted task completion events (#1578) 27 tests, all green: - Emit helper: fired (success/failed/cancelled), not-fired-no-sub (AC #1/#5), recursion-break, status-as-.value (#1085 footgun), payload shape (fan_out_id/loop_id), duration/cost row fallback, truncation, fail-open. - Every CAS-won terminal writer spawns on won / not on lost CAS: apply_result inline path, _write_terminal_and_gate (timeout class — the critical pin), the #1083 lease-reaper, and the pull sink. - Both #1083 terminal paths GENUINELY (dossier §5): inline apply_result call AND the async result-callback endpoint driving the REAL apply_result — distinct entry points, not two tests on the same inline call. - Reserved-namespace guards: emit 400, create self-sub 400, PUT self-sub 400, cross-agent subscribe allowed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(feature-flows): task-completion-events flow + index + cross-links (#1578) New feature-flows/task-completion-events.md: end-to-end report-back path, the terminal-writer coverage table, system- vs agent-emitted split, 3-layer loop safety, honest best-effort delivery note, content-trust note, test map. Adds the Recent Updates row + category index entry (feedback: always add the index row), and cross-link pointers from task-execution-service.md and agent-event-subscriptions.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(architecture): Task Completion Events cross-cutting block (#1578) - New 'Task Completion Events (#1578)' subsystem block: system- vs agent-emitted, shared event_dispatch_service helper, CAS-won terminal-writer coverage table, 3-layer loop safety, honest best-effort delivery note. - Pointer from Fire-and-Forget Dispatch (#1083); services-catalog row for event_dispatch_service.py; reserved-namespace note on the events.ts MCP row + the agent_events DDL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(target-arch): reconcile §Async-First — completion events implemented (#1578) Mark the agent.task.completed/failed contract IMPLEMENTED, note the divergence (EVT-001 subscription dispatch, best-effort to a running subscriber; durable pull-queue delivery deferred; not the WS event_bus) and the pull-sink coverage. Add a '├─ Bankable win 3' #1578 row under the #1081 umbrella. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(events): authenticate recursion-break header + sanitize task-event summary (#1578) Two review findings on the #1578 completion-event path, both pre-merge: 1. Spoofable recursion-break. The reserved-namespace loopback was suppressed purely on an `X-Event-Trigger` header, so an external `/task` caller could spoof it to suppress a real agent's completion event. The header is now honored ONLY with a valid backend-internal `X-Internal-Secret` (C-003): `trigger_subscription` stamps both, `execute_parallel_task` verifies via `event_dispatch_service.verify_internal_dispatch_secret` (constant-time) before persisting `triggered_by="event"`. 2. Un-sanitized failure summary. Success/pull terminals sanitize upstream, but the failure error strings (`envelope.error` / `str(exc)`) reached the emit helper raw. `emit_task_terminal_event` now credential-sanitizes at the single chokepoint over a 2x-cap window BEFORE the final truncation, so a secret straddling the `TASK_EVENT_SUMMARY_MAX` boundary can't leak an un-redactable head fragment. Tests: +7 (spoof-guard at the helper AND the router call-site; sanitize incl. the boundary-straddle case) -> 37 pass. Docs: architecture.md delivery note + feature-flow recursion-break/second-terminal notes updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
vybe
pushed a commit
that referenced
this pull request
Jul 18, 2026
…) (#1687) Follow-up 5/5 to the #964 display-name track, building on the ent#181 display_label foundation (#1676): the label + immutable slug, the `utils/agentName.js` resolver (`agentDisplayName`/`hasDistinctLabel`/ `agentNameTooltip`), and the `agent_label_changed` WS handler already shipped. This spreads the label to the surfaces #1676 didn't touch. Decision (AC #1): resolve slug -> label on the FRONTEND off the loaded agents, not by adding a mutable `display_name` to operator/monitoring/executions payloads. Those endpoints are high-volume; duplicating a presentation value across each invites staleness and N sources of truth. Two store getters do it from one place and stay live via the existing WS handler: - `displayNameForSlug(slug)` -> label or slug (prose) - `agentRefForSlug(slug)` -> agent object or bare slug (feeds the tooltip) An unloaded slug falls back to itself, so a cold surface never regresses. Render rule by surface class (#964): - Dense operational tables keep the SLUG primary, label as a hover tooltip: ExecutionsPanel, operator QueueCard/QueueList/QueueItemDetail/ResolvedCard/ NotificationsPanel, MonitoringPanel (alert + health rows), MobileAdmin ops + notif rows, RoleMatrix (100px column, slug-only + tooltip). - Prose / toasts use the label alone: Agents.vue toasts, Settings.vue agent confirm + subscription option, PortalConversation header/switcher/placeholder, PortalFilesPanel headings/toast. Comma-joined lists (GitHub-PAT propagation) stay slug — long labels make them unreadable. - Collaboration graph: network.js carries `display_label` on the agent and system-agent node data + a live `agent_label_changed` handler; AgentNode renders the label but keeps `data.label` (the slug) as the action key for router.push / toggleAutonomy / toggleAgentRunning. AgentAvatar stays on slug. - Router tab titles resolve the label on warm SPA nav, fall back to the slug on a cold direct load (store not fetched yet); next nav self-heals. - Reviewed against the rule: ExecutionDetail breadcrumb + source-agent attribution (label + slug link), PermissionsPanel target rows, ReplayTimeline lane, FoldersPanel consumer/source rows, AgentWorkspace + AgentBrainOrb headers. Terminal header stays slug (machine-facing). Requirements §1.3.1 FR-6 records the decision. No backend change. All 20 touched SFCs compile via @vue/compiler-sfc; the 3 JS files pass node --check. Related to #1643 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vybe
pushed a commit
that referenced
this pull request
Jul 18, 2026
…se#162) (#1686) Let each user store a personal GitHub PAT so a non-admin can create an agent from a repo the shared admin token can't see. Agent creation now resolves per-agent -> owner's per-user (live) -> global, and each agent carries its creator's scope instead of the fleet-wide admin token. Shape A (create-time convenience): - users.github_pat_encrypted (AES-256-GCM, Invariant #12) + four accessors with DatabaseManager delegations; column kept out of the general user dict so it never rides /api/users/me. - resolve_github_pat(agent_name, owner_id) in settings_service returns (pat, tier); both crud.py create sites use it. Persist as the #347 per-agent PAT ONLY for tier in {per_user, fork}, never global, so a global-fallback agent keeps NULL and still receives admin rotation. - get_github_pat_for_agent relocated into settings_service (kills the service->router import, Invariant #1) and stays 2-tier per-agent -> global. The recreate/restart ladder never re-derives the per-user tier, so adding a PAT in Settings can't force-recreate a running agent (the #1560/#1557 recreate-storm class). - Self-service GET/PUT/DELETE /api/users/me/github-pat: token never echoed on read; honest 400 (GitHub rejected) vs 503 (unreachable); create's bare 500 becomes an actionable 400. - UserGitHubPatPanel.vue on the personal MCP Keys settings tab. - users column registered in rotate-credential-key.py; dual-track migration (SQLite + Alembic 0025 on head 0024). Out of scope: public-repo-no-PAT (#123), OAuth/GitHub App (#177), rotation fan-out to existing agents (fast-follow). Tests: real migrated-DB + real facade — resolution ladder, owner isolation, persist-only-user-tier, live column + encrypted round-trip, facade delegation, read-never-echoes; plus the load-bearing recreate-no-storm guard (proven to fail if the ladder goes 3-tier). Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
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.
The system agent was missing required Docker labels (trinity.ssh-port, trinity.cpu, trinity.memory, trinity.created), causing port allocation conflicts when creating new agents.
Without trinity.ssh-port label:
This fix adds all required labels to match the standard agent creation pattern in routers/agents.py, ensuring proper port tracking and conflict prevention.