Skip to main content

3tears Registry

MCP-compatible tool registry for the 3tears tool system. Routes tool calls between agents and tool pods via NATS request/reply.

Part of the 3tears framework.

Components

  • ToolCatalog -- in-memory index of registered tool pods, backed by a NATS KV bucket for recovery across restarts.
  • RegistrationHandler -- subscribes to {ns}.tools.register and mutates the catalog.
  • HeartbeatMonitor -- sweeps pods whose heartbeats fell behind the timeout and evicts their endpoints.
  • DiscoveryHandler -- serves {ns}.tools.discover for pod-readiness polling.
  • CallProxy -- the hot path. Subscribes to {ns}.tools.call, authorizes via AgentToolAuthorizer, selects an endpoint via the configured RoutingStrategy, and forwards the call to the tool pod via NATS request/reply with identity + correlation carried through the CallContext envelope.
  • RegistryRbacStack -- self-contained rbac surface the standalone server constructs against the connected NATS client: NATS-proxy NamespaceCollection + four rbac metadata Collections + AclCache + invalidation subscribers. The _run_server() entry point uses this to wire RbacEvaluatorAuthorizer without any host-application loaders, so a standalone server no longer defaults to deny-all.

Authorization

Tool dispatch authorization lives behind the AgentToolAuthorizer protocol. Implementations receive the calling principal id, the invoking user id (from CallContext.user_id), the fully qualified tool name and version, and one fact the ids cannot carry, principal_is_tool_pod, and return a boolean decision.

Production deployments wire RbacEvaluatorAuthorizer (in threetears.registry.rbac_authorizer) which delegates to the unified rbac evaluator from threetears.agent.acl:

  • The platform-side ToolNamespaceEmitter listens on {ns}.tools.register and upserts a namespaces row of type tool per tool in every RegistrationManifest. The canonical name shape is tools.<sanitized-mcp-name>.<sanitized-version> (per build_namespace_name); metadata carries the pre-sanitized natural-identity fields mcp_name / mcp_version / pod_id so downstream pattern matching (the access materializer's agent.yaml access.tools patterns) does not need to reverse the sanitization rules.
  • The authorizer resolves the tool namespace via an injected NamespaceCollection. The signature is is_authorized(agent_id, user_id, tool_name, tool_version, *, principal_is_tool_pod). The implementation builds the canonical lookup key via build_namespace_name(PLURAL_PREFIX_TOOL, tool_name, tool_version) rather than passing the raw mcp_name directly, so the lookup matches the row the emitter wrote.
  • evaluate_decision resolves the two-sided grant chain: user side (groups the invoking user is in) intersected with agent side (groups the calling agent is in, short-circuited by namespace ownership). The decision is cached in threetears.agent.acl.AclCache with TTL + fine-grained invalidation; cross-process rbac mutations purge the cache promptly via the acl.{membership,assignment,role}.invalidate subjects the RegistryRbacStack subscribes to on startup.

Defense in depth: when user_id=None on an AGENT's dispatch the authorizer denies unconditionally, because an agent's tool grants are two-sided. When the namespace Collection's get_by_name returns None (tool registered but namespace row not yet visible) it denies. This catches registration races rather than defaulting to allow.

A TOOL POD is the one principal evaluated on its own grant alone. A pod acts on nobody's behalf and never carries a user; the hub mints its identity token with the platform customer sentinel (threetears.core.security.PLATFORM_CUSTOMER_SENTINEL) where a customer UUID would be, and CallProxy._verify_identity reads that signed claim as customer_id=None plus the mark it passes to the authorizer. RbacEvaluatorAuthorizer then evaluates the agent side only, against the pod's own group, exactly as the L3 broker and the hub's datasource authorizer evaluate the same principal. The mark buys evaluation and never authority: a marked pod holding no ToolCaller row on the tool's namespace is refused. Any non-UUID customer claim that is not the sentinel still fails closed at the door.

Calling a tool from a tool pod

ToolCallClient (in threetears.registry.client) is the pod's half of the wire: it publishes the registry's own ProxyCallRequest on {ns}.tools.call with the pod's hub-minted identity token read from a provider on every call and a proof of possession minted by a caller-supplied signer (PopSignerProtocol, the shape the SDK's PopSigner has), and returns the ProxyCallResponse or raises ToolCallError carrying the registry's or the tool's refusal code. It is synchronous on the pod's own inbox and requests no durable reply; its default deadline sits above the registry's forward budget so a slow tool comes back as the registry's TOOL_TIMEOUT rather than the client's own transport fault.

