Part of Trinity's requirements set. Index & write-path rule: requirements.md.
- Status: ✅ Complete (Phases 1, 2a, 2b, 3, 4, 5 shipped via #20 / PR #371, 2026-04-17).
- Requirement ID: SEC-001
- Priority: HIGH
- Description: Comprehensive audit logging for all user and agent actions with full actor attribution. Enables investigation, compliance reporting, and accountability.
- Key Features:
- Append-only
audit_logtable with immutability triggers (UPDATE blocked unconditionally; DELETE blocked within 365-day retention) - Full actor attribution (user, agent, MCP client, system)
- MCP API key tracking per tool call (all 71 tools wrapped transparently)
- Hash chain (SHA-256) for tamper evidence with verify endpoint
- Query API with filters, pagination, stats aggregation, and JSON/CSV export
- Distinct from Process Engine audit (
audit_entries) — coexist intentionally
- Append-only
- Phase 1 Delivery:
audit_logtable + indexes + immutability triggers (db/schema.py, migration #31)PlatformAuditOperations(db/audit.py)PlatformAuditServicewith global instance (services/platform_audit_service.py)- Admin query API:
GET /api/audit-log,GET /api/audit-log/stats,GET /api/audit-log/{event_id} - 29 unit tests (schema, query, filters, pagination, immutability, service actor resolution, error handling)
- Phase 2a Delivery (agent lifecycle smoke test):
routers/agents.pyemits audit rows after successful create / start / stop / delete- 5 integration-shape tests asserting the exact field layout produced by the handlers
- Phase 2b Delivery:
auth.py— login_success / login_failed (admin + email)sharing.py— share / unsharecredentials.py— inject / export / import (CRED-002 file-injection ops)settings.py— settings_changeagent_rename.py— rename- Request-ID correlation middleware (
X-Request-IDheader, UUID, passthrough)
- Phase 3 Delivery (MCP tool call audit):
src/mcp-server/src/audit.ts—withAudittransparent wrapper- All 71 tools auto-wrapped at registration time in
server.ts - Fire-and-forget POST to
/api/internal/audit(shared-secret auth viaINTERNAL_API_SECRET) - Captures tool name, auth context (user/agent/system scope), duration, success/failure with error message
- Phase 4 Delivery (hash chain + export):
POST /api/audit-log/hash-chain/enable?enabled=true|false— runtime togglePOST /api/audit-log/verify?start_id=&end_id=— chain integrity checkGET /api/audit-log/export?format=json|csv— compliance export_compute_hashnormalizesdetailsfield across write/read paths for stable SHA-256
- Phase 5 Delivery (action coverage gaps):
execution: chat_started (chat.py), task_triggered (schedules.py), schedule_triggered (internal.py)authorization: permission_grant / permission_revoke / permissions_set (agent_files.py)configuration: autonomy_toggle / resource_limits (agent_config.py)mcp_operation: key_create / key_revoke / key_delete (mcp_keys.py)git_operation: sync / pull / init (git.py)system: startup / shutdown (main.pylifespan), emergency_stop (ops.py)credentials: oauth_complete (slack.pyOAuth callback)
- Event Categories (actions tracked):
AGENT_LIFECYCLE: create, start, stop, delete, rename (recreate — no endpoint)EXECUTION: chat_started, task_triggered, schedule_triggeredAUTHENTICATION: login_success, login_failed (logout / token_refresh — no endpoints in Trinity)AUTHORIZATION: share, unshare, permission_grant, permission_revoke, permissions_setCONFIGURATION: settings_change, resource_limits, autonomy_toggleCREDENTIALS: inject, export, import, oauth_complete (CRED-002 replaced spec's create/delete/reload)MCP_OPERATION: tool_call, key_create, key_revoke, key_deleteGIT_OPERATION: sync, pull, init (commit — folded into sync)SYSTEM: startup, shutdown, emergency_stop
- Architecture:
docs/requirements/AUDIT_TRAIL_ARCHITECTURE.md - Flow:
docs/memory/feature-flows/audit-trail.md - Test plan:
docs/testing/audit-trail-manual-test-plan.md(19 acceptance checks; 18/19 passed live, hash-chain verify bug fixed in-flight and re-verified) - Follow-up (optional): admin UI (no requirement in spec — API export satisfies compliance criterion); forward
schedule_id/schedule_namefrom scheduler to/api/internal/execute-tasksoschedule_triggeredaudit carries that context.
- Status: ⏳ Pending Implementation
- Requirement ID: AUDIT-001
- Priority: HIGH
- Description: Track WHO triggered each execution with full actor attribution. Captures user identity, MCP API key info, and source agent for agent-to-agent calls.
- Key Features:
- Extended
schedule_executionsschema with origin columns - User ID and email captured for manual and MCP triggers
- MCP API key ID and name tracked for external calls
- Source agent name tracked for agent-to-agent collaboration
- UI display of origin info on Execution Detail page
- Filter executions by trigger type (manual/schedule/mcp/agent)
- Extended
- New Database Columns:
source_user_id(INTEGER) - FK to users tablesource_user_email(TEXT) - Denormalized for queriessource_agent_name(TEXT) - Calling agent for agent-to-agentsource_mcp_key_id(TEXT) - MCP API key ID usedsource_mcp_key_name(TEXT) - MCP API key name
- Spec:
docs/requirements/EXECUTION_ORIGIN_TRACKING.md - Implementation Phases:
- Database migration and backend CRUD updates
- MCP server header integration
- Frontend display and filtering
- Status: ✅ Implemented (2026-03-03)
- Requirement ID: SUB-002
- Priority: HIGH
- Replaces: SUB-001 (
.credentials.jsoninjection — removed) - Description: Centralized management of Claude Max/Pro subscription tokens. Register long-lived tokens from
claude setup-token(~1 year lifetime), assign to multiple agents viaCLAUDE_CODE_OAUTH_TOKENenv var injection. - Key Features:
- Subscription registry storing encrypted tokens (AES-256-GCM)
- MCP tools:
register_subscription,list_subscriptions,assign_subscription,get_agent_auth,delete_subscription - REST endpoints:
POST/GET/DELETE /api/subscriptions,PUT/DELETE/GET /api/subscriptions/agents/{name} - Token injected as
CLAUDE_CODE_OAUTH_TOKENenv var on container creation - No file injection — env var persists across restarts automatically
- Auth detection endpoint showing which method each agent uses
- Fleet auth report at
/api/ops/auth-report
- Workflow:
- User runs
claude setup-tokenlocally to generate long-lived token - Registers subscription via MCP:
register_subscription("name", "sk-ant-oat01-...") - Assigns to agents:
assign_subscription("agent-name", "subscription-name") - Agent container is (re)created with
CLAUDE_CODE_OAUTH_TOKENenv var;ANTHROPIC_API_KEYremoved
- User runs
- Database:
subscription_credentialstable,subscription_idFK onagent_ownership - Files:
src/backend/db/subscriptions.py- Database operationssrc/backend/routers/subscriptions.py- REST APIsrc/backend/services/subscription_service.py- Auth mode detectionsrc/mcp-server/src/tools/subscriptions.ts- MCP tools
- Status: ✅ Implemented (2026-03-25)
- GitHub Issue: #74
- Extends: SUB-002
- Description: When a new agent is created, automatically assign the subscription with fewest assigned agents (round-robin). Tie-break: alphabetical by name. Falls back to platform API key if no subscriptions exist or token decryption fails. System agents (
trinity-system) are unaffected (separate creation path). - Key Features:
get_least_used_subscription()DB method (SQL: COUNT + ORDER BY)- Auto-assign logic in
create_agent_internal()— token injected before container creation, DB assignment afterregister_agent_owner() - Graceful fallback: no subs → API key, decrypt fail → API key, exception → API key
- Files:
db/subscriptions.py,database.py,services/agent_service/crud.py
- Status: ✅ Implemented (2026-03-21)
- Requirement ID: SUB-003
- Extends: SUB-002
- Priority: HIGH
- Spec:
docs/requirements/SUB-003-subscription-auto-switch.md - Description: Automatically switches an agent to a different subscription when it encounters 2+ consecutive rate-limit (429) errors. Requires opt-in system setting.
- Preconditions: Setting enabled + 2+ consecutive errors + alternative subscription available
- Key Features:
- System setting
auto_switch_subscriptions(default OFF) with Settings UI toggle - Rate-limit event tracking per (agent, subscription) with 2h window
- Best-alternative selection: prefer fewer assigned agents, skip recently rate-limited
- Activity event logged on auto-switch, notification sent to agent owner
- Hooks into chat proxy 429 handler and background task failure path
- System setting
- Database:
subscription_rate_limit_eventstable - Files:
src/backend/db/subscriptions.py- Rate-limit tracking queriessrc/backend/services/subscription_auto_switch.py- Auto-switch orchestrationsrc/backend/routers/subscriptions.py- Setting endpointssrc/backend/routers/chat.py- 429 interception hookssrc/frontend/src/views/Settings.vue- Toggle UI
- Negative markers on
is_auth_failure(#904, 2026-05-21): substring match onAUTH_INDICATORSnow short-circuits to False when the error message also contains an unambiguous signal-kill / OOM / timeout marker (sigkill,sigterm,sigint,exit code -9,exit code 137,exit code 143,out of memory,oom,memory cgroup,terminated by,killed by). Prevents the SUB-003 trigger from firing on cgroup OOM kills whose detail string happens to contain a word like "token" or "authentication" via downstream wrapping. The same exclusion list lives insrc/scheduler/service.py:_is_auth_failureto keep the two surfaces from drifting (see §10.4.1). - Hot-reload, not recreate (#1089, 2026-06-13): the auto-switch no longer recreates the container —
_perform_auto_switchhot-reloads the new token in place so in-flight turns on the agent survive. See §20.6. - Retry the triggering execution after a successful switch (#792, 2026-06-27): previously, when a switch fired mid-execution the triggering row was marked FAILED. Interactive chat retries client-side (
routers/chat.py) and recurring cron recovers next tick, but one-shot triggers (manual…/schedules/{id}/trigger, webhook, MCPtrigger_agent_schedule) had no recovery.TaskExecutionService.execute_tasknow intercepts a returned 429/auth response pre-raise_for_status(mirroring the #678 reader-race retry); when SUB-003 reports a successful switch it re-issues the turn once with the sameexecution_idand the row lands SUCCESS. Details:- Trigger surface: the full SUB-003 surface via
classify_switch_failure(response)(429 → rate_limit; 503/401/403/402 oris_auth_failurebody → auth), not just status codes — so "any switch-success retries" holds. - Budget: one retry, guarded by a dedicated
subscription_switch_attemptedflag (NOTretry_count, which the #678 retry owns, so the two never suppress each other). A cascade (retry still failing) writes FAILED; theexcepthandler is gated on the same flag so it does not switch a second time. The 2h skip-list prevents re-selecting the exhausted sub. - Settle: the retry is the readiness probe (
_SWITCH_RETRY_DELAY_Sshort pre-delay only) — no circuit-aware/healthpoll (would poison the transport breaker on cold start) and no trust inrestart_result's string status. - Cost/budget: first-attempt cost salvaged into
previous_attempt_cost(#678 R2 rollup); retry timeout capped to the remaining original budget so a post-long-run 429 can't balloon wall-clock/slot time. - Same-
execution_idretry means #1084effect_guarddedups wired outbound sinks; residual double-fire risk for arbitrary MCP tool calls is the same the #678 retry already accepts. - Out of scope / follow-ups: the #1083 fire-and-forget async path (
DISPATCH_ASYNC, default OFF) routes 429s through the result-callback, bypassing this sync path; and a concurrent switch-lock loser (getsNonefromhandle_subscription_failure) does not retry. Both deferred. - Files:
src/backend/services/task_execution_service.py(classify_switch_failure,_extract_agent_error,_salvage_attempt_cost, pre-raise block, except-handler gate); teststests/unit/test_792_subscription_retry.py.
- Trigger surface: the full SUB-003 surface via
- Status: ✅ Implemented (2026-04-01)
- Requirement ID: SUB-004
- Extends: SUB-002
- Priority: MEDIUM
- Description: Track token usage (input, output, cost) per subscription across all agents, enabling admins to see how much each subscription is being consumed. Snapshots subscription_id at execution time so usage history survives SUB-003 auto-switches.
- Key Features:
subscription_idcolumn added totask_executionsandchat_sessionstables (nullable, safe migration)- Admin-only
/api/subscriptions/{name}/usageendpoint with dual-window aggregation (24h + 7d) - Per-agent breakdown of input/output tokens, execution count, and estimated cost
- Snapshot strategy: subscription_id captured at execution time, not looked up retroactively
- Database:
subscription_idcolumns ontask_executions,chat_sessions - Files:
src/backend/db/subscriptions.py- Usage aggregation queriessrc/backend/routers/subscriptions.py- Usage endpointsrc/backend/routers/chat.py- Subscription ID capture at execution timesrc/backend/db/chat.py- Session creation with subscription_idsrc/frontend/src/views/Settings.vue- Usage display (if applicable)
- Status: ✅ Implemented (2026-06-13)
- GitHub Issue: #1089
- Extends: SUB-002 / SUB-003
- Priority: HIGH (
theme-reliability) - Builds on: #799 (per-agent
agent_switch_lock) - Description: Rotating an agent's subscription token used to recreate the container, making "rotate a credential" and "kill every in-flight turn" the same operation (#1037 collateral kills — one 429 on a shared subscription would auto-switch and destroy every parallel execution). Token rotation now goes through a surgical hot-reload of the running container; recreate is reserved for image/template/auth-mode changes. This removes the credential↔execution collision class structurally (TARGET_ARCHITECTURE §Agent Runtime).
- Mechanism: the agent server spawns Claude via
subprocess.Popen(..., env={**os.environ, ...})and authenticates purely from theCLAUDE_CODE_OAUTH_TOKENenv var (no.credentials.jsonwrite); it is a single uvicorn worker. Mutating the agent-server processos.environ["CLAUDE_CODE_OAUTH_TOKEN"]makes the next Claude subprocess use the new token; in-flight subprocesses keep their already-inherited old token and finish. - Rotation paths converted to hot-reload:
- Auto-switch (SUB-003):
_perform_auto_switchhot-reloads instead of_restart_agent(runs inside the #799agent_switch_lock). - Manual reassignment (
PUT /api/subscriptions/agents/{name}): a sub→sub swap hot-reloads under the lock; an auth-mode change (none/api-key → subscription) still recreates soANTHROPIC_API_KEYis dropped and the OAuth token is baked intoConfig.Env. - Key rollover (
POST /api/subscriptionsupsert): re-registering a subscription's token fans a best-effort hot-reload out to every running agent on that subscription (one agent's failure never fails the upsert nor blocks the others).
- Auto-switch (SUB-003):
- Key Features:
- Agent-server endpoint
POST /api/credentials/reload-token({token, remove_api_key}) — mutatesos.environ+ persists the token to the writable-layer override; does not rewrite.env/.mcp.jsonor re-inject Trinity MCP. - Durable override (F2): the token is written to
/var/lib/trinity/oauth-token(0600), deliberately not under/home/developer(the persisted workspace volume).startup.shexports it before launching the agent server, so a plain fleet restart (ops.pyraw stop+start, which bypassesstart_agent_internal) keeps the rotated token. Self-reconciling by Docker semantics: the writable layer survivesstop→startbut is wiped on recreate (fresh layer), so a DB-driven recreate re-bakesConfig.Env(DB token) and the stale override is gone — no marker logic. - Back-compat fallback: running containers on an older base image return 404 for the endpoint → the backend falls back to
_restart_agent(identical to pre-#1089 behavior). Per #1037, recreate stays out of scope; the fallback inherits whatever #1037 lands. An agent only gains the endpoint once recreated onto a rebuilt base image (no automatic fleet-wide adoption).
- Agent-server endpoint
- Backend helpers (
services/subscription_auto_switch.py):_hot_reload_subscription_token(agent_name)(POST + restart fallback on 404/transport/no-token;no_container/not_runningshort-circuits) andreload_subscription_for_all_agents(subscription_id)(key-rollover fan-out under the lock). - Files:
docker/base-image/agent_server/routers/credentials.py-reload-tokenendpoint + writable-layer override writedocker/base-image/agent_server/models.py-TokenReloadRequest/TokenReloadResponsedocker/base-image/Dockerfile-mkdir+chown /var/lib/trinity(Invariant #17 non-root)docker/base-image/startup.sh- export override token before agent-server launchsrc/backend/services/subscription_auto_switch.py- hot-reload helper + fan-out + auto-switch wire-insrc/backend/routers/subscriptions.py- manual sub→sub under lock + key-rollover fan-out
- Known limitations: cross-worker race on the process-local
agent_switch_lock(prod--workers 2) is flagged for #1166/#799 (escalate to RedisSETNX); a bulkdelete_subscriptionstill leaves the deleted token live until next start (pre-existing, out of scope). Both self-heal via the durable override /check_api_key_env_matchesreconciliation.
- Status: ✅ Implemented (2026-07-04)
- GitHub Issue: #186 (Epic #1054 Security Hardening) — UnderDefense pentest 3.3.3 (CVSS 2.0 Low)
- Description: Closed two enumeration oracles built from differential API responses. (1) User (email) enumeration —
POST /api/auth/email/requestreturned a distinct body ("Verification code sent…"+expires_in_seconds), a whitelist-only 429, and a slower whitelisted latency (blocking SMTP send). (2) Agent enumeration — the agent-access dependency family (and routers re-implementing it ad-hoc) returned404 "Agent not found"for a non-existent agent but403 "Access denied"for an existing-but-inaccessible one across 30+ endpoints; someagent_configGETs had no access check at all (404-vs-200 oracle + read-hole); the MCPchat.tslayer additionally disclosed the owner username. - Fix:
- Deps (
dependencies.py): all four helpers return a uniform 404, evaluating existence AND access before branching (equal timing) and running_enforce_connector_scopefirst. See architecture Invariant #8 (self-uniform rule). - Email (
routers/auth.py): identical generic body/status for all branches; over-limit returns the generic 200 (WARN-logged, no 429); email dispatched fire-and-forget (latency parity). See auth requirements §2.1. - Router sweep:
avatar.py(→OwnedAgentByName),nevermined.py(uniform 404 helpers),event_subscriptions.py(source-agent 400/403 → uniform 403),schedules.pywebhook endpoints (→AuthorizedAgent),agent_config.pycapabilities/timeout/public-channel-model/guardrails GETs (→AuthorizedAgentByName, closing the authz hole). - MCP (Invariant #13):
chat.ts checkAgentAccessreturns one uniform reason and no owner username;reports.ts/messages.tsconsumer classifiers treat the dep's 404 as not-authorized;nevermined.tsreportsconfigured:falsefor an inaccessible agent (enumeration-safe).
- Deps (
- Contract note: access-first inline handlers stay uniform 403 (already self-uniform);
DELETE /{agent_name}stays 403 (system-agent semantics). The rule is self-uniform, never 404-then-403 — not "always 404". - Tests:
tests/unit/test_186_enumeration_uniformity.py(real-DB dep uniformity + email body/no-429 + Tier-4 wiring guard); flipped asserts intests/test_access_control.pyand dep-override unit tests.
- Status: ✅ Implemented (#1164)
- GitHub Issue: #1164 (Epic #1054 Security Hardening) — the deferred prevention half of #1158 / PR #1162
- Priority: MEDIUM (theme-security, complexity-low)
- Description: Commit-time secret scanner (
gitleaksMIT CLI) that fails any PR whose changes introduce a credential — closing the #1158 gap where an embeddedre_-prefixed Resend key shipped in the published CLI and was only caught by a later audit. Public repo → a committed secret is world-readable and permanent, so the guard runs on every PR (no path filter, no label gate — #878 lesson). - Key Features:
.github/workflows/secret-scan.yml: thegitleaksbinary (not the org-licensedgitleaks/gitleaks-action, which requires a paidGITLEAKS_LICENSEfor org-owned repos and fails run 1), version + sha256-pinned,permissions: contents: read, scoped to the PR/push commit range viagit merge-base(--log-opts). The--exit-code 2tri-branch distinguishes clean (0) / finding (2 → fail) / scanner-error (other → fail closed)..gitleaks.toml: default ruleset ([extend] useDefault = true, keepingsk-/ghp_/xox*/AKIA+ built-in stopwords) + a customtrinity-resend-api-keyrule (re_-prefixed, entropy floor 3.8) + repo-specific allowlists. The v8.30.1 default set has no Resend rule, so the custom rule is the solere_coverage; the repo-prefixed id can never override a future default rule.--redact=100: findings are masked in the (public) CI log so a match never re-leaks the secret (learnings #1595: CI output is a credential sink).
- Distinct from GUARD-002 (§28.2): GUARD-002 is a runtime hook that scans agent stdout/stderr at execution time; §20.8 is commit-time source scanning. Complementary layers, not a duplicate.
- Enforcement status (be honest): the workflow runs on every PR, but the check is NON-BLOCKING until a repo admin adds
secret-scantodev/mainbranch protection (a repo-settings toggle a code PR cannot perform, tracked as a follow-up). Its unconditional trigger is precisely what makes requiring it safe (never left "Expected — waiting"). Until then, prevention is detection-only — a red scan does not block merge, so #1164 is "detection shipped; enforcement = follow-up", not "cannot reland / solved". - Non-scanned zones:
tests/**(~192 intentionally-fake fixtures),docs/memory/**(the live engineering docs — architecture/requirements/feature-flows — carry API-usage examples: curlAuthorization: Bearercommands, truncated JWTs,KEY=/condition=samples the defaultcurl-auth-header/generic-api-keyrules flag; 8 such FPs were verified during #1164), anddocs/archive|releases|security-reports/**(historical records; the CSO reports hold secret-pattern examples) are blanket path allowlists — gitleaks pre-skips allowlisted paths before per-finding regex, so these are documented non-scanned zones rather than a narrowing that wouldn't hold. A real credential belongs in.env/injection, never a test/doc file; GUARD-002 runtime scanning + human review are the complementary layers..env.exampleis deliberately NOT excluded (a real key pasted there fires). (Scope note for review: onlydocs/memory/**is excluded, not all ofdocs/**— a broaderdocs/**exclusion is deferred to reviewer judgement; other doc trees stay scanned and rely on the inlinegitleaks:allowescape hatch for any example FP.) - Honest limit: the custom rule catches a verbatim
re_-body key. It does NOT catch a re-split / XOR-obfuscated secret — verified: the settled #1158 leak's two base85 halves are undetectable by gitleaks under bothuseDefaultand this config (neither half is are_key nor keyword-adjacent, and generic entropy on base85 is unreliable in a codebase full of legitimate encoded data). Regex+entropy is defense-in-depth against a re-land; credential rotation (done in #1158) is the real defense against the original leak. Because the halves produce no finding,.gitleaksignorecarries no #1158 fingerprint (documented there); it is a non-load-bearing baseline for any future known historical finding, and the range-scoped CI gate does not depend on it.
- Status: ✅ Implemented (v0.8.5 payload).
- Credential-storage summary (cross-reference): the per-user GitHub token is a
stored user credential — a new credential-bearing column
users.github_pat_encrypted, an AES-256-GCM JSON envelope under Invariant #12 (plaintext persistence forbidden; the column is listed among the Invariant #12 tables inarchitecture.md). Set/cleared self-service by its owner only; the token is never echoed on read (status/configuredflag only) — a standing requirement, not just current behavior. Resolution keys on agent ownership, never a calling/sharing user, so a sharee cannot inject their PAT as an agent's git identity. - Full requirement (capability + resolution ladders + persist carve-out):
docs/memory/requirements/github.md§11.10 — this section is the security-surface pointer; the resolution mechanics and the recreate-vs-create ladder distinction live there.
Requirements Doc: OPERATOR_QUEUE_OPERATING_ROOM.md Feature Flow: operating-room.md
- Status: ✅ Implemented (2026-03-07)
- Requirement ID: OPS-001-AGENT
- Description: File-based operator queue (
~/.trinity/operator-queue.json) for agent-to-platform communication. Request types: approval, question, alert. Meta-prompt section teaches agents the protocol. - Files:
config/trinity-meta-prompt/prompt.md(Operator Communication section)
- Status: ✅ Implemented (2026-03-07)
- Requirement ID: OPS-001-SYNC
- Description: Background polling service (5s interval) syncs agent queue files with platform database. Reads new agent requests, writes operator responses back to agent files, handles expiration and acknowledgement.
- Files:
src/backend/services/operator_queue_service.py
- Status: ✅ Implemented (2026-03-07)
- Requirement ID: OPS-001-API
- Description: REST API for queue items — list with filters, get single item, submit response, cancel, stats, agent-specific queries. WebSocket events for real-time updates.
- Files:
src/backend/routers/operator_queue.py,src/backend/db/operator_queue.py - Tests:
tests/test_operator_queue.py(37 tests)
- Status: ✅ Implemented (2026-03-07)
- Requirement ID: OPS-001-UI
- Description: Card-based inbox for processing agent requests. Single-column feed with agent avatars, Open/Resolved tabs, inline response controls with auto-advance. NavBar badge for pending count. WebSocket real-time updates with polling fallback.
- Files: OperatingRoom.vue, QueueCard.vue, ResolvedCard.vue, operatorQueue.js store, NavBar badge
- Remaining: Sound/desktop notifications for critical items
- Status: ⏳ Not Started
- Requirement ID: OPS-001-SKILL
- Description: Marketplace skill teaching agents how to write requests, read responses, escalate, and internalize operator preferences into memory.
- Status: ⏳ Not Started
- Requirement ID: OPS-001-MCP
- Description: MCP tools for programmatic queue access — list items, respond to requests, get stats. Enables orchestrator agents to auto-process queue items.
- Status: ✅ Implemented (2026-07-17, #1632)
- Requirement ID: OPS-001-CAPS
- Priority: HIGH (blocks pull-mode default-ON, #1081 / #1402)
- Description: The operator-queue create path (the agent-authored
~/.trinity/operator-queue.jsonsync ingestion boundary in_sync_agent) previously accepted unbounded agent input with no per-agent ingestion cap. #1402 makes this queue the approval channel for irreversible actions (payments, destructive ops), so a compromised / prompt-injected agent that floods plausible "approve this" items causes operator fatigue → reflexive approval. XSS is already handled (DOMPurify); the exposure is volume + social engineering. The fix must bound a hostile agent, not just a runaway. Two independent bounds plus field hygiene, all at the one agent-authored seam:- Depth cap (primary, DB-measured ⇒ Redis-independent):
db.count_operator_queue_pending_for_agent(agent)is computed once per cycle; new items are admitted only whilepending + admitted < OPERATOR_QUEUE_MAX_PENDING_PER_AGENT(default 25). At the cap, ingestion stops (break, not drip — avoids the C1 per-cycle DoS of re-scanning a growing file) and the surplus is held behind one aggregated summary alert. Bounds per-agent pending rows toMAX_PENDING (+ platform items)regardless of Redis. - Rate cap (burst smoothing, Redis, fail-open): per-agent
rate_limiter.check("operator_queue_create:{agent}", OPERATOR_QUEUE_CREATE_RATE_LIMIT=60, OPERATOR_QUEUE_CREATE_RATE_WINDOW=60)+ fleet-levelcheck("operator_queue_create:_fleet", OPERATOR_QUEUE_FLEET_CREATE_RATE_LIMIT=300, 60)at the real create point only. Denied → item held this cycle,breakthe new-item scan. The fleet cap bounds a colluding / shared-upstream-injected fleet in aggregate (#1402 threat model, #1085 governor precedent). - Field hygiene (
_clamp_ingested_item, total helper, run INSIDE the #1525 create try/except):titletruncated toOPERATOR_QUEUE_TITLE_MAX(300),questiontoOPERATOR_QUEUE_QUESTION_MAX(4000) — truncate-with-marker (losing a real approval is worse than a clamped one);contextserialized >OPERATOR_QUEUE_CONTEXT_MAX_BYTES(8192) → replaced by a{"_truncated":true,"_original_bytes":N,"execution_id":<validated ≤128 or dropped>}marker (so the context cap can't be defeated by a verbatimexecution_id); non-dictcontext→{}(fixes the pre-existingcreate_item.getcrash class);optionsserialized >OPERATOR_QUEUE_OPTIONS_MAX_BYTES(4096) → dropped-with-marker; agent-suppliedcreated_atnormalized to ingest time (defeats future-date sort-pinning;expires_atstill honored);priorityvalidate-only (unknown →medium; legitcriticaluntouched — the depth cap already bounds critical volume). - Reserved-id guard + malformed-id reject: an agent item whose
idstarts with a platform-reserved prefix (queue-flood-,poison-,cb-dormant-,sync-failing-,git-bloat-,skill-not-found-,val_) is rejected so an agent can't pre-create (and thereby self-suppress viaon_conflict_do_nothing) its own flood alarm or the #1402 poison alert; anidlonger thanOPERATOR_QUEUE_ID_MAX(256) or not matching^[A-Za-z0-9._:-]+$is rejected (a PK can't be safely rewritten). - Leader lock (
opqueue:leader, mirror monitoring #1464): only the lease-holding uvicorn worker runs_poll_cycle, so--workers 2no longer double-charges the limiter, double-broadcasts the alert, or double-scans the file. Fail-open to leader on Redis down. - Summary/flood alert: when depth-held or rate-skipped items occur, one
type:"alert"operator-queue item is emitted via a platform direct-DB create (exempt), with an un-guessablequeue-flood-{agent}-{utc_now_iso()}id, priorityhigh, softened wording, and an in-memoryFLOOD_ALERT_COOLDOWN_SECONDS(300) cooldown so it fires once per episode; wrapped so an emit failure never kills the sync. - Generous DB belt (
create_item): rejects (ValueError)title>4 KiB,question>16 KiB, serializedcontext>64 KiB,id>512 — an order of magnitude above the service caps so platform items never trip it, but the "platform bypasses the boundary" invariant stops being solely load-bearing (#1525 two-layer philosophy: validate at the boundary AND at the sink). - Platform exemption made true:
validation_service._notify_operator_on_failurenow creates its notification via a directdb.create_operator_queue_item(...)instead of writing into the agent file (which would flow through_sync_agentand be capped) — this restores exemption-by-construction and fixes the pre-existing latent bug where it.appended to a bare list the sync loop can't parse.
- Depth cap (primary, DB-measured ⇒ Redis-independent):
- Fail-open policy: the rate/fleet limiters fail open to the per-worker in-process window; the DB depth cap is the Redis-independent hard bound, so fail-open never leaves the channel unbounded (a Redis outage is covered by the depth cap, not by a fail-closed defer that would delay legit escalations).
- Env knobs (all env-tunable, generous by design — "cap a hostile/runaway agent, not throttle normal use"):
OPERATOR_QUEUE_CREATE_RATE_LIMIT(60),OPERATOR_QUEUE_CREATE_RATE_WINDOW(60),OPERATOR_QUEUE_FLEET_CREATE_RATE_LIMIT(300),OPERATOR_QUEUE_MAX_PENDING_PER_AGENT(25),OPERATOR_QUEUE_MAX_SCAN_PER_CYCLE(500),OPERATOR_QUEUE_MAX_FILE_BYTES(2 MiB),OPERATOR_QUEUE_TITLE_MAX(300),OPERATOR_QUEUE_QUESTION_MAX(4000),OPERATOR_QUEUE_CONTEXT_MAX_BYTES(8192),OPERATOR_QUEUE_OPTIONS_MAX_BYTES(4096),OPERATOR_QUEUE_ID_MAX(256),OPERATOR_QUEUE_EXECUTION_ID_MAX(128),OPERATOR_QUEUE_FLOOD_ALERT_COOLDOWN_SECONDS(300). - Files:
src/backend/services/operator_queue_service.py,src/backend/db/operator_queue.py,src/backend/services/validation_service.py,src/backend/database.py - Tests:
tests/unit/test_1632_operator_queue_caps.py
- Status: 🚧 In Progress (Phase 1 implemented — #140)
- Requirement ID: GUARD-001
- Priority: HIGH
- Description: Deterministic safety guardrails for autonomous agent execution. Prevents costly mistakes (destructive commands, credential leaks, runaway loops, unauthorized network access) through layered enforcement baked into the base image and agent-server.py — not relying on model compliance alone.
- Design Principle: Trinity controls the base image, the agent server, and the deployment pipeline. Guardrails are injected infrastructure-level, not advisory. Agents cannot opt out.
- Status: ✅ Implemented (#140)
- Requirement ID: GUARD-002
- Priority: HIGH
- Description: Pre-configure Claude Code hooks in the base image (
~/.claude/settings.json) that all agents inherit. Hooks fire deterministically on every tool call — including in--dangerously-skip-permissionsmode. - Key Features:
PreToolUsehooks onBashtool: deny-list of destructive patterns (rm -rf /,rm -rf ~,chmod 777,curl | sh,git push --force, production domain access)PreToolUsehooks onEdit/Writetools: block writes to credential files (.env,.mcp.json,~/.ssh/,~/.aws/)PostToolUsehooks onBash: scan stdout/stderr for leaked credentials (API key patterns:sk-,ghp_,AKIA, bearer tokens)- Hook scripts installed at
/opt/trinity/hooks/in base image - Configurable per-agent overrides via
agent-config.yaml(operator can relax rules for specific agents that need broader access) - All blocked actions logged to Vector pipeline with reason and tool input
- Architecture:
- Base image writes
~/.claude/settings.jsonwith default hooks during build startup.shmerges agent-specific hook overrides from/config/agent-config.yaml- Hook scripts receive JSON on stdin, return
permissionDecision: denyto block - Exit code 2 = block action, exit code 0 = allow
- Base image writes
- Implementation:
/opt/trinity/hooks/bash-guardrail.sh— Deny-list pattern matching on bash commands/opt/trinity/hooks/file-guardrail.sh— Block credential file modifications/opt/trinity/hooks/output-scanner.sh— Post-execution credential leak detection~/.claude/settings.json— Hook registration (baked into Dockerfile)
- Status: 🚧 Partially Implemented —
--max-turns+--disallowedToolsshipped in #140; chat-mode wall-clock timeout tracked in #313 - Requirement ID: GUARD-003
- Priority: HIGH
- Description: Enforce execution limits on every Claude Code invocation via CLI flags in agent-server.py. Prevents runaway cost, infinite loops, and excessive tool access.
- Key Features:
--max-turnson all executions (configurable per agent, default: 50 for chat, 20 for tasks)--allowedToolson task/headless executions (restrict to minimum required tools)--disallowedToolsfor globally banned tools (e.g., blockWebFetchfor agents that shouldn't access the internet)- Execution timeout enforced by agent-server.py (kill process after configurable limit, default: 30 minutes)
- Per-agent configuration via backend API and agent-config.yaml
- Architecture:
claude_code.pyreads guardrail config from agent state/config- CLI flags injected into every
subprocess.Popencommand array - Backend API:
PUT /api/agents/{name}/guardrailsto configure per-agent limits - Defaults set in base image, overridable per-agent by operator
- Configuration Model:
guardrails: max_turns_chat: 50 max_turns_task: 20 execution_timeout_minutes: 30 allowed_tools: null # null = all tools allowed disallowed_tools: [] # tools to remove from context deny_patterns: [] # additional bash deny patterns allow_credential_writes: false
- Status: ⏳ Not Started
- Requirement ID: GUARD-004
- Priority: MEDIUM
- Description: Prevent agents from reading, logging, or exfiltrating their own credentials. Credentials should be usable (via MCP configs, env vars) but not inspectable.
- Key Features:
PreToolUsehook blocksRead/Bash(cat|head|tail|less|more)on.env,.mcp.json,~/.ssh/*- Credential files mounted read-only with restrictive permissions (already 600, enforce via hook)
PostToolUseoutput scanner detects credential values in command output- Environment variable values masked if agent tries to
envorprintenv
- Limitation: Agents need env vars to function (e.g.,
ANTHROPIC_API_KEY). The goal is preventing accidental exposure, not defeating a determined adversary — the Docker isolation boundary is the true security layer.
- Status: ⏳ Not Started
- Requirement ID: GUARD-005
- Priority: MEDIUM
- Description: Visibility into guardrail enforcement across the fleet. Operators need to see what's being blocked, how often, and whether guardrails are causing legitimate work to fail.
- Key Features:
- Guardrail event log: blocked action, reason, agent, timestamp, tool input
- Per-agent guardrail configuration display on Agent Detail page
- Fleet-wide guardrail stats on Operating Room dashboard (blocked/allowed ratio, top blocked patterns)
- Notifications for high-frequency blocks (may indicate misconfigured agent or attack)
- Export guardrail events for compliance reporting
- Architecture:
- Hook scripts write structured JSON to
/logs/guardrails.jsonl - Vector pipeline ingests guardrail logs alongside existing container logs
- Backend API:
GET /api/agents/{name}/guardrail-events,GET /api/ops/guardrail-stats - Frontend: Guardrails tab on Agent Detail, summary widget on Operating Room
- Hook scripts write structured JSON to
- Status: ⏳ Not Started
- Requirement ID: GUARD-006
- Priority: LOW (Docker network isolation already provides baseline)
- Description: Fine-grained control over which external domains/services each agent can reach. Currently agents share the Docker bridge network and can reach any internet host.
- Key Features:
- Per-agent network policy: allowlist of domains the agent can access
- Default policy: allow all (backward compatible), restrictable per-agent
- DNS-level filtering via container-specific resolv.conf or iptables rules
- Log all outbound connections for audit trail
- Implementation Options:
- Docker network policies with iptables rules injected on container creation
- Sidecar proxy (envoy/nginx) per agent with domain allowlist
- Claude Code sandbox mode (
sandbox.network.allowedDomainsin settings.json)
- Note: This is lower priority because Docker isolation already prevents cross-agent access, and most Trinity agents operate within controlled environments. Prioritize when deploying agents that handle sensitive data or untrusted inputs.
- Phase 1 — Foundation (GUARD-002 + GUARD-003): Hook scripts in base image + CLI budget controls in claude_code.py. Immediate protection against the most common failure modes.
- Phase 2 — Credential Protection (GUARD-004): Prevent agents from inspecting their own credentials. Requires hook scripts + output scanning.
- Phase 3 — Observability (GUARD-005): Dashboard and logging for guardrail events. Requires Vector pipeline integration + frontend work.
- Phase 4 — Network Controls (GUARD-006): Per-agent network policies. Requires Docker network configuration changes.