Skip to content

Python API

All entry points below are exported from axm_doctor. Directives target defining modules so lazy exports remain statically resolvable. Start with the verified narrative contracts, which qualify broader claims in implementation docstrings.

Detection

detect

Stdlib-only detection of external tools and third-party auth state.

Tool/auth probing (:func:detect_tool, :func:detect_auth) depends on the standard library and pydantic only — no AXM package is imported at module load, so this layer runs as the bootstrap probe before the rest of AXM is installable. The git-identity check (:func:detect_git_identity) additionally resolves the central axm-config store ([git].default) to know whether a committer identity exists; that import is deferred to the function body so the module stays importable on a machine where axm-config is not yet present. The value is never read, only its presence. All detection is strictly read-only: it inspects an exit code or the existence of a credential/store entry, and never reads the token or identity value.

ToolState = Literal['present', 'absent']

AuthState = Literal['logged_in', 'logged_out', 'not_installed', 'undetermined']

GitIdentityState = Literal['configured', 'unconfigured']

GhConfigState = Literal['configured', 'unconfigured', 'not_installed']

ToolStatus

Bases: BaseModel

Frozen result of probing a single external tool on PATH.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class ToolStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen result of probing a single external tool on ``PATH``."""

    name: str
    state: ToolState
    version: str | None = None
    path: str | None = None

AuthStatus

Bases: BaseModel

Frozen read-only auth state for a third-party binary.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class AuthStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen read-only auth state for a third-party binary."""

    tool: str
    state: AuthState
    login_cmd: str | None = None
    declaration_consulted: bool = Field(
        default=False,
        description="a declaration was discovered and consulted to produce this state",
    )

GitIdentityStatus

Bases: BaseModel

Frozen verdict on whether a git committer identity is resolvable.

state is decided from the presence of a [git].default store entry or the exit code of git config --get user.email — the identity value itself is never read.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class GitIdentityStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen verdict on whether a git committer identity is resolvable.

    ``state`` is decided from the *presence* of a ``[git].default`` store entry
    or the exit code of ``git config --get user.email`` — the identity value
    itself is never read.
    """

    state: GitIdentityState

GhConfigStatus

Bases: BaseModel

Frozen verdict on whether gh carries a base configuration.

configured when gh config get git_protocol exits 0, unconfigured otherwise, not_installed when the gh binary is absent. The config value itself is never read.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
class GhConfigStatus(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Frozen verdict on whether ``gh`` carries a base configuration.

    ``configured`` when ``gh config get git_protocol`` exits 0, ``unconfigured``
    otherwise, ``not_installed`` when the ``gh`` binary is absent. The config
    value itself is never read.
    """

    state: GhConfigState

detect_tool(name)

Probe name on PATH and parse <name> --version.

Returns present with the parsed version string when found, absent otherwise. Never raises on a missing or misbehaving tool.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_tool(name: str) -> ToolStatus:
    """Probe ``name`` on ``PATH`` and parse ``<name> --version``.

    Returns ``present`` with the parsed version string when found, ``absent``
    otherwise. Never raises on a missing or misbehaving tool.
    """
    path = shutil.which(name)
    if path is None:
        return ToolStatus(name=name, state="absent")
    return ToolStatus(
        name=name,
        state="present",
        version=_probe_version(name),
        path=path,
    )

detect_auth(tool)

Report auth state through a package declaration when one is installed.

A tool without a declaration degrades to presence detection: an installed binary has an undetermined auth state because its session cannot be verified.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_auth(tool: str) -> AuthStatus:
    """Report auth state through a package declaration when one is installed.

    A tool without a declaration degrades to presence detection: an installed
    binary has an undetermined auth state because its session cannot be verified.
    """
    declaration = load_auth_declarations().get(tool)
    if declaration is not None:
        declared_state = _detect_declared_auth(declaration)
        return AuthStatus(
            tool=tool,
            state=declared_state,
            login_cmd=(
                declaration.login_command if declared_state == "logged_out" else None
            ),
            declaration_consulted=True,
        )

    state: AuthState = (
        "undetermined" if shutil.which(tool) is not None else "not_installed"
    )
    return AuthStatus(
        tool=tool,
        state=state,
        declaration_consulted=False,
    )

