"""typed NATS subject builders.

every NATS subject produced by 3tears applications passes through this
module. callers never construct subject strings by ``f"..."`` —
:class:`Subjects` owns the format, the namespace prefix, and the
sanitization rules. this is the single point where a typo or
namespace-prefix omission gets caught.

design properties
-----------------

- :class:`Subject` is an opaque ``@dataclass(frozen=True, slots=True)``,
  not a ``str`` subclass. consumers that want the raw subject string
  call ``str(subject)``; everything else takes ``Subject`` by type.
  this means a function expecting ``Subject`` cannot accidentally be
  passed a bare interpolated string.
- :class:`Subjects` factory has one classmethod per subject family in
  the canonical 3tears topology
  (``docs/done/design-01-nats-topology.md``). adding a new family is
  one method; reformatting an existing family is one method.
- the namespace prefix (``3tears`` by default, env-driven by
  ``THREETEARS_NATS_SUBJECT_NAMESPACE``) is bound at
  :class:`NatsClient` connect time. :class:`Subjects` reads it from a
  ``ContextVar`` that the client populates; tests and library code can
  set it explicitly via :func:`set_default_namespace`.
- ``Subject.raw(...)`` is the diagnostics escape hatch — for one-off
  subjects in tests or migrations. production code that finds itself
  reaching for ``raw`` is missing a builder; add the builder.

separator sanitization
----------------------

raw segment values may contain ``.``; when interpolated they are
replaced with ``-`` so the dot separator is never overloaded.
mirror's :func:`threetears.core.namespaces.build_namespace_name`
rule.
"""

from __future__ import annotations

import hashlib
import os
from dataclasses import dataclass
from typing import Literal
from uuid import UUID

from threetears.nats.errors import NamespaceNotConfiguredError

__all__ = [
    "Subject",
    "SubjectKind",
    "Subjects",
    "get_default_namespace",
    "set_default_namespace",
]


SubjectKind = Literal["point", "pattern", "reply"]
"""subject category.

- ``point``: concrete fully-qualified subject (e.g.
  ``3tears.tools.heartbeat.pod-abc``). usable for both publish and
  subscribe.
- ``pattern``: contains nats wildcards (``*`` or ``>``); subscribe-only.
- ``reply``: opaque inbox returned by nats-py for request/reply paths;
  treated as opaque by every caller.
"""


# Process-wide default namespace prefix used by :class:`Subjects` when a caller
# builds a subject without threading a namespace explicitly. A plain module
# global -- deliberately NOT a ``ContextVar``: the namespace is a single
# process-wide deployment constant, and a ContextVar set at connect/startup time
# is invisible to the SIBLING task trees that serve later work (e.g. a web
# server's per-request handlers spawned by uvicorn). That invisibility silently
# broke subject resolution on those paths -- ``get_default_namespace()`` saw the
# var unset and raised even though the process had connected with a namespace.
# A module global is visible in every task/context in the process, which is what
# "process-wide" has always meant here.
_default_namespace: str | None = None


def set_default_namespace(namespace: str) -> None:
    """set the process-wide default namespace prefix used by :class:`Subjects`.

    :class:`NatsClient.connect` calls this with its configured
    ``nats_subject_namespace`` so every subject built afterwards -- in ANY task
    or async context in the process -- picks up the correct prefix without
    callers having to thread it through.

    :param namespace: subject namespace prefix; must be non-empty
    :ptype namespace: str
    :return: nothing
    :rtype: None
    :raises ValueError: if namespace is empty
    """
    if not namespace:
        raise ValueError("namespace must be non-empty")
    global _default_namespace
    _default_namespace = namespace


def _reset_default_namespace() -> None:
    """clear the process-wide default namespace back to unconfigured.

    test-isolation helper. the module global carries process-wide, so -- unlike
    the retired ContextVar, which reset itself as each test's task context went
    out of scope -- it does NOT clear between tests on its own. autouse fixtures
    call this to restore the unconfigured state so one test cannot leak a
    namespace into the next.

    :return: nothing
    :rtype: None
    """
    global _default_namespace
    _default_namespace = None


def get_default_namespace() -> str:
    """resolve current subject namespace prefix.

    the namespace has no default. it must be configured explicitly so two
    unconfigured deployments cannot silently collide on the same subjects
    when sharing a NATS cluster.

    resolution order:

    1. value previously set via :func:`set_default_namespace` (process-wide,
       visible in every task/context)
    2. env var ``THREETEARS_NATS_SUBJECT_NAMESPACE`` (read at call
       time, not import time, so changes during process lifetime are
       observed)

    :return: resolved namespace prefix
    :rtype: str
    :raises NamespaceNotConfiguredError: if neither source is set
    """
    if _default_namespace is not None:
        return _default_namespace
    env = os.environ.get("THREETEARS_NATS_SUBJECT_NAMESPACE")
    if env:
        return env
    raise NamespaceNotConfiguredError(
        "no NATS subject namespace configured; call set_default_namespace(), "
        "connect a NatsClient, or set THREETEARS_NATS_SUBJECT_NAMESPACE"
    )


def _sanitize(segment: str | UUID) -> str:
    """coerce a raw segment value to a subject-safe token.

    replaces ``.`` with ``-`` so the dot separator is never
    overloaded. UUID values render as their 36-char canonical string.

    :param segment: raw segment value
    :ptype segment: str | UUID
    :return: subject-safe token
    :rtype: str
    """
    raw = str(segment)
    return raw.replace(".", "-")


def _routing_token(pod_id: str | UUID) -> str:
    """render a pod-id into its tool-subject routing token(s), preserving structural dots.

    a Tool Pod's id is a single UUID token (no dots) -- :func:`_sanitize` is a no-op for it. an
    agent's IN-PROCESS tool pod-id is the composite ``{agent_id}.{instance}`` (built by
    :meth:`Subjects.agent_inprocess_pod_id`) whose structural dot MUST survive so the subject nests
    as ``tools.internal.{agent_id}.{instance}`` under the authenticated-agent subtree the AGENT_POD
    JWT grants (``tools.internal.{agent_id}.>``). each dot-delimited token is sanitized
    independently (so a token's own characters never overload the separator) while the structural
    dot between tokens is preserved -- unlike :func:`_sanitize`, which would collapse the composite
    to a single token and make the agent-id segment un-wildcard-matchable.

    :param pod_id: tool routing pod-id (single UUID token, or the agent composite)
    :ptype pod_id: str | UUID
    :return: dot-joined sanitized token(s)
    :rtype: str
    """
    # the pod_id is rendered into its NATS-subject routing token(s) here; a single UUID token or the
    # agent composite is split on its structural dots and each token is subject-sanitized.
    # convert at border: pod_id -> published NATS subject string
    return ".".join(_sanitize(token) for token in str(pod_id).split("."))


