Skip to content

Access

access

Positive tool scopes for nested name-based discovery and invocation.

ToolAccessDenied

Bases: PermissionError

A named dispatch attempted to escape its positive tool scope.

Source code in packages/axm/src/axm/tools/access.py
Python
class ToolAccessDenied(PermissionError):  # noqa: N818
    """A named dispatch attempted to escape its positive tool scope."""

check_tool_execution(name, kwargs, execute)

Revalidate all live execution guards immediately before an effect.

Source code in packages/axm/src/axm/tools/access.py
Python
def check_tool_execution(name: str, kwargs: dict[str, object], execute: object) -> None:
    """Revalidate all live execution guards immediately before an effect."""
    require_tool_access(name)
    for guard in _GUARDS.get():
        guard(name, kwargs, execute)

guard_tool(name, tool)

Retain execution authority around a resolved tool without changing metadata.

Legacy resolution returns the original object. A scoped handle retains its guards even if cached and subsequently invoked outside the creating block.

Source code in packages/axm/src/axm/tools/access.py
Python
def guard_tool(name: str, tool: AXMTool) -> AXMTool:
    """Retain execution authority around a resolved tool without changing metadata.

    Legacy resolution returns the original object. A scoped handle retains its
    guards even if cached and subsequently invoked outside the creating block.
    """
    guards = _GUARDS.get()
    allowed = _ALLOWED.get()
    if not guards and allowed is None:
        return tool

    original = tool.execute
    origin_contract = current_read_contract()
    origin_bound = read_context_bound() or origin_contract is not None

    @wraps(original)
    def execute(**kwargs: object) -> ToolResult:
        require_tool_access(name)
        if allowed is not None and name not in allowed:
            raise ToolAccessDenied(f"undeclared tool refused: {name}")
        combined = (*guards, *_GUARDS.get())
        for index, guard in enumerate(combined):
            if any(guard is previous for previous in combined[:index]):
                continue
            guard(name, kwargs, original)
        authority = (
            restricted_read_context(origin_contract) if origin_bound else nullcontext()
        )
        with authority:
            return original(**kwargs)

    class GuardedTool:
        def __getattr__(self, key: str) -> object:
            return getattr(tool, key)

    guarded = GuardedTool()
    guarded.execute = execute  # type: ignore[attr-defined]
    return cast("AXMTool", guarded)

has_tool_execution_guard()

Whether execution needs a host policy check (and a worker thread).

Source code in packages/axm/src/axm/tools/access.py
Python
def has_tool_execution_guard() -> bool:
    """Whether execution needs a host policy check (and a worker thread)."""
    return bool(_GUARDS.get())

require_tool_access(name)

Recheck resolutions, including cached nodes and injected overrides.

Source code in packages/axm/src/axm/tools/access.py
Python
def require_tool_access(name: str) -> None:
    """Recheck resolutions, including cached nodes and injected overrides."""
    if not tool_is_allowed(name):
        raise ToolAccessDenied(f"undeclared tool refused: {name}")

tool_access_scope(names)

Apply an immutable allowlist; nested scopes can only narrow it.

Source code in packages/axm/src/axm/tools/access.py
Python
@contextmanager
def tool_access_scope(names: frozenset[str]) -> Iterator[None]:
    """Apply an immutable allowlist; nested scopes can only narrow it."""
    if not isinstance(names, frozenset):
        raise ValueError("tool scope must be an immutable frozenset")
    outer = _ALLOWED.get()
    token = _ALLOWED.set(names if outer is None else outer & names)
    try:
        yield
    finally:
        _ALLOWED.reset(token)

tool_execution_guard(guard)

Install a neutral execution guard; inner scopes cannot remove outer guards.

Guards validate the current lease and may normalize detached keyword args. They run immediately before execution, including cached and injected tools. A transport owns its callback: the SDK does not import the transport.

Source code in packages/axm/src/axm/tools/access.py
Python
@contextmanager
def tool_execution_guard(guard: ExecutionGuard) -> Iterator[None]:
    """Install a neutral execution guard; inner scopes cannot remove outer guards.

    Guards validate the current lease and may normalize detached keyword args.
    They run immediately before execution, including cached and injected tools.
    A transport owns its callback: the SDK does not import the transport.
    """
    token = _GUARDS.set((*_GUARDS.get(), guard))
    try:
        yield
    finally:
        _GUARDS.reset(token)

tool_is_allowed(name)

Whether a canonical name belongs to the current tool scope.

Source code in packages/axm/src/axm/tools/access.py
Python
def tool_is_allowed(name: str) -> bool:
    """Whether a canonical name belongs to the current tool scope."""
    allowed = _ALLOWED.get()
    return allowed is None or name in allowed