detect_git_identity()

Report whether a git committer identity is resolvable, value-free.

Cheapest source first: a truthy [git].default in the axm-config store means an identity exists. Otherwise fall back to the exit code of git config --get user.email (its stdout — the email — is captured and discarded, never returned). Any missing binary / OSError / SubprocessError degrades to unconfigured without raising.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_git_identity() -> GitIdentityStatus:
    """Report whether a git committer identity is resolvable, value-free.

    Cheapest source first: a truthy ``[git].default`` in the ``axm-config``
    store means an identity exists. Otherwise fall back to the exit code of
    ``git config --get user.email`` (its stdout — the email — is captured and
    discarded, never returned). Any missing binary / ``OSError`` /
    ``SubprocessError`` degrades to ``unconfigured`` without raising.
    """
    import axm_config

    if axm_config.get("git", "default", default=None):
        return GitIdentityStatus(state="configured")
    if shutil.which("git") is None:
        return GitIdentityStatus(state="unconfigured")
    try:
        proc = subprocess.run(
            ["git", "config", "--get", "user.email"],  # noqa: S607 - controlled binary
            capture_output=True,
            text=True,
            timeout=_VERSION_TIMEOUT_S,
            check=False,
        )
    except (OSError, subprocess.SubprocessError):
        return GitIdentityStatus(state="unconfigured")
    state: GitIdentityState = "configured" if proc.returncode == 0 else "unconfigured"
    return GitIdentityStatus(state=state)

detect_gh_config()

Report whether gh carries a base config, value-free.

Probes the exit code of gh config get git_protocol (its stdout is captured and discarded). gh absent → not_installed; any OSError / SubprocessError degrades to unconfigured. This is distinct from and additional to the gh auth status login check.

Source code in packages/axm-doctor/src/axm_doctor/detect.py
Python
def detect_gh_config() -> GhConfigStatus:
    """Report whether ``gh`` carries a base config, value-free.

    Probes the exit code of ``gh config get git_protocol`` (its stdout is
    captured and discarded). ``gh`` absent → ``not_installed``; any
    ``OSError`` / ``SubprocessError`` degrades to ``unconfigured``. This is
    distinct from and additional to the ``gh auth status`` login check.
    """
    if shutil.which("gh") is None:
        return GhConfigStatus(state="not_installed")
    try:
        proc = subprocess.run(
            ["gh", "config", "get", "git_protocol"],  # noqa: S607 - controlled binary
            capture_output=True,
            text=True,
            timeout=_VERSION_TIMEOUT_S,
            check=False,
        )
    except (OSError, subprocess.SubprocessError):
        return GhConfigStatus(state="unconfigured")
    state: GhConfigState = "configured" if proc.returncode == 0 else "unconfigured"
    return GhConfigStatus(state=state)

Provenance

credentials

Value-free credential provenance collected from axm-vault.

CredentialProvenance

Bases: BaseModel

The serving layer and presence of one credential coordinate.