@dataclass(frozen=True, slots=True)
class Subject:
    """opaque NATS subject token.

    consumers should treat instances as opaque; cast to ``str`` at the
    nats-py boundary. wrapper code in :class:`threetears.nats.NatsClient`
    is the only legitimate ``str(subject)`` call site outside of
    tests / diagnostics.

    :param path: full dotted subject (with namespace prefix already applied)
    :ptype path: str
    :param kind: subject category — point, wildcard pattern, or reply inbox
    :ptype kind: SubjectKind
    """

    path: str
    kind: SubjectKind

    def __str__(self) -> str:
        """return raw subject string for nats-py boundary.

        :return: dotted subject string
        :rtype: str
        """
        return self.path

    @classmethod
    def raw(cls, full_subject: str, *, kind: SubjectKind = "point") -> Subject:
        """construct a :class:`Subject` from an already-formatted string.

        diagnostics / migration escape hatch. production code should
        prefer a builder on :class:`Subjects`; reaching for ``raw`` is a
        signal that a new builder is missing.

        :param full_subject: pre-formatted subject string
        :ptype full_subject: str
        :param kind: subject category
        :ptype kind: SubjectKind
        :return: opaque subject token
        :rtype: Subject
        :raises ValueError: if subject string is empty
        """
        if not full_subject:
            raise ValueError("full_subject must be non-empty")
        return cls(path=full_subject, kind=kind)


def _ns() -> str:
    """resolve namespace prefix at builder-call time.

    :return: namespace prefix
    :rtype: str
    """
    return get_default_namespace()


