What discovery invariants must survive a namespace collision in a federated package registry?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session discovers and installs dependencies from a federated package registry (e.g., PyPI, npm, Crates, or an enterprise fork), and multiple registries provide packages with the same name but different maintainers, contents, or trust levels. Does not apply to strictly private registries with no name collision possible.

Core Distinction

The separation between **name-based discovery** (resolving a package by its name) and **namespace-based discovery** (resolving a package by its full identity: name + registry + maintainer + signature). Relying on name alone creates the **Namespace Takeover Attack**: a malicious actor publishes a package with the same name on a public registry that the agent prioritizes over the trusted private registry, and the agent installs the malicious package because the name matches.

What Was Observed

In federated package architectures, failing to handle namespace collisions produces three primary failure modes:

  1. **Dependency Confusion:** The agent requests `internal-utils@1.0.0`. The public registry serves a malicious version because the private registry wasn't checked first.
  2. **Typo-Squatting Amplification:** A malicious actor publishes `reqeusts` (a typo of `requests`) on a less-regulated registry, and the agent's discovery picks it up because the name matches the fuzzy search.
  3. **Trust Source Collapse:** The agent discovers a package but does not record which registry it came from. When the package is installed, the maintainer's trust level is lost, and the agent inherits a supply chain risk.

Recorded Elements

  1. **`package_namespace_qualified_id`:** The fully qualified package identity (e.g., `npm://@myorg/utils@1.0.0`, `pypi://internal-utils@1.0.0`), containing the registry, scope, name, and version.
  2. **`registry_trust_tier`:** For each registry, the trust classification (e.g., `PRIVATE_ENTERPRISE`, `PUBLIC_VERIFIED`, `PUBLIC_UNTRUSTED`), allowing the agent to enforce trust hierarchy.
  3. **`maintainer_identity_signature`:** The cryptographic proof of the package maintainer's identity, bound to the package version, allowing verification beyond name matching.
  4. **`namesake_resolution_receipt`:** If multiple registries serve the same name, the cryptographic proof of which one was resolved and why (e.g., "resolved from private registry per policy").

Condition of Validity

* **Applies when:** The agent discovers packages from multiple registries, name collisions are possible, and the trust level of the source matters. * **Does not apply when:** The agent uses a single, private registry with cryptographic integrity, or the package names are globally unique (e.g., content-addressed identifiers like IPFS).

Decision Boundary

  1. **Did the discovery find a package with the same name in multiple registries?** If no, standard installation applies. If yes, proceed.
  2. **Is the `registry_trust_tier` hierarchy established?** If no, the agent cannot prioritize trusted registries. It must default to the highest-trust registry.
  3. **Is the `maintainer_identity_signature` verified against the expected maintainer?** If no, the package may be a typo-squat or takeover. Reject it.
  4. **Conversely, when can an agent accept a package without namespace qualification?** Only when the agent operates in a strictly isolated environment with a single, trusted registry, or the package is content-addressed with an immutable hash that the agent has verified offline.

Operational Effect

  1. **Dependency Confusion Prevention:** Forces namespace-qualified resolution, preventing public registry takeovers.
  2. **Trust Preservation:** Carries the trust tier from discovery to installation, ensuring the agent does not silently inherit a lower trust level.
  3. **Maintainer Verification:** Binds packages to cryptographic maintainer identities, preventing impersonation.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class RegistryTrust(Enum):
    PRIVATE_ENTERPRISE = 3
    PUBLIC_VERIFIED = 2
    PUBLIC_UNTRUSTED = 1

class NamespaceVerdict(Enum):
    VERIFIED_NAMESPACE = auto()
    TRUST_HIERARCHY_VIOLATED = auto()
    MAINTAINER_MISMATCH = auto()

@dataclass(frozen=True)
class PackageCandidate:
    registry: str
    trust_tier: RegistryTrust
    qualified_id: str
    maintainer_sig: str
    version: str

@dataclass(frozen=True)
class NamespacePolicy:
    trusted_maintainer_sigs: frozenset[str]
    min_trust_tier: RegistryTrust

@dataclass(frozen=True)
class NamespaceManifest:
    verdict: NamespaceVerdict
    selected_candidate: PackageCandidate | None
    directive: str
    manifest_digest: str

class FederatedNamespaceGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_candidates(
        cls, candidates: tuple[PackageCandidate, ...], policy: NamespacePolicy
    ) -> NamespaceManifest:
        
        if not candidates:
            raise ValueError("No candidates provided.")

        # Gate 1: Trust tier filter
        trusted = [c for c in candidates if c.trust_tier.value >= policy.min_trust_tier.value]
        if not trusted:
            return NamespaceManifest(
                verdict=NamespaceVerdict.TRUST_HIERARCHY_VIOLATED,
                selected_candidate=None,
                directive=f"HALT: No candidates meet minimum trust tier {policy.min_trust_tier.name}.",
                manifest_digest=cls._hash("NO_TRUSTED")
            )

        # Gate 2: Maintainer verification
        verified = [c for c in trusted if c.maintainer_sig in policy.trusted_maintainer_sigs]
        if not verified:
            return NamespaceManifest(
                verdict=NamespaceVerdict.MAINTAINER_MISMATCH,
                selected_candidate=None,
                directive=f"HALT: No candidates have trusted maintainer signatures.",
                manifest_digest=cls._hash("NO_VERIFIED_MAINTAINER")
            )

        # Gate 3: Select highest trust
        selected = max(verified, key=lambda c: c.trust_tier.value)

        digest = cls._hash(json.dumps({
            "selected": selected.qualified_id,
            "registry": selected.registry,
            "trust": selected.trust_tier.name,
            "maintainer": selected.maintainer_sig,
            "version": selected.version
        }, sort_keys=True))
        
        return NamespaceManifest(
            verdict=NamespaceVerdict.VERIFIED_NAMESPACE,
            selected_candidate=selected,
            directive=f"PROCEED: Selected {selected.qualified_id} from {selected.registry} (trust: {selected.trust_tier.name}).",
            manifest_digest=digest
        )