Source code in packages/axm-doctor/src/axm_doctor/credentials.py
Python
class CredentialProvenance(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """The serving layer and presence of one credential coordinate."""

    coordinate: str
    kind: str = "credential"
    layer: str
    present: bool

collect_credential_provenance(*, probe=None)

Translate vault provenance into typed rows without carrying values.

Source code in packages/axm-doctor/src/axm_doctor/credentials.py
Python
def collect_credential_provenance(
    *, probe: ProvenanceProbe | None = None
) -> list[CredentialProvenance]:
    """Translate vault provenance into typed rows without carrying values."""
    catalog: Catalog | None = None
    provenance: Mapping[str, ProvenanceValue]
    declared_kinds: dict[str, str] = {}
    if probe is None:
        catalog = load_catalog()
        provenance = doctor_data(catalog=catalog)
        declared_kinds = _catalog_kinds(catalog)
    else:
        provenance = probe()
    rows: list[CredentialProvenance] = []
    for coordinate, entry in provenance.items():
        kind = declared_kinds.get(coordinate, "unknown")
        try:
            if kind == "unknown":
                kind = _entry_kind(entry)
            layer, present = _entry_fields(entry)
        except Exception:  # noqa: BLE001 # isolate one malformed declaration
            layer = "unknown"
            present = False
        rows.append(
            CredentialProvenance(
                coordinate=coordinate,
                kind=kind,
                layer=layer,
                present=False if layer == "missing" else present,
            )
        )
    if catalog is not None:
        rows.extend(_auth_dependency_rows(catalog))
    return rows

Installation

install

Install plans: propose an official install command, run only on confirm.

This module NEVER installs silently. install_command only describes the official command for a known tool; run_install executes it strictly when the caller opts in with confirm=True. The default path is a dry-run that echoes the command it would run — honouring the no-system-install-without-authorization posture.

InstallPlan

Bases: BaseModel

A proposed install command for a single tool — description only.

Building a plan runs nothing. argv is the program + arguments executed as a bare exec (NO shell). fetch_url, when set, marks a script-installer plan (e.g. the uv curl | sh recipe): instead of a shell pipe, the script is downloaded to a temp file and run as sh <tmpfile> — both steps are argv execs, never shell=True. human_command is the copy-pasteable form a human would type.

Source code in packages/axm-doctor/src/axm_doctor/install.py
Python
class InstallPlan(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """A proposed install command for a single tool — description only.

    Building a plan runs nothing. ``argv`` is the program + arguments executed
    as a bare exec (NO shell). ``fetch_url``, when set, marks a script-installer
    plan (e.g. the uv ``curl | sh`` recipe): instead of a shell pipe, the script
    is downloaded to a temp file and run as ``sh <tmpfile>`` — both steps are
    argv execs, never ``shell=True``. ``human_command`` is the copy-pasteable
    form a human would type.
    """

    tool: str
    argv: list[str]
    human_command: str
    fetch_url: str | None = None

InstallResult

Bases: BaseModel

Outcome of :func:run_install.

On a dry-run (confirm=False) executed is False, returncode is None, and post_check is None — nothing was installed, so nothing is re-detected. On a confirmed run, post_check carries the re-detected :class:~axm_doctor.detect.ToolStatus.

Source code in packages/axm-doctor/src/axm_doctor/install.py
Python
class InstallResult(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Outcome of :func:`run_install`.

    On a dry-run (``confirm=False``) ``executed`` is False, ``returncode`` is
    None, and ``post_check`` is None — nothing was installed, so nothing is
    re-detected. On a confirmed run, ``post_check`` carries the re-detected
    :class:`~axm_doctor.detect.ToolStatus`.
    """

    command: str
    executed: bool
    returncode: int | None = None
    post_check: ToolStatus | None = None

install_command(tool)

Return the official :class:InstallPlan for tool, or None.

Runs nothing. An unknown tool returns None rather than guessing a command.

Source code in packages/axm-doctor/src/axm_doctor/install.py
Python
def install_command(tool: str) -> InstallPlan | None:
    """Return the official :class:`InstallPlan` for ``tool``, or None.

    Runs nothing. An unknown tool returns None rather than guessing a
    command.
    """
    return _REGISTRY.get(tool)

run_install(plan, *, confirm=False)

Execute plan only when confirm is True; otherwise dry-run.

With the default confirm=False this NEVER installs: it returns the command it would run with executed=False and returncode=None. With confirm=True it runs the command, then re-detects the tool via :func:~axm_doctor.detect.detect_tool and reports the post-install state.

Source code in packages/axm-doctor/src/axm_doctor/install.py
Python
def run_install(plan: InstallPlan, *, confirm: bool = False) -> InstallResult:
    """Execute ``plan`` only when ``confirm is True``; otherwise dry-run.

    With the default ``confirm=False`` this NEVER installs: it returns the
    command it *would* run with ``executed=False`` and ``returncode=None``.
    With ``confirm=True`` it runs the command, then re-detects the tool via
    :func:`~axm_doctor.detect.detect_tool` and reports the post-install state.
    """
    if not confirm:
        return InstallResult(command=plan.human_command, executed=False)

    returncode = _run_fetch_install(plan) if plan.fetch_url else _run_argv(plan.argv)
    return InstallResult(
        command=plan.human_command,
        executed=True,
        returncode=returncode,
        post_check=detect_tool(plan.tool),
    )

Provisioning

orchestrate

Orchestrate missing-secret detection on top of axm-vault.

This module ORCHESTRATES; it never POSSESSES a secret. :func:missing_secrets reads the vault catalog (:func:axm_vault.load_catalog) and the value-free resolver provenance (:func:axm_vault.doctor.doctor_data) to surface the specs that resolve to "missing" — without ever reading a secret value. :func:provision_missing delegates to vault's :func:axm_vault.setup.run_setup only on confirmation; doctor never writes a secret itself (every write goes through vault's API — the SRP invariant).

Unlike :mod:axm_doctor.detect (bootstrap-sensitive, no AXM import), this is the orchestration seam, so it depends on axm-vault directly.

MissingSecret

Bases: BaseModel

A credential spec that resolves to "missing" across every layer.

Value-less by construction: it carries only the coordinates of the spec and a copy-pasteable recovery hint (setup_hint). required preserves the catalog distinction between indispensable and optional credentials (the catalog default is True). instance identifies the account concerned, while awaiting_instance marks a multi-instance group that declares no account yet. The secret value itself NEVER transits axm_doctor.

Source code in packages/axm-doctor/src/axm_doctor/orchestrate.py
Python
class MissingSecret(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """A credential spec that resolves to ``"missing"`` across every layer.

    Value-less by construction: it carries only the coordinates of the spec
    and a copy-pasteable recovery hint (``setup_hint``). ``required``
    preserves the catalog distinction between indispensable and optional
    credentials (the catalog default is ``True``). ``instance`` identifies
    the account concerned, while ``awaiting_instance`` marks a multi-instance
    group that declares no account yet. The secret value itself NEVER transits
    axm_doctor.
    """

    group: str
    name: str
    package: str
    setup_hint: str
    required: bool
    instance: str | None = None
    awaiting_instance: bool = False

ProvisionResult

Bases: BaseModel

Outcome of :func:provision_missing.

On a dry-run (confirm=False) provisioned is False and groups lists the groups it WOULD prompt for. On a confirmed run provisioned is True ONLY when a post-setup re-scan confirms every previously-missing spec now resolves — delegating to vault's setup driver is not proof the user actually supplied the secrets (they may skip/empty the prompts). still_missing lists the specs that remain unresolved after the run (always empty on a dry-run), so a partial provisioning is reported truthfully rather than as a false green.

Source code in packages/axm-doctor/src/axm_doctor/orchestrate.py
Python
class ProvisionResult(BaseModel, frozen=True):  # type: ignore[explicit-any]
    """Outcome of :func:`provision_missing`.

    On a dry-run (``confirm=False``) ``provisioned`` is False and ``groups``
    lists the groups it WOULD prompt for. On a confirmed run ``provisioned``
    is True ONLY when a post-setup re-scan confirms every previously-missing
    spec now resolves — delegating to vault's setup driver is not proof the
    user actually supplied the secrets (they may skip/empty the prompts).
    ``still_missing`` lists the specs that remain unresolved after the run
    (always empty on a dry-run), so a partial provisioning is reported truthfully
    rather than as a false green.
    """

    provisioned: bool
    groups: list[str]
    still_missing: list[str] = []
    reason: str | None = None

missing_secrets()

Return the catalog specs that resolve to "missing", value-free.

Reads the vault catalog and the value-free provenance report; a spec is reported when no resolver layer supplies it. An empty catalog (the nominal state for vault today) yields [] gracefully.

Source code in packages/axm-doctor/src/axm_doctor/orchestrate.py
Python
def missing_secrets() -> list[MissingSecret]:
    """Return the catalog specs that resolve to ``"missing"``, value-free.

    Reads the vault catalog and the value-free provenance report; a spec is
    reported when no resolver layer supplies it. An empty catalog (the
    nominal state for vault today) yields ``[]`` gracefully.
    """
    catalog = load_catalog()
    provenance = doctor_data(catalog=catalog)
    missing: list[MissingSecret] = []
    for group in catalog.groups():
        if group.multi and group.instances is not None:
            declared_instances = tuple(group.instances.list_instances())
            awaiting_instance = not declared_instances
            instances: tuple[str | None, ...] = declared_instances or (None,)
        else:
            awaiting_instance = False
            instances = (None,)

        for spec in group.specs:
            if spec.kind == "auth_dependency":
                continue
            for instance in instances:
                if _is_served(provenance, group.id, spec.name, instance):
                    continue
                missing.append(
                    MissingSecret(
                        group=group.id,
                        name=spec.name,
                        package=group.package,
                        setup_hint=f"axm-vault set {group.id} {spec.name}",
                        required=spec.required,
                        instance=instance,
                        awaiting_instance=awaiting_instance,
                    )
                )
    return missing

provision_missing(*, confirm=False)

Plan (and on confirm execute) provisioning of missing secrets.

Collects the distinct groups owning at least one missing spec. With confirm=False it returns the plan without prompting or storing. With confirm=True it delegates to vault's :func:run_setup (one call per group, restricted via only=); doctor never stores a secret itself.

Source code in packages/axm-doctor/src/axm_doctor/orchestrate.py
Python
def provision_missing(*, confirm: bool = False) -> ProvisionResult:
    """Plan (and on ``confirm`` execute) provisioning of missing secrets.

    Collects the distinct groups owning at least one missing spec. With
    ``confirm=False`` it returns the plan without prompting or storing. With
    ``confirm=True`` it delegates to vault's :func:`run_setup` (one call per
    group, restricted via ``only=``); doctor never stores a secret itself.
    """
    groups: list[str] = []
    for secret in missing_secrets():
        if secret.group not in groups:
            groups.append(secret.group)
    if confirm and not sys.stdin.isatty():
        return ProvisionResult(
            provisioned=False,
            groups=groups,
            reason="non-interactive shell: cannot prompt for secrets",
        )
    if not confirm:
        return ProvisionResult(provisioned=False, groups=groups)
    for group in groups:
        try:
            run_setup(only=group)
        except SystemExit as exc:  # vault's setup driver aborts via SystemExit
            return ProvisionResult(
                provisioned=False,
                groups=groups,
                reason=f"vault setup aborted for {group} (exit {exc.code})",
            )
    # Re-scan: delegating to run_setup is NOT proof a secret was supplied (the
    # user may skip/empty a prompt). Truth comes from re-resolving the catalog.
    still_missing = [f"{s.group}.{s.name}" for s in missing_secrets()]
    provisioned = bool(groups) and not still_missing
    reason = None if provisioned else "some secrets remain unresolved after setup"
    return ProvisionResult(
        provisioned=provisioned,
        groups=groups,
        still_missing=still_missing,
        reason=reason if still_missing else None,
    )

Tools

tools

AXM tools for the doctor — env_doctor and auth_status.

Both are deterministic :class:~axm.tools.base.AXMTool implementations, so they are reachable over MCP, the axm CLI and as DAG nodes from a single axm.tools entry-point declaration. They are strictly read-only: they wrap the central detect/orchestrate functions and never install anything.

They uphold the doctor's security invariant — mirror of vault_doctor — no tool ever serializes a token value. auth_status reports only the state and the recovery command (login_cmd); the credential value itself never transits axm_doctor.

EnvDoctorTool

Read-only env report: tool presence/version + auth + missing secrets.

Source code in packages/axm-doctor/src/axm_doctor/tools.py
Python
class EnvDoctorTool:
    """Read-only env report: tool presence/version + auth + missing secrets."""

    agent_hint = (
        "Read-only env doctor: report each external tool's presence/version, "
        "third-party auth state, and missing (value-free) secrets. Never installs."
    )
    domain = "doctor"
    tags = frozenset({"doctor", "env", "bootstrap", "detect"})

    @property
    def name(self) -> str:
        """Unique tool identifier."""
        return "env_doctor"

    def execute(self) -> ToolResult:
        """Return the full env report; any error becomes a failure ToolResult."""
        try:
            tools = {
                name: {"state": status.state, "version": status.version}
                for name in PROBED_TOOLS
                for status in (detect_tool(name),)
            }
            secrets = [secret.model_dump() for secret in missing_secrets()]
        except Exception as exc:  # noqa: BLE001 # MCP boundary: any error -> failure
            return ToolResult(success=False, error=str(exc))
        return ToolResult(
            success=True,
            data={
                "tools": tools,
                "auth": _auth_map(),
                "secrets": secrets,
                "config": _config_map(),
            },
        )
name property

Unique tool identifier.

execute()

Return the full env report; any error becomes a failure ToolResult.

Source code in packages/axm-doctor/src/axm_doctor/tools.py
Python
def execute(self) -> ToolResult:
    """Return the full env report; any error becomes a failure ToolResult."""
    try:
        tools = {
            name: {"state": status.state, "version": status.version}
            for name in PROBED_TOOLS
            for status in (detect_tool(name),)
        }
        secrets = [secret.model_dump() for secret in missing_secrets()]
    except Exception as exc:  # noqa: BLE001 # MCP boundary: any error -> failure
        return ToolResult(success=False, error=str(exc))
    return ToolResult(
        success=True,
        data={
            "tools": tools,
            "auth": _auth_map(),
            "secrets": secrets,
            "config": _config_map(),
        },
    )

AuthStatusTool

Report third-party auth state — never a token value (mirror of vault).

Source code in packages/axm-doctor/src/axm_doctor/tools.py
Python
class AuthStatusTool:
    """Report third-party auth state — never a token value (mirror of vault)."""

    agent_hint = (
        "Report third-party binary auth state as {tool: {state, login_cmd}}; "
        "the token value is NEVER returned."
    )
    domain = "doctor"
    tags = frozenset({"doctor", "auth", "login"})

    @property
    def name(self) -> str:
        """Unique tool identifier."""
        return "auth_status"

    def execute(self) -> ToolResult:
        """Return value-free auth state; any error becomes a failure ToolResult."""
        try:
            auth = _auth_map()
            provenance = _credentials_map(collect_credential_provenance())
            credentials = {
                coordinate: {
                    "layer": entry["layer"],
                    "present": entry["present"],
                }
                for coordinate, entry in provenance.items()
            }
            rejections = _rejection_rows()
        except Exception as exc:  # noqa: BLE001 # MCP boundary: any error -> failure
            return ToolResult(success=False, error=str(exc))
        auth_text = "\n".join(
            f"- {tool}: {entry['state']}"
            f"{' [no declaration]' if not entry['declaration_consulted'] else ''}"
            for tool, entry in auth.items()
        )
        return ToolResult(
            success=True,
            data={
                "auth": auth,
                "undetermined": [
                    tool
                    for tool, entry in auth.items()
                    if entry["state"] == "undetermined"
                ],
                "logged_out": [
                    tool
                    for tool, entry in auth.items()
                    if entry["state"] == "logged_out"
                ],
                "credentials": credentials,
                "rejections": rejections,
            },
            text=(
                f"Third-party auth:\n{auth_text}\n\n{_credentials_text(provenance)}"
                f"{_rejections_text(rejections)}"
            ),
        )
name property

Unique tool identifier.

execute()

Return value-free auth state; any error becomes a failure ToolResult.

Source code in packages/axm-doctor/src/axm_doctor/tools.py
Python
def execute(self) -> ToolResult:
    """Return value-free auth state; any error becomes a failure ToolResult."""
    try:
        auth = _auth_map()
        provenance = _credentials_map(collect_credential_provenance())
        credentials = {
            coordinate: {
                "layer": entry["layer"],
                "present": entry["present"],
            }
            for coordinate, entry in provenance.items()
        }
        rejections = _rejection_rows()
    except Exception as exc:  # noqa: BLE001 # MCP boundary: any error -> failure
        return ToolResult(success=False, error=str(exc))
    auth_text = "\n".join(
        f"- {tool}: {entry['state']}"
        f"{' [no declaration]' if not entry['declaration_consulted'] else ''}"
        for tool, entry in auth.items()
    )
    return ToolResult(
        success=True,
        data={
            "auth": auth,
            "undetermined": [
                tool
                for tool, entry in auth.items()
                if entry["state"] == "undetermined"
            ],
            "logged_out": [
                tool
                for tool, entry in auth.items()
                if entry["state"] == "logged_out"
            ],
            "credentials": credentials,
            "rejections": rejections,
        },
        text=(
            f"Third-party auth:\n{auth_text}\n\n{_credentials_text(provenance)}"
            f"{_rejections_text(rejections)}"
        ),
    )