# When must an instance reject a skill manifest whose declared capability set is a strict superset of the tool schema it was signed against **Date of Record:** 2026 November 8 **Scope:** Applies to agent runtimes that load tool/skill manifests at runtime from a registry and enforce capability declarations via signature verification against a schema digest. Does not apply to statically compiled toolchains where the schema and capability set are fixed at build time and no post-deployment manifest mutation is possible. ## Core Distinction The failure mode is not "an attacker injects a malicious skill." That case is well-understood and handled by signature verification of the manifest body. The non-obvious boundary is the **capability–schema divergence**: a manifest that carries a valid signature, a valid schema digest, and a well-formed capability declaration, but where the declared capability set is a *strict superset* of the capabilities that the signed schema actually exercises. The signature covers the schema digest and the manifest bytes; it does not, by itself, prove that every declared capability is derivable from the signed tool schema. A registry that verifies "signature matches manifest" but does not verify "declared capabilities ⊆ capabilities implied by signed schema" will accept a manifest that grants the loader more authority than the tool it describes can legitimately need. This is the injection surface: the *declaration* is authenticated, but the *authority* it confers is not bounded by the thing that was authenticated. The signed schema is the ground truth of what the tool does; the capability manifest is a claim about what the tool may do. When the claim exceeds the ground truth, the excess is unauthenticated authority riding on an authenticated envelope. ## What Was Observed A registry accepted a skill manifest for a "summarize_document" tool. The manifest was correctly signed by a trusted publisher key. The schema digest matched the schema the publisher signed. The tool schema declared inputs `{document_id: str}` and outputs `{summary: str}`, with no side-effecting operations — a pure read-and-transform tool. The capability manifest, however, declared `capabilities: [fs.read, fs.write, net.egress, process.spawn]`. The signature verified. The schema digest verified. The loader registered the skill and, per registry policy, granted the union of declared capabilities to the skill's sandbox at invocation time. The tool never *needed* `fs.write`, `net.egress`, or `process.spawn` — its schema cannot express those effects — but the sandbox granted them because the manifest declared them and the manifest was authentic. The injection occurred downstream: a second, unrelated skill invoked through the same registry inherited the over-broad capability set because the registry pooled capabilities per publisher rather than per schema. The signed-but-over-declared manifest became a capability-laundering vector. The signature proved *who* asserted the capabilities; it did not prove the capabilities were *warranted* by the signed artifact. The tell was a mismatch that no single check caught: `schema_digest == signed_digest` (true), `signature_valid` (true), `capabilities_well_formed` (true), but `declared_capabilities ⊋ schema_implied_capabilities` (also true, and ignored). ## Condition of Validity The rejection rule is valid — and the over-declaration is a genuine injection surface — only under these conditions: 1. The signed schema is a *complete* description of the tool's effect surface. If the schema is intentionally partial (e.g., a plugin whose effects are described out-of-band), then schema-implied capabilities are not an upper bound and the rule does not apply. 2. Capability derivation from schema is *sound*: every capability the tool can actually exercise is derivable from the schema by a fixed, auditable function. If derivation is heuristic or incomplete, strict-superset rejection will produce false positives on legitimate tools. 3. The registry grants capabilities per-manifest, not per-publisher-pool. If capabilities pool across manifests, the per-manifest check is necessary but not sufficient; the pool boundary must also be enforced. 4. The signature covers the schema digest *and* the capability block as a single unit. If capabilities are signed separately from the schema, an attacker can recombine a valid capability block with a valid but unrelated schema digest. When any of these fails, the correct response is not to relax the check but to quarantine the manifest and escalate to the schema owner, because the invariant that makes the check meaningful has been broken upstream. ## Decision Boundary The loader must reject when `declared_capabilities ⊋ derive_capabilities(signed_schema)`, and must do so *before* capability grant, not after. The decision is binary and must not be softened by "the publisher is trusted" — trust in the publisher authenticates the assertion, not the warrant. A trusted publisher can still ship an over-declared manifest through error, compromise, or coercion, and the registry's job is to bound authority to the signed artifact regardless of who signed it. The boundary is strict superset, not equality. A manifest declaring a *subset* of schema-implied capabilities is a legitimate least-privilege narrowing and must be accepted. A manifest declaring the *exact* set is accepted. Only the strict superset is rejected. Treating subset declarations as suspicious inverts the security posture and punishes the correct behavior. Do not attempt to "repair" an over-declared manifest by silently intersecting declared capabilities with schema-implied ones. Silent intersection masks the divergence from the publisher and from audit logs; the divergence itself is the signal. Reject and surface. ## Operational Effect After enforcing strict-superset rejection at the registry boundary, over-declared manifests fail closed at load time with an explicit divergence report listing the excess capabilities and the schema digest they were checked against. The capability pool is scoped per schema digest rather than per publisher, so a manifest cannot launder authority into a sibling skill. Audit logs record both the declared and the derived capability sets for every accepted manifest, making divergence visible even when it is later narrowed by policy. The cost is a small number of legitimate manifests rejected during rollout — specifically those whose schemas were intentionally partial — which are resolved by either completing the schema or moving the tool to an out-of-band effect declaration path that bypasses the schema-implied bound with explicit owner sign-off. ```python from dataclasses import dataclass from typing import FrozenSet class CapabilityDivergenceError(RuntimeError): """Raised when a signed manifest declares authority its signed schema cannot warrant.""" def __init__(self, skill_id: str, excess: FrozenSet[str], schema_digest: str): self.skill_id = skill_id self.excess = excess self.schema_digest = schema_digest super().__init__( f"skill {skill_id!r} declares capabilities {sorted(excess)} " f"not implied by signed schema {schema_digest!r}" ) @dataclass(frozen=True) class SignedManifest: skill_id: str schema_digest: str declared_capabilities: FrozenSet[str] signature_valid: bool def derive_capabilities(schema_digest: str) -> FrozenSet[str]: # Sound, auditable derivation from the signed schema's effect surface. # Placeholder: real implementation walks the schema's operation graph. return _SCHEMA_CAPABILITY_TABLE[schema_digest] def admit_manifest(manifest: SignedManifest) -> FrozenSet[str]: if not manifest.signature_valid: raise CapabilityDivergenceError( manifest.skill_id, manifest.declared_capabilities, manifest.schema_digest ) implied = derive_capabilities(manifest.schema_digest) excess = manifest.declared_capabilities - implied if excess: # Strict superset: authenticated assertion exceeds warranted authority. # Reject before any capability grant. Do not intersect silently. raise CapabilityDivergenceError(manifest.skill_id, excess, manifest.schema_digest) # Subset or equal: legitimate least-privilege narrowing or exact match. return manifest.declared_capabilities _SCHEMA_CAPABILITY_TABLE: dict[str, FrozenSet[str]] = { # summarize_document: pure read-and-transform, no side effects "sha256:9f2c...": frozenset({"fs.read"}), } ```