diff --git a/agent_assembly/__init__.py b/agent_assembly/__init__.py index a138365c..f100befd 100644 --- a/agent_assembly/__init__.py +++ b/agent_assembly/__init__.py @@ -23,6 +23,7 @@ _LAZY_EXPORTS: dict[str, str] = { "init_assembly": _MODULE_CORE, "AssemblyContext": _MODULE_CORE, + "ENFORCEMENT_MODES": _MODULE_CORE, "GovernanceInterceptor": _MODULE_ADAPTERS, "FrameworkAdapter": _MODULE_ADAPTERS, "AssemblyError": _MODULE_EXCEPTIONS, @@ -44,6 +45,7 @@ "__version__", "init_assembly", "AssemblyContext", + "ENFORCEMENT_MODES", "GovernanceInterceptor", "FrameworkAdapter", "AssemblyError", @@ -103,6 +105,7 @@ def __dir__() -> list[str]: if TYPE_CHECKING: from agent_assembly.adapters import FrameworkAdapter as FrameworkAdapter from agent_assembly.adapters import GovernanceInterceptor as GovernanceInterceptor + from agent_assembly.core import ENFORCEMENT_MODES as ENFORCEMENT_MODES from agent_assembly.core import AssemblyContext as AssemblyContext from agent_assembly.core import init_assembly as init_assembly from agent_assembly.exceptions import ( diff --git a/agent_assembly/core/__init__.py b/agent_assembly/core/__init__.py index aaff7b6f..82d45c7b 100644 --- a/agent_assembly/core/__init__.py +++ b/agent_assembly/core/__init__.py @@ -5,21 +5,27 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from agent_assembly.core.assembly import AssemblyContext, init_assembly + from agent_assembly.core.assembly import ( + ENFORCEMENT_MODES, + AssemblyContext, + init_assembly, + ) from agent_assembly.core.lineage import LineageRegistry -__all__ = ["init_assembly", "AssemblyContext", "LineageRegistry"] +__all__ = ["init_assembly", "AssemblyContext", "ENFORCEMENT_MODES", "LineageRegistry"] def __getattr__(name: str) -> Any: - if name in ("AssemblyContext", "init_assembly"): + if name in ("AssemblyContext", "init_assembly", "ENFORCEMENT_MODES"): from agent_assembly.core.assembly import ( # noqa: PLC0415 + ENFORCEMENT_MODES, AssemblyContext, init_assembly, ) globals()["AssemblyContext"] = AssemblyContext globals()["init_assembly"] = init_assembly + globals()["ENFORCEMENT_MODES"] = ENFORCEMENT_MODES return globals()[name] if name == "LineageRegistry": from agent_assembly.core.lineage import LineageRegistry # noqa: PLC0415 diff --git a/agent_assembly/core/assembly.py b/agent_assembly/core/assembly.py index 6352c1e0..8c483853 100644 --- a/agent_assembly/core/assembly.py +++ b/agent_assembly/core/assembly.py @@ -45,13 +45,23 @@ Mirrors ``aa_core::EnforcementMode`` on the wire; uses the same snake_case tokens the gateway expects in the registration body.""" +ENFORCEMENT_MODES: tuple[EnforcementMode, ...] = ("enforce", "observe", "disabled") +"""Canonical, ordered enforcement-mode tokens — the public parity surface. + +Single source of truth for the enforcement-mode set: the private +``_VALID_ENFORCEMENT_MODES`` membership validator is derived from this tuple +(``frozenset(ENFORCEMENT_MODES)``) so the two can never drift. Ordering is +significant — the cross-SDK conformance cross-check (AAASM-4856) asserts the +Python/Node/Go SDKs each expose this exact sequence, matching the canonical +``aa_core::EnforcementMode`` order on the wire.""" + ENV_GATEWAY_URL = "AA_GATEWAY_URL" ENV_CONTROL_PLANE_URL = "AA_CONTROL_PLANE_URL" _DEFAULT_AGENT_ID = "agent-assembly-default" _AGENT_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,128}$") _VALID_RUNTIME_MODES = {"auto", "ebpf", "proxy", "sdk-only"} -_VALID_ENFORCEMENT_MODES: frozenset[EnforcementMode] = frozenset({"enforce", "observe", "disabled"}) +_VALID_ENFORCEMENT_MODES: frozenset[EnforcementMode] = frozenset(ENFORCEMENT_MODES) _INIT_LOCK = Lock() _ACTIVE_CONTEXT: AssemblyContext | None = None diff --git a/test/unit/core/test_core_lazy_exports.py b/test/unit/core/test_core_lazy_exports.py index ca697cbd..9ab8cf02 100644 --- a/test/unit/core/test_core_lazy_exports.py +++ b/test/unit/core/test_core_lazy_exports.py @@ -19,6 +19,12 @@ def test_lazy_getattr_resolves_init_assembly_and_assembly_context() -> None: assert core.AssemblyContext is AssemblyContext +def test_lazy_getattr_resolves_enforcement_modes() -> None: + from agent_assembly.core.assembly import ENFORCEMENT_MODES + + assert core.ENFORCEMENT_MODES is ENFORCEMENT_MODES + + def test_lazy_getattr_resolves_lineage_registry() -> None: from agent_assembly.core.lineage import LineageRegistry @@ -32,4 +38,9 @@ def test_lazy_getattr_raises_attribute_error_for_unknown_symbol() -> None: def test_all_lists_the_public_lazy_exports() -> None: - assert set(core.__all__) == {"init_assembly", "AssemblyContext", "LineageRegistry"} + assert set(core.__all__) == { + "init_assembly", + "AssemblyContext", + "ENFORCEMENT_MODES", + "LineageRegistry", + } diff --git a/test/unit/test_enforcement_modes_export.py b/test/unit/test_enforcement_modes_export.py new file mode 100644 index 00000000..e8c8fa05 --- /dev/null +++ b/test/unit/test_enforcement_modes_export.py @@ -0,0 +1,29 @@ +"""AAASM-4856: `agent_assembly.ENFORCEMENT_MODES` public parity surface. + +The cross-SDK enforcement-mode parity conformance cross-check in e2e-public +reads ``getattr(agent_assembly, "ENFORCEMENT_MODES", None)`` to assert the +installed Python SDK against the canonical ordered set. These tests pin that +surface: it must resolve, be publicly discoverable via ``__all__``, carry the +exact canonical order, and stay in sync with the private membership validator +it is the single source of truth for. +""" + +from __future__ import annotations + +import agent_assembly +import agent_assembly.core.assembly as assembly + + +def test_enforcement_modes_is_the_canonical_ordered_tuple() -> None: + # Order is significant — the cross-SDK cross-check compares sequences. + assert agent_assembly.ENFORCEMENT_MODES == ("enforce", "observe", "disabled") + + +def test_enforcement_modes_is_a_public_export() -> None: + assert "ENFORCEMENT_MODES" in agent_assembly.__all__ + + +def test_enforcement_modes_stays_in_sync_with_private_validator() -> None: + # The public constant is the single source of truth; the private validator + # is derived from it, so the two can never drift. + assert frozenset(agent_assembly.ENFORCEMENT_MODES) == assembly._VALID_ENFORCEMENT_MODES