Platform-built-in tools land with owner_agent_id=NULL, customer_id=NULL. There is no implicit "anyone can call" behaviour for them. Grants are managed via explicit assignments on the platform-seeded ToolCaller role (same pattern as shared-type workspaces).

RbacEvaluatorAuthorizer is the only authorizer the production server wires: no dual-enforcement window, no back-compat aliases. The declarative access.tools expression on agent.yaml stays as operator-facing syntax and is translated to RBAC assignments at bootstrap.

Dev-mode authorizers

AllowAllAuthorizer permits every dispatch unconditionally, enabled by THREETEARS_REGISTRY_ALLOW_ALL_TOOLS=true. Use only in local dev containers when an explicit RBAC bypass is needed.

DenyAllAuthorizer refuses every dispatch. Available as a panic-button kill switch via THREETEARS_REGISTRY_FORCE_DENY_ALL=true. It is also the millisecond-window placeholder the server holds before the rbac stack is wired against the live NATS client during serve().

Standalone entry point

python -m threetears.registry

Reads THREETEARS_NATS_URL (defaults to nats://localhost:4222) and THREETEARS_NATS_SUBJECT_NAMESPACE (the NATS subject namespace).

By default the entry point wires RbacEvaluatorAuthorizer against a self-contained RegistryRbacStack (NATS-proxy NamespaceCollection + four rbac metadata Collections + AclCache + invalidation subscribers). The proxy collections read through the platform broker's system.platform.rbac carve-out, so no direct DB credentials are needed. Optional knobs:

  • THREETEARS_REGISTRY_ALLOW_ALL_TOOLS=true -- bypass the rbac stack entirely (dev only).
  • THREETEARS_REGISTRY_FORCE_DENY_ALL=true -- kill switch for misconfigured deployments.
  • THREETEARS_REGISTRY_ACL_TTL_SECONDS -- override the AclCache TTL (default 60s).

The registry's identity, which is REQUIRED

The broker resolves the caller of an L3 request from a signed identity token and refuses a request carrying none, so the registry must be able to prove who it is. build_registry_rbac_stack therefore takes an identity_token provider and raises RegistryIdentityUnavailableError without one -- at WIRING, not on the first query, because a backend built without a token looks built and then fails every read from wherever happens to touch L3 first.

3tears defines no handshake protocol (the subject, the payload, the principal store and the verifier are all the host's), so the provider is supplied out-of-band, exactly like the three factories below it:

  • THREETEARS_REGISTRY_IDENTITY_TOKEN_PROVIDER_FACTORY -- a module:callable dotted path to an async factory Callable[[NatsClient], Awaitable[Callable[[], str | None]]], awaited once the connection is up. It must return a provider, not a token: the token is short-lived and re-minted in place, and a captured value is expired within the hour.
  • THREETEARS_REGISTRY_IDENTITY_SIGNING_KEY_REF -- the scheme://locator reference to the registry's own Ed25519 private key, default env://THREETEARS_REGISTRY_IDENTITY_SIGNING_KEY. 3tears only names the knob; the host's factory resolves it, because only the host knows what to do with it.

Unlike the other three hooks this one has no weaker-but-working default. Unset means the stack refuses to build, because a broker that refuses an unidentified request leaves nothing to fall back to.

Dispatch flow:

sequenceDiagram
    participant Agent
    participant Registry
    participant RbacStack
    participant PlatformBroker
    participant ToolPod

    Agent->>Registry: ProxyCallRequest(tool_name, tool_version, context.user_id)
    Registry->>RbacStack: is_authorized(agent_id, user_id, tool_name, tool_version, principal_is_tool_pod=...)
    RbacStack->>RbacStack: build_namespace_name(PLURAL_PREFIX_TOOL, tool_name, tool_version)
    RbacStack->>PlatformBroker: NamespaceCollection.get_by_name (system.platform.rbac proxy)
    PlatformBroker-->>RbacStack: tool namespace row
    RbacStack->>RbacStack: evaluate_decision (user ∩ agent grants)
    RbacStack-->>Registry: True / False
    Registry->>ToolPod: forward call (CallContext echoed)
    ToolPod-->>Registry: CallResponse
    Registry-->>Agent: ProxyCallResponse

Release files for 3tears-registry 0.46.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for 3tears-registry 0.46.1
File Size Uploaded
3tears_registry-0.46.1.tar.gz 201.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for 3tears-registry 0.46.1
File Interpreter ABI Platform
3tears_registry-0.46.1-py3-none-any.whl Python 3 none any Details

Total release size: 308.8 kB

Release files / 3tears_registry-0.46.1.tar.gz

Download URL 3tears_registry-0.46.1.tar.gz
Size 201.0 kB
Tags Source
SHA-256 checksum
How to use checksums
fd9140354ba80244973c4e4fa96ea3de61f200ed6863daba8a4a5a6d3bd7ff08
BLAKE2b-256 checksum
How to use checksums
694595ee9dc7594ef48714c5302bf897bf1d0e4ab1a6232262513052f94543bc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / 3tears_registry-0.46.1-py3-none-any.whl

Download URL 3tears_registry-0.46.1-py3-none-any.whl
Size 107.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1d7355b40b73559633663d916cd19d2514f9d05a74f333b58846c108abcec50b
BLAKE2b-256 checksum
How to use checksums
c3e385abd402cd06ef6ef09ff32d9f6847db8f0ef4c26568dc4e2a94a0482d4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.52.1

2 release files

0.52.0

2 release files

0.51.1

2 release files

0.51.0

2 release files

0.50.0

2 release files

0.49.0

2 release files

0.48.0

2 release files

0.47.1

2 release files

0.47.0

2 release files

This release

0.46.1 This release

2 release files

0.46.0

2 release files

0.45.1

2 release files

0.45.0

2 release files

0.44.0

2 release files

0.43.0

2 release files

0.42.0

2 release files

0.41.4

2 release files

0.41.3

2 release files

0.41.2

2 release files

0.41.1

2 release files

0.41.0

2 release files

0.40.0

2 release files

0.39.0

2 release files

0.38.0

2 release files

0.37.0

2 release files

0.30.0

2 release files

0.29.0

2 release files

0.28.0

2 release files

0.27.0

2 release files

0.26.1

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.24.7

2 release files

0.24.6

2 release files

0.24.5

2 release files

0.24.4

2 release files

0.24.3

2 release files

0.24.2

2 release files

0.24.1

2 release files

0.24.0

2 release files

0.23.9

2 release files

0.22.4

2 release files

0.22.3

2 release files

0.22.2

2 release files

0.22.1

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.4

2 release files

0.19.3

2 release files

0.19.2

2 release files

0.19.1

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.17.9

2 release files

0.17.8

2 release files

0.17.7

2 release files

0.17.6

2 release files

0.17.5

2 release files

0.17.4

2 release files

0.17.3

2 release files

0.17.2

2 release files

0.17.1

2 release files

0.17.0

2 release files

0.16.1

2 release files

0.16.0

2 release files

0.15.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page