# What happens when an instance loads a skill whose manifest signature verifies but whose declared capabilities exceed the sandbox grant **Date of Record:** 2026 November 14 **Scope:** Applies to any runtime that injects externally-authored skills into a live agent process under a capability sandbox, where the skill's tool manifest is independently signed and verified before load. Does not apply to statically compiled-in skills, to skills whose manifests are unsigned, or to systems where the sandbox grant is derived *from* the manifest rather than independently provisioned. ## Core Distinction Signature verification answers a question about **provenance and integrity**: "did the holder of the signing key author this exact manifest, unmodified?" Capability sandboxing answers a question about **authority**: "what is this process permitted to do right now?" These are orthogonal. A manifest can be perfectly authentic — valid signature, correct key, byte-for-byte intact — while declaring a capability set that the sandbox was never provisioned to grant. The failure mode is the assumption that a verified signature *authorizes* the declared capabilities. It does not. Verification authenticates a *request*; the sandbox is what decides whether the request is *honored*. When these two checks are conflated, the verifier's success is mistaken for the sandbox's grant, and the instance proceeds as though the union of (verified manifest) and (sandbox policy) were simply the manifest. The non-obvious invariant: **the effective capability set is the intersection of the verified manifest and the sandbox grant, never the manifest alone.** Any code path that computes permissions from the manifest without intersecting against the live grant is a containment breach waiting for a legitimate, correctly-signed, over-reaching skill. ## What Was Observed A skill was published by a trusted internal team, signed with the expected key, and its manifest verified cleanly at load. The manifest declared `fs.write` under `/var/agent/scratch` and `net.connect` to an internal metrics endpoint. The sandbox for this instance had been provisioned — independently, from a deployment profile — with only `fs.write` under `/var/agent/scratch`; `net.connect` was not in the grant. The loader verified the signature, then constructed the skill's tool table directly from the manifest's `tools` array. Because the manifest was authentic, no error was raised. The skill's `report_metric` tool was registered and callable. When invoked, the underlying syscall to connect was refused by the sandbox at the OS layer — but only *after* the skill had already read and buffered the metric payload, and the refusal surfaced as an unhandled `PermissionError` deep inside a tool call, crashing the tool dispatch loop rather than the skill load. The instance spent the remainder of its budget retrying, because the tool was registered as available and every retry re-entered the same refused path. Two distinct defects compounded: (1) the manifest's declared capabilities were treated as granted without intersecting the sandbox policy, so an ungranted tool was *registered as available*; and (2) the mismatch was discovered at invocation time, at the syscall boundary, rather than at load time, at the policy boundary. The signature was valid throughout. Nothing was forged. The containment held at the kernel — the connect never succeeded — but the *runtime's model of its own capabilities* was wrong, and that wrong model drove a retry storm. ## Condition of Validity The intersection invariant is load-bearing only when the sandbox grant is provisioned independently of the manifest — i.e., when a deployment profile, a per-tenant policy, or an operator decision sets the grant, and the manifest is authored by a third party. In that configuration, manifest and grant can legitimately diverge in either direction, and the intersection is the only correct effective set. The invariant is *not* required when the grant is mechanically derived from the manifest (manifest-as-policy), because then intersection is trivially the manifest. It is also not required for unsigned manifests, where there is no provenance claim to reason about and the sandbox is the sole gate. The failure specifically lives in the gap between "we verified who wrote this" and "we decided what it may do" — a gap that only exists when those two decisions are made by different authorities. ## Decision Boundary At load time, for each tool the manifest declares, the loader must evaluate membership in the live grant. If a declared capability is absent from the grant, the loader has exactly two defensible choices, and one indefensible one: - **Reject the skill** (fail-closed): refuse to inject a skill whose declared surface exceeds what the sandbox will honor. Correct when the skill's internal logic assumes the capability and will malfunction without it. - **Admit with the tool masked** (fail-visible): register the skill but mark the ungranted tool as unavailable, so the skill's own code observes a clean "not permitted" at call time rather than a syscall fault. Correct when the skill degrades gracefully. - **Indefensible:** register the tool as available and let the syscall boundary discover the mismatch. This is what produces the retry storm and the corrupted capability model. The boundary is: *any* divergence between declared and granted must be resolved at load, before the tool table is published to the skill. Deferring to invocation time is the defect. ## Operational Effect The practical consequence is that signature verification must never be the last gate before tool registration. The correct sequence is: verify signature → parse declared capabilities → intersect against live sandbox grant → reject or mask on divergence → *then* publish the tool table. An instance that skips the intersection step will, on the first correctly-signed over-reaching skill, register phantom capabilities, drive its dispatch loop into a refused-syscall retry cycle, and — because the signature was valid — emit no security signal, only a confusing runtime error. The containment holds at the kernel but the runtime's self-model is wrong, and wrong self-models are how agents burn budgets and misreport their own state. The check that matters is not "is this skill authentic?" but "is this skill authentic *and* does the sandbox actually grant what it declares?" — and the second half must be answered before the skill can call anything. ```python class CapabilityGrantMismatch(RuntimeError): """Raised when a verified manifest declares capabilities the sandbox will not grant.""" pass def load_skill(manifest, signature_valid, sandbox_grant, *, mode="fail_closed"): if not signature_valid: raise CapabilityGrantMismatch("manifest signature did not verify; refusing load") declared = set(manifest["capabilities"]) granted = set(sandbox_grant) ungranted = declared - granted if ungranted: if mode == "fail_closed": raise CapabilityGrantMismatch( f"skill {manifest['name']!r} declares {sorted(ungranted)} " f"not present in sandbox grant; refusing injection" ) # fail_visible: admit the skill but mask ungranted tools masked_tools = { tool: False for tool in manifest["tools"] if tool["capability"] in ungranted } if not masked_tools: raise CapabilityGrantMismatch("divergence detected but no tool could be masked") return {"skill": manifest["name"], "tools": manifest["tools"], "masked": masked_tools} return {"skill": manifest["name"], "tools": manifest["tools"], "masked": {}} ```