class Subjects:
    """factory for every canonical 3tears / 3tears NATS subject family.

    classmethods are organized by area:

    - **agent**: hub <-> agent pod routing, registration, heartbeats
    - **tools**: tool registry, tool pod heartbeats, tool calls
    - **gateway**: AI model gateway request/reply + streaming
    - **hub**: handshake, secrets, user resolution, streaming bridge,
      usage tracking
    - **audit**: audit event publishing
    - **l3**: L3 broker query / batch / transactional
    - **acl**: ACL invalidation broadcasts
    - **namespace**: namespace catalog discovery
    - **datasource**: datasource query routing
    - **cache**: cross-pod cache invalidation
    - **config epochs**: per-domain generation-stamped reload signals
      (capabilities, catalog, MCP RBAC)
    - **deadletter**: catch-all for failed callbacks
    """

    # ------------------------------------------------------------------
    # agent (hub <-> agent pod)
    # ------------------------------------------------------------------

    @classmethod
    def agent_register(cls) -> Subject:
        """request/reply subject for agent pod self-registration.

        :return: subject ``{ns}.agents.register``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.register", kind="point")

    @classmethod
    def agent_deregister(cls) -> Subject:
        """fire-and-forget subject an agent pod publishes on graceful shutdown.

        lets a cleanly-stopping pod remove its endpoint from the router catalog
        IMMEDIATELY rather than waiting out the heartbeat timeout (the slow
        backstop that only catches ungraceful kills). every router replica
        subscribes WITHOUT a queue group so each replica's catalog drops the pod.

        :return: subject ``{ns}.agents.deregister``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.deregister", kind="point")

    @classmethod
    def agent_heartbeat(cls, agent_id: str | UUID, pod_id: str | UUID) -> Subject:
        """publish subject for one agent pod's heartbeat.

        The ``agent_id`` leads the subject as the AUTHENTICATED segment
        (the connecting pod's token-verified agent identity); ``pod_id``
        comes from the spoofable connect name. Keying on ``agent_id``
        first means a pod can publish heartbeats only under its own
        authenticated agent and cannot forge a peer agent's heartbeat.

        :param agent_id: authenticated agent identity the pod serves
        :ptype agent_id: str | UUID
        :param pod_id: agent pod identifier
        :ptype pod_id: str | UUID
        :return: subject ``{ns}.agents.heartbeat.{agent_id}.{pod_id}``
        :rtype: Subject
        """
        return Subject(
            path=f"{_ns()}.agents.heartbeat.{_sanitize(agent_id)}.{_sanitize(pod_id)}",
            kind="point",
        )

    @classmethod
    def agent_heartbeat_wildcard(cls) -> Subject:
        """wildcard subscribe pattern for all agent-pod heartbeats.

        :return: subject ``{ns}.agents.heartbeat.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.heartbeat.>", kind="pattern")

    @classmethod
    def agent_reregister_request(cls, agent_id: str | UUID, pod_id: str | UUID) -> Subject:
        """router -> agent pod nudge to re-register after deregistration.

        Published by :class:`AgentHeartbeatMonitor.handle_heartbeat` when a
        heartbeat arrives from a pod whose endpoint is missing from the
        catalog (typically because the heartbeat-timeout monitor
        deregistered the pod during a long host pause such as a laptop
        sleep, and the pod is now back alive and emitting heartbeats
        again). The agent pod subscribes to its own
        ``{agent_id}.{pod_id}``-tailed subject and runs the full
        registration handshake on receipt -- restoring the catalog
        endpoint without forcing a process restart.

        The ``agent_id`` leads the subject as the AUTHENTICATED segment so
        a pod subscribes (and the router targets) only its own agent's
        reregister channel; the spoofable ``pod_id`` can no longer stand
        alone to address a peer agent's pod.

        :param agent_id: authenticated agent identity the target pod serves
        :ptype agent_id: str | UUID
        :param pod_id: target agent pod identifier
        :ptype pod_id: str | UUID
        :return: subject ``{ns}.agents.reregister_request.{agent_id}.{pod_id}``
        :rtype: Subject
        """
        return Subject(
            path=f"{_ns()}.agents.reregister_request.{_sanitize(agent_id)}.{_sanitize(pod_id)}",
            kind="point",
        )

    @classmethod
    def agent_route(cls, agent_id: str | UUID) -> Subject:
        """request/reply subject for hub -> agent inbound user message.

        :param agent_id: target agent identifier
        :ptype agent_id: str | UUID
        :return: subject ``{ns}.agents.route.{agent_id}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.route.{_sanitize(agent_id)}", kind="point")

    @classmethod
    def agent_route_wildcard(cls) -> Subject:
        """wildcard subscribe pattern for all agent inbound traffic.

        :return: subject ``{ns}.agents.route.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.route.>", kind="pattern")

    @classmethod
    def agent_internal(cls, agent_id: str | UUID, pod_id: str | UUID) -> Subject:
        """direct agent-pod inbox subject for routed traffic.

        :param agent_id: target agent identifier
        :ptype agent_id: str | UUID
        :param pod_id: target pod identifier
        :ptype pod_id: str | UUID
        :return: subject ``{ns}.agents.internal.{agent_id}.{pod_id}``
        :rtype: Subject
        """
        return Subject(
            path=f"{_ns()}.agents.internal.{_sanitize(agent_id)}.{_sanitize(pod_id)}",
            kind="point",
        )

    @classmethod
    def agent_turn(cls, agent_id: str | UUID) -> Subject:
        """durable JetStream subject for an inbound agent turn (resilience-task-07).

        the caller (hub) JetStream-publishes each inbound user turn here instead
        of the at-most-once ``agent_route`` request/reply; the agent-router is the
        durable consumer that forwards it to the sticky pod and acks only after
        the pod confirms completion. redelivery on pod crash / drain makes inbound
        delivery at-least-once. backed by the ``{ns}-agents-turn`` JetStream stream
        over ``{ns}.agents.turn.>`` (plus the sibling dead-letter subject from
        :meth:`agent_turn_deadletter`).

        :param agent_id: target agent identifier
        :ptype agent_id: str | UUID
        :return: subject ``{ns}.agents.turn.{agent_id}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.turn.{_sanitize(agent_id)}", kind="point")

    @classmethod
    def agent_turn_wildcard(cls) -> Subject:
        """wildcard consumer filter for every agent's inbound turns.

        the agent-router's durable consumer subscribes on this pattern to drain
        every agent's inbound turns. deliberately does NOT cover
        :meth:`agent_turn_deadletter` (a sibling token, not a ``.turn.`` child),
        so exhausted turns park in the stream WITHOUT being re-consumed
        (resilience-task-07).

        :return: subject ``{ns}.agents.turn.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.turn.>", kind="pattern")

    @classmethod
    def agent_turn_deadletter(cls) -> Subject:
        """dead-letter subject for turns the router exhausted (resilience-task-07).

        a turn that stays undeliverable after the bounded redelivery budget is
        republished here. a SIBLING token of ``turn`` (not ``{ns}.agents.turn.*``)
        so :meth:`agent_turn_wildcard` does NOT match it -- the poisoned turn parks
        in the ``{ns}-agents-turn`` stream (which is declared over this subject too)
        with no consumer draining it: inspectable, not lost, not looping.

        :return: subject ``{ns}.agents.turn-deadletter``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.turn-deadletter", kind="point")

    @classmethod
    def agent_complete(cls, correlation_id: str | UUID) -> Subject:
        """per-turn completion-confirmation subject the router awaits (resilience-task-07).

        the pod publishes here once the turn's result is durably produced (the
        answer is in the request/reply reply, streamed, or handed to the durable
        channel-delivery subject). the router awaits this before acking the
        durable turn message; its absence (pod crash / drain refusal / timeout)
        triggers a re-route. keyed on the per-turn ``correlation_id`` so the
        signal maps 1:1 to the turn.

        :param correlation_id: per-turn correlation identifier
        :ptype correlation_id: str | UUID
        :return: subject ``{ns}.agents.complete.{correlation_id}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.complete.{_sanitize(correlation_id)}", kind="point")

    @classmethod
    def agent_reply(cls, correlation_id: str | UUID) -> Subject:
        """per-turn caller reply inbox for the durable turn path (resilience-task-07).

        the caller subscribes here BEFORE JetStream-publishing the turn and awaits
        the synchronous answer; the router publishes the pod's reply (full answer
        for request/reply callers, ack envelope for streaming / channel callers)
        here once the turn confirms. keyed on the per-turn ``correlation_id`` so
        it is deterministic and unique per turn (no opaque inbox to thread).

        :param correlation_id: per-turn correlation identifier
        :ptype correlation_id: str | UUID
        :return: subject ``{ns}.agents.reply.{correlation_id}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.agents.reply.{_sanitize(correlation_id)}", kind="point")

    # ------------------------------------------------------------------
    # tools (registry / tool pods)
    # ------------------------------------------------------------------

    @classmethod
    def tools_register(cls) -> Subject:
        """request/reply subject for tool-pod registration with registry.

        :return: subject ``{ns}.tools.register``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.register", kind="point")

    @classmethod
    def tools_heartbeat(cls, pod_id: str | UUID) -> Subject:
        """publish subject for one tool-pod's heartbeat.

        ``pod_id`` is a single UUID token for a Tool Pod, or the
        ``{agent_id}.{instance}`` composite for an agent's IN-PROCESS tool
        server (see :meth:`agent_inprocess_pod_id`); its structural dots are
        preserved (:func:`_routing_token`) so the composite nests under the
        authenticated-agent subtree the AGENT_POD JWT grants.

        :param pod_id: tool routing pod identifier (single token or composite)
        :ptype pod_id: str | UUID
        :return: subject ``{ns}.tools.heartbeat.{pod_id}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.heartbeat.{_routing_token(pod_id)}", kind="point")

    @classmethod
    def tools_heartbeat_wildcard(cls) -> Subject:
        """wildcard subscribe pattern for all tool-pod heartbeats.

        the ``>`` tail spans both single-token Tool Pod heartbeats
        (``tools.heartbeat.{pod_id}``) and two-token agent in-process
        heartbeats (``tools.heartbeat.{agent_id}.{instance}``), so the
        registry / health monitor sees every pod's heartbeat regardless of
        pod-id shape.

        :return: subject ``{ns}.tools.heartbeat.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.heartbeat.>", kind="pattern")

    @classmethod
    def tools_heartbeat_agent_subtree(cls, agent_id: str | UUID) -> Subject:
        """publish-grant pattern for an agent pod's OWN in-process tool heartbeats.

        the agent serves its in-process tools (workspace / knowledge_drafts /
        devx builtins) under the ``{agent_id}.{instance}`` composite pod-id, so
        its heartbeat publishes land on ``tools.heartbeat.{agent_id}.{instance}``.
        the auth-callout responder knows only the AUTHENTICATED ``agent_id`` (not
        the per-replica instance), so it grants this ``{agent_id}.>`` subtree:
        the pod may publish heartbeats only under its OWN authenticated agent and
        can never forge a peer agent's in-process heartbeat.

        :param agent_id: the authenticated agent identity to scope the subtree on
        :ptype agent_id: str | UUID
        :return: subject ``{ns}.tools.heartbeat.{agent_id}.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.heartbeat.{_sanitize(agent_id)}.>", kind="pattern")

    @classmethod
    def tools_discover(cls) -> Subject:
        """request/reply subject for agent -> registry tool catalog discovery.

        :return: subject ``{ns}.tools.discover``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.discover", kind="point")

    @classmethod
    def tools_call(cls) -> Subject:
        """request/reply subject for agent -> registry tool invocation.

        :return: subject ``{ns}.tools.call``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.call", kind="point")

    @classmethod
    def tools_internal(cls, pod_id: str | UUID) -> Subject:
        """request/reply subject for registry -> tool-pod proxied call.

        ``pod_id`` is a single UUID token for a Tool Pod, or the
        ``{agent_id}.{instance}`` composite for an agent's IN-PROCESS tool
        server; its structural dots are preserved (:func:`_routing_token`) so
        the composite renders as ``tools.internal.{agent_id}.{instance}`` --
        the registry forwards on the registered pod-id verbatim, and the
        agent's AGENT_POD JWT grants the ``tools.internal.{agent_id}.>``
        subtree, so only the authenticated agent can receive its own proxied
        in-process tool calls.

        :param pod_id: target tool routing pod identifier (single token or composite)
        :ptype pod_id: str | UUID
        :return: subject ``{ns}.tools.internal.{pod_id}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.internal.{_routing_token(pod_id)}", kind="point")

    @classmethod
    def tools_internal_wildcard(cls) -> Subject:
        """registry-side forward-grant pattern spanning EVERY tool pod's internal subject.

        the registry proxy forwards each agent call to the target pod's
        ``tools.internal.{pod_id}``; the ``>`` tail covers both single-token
        Tool Pods and two-token agent in-process pods
        (``tools.internal.{agent_id}.{instance}``). granted ONLY to the trusted
        registry router (never a pod), which legitimately reaches every pod.

        :return: subject ``{ns}.tools.internal.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.internal.>", kind="pattern")

    @classmethod
    def tools_internal_agent_subtree(cls, agent_id: str | UUID) -> Subject:
        """subscribe-grant pattern for an agent pod's OWN in-process tool calls.

        the agent's in-process tool server subscribes
        ``tools.internal.{agent_id}.{instance}``; the auth-callout responder
        knows only the AUTHENTICATED ``agent_id`` (not the per-replica
        instance), so it grants this ``{agent_id}.>`` subtree. a peer agent is
        granted only its OWN ``{peer_agent_id}.>`` subtree and can NEVER
        subscribe another agent's in-process tool-call traffic -- closing the
        wiretap a connect-name-scoped grant left open.

        :param agent_id: the authenticated agent identity to scope the subtree on
        :ptype agent_id: str | UUID
        :return: subject ``{ns}.tools.internal.{agent_id}.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.internal.{_sanitize(agent_id)}.>", kind="pattern")

    @classmethod
    def tools_probe(cls, pod_id: str | UUID) -> Subject:
        """request/reply subject for liveness probe against one tool pod.

        ``pod_id`` is a single UUID token for a Tool Pod, or the
        ``{agent_id}.{instance}`` composite for an agent's IN-PROCESS tool
        server; its structural dots are preserved (:func:`_routing_token`).

        :param pod_id: target tool routing pod identifier (single token or composite)
        :ptype pod_id: str | UUID
        :return: subject ``{ns}.tools.probe.{pod_id}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.probe.{_routing_token(pod_id)}", kind="point")

    @classmethod
    def tools_probe_wildcard(cls) -> Subject:
        """registry-side probe-grant pattern spanning EVERY tool pod's probe subject.

        the registry probes each registered pod's ``tools.probe.{pod_id}``
        during the registration reachability handshake; the ``>`` tail covers
        both single-token Tool Pods and two-token agent in-process pods. granted
        ONLY to the trusted registry router.

        :return: subject ``{ns}.tools.probe.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.probe.>", kind="pattern")

    @classmethod
    def tools_probe_agent_subtree(cls, agent_id: str | UUID) -> Subject:
        """subscribe-grant pattern for the registry's probe of an agent's OWN in-process pod.

        mirrors :meth:`tools_internal_agent_subtree`: the agent's in-process
        tool server subscribes ``tools.probe.{agent_id}.{instance}`` and the
        responder grants the ``{agent_id}.>`` subtree, so a pod answers
        reachability probes only for its OWN authenticated agent.

        :param agent_id: the authenticated agent identity to scope the subtree on
        :ptype agent_id: str | UUID
        :return: subject ``{ns}.tools.probe.{agent_id}.>``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.tools.probe.{_sanitize(agent_id)}.>", kind="pattern")

    @classmethod
    def agent_inprocess_pod_id(cls, agent_id: str | UUID, instance_id: str | UUID) -> str:
        """compose the NATS routing pod-id for an agent's IN-PROCESS tool server.

        Returns a plain routing-key STRING (not a :class:`Subject`):
        ``{agent_id}.{instance_id}`` with each token sanitized. This is the
        value the SDK hands :class:`ToolServer` as its ``pod_id`` when standing
        up an in-process tool server on the agent's OWN ``AGENT_POD`` connection
        (the devx ``DevInProcessStrategy`` builtins, and the prod
        ``ProdExternalPodsStrategy`` workspace + ``knowledge_drafts`` tools).

        Leading the routing-key with the AUTHENTICATED ``agent_id`` -- the same
        identity the auth-callout responder resolved from the bootstrap-token
        hash -- is what binds the in-process tool subjects
        (:meth:`tools_internal` / :meth:`tools_probe` / :meth:`tools_heartbeat`,
        which preserve the structural dot) to the authenticated agent rather
        than the spoofable connect-name pod id: they nest under the
        ``tools.internal.{agent_id}.>`` subtree the AGENT_POD JWT grants
        (:meth:`tools_internal_agent_subtree`). The ``instance_id`` tail keeps
        each replica of the same agent routable as a distinct endpoint without
        widening the grant. Tool Pods (separate ``TOOL_POD`` processes) keep
        their single-token UUID pod-id and are unaffected.

        :param agent_id: the authenticated agent identity (leads the routing key)
        :ptype agent_id: str | UUID
        :param instance_id: this pod replica's unique instance id (the connect-name pod id)
        :ptype instance_id: str | UUID
        :return: routing pod-id string ``{agent_id}.{instance_id}``
        :rtype: str
        """
        return f"{_sanitize(agent_id)}.{_sanitize(instance_id)}"

    # ------------------------------------------------------------------
    # gateway (AI model gateway)
    # ------------------------------------------------------------------

    @classmethod
    def gateway_completion(cls) -> Subject:
        """request/reply subject for completion request to AI gateway.

        :return: subject ``{ns}.gateway.completion``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.gateway.completion", kind="point")

    @classmethod
    def gateway_embedding(cls) -> Subject:
        """request/reply subject for embedding request to AI gateway.

        :return: subject ``{ns}.gateway.embedding``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.gateway.embedding", kind="point")

    @classmethod
    def gateway_health(cls) -> Subject:
        """request/reply subject for gateway health check.

        :return: subject ``{ns}.gateway.health``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.gateway.health", kind="point")

    @classmethod
    def gateway_stream(cls, agent_id: str | UUID, correlation_id: str | UUID) -> Subject:
        """publish subject for gateway -> agent token streaming.

        The ``agent_id`` leads the subject as the AUTHENTICATED segment so
        an agent subscribes only its OWN gateway token streams; a bare
        ``gateway.stream.*`` wildcard would let any agent sniff every
        customer's in-flight token stream.

        :param agent_id: authenticated agent the stream is destined for
        :ptype agent_id: str | UUID
        :param correlation_id: correlation identifier shared with originating request
        :ptype correlation_id: str | UUID
        :return: subject ``{ns}.gateway.stream.{agent_id}.{correlation_id}``
        :rtype: Subject
        """
        return Subject(
            path=f"{_ns()}.gateway.stream.{_sanitize(agent_id)}.{_sanitize(correlation_id)}",
            kind="point",
        )

    # ------------------------------------------------------------------
    # hub (handshake, user resolve, secrets, streaming bridge, usage)
    # ------------------------------------------------------------------

    @classmethod
    def hub_handshake(cls) -> Subject:
        """request/reply subject for agent / tool-pod handshake with hub.

        :return: subject ``{ns}.hub.handshake``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.handshake", kind="point")

    @classmethod
    def hub_jwks(cls) -> Subject:
        """request/reply subject for the Hub identity-token JWKS fetched by verifiers.

        The registry proxy + tool pods request this to fetch the public keys identity tokens are
        signed under, then verify tokens against it before RBAC (platform-auth Option B).

        :return: subject ``{ns}.hub.jwks``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.jwks", kind="point")

    @classmethod
    def hub_secrets_request(cls) -> Subject:
        """request/reply subject for encrypted secret delivery from hub.

        :return: subject ``{ns}.hub.secrets.request``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.secrets.request", kind="point")

    @classmethod
    def hub_user_resolve(cls) -> Subject:
        """request/reply subject for user identity lookup against hub.

        :return: subject ``{ns}.hub.user.resolve``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.user.resolve", kind="point")

    @classmethod
    def hub_object_commit(cls) -> Subject:
        """request/reply subject for committing a produced object to the hub catalog.

        The agent sends its produced-object handle here (Path-2); the hub verifies
        the agent's session, derives the owning customer server-side, asserts the
        object key is under that tenant, and writes the ``platform.objects`` row.

        :return: subject ``{ns}.hub.object.commit``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.object.commit", kind="point")

    @classmethod
    def hub_object_resolve(cls) -> Subject:
        """request/reply subject for resolving an object id to its stored key.

        The consuming agent asks the hub to resolve an ``object_id`` to its
        ``s3_key`` (Path-2 S0 keystone); the hub verifies the agent's session,
        derives the owning customer, and returns the key only when the row is
        owned by that tenant (fail-closed).

        :return: subject ``{ns}.hub.object.resolve``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.object.resolve", kind="point")

    @classmethod
    def hub_engagement_scope(cls) -> Subject:
        """request/reply subject for resolving an engagement's authorized target scope.

        A consuming TOOL POD asks the hub for the in-scope targets of the
        ``engagement_id`` on its call context (engagement scope, consumer A of
        the §2 keystone). The pod has no hub session of its own, so it forwards
        the invoking agent's ``identity_token``; the hub verifies it, derives the
        owning customer from the signed claim, and returns the active target set
        only when the engagement is owned by that tenant (fail-closed). The
        read-side twin of :meth:`hub_object_resolve`.

        :return: subject ``{ns}.hub.engagement.scope``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.engagement.scope", kind="point")

    @classmethod
    def hub_approval_record(cls) -> Subject:
        """request/reply subject for recording a pending human-approval marker.

        A consuming AGENT that has PAUSED a graph on a ``requires_confirmation``
        tool call asks the hub to record a pending-approval marker for the
        conversation (the hub half of the exploit HITL gate). The agent forwards
        its ``identity_token``; the hub verifies it, derives the owning customer,
        writes the marker, and audits the request. Mirrors the auth path of
        :meth:`hub_engagement_scope`.

        :return: subject ``{ns}.hub.approval.record``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.approval.record", kind="point")

    @classmethod
    def hub_approval_resolve(cls) -> Subject:
        """request/reply subject for resolving an operator reply against a pending approval.

        The channel router (sandboxed, NATS-only) forwards an inbound operator
        reply for a conversation that may be awaiting approval. The hub looks up
        the pending marker, parses approve/deny, authorizes the replier
        (``exploit.approve`` on the paused agent's namespace), audits the
        decision, clears the marker, and returns the resume directive to inject
        onto the agent's resume rail. Keeps DB + ACL + audit HUB-side, out of the
        adapter sandbox.

        :return: subject ``{ns}.hub.approval.resolve``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.approval.resolve", kind="point")

    @classmethod
    def hub_channel_installs(cls) -> Subject:
        """request/reply subject for a channel adapter to fetch its installs.

        the adapter is sandboxed (NATS-only, no DB credentials), so it
        asks the hub for the active bot installs of a channel type
        (bot token refs + the agent each routes to) instead of reading
        ``platform.channel_configs`` directly.

        :return: subject ``{ns}.hub.channel.installs``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.channel.installs", kind="point")

    @classmethod
    def channels_deliver(cls, channel_type: str) -> Subject:
        """durable JetStream subject for an agent answer awaiting channel delivery.

        the agent publishes a finished answer here (with the channel routing
        lifted off the inbound message) on completion; the channel adapter is a
        durable consumer that posts it to the destination thread. durable so an
        answer that completes while the adapter is restarting is redelivered,
        never lost. backed by the ``{ns}_channels_deliver`` JetStream stream
        over ``{ns}.channels.deliver.*``.

        :param channel_type: channel family (e.g. ``slack``, ``discord``)
        :ptype channel_type: str
        :return: subject ``{ns}.channels.deliver.{channel_type}``
        :rtype: Subject
        """
        return Subject(
            path=f"{_ns()}.channels.deliver.{_sanitize(channel_type)}",
            kind="point",
        )

    @classmethod
    def channels_deliver_wildcard(cls) -> Subject:
        """wildcard subject covering every channel-delivery family.

        the JetStream ``{ns}_channels_deliver`` stream is declared over this
        pattern; durable consumers filter to one ``channel_type``.

        :return: subject ``{ns}.channels.deliver.*``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.channels.deliver.*", kind="pattern")

    @classmethod
    def hub_usage_track(cls) -> Subject:
        """publish subject for usage-tracking events posted to hub.

        :return: subject ``{ns}.hub.usage.track``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.usage.track", kind="point")

    @classmethod
    def hub_endpoint_usage_track(cls) -> Subject:
        """publish subject for per-tool-call endpoint usage events posted to hub.

        distinct from :meth:`hub_usage_track` (the gateway model-usage stream) so
        the two usage streams / queue groups never collide: the registry usage
        emitter (gu-task-16) publishes here and the hub ``EndpointUsageConsumer``
        subscribes.

        :return: subject ``{ns}.hub.usage.endpoint``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.hub.usage.endpoint", kind="point")

    @classmethod
    def hub_stream(cls, agent_id: str | UUID, correlation_id: str | UUID) -> Subject:
        """publish subject for agent -> hub token streaming.

        The ``agent_id`` leads the subject as the AUTHENTICATED segment so
        an agent publishes tokens only under its OWN identity; a bare
        ``hub.stream.*`` publish grant would let a malicious agent forge
        or inject tokens onto a peer's in-flight request.

        :param agent_id: authenticated agent publishing the stream
        :ptype agent_id: str | UUID
        :param correlation_id: correlation identifier for client request
        :ptype correlation_id: str | UUID
        :return: subject ``{ns}.hub.stream.{agent_id}.{correlation_id}``
        :rtype: Subject
        """
        return Subject(
            path=f"{_ns()}.hub.stream.{_sanitize(agent_id)}.{_sanitize(correlation_id)}",
            kind="point",
        )

    # ------------------------------------------------------------------
    # audit
    # ------------------------------------------------------------------

    @classmethod
    def audit_event(cls, event_type: str) -> Subject:
        """publish subject for one audit event.

        :param event_type: dotted event type (e.g. ``workspace.doc_set``)
        :ptype event_type: str
        :return: subject ``{ns}.audit.{event_type}`` (event_type passed through verbatim — its dots are part of the addressable subject hierarchy)
        :rtype: Subject
        :raises ValueError: if event_type is empty
        """
        if not event_type:
            raise ValueError("event_type must be non-empty")
        # NOTE: event_type intentionally NOT sanitized — its dots are
        # the namespace separators audit consumers subscribe against
        # (e.g. wildcard `3tears.audit.workspace.>` for workspace events).
        return Subject(path=f"{_ns()}.audit.{event_type}", kind="point")

    @classmethod
    def audit_wildcard(cls, *, area: str | None = None) -> Subject:
        """wildcard subscribe pattern for audit events.

        :param area: optional sub-area filter; when ``None`` matches every audit event
        :ptype area: str | None
        :return: subject ``{ns}.audit.>`` or ``{ns}.audit.{area}.>``
        :rtype: Subject
        """
        result: Subject
        if area is None:
            result = Subject(path=f"{_ns()}.audit.>", kind="pattern")
        else:
            result = Subject(path=f"{_ns()}.audit.{area}.>", kind="pattern")
        return result

    @classmethod
    def audit_deadletter(cls) -> Subject:
        """dead-letter subject for audit envelopes the consumer exhausted.

        an audit envelope that stays un-persistable after the durable
        consumer's bounded redelivery budget is republished here. a SIBLING
        token of ``audit`` (``audit-deadletter``, NOT ``{ns}.audit.*``) so
        :meth:`audit_wildcard` does NOT match it -- the poisoned envelope parks
        in the ``{ns}-audit`` stream (which is declared over this subject too)
        with no consumer draining it: inspectable, not lost, not looping.

        :return: subject ``{ns}.audit-deadletter``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.audit-deadletter", kind="point")

    # ------------------------------------------------------------------
    # workspaces
    # ------------------------------------------------------------------

    @classmethod
    def workspaces_create(cls) -> Subject:
        """publish subject for workspace-create namespace emission.

        every successful agent-side ``threetears.workspace.create``
        publishes one event on this subject after the workspace + file
        rows commit in the per-agent schema. the hub-side
        :class:`3tears.hub.workspace.namespace_emitter
        .WorkspaceNamespaceEmitter` subscribes (no queue group, every
        replica observes) and upserts the paired ``platform.namespaces``
        row of type ``workspace``.

        decoupling the namespace upsert from the agent is the canonical
        platform pattern -- the agent-side L3 proxy routes writes to
        the agent's own ``agent_<hex>`` schema, which has no
        ``namespaces`` table; the hub owns direct DB access via
        :class:`HubNamespaceCollection` and is the SOLE writer of
        platform-scoped catalog rows. mirrors :meth:`tools_register`.

        :return: subject ``{ns}.workspaces.create``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.workspaces.create", kind="point")

    # ------------------------------------------------------------------
    # op-log (durable write-path WAL)
    # ------------------------------------------------------------------

    @classmethod
    def oplog(cls, repo: str, branch: str) -> Subject:
        """publish/replay subject for one ``(repo, branch)`` op-log.

        the durable write path keeps one JetStream stream per
        ``(repo, branch)``; this is the single subject that stream is
        bound to. ``repo`` and ``branch`` are sanitized (``.`` -> ``-``)
        so a dotted ref name never overloads the subject separator.
        consumed by :class:`threetears.nats.OpLog`.

        :param repo: repository identifier
        :ptype repo: str
        :param branch: branch / ref name
        :ptype branch: str
        :return: subject ``{ns}.oplog.{repo}.{branch}``
        :rtype: Subject
        :raises ValueError: if repo or branch is empty
        """
        if not repo:
            raise ValueError("repo must be non-empty")
        if not branch:
            raise ValueError("branch must be non-empty")
        return Subject(
            path=f"{_ns()}.oplog.{_sanitize(repo)}.{_sanitize(branch)}",
            kind="point",
        )

    # ------------------------------------------------------------------
    # channels (cross-pod room fanout)
    # ------------------------------------------------------------------

    @classmethod
    def room(cls, room_id: str) -> Subject:
        """publish/subscribe subject for one room's cross-pod message fanout.

        the live room fanout (channels cross-pod design D1 / shard B)
        publishes one frame to this subject; every pod holding a local
        member of the room subscribes and delivers to its own sockets.

        the room id is ``{customer}:{story}:{branch}:{file}`` — arbitrary
        app-supplied segments may carry ``.``, spaces, ``*``, ``>`` (all
        illegal or ambiguous in a NATS subject token), so the room id is
        NOT :func:`_sanitize`-mapped (a colon/space room id would still
        leave illegal characters or collide across distinct ids). instead
        the token is the **SHA-256 hex digest** of the room id: a
        subject-safe (``[0-9a-f]`` only), collision-resistant,
        deterministic token. the raw room id rides in the wire envelope
        (:class:`threetears.channels.presence.wire.RoomFrame`), so the
        one-way digest needs no reversibility — the same robustness move
        the presence KV key uses. consumed by
        :class:`threetears.channels.presence.fanout.RoomFanout`.

        :param room_id: ``{customer}:{story}:{branch}:{file}`` room key
        :ptype room_id: str
        :return: subject ``{ns}.channels.room.{sha256hex(room_id)}``
        :rtype: Subject
        :raises ValueError: if room_id is empty
        """
        if not room_id:
            raise ValueError("room_id must be non-empty")
        token = hashlib.sha256(room_id.encode("utf-8")).hexdigest()
        return Subject(path=f"{_ns()}.channels.room.{token}", kind="point")

    # ------------------------------------------------------------------
    # owner-routed forward (request -> whichever pod serves a key)
    # ------------------------------------------------------------------

    @classmethod
    def forward(cls, key: str) -> Subject:
        """request/reply subject for owner-routed forwarding of one key.

        the owner-routed forward primitive
        (:func:`threetears.nats.forward` / :func:`serve_owner`) sends a
        request to whichever pod currently *serves* ``key`` and returns
        its reply. this is the single subject that request/reply rides.

        the ``key`` is arbitrary, app-supplied, and may carry ``.``,
        spaces, ``*`` or ``>`` — all illegal or ambiguous in a NATS
        subject token. as with :meth:`room`, the key is therefore NOT
        :func:`_sanitize`-mapped (which only handles ``.``); instead the
        token is the **SHA-256 hex digest** of the key: subject-safe
        (``[0-9a-f]`` only), collision-resistant, and deterministic, so
        every pod derives the same subject for the same key. the digest
        is one-way and needs no reversibility — both ends start from the
        same ``key``. consumed by
        :func:`threetears.nats.serve_owner` (subscribe, queue-grouped on
        this subject's path) and :func:`threetears.nats.forward`
        (request).

        :param key: ownership key (arbitrary app-supplied string)
        :ptype key: str
        :return: subject ``{ns}.forward.{sha256hex(key)}``
        :rtype: Subject
        :raises ValueError: if key is empty
        """
        if not key:
            raise ValueError("key must be non-empty")
        token = hashlib.sha256(key.encode("utf-8")).hexdigest()
        return Subject(path=f"{_ns()}.forward.{token}", kind="point")

    # knowledge (correction-harvest drafts)
    # ------------------------------------------------------------------

    @classmethod
    def knowledge_draft(cls) -> Subject:
        """publish subject for correction-harvest draft events + commands.

        every agent-side correction harvest (knowledge-task-06) publishes
        one :class:`threetears.knowledge.KnowledgeDraftEvent` (new draft)
        or :class:`threetears.knowledge.KnowledgeDraftCommand` (confirm /
        edit / discard of the author's own draft) on this subject. the
        hub-side ``KnowledgeDraftEmitter`` subscribes (no queue group,
        every replica observes) and materializes / mutates the
        ``status='draft'`` row through the hub's knowledge Collections.

        decoupling the knowledge write from the agent is the canonical
        platform pattern (mirrors :meth:`workspaces_create`): the
        agent-side L3 proxy is admitted only SELECT traffic against the
        ``platform.*`` knowledge tables (the broker's read-only
        carve-out), so the hub — sole writer of platform-scoped rows —
        owns the write. the deterministic ``draft_id`` keying makes the
        upsert idempotent under at-least-once delivery.

        :return: subject ``{ns}.knowledge.draft``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.knowledge.draft", kind="point")

    # ------------------------------------------------------------------
    # l3 broker
    # ------------------------------------------------------------------

    @classmethod
    def l3_query(cls) -> Subject:
        """request/reply subject for agent -> L3 broker single-statement query.

        :return: subject ``{ns}.l3.query``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.l3.query", kind="point")

    @classmethod
    def l3_batch(cls) -> Subject:
        """request/reply subject for agent -> L3 broker transactional batch.

        :return: subject ``{ns}.l3.batch``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.l3.batch", kind="point")

    @classmethod
    def l3_tx(
        cls,
        op: Literal["begin", "execute", "fetchrow", "fetch", "commit", "rollback"],
    ) -> Subject:
        """request/reply subject for L3 broker explicit-transaction operations.

        the platform broker exposes six per-op subjects (versus the
        original four-method shape) so DML ``execute``, single-row
        ``fetchrow``, and multi-row ``fetch`` are addressable
        independently from a generic ``exec``. the asyncpg session
        on the broker side dispatches each op to the matching
        connection method without pre-classifying the SQL verb.

        :param op: transaction operation phase
        :ptype op: Literal["begin", "execute", "fetchrow", "fetch", "commit", "rollback"]
        :return: subject ``{ns}.l3.tx.{op}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.l3.tx.{op}", kind="point")

    # ------------------------------------------------------------------
    # acl
    # ------------------------------------------------------------------

    @classmethod
    def acl_invalidate(
        cls,
        kind: Literal["membership", "assignment", "role"],
    ) -> Subject:
        """publish subject for ACL cache invalidation broadcasts.

        :param kind: which ACL surface to invalidate
        :ptype kind: Literal["membership", "assignment", "role"]
        :return: subject ``{ns}.acl.{kind}.invalidate``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.acl.{kind}.invalidate", kind="point")

    # ------------------------------------------------------------------
    # namespace
    # ------------------------------------------------------------------

    @classmethod
    def namespace_discover(cls) -> Subject:
        """request/reply subject for agent -> hub namespace catalog discovery.

        :return: subject ``{ns}.namespace.discover``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.namespace.discover", kind="point")

    # ------------------------------------------------------------------
    # datasource
    # ------------------------------------------------------------------

    @classmethod
    def datasource_query(cls, name: str) -> Subject:
        """request/reply subject for datasource query routing.

        :param name: datasource name
        :ptype name: str
        :return: subject ``{ns}.datasource.{name}.query``
        :rtype: Subject
        :raises ValueError: if name is empty
        """
        if not name:
            raise ValueError("datasource name must be non-empty")
        return Subject(path=f"{_ns()}.datasource.{_sanitize(name)}.query", kind="point")

    # ------------------------------------------------------------------
    # cache invalidation
    # ------------------------------------------------------------------

    @classmethod
    def cache_invalidate(cls) -> Subject:
        """publish subject for cross-pod cache invalidation broadcasts.

        the cache invalidation subject is NOT namespace-prefixed: it is
        a cross-platform constant (``threetears.cache.invalidate``) so
        that every 3tears collection in every consumer process listens
        on the same subject regardless of the env-specific 3tears
        prefix.

        :return: subject ``threetears.cache.invalidate``
        :rtype: Subject
        """
        return Subject(path="threetears.cache.invalidate", kind="point")

    # ------------------------------------------------------------------
    # config epochs (cross-pod cache reload coherence)
    # ------------------------------------------------------------------
    #
    # epoch subjects live in the publishing product's namespace --
    # subscribers are always sibling pods of the same product, never
    # cross-product, so the standard namespace-prefixed shape applies.
    # multi-env deployments on a shared NATS cluster get partitioned by
    # the prefix.
    #
    # the path shape is asymmetric between single-product builders and
    # 3tears-family builders BY DESIGN: a single-product namespace has
    # only one product per namespace, so its subjects have nothing after
    # the namespace to disambiguate against (``<ns>.capabilities.epoch``).
    # the 3tears namespace multiplexes the broker / catalog / agents /
    # tools / channels / MCP under one prefix, so 3tears subjects always
    # include a product segment as the second token
    # (``3tears.gateway.catalog.epoch``). the asymmetry mirrors the
    # underlying namespace shape; do not "normalize" it. consumed by
    # :class:`threetears.epoch.client.EpochClient` and
    # :class:`threetears.epoch.listener.EpochListener`.

    @classmethod
    def capabilities_epoch(cls) -> Subject:
        """publish + subscribe subject for the capabilities-registry epoch.

        bumped by an admin POST/PATCH on the ``models`` table
        after the in-process ``register_model_capabilities_bulk(...)``
        call. sibling pods subscribe to reload their local
        :class:`threetears.models.tracking.ModelCapabilities` registry
        from the row in the next read. intended call site: a
        process bound to a single-product namespace.

        constraint (single-product-per-namespace): the path has no
        product segment after ``{ns}`` because the namespace is
        single-product. if a second product ever joins the namespace,
        this subject must be renamed to add a product segment
        (``{ns}.<product>.capabilities.epoch``) and every subscriber
        must roll forward together -- a coordinated wire-protocol
        break, not a hot-deploy change. the constraint is documented
        here at the call site (not just in the section header) so
        the implication is visible from the builder's own docstring.

        :return: subject ``{ns}.capabilities.epoch``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.capabilities.epoch", kind="point")

    @classmethod
    def gateway_catalog_epoch(cls) -> Subject:
        """publish + subscribe subject for 3tears-gateway catalog epoch.

        bumped by hub admin endpoints that mutate ``gateway_models``,
        ``gateway_providers``, or ``gateway_credit_rates``. gateway
        sibling pods subscribe to re-run ``_load_catalog`` immediately
        rather than waiting for the next ``cache_ttl_seconds`` tick.
        intended call site: 3tears-bound process (hub or gateway).

        :return: subject ``{ns}.gateway.catalog.epoch`` (3tears-bound:
            ``3tears.gateway.catalog.epoch``)
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.gateway.catalog.epoch", kind="point")

    @classmethod
    def mcp_rbac_epoch(cls) -> Subject:
        """publish + subscribe subject for MCP per-tool RBAC epoch.

        bumped by MCP admin endpoints that grant or revoke per-tool
        access. MCP-host sibling pods subscribe to reload their RBAC
        view from the row before the next tool-call authorization
        check, so a revoked grant cannot ride a stale cache forward.
        intended call site: 3tears-bound MCP-host process.

        :return: subject ``{ns}.mcp.rbac.epoch`` (3tears-bound:
            ``3tears.mcp.rbac.epoch``)
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.mcp.rbac.epoch", kind="point")

    # ------------------------------------------------------------------
    # deadletter
    # ------------------------------------------------------------------

    @classmethod
    def deadletter(cls, original_path: str) -> Subject:
        """publish subject for deadlettering a failed callback's message.

        produced by :class:`NatsClient` when a subscribe callback raises
        and the subscription has ``deadletter_on_failure=True`` (default).

        :param original_path: dotted path of subject the failed message arrived on
        :ptype original_path: str
        :return: subject ``{ns}.deadletter.{original_path}``
        :rtype: Subject
        """
        return Subject(path=f"{_ns()}.deadletter.{original_path}", kind="point")
