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
35 changes: 34 additions & 1 deletion agent_assembly/core/runtime_interceptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import json
import os
from importlib import metadata
from typing import Any

from agent_assembly.exceptions import OpTerminatedError
Expand Down Expand Up @@ -288,12 +289,44 @@ def connect_runtime_client(agent_id: str) -> Any | None:
return None

socket_path = _resolve_runtime_socket_path(agent_id)
sdk_version = _sdk_version()
try:
return RuntimeClient.connect(socket_path)
# Forward the agent identity (signed into the runtime handshake,
# AAASM-3587) and the installed PyPI package version (signed into the
# handshake so downgrade detection reflects the real SDK release,
# AAASM-3683).
return RuntimeClient.connect(socket_path, agent_id, sdk_version)
except TypeError:
# Native build predates the agent_id / sdk_version parameters; fall back
# to the legacy positional signature so connecting still works against an
# older core (the version simply is not forwarded).
try:
return RuntimeClient.connect(socket_path)
except Exception:
return None
except Exception:
return None


def _sdk_version() -> str | None:
"""Return the installed ``agent-assembly`` distribution version, or ``None``.

Prefers ``importlib.metadata.version`` (the authoritative installed-package
version) and falls back to the in-tree ``agent_assembly.__version__`` when the
distribution metadata is unavailable (e.g. an editable checkout that was never
installed). ``None`` lets the native shim fall back to the crate version.
"""
try:
return metadata.version("agent-assembly")
except metadata.PackageNotFoundError:
try:
from agent_assembly import __version__

return __version__
except Exception:
return None


