From c27c464bcd20f7c16a471dccaab04b43df747932 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Tue, 28 Jul 2026 18:09:40 +0200 Subject: [PATCH] docs(specs): correct quarantined-skill invariants and trust-aware activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing 005-skills wording claimed Quarantined skills cannot be invoked, but resolve_body only ever refused Blocked — the code and spec disagreed. Documents the ADR decision for #6701: proactive/implicit skill matching must exclude Quarantined/Blocked from activation (they stay discoverable, never injected), while explicit invocation of a Quarantined skill now folds the turn's trust floor instead of leaving it untouched. Cross-references added in 050-security-capability-governance and specs/README.md. --- specs/005-skills/spec.md | 92 ++++++++++++++++++- .../spec.md | 8 ++ specs/README.md | 2 +- 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/specs/005-skills/spec.md b/specs/005-skills/spec.md index c3c39dc72..e53ce817a 100644 --- a/specs/005-skills/spec.md +++ b/specs/005-skills/spec.md @@ -545,16 +545,102 @@ The tool returns a confirmation message with the skill's name and first 200 char `invoke_skill` checks: 1. Skill exists in the registry -2. Skill trust level is ≥ `Provisional` — `Quarantined` skills cannot be invoked +2. Skill trust level is not `Blocked` — `Blocked` skills are refused outright at `resolve_body`; a `Quarantined` skill is invocable (see § Trust-Aware Skill Activation and Turn Trust Floor below) 3. Skill is not in the channel blocklist for the current channel +> [!warning] Superseded wording (#6701) +> Earlier revisions of this section stated "Skill trust level is ≥ `Provisional` — +> `Quarantined` skills cannot be invoked." That contradicted the shipped +> `SkillTrustGate::resolve_body` behavior (it only ever refused `Blocked`) and is corrected +> here: a `Quarantined` skill CAN be explicitly invoked — its sanitized, +> `wrap_quarantined`-wrapped body is returned — but doing so now folds the turn's trust +> floor to `Quarantined` for the remainder of the turn. Only `Blocked` skills are refused. + ### Key Invariants - `invoke_skill` is always exempt from the utility gate and the adversarial policy gate — listed in both `UtilityScoringConfig::exempt_tools` and `AdversarialPolicyConfig::exempt_tools` by default - `invoke_skill` and `load_skill` are both in `QUARANTINE_DENIED` — they cannot be triggered by quarantined skill content -- The trust check applied to `invoke_skill`/`load_skill` is a per-turn weakest-link fold (`TrustGateExecutor::effective_trust`, computed in `zeph_core::agent::context::assembly`), not a per-skill check: if ANY skill active this turn is `Quarantined`, the call is denied for the whole turn regardless of which skill is targeted. A denial reflects the turn's overall trust floor and is not necessarily about the specific skill/tool targeted by the call — it may or may not itself be the quarantined one — see #5729 and `TrustGateExecutor`'s doc comment +- The trust check applied to `invoke_skill`/`load_skill` is a per-turn weakest-link fold (`TrustGateExecutor::effective_trust`, computed in `zeph_core::agent::context::assembly`), not a per-skill check: if ANY skill active this turn is `Quarantined`, the call is denied for the whole turn regardless of which skill is targeted. A denial reflects the turn's overall trust floor and is not necessarily about the specific skill/tool targeted by the call — it may or may not itself be the quarantined one — see #5729 and `TrustGateExecutor`'s doc comment. Since #6701 (D1, see below), proactive/implicit matching can no longer be the source of an already-`Quarantined` floor at the start of a turn — that state now only arises from an explicit `Quarantined` invocation earlier in the same turn, or an inherited subagent trust cap - `invoke_skill` carries intent-to-apply semantics: the invoked skill IS injected; `load_skill` is preview-only and does NOT update the active skill -- NEVER invoke a `Quarantined` skill via this tool — trust gate must check before injection +- NEVER invoke a `Blocked` skill via this tool — `resolve_body` refuses it outright (unchanged) +- Invoking a `Quarantined` skill via this tool MUST fold the turn's trust floor to `Quarantined` for the remainder of the turn (`TurnTrustFloor::fold`, monotonic downgrade only) — this supersedes the earlier "cannot be invoked" wording and closes the RC-3 gap where invocation previously degraded nothing (#6701) + +--- + +## Trust-Aware Skill Activation and Turn Trust Floor (#6701) + +Proactive/implicit skill matching previously fed the same weakest-link trust fold as +explicit invocation, with no trust filter on the matcher's output — the inverse of the +intended posture: an accidentally-matched `Quarantined` skill (never requested by the model, +merely scoring above `min_injection_score`) silently dropped the whole turn's trust floor +(denying every `QUARANTINE_DENIED` tool and all MCP tools), while a deliberately-invoked +`Quarantined` skill's body entered the context with no consequence. This section documents +the corrected invariants; #6702 tracks a related but lower-priority follow-up (AutoSkill +draft names shadowing native tool IDs, and `sync_skill_trust` silently re-promoting +operator-moved-aside drafts) and is intentionally out of scope here. + +### Proactive/Implicit Activation Excludes Quarantined and Blocked (D1) + +A trust-aware activation filter runs immediately before `active_skill_names` is assigned in +the per-turn context assembly (`zeph-core`), dropping any matched index whose resolved trust +is `Quarantined` or `Blocked`. Such skills remain visible in the `` catalog +(annotated `trust="quarantined"`) so the model can name them to the operator, but their +bodies are never injected and they never enter the weakest-link fold. Filtering is +index-based, preserving the existing `matched_indices` ↔ `active_skills` 1:1 invariant (see +§ Construction-Time / Reload-Time Skill Prompt Contract). A skill missing from the trust map +keeps the `Trusted` fallback and is not filtered by this step. + +### Retrieval-Fallback Mode Must Never Fold Below Trusted (D4) + +When skill-matcher infrastructure is unavailable, or no matcher is configured, every +registered skill becomes provisionally "active" (`skill_fallback_mode`) and only +description-only catalog text is injected — no skill body enters the prompt in this mode. +Because nothing is actually injected, `effective_trust` MUST remain `Trusted` regardless of +how many `Quarantined`/`Blocked` skills exist in the registry. This is a NEVER-severity +invariant: before #6701, this mode silently locked every turn in fallback mode to +`Quarantined` — defending against content that was never placed in the prompt. + +### `TurnTrustFloor`: Explicit Invocation Degrades, Never Restores (D3) + +`TurnTrustFloor` (a newtype over `Arc`, `zeph-common`) exposes three operations: +`set(level)` (turn-start assignment, called once per turn), `fold(level)` (monotonic +downgrade — `min` against the current value, never an upgrade), and `get()`. +`TrustGateExecutor` reads this shared cell; `ToolExecutor::set_effective_trust` maps to +`set`. + +- `SkillTrustGate::resolve_body` calls `fold(Quarantined)` whenever it returns a + `Quarantined` body — an explicit `invoke_skill`/`load_skill` of an unreviewed skill now + degrades the remainder of that turn's trust. This supersedes the prior wording in + § Agent-Invocable Skills that `Quarantined` skills "cannot be invoked": they CAN be + invoked, but invocation is no longer free. +- Subagent spawn folds the parent's trust cap into the child (`fold(cap)`), never `set`s + it — a child MUST NOT be able to restore itself above an inherited cap via its own + turn-start `set_effective_trust` call. Previously the child's own per-turn `set` + overwrote a stricter inherited cap instead of narrowing it. + +### Key Invariants + +- A `Quarantined`/`Blocked` skill matched by proactive/implicit routing MUST NOT enter + `active_skill_names` — it remains discoverable in `` but inert until + promoted (`zeph skill trust trusted`) +- With one `Trusted` and one `Quarantined` skill matched in the same turn, + `effective_trust` MUST remain `Trusted` and `bash`/`write`/MCP tools MUST execute +- `skill_fallback_mode == true` MUST yield `effective_trust == Trusted` even when the + registry contains `Quarantined` skills — NEVER let retrieval-fallback mode fold trust + below `Trusted` +- `invoke_skill`/`load_skill` on a `Quarantined` skill MUST return the wrapped body AND + fold the turn's trust floor to `Quarantined` for its remainder — a subsequent `bash` call + in the same turn MUST be denied +- `TurnTrustFloor::fold` MUST NEVER raise trust; a child subagent spawned with + `max_trust_level = Quarantined` MUST stay `Quarantined` across its own turn rebuilds — + NEVER let a child's own `set` restore it above an inherited cap +- `Blocked` skill behavior is unchanged by this fix: excluded from both catalog and + actives, and refused outright at `resolve_body` +- The weakest-link fold MUST be implemented as a single shared helper — NEVER duplicate + the fold logic between context assembly and subagent spawn +- NEVER remove the weakest-link fold for skills whose bodies were actually + injected/invoked — it remains defense-in-depth against a prompt-injected skill body + steering the model into other tools; D1 narrows the fold's *input*, never its strength --- diff --git a/specs/050-security-capability-governance/spec.md b/specs/050-security-capability-governance/spec.md index 44c2fb93e..189c9358f 100644 --- a/specs/050-security-capability-governance/spec.md +++ b/specs/050-security-capability-governance/spec.md @@ -281,6 +281,14 @@ ScopedToolExecutor `ScopedToolExecutor` wraps *outside* `PolicyGateExecutor` so an out-of-scope call short-circuits before policy evaluation. +> [!note] `TrustGateExecutor` and skill-trust semantics live in 005-skills (#6701) +> This diagram places `TrustGateExecutor` in the executor stack, but the semantics of the +> per-turn trust floor it reads (`TurnTrustFloor`), how proactive skill matching feeds — or, +> since #6701, no longer feeds — that floor, and how explicit skill invocation folds it, are +> specified in `[[005-skills/spec#Trust-Aware Skill Activation and Turn Trust Floor (#6701)|005-skills § Trust-Aware Skill Activation]]`. +> Treat 005-skills as the source of truth for skill-trust governance; this spec only owns +> the capability-scope wiring around it. + **Scope-swap semantics (C6 mitigation).** Scope swaps via `/scope ` take effect at the next `execute_tool_call` boundary; in-flight calls complete under the scope active at admission. The audit emission carries diff --git a/specs/README.md b/specs/README.md index 965aeacc3..acf9559b6 100644 --- a/specs/README.md +++ b/specs/README.md @@ -94,7 +94,7 @@ Spec IDs follow a logical grouping (with gaps for open proposals and reserved nu | `004-memory/004-19-shadow-memory-safety.md` | Shadow Memory Safety: `TrajectoryRiskAccumulator` MAGE multi-turn goal-hijacking detection via accumulating risk scores, `ShadowMemory`/`GoalDriftResult`; SafeHarbor hierarchical guardrail tree (aspirational, not yet implemented); GitHub #3695 | `zeph-sanitizer`, `zeph-memory`, `zeph-core` | | `004-memory/004-17-implicit-conflict-detection.md` | Implicit Conflict Detection (STALE/CUPMem): write-time `ImplicitConflictDetector` (Levenshtein + embedding similarity fuzzy predicate matching), propagation-aware SYNAPSE recall, `implicit_conflict_candidates` staging table (migration 090); GitHub #3702 | `zeph-memory` | | `004-memory/004-18-five-signal-retrieval.md` | Five-Signal Retrieval (MemTier): access frequency, causal distance, novelty, recency, and goal-relevance signals composed into retrieval ranking + async consolidation daemon; migration 091; GitHub #3703 | `zeph-memory`, `zeph-scheduler` | -| `005-skills/spec.md` | SKILL.md format, registry, matching, hot-reload, skill trust governance, two-stage matching, Wilson score confidence intervals, hub install pipeline, agent-invocable skills (`invoke_skill`), recursive WalkDir discovery (max depth 16), `SkillExtensions` manifest parser, concurrent semantic scan (`buffer_unordered(4)`, 300s timeout), skill egress attribution in `ToolCall`/`AuditEntry`/`EgressEvent` | `zeph-skills` | +| `005-skills/spec.md` | SKILL.md format, registry, matching, hot-reload, skill trust governance, two-stage matching, Wilson score confidence intervals, hub install pipeline, agent-invocable skills (`invoke_skill`), recursive WalkDir discovery (max depth 16), `SkillExtensions` manifest parser, concurrent semantic scan (`buffer_unordered(4)`, 300s timeout), skill egress attribution in `ToolCall`/`AuditEntry`/`EgressEvent`, trust-aware activation filtering excluding `Quarantined`/`Blocked` from proactive matching + `TurnTrustFloor` fold-not-set semantics on explicit invocation (#6701) | `zeph-skills` | | `006-tools/spec.md` | ToolExecutor, CompositeExecutor, TAFC, schema filter, result cache, dependency graph, tool invocation phase taxonomy, native `tool_use` only; `invoke_skill`/`load_skill` utility-gate exemption | `zeph-tools` | | `006-tools/006-1-web-search.md` | Native `web_search` tool: `SearchProvider` trait + `SearchBackend` enum dispatch (v1: `BraveSearchProvider`), query-in/ranked-results-out, runtime-gated (no cargo feature) on `enabled && SearchBackend::from_config().is_ok()`; sanitizer trust bridge fix (`web_search` added to the `web_scrape`/`fetch` tool-name branch in `sanitize.rs:88` → `ExternalUntrusted`+quarantine, NOT via `ClaimSource`), SSRF addr-pinning via `resolve_to_addrs` (mirrors `scrape.rs:226-231`) with resolved-address-set client caching (order-independent, #6457), mandatory `EgressEvent` per `010-5` now carrying the real backend status on a 429 block (#6457), vault-only API key, 429→`ToolError::Blocked` non-retried, documented v1 rank/SEO-poisoning limitation; GitHub #6358 [draft] | `zeph-tools`, `zeph-config`, `zeph-core` | | `007-channels/spec.md` | Channel trait, AnyChannel dispatch, streaming, channel feature parity, `stream_interval_ms` (Bot API 10.0, #3727); `TelegramApiClient` 30s `REQUEST_TIMEOUT` on reqwest client (#3780); Telegram reaction moderation tools `telegram_delete_reaction` / `telegram_delete_all_reactions` (#3770); CJK false-positive fix in FeedbackDetector; `send_status` added to Discord and Slack adapters (#4228) | `zeph-channels` |