Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions agent_assembly/_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Install-time runtime binary resolution for the agent-assembly Python SDK.

This module is the lean, blocking presence check for the ``aasm`` sidecar
binary. It is intentionally separate from :mod:`agent_assembly.runtime`,
which manages the full lifecycle (port probe + subprocess spawn). The
intended use is at import time or at the start of long-running scripts:
fail fast with a clear install hint when the binary is unavailable, before
the user discovers it via a subtle subprocess failure deep in the SDK call.
"""

from __future__ import annotations

import os
import shutil
from pathlib import Path

__all__ = [
"BINARY_NAME",
"INSTALL_HINT",
"WHEEL_BUNDLED_BIN",
"ensure_runtime",
]

BINARY_NAME = "aasm"

# Path where the platform-wheel ([runtime] extra) bundles the sidecar binary.
# Mirrors the location runtime.py's find_aasm_binary() also searches, so
# both modules observe the same wheel artifact without coordination.
WHEEL_BUNDLED_BIN = Path(__file__).resolve().parent / "bin" / BINARY_NAME

INSTALL_HINT = (
"agent-assembly runtime binary `aasm` was not found.\n"
" Install the platform wheel: pip install agent-assembly[runtime]\n"
" Or install manually: brew install agent-assembly/tap/aasm\n"
" curl -fsSL https://get.agent-assembly.io | sh"
)


def ensure_runtime() -> Path:
"""Return the resolved path to the ``aasm`` sidecar binary.

Search order, fast to slow:

1. ``$PATH`` (Homebrew tap, ``cargo install``, ``curl`` installer default).
2. ``agent_assembly/bin/aasm`` bundled by the ``[runtime]`` platform wheel.

Raises:
RuntimeError: when no binary is found on either path. The message
carries :data:`INSTALL_HINT` with copy-paste install commands.
"""
path_hit = shutil.which(BINARY_NAME)
if path_hit:
return Path(path_hit)
if WHEEL_BUNDLED_BIN.is_file() and os.access(WHEEL_BUNDLED_BIN, os.X_OK):
return WHEEL_BUNDLED_BIN
raise RuntimeError(INSTALL_HINT)
66 changes: 66 additions & 0 deletions test/unit/test_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Unit tests for agent_assembly._install — install-time runtime resolution."""

from __future__ import annotations

from pathlib import Path

import pytest

from agent_assembly import _install


@pytest.fixture
def isolate_runtime(monkeypatch, tmp_path: Path) -> Path:
"""Isolate ensure_runtime() from the host environment.

Yields a context where:
- PATH is empty (so ``shutil.which`` cannot find any system binary).
- ``WHEEL_BUNDLED_BIN`` points at ``tmp_path/bin/aasm`` (missing by
default; tests opt in by creating the file).
"""
fake_bin_dir = tmp_path / "bin"
fake_bin_dir.mkdir()
fake_binary = fake_bin_dir / _install.BINARY_NAME
monkeypatch.setattr(_install, "WHEEL_BUNDLED_BIN", fake_binary)
monkeypatch.setenv("PATH", "")
return fake_binary


def test_ensure_runtime_returns_path_match_first(monkeypatch, tmp_path: Path) -> None:
"""When `aasm` is on PATH, ensure_runtime returns that resolved path."""
import stat

bin_dir = tmp_path / "system-bin"
bin_dir.mkdir()
on_path = bin_dir / _install.BINARY_NAME
on_path.write_text("#!/bin/sh\nexit 0\n")
on_path.chmod(on_path.stat().st_mode | stat.S_IXUSR)
monkeypatch.setenv("PATH", str(bin_dir))

resolved = _install.ensure_runtime()

assert resolved == on_path


def test_ensure_runtime_falls_back_to_wheel_bundled(isolate_runtime: Path) -> None:
"""When PATH has no aasm, ensure_runtime returns the wheel-bundled path."""
import stat

fake_binary = isolate_runtime
fake_binary.write_text("#!/bin/sh\nexit 0\n")
fake_binary.chmod(fake_binary.stat().st_mode | stat.S_IXUSR)

resolved = _install.ensure_runtime()

assert resolved == fake_binary


def test_ensure_runtime_raises_with_install_hint(isolate_runtime: Path) -> None:
"""When no binary exists, raise RuntimeError carrying INSTALL_HINT."""
# Sanity: isolate_runtime points at a path that doesn't exist yet.
assert not isolate_runtime.exists()

with pytest.raises(RuntimeError) as exc_info:
_install.ensure_runtime()

assert _install.INSTALL_HINT in str(exc_info.value)