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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/memory/architecture/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@

*Subscriptions & Skills:*
- `subscriptions.py` - Subscription management (SUB-002). **#2572:** `POST` now also runs the credential-less adoption sweep (`subscription_service.adopt_for_credentialless_agents`) beside the #1089 rollover fan-out, in its own swallow-everything `try/except` — the sweep must never fail a registration whose credential is already stored, which is why the body param is `payload` and the injected one `http_request` (two things called `request` is how a log line inside an `except` raises `AttributeError` into a 500). The manual assign/clear routes now write the `subscription_assign` / `subscription_clear` audit rows this router had never carried (#2421 owns the remaining three actions); id and name only, never a token. #471 usage observability — extended `GET /{id}/usage` (failure counters + one-gate `rate_limited_now` + `headroom` block, `source: anthropic|observed`), `GET /{id}/usage/breakdown` (per-agent, cost-ranked), `POST /{id}/usage/refresh` (click probe), `GET/PUT /settings/headroom-auto-refresh` — all `assert_admin`; ent#433 adds `GET /{id}/headroom/history?window=24h|7d|30d` (bounded `last`-per-bucket series, `bucket_start` + real `fetched_at`, `coverage_pct`; 422 on an unknown window, id-OR-name + 404 parity with `/usage`; read-only, never probes); ent#434 adds `PUT /settings/headroom-alert-threshold` (`0` disables — the `operator_queue_retention_days` idiom — else 50–99 with a named 422; the escalation tier is DERIVED `max(threshold, 90)`, never a second knob, because two settable thresholds are an oscillator and `validate_ops_setting` is per-key so it cannot express the cross-field invariant) and extends the auto-refresh GET with a `weekly_alert` block carrying `active` + an `inactive_reason` of `no_subscriptions` / `threshold_disabled` / `auto_refresh_off` / `redis_unavailable` / `count_unavailable`, so "no alerts" is distinguishable from "not checking" (#2217) — an unreadable subscription list is its OWN reason rather than falling through to `active: true`, which it did while `None == 0` decided the first arm. Rendered by the threshold control in `SubscriptionsPanel.vue`, whose decidable rules live in `utils/headroomAlertSettings.js` (vitest runs `environment: 'node'` with no mount harness, so a rule inside the SFC is untestable — the ent#392 precedent). The key is 422-blocked on the generic `PUT /api/settings/{key}`
- `skills.py` - Skill CRUD and assignment. **#2703:** assignment DELIVERS — `POST`/`PUT` call `skill_service.deliver_assigned` (the start-path injection, all assigned + `force=False`; a subset would rewrite CLAUDE.md's section) and return a `delivery` block (`injected|partial|pending_start|in_progress|not_delivered+reason`) on a 200 the row never rolls back; bounded by `SKILL_DELIVERY_BUDGET_SECONDS` with the work kept alive past it. All four routes fire the thin `agent_skills_changed` WS trigger (`skill_service.broadcast_skills_changed`, identifiers only — #918 rule) — see [skill-injection.md](../feature-flows/skill-injection.md)
- `skills.py` - Skill CRUD and assignment. **#2703:** assignment DELIVERS — `POST`/`PUT` call `skill_service.deliver_assigned` (the start-path injection, all assigned + `force=False`; a subset would rewrite CLAUDE.md's section) and return a `delivery` block (`injected|partial|pending_start|in_progress|not_delivered+reason`) on a 200 the row never rolls back; bounded by `SKILL_DELIVERY_BUDGET_SECONDS` with the work kept alive past it. **ent#672:** `delivery.skills[name].warnings` names a deprecated skill on every outcome (read from the library entry, so a stopped agent is told too); `GET /skills/library` carries `deprecated` / `superseded_by`. All four routes fire the thin `agent_skills_changed` WS trigger (`skill_service.broadcast_skills_changed`, identifiers only — #918 rule) — see [skill-injection.md](../feature-flows/skill-injection.md)
- `settings/` (package, #1028 — eleven domain modules, `provider_keys` carrying the #2715 Resend/Gemini routes split out of `credentials`; `generic.py`'s `/{key}` catch-all is included LAST, Invariant #4) - Platform admin settings (incl. Slack transport connect/disconnect/install). **#2572:** both Anthropic-key clear paths fire the credential-less adoption sweep, each gated on the route's existing `deleted` truthiness — the dedicated `DELETE /api-keys/anthropic` **and** the generic `DELETE /{key}` keyed on the two key aliases, because `db.delete_setting` has no delete-side twin of ent#435's `db.set_setting` sink guard and so reaches the key without ever touching `clear_secret_setting`. The hook sits on the ROUTES, not that leaf, which also serves `github_pat` and the Slack keys. Hosts the Tier-2 usage-sharing family (ent#12/ent#437): `GET /telemetry-sharing` (admin + human-only; `?preview=0` skips the aggregate build), `PUT /telemetry-sharing` (consent; mints/discards the share id), `POST /telemetry-sharing/ask/dismiss` (the once-per-install "don't ask again" marker) — see [Opt-in Instance Telemetry](#opt-in-instance-telemetry-ent437)

*Content & Files:*
Expand Down Expand Up @@ -224,7 +224,7 @@

*Skills & System:*
- `skill_service.py` - Skills library sync + full-directory package injection (ent#183): `git archive`-sourced tars via the existing agent-server restore primitive, tree-SHA versioning, manifest prune, declaration-only dep check — see [skill-injection.md](../feature-flows/skill-injection.md). **ent#236** adds the removal half — `remove_skills` (manifest-driven, takes the same per-agent inject lock) and `reconcile_agent_skills` (start-path diff of the agent's platform-managed skill dirs against the assignment set, so a removal reaches an agent that was stopped when it happened), plus durable sync status in `system_settings` (the in-process `_last_sync` is invisible to the other uvicorn worker). **ent#237** makes it multi-source: `sync_library` iterates every enabled `skill_sources` row under ONE library-wide lock, one failing source never blinds the others (per-source outcomes, aggregate succeeds if any synced), and `commit_changed` — the gate ent#236's fleet re-inject fires on — is computed per source against that source's own durable `last_commit_sha` and OR'd. `services/skill_source_clone.py` owns one checkout's git lifecycle (clone/fetch/reset/checkout, tag-pin refusal on **both** the update and clone paths — the update path compares the tag *peeled* (`^{commit}`), because every `trinity-skills` release tag is annotated and a bare rev-parse of one is the tag object, not the recorded commit (#2550) — non-repo quarantine); `skill_service` orchestrates N of them. **Editing a source has to reach disk**, which took three fixes of one shape (row moved, checkout didn't): (1) `_update_*` fetches **`origin`**, written at clone time, so a repointed `url` kept pulling the OLD repo forever while reporting `success` with a moving commit that fed the fleet re-inject — `sync` now compares `origin` against the configured remote (`canonical_remote`, credential-stripped so a PAT-bearing `origin` isn't a false repoint) and **discards + re-clones** on a genuine mismatch, since `remote set-url` would leave the old repo's tag refs behind and a tag name present in both at different commits then reads as a moved pin forever; an unreadable `origin` counts as a match, because the action gated on it is an `rmtree`. (2) `update_source` clears the sync bookkeeping on a `url`/`ref`/`ref_type` change — without it the documented tag-bump path (`v1` → `v2`) compared v2 against v1's recorded SHA and was refused as `moved_tag`, and on the fresh-clone path that refusal `rmtree`s first, so bumping a tag emptied the source. Non-identity edits (name/enabled/priority) must NOT clear it: a cleared baseline on disable would let a tag moved during the disabled window be adopted silently on re-enable. (3) `discard_source_checkout` reclaims `<source_id>/` + `<source_id>.broken` on delete, with `_reclaim_orphan_checkouts` (full sweeps only, **fail-closed** on the row read, id-shape-scoped so the legacy checkout and operator dirs are out of scope, logs what it reclaims per #1644) as the backstop for the crash window and for installs predating the fix. **ent#332** makes each source's layout root resolvable — `catalog.yaml` `skills_root:` (ent#314 hardened parse, segment-wise validation, realpath containment) → evidence-gated `skills/` probe (dual-layout keeps legacy + flags `layout_conflict`) → `.claude/skills/` fallback; every invalid tier falls through, and `skill_packaging.filter_skill_archive(source_root=…)` rewrites tar arcnames to the canonical agent-side `.claude/skills/` destination so manifests/prune/removal stay destination-canonical with zero migration (requirements §21.1.4)
- `skill_packaging.py` - Pure skill-package primitives (ent#183): hardened frontmatter contract parse, archive member vetting, injection-tar assembly, prune diff; `compute_removal` (ent#236) is `compute_prune` against an empty new manifest, so confinement + cap live in one place
- `skill_packaging.py` - Pure skill-package primitives (ent#183): hardened frontmatter contract parse, archive member vetting, injection-tar assembly, prune diff; `compute_removal` (ent#236) is `compute_prune` against an empty new manifest, so confinement + cap live in one place. **ent#672:** the contract carries the library's lifecycle keys (`deprecated`, `superseded-by` → `superseded_by`, the author's text) and `deprecation_warning` is the one producer of the `deprecated[:<successor>]` code — see [skill-injection.md](../feature-flows/skill-injection.md)
- `skills_sync_service.py` - Scheduled skills-library auto-sync + fleet-wide re-inject (ent#236). Leader-locked (`skills:sync:leader`), both flags default OFF; sweeps only when the library commit actually changed, over **running** non-ghost agents at bounded concurrency with skip-and-report on inject-lock contention. Backend-hosted rather than in the standalone scheduler because the sweep must reach agent containers and the scheduler is platform-network-only
- `skill_sets.py` / `skill_set_service.py` - Skill sets (trinity-enterprise#530): the pure catalog parser + `plan_member_rows` (fail-closed), and resolution (skill precedence, fingerprint-cached), per-agent honest status, assign/unassign/replace. Every inject path (start, manual Sync, fleet re-inject) runs `reconcile_agent` BEFORE reading the names, then prunes. See [database.md](database.md) → agent_skill_sets
- `system_agent_service.py` - System agent lifecycle. **#1816:** `ensure_deployed` is read-only when the container is running (3-state `check_base_image_state` → `base_image_state`, WARNING + an edge-triggered `base-image-stale-` operator alarm on `stale` only, **never** on `unknown`) and delegates to `start_agent_internal` when it is stopped, so the cold boundary adopts a rebuilt base image through the shared lifecycle instead of a bare `container_start`. Creation converges on the recreate path's contract (`TRINITY_AGENT_AUTH_TOKEN`, `trinity.full-capabilities`) so no predicate is permanently false. The generic `recreate_missing_container` rebuild **refuses** `trinity-system` (409, ADOPT-006) — it reconstructs a *regular* agent and would irreversibly downgrade the orchestrator (system-scoped MCP key deactivated for an agent-scoped one); `ensure_deployed`'s create branch is the only supported rebuild
Expand Down
2 changes: 1 addition & 1 deletion docs/memory/architecture/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ FastMCP, Streamable HTTP transport, port 8080. API-key auth via `Authorization:
| `chat.ts` (4) | `chat_with_agent`, `get_chat_history`, `get_agent_logs`, `fan_out` | Chat (enforces sharing rules), history, logs; `fan_out` = parallel dispatch of one message to several agents (FANOUT-001). Both sync modes apply `MCP_CHAT_TIMEOUT_MS` (default 25000) — sequential `/chat` (#914) and parallel `/task` (#2661); on abort the client queries `/api/agents/{name}/executions` and returns `{status:"queued_timeout", execution_id, message}` so callers poll instead of duplicate-queueing. **Attribution must be provable, never newest-wins**: `/task` runs N tasks concurrently, so `triggered_by`/`source_mcp_key_id`/window are identical across one caller's rows — the matcher also keys on the call's own `message`, takes its trigger set per call site (`/chat` must NOT accept `self_task`), derives the window from the timeout (a fixed 30s made raising the documented knob a silent kill-switch), and returns **nothing when >1 candidate survives**. A duplicate under the same `Idempotency-Key` is answered from the 409's `execution_id` instead of an opaque `API error (409)`. The recovery read is separately bounded (`MCP_RECOVERY_TIMEOUT_MS`, no 401-reauth) because it spends what is left of the gateway budget. **`fan_out` is the third route (#2670)**, and the one that exceeds the gateway most reliably — a batch runs longer than any single task in it. Its receipt names a `fan_out_id`, not an `execution_id`: N rows share one id, so a single execution id could only name an arbitrary member. Ambiguity is therefore measured on the BATCH (>1 distinct `fan_out_id` ⇒ no receipt), and status is deliberately NOT filtered — by abort time a batch is normally a mix of finished and running rows, so requiring non-terminal ones would drop the batches furthest along. Poll `get_fan_out_result`, which folds `schedule_executions` rather than the idempotency snapshot (that snapshot is written by `complete()`, i.e. only once the batch is over, so it cannot answer what is happening now). **Chain-depth refusal (#2806):** `client.chat`/`task`/`fanOut` turn a 403 carrying `detail.error == "inter_agent_depth_exceeded"` into a typed `DepthRefusal` (`parseDepthRefusal`) instead of throwing; `runAgentChat` (every branch) and `fan_out` return it as `{status: "inter_agent_depth_exceeded", agent, depth, max_depth, retryable: false, message}`. A 403 without the code still throws |
| `schedules.ts` (8) | `list_agent_schedules`, `create_agent_schedule`, `get_agent_schedule`, `update_agent_schedule`, `delete_agent_schedule`, `toggle_agent_schedule`, `trigger_agent_schedule`, `get_schedule_executions` | Schedule CRUD and execution history |
| `executions.ts` (5) | `list_recent_executions`, `get_execution_result`, `get_fan_out_result`, `get_agent_activity_summary`, `search_executions` | Execution queries, async result polling, activity monitoring (MCP-007). `get_execution_result` returns `compact_metadata` parsed (#2958), so a long turn reads as a one-off auto-compaction rather than a degraded agent. `get_fan_out_result` polls a BATCH — `GET /api/agents/{name}/fan-out/{fan_out_id}`, the only surface that answers while it is still running (#2670). `search_executions` (ent#653) proxies the entitlement-gated `/api/enterprise/execution-search` route (`mode=substring` or `mode=regex`, the latter PostgreSQL-only): a per-tool `canAccess` ALLOW-LIST of `system`/`user` scope (agent-scoped sessions never see it, and the backend refuses them regardless) and an honest `available:false` envelope when the module is absent (404) or unentitled (403) — never "no results" |
| `skills.ts` (11) | `list_skills`, `get_skill`, `get_skills_library_status`, `list_skill_sets`, `assign_skill_to_agent`, `set_agent_skills`, `sync_agent_skills`, `get_agent_skills`, `unassign_skill_set`, `list_runnable_skills`, `run_skill` | Skill management and assignment, plus the deterministic skill runner (ent#139). **Skill sets (ent#530):** `list_skill_sets` reads `GET /api/skills/library/sets` (names, status, members, problems — no agent target); `set:<name>` is accepted wherever a skill name is — `assign_skill_to_agent` routes it to `POST /skill-sets/{set}`, `set_agent_skills` forwards it to the PUT (`set:` entries only add sets), `get_agent_skills` adds `individual`/`via_sets` per skill and the agent's `sets` (a failed set read omits the key, never the skills). The four write tools are `baselined` on the **backend skill-manager fence** (ent#596): an agent key needs the `skills.manage` capability — for its own skills too — or the call is refused with `skill_management_not_permitted` and nothing changes. `get_agent_skills` (a read) stays on ENT629 |
| `skills.ts` (11) | `list_skills`, `get_skill`, `get_skills_library_status`, `list_skill_sets`, `assign_skill_to_agent`, `set_agent_skills`, `sync_agent_skills`, `get_agent_skills`, `unassign_skill_set`, `list_runnable_skills`, `run_skill` | Skill management and assignment, plus the deterministic skill runner (ent#139). **Lifecycle (ent#672):** `list_skills` maps `deprecated` / `superseded_by` by name (a field it does not name stops at REST); the assign tools pass `delivery.skills[name].warnings` through unchanged. **Skill sets (ent#530):** `list_skill_sets` reads `GET /api/skills/library/sets` (names, status, members, problems — no agent target); `set:<name>` is accepted wherever a skill name is — `assign_skill_to_agent` routes it to `POST /skill-sets/{set}`, `set_agent_skills` forwards it to the PUT (`set:` entries only add sets), `get_agent_skills` adds `individual`/`via_sets` per skill and the agent's `sets` (a failed set read omits the key, never the skills). The four write tools are `baselined` on the **backend skill-manager fence** (ent#596): an agent key needs the `skills.manage` capability — for its own skills too — or the call is refused with `skill_management_not_permitted` and nothing changes. `get_agent_skills` (a read) stays on ENT629 |
| `tags.ts` (5) | `list_tags`, `get_agent_tags`, `tag_agent`, `untag_agent`, `set_agent_tags` | Agent tagging |
| `systems.ts` (5) | `deploy_system`, `list_systems`, `restart_system`, `get_system_manifest`, `teardown_system` | System manifest deployment + teardown. `teardown_system` (ent#454) is a **license-blind proxy** over an entitlement-gated route: it degrades rather than throws, keeping 404 (route absent on this build) / 403 (not entitled, or an agent key on a human-only verb) / 503 (membership unverified, retryable) as three distinct operator situations, and returns a `failed` report that arrives as a 500 AS a result. `dry_run` defaults to **`true`** — the opposite of `deploy_system`, because an unwanted preview costs a round trip and an unwanted execute costs a fleet |
| `subscriptions.ts` (6) | `register_subscription`, `list_subscriptions`, `assign_subscription`, `clear_agent_subscription`, `get_agent_auth`, `delete_subscription` | Subscription management |
Expand Down
Loading
Loading