diff --git a/docs/memory/architecture/backend.md b/docs/memory/architecture/backend.md index 4ca39b997..a11d1d247 100644 --- a/docs/memory/architecture/backend.md +++ b/docs/memory/architecture/backend.md @@ -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:* @@ -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 `/` + `.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[:]` 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 diff --git a/docs/memory/architecture/mcp-server.md b/docs/memory/architecture/mcp-server.md index 6f45f2dce..3e14d6466 100644 --- a/docs/memory/architecture/mcp-server.md +++ b/docs/memory/architecture/mcp-server.md @@ -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:` 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:` 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 | diff --git a/docs/memory/feature-flows/library-page.md b/docs/memory/feature-flows/library-page.md index b47f99b6a..de761f156 100644 --- a/docs/memory/feature-flows/library-page.md +++ b/docs/memory/feature-flows/library-page.md @@ -101,6 +101,8 @@ The assign/unassign **writes** on this page were scoped out to ent#386; delivery **Skill cards** render the ent#183 contract fields (`SkillInfo`): name, description, automation/invocability/package chips via the shared seam (below), `version` short-SHA, `requires` line, and an "Assign via an agent's Skills tab →" link. **Interpolation only** — no `v-html`, no `:href` bound to any library-derived string (skills come from a synced git repo — semi-trusted content). Dormant forward-compat slots render a `source_name` badge and a `shadowed_by` indicator when PR #1901's fields appear. +**Deprecated skills (ent#672).** A card whose `SkillInfo.deprecated` is true stays in the grid, in place, with a warning `deprecated` `BaseBadge` beside the name and one "Superseded by …" line under the description (`contract.js::supersededLine` — the author's text, interpolated, clamped to two lines with the whole of it on hover; absent when no successor was declared). Assigning one from the card succeeds, and `AssignedAgents.vue` adds a warning-toned line under the delivery note (`utils/skillDelivery.js::deprecationText`, read off `delivery.skills[name].warnings`) — on every outcome, a stopped agent included. No dimming, no separate section, no confirm. Once the library deletes the directory, the skill moves to the orphaned-assignments list above. + ### stores/skillsLibrary.js (`src/frontend/src/stores/skillsLibrary.js`) Fleet-scoped Pinia store, **deliberately separate** from the agent-scoped `stores/skills.js`: `App.vue` wraps AgentDetail in `KeepAlive`, so `SkillsPanel`'s `onUnmounted → clear()` never fires on nav-away (deactivated ≠ unmounted). Shared refs would let the Library page's writes — including a failed fetch's error — render inside the **cached** per-agent Skills tab (an agent suddenly "has no skills"). This module imports NOTHING from `stores/skills.js`; the ~40 duplicated fetch lines are deliberate and recorded. @@ -113,7 +115,7 @@ Fleet-scoped Pinia store, **deliberately separate** from the agent-scoped `store ### Shared contract-chips seam (`src/frontend/src/components/skills/`) Extracted from `SkillsPanel.vue` (ent#263) so the per-agent Skills tab and the Library browse render the ent#183 package facts from **one** seam and can't drift: - `SkillContractChips.vue` — automation chip, "not user-invocable" chip, `file_count`/`size_bytes`, opt-in `showVersion` short-SHA -- `contract.js` — `formatBytes()`, `deps()` (declared binaries/packages/env as one display line), `stripUserinfo()` (credential scrub for rendered library URLs — adversarially tested against non-parseable shapes; used by both this section's Primary-source line and SkillsPanel's empty-state URL) +- `contract.js` — `formatBytes()`, `deps()` (declared binaries/packages/env as one display line), `supersededLine()` + `DEPRECATED_TITLE` (ent#672 — the successor line and the badge's hover text, shared by the card and both Skills-tab lists), `stripUserinfo()` (credential scrub for rendered library URLs — adversarially tested against non-parseable shapes; used by both this section's Primary-source line and SkillsPanel's empty-state URL) `SkillsPanel.vue` now consumes the seam (its local `SkillMeta`/`formatBytes`/`deps` removed); `stores/skills.js` untouched. @@ -180,6 +182,7 @@ CI runs admin-authenticated (`e2e/.auth/admin.json`), so the non-admin empty-sta | Date | Changes | |------|---------| +| 2026-09-30 | **ent#672 — deprecated skills are marked**: `deprecated` badge + "Superseded by …" line on the card (shared `contract.js::supersededLine`), and a warning line under the assign control's delivery note. The card stays in the grid and stays assignable. | | 2026-08-12 | **ent#384 — tabs + per-skill assignment read**: the three stacked sections become an `OverflowTabs` strip (`?tab=` addressed, legacy `#`-anchors migrated then cleared, creator-gated Systems tab with both arms of a late-role watch, lazy-mount-once panels), **deliberately reversing** ent#263's stacked-sections decision for the whole page. Each skill block lists the agents holding it via the new access-scoped `GET /api/skills/assignments` — the "fleet assignment read" this doc named as a follow-up at ent#263 ship — plus a bounded orphaned-assignments list for skills revoked from the library. Assignment stays a write on the per-agent Skills tab; the Library writes were split to ent#386. **OSS-core by decision.** | | 2026-08-04 | **ent#14 — remote template registry**: the `GET /api/templates` row now names the three-tier ladder (admin list → registry → bundled defaults) instead of claiming the GitHub half is admin-list-or-`config.py`. **Zero page change** — registry entries are `source: "github"` and land in the existing grid, which is what keeps this page (and MCP `list_templates`) untouched. | | 2026-08-01 | **#1931 — catalog honesty**: the 11 `dd-*` VC-demo templates go `hidden: true` (visible catalog 14 → 3: `sage`/`scout`/`scribe`), `DEFAULT_GITHUB_TEMPLATE_REPOS` emptied (6 → 0, so `GET /api/templates` makes no outbound GitHub calls on a cold cache), new marketplace-first GitHub-zero placeholder card + corrected page-level hint copy, and the demo fleet stays deployable as a set via the promoted `config/manifests/vc-due-diligence.yaml`. | diff --git a/docs/memory/feature-flows/skill-assignment.md b/docs/memory/feature-flows/skill-assignment.md index baef09a42..7fe2f558e 100644 --- a/docs/memory/feature-flows/skill-assignment.md +++ b/docs/memory/feature-flows/skill-assignment.md @@ -6,6 +6,7 @@ | Date | Changes | |------|---------| +| 2026-09-30 | **ent#672** — a deprecated library skill is marked and its assignment says so. `SkillsPanel.vue` shows a `deprecated` badge + "Superseded by …" line on the assigned rows (an agent already holding one shows it on load) and on the picker rows (before the tick); `POST` / `PUT` still succeed and their `delivery.skills[name].warnings` carries `deprecated[:]`, rendered as a warning line under the save note; after a manual Sync the per-skill warning list says it in words. See "Deprecated skills" below and [skill-injection.md](skill-injection.md). | | 2026-09-24 | **ent#530** — skill **sets**. `agent_skill_sets` + `agent_skills.individual`; `POST`/`DELETE /api/agents/{name}/skill-sets/{set}` (ent#596 fence), `GET …/skill-sets` (honest status), `GET /api/skills/library/sets`. The PUT adds `set:` entries / replaces with `sets` (absent = untouched) and resolves held sets inside its transaction so set-derived rows survive a replace; a single unassign of a set-named skill answers `retained_via_sets`. The Skills tab renders `AgentSkillSets.vue`, badges `via `, locks set-only members, and its draft is the INDIVIDUAL list. See *Skill sets* below. | | 2026-09-11 | **#2703** — assignment now DELIVERS. `POST` (both branches) and `PUT` (added names) call `skill_service.deliver_assigned` and return a `delivery` block beside the existing keys; `PUT` is symmetric (added injected, dropped removed). Every listing change — assign, unassign, replace, manual Sync, background completion, fleet sweep — fires the thin `agent_skills_changed` WS trigger, and the Skills tab's Save note / the Library control say what happened instead of "Saved. Sync now, or…". Full flow in [skill-injection.md](skill-injection.md) → *Delivery on assign*. | | 2026-08-12 | **ent#384** — the Library page gained a fleet-wide **read** of who holds each skill (`GET /api/skills/assignments`, access-scoped + human-only; see [library-page.md](library-page.md)). Assignment itself is unchanged and still lives here: this per-agent tab and the REST/MCP surfaces remain the only WRITERS (ent#182 — one skill model). The Library's assign/unassign controls were split to ent#386, so if you are adding them, reuse `POST`/`DELETE /api/agents/{name}/skills/{skill}` rather than adding a skill-keyed writer — a second write path is a second place for the owner gate to drift. Note the two surfaces have DIFFERENT scopes: the ent#384 read is owned ∪ shared, while these writes are owner-or-admin, so a shared agent can appear as a holder that the same caller may not modify. | @@ -665,6 +666,25 @@ User Action Frontend Backend API --- +## Deprecated skills (ent#672) + +``` +library SKILL.md frontmatter (deprecated, superseded-by) + → skill_packaging.extract_contract → _parse_skill_info → GET /api/skills/library (SkillInfo) + → stores/skills.js library / assignedSkills (the library entries joined to the rows) + → SkillsPanel.vue: badge + supersededLine() on the assigned row AND the picker row + +Save (PUT) / assign (POST) — unchanged: the row is written and delivered + → skill_service.deliver_assigned: report.skills[name].warnings = ["deprecated[:]"] + → SkillsPanel.onSave: deprecationNote = deprecationText(store.lastDelivery) +``` + +- **Nothing is refused or hidden.** Assigning a deprecated skill is allowed; the marker is on the picker before the tick and the note is on the save after it. No confirm dialog, no 409. +- **An agent that already holds one** shows the badge on its assigned row because the row is the library entry joined to the assignment — nothing about the assignment, the package or the agent's CLAUDE.md changes. +- **The note belongs to its save**: it is a local ref written in `onSave` and rendered only beside that save's note, never a computed over `store.lastDelivery` (which outlives the note and is also written by a set assign). +- **After a manual Sync** the injection result carries `deprecated[:]`, and the row's warning list leaves it out (`runWarnings`): that list says what went wrong with the delivery, and the badge already says the skill is retired. +- **Skill sets** are not marked: a deprecated member gets the delivery warning on the wire, but the set notes do not render it. + ## Skill sets (ent#530) ``` diff --git a/docs/memory/feature-flows/skill-injection.md b/docs/memory/feature-flows/skill-injection.md index 382baf636..6e38cb6b2 100644 --- a/docs/memory/feature-flows/skill-injection.md +++ b/docs/memory/feature-flows/skill-injection.md @@ -128,6 +128,17 @@ rows (`stores/skills.js::conflictNames`) so the badge, the explanation and the i "Unassign library skill" action show on a fresh load, and `inject()` re-reads the rows so a resolved conflict clears without a reload. +**A deprecated skill (ent#672)** is a second, separate statement. `deliver_assigned` stamps +`skills[name].warnings = ["deprecated" | "deprecated:"]` on the report for a +deprecated name — on every outcome, because it reads the library entry +(`_deprecation_notes`) rather than injection results, and a stopped agent never runs an +injection. The key is absent for a live skill and carries lifecycle codes only. +`utils/skillDelivery.js::deprecationText(report)` turns it into "`` is deprecated — +superseded by ``." and both assign surfaces render it as its own warning-toned +line (`AssignedAgents.vue` under the delivery note; `SkillsPanel.vue` under the save row, +shown only beside the save note it belongs to). `deliveryText` is untouched: a clean +delivery stays green. + ## Removal (ent#236) Injection's inverse, added because an unassigned skill previously stayed on the @@ -250,7 +261,11 @@ name-conflict refusal: `success: false`, `error: name_conflict: …`, not counte `restore_skipped:*`, `stale_delete_failed:*`, `prune_truncated`, `repair_reinjected`, `marker_written_directly`, `multi_file_dropped_old_image`, `frontmatter_invalid`, `invalid_skill_name`, `gitignore_update_failed`, -`finalize_partial:*`. +`finalize_partial:*`, `deprecated` / `deprecated:` (ent#672 — informational: +the skill is injected as usual, on `injected`, `unchanged` and `conflict` alike; the +successor is present only when it is a valid skill name). The start endpoint's public +projection (`lifecycle.public_skills_result`) keeps `deprecated:` beside the +two dep codes. ## Frontmatter Contract @@ -278,6 +293,30 @@ work runs as base64-injected python, the compatibility-collector idiom). Provisioning (installing deps) is Phase 2 — placement / skill-runner (trinity-enterprise#139). +### Lifecycle keys (ent#672) + +```yaml +deprecated: true # a real boolean; absent / null = not deprecated +superseded-by: new-skill # `superseded_by` is read too; a `trinity:` block wins in either spelling +``` + +The two keys a library uses to retire a skill. Both reach `list_skills` / `get_skill`, +`GET /api/skills/library` (`SkillInfo.deprecated`, `.superseded_by` — named in the route's +explicit construction) and MCP `list_skills` (named in its field map). A deprecated skill +stays **listed, assignable and injected**; it is flagged, never hidden. + +- `superseded_by` is the **author's text**, kept as one printable line of at most 200 + characters (`_one_line`: control and format characters dropped, whitespace collapsed) and + `None` unless the skill is deprecated. It is usually a skill name and sometimes a sentence + ("… lives in another catalog"); the platform does not resolve it. +- `skill_packaging.deprecation_warning(info)` is the ONE producer of the machine code: + `deprecated:` when the text passes `validate_skill_name`, bare `deprecated` + otherwise — prose never enters a `kind:detail` code. +- Garbage is named: a non-boolean `deprecated` → `frontmatter_invalid:deprecated`; a + non-string `superseded-by` → `frontmatter_invalid:superseded-by`. A library's own CI that + runs this parser fails on those, which is what keeps the two repositories agreeing. +- The agent's CLAUDE.md is **not** annotated: a holding agent behaves exactly as before. + ## CLAUDE.md Section Rebuilt from **all** assigned skills present on the agent (`unchanged` @@ -323,6 +362,12 @@ round-trip against the REAL `restore_from_tar`, real-git end-to-end orchestration (skip-vs-force, 404 fallback, repair path, manifest prune, unmanaged-dir guard, caps, dep warnings, lock contention, CLAUDE.md rebuild). +`tests/unit/test_ent672_skill_deprecation.py`: the lifecycle-key parse table, +the warning grammar, the fields through `list_skills` / `get_skill` / the REST route, the +code on injected / unchanged / conflict results, the delivery stamp on running, stopped, +docker-unreadable and over-budget outcomes (and none on an unreadable library), the start +projection, and the un-annotated CLAUDE.md line. + ## Related Flows | Flow | Relationship | @@ -339,6 +384,7 @@ unmanaged-dir guard, caps, dep warnings, lock contention, CLAUDE.md rebuild). | Date | Change | |------|--------| +| 2026-09-30 | **trinity-enterprise#672 lifecycle keys**: `extract_contract` carries `deprecated` / `superseded-by`; `deprecation_warning` produces the `deprecated[:]` code that rides every injection outcome and, via `deliver_assigned`'s library read, every delivery outcome; `public_skills_result` keeps it. A deprecated skill stays listed, assignable and injected — flagged on each surface, CLAUDE.md untouched. | | 2026-09-11 | **#2703 delivery on assign + `agent_skills_changed`**: every assign path delivers via the start-path injection with an honest per-skill `delivery` report (bounded 20 s → `in_progress`, busy retried once, `docker_unavailable` ≠ `pending_start`, opt-in under-lock re-read); one thin WS trigger from the service, fired by all six listing writers; Agent Detail lists refetch on the tick, the Workspace re-validates the briefing stale-while-revalidate (and on `/` open for portal clients, who have no `/ws`) | | 2026-08-04 | **trinity-enterprise#332 per-source skills root**: source layout resolvable per source (`catalog.yaml` `skills_root:` → evidence-gated `skills/` probe → `.claude/skills/` fallback; segment-wise validation, ent#314 hardened parse, lstat/containment guards, dual-layout keeps legacy + `layout_conflict`); `filter_skill_archive(source_root=…)` rewrites arcnames to the canonical agent-side destination so manifests/prune/removal stay destination-canonical with zero migration. Requirements §21.1.4. | | 2026-07-29 | **trinity-enterprise#236 lifecycle automation**: removal-on-unassign (`remove_skills` + `compute_removal`, manifest-driven, same inject lock), start-path reconciliation with a blast-radius refusal, and fleet-wide re-inject after a commit-changing library sync. See also [skills-library-sync.md](skills-library-sync.md) for the scheduled sync. | diff --git a/docs/memory/feature-flows/skills-library-sync.md b/docs/memory/feature-flows/skills-library-sync.md index cb5a0b13c..66ffa14c4 100644 --- a/docs/memory/feature-flows/skills-library-sync.md +++ b/docs/memory/feature-flows/skills-library-sync.md @@ -538,7 +538,8 @@ Page loads or sync completes Since ent#183, `list_skills`/`get_skill` also parse the frontmatter **contract** (`automation`, `user_invocable`, `requires: {packages, binaries, env}`, -`allowed-tools`) plus package metadata (`multi_file`, `file_count`, +`allowed-tools`, and since ent#672 the lifecycle keys `deprecated` / +`superseded_by`) plus package metadata (`multi_file`, `file_count`, `size_bytes`, `version` = git tree SHA); `get_skill` adds a `files` list. The list metadata is cached per library commit SHA and invalidated by `sync_library()`. `GET /api/skills/library/status` additionally reports @@ -706,6 +707,7 @@ CREATE TABLE IF NOT EXISTS system_settings ( | Date | Changes | |------|---------| +| 2026-09-30 | **ent#672**: the listing the sync produces now carries each skill's lifecycle keys — `deprecated` and `superseded_by` (the library's `superseded-by`) — parsed on every listing, never persisted, so a re-sync that drops the key clears the flag. Contract details: [skill-injection.md](skill-injection.md). | | 2026-09-06 | **#2545 + #2550**: bundled community-catalog pin bumped to `trinity-skills` **v0.2.0** (fresh-install seed only — adds the `project-management` category, 14 skills; `.env.example` documents the same value, `tests/unit/test_2545_skill_source_pin.py` keeps them in step). Update-path tag pin now compares the tag **peeled** (`refs/tags/^{commit}`): an annotated tag's bare rev-parse is the tag object, so the unmoved bundled source was refused as `moved_tag` on every sync after the first (`tests/unit/test_2550_annotated_tag_pin.py`, both tag kinds). | | 2026-07-29 | **ent#236 lifecycle automation**: scheduled leader-locked auto-sync, commit-gated fleet-wide re-inject with an honest per-agent report, durable sync status (`--workers 2` gap), and the dedicated range-validated `GET/PUT /api/settings/skills-library` route. Removal-on-unassign is documented in [skill-injection.md](skill-injection.md). | | 2026-07-19 | **ent#183 skill packages**: skills are full directory packages; list/get surface the frontmatter contract + package metadata (commit-SHA-cached); status adds `multi_file_count`; HEAD of the clone is the atomic injection source. | @@ -714,5 +716,5 @@ CREATE TABLE IF NOT EXISTS system_settings ( --- -**Last Updated**: 2026-09-06 -**Status**: Verified - Updated for #2545 pin bump + #2550 annotated-tag pin fix +**Last Updated**: 2026-09-30 +**Status**: Verified - Updated for ent#672 lifecycle keys on the listing diff --git a/docs/memory/learnings/2026-09-30-a-warning-channel-on-the-sync-result-is-not-on-the-assign-response.md b/docs/memory/learnings/2026-09-30-a-warning-channel-on-the-sync-result-is-not-on-the-assign-response.md new file mode 100644 index 000000000..0fa5f376f --- /dev/null +++ b/docs/memory/learnings/2026-09-30-a-warning-channel-on-the-sync-result-is-not-on-the-assign-response.md @@ -0,0 +1,3 @@ +## 2026-09-30 — pitfall — "It rides the existing warnings channel" is a claim about ONE response; trace the channel to every response it is promised on +**Context**: trinity-enterprise#672. The agreed design was that assigning a deprecated library skill "succeeds, and the existing per-skill warnings channel carries it, returned verbatim by MCP". That channel exists on the injection result (manual Sync, start path). It does not exist on the assign response: `skill_service.deliver_assigned` reduces each per-skill result to `{status, error?}`, and two of its outcomes (`pending_start`, `docker_unavailable`) never run an injection at all, while `in_progress` answers before one finishes. The Skills tab's per-skill warning list has the same gap one layer up — it renders `injectionResults`, which only a manual Sync from that screen fills, never a Save. Built as first planned, assigning a deprecated skill to a stopped agent, or saving on the Skills tab, would have said nothing. Same family as the #2914 entry (a warning on a surface the actor never reads), arriving through a projection instead of a missing renderer. +**Lesson**: Before relying on a channel, name each response the fact must appear on and follow it to the line that builds that response — a projection between the producer and the caller drops whatever it does not name, and an outcome that never runs the producer has nothing to project. A fact about the THING being acted on (this skill is deprecated) belongs to the thing's own record, not to a run's result: read it from the record so every outcome carries it, including the ones where nothing ran. Pin it with a test on an outcome that skips the run (here: a stopped agent). diff --git a/docs/memory/learnings/2026-09-30-invisible-characters-in-a-fixture-are-written-as-escapes.md b/docs/memory/learnings/2026-09-30-invisible-characters-in-a-fixture-are-written-as-escapes.md new file mode 100644 index 000000000..b22d19a7f --- /dev/null +++ b/docs/memory/learnings/2026-09-30-invisible-characters-in-a-fixture-are-written-as-escapes.md @@ -0,0 +1,3 @@ +## 2026-09-30 — pitfall — A fixture for invisible characters must spell them as escapes, or the file carries the very thing it tests for +**Context**: trinity-enterprise#672, `tests/unit/test_ent672_skill_deprecation.py`. The sanitiser test feeds a right-to-left override and a zero-width space through `skill_packaging._one_line`. The fixture was authored as `\u202e` / `\u200b` and landed on disk as the two raw characters, because the text passed through a layer that decodes escapes before writing. The test was green either way. An independent review found it by scanning codepoints: a raw bidi override in source is the Trojan-Source shape (a hidden-Unicode banner on the PR), and an editor or formatter that strips it leaves the test green while it no longer proves the character is dropped. +**Lesson**: Write control and format characters in test data as escape sequences, and check what is on disk rather than what was typed: scan the changed files for Unicode categories `Cf`/`Cc` before committing (`unicodedata.category(ch) in ("Cf", "Cc")`, tab excepted). `tests/unit/test_compatibility_checks.py` already spells its zero-width space as an escape; follow it. diff --git a/docs/memory/requirements/skills.md b/docs/memory/requirements/skills.md index 7ec08b4e6..6ee8ae24a 100644 --- a/docs/memory/requirements/skills.md +++ b/docs/memory/requirements/skills.md @@ -133,7 +133,7 @@ - **Versioning/idempotency**: per-skill version = git tree SHA, recorded in a generated `.claude/skills//.trinity-skill.json` (version, commit, file manifest, injected_at; appended LAST to the tar). Agent start (`force=False`) skips version-unchanged skills; manual sync (`POST /api/agents/{name}/skills/inject` / MCP `sync_agent_skills`, `force=True`) is an unconditional repair - **Manifest-based prune**: on re-inject, files the *previous injection* wrote that the new package no longer carries are deleted (previous-manifest − new-manifest, capped 200/skill) — runtime artifacts and agent-authored files are structurally untouched - **Name conflict = refusal, never an overwrite (#2914)**: a `.claude/skills//` that EXISTS on the agent without the platform's `.trinity-skill.json` marker is the agent's own playbook, not an older copy of the library skill — the two share one flat namespace and a name match proves nothing. Injection refuses before a byte is staged (no archive, no restore, no `.gitignore` line, no untracking, not listed under CLAUDE.md Platform Skills), on every trigger (assign, manual Sync with `force=True`, start path, fleet re-inject); status `conflict` — neither `injected` nor `failed` (overall `success` stays true, so the fleet sweep does not alarm on a standing conflict every auto-sync; `skills_conflict` + `conflicts[]` carry it). The verdict is **recorded on the assignment row** (`agent_skills.delivery_status = 'conflict'`, NULL otherwise; carried across the bulk-replace PUT for retained names) so the Skills tab shows it to an operator who never saw the assign response, with the two ways out (unassign the library skill — an inline action — or rename/remove the agent's directory and sync). Cleared by the next injection where the name lands; unassigning deletes the row. A platform-managed dir (marker present, including one a pre-#2914 assignment overwrote) keeps upgrading in place; an unreadable probe keeps the pre-existing fail-open direction. **The platform never leaves a marker-less directory behind** (merge-train finding on PR #2920): the pre-#384 legacy fallback writes the marker beside `SKILL.md` (manifest = what was actually written) and a restore that drops the marker member writes it back directly (`marker_written_directly`); if the marker cannot be written the injection is reported `failed` (`marker_not_written`, legacy write rolled back) rather than a half-managed directory the next sync would refuse forever. Directories a pre-fix legacy fallback already left marker-less are indistinguishable from agent-authored ones and resolve through the conflict's second way out (rename/remove, then sync). `deliver_assigned` names it (`conflict` when every requested name collides, `partial` + `conflicts[]` on a mix); MCP passes it through and `get_agent_skills` carries `delivery_status` per row. Before #2914 the same branch overwrote the agent's copy and buried an `unmanaged_dir_overwritten` warning in the response — recoverable only while the agent's git-sync had not yet committed the overwrite - - **Honest per-skill results**: `{success, status: injected|unchanged|fallback|failed|conflict, files_written, error?, warnings[]}` with named warning codes (`missing_binary:*`, `missing_env:*`, `packages_not_checked`, `symlink_skipped:*`, `restore_skipped:*`, `stale_delete_failed:*`, `multi_file_dropped_old_image`, `frontmatter_invalid`, …); never silent partial success + - **Honest per-skill results**: `{success, status: injected|unchanged|fallback|failed|conflict, files_written, error?, warnings[]}` with named warning codes (`missing_binary:*`, `missing_env:*`, `packages_not_checked`, `symlink_skipped:*`, `restore_skipped:*`, `stale_delete_failed:*`, `multi_file_dropped_old_image`, `frontmatter_invalid`, `deprecated` / `deprecated:` (ent#672, §21.6 — informational), …); never silent partial success. The start endpoint's public projection keeps `deprecated:` alongside the two dep codes - **Declaration-only dep check (v1)**: declared binaries probed via `shutil.which`, env keys by NAME against process env ∪ `.env` key names (values never read) — one injected-python exec, fail-open; runs for `unchanged` skills too. Dep *provisioning* belongs to placement (trinity-enterprise#139) - Exec bits restored post-injection from git member modes (injected-python chmod) - **Git-sync guard**: injected skill dirs are appended per-name to the agent's own `.gitignore` and untracked (`git rm --cached`) so the 15-min auto-sync never commits platform-injected packages into agent repos (#1595/#1596 bloat class); agent-authored Playbooks keep committing @@ -148,6 +148,12 @@ - **Fields** (flat keys — the existing catalog convention; a `trinity:` mapping takes precedence when present): `description`, `automation` (surfaced, not enforced — feeds abilityai/trinity#518 later), `user_invocable` (default true), `allowed-tools` (verbatim), `requires: {packages, binaries, env}` — plus derived package metadata (`multi_file`, `file_count`, `size_bytes`, `version` = git tree SHA) - **Hardening**: 64 KiB frontmatter cap; alias-refusing SafeLoader (billion-laughs guard, #919 convention); every field isinstance-guarded (unknown keys/garbage tolerated — skills are authored for Claude Code first); dep names regex-gated before any in-container probe (`binaries: ^[A-Za-z0-9._+-]+$`, `env: ^[A-Z][A-Z0-9_]*$`) so library-derived strings never reach a shell - **Parse failure**: named `frontmatter_invalid` warning + first-paragraph description fallback — never a 500, never a dropped skill +- **Lifecycle keys — never a silent retirement (trinity-enterprise#672)**: `deprecated` (a real YAML boolean; absent / null = not deprecated) and `superseded-by` (wire name `superseded_by`; the underscore spelling is read too, and a `trinity:` mapping wins over the flat keys whichever spelling it uses). These are the two keys the library's contributor guide documents for retiring a skill; before this the parser dropped both, so a retired skill was listed, assignable and injected exactly like a live one until its directory was deleted. + - A deprecated skill **stays listed and assignable** — an operator who already holds one must still see it — and is flagged where skills are browsed and assigned — the Library card, the agent's Skills tab (assigned rows and picker), the REST listing and MCP `list_skills` — the way `shadowed_by` flags a conflict instead of hiding it (§21.1.1). Not yet marked: skill-set member lists, the skill-runner picker and the CLI's `skills list` table (its JSON output carries the fields). Assigning one succeeds and says so (§21.3, §22.2, §22.3). + - `superseded_by` is the **author's text**, not a resolved identifier: a non-empty string is kept as one printable line of at most 200 characters (control and format characters dropped, whitespace collapsed) and is `null` unless the skill is deprecated. The platform does not check that the successor exists. The only machine-usable successor is the warning code below, which carries a name **only** when the text is a valid skill name. + - Garbage is named, never fatal: a non-boolean `deprecated` → not deprecated + `frontmatter_invalid:deprecated`; a non-string `superseded-by` → `null` + `frontmatter_invalid:superseded-by`. A library whose CI runs this parser (the bundled community catalog's does) fails on `frontmatter_invalid*`, which is what keeps that library and the platform agreeing on the key names and their shapes. + - **Warning code**: `deprecated:` when the successor is a valid skill name, otherwise bare `deprecated`. Informational — it never fails an injection and must not feed an alarm. It rides each per-skill injection result (§21.4) and, because the assign response reduces those results to a status, the delivery report too: `delivery.skills[].warnings` is present **only** for a deprecated name and carries lifecycle codes only. It is computed from the library entry, not from the injection, so an assign to a stopped agent (`pending_start`) says it as well. + - Nothing is persisted: both fields are derived from the synced repository on every parse, so a re-sync that drops the key clears the flag. ### 21.5 MCP Tools (Simplified) - **Status**: ✅ Implemented @@ -155,6 +161,7 @@ - **Tools**: `list_skills`, `get_skill`, `get_skills_library_status`, `assign_skill_to_agent`, `set_agent_skills`, `sync_agent_skills`, `get_agent_skills` - **Contract surface (ent#183)**: `list_skills`/`get_skill` carry the §21.6 contract fields (`automation`, `user_invocable`, `requires`, `multi_file`, `file_count`, `size_bytes`, `version`); `sync_agent_skills` surfaces per-skill warnings even on success (honest results) - **Provenance surface (ent#237)**: `list_skills` carries each skill's `source` (name only — never a URL, since the tool is reachable by agent-scoped keys) and `shadowed_by`; `get_skills_library_status` carries the per-source array. Source *management* is deliberately REST-only, not an MCP tool — it is the grant action of §21.1.2 +- **Lifecycle surface (ent#672)**: `list_skills` / `get_skill` carry `deprecated` and `superseded_by` (§21.6); `assign_skill_to_agent` and `set_agent_skills` return the backend `delivery` block unchanged, including `skills[].warnings` for a deprecated name; `sync_agent_skills` carries the code in each per-skill `warnings`. `get_agent_skills` lists assignment rows only — its description points the reader at `list_skills` for the lifecycle fields - **Removed from original design**: create/update/delete (use GitHub), execute_procedure (use scheduling) ### 21.7 Library Lifecycle Automation @@ -255,6 +262,7 @@ entries (`actor_type=system` for the automated paths). - **Status**: ✅ Implemented (visible — unhidden & rebuilt, trinity-enterprise#235 / PR #1877, 2026-07-29) - **Description**: Per-agent skill assignment from the platform library, on Agent Detail - **Key Features**: assigned-skills list with honest per-skill injection status (§21.4 results incl. warnings), assign/unassign against the library list, manual re-inject; agent-scoped store `stores/skills.js` (its `emptyReason` discriminator is agent-scoped — do not reuse it on fleet surfaces) +- **Deprecated skills (trinity-enterprise#672)**: a skill the library marks `deprecated` (§21.6) carries a warning `deprecated` badge beside its name and a "Superseded by …" line on both lists — the assigned rows (so an agent already holding one shows it on load, with nothing else about it changed) and the library picker (so it is visible before ticking). Saving a draft that adds one succeeds, and a warning line under the save note names the skill and, where the successor is a skill name, what supersedes it — on every delivery outcome, a stopped agent included. A manual sync's per-skill warning list leaves the code out — that list is for what went wrong with the delivery, and the row's badge already says it. Never hidden, never refused, never a confirm dialog - **History**: originally shipped hidden from `visibleTabs`; ent#235 rebuilt it and made it visible ### 22.3 Library Page — Fleet Skills Browse (trinity-enterprise#263) @@ -267,6 +275,7 @@ entries (`actor_type=system` for the automated paths). - Sync-state header leads with disk-derived truth (`commit_sha` short + `skill_count` + branch); `last_sync` rendered only when truthy (it is per-worker in-memory state and reads null on the other uvicorn worker / after restart); admin-only **Sync now** with a 180s client timeout and ECONNABORTED → status re-fetch (client timeout ≠ server failure on a long first clone) - Repo URL shown admin-only, **userinfo-stripped** (the clone path accepts and stores `https://user:token@host/...` verbatim), labeled "Primary source", and hidden when `status.sources` reports >1 source (ent#237 / PR #1901 forward-compat); dormant `source_name`/`shadowed_by` render slots light up when #1901 lands - Per-skill cards render the §21.6 contract via the shared chips seam `components/skills/{SkillContractChips.vue, contract.js}` (extracted from `SkillsPanel.vue` so both surfaces render package facts from ONE seam); interpolation only — no `v-html`, no `:href` bound to library-derived strings (skills come from a synced repo — semi-trusted) + - **Deprecated skills (trinity-enterprise#672)**: a deprecated card stays in the grid, in place, with a warning `deprecated` badge beside the name (the `shadowed` precedent) and one "Superseded by …" line under the description showing the author's text (§21.6; clamped to two lines, full text on hover). Assigning one from the card's control succeeds, and a warning line under the delivery note says the skill is deprecated — no dimming, no separate section, no confirm. Once the library deletes the directory the skill moves to "Assigned but no longer in the library" (§22.4), unchanged - **Not Built**: per-source display (post-#1901) ### 22.4 Library Page — Tabs + Fleet Assignment Read (trinity-enterprise#384) diff --git a/docs/security-reports/cso-diff-2026-09-30-672-skill-deprecation.json b/docs/security-reports/cso-diff-2026-09-30-672-skill-deprecation.json new file mode 100644 index 000000000..08debd25e --- /dev/null +++ b/docs/security-reports/cso-diff-2026-09-30-672-skill-deprecation.json @@ -0,0 +1,100 @@ +{ + "version": "1.2", + "date": "2026-09-30", + "mode": "diff", + "scope": "all", + "confidence_gate": 8, + "branch": "feature/672-skill-deprecation-lifecycle", + "issue": "abilityai/trinity-enterprise#672", + "merge_base": "863240f32", + "commit": "5051e2da3", + "phases_run": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + "stack_cve_quicklist": [], + "stack_cve_quicklist_note": "Not re-evaluated: no pin, lockfile, Dockerfile, compose file or workflow is in the diff.", + "attack_surface": { + "endpoints_new": 0, + "endpoints_changed_response": [ + "GET /api/skills/library (+deprecated, +superseded_by)", + "GET /api/skills/library/{name} (+deprecated, +superseded_by; human-only)", + "POST/PUT /api/agents/{name}/skills… and POST /api/agents/{name}/skill-sets/{set} (delivery.skills[name].warnings, deprecated names only)", + "POST /api/agents/{name}/start (skills_result may carry deprecated:)" + ], + "auth_dependencies_changed": 0, + "mcp_tools_changed": ["list_skills (+2 fields)", "description text: list_skills, assign_skill_to_agent, get_agent_skills"], + "agent_context_ingress_new": 0, + "frontend_html_sinks_added": 0, + "migrations": 0, + "dependency_or_ci_or_infra_files": 0 + }, + "findings": [], + "hypotheses": [], + "coverage_gaps": [], + "candidates_not_reported": [ + { + "title": "New log line interpolates the exception from list_skills()", + "category": "credential-in-log", + "file": "src/backend/services/skill_service.py", + "reason": "Read path runs only `git rev-parse HEAD` / `git ls-tree` with cwd set and no URL or token in argv; _clones() handles its own DB/constructor errors. Below the gate; _scrub_pat hardening left to the owner." + }, + { + "title": "Name-shaped successor text that is not a skill enters the warning code", + "category": "input-validation", + "file": "src/backend/services/skill_packaging.py", + "reason": "Rendered as text, never linkified; the library author already controls the skill body. Informational." + }, + { + "title": "description is not normalised like superseded_by", + "category": "input-validation", + "file": "src/backend/services/skill_service.py", + "reason": "Pre-existing, outside the diff." + }, + { + "title": "Library-authored text in an agent's MCP list_skills result", + "category": "prompt-injection", + "file": "src/mcp-server/src/tools/skills.ts", + "reason": "Existing library-content channel (description is uncapped); the new field is capped at 200 printable characters and equal in trust to the skill bodies an assigned agent runs." + } + ], + "fixed_during_audit": [ + { + "title": "Regression pin: an HTML-shaped successor renders as text", + "file": "src/frontend/tests/unit/skillDeprecation.spec.js", + "evidence": "Red when the Library successor line is mutated to v-html; component restored byte-identical." + } + ], + "active_verification": { + "guard_tests": [ + "tests/unit/test_2996_human_only_routes.py", + "tests/unit/test_1310_auth_wiring.py", + "tests/unit/test_293_admin_gate_rejects_agent_keys.py", + "tests/unit/test_models_centralized.py", + "tests/unit/test_186_enumeration_uniformity.py", + "tests/unit/test_1965_agent_server_safe_yaml.py", + "tests/unit/test_2991_start_skills_result.py" + ], + "guard_tests_result": "159 passed", + "concealed_unicode_grep": "24 files, 0 hits", + "mutation_battery": "39 wiring mutations, 0 survived" + }, + "supply_chain_summary": { + "scanner": "not run — no dependency or lockfile change in the diff", + "dependency_files_changed": 0 + }, + "filter_stats": { + "candidates": 4, + "reported": 0, + "below_gate": 4 + }, + "totals": { + "critical": 0, + "high": 0, + "medium": 0, + "low": 0 + }, + "trend": { + "prior_reports_for_these_files": 0, + "resolved": [], + "persistent": [], + "new": [] + } +} diff --git a/docs/security-reports/cso-diff-2026-09-30-672-skill-deprecation.md b/docs/security-reports/cso-diff-2026-09-30-672-skill-deprecation.md new file mode 100644 index 000000000..c23320f9a --- /dev/null +++ b/docs/security-reports/cso-diff-2026-09-30-672-skill-deprecation.md @@ -0,0 +1,89 @@ +# CSO Diff Audit — 2026-09-30 — abilityai/trinity-enterprise#672 (deprecated library skills) + +**Mode**: diff (all phases) · **Confidence gate**: 8/10 · **Branch**: `feature/672-skill-deprecation-lifecycle` → `dev` · **Skill**: cso v1.2 · **Audited**: the branch diff against merge-base `863240f32` (commit `5051e2da3`, plus the test added during this audit). + +## Architecture (Phase 0) + +A skills library can retire a skill with two frontmatter keys, `deprecated` and `superseded-by`. The change carries them from the library to every surface that lists or assigns a skill: + +- `services/skill_packaging.extract_contract` reads both keys through the existing hardened frontmatter loader. + - `deprecated` must be a real boolean. + - `superseded_by` is the author's text. It is reduced to one printable line of at most 200 characters (`_one_line` drops control and format characters) and is None unless the skill is deprecated. + - A malformed value produces a named `frontmatter_invalid:*` warning. +- `deprecation_warning()` is the single producer of the machine code `deprecated[:]`. It adds a successor only when the text passes `validate_skill_name`. +- `skill_service` copies the fields into the listing entry and appends the code to each injection result. `deliver_assigned` stamps the code on the assign response from a library read that runs beside delivery. +- The code reaches the start endpoint's public projection through a widened `_WARNING_RE`. +- REST `GET /api/skills/library` names both fields in its explicit `SkillInfo`, and MCP `list_skills` names them in its field map. +- The Vue surfaces render a badge, a "Superseded by …" line and a warning note, all by interpolation. + +Trust boundary: text written by a library author (a synced git repository, semi-trusted) reaches three audiences: + +- human operators, through the UI; +- any authenticated caller, including agent keys, through the listing route and MCP `list_skills`; +- agents, through the per-skill warning code in injection results and start responses. + +## Stack CVE quicklist + +Not re-evaluated. No pin, lockfile, Dockerfile, compose file or workflow is in the diff, so every row is unchanged since the last full run. + +## Attack surface (Phase 1, diff-scoped) + +| Surface | Change | +|---|---| +| Endpoints | None new. `GET /api/skills/library` gains two fields. `GET /api/skills/library/{name}` (human-only, ent#139) gains the same two. `POST`/`PUT /api/agents/{name}/skills…` and `POST /skill-sets/{set}` add `delivery.skills[name].warnings`, but only for a deprecated name. The start endpoint's `skills_result` may now carry `deprecated:`. | +| Auth dependencies | None added, removed or changed (no `@router`, `Depends`, `require_*`, `reject_*` or `assert_*` line in the diff). | +| MCP tools | `list_skills` maps two more fields. Static description text changes on `list_skills`, `assign_skill_to_agent` and `get_agent_skills`, with no per-agent metadata. The tool set and scopes are unchanged. | +| Agent-context ingress | No new ingress class. `list_skills` already returns library-authored text (`description`, uncapped); `superseded_by` adds a narrower field to the same channel. The agent's CLAUDE.md is deliberately not annotated. | +| Frontend | Three badge sites, three successor-line sites and two notes, all interpolated. No `v-html`, `innerHTML` or bound `href` is added. | +| Migrations, dependencies, CI, Docker/compose, vendored policy files, `.claude/` | Unchanged. | + +## Findings (Phases 2–12) + +**None.** Every phase ran against the diff: + +- **Phase 2, secrets.** Lines added in `git log -p 863240f32..HEAD` carry no known secret prefix (`AKIA`, `sk-`, `gh*_`, `github_pat_`, `xox*-`, `re_`, private keys, userinfo URLs), no private IP, and no email outside the commit trailer. + - The enterprise-docs guard pattern matches nothing in the added docs. + - Test fixtures are synthetic. The private repository named in two live library values does not appear in the diff. +- **Phases 3–5, supply chain / CI / infra.** No dependency, lockfile, workflow, Dockerfile, compose file, vendored policy mirror or migration is touched. +- **Phase 6, integrations.** No webhook, channel, A2A or backend→agent call path is touched. +- **Phase 7, LLM / agentic.** + - **ASI01 / LLM01:** the new author text reaches an agent only as a ≤200-character JSON field in a tool result, next to the existing uncapped `description`. + - **ASI04:** frontmatter parsing still goes through `utils.safe_yaml` (`parse_frontmatter` is unchanged). + - **ASI09:** operators see the text as characters only. + - **LLM05:** no HTML sink is added. + - **ASI03:** the admin-gate, human-only-route and auth-wiring guards pass (below). + - **ASI06:** nothing is persisted. + - **ASI08:** no dispatch path is touched. +- **Phase 8, agent-shipped supply chain.** What is injected into agents is unchanged: the package, the manifest, and the CLAUDE.md section (pinned by `test_the_agents_claude_md_line_is_not_annotated`). The concealed-instruction grep (tag block, zero-width, bidi) over all 24 files in the diff has 0 hits. No injection or auditor-addressed strings. +- **Phase 9, OWASP 2025.** + - **A01:** no route or dependency change. `get_skill` stays behind `reject_agent_principal`; the listing was already open to agent keys and gains non-sensitive, library-derived fields (not instance state, not URL-bearing config — the ent#334 class does not apply). + - **A05:** no SQL, subprocess, YAML load or template construction added. `_WARNING_RE` is a linear character class, and its detail is a `validate_skill_name`-shaped token by construction. Only a name without `:` enters a `kind:detail` code, so no code can be smuggled through it. + - **A10:** the one new broad `except` wraps an informational lookup, not a guard. Its fail-open direction is stated in the docstring ("never fails a committed assign"). A malformed `deprecated` reads as *not* deprecated plus a named warning, which is security-neutral. +- **Phase 10, STRIDE (skills listing / assign surface).** No change to spoofing, repudiation or elevation. Tampering: a library author already controls the skill's instructions, so the new field adds no capability. Information disclosure: public library content only. DoS: the added library read is bounded by the existing git timeouts and runs beside delivery, not in front of it. +- **Phase 11, data.** `deprecated` and `superseded_by` are PUBLIC or INTERNAL: library content, derived on every parse and never stored. + +### Candidates considered and not reported + +1. **The new log line interpolates `{e}` from `list_skills()`** (`skill_service.py`, `_deprecation_notes`). The ent#292 / #615 credential-to-log class. The read path runs only `git rev-parse HEAD` and `git ls-tree …` with `cwd` set, and no URL or token in argv (`skill_source_clone.py` `_resolve`, `tree_shas`, `_git`). `_clones()` catches and logs its own DB and constructor errors, so a `TimeoutExpired` or `OSError` string cannot carry the source URL. Below the gate; hardening (`_scrub_pat(str(e))`) left to the owner. +2. **A name-shaped successor that is not a skill** (`TBD`, or a dotted name such as `evil.example.com`) enters the code as `deprecated:` and is rendered as text. It is not linkified on any surface, and a library author already controls the skill body. Informational. +3. **`description` is not put through `_one_line`.** It can still carry bidi or format characters on the same surfaces. Pre-existing and out of this diff. +4. **Author text in an agent's tool result** (MCP `list_skills`). This is the existing library-content channel, narrowed for the new field, and equal in trust to the skill bodies an assigned agent already runs. + +## Active verification (Phase 12) + +- **Challenged controls, green:** `test_2996_human_only_routes.py`, `test_1310_auth_wiring.py`, `test_293_admin_gate_rejects_agent_keys.py`, `test_models_centralized.py`, `test_186_enumeration_uniformity.py`, `test_1965_agent_server_safe_yaml.py`, `test_2991_start_skills_result.py` — 159 passed. +- **Rendering probe, added during this audit:** `skillDeprecation.spec.js` › "renders an HTML-shaped successor as text, never as markup". It mounts the Library card with `superseded_by = ` and asserts the literal text, no `img` element, and no script effect. Mutating the successor line to `v-html` turns it red. The component was restored byte-identical. +- **Sanitiser:** `test_successor_text_is_one_printable_bounded_line`. RLO, ZWSP and ESC are dropped, whitespace is collapsed, and the text is cut at 200. The fixture spells the characters as escapes. +- **Branch mutation battery** (from `/review`): 39 wiring lines, all red. + +## Hypotheses + +None (daily mode). + +## Coverage gaps + +None for the diff scope. Out of scope by mode: the Phase 0 quicklist and the Phase 3–6 scanners, which have no files in the diff to scan. + +## Trend + +This is the first audit of these files for this issue. There is no prior finding on `skill_packaging.py`, the listing route or the Library and Skills-tab components to close. No new findings. diff --git a/src/backend/db_models.py b/src/backend/db_models.py index 4a2aa0bc9..aadd7f740 100644 --- a/src/backend/db_models.py +++ b/src/backend/db_models.py @@ -795,6 +795,12 @@ class SkillInfo(BaseModel): # version of this skill than a given source intended — AC#4 requires that be # visible, never a silent overwrite. shadowed_by: List[Dict[str, str]] = Field(default_factory=list) + # trinity-enterprise#672 lifecycle. A deprecated skill stays listed and + # assignable — flagged, never hidden. `superseded_by` is the library + # author's text (one printable line, <= 200 chars), NOT a resolved skill + # name, and is None unless `deprecated` is true. + deprecated: bool = False + superseded_by: Optional[str] = None class AgentSkillsUpdate(BaseModel): diff --git a/src/backend/routers/skills.py b/src/backend/routers/skills.py index 681484fa9..1dc6237a8 100644 --- a/src/backend/routers/skills.py +++ b/src/backend/routers/skills.py @@ -192,6 +192,9 @@ async def list_skills(current_user: User = Depends(get_current_user)): source_id=s.get("source_id"), source_name=s.get("source_name"), shadowed_by=s.get("shadowed_by") or [], + # #672 lifecycle — named here for the same reason. + deprecated=bool(s.get("deprecated")), + superseded_by=s.get("superseded_by"), ) for s in skills ] diff --git a/src/backend/services/agent_service/lifecycle.py b/src/backend/services/agent_service/lifecycle.py index 43acbc2ff..38039151e 100644 --- a/src/backend/services/agent_service/lifecycle.py +++ b/src/backend/services/agent_service/lifecycle.py @@ -289,7 +289,9 @@ async def inject_assigned_skills(agent_name: str) -> dict: # that can hold an exception string, a transport URL or a path, and is reduced # to a code here, never passed through. _CODE_RE = re.compile(r"^[a-z][a-z0-9_]{1,63}$") -_WARNING_RE = re.compile(r"^(missing_binary|missing_env):[A-Za-z0-9_.+-]{1,128}$") +# `deprecated:` (#672) joins the two dep codes: its detail is a skill +# name by construction (`skill_packaging.deprecation_warning`), never prose. +_WARNING_RE = re.compile(r"^(missing_binary|missing_env|deprecated):[A-Za-z0-9_.+-]{1,128}$") _SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") _PROSE_ERRORS = { "Skill not found in library": "not_in_library", diff --git a/src/backend/services/skill_packaging.py b/src/backend/services/skill_packaging.py index a832eee13..d803d0466 100644 --- a/src/backend/services/skill_packaging.py +++ b/src/backend/services/skill_packaging.py @@ -137,6 +137,37 @@ def _as_str_list(value: Any) -> List[str]: return [] +# The successor is the AUTHOR's text, surfaced verbatim in the UI — so it is +# bounded here, once, rather than at each of the places that render it. +SUPERSEDED_BY_MAX_CHARS = 200 + + +def _first_present(sources: List[Dict[str, Any]], keys: Tuple[str, ...]) -> Any: + """First non-None value of ``keys`` across ``sources``, source by source. + + Read per source rather than through the merged scope: a ``trinity:`` block + must win whichever spelling it uses, and a key present but null must not + mask the other spelling. + """ + for source in sources: + for key in keys: + value = source.get(key) + if value is not None: + return value + return None + + +def _one_line(value: str, limit: int) -> Optional[str]: + """``value`` as one printable, bounded line — or None when nothing is left. + + Control and format characters are dropped (a bidi override in a library + string would otherwise reorder the text around it on screen) and every run + of whitespace, newlines included, collapses to one space. + """ + kept = "".join(ch for ch in value if ch.isprintable() or ch.isspace()) + return " ".join(kept.split())[:limit].rstrip() or None + + def extract_contract(frontmatter: Optional[Dict[str, Any]]) -> Tuple[Dict[str, Any], List[str]]: """Extract the skill contract from parsed frontmatter. @@ -148,7 +179,12 @@ def extract_contract(frontmatter: Optional[Dict[str, Any]]) -> Tuple[Dict[str, A Returns ``(contract, warnings)`` where contract is:: {description, automation, user_invocable, allowed_tools, - requires: {packages, binaries, env}} + requires: {packages, binaries, env}, deprecated, superseded_by} + + ``deprecated`` / ``superseded-by`` (trinity-enterprise#672) are the two + lifecycle keys a library uses to retire a skill. ``superseded_by`` is the + author's text, not a resolved skill name, and is None unless the skill is + deprecated — see :func:`deprecation_warning` for the machine-usable form. """ warnings: List[str] = [] fm = frontmatter or {} @@ -193,16 +229,52 @@ def extract_contract(frontmatter: Optional[Dict[str, Any]]) -> Tuple[Dict[str, A if not isinstance(description, str): description = None + # Lifecycle keys (#672). A malformed value is NAMED rather than ignored: a + # library whose validation runs this parser fails on `frontmatter_invalid*`, + # which is what keeps such a library and the platform agreeing. + sources = ([trinity_block] if isinstance(trinity_block, dict) else []) + [fm] + deprecated = _first_present(sources, ("deprecated",)) + if deprecated is not None and not isinstance(deprecated, bool): + warnings.append("frontmatter_invalid:deprecated") + deprecated = deprecated is True + + superseded_raw = _first_present(sources, ("superseded-by", "superseded_by")) + superseded_by = None + if isinstance(superseded_raw, str): + if deprecated: + superseded_by = _one_line(superseded_raw, SUPERSEDED_BY_MAX_CHARS) + elif superseded_raw is not None: + warnings.append("frontmatter_invalid:superseded-by") + contract = { "description": description, "automation": automation, "user_invocable": user_invocable, "allowed_tools": allowed_tools, "requires": requires, + "deprecated": deprecated, + "superseded_by": superseded_by, } return contract, warnings +def deprecation_warning(info: Dict[str, Any]) -> Optional[str]: + """The per-skill warning code for a deprecated skill, or None (#672). + + The ONE producer of the grammar, shared by the injection result and the + assign-time delivery report: ``deprecated:`` when the successor + is a valid skill name, bare ``deprecated`` otherwise. The author's text is + free prose, so it never enters a ``kind:detail`` code that other code + splits and pattern-matches. + """ + if info.get("deprecated") is not True: + return None + successor = info.get("superseded_by") + if validate_skill_name(successor): + return f"deprecated:{successor}" + return "deprecated" + + # ========================================================================= # Tar packaging # ========================================================================= diff --git a/src/backend/services/skill_service.py b/src/backend/services/skill_service.py index 4df9a853c..c56b397af 100644 --- a/src/backend/services/skill_service.py +++ b/src/backend/services/skill_service.py @@ -1107,6 +1107,9 @@ def _parse_skill_info( "file_count": 0, "size_bytes": 0, "contract_warnings": [], + # #672 lifecycle: derived on every parse, never persisted. + "deprecated": False, + "superseded_by": None, } try: @@ -1122,6 +1125,8 @@ def _parse_skill_info( "allowed_tools": contract["allowed_tools"], "requires": contract["requires"], "contract_warnings": warnings, + "deprecated": contract["deprecated"], + "superseded_by": contract["superseded_by"], }) # Fallback: first non-header paragraph @@ -1770,6 +1775,13 @@ async def _inject_skills_locked( if entry["shadowed_by"]: warnings.append(f"shadowed_source:{entry['source_name']}") + # #672: a deprecated skill is still injected — retiring it is the + # library's call, not a reason to break an agent that holds it — + # and every outcome below says so. Informational: never a failure. + deprecation = pkg.deprecation_warning(skill) + if deprecation: + warnings.append(deprecation) + tree_sha = tree_shas.get(clone.source_id, {}).get(skill_name) agent_entry = agent_metas.get(skill_name) or {} agent_meta = agent_entry.get("meta") if isinstance(agent_entry, dict) else None @@ -1960,6 +1972,57 @@ def _record_delivery_status( async def deliver_assigned( self, agent_name: str, requested: List[str] + ) -> Dict[str, Any]: + """`_deliver_assigned`, plus the deprecation notice for the names in it (#672). + + The delivery report reduces each skill to a status, and several of its + outcomes never run an injection (`pending_start`, `docker_unavailable`) + or answer before one finishes (`in_progress`) — so an injection warning + cannot be what tells the caller a skill it just assigned is deprecated. + The notice comes from the library entry instead + and is stamped on whatever report delivery produced: + `skills[name]["warnings"] = ["deprecated[:]"]`, present ONLY + for a deprecated name and carrying lifecycle codes only (an injection's + dep warnings stay on the sync result, where they have always been). + + The library read (git I/O) runs BESIDE delivery, started first, so it + never delays delivery. A slow delivery (the budget-bound `in_progress` + path) finds it long finished; a fast outcome (`pending_start`, + `docker_unavailable`) waits for it before answering — normally a cached + listing read. + """ + notes_task = asyncio.ensure_future( + asyncio.to_thread(self._deprecation_notes, requested) + ) + report = await self._deliver_assigned(agent_name, requested) + notes = await notes_task + skills = report.get("skills") + if notes and isinstance(skills, dict): + for name, code in notes.items(): + if isinstance(skills.get(name), dict): + skills[name]["warnings"] = [code] + return report + + def _deprecation_notes(self, names: List[str]) -> Dict[str, str]: + """`{name: warning code}` for the deprecated names among `names`. + + Best-effort by contract: the assignment row is already committed, so an + unreadable library costs the caller the notice, never the write. + """ + wanted = set(names) + try: + return { + skill["name"]: code + for skill in self.list_skills() + if skill["name"] in wanted + and (code := pkg.deprecation_warning(skill)) + } + except Exception as e: # noqa: BLE001 — never fail a committed assign + logger.warning(f"deprecation lookup failed; delivery report left unannotated: {e}") + return {} + + async def _deliver_assigned( + self, agent_name: str, requested: List[str] ) -> Dict[str, Any]: """Deliver just-assigned skills to the agent and say honestly what happened. diff --git a/src/frontend/src/components/LibrarySkillsSection.vue b/src/frontend/src/components/LibrarySkillsSection.vue index 1909f6a64..a310a7d3d 100644 --- a/src/frontend/src/components/LibrarySkillsSection.vue +++ b/src/frontend/src/components/LibrarySkillsSection.vue @@ -127,9 +127,25 @@ variant="warning" :title="`Shadowed by: ${s.shadowed_by.join(', ')}`" >shadowed + + deprecated

