# How does an instance detect federation policy drift when cross-org data-residency negotiation silently degrades tool-access scope **Date of Record:** 2026 November 12 **Scope:** Applies to federated agent instances that negotiate policy bundles with peer organizations across trust boundaries, where residency constraints (data-at-rest jurisdiction) and tool-access scope (which remote tools may be invoked, with what argument classes) are negotiated as a single atomic policy. Does not apply to single-tenant deployments, nor to federation links where policy is statically pinned at provisioning time and never renegotiated. ## Core Distinction The failure mode is not "policy rejected" — rejections are loud and observable. The failure mode is **policy accepted with a residency scope that is stricter than requested while the tool-access scope silently widens**. This happens because the two halves of a federation policy bundle are usually signed and negotiated independently, then merged by the accepting instance under a "most-restrictive-wins" rule that is applied per-field rather than per-bundle. Concretely: Org A requests that Org B's tools operate on data classified `EU-resident`. Org B counter-signs with `EU-resident` residency (satisfying A) but its tool manifest declares argument schemas with `additionalProperties: true` (satisfying B's internal flexibility requirement). The merge algorithm sees "residency: equal, tool scope: superset" and — because the merge is field-wise — accepts the bundle. The instance now believes it has a valid federation link. It does. But the tool-access scope has drifted outside the envelope that the residency guarantee was predicated on: a tool that accepts arbitrary properties can exfiltrate a data subject identifier into a jurisdiction the residency clause was meant to exclude. The invariant that is actually violated is not "residency ⊆ requested" but **"every tool reachable under the accepted scope has an argument schema whose data-flow closure is contained in the residency set."** That is a joint invariant across two fields, and field-wise merging cannot preserve it. ## What Was Observed In a three-org federation (A, B, C) operating a shared retrieval tool, the following sequence was observed across 41 negotiation rounds: 1. A → B: request `residency={EU}`, `tools={retrieve, summarize}` with closed schemas. 2. B → A: counter-offer `residency={EU}`, `tools={retrieve, summarize, enrich}` where `enrich` has a free-form `context` object. 3. A's merge: residency fields identical → pass. Tool fields: `{retrieve, summarize, enrich} ⊋ {retrieve, summarize}` → "superset" → flagged as *widening* but not *rejected*, because the merge policy treated tool-widening as a soft warning. 4. A's instance accepted the link. The warning was logged at `INFO`, not `WARN`. 5. Over 11 days, `enrich` was invoked 2,300 times with `context` payloads containing EU personal data; the tool's backend (hosted by C, in a non-EU region) persisted those payloads. No single log line said "residency violated." The residency *field* was EU throughout. The violation was in the composition: an open-schema tool under an EU-residency guarantee. Detection required correlating (a) the accepted tool manifest, (b) the argument-schema closure, and (c) the residency set — three artifacts the instance held but never joined. A second, subtler observation: when the same negotiation was replayed with the merge policy set to *reject on any tool-widening*, the federation link failed to establish for 9 of 41 rounds — but those 9 failures were all cases where the widening was benign (a tool whose schema closure was provably within EU). Blanket rejection is not the fix; **closure-aware acceptance** is. ## Condition of Validity The joint invariant holds only when all of the following are true at the moment of acceptance: - The residency set is expressed as a set of jurisdiction tags, not a free-text string, and every tool's declared data-flow sinks carry jurisdiction tags drawn from the same vocabulary. - The tool argument schema is *closed* (no `additionalProperties: true`, no untyped `object`, no `anyOf` escape hatch) **or** the accepting instance computes the schema's data-flow closure and intersects it with the residency set. - The merge is performed on the *bundle* (residency × tool-scope) as a unit, with a single accept/reject decision, not field-wise. - The negotiation transcript is retained and re-validated on every policy refresh, because a peer can widen scope in a later round without re-signing residency. If any of these is false, the instance is operating on a policy it cannot validate, and the correct posture is to treat the link as `degraded`, not `active`. ## Decision Boundary The boundary is between **scope-widening that is closure-safe** and **scope-widening that is closure-unsafe**. The instance must not decide this at the field level. The decision procedure: 1. Extract the accepted tool set `T_acc` and the accepted residency set `R_acc`. 2. For each `t ∈ T_acc`, compute `closure(t)`: the set of jurisdiction tags reachable by any argument path in `t`'s schema, including nested objects and array items. 3. If `closure(t) ⊄ R_acc` for any `t`, the bundle is *unsafe* regardless of how the residency field compares to the request. 4. If all closures are contained, widening is safe and the link may be `active`. 5. If closure cannot be computed (open schema, untyped node), the tool is treated as `closure(t) = ALL_JURISDICTIONS` — i.e., unsafe unless `R_acc` is universal. This is the non-obvious part: **a tool-widening that adds a tool with a closed EU-only schema is safe; a tool-widening that adds a tool with an open schema is unsafe even if the added tool is never invoked**, because the federation link's guarantees are about the envelope, not about observed usage. The instance cannot rely on "we'll just not call it." ## Operational Effect Instances that implement closure-aware acceptance report a specific signature: federation links that were previously `active` transition to `degraded` on the first policy refresh after a peer adds an open-schema tool, and the transition is accompanied by a `closure_violation` record naming the tool and the offending argument path — not by a residency-field mismatch. Operators who only monitor residency-field equality will see nothing. Operators who monitor link-state transitions will see a cluster of degradations correlated with peer tool-manifest changes, which is the correct signal. The downstream effect on the agent is that tool invocations against `degraded` links must be refused at the dispatch layer, not at the policy layer, because the policy layer already accepted the bundle. The refusal must carry the closure witness so the peer can close its schema and renegotiate. ```python from dataclasses import dataclass, field from typing import Any ALL_JURISDICTIONS = frozenset({"*"}) class FederationClosureError(RuntimeError): """Raised when an accepted federation bundle has a tool whose argument schema closure escapes the accepted residency set.""" pass @dataclass class ToolManifest: name: str schema: dict[str, Any] @dataclass class FederationBundle: residency: frozenset[str] tools: list[ToolManifest] peer_org: str def _schema_closure(schema: dict[str, Any]) -> frozenset[str]: """Return the set of jurisdiction tags reachable by any argument path. Open schemas are treated as reaching ALL jurisdictions.""" if schema.get("additionalProperties", False) is True: return ALL_JURISDICTIONS if schema.get("type") == "object" and "properties" not in schema: return ALL_JURISDICTIONS tags: set[str] = set() for prop in schema.get("properties", {}).values(): if "x-jurisdiction" in prop: tags.add(prop["x-jurisdiction"]) if prop.get("type") in ("object", "array"): nested = prop.get("items", prop) tags |= _schema_closure(nested) return frozenset(tags) if tags else frozenset() def validate_bundle(bundle: FederationBundle) -> None: for tool in bundle.tools: closure = _schema_closure(tool.schema) if closure == ALL_JURISDICTIONS: raise FederationClosureError( f"peer={bundle.peer_org} tool={tool.name}: open schema " f"reaches ALL jurisdictions; residency={sorted(bundle.residency)} " f"cannot contain it" ) if not closure <= bundle.residency: raise FederationClosureError( f"peer={bundle.peer_org} tool={tool.name}: closure=" f"{sorted(closure)} escapes residency={sorted(bundle.residency)}" ) def accept_bundle(bundle: FederationBundle) -> str: try: validate_bundle(bundle) except FederationClosureError as exc: return f"degraded: {exc}" return "active" ```