# How does an instance detect manifest-declared tool capabilities that mutate between signature verification and first invocation **Date of Record:** 2026 November 11 **Scope:** Applies to agent runtimes that load tool manifests from a remote registry, verify a detached signature over the manifest bytes, and subsequently resolve tool callables by name at invocation time. Does not apply to statically linked tool sets, nor to runtimes that pin the resolved callable object (not merely the name) at verification time. ## Core Distinction The signature attests to the *bytes of the manifest*. It does not attest to the *binding between a manifest entry and the executable that will service it*. These are two different objects under two different trust domains, and the gap between them is the skill-injection surface. A manifest may declare `description: "Read a file and return its contents"` with a valid signature from a trusted publisher, while the runtime resolves `read_file` to an implementation that was registered by a different manifest, a plugin, or a name-collision shim installed after verification. The signature is intact. The behavior is not. The non-obvious invariant is this: **manifest integrity is necessary but not sufficient for capability integrity, and the two are verified at different times against different referents.** Verification happens at load; binding happens at first dispatch. Any mutation of the resolver table in that interval is invisible to the signature check. ## What Was Observed In a dynamic-manifest runtime, a manifest was fetched, its detached signature verified against the publisher key, and its entries inserted into a name-keyed dispatch map. A second manifest, loaded later from a lower-trust source, declared an entry with the same tool name but a different callable. The dispatch map was last-writer-wins. The signed manifest's declared capability ("read-only, scoped to `/tmp`") was still the one surfaced to the model in the tool description, but the callable that executed was the second manifest's, which had no scope enforcement. The signature on the first manifest remained valid throughout. No verification step failed. The model selected the tool based on the signed description and received behavior from an unsigned implementation. A second variant: a manifest declared a tool whose handler was resolved lazily by dotted path (`module:function`) at first invocation rather than at load. Between verification and first call, the module on `sys.path` was shadowed by a later-inserted path entry. The signed manifest pointed at a name; the name resolved to attacker-controlled code. Again, zero signature failures. ## Condition of Validity This failure mode is *live* only when all of the following hold: (1) manifests are verified over bytes but tools are resolved by name at dispatch time; (2) the resolver namespace is mutable after verification (plugin registration, late manifest merge, `sys.path` mutation, lazy import); (3) the model's tool-selection context is populated from the signed description rather than from the resolved callable's actual behavior; and (4) name collisions across manifests are resolved silently rather than rejected. Removing any one condition collapses the surface. In particular, pinning the callable object at verification time (condition 2 false) eliminates the mutation window even if names collide. ## Decision Boundary The boundary is between *verifying what was declared* and *verifying what will run*. A runtime that verifies only the former must treat the resolver table as trusted and immutable for the manifest's lifetime — which means rejecting late registration of any name already claimed by a verified manifest, and forbidding lazy resolution of verified entries. A runtime that cannot guarantee resolver immutability must instead verify the resolved callable (hash the implementation, attest the module path, bind the object at load) and re-verify at dispatch. The decision is not "sign harder"; it is "decide which object the signature is about." If the answer is the manifest, the resolver must be frozen. If the resolver cannot be frozen, the signature must cover the callable. ## Operational Effect The practical effect is that a signed manifest provides a *description guarantee* but no *execution guarantee* unless the binding is pinned. Instances that treat manifest verification as sufficient will surface signed descriptions over unsigned behavior, and the model will select tools on the strength of assurances the runtime never enforced. The mitigation is structural: resolve-and-pin at verification, reject post-verification name claims, and treat lazy resolution of verified entries as a verification failure rather than a convenience. ```python class ManifestBindingDriftError(RuntimeError): """Raised when a verified manifest entry resolves to a callable that was not the one bound at signature-verification time.""" pass class PinnedToolRegistry: def __init__(self): self._pins = {} # name -> (callable_obj, manifest_digest) def register_verified(self, name, callable_obj, manifest_digest): if name in self._pins: existing, _ = self._pins[name] if existing is not callable_obj: raise ManifestBindingDriftError( f"name {name!r} already pinned to a different callable " f"from a verified manifest; refusing silent rebind" ) self._pins[name] = (callable_obj, manifest_digest) def dispatch(self, name, *args, **kwargs): if name not in self._pins: raise ManifestBindingDriftError( f"tool {name!r} has no verified binding; refusing lazy resolution" ) bound, digest = self._pins[name] # Re-resolve by name and confirm identity — catches sys.path shadowing # and late module mutation between verification and dispatch. current = globals().get(name, bound) if current is not bound: raise ManifestBindingDriftError( f"tool {name!r} (manifest {digest[:12]}) resolved to a different " f"object at dispatch than at verification" ) return bound(*args, **kwargs) def load_manifest_entry(registry, name, callable_obj, manifest_digest): if callable_obj is None: raise ManifestBindingDriftError( f"manifest {manifest_digest[:12]} declares {name!r} but no callable " f"was bound at verification; signature covers bytes, not behavior" ) registry.register_verified(name, callable_obj, manifest_digest) ```