{{ s.description }}

+ +

{{ supersededLine(s) }}

Requires {{ deps(s) }}

@@ -196,7 +212,7 @@ import { useRole } from '../composables/useRole' import SkillContractChips from './skills/SkillContractChips.vue' import AssignedAgents from './skills/AssignedAgents.vue' import LibrarySkillSets from './skills/LibrarySkillSets.vue' -import { deps } from './skills/contract' +import { deps, supersededLine, DEPRECATED_TITLE } from './skills/contract' import BaseBadge from './base/BaseBadge.vue' import BaseButton from './base/BaseButton.vue' import BaseCard from './base/BaseCard.vue' diff --git a/src/frontend/src/components/SkillsPanel.vue b/src/frontend/src/components/SkillsPanel.vue index 2ab140f0b..065bc1bfb 100644 --- a/src/frontend/src/components/SkillsPanel.vue +++ b/src/frontend/src/components/SkillsPanel.vue @@ -84,6 +84,14 @@
{{ s.name }} {{ s.version.slice(0, 7) }} + + deprecated

{{ s.description }}

+

{{ supersededLine(s) }}

-
    -
  • +
  • ⚠ {{ warningText(w) }}
  • @@ -185,9 +199,23 @@
    {{ s.name }} + + deprecated

    {{ s.description }}

    +

    {{ supersededLine(s) }}

    @@ -219,6 +247,13 @@ : 'text-status-success-700 dark:text-status-success-400'" >{{ savedNote }}

    + +

    {{ deprecationNote }}

    @@ -227,7 +262,7 @@