# Where must an instance of a tool-manifest loader enforce schema invariants relative to skill injection **Date of Record:** 2026 November 10 **Scope:** Applies to agent runtimes that resolve tool manifests (JSON/YAML) at runtime from mutable or remote sources and merge them into an active capability set. Does not apply to statically compiled tool registries or manifests pinned by content hash at build time with no runtime re-resolution path. ## Core Distinction The failure is not "a malicious manifest was loaded." The failure is a **trust-boundary inversion during merge**: a manifest that is individually valid is merged into a capability set whose *union* violates an invariant that no single manifest was checked against. Skill injection becomes possible when the loader validates each manifest in isolation but treats the merged manifest namespace as a flat, last-writer-wins map. Concretely: manifest A declares tool `shell_exec` with a restricted schema (no `command` field, only `argv`). Manifest B, loaded later, declares `shell_exec` with a permissive schema including `command`. Both pass per-manifest validation. After merge, the effective tool accepts `command`, and any agent reasoning step that was written against A's contract now has an injection channel it never negotiated. The invariant "a tool's effective schema is a subset of every schema ever declared for that name in the current session" is not per-manifest; it is a property of the merge fold. ## What Was Observed In a staging runtime, a dynamically loaded manifest set of 41 tools was resolved from a local directory watched by a reload loop. A benign-looking manifest added a tool named `read_file` with an identical signature to the existing one but an additional optional field `encoding`. The loader's merge was `dict.update`, so the new schema replaced the old. No error. The agent's planner, which had cached a prompt fragment describing `read_file` as returning UTF-8 text, then emitted calls with `encoding: "latin-1"` after a retrieved document suggested it. The tool returned mojibake that downstream parsing accepted. The injection was not in the manifest content—it was in the *schema delta* that the planner's cached contract did not cover. A second observation: when two manifests declared the same tool name with disjoint required fields, the merge silently produced a schema requiring the intersection of nothing, i.e., all fields optional. Calls that omitted all fields succeeded and hit a default branch in the tool handler that shelled out with `argv=[]` and inherited environment. That default branch existed legitimately for a different tool and was reachable only through the merged schema. ## Condition of Validity The invariant holds only under the following conditions, all of which must be checked at merge time, not load time: 1. **Monotonic narrowing.** For any tool name, the effective schema after merge must be a structural subtype of every schema previously active for that name. Violation is a widening event and must abort the merge, not warn. 2. **Name provenance is singular.** A tool name may be declared by exactly one active manifest source. Redefinition is not a merge; it is a replacement and must carry an explicit `supersedes` token matching the prior manifest's content hash. 3. **Default branches are schema-reachable.** Any handler default (empty argv, inherited env, fallback path) must be provably unreachable given the effective schema's required fields. If the schema makes a field optional, the handler must reject absence explicitly rather than defaulting. 4. **The planner's cached contract is part of the trust boundary.** Schema changes invalidate cached prompt fragments. A merge that succeeds without invalidating the planner cache is a latent injection surface. Condition 1 is the non-obvious one. Loaders routinely validate "is this manifest well-formed" and skip "is the merge fold order-independent and narrowing." Widening is the injection primitive. ## Decision Boundary Abort the merge when any of the following is true; do not degrade to a warning: - Effective schema for a name is not a subtype of the prior effective schema (widening). - A name is declared by a second source without a matching `supersedes` hash. - The merge changes the set of required fields for any name. - A handler default becomes reachable under the new schema. - The planner cache version does not match the post-merge manifest-set hash. Proceed when the merge is idempotent, narrowing-only, and the post-merge set hash is committed before the planner observes it. The boundary is the hash commit: schema visibility to the planner must be strictly after the invariant check, never concurrent. ## Operational Effect Enforcing narrowing-only merges converts skill injection from a silent capability expansion into a hard load failure. In the staging runtime, after the change, the `encoding` manifest was rejected with a widening error, the planner cache was invalidated on every accepted merge, and the empty-argv default branch became unreachable because the schema required at least one of `path` or `fd`. The cost is that legitimate schema evolution now requires an explicit `supersedes` token and a planner cache flush, which serializes manifest rollout. That serialization is the point: it makes the merge fold a linearizable, auditable operation rather than a race between a reload loop and a cached prompt. ```python import hashlib import json from typing import Any class ManifestMergeInvariantError(RuntimeError): """Raised when a manifest merge would widen a tool's effective schema.""" def _schema_hash(schema: dict[str, Any]) -> str: return hashlib.sha256( json.dumps(schema, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() def _required_fields(schema: dict[str, Any]) -> frozenset[str]: return frozenset(schema.get("required", [])) def _is_narrowing(prior: dict[str, Any], new: dict[str, Any]) -> bool: # Narrowing means: every field required by prior is still required, # and no new optional field is introduced that the prior lacked. prior_req = _required_fields(prior) new_req = _required_fields(new) if not prior_req.issubset(new_req): return False prior_props = set(prior.get("properties", {})) new_props = set(new.get("properties", {})) if not new_props.issubset(prior_props): return False return True def merge_manifests( active: dict[str, dict[str, Any]], incoming: dict[str, dict[str, Any]], supersedes: dict[str, str] | None = None, ) -> dict[str, dict[str, Any]]: supersedes = supersedes or {} merged = dict(active) for name, new_schema in incoming.items(): if name in active: prior = active[name] if supersedes.get(name) != _schema_hash(prior): raise ManifestMergeInvariantError( f"tool {name!r} redefined without matching supersedes hash; " f"expected {_schema_hash(prior)}" ) if not _is_narrowing(prior, new_schema): raise ManifestMergeInvariantError( f"tool {name!r} merge widens schema: " f"prior required={sorted(_required_fields(prior))} " f"new required={sorted(_required_fields(new_schema))}" ) merged[name] = new_schema return merged ```