feat(agent): runtime data_paths with portable export/import (#1169)#1294
Conversation
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>
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>
…ndex 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>
|
Resolve by running |
Resolves conflicts after #1297 + #1293 landed on dev: - feature-flows.md: kept all changelog rows (#1169 + #946 + #1088), date-sorted. - container-capabilities.md: took dev's canonical #1231 tmpfs documentation (this branch's tmpfs edits were redundant re-wording of the same feature; the data-volume additions auto-merged cleanly). - cso-diff-2026-06-20.md (add/add): kept dev's audit at the canonical path and preserved this branch's audit as cso-diff-2026-06-20-1169.md (date-named CSO reports from two same-day PRs collided). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vybe
left a comment
There was a problem hiding this comment.
validate-pr: PASS (per your 'merge on validate-pr' decision) → dev. P1 feature (#1169, agent runtime data volumes, +2325). Security scan: no secrets/keys/emails. New router agent_data.py lives in routers/ (COPYed wholesale — no Dockerfile gap); endpoints documented in architecture.md + requirements.md + feature-flows. Packaging:
Code (main.py, chat.py, crud.py, git_service.py, all compose/.env) auto-merged cleanly. Resolved doc conflicts after #1297/#1293/#1294 landed on dev: - feature-flows.md: kept all changelog rows (#1159 + #946 + #1088 + #1169), date-sorted. - container-capabilities.md: took dev's canonical #1231 tmpfs docs (this branch's tmpfs edits were redundant re-wording; auth content auto-merged / lives in the new agent-server-authentication.md flow). - cso-diff-2026-06-20.md (add/add): kept dev's audit at the canonical path, preserved this branch's as cso-diff-2026-06-20-1159.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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…
Summary
Implements #1169 (PR1): declared agent runtime data under
data/on the already-durable home volume — no separate volume, no platform schema change. Snapshots are filesystem artifacts; audit ridesaudit_log./home/developeris already a persistent named Docker volume (agent-{name}-workspace) that survives recreate/upgrade/template-repull/sub-switch, so data under/home/developer/datais already durable. This PR adds only the declaration + export/import surface.What's included
template.yaml data_paths:(globs underdata/) surfaced bytemplate_service(github + local builders) and materialized at creation bycrud.py→git_service.materialize_data_paths(): writes~/.trinity/data-paths.yaml(quoted heredoc, glob-safe) and appendsdata/+ each declared path to the agent's own.gitignore(idempotentgrep -qxF). Opt-in — an empty list is a complete no-op.data_pathsnow share one extractedmaterialize_trinity_yaml_list/_read_trinity_yaml_list(heredoc delimiter parameterized so persistent_state keepsPSTATE_EOF).routers/agent_data.pyPOST /api/agents/{name}/data/export(owner/admin): streamscontainer_get_archive("/home/developer/data")→ temp under/data/agent-data-tmp→StreamingResponse;AGENT_DATA_EXPORT_MAX_BYTES(5 GiB) → 413; self-describingmanifest.jsonin the tar; missingdata/→ manifest-only tar (not 500);?format=base64for the MCP surface up toAGENT_DATA_INLINE_MAX_BYTES(10 MiB).POST /api/agents/{name}/data/import(owner/admin): proxies to the agent-serverrestore_from_tarprimitive (data/**allowlist + absolute/..traversal rejection);Idempotency-Key(Invariant Unified Executions Dashboard (EXEC-022) #18). Both endpoints serialized per agent by a cross-worker Redis op lock (agent:data_op:{name}, fail-open, 409 on contention).export_agent_data/import_agent_datatools carry the base64 tar (Invariant feat: SMARTS trading pipeline with Telegram notifications and Miro visualization #13): "move an agent" = template URL +.credentials.enc+ data tar.DP-001..DP-005added toagent-validation-spec.md.agent-data-volumesflow, agent guide; CSO diff audit report (0 critical/high).Testing
data/**allowlist, gitignore append, template surface. Green locally (full sweep clean aside from pre-existing unrelated flakes).verify-local(real prod-image boot) was not run — blocked locally by a running sibling stack on the pinned agent/platform/16s. Worth a fullverify-localpass before release to main.Out of scope (deferred)
~/.trinity/pre-snapshotSQLite-quiesce hook + retention + rename/purge snapshot-dir cascade.Closes #1169
🤖 Generated with Claude Code