def register_agent(
runtime_client: Any,
agent_id: str,
Expand Down
24 changes: 20 additions & 4 deletions native/aa-ffi-python/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions native/aa-ffi-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ crate-type = ["cdylib"]
# aa-proto, and aa-sdk-client must share one SHA so cargo resolves a single
# checkout of the agent-assembly workspace.
#
# Pinned to the merge commit of agent-assembly PR #1160, which adds the
# `team_id` / `parent_agent_id` fields to `AssemblyConfig` the shim now sets.
aa-core = { git = "https://github.com/ai-agent-assembly/agent-assembly.git", rev = "ebc4d7dc9da81cdf4f265d681c138500ca6e98e7", package = "aa-core", features = ["serde"] }
aa-proto = { git = "https://github.com/ai-agent-assembly/agent-assembly.git", rev = "ebc4d7dc9da81cdf4f265d681c138500ca6e98e7", package = "aa-proto" }
aa-sdk-client = { git = "https://github.com/ai-agent-assembly/agent-assembly.git", rev = "ebc4d7dc9da81cdf4f265d681c138500ca6e98e7", package = "aa-sdk-client" }
# Pinned to agent-assembly AAASM-3683, which adds the `sdk_version` field to
# `AssemblyConfig` and threads it through `spawn_ipc_thread` into the signed IPC
# handshake — the version this shim now forwards from the PyPI package version.
# Re-pin to the squash-merge commit once PR #1226 lands on master.
aa-core = { git = "https://github.com/ai-agent-assembly/agent-assembly.git", rev = "b0576a72422bad7c6f380e666aa6ad70d1939822", package = "aa-core", features = ["serde"] }
aa-proto = { git = "https://github.com/ai-agent-assembly/agent-assembly.git", rev = "b0576a72422bad7c6f380e666aa6ad70d1939822", package = "aa-proto" }
aa-sdk-client = { git = "https://github.com/ai-agent-assembly/agent-assembly.git", rev = "b0576a72422bad7c6f380e666aa6ad70d1939822", package = "aa-sdk-client" }
prost = "0.14"
pyo3 = { version = "0.29", features = ["extension-module"] }
serde = { version = "1.0", features = ["derive"] }
Expand Down
26 changes: 23 additions & 3 deletions native/aa-ffi-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,35 @@ impl RuntimeClient {
/// Spawns the shared client's background IPC thread and returns a handle.
/// Event reporting is fire-and-forget; a failed connection surfaces on a
/// later `send_event` rather than here.
///
/// `agent_id` is the agent identity the background thread signs the runtime
/// session handshake with (AAASM-3587); pass `None` only when no identity is
/// available yet.
///
/// `sdk_version` is the user-facing PyPI package version (`agent_assembly.__version__`)
/// the Python layer forwards so it — not the shared `aa-sdk-client` crate
/// version — is what gets signed into the handshake (AAASM-3683). `None`
/// lets the shared client fall back to the crate version (no regression vs
/// AAASM-3666).
#[staticmethod]
fn connect(socket_path: String) -> PyResult<Self> {
#[pyo3(signature = (socket_path, agent_id=None, sdk_version=None))]
fn connect(socket_path: String, agent_id: Option<String>, sdk_version: Option<String>) -> PyResult<Self> {
let config = AssemblyConfig {
agent_id: String::new(),
agent_id: agent_id.unwrap_or_default(),
socket_path: Some(socket_path.clone()),
// Registration is a separate explicit `register` call; connecting
// the runtime UDS does not need a gateway endpoint or lineage.
gateway_endpoint: None,
team_id: None,
parent_agent_id: None,
sdk_version,
};
let handle = spawn_ipc_thread(config.resolve_socket_path()).map_err(|error| {
let handle = spawn_ipc_thread(
config.resolve_socket_path(),
config.agent_id.clone(),
config.resolved_sdk_version(),
)
.map_err(|error| {
PyRuntimeError::new_err(format!("failed to start runtime IPC thread: {error}"))
})?;
Ok(Self {
Expand Down Expand Up @@ -139,6 +156,9 @@ impl RuntimeClient {
gateway_endpoint,
team_id,
parent_agent_id,
// The version is signed at IPC-handshake time (`connect`), not on the
// gateway register, so it is not needed for this config.
sdk_version: None,
};
// `register` is async (tonic). Release the GIL and drive the future on a
// private current-thread runtime so the blocking gRPC round-trip never
Expand Down
37 changes: 35 additions & 2 deletions test/unit/core/_fake_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import sys
import types
from collections.abc import Callable
from typing import Any

import pytest
Expand All @@ -25,6 +26,9 @@ def __init__(self, decision: str = "allow", reason: str = "") -> None:
self.register_calls: list[tuple[str, str, str, str | None, str | None, str | None]] = []
self.query_calls: list[tuple[Any, ...]] = []
self.register_should_raise: Exception | None = None
# Set by install_fake_core's connect to the (socket_path, agent_id,
# sdk_version) it was called with (AAASM-3683).
self.connect_args: tuple[str, str | None, str | None] | None = None

def register(
self,
Expand Down Expand Up @@ -85,14 +89,43 @@ def install_fake_core(
runtime_client: Any,
) -> Any:
"""Install a fake ``agent_assembly._core`` whose ``RuntimeClient.connect``
returns ``runtime_client``. Returns the same client for assertions."""
returns ``runtime_client``. Returns the same client for assertions.

``connect`` accepts the AAASM-3683 ``agent_id`` / ``sdk_version`` arguments
and records them on ``runtime_client.connect_args`` so callers can assert the
installed package version is forwarded into the handshake.
"""

class _ConnectingRuntimeClient:
@staticmethod
def connect(_socket_path: str) -> Any:
def connect(_socket_path: str, agent_id: str | None = None, sdk_version: str | None = None) -> Any:
runtime_client.connect_args = (_socket_path, agent_id, sdk_version)
return runtime_client

fake_core = types.ModuleType("agent_assembly._core")
fake_core.RuntimeClient = _ConnectingRuntimeClient # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "agent_assembly._core", fake_core)
return runtime_client


def install_fake_core_with_connect(
monkeypatch: pytest.MonkeyPatch,
connect: Callable[..., Any],
) -> None:
"""Install a fake ``agent_assembly._core`` whose ``RuntimeClient.connect`` is
the supplied callable.

Lets a test drive the connect path against a native build that, for example,
predates the AAASM-3683 ``sdk_version`` parameter (``connect`` raising
``TypeError`` for the extra argument) or fails outright.
"""

runtime_client_cls = type(
"_CustomConnectRuntimeClient",
(),
{"connect": staticmethod(connect)},
)

fake_core = types.ModuleType("agent_assembly._core")
fake_core.RuntimeClient = runtime_client_cls # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "agent_assembly._core", fake_core)
Loading