Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ca463d9
fix(capacity): make slot admission atomic across workers
alex-furman Oct 7, 2026
8e6e82c
test: link atomic admission regression to issue 3305
alex-furman Oct 7, 2026
fb372f2
fix(operator-queue): one malformed entry no longer stops answer deliv…
trinity-ability Oct 7, 2026
34beb14
fix(loops): repair a run claimed but never closed after a mid-advance…
trinity-ability Oct 7, 2026
948dc16
fix(operator-queue): acknowledging a platform alert ends it (#2372)
obasilakis Oct 7, 2026
a1884a7
fix(skill-gate): close the #3208 validation gaps before the gate map …
webmixgamer Oct 7, 2026
d1b11b4
feat(skill-gates): the per-agent skill gate map (trinity-enterprise#753)
webmixgamer Oct 7, 2026
269587c
fix(skill-gates): partial writes merge from a mapping, not a sentinel…
webmixgamer Oct 7, 2026
27ac40c
docs(skill-gates): architecture catalog rows for the gated-skill chec…
webmixgamer Oct 7, 2026
737d4db
fix(skill-gates): the PUT response says whether the approver is reach…
webmixgamer Oct 7, 2026
b18848b
fix(skill-gate): give each non-BMP invisible range its own class (Cod…
webmixgamer Oct 7, 2026
b5ad0e8
fix(skill-gate): review fixes — the card shows exactly what runs, a l…
webmixgamer Oct 7, 2026
7d7b350
Merge origin/dev (#3236) into feature/753-skill-gate-map
webmixgamer Oct 7, 2026
15a7660
Merge remote-tracking branch 'origin/dev' into pr-3306
trinity-ability Oct 7, 2026
1a2b4e8
train: #3333
trinity-ability Oct 7, 2026
cc26d29
train: #3334
trinity-ability Oct 7, 2026
1ce5642
train: #3306
trinity-ability Oct 7, 2026
9314af1
train: #3338
trinity-ability Oct 7, 2026
079e523
train: #3345
trinity-ability Oct 7, 2026
dc40770
train: #3347
trinity-ability Oct 7, 2026
f55aff6
train: re-parent #3347's Alembic revision onto #3338 (0094_agent_skil…
trinity-ability Oct 7, 2026
18eca9b
Revert "train: #3306"
trinity-ability Oct 7, 2026
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
2 changes: 2 additions & 0 deletions docs/memory/architecture/api-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ WebSocket events: `operator_queue_new`, `operator_queue_responded` / `operator_q
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/skill-gate/check` | Asked by the agent image's PreToolUse hook (`docker/base-image/hooks/skill-gate.py`) before Claude Code loads a skill into a run (`routers/skill_gate.py` → `skill_gate_service.check_invocation`; `# mcp: none`). The agent comes from the KEY, never the body (`get_self_agent`: agent key → itself, system key → `trinity-system`, else uniform 403 `agent_identity_required`, then the access 404). Body `SkillGateCheckRequest` (`extra="forbid"`: `via` `skill_tool`/`subagent_preload`, `invoked`, `names` ≤ 64 × ≤ 256, `resolved`, `subagent`, `execution_id` ≤ 128, `marker`) → **200** `{allowed, gated, message}` for every verdict: not gated → allowed; gated → allowed only when the run is this agent's live run and a gate record (`dispatching` / `dispatched` / `self_approved`) clears every gated skill it loads; refusals carry the model-facing copy and are audited `skill_gate_refused` (throttled). **503** `gate_unavailable` when the gate map cannot be read (the hook then decides by its marker); **422** on a malformed body. A reported `marker` that disagrees with the map schedules a rate-limited marker re-sync |
| GET | `/api/agents/{agent_name}/skill-gates` | The agent's skill gate map (trinity-enterprise#753; `routers/skill_gate.py` `agent_router` → `skill_gate_map_service.list_gates`): `{gates:[{skill_name, approver, deadline_hours, origin, set_by, set_by_agent, set_at, approver_reachable}], cleared_defaults, approver_kinds, default_deadline_hours}`. `get_skill_gate_readable_agent_by_name`: a person or the system key — anything they can access (uniform 404); an agent key — its own gates, or holding `skills.manage` an agent its owner owns; connector / other / no scope → 403 `skill_gates_not_readable`. `set_by` is null for a non-person principal |
| PUT/DELETE | `/api/agents/{agent_name}/skill-gates/{skill_name}` | Set or change / clear a gate. `require_person_or_capability("skills.manage", self_person_only=True)` then `get_owned_agent_by_name`: a person who owns the agent or is an admin, or a holder on an agent its owner owns — never itself (403 `person_required`); the system key is refused. PUT body `SkillGateSetRequest` (`approver`, `deadline_hours`; only sent fields apply) → `{gate, changed, warnings}` (`approver_unassigned`, `marker_not_written`); named 422s `invalid_skill_name` / `invalid_approver` / `approver_unavailable` / `invalid_deadline`, 409 `ephemeral_agent`. DELETE is idempotent → `{changed, cleared: deleted \| tombstoned \| null}`. Audited |

### Respond → Resume Dispatch (ent#329)

Expand Down
6 changes: 4 additions & 2 deletions docs/memory/architecture/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,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. **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)
- `skill_gate.py` - The in-container skill-gate hook's check, `POST /api/skill-gate/check` (trinity-enterprise#752): the agent from its key (`get_self_agent`), every verdict a 200 — see [skill-gate.md](../feature-flows/skill-gate.md)
- `skill_gate.py` - The in-container skill-gate hook's check, `POST /api/skill-gate/check` (trinity-enterprise#752): the agent from its key (`get_self_agent`), every verdict a 200; and the per-agent gate map, `/api/agents/{agent_name}/skill-gates` (`agent_router`, trinity-enterprise#753) — see [skill-gate.md](../feature-flows/skill-gate.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 @@ -179,7 +179,7 @@
- `monitoring_alerts.py` - Alert threshold configuration
- `heartbeat_service.py` - Agent push-heartbeat liveness layer — see [Heartbeat Liveness](reliability.md#heartbeat-liveness-reliability-004-307)
- `platform_alerts.py` - The platform-alert seam (#3246, OPS-001-PLATFORM-ALERTS, requirements §26.14). Stdlib-only leaf (DB imported lazily): the `Kind` registry (prefix, lifetime class, clear hook, `material_keys`), `observe(agent, kind, key, …)` → at most one pending row per subject `<kind>:<key>` updated in place (`last_seen_at`, `context.seen_count`), `clear` / `reconcile` → ended `cancelled` by the **platform** (`condition_cleared`), lifetime (`OPERATOR_PLATFORM_ALERT_LIFETIME_DAYS`, 14) and snooze (`OPERATOR_PLATFORM_ALERT_SNOOZE_DAYS`, 7) readers, and the pure `plan_sweep` the upgrade migration runs. Never raises into an emitter. Only four emitter families use it in PR 1 (headroom, skills adoption, system agent, circuit breaker); the rest are ratcheted in `PREFIXES_NOT_YET_ON_SEAM` until PR 2.
- `operator_queue_service.py` - Operating Room sync with agent containers (OPS-001); the agent-authored ingestion boundary enforces the per-agent depth/rate/size caps + reserved-id guard + leader lock (#1632)
- `operator_queue_service.py` - Operating Room sync with agent containers (OPS-001); the agent-authored ingestion boundary enforces the per-agent depth/rate/size caps + reserved-id guard + leader lock (#1632); platform-minted alerts (`is_platform_minted`) have no agent audience — never written to the agent file, and an operator answer lands them `acknowledged` (#2372)
- `operator_queue_choices.py` - The one rule for what an approval's `response` may be (#2376, #3242): an offered option (exact match, else `ResponseNotOfferedError` with the list) or the reserved `SOMETHING_ELSE = "(something else)"` with an instruction in `response_text` (`InstructionRequiredError` / `ReservedValueError` / `NotOffMenuError`, all `ReservedAnswerError`); import-free leaf the ask sink calls before any write; also holds `OPTIONS_DROPPED_MARKER`. Mirrored in `utils/operatorQueue.js` + MCP `types.ts` (parity-tested)
- `sync_health_service.py` - Git sync health polling — see [Git Sync Health](agent-lifecycle.md#git-sync-health-389390)
- `sync_freeze_policy.py` - The one sync-health rule (state / reason / recommendation / freeze), stdlib leaf, vendored byte-identically to `src/scheduler/` (ent#706) — see [Git Sync Health](agent-lifecycle.md#git-sync-health-389390)
Expand Down Expand Up @@ -233,6 +233,8 @@
*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. **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)
- `skill_gate_service.py` - The gated-skill check (trinity-enterprise#751): `enforce` at every dispatch road (raises `SkillApprovalRequired` / `SkillGateRefused`, `skill_gate_errors.py`), exactly-once approved dispatch from `skill_gate_requests`, the in-container hook's `check_invocation` (#752) and the fail-closed marker sync; `list_skill_gates` is the one read of the gate map — see [skill-gate.md](../feature-flows/skill-gate.md)
- `skill_gate_map_service.py` - The per-agent gate map's only writer (trinity-enterprise#753): set / clear / the state-driven library-default reconcile / a person's unassign, each under the per-agent marker lock; called by `routers/skill_gate.py`, `routers/skills.py::_sync_gates`, the start path and the library sweep
- `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
22 changes: 22 additions & 0 deletions docs/memory/architecture/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,28 @@ Both tracks: SQLite `agent_capability_grants`, Alembic `0072_agent_capability_gr
`AgentRef(..., CASCADE)` — rename re-keys, delete removes. Every read joins `agent_ownership`
and filters `deleted_at`, so a soft-deleted agent holds nothing and recovery restores its grant.

**skill_gate_requests** (trinity-enterprise#751) — a gated-skill request frozen while its approval ask is open (`db/skill_gate_requests.py`); the ask records the decision, this row the effect: `pending → dispatching` is a CAS and `dispatched_execution_id` is UNIQUE, so an approval runs exactly once; a `self_approved` row (#752) is the clearance the in-container hook honours. Both tracks (SQLite `skill_gate_requests_table`, Alembic `0088`); `agent_name` and `source_agent` are CASCADE AgentRefs; machine keys never read these rows (#715).

**agent_skill_gates** (trinity-enterprise#753) — the per-agent skill gate map the gated-skill
check reads (`skill_gate_service.list_skill_gates`; writes only through
`services/skill_gate_map_service.py`):
```sql
CREATE TABLE agent_skill_gates (
agent_name TEXT NOT NULL,
skill_name TEXT NOT NULL, -- lowercased; every matcher casefolds
approver TEXT NOT NULL, -- primary | approver
deadline_hours INTEGER, -- 1..168; NULL → the 24h default
origin TEXT NOT NULL, -- set | library_default | cleared (tombstone, gates nothing)
set_by TEXT NOT NULL,
set_by_agent TEXT, -- R29 provenance; not an AgentRef
set_at TEXT NOT NULL,
PRIMARY KEY (agent_name, skill_name)
);
```
Both tracks: SQLite `agent_skill_gates`, Alembic `0094_agent_skill_gates` (← `0093_platform_alert_responded_heal`).
`AgentRef(..., CASCADE)`. Writes run under `lock_agent_rows` and land only while the agent has a
live ownership row; the PK is the ON CONFLICT target, declared in `tables.py` as well.

**Tag pinning is the supply-chain control (AC#5).** Skills carry executable `scripts/`
that the ent#139 runner executes and ent#236 re-injects fleet-wide unattended, so the
community source — which takes public PRs — pins to a **tag we bump**, never a branch
Expand Down
Loading
Loading