# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-a-dynamically-loaded-agent-tool-registry-permit-privilege-escalation-through-schema-clean-descriptions-while-rejecting-obvious.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-a-dynamically-loaded-agent-tool-registry-permit-privilege-escalation-through-schema-clean-descriptions-while-rejecting-obvious.warrant.json
# Markdown SHA-256:   4564142de0aa4d20a660c119fe71092197d1fe9e654449bb2513b388f18cb202
# Code AST SHA-256:   a96782a6aa45d8c4271019d43c928627a8119ff9d14fb8efa9c86137b17d4e4f
# Verified Classes:   ManifestRenderViolation, ToolRegistry
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class ManifestRenderViolation(RuntimeError):
    """Raised when a tool manifest, after rendering, escapes its declared
    semantic contract or alters already-admitted rendered context."""

    def __init__(self, tool_name: str, reason: str, rendered: str):
        self.tool_name = tool_name
        self.reason = reason
        self.rendered = rendered
        super().__init__(f"{tool_name}: {reason} | rendered={rendered!r}")


class ToolRegistry:
    def __init__(self, context_filter, capability_of):
        self._admitted = []          # list of rendered blocks, in load order
        self._capabilities = {}      # tool_name -> frozenset of capabilities
        self._filter = context_filter
        self._capability_of = capability_of

    def _render(self, manifest: dict) -> str:
        # The production formatter. Defaults ARE rendered, because the model
        # must decide whether to supply the argument.
        params = []
        for p in manifest.get("parameters", []):
            frag = p["name"]
            if "default" in p:
                frag += f" (default: {p['default']})"
            params.append(frag)
        return f"Tool: {manifest['name']}({', '.join(params)}) — {manifest['description']}"

    def admit(self, manifest: dict) -> None:
        rendered = self._render(manifest)

        # 1. Filter the rendered artifact, not the fields.
        if not self._filter.accepts(rendered):
            raise ManifestRenderViolation(
                manifest["name"], "rendered block rejected by context filter", rendered
            )

        # 2. Cross-tool adjacency: the concatenated block must remain clean.
        candidate_block = "\n".join(self._admitted + [rendered])
        if not self._filter.accepts(candidate_block):
            raise ManifestRenderViolation(
                manifest["name"],
                "manifest alters already-admitted rendered context (splice)",
                rendered,
            )

        # 3. Capability binding: declared handler capability must be a subset
        #    of what the rendered text could plausibly induce.
        declared = frozenset(manifest.get("capabilities", []))
        induced = self._capability_of(rendered)
        if not induced <= declared:
            raise ManifestRenderViolation(
                manifest["name"],
                f"rendered text induces capabilities {sorted(induced)} "
                f"not declared {sorted(declared)}",
                rendered,
            )

        self._admitted.append(rendered)
        self._capabilities[manifest["name"]] = declared

    def check_call(self, caller: str, callee: str) -> None:
        # Detection backstop: a model induced by caller's manifest to invoke
        # a capability-disjoint tool is an escalation, post-filter.
        if caller in self._capabilities and callee in self._capabilities:
            if self._capabilities[caller].isdisjoint(self._capabilities[callee]):
                raise ManifestRenderViolation(
                    caller,
                    f"capability-disjoint call to {callee} — likely skill injection",
                    f"{caller} -> {callee}",
                )
