Python SDK reference
Stable root imports
from axm import ... supports the following contracts:
| Export | Purpose |
|---|---|
AXMTool |
Structural tool protocol: name and execute |
ToolResult |
Tool success, data, error, hint and text |
ToolMetadata, tool_metadata |
Optional discovery settings and their defaults |
tool_node, ToolNodeError |
Tool-to-node adapter and its contract errors |
WitnessRule, WitnessResult, ValidationFeedback |
Validation contracts |
__version__ |
Installed distribution version; "0.0.0" if metadata is missing |
DeclarativeToolDefinition, DeclarativeToolManifest, load_declarative_manifest |
Versioned UI tool metadata, JSON Schema validation and immutable declarations |
DeclarativeTool |
One shared adapter, explicitly unavailable without a host binding |
tool_access_scope, ToolAccessDenied |
Positive, context-local permissions including nested resolution and cached nodes |
tool_execution_guard |
Host-injected lease and argument-authority checks immediately before execution |
validate_declarative_schema |
Detached object schemas checked offline under the supported 2020-12 dialect |
tool_scope_sha256 |
SHA256 of compact UTF-8 JSON containing sorted logical target names |
require_write_path |
Fail-closed write authority for a host-configured persisted target |
require_read_path |
Canonical read authority for explicit and configured persisted targets |
Definitions remain in their submodules. Prefer root imports for these
contracts. load_tool and override_tools are additionally re-exported by
axm.tools. CLI helpers and axm.tools.write_scope are implementation
modules, not root SDK exports.
Tool results
ToolResult is a frozen dataclass, not a Pydantic model. It requires
success; data defaults to a fresh empty dict, and error, hint,
text default to None. Frozen fields do not make nested data immutable.
data is for machine consumers; text is an optional prepared rendering.
Neither automatically derives from the other. hint is available to
consumers but the generic CLI does not print it as a separate field.
Use dataclasses.asdict when you explicitly need the whole dataclass;
the CLI's shared --json-output emits only data.
ToolResult
dataclass
Immutable result of a tool execution.
Attributes:
| Name | Type | Description |
|---|---|---|
success |
bool
|
Whether the tool execution succeeded. |
data |
dict[str, Any]
|
Structured output data (backend-specific). |
error |
str | None
|
Human-readable error message, if any. |
hint |
str | None
|
Optional next-step suggestion for the agent. |
text |
str | None
|
Optional pre-rendered text representation (e.g. Markdown). |
Source code in packages/axm/src/axm/tools/base.py
AXMTool
Bases: Protocol
Structural protocol for AXM deterministic tools.
Implementors must provide:
- name (property): unique tool identifier
- execute(...) : deterministic execution with explicit params
Optionally provide:
- agent_hint (class attribute): optional, free-form one-liner
optimized for LLM consumption: what the tool does, key params, and
what it replaces. Like the other discovery attributes below, it is
not a protocol member and carries no guaranteed fallback — some
discovery tooling reads it best-effort via getattr /
:func:tool_metadata; absent, nothing is substituted.
- expose_directly (class attribute, default False): when
True, the MCP server registers this tool directly in
tools/list (the hot path). When False (default), the
tool is reachable only through the MCP facade
(axm_search -> axm_describe -> axm_call), keeping the
tools/list payload small.
- domain (class attribute, default None): coarse capability
group used by the facade for axm_capabilities and to scope
axm_search (e.g. "ast", "git", "ticket").
- tags (class attribute, default frozenset()): free-form
keywords feeding facade discovery (axm_search).
Uses structural typing (PEP 544) — no inheritance required.
@runtime_checkable enables isinstance() checks. The discovery
attributes (expose_directly / domain / tags) are not
protocol members on purpose: adding data attributes to a
runtime_checkable protocol would make them required for
isinstance() and break the check for the many tools that satisfy
AXMTool structurally without subclassing it. Read them through
:func:tool_metadata (or getattr(tool, name, default)) instead,
which works for subclasses and structural tools alike.
Example::
class MyTool(AXMTool):
agent_hint = "Frobnicate widgets — use width param."
expose_directly = True # hot path (read via tool_metadata)
domain = "widget"
tags = frozenset({"frobnicate"})
@property
def name(self) -> str:
return "my-tool"
def execute(self, *, value: int = 0) -> ToolResult:
return ToolResult(success=True, data={"result": value})
Source code in packages/axm/src/axm/tools/base.py
name
property
Unique tool identifier (e.g., 'esbmc', 'dafny', 'pytest').
execute(**kwargs)
Execute the tool with given arguments.
Subclasses should override with explicit, typed parameters::
def execute(self, *, title: str = "", body: str = "") -> ToolResult:
...
The **kwargs signature here is the structural minimum —
any callable accepting keyword arguments satisfies it.
Returns:
| Type | Description |
|---|---|
ToolResult
|
ToolResult with success status and structured data. |
Source code in packages/axm/src/axm/tools/base.py
Discovery metadata
tool_metadata(tool) returns ToolMetadata with defaults
expose_directly=False, domain=None, tags=frozenset().
It uses attributes rather than requiring inheritance. Tags are coerced to a
frozenset; expose_directly is converted with bool.
agent_hint is a separate optional attribute, absent from ToolMetadata.
The helper does not invent an agent hint or guarantee a docstring fallback.
ToolMetadata
dataclass
Facade/CLI discovery metadata for a tool, with safe defaults.
Built by :func:tool_metadata from the optional expose_directly /
domain / tags attributes a tool may declare. Centralising the
defaults here keeps the MCP facade and the CLI reading the same contract.
Attributes:
| Name | Type | Description |
|---|---|---|
expose_directly |
bool
|
Whether the tool is on the MCP hot path
(registered directly in |
domain |
str | None
|
Coarse capability group, or |
tags |
frozenset[str]
|
Discovery keywords (possibly empty). |
Source code in packages/axm/src/axm/tools/base.py
tool_metadata(tool)
Read a tool's optional discovery attributes into a :class:ToolMetadata.
Works for tools that subclass :class:AXMTool, tools that satisfy it
structurally, and plain callables — anything missing an attribute falls
back to the default. tags is coerced to a frozenset so callers
can rely on set semantics regardless of how the tool declared it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool
|
object
|
The tool instance (or plain callable) to introspect. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
ToolMetadata
|
class: |
Source code in packages/axm/src/axm/tools/base.py
Tool nodes
Read the composition guide before choosing mappings
and failure handling. ToolNodeError is a RuntimeError subclass.
tool_node(name, *, args=None, returns=None, allow_failure_data=False)
Build a DAG python-node fn(payload) -> dict around an axm.tools tool.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The tool's |
required |
args
|
Mapping[str, str] | None
|
Optional |
None
|
returns
|
Mapping[str, str] | None
|
Precedence / collision: the literal |
None
|
allow_failure_data
|
bool
|
Opt-in for observation tools whose |
False
|
Returns:
| Type | Description |
|---|---|
Callable[[Mapping[str, object]], dict[str, object]]
|
A callable mapping the node's |
Raises:
| Type | Description |
|---|---|
ToolNodeError
|
At call time, if the tool is unknown, returns
|
Source code in packages/axm/src/axm/tools/node.py
| Python | |
|---|---|
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | |
ToolNodeError
load_tool(name)
Resolve and instantiate the AXM tool named name.
The name is resolved by :func:axm.tools.discovery.tool_entry_points
(tribunal group first, legacy axm.tools second), the resolver the MCP
catalog and the axm CLI share.
Raises:
| Type | Description |
|---|---|
ToolNodeError
|
No tool is registered under name. |
DuplicateToolError
|
One entry-point group publishes a name twice. |
Source code in packages/axm/src/axm/tools/node.py
override_tools(tools)
Substitute tools ({entry_point_name: tool}) for the duration of a block.
Inside the block, a :func:tool_node built for one of the named tools calls
the substitute instead of resolving the axm.tools entry point — whether
the node was built before or after entering the block, and whether or not
the real tool had already been resolved and memoized. Blocks nest: an inner
block adds to (or shadows, per name) the enclosing one, and leaving it
restores exactly what was active before.
The substitution applies to every resolution by name — :func:tool_node
nodes and direct :func:_load_tool callers alike — so a graph node that
resolves a tool itself (_load_tool("echo_check") in a python node) is
covered too, not only tool_node wrappers.
The scope is a :mod:contextvars context, not a global: it propagates to
asyncio tasks and :func:asyncio.to_thread calls started inside the
block (how axm_dag executes python nodes) and is invisible to concurrent
work started outside it. Substitutes are never memoized, so the real tool
resolves again as soon as the block exits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tools
|
Mapping[str, AXMTool]
|
Entry-point name → substitute implementing |
required |
Source code in packages/axm/src/axm/tools/node.py
Process sandbox
axm.sandbox runs a subprocess that may write only under given directories,
in a filtered environment. It is a submodule contract, not a root export.
| Symbol | Purpose |
|---|---|
confinement(writable, protected=()) |
Probe the platform sandbox (sandbox-exec on macOS, bwrap on Linux) with the profile it will apply; Confinement(isolation="sandbox") or UNCONFINED ("detect-only") |
run_confined(command, *, writable, cwd, env=None, protected=(), timeout=None, confined=None) |
Run one command under a confinement; returns ConfinedRun (isolation, return code, output) |
inherited_environment(source=None) |
The allow-listed variables (INHERITED_ENV) only: no PYTHONPATH, FORCE_COLOR or credentials |
private_environment(root) |
HOME, temporary and XDG variables pointed into fresh directories of root |
darwin_profile, bubblewrap_argv |
The platform prefixes, for callers that launch the process themselves |
A detect-only confinement does not confine anything: callers refuse it or
report it, never treat it as a sandbox. A protected path inside a writable
directory stays read-only. On macOS, sandbox-exec applies explicit write
denials for protected paths after the writable allowances. On Linux,
bubblewrap mounts the filesystem read-only, binds writable directories, then
remounts every protected path read-only after those writable binds. Both
platforms enforce the protection when confinement reports sandbox.
run_confined(command, *, writable, cwd, env=None, protected=(), timeout=None, confined=None)
Run command writing only under writable, in a filtered environment.
The environment is :func:inherited_environment updated with env;
nothing else of the caller's environment is passed. confined is the
confinement to apply, probed from writable and protected when
omitted: the caller refuses a detect-only one beforehand if it must.
Raises:
| Type | Description |
|---|---|
TimeoutExpired
|
The process outlived |
OSError
|
The process could not start. |
Source code in packages/axm/src/axm/sandbox.py
confinement(writable, protected=())
The confinement writing only under writable, never under protected.
sandbox when the platform's sandbox applies here (probed from the first
writable directory), else :data:UNCONFINED (detect-only).
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in packages/axm/src/axm/sandbox.py
Other contracts
Witnesses covers the remaining root protocols and result types. CLI reference documents launcher helpers.
The workspace build also generates module reference pages from
docs/gen_ref_pages.py. This curated reference renders directly from the same
source, so it works in both package-only and monorepo builds.
Declared instrument runtime
axm.DependencyEnvironment is the immutable runtime transported with a phase
contract. axm.session_dependency_environment(project_path) returns the matching
declared environment, or None only without a contract. A missing or ambiguous
runtime, an unrelated readable root, or a missing interpreter fails explicitly.
The interpreter retains its lexical virtualenv path. Resolving this runtime does
not grant additional filesystem write authority.
Declarative UI tools
The frontend build publishes ui/tools.json in the installed resource package.
One entry point identifies that package, not an executable per tool:
The entry-point name must equal addon_id. The common resolver used by MCP,
CLI and load_tool reads this resource. It never compiles or scans Svelte.
Python tribunal/legacy precedence is unchanged; UI/Python collisions and duplicate
UI owners fail explicitly. A missing resource emits a provider-unavailable warning
without disabling legacy tools. Invalid or incompatible manifests fail closed.
{
"schema_version": 1,
"addon_id": "rugby",
"bundle_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"tools": [{
"name": "rugby_ui_read_state",
"handler": "rugby_ui_read_state",
"description": "Read this view's presentation state",
"inputSchema": {"type": "object", "additionalProperties": false},
"outputSchema": {"type": "object"}
}]
}
Schemas use JSON Schema 2020-12, object roots and local references only.
The same validate_declarative_schema(schema) -> dict helper validates view
snapshot schemas. It rejects remote references/IDs, non-2020-12 dialects and
custom vocabularies. Legacy $recursiveRef/$recursiveAnchor are refused rather
than silently ignored; use local $dynamicRef in the supported dialect.
Schema annotations/examples and property names remain data, not reference keywords.
Extra manifest fields are refused. Definitions detach their schema dictionaries;
mutating an input dictionary does not change the published contract. The host
separately verifies the bundle hash and owns target, revision, deduplication,
permissions and lifecycle. Metadata does not grant execution authority.
Without an explicit binding, a discovered DeclarativeTool.execute returns
success=False, status unavailable: no active-global-view fallback. A synchronous
host can inject (name, arguments) -> ToolResult into DeclarativeTool;
Orison's asynchronous host uses the scoped MCP factory. Successful ToolResult.data
must satisfy outputSchema; invalid arguments never reach the invoker.
CLI tools accept one JSON --arguments value, for example
axm rugby_ui_read_state --arguments '{}'; a headless call remains unavailable.
The generic transport is not a second implementation of persistent business actions.
tool_access_scope(frozenset({...})) filters discovery and denies name-based loads,
overrides and cached tool nodes outside the positive list. Nested scopes intersect
instead of widening; outside a scope, legacy behavior is unchanged. This is an
authorization contract for trusted tools, not a sandbox against arbitrary Python
code holding direct function references.
tool_execution_guard(callback) installs
callback(name, detached_kwargs, original_execute) -> None. A callback may normalize
the detached keyword arguments and raises on a stale lease or denied effect.
Scopes compose and restore using ContextVar; a nested scope cannot remove an outer
guard. Resolved handles preserve the implementation's signature and metadata while
retaining their creating guards. Tool-node caches retain only implementations,
then apply the current request's guards at every execution. Legacy resolution
without a scope/guard still returns the original implementation.
Retained handles also capture bound filesystem authority. During the service
body, the current caller and originating authority must both permit a read or
write; reusing a readonly handle inside a writable conversation cannot widen it.
The intersection remains active across nested dependency-read contexts. An
explicit None on either side denies authority and cannot borrow an ambient grant.
This cooperative boundary checks named dispatch immediately before invoking a trusted tool. It cannot atomically revoke an effect already running or sandbox arbitrary Python references. Business services still enforce their own validations.
DeclarativeToolDefinition
dataclass
Immutable tool metadata; schemas are detached on every access.
Source code in packages/axm/src/axm/tools/declarative.py
input_schema
property
Detached JSON Schema for functional arguments, without routing fields.
output_schema
property
Detached JSON Schema for successful ToolResult.data.
validate_input(arguments)
Reject malformed/non-JSON arguments before contacting any view.
Source code in packages/axm/src/axm/tools/declarative.py
validate_result(result)
Validate successful data; failures retain their structural signal.
Source code in packages/axm/src/axm/tools/declarative.py
DeclarativeToolManifest
dataclass
One addon bundle's closed tool declarations.
Source code in packages/axm/src/axm/tools/declarative.py
| Python | |
|---|---|
load_declarative_manifest(raw)
Validate a JSON manifest from a trusted installed package.
Extra fields, remote schema references, versions and collisions are refused. This validates metadata only: the host verifies the mounted bundle.
Source code in packages/axm/src/axm/tools/declarative.py
DeclarativeTool
Common AXMTool adapter, unavailable without an explicit host binding.
Source code in packages/axm/src/axm/tools/declarative.py
name
property
Canonical name shared by CLI, nodes and MCP.
execute(**kwargs)
Validate and invoke the host binding, never an ambient view.
Source code in packages/axm/src/axm/tools/declarative.py
tool_access_scope(names)
Apply an immutable allowlist; nested scopes can only narrow it.
Source code in packages/axm/src/axm/tools/access.py
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
validate_declarative_schema(schema)
Return a detached object schema validated offline for tools or snapshots.
Only the supported 2020-12 dialect and local references are accepted. Remote IDs/vocabularies and legacy recursive dialect keywords cannot weaken it.
Source code in packages/axm/src/axm/tools/declarative.py
ToolAccessDenied
tool_scope_sha256(names)
Fingerprint a positive tool list; no provider-specific spelling.
Source code in packages/axm/src/axm/tools/declarative.py
require_write_path(path) is a separate service-side persistence guard. It raises
PermissionError without an active contract or outside its allowed write prefixes;
readable roots and dependency aliases do not grant writes. Relative paths resolve
against the contract execution root, never the ambient working directory, and
symlinks are canonicalized. Check the host-configured store/file before the canonical
mutation. Metadata tags alone do not classify arbitrary domain tools as writes.
MCP wrappers supply the bound contract; a local CLI can use an explicit
AXM_WRITE_CONTRACT. A trusted UI route retains its own host authorization and
shares the same business service rather than imitating an MCP click.
require_write_path(path)
Require active write authority for a persisted target, or raise PermissionError.
Use this before the canonical business mutation, with the host-configured path, not a path chosen by the model. Only allowed write prefixes count: readable roots/dependency aliases never grant writes. Resolution is relative to the contract execution root and follows symlinks through its canonical resolver. Missing authority is refused, including headless calls without an explicit contract. Local CLI callers may use their explicit AXM_WRITE_CONTRACT.
Source code in packages/axm/src/axm/tools/write_authority.py
require_read_path(path) must run after a business service resolves either a
caller-supplied store path or its configured default, immediately before opening
that target. A bound contract uses canonical readable authority (read roots and
declared dependency sources); relative paths use the execution root and symlinks
cannot escape. An explicitly transport-bound None refuses reads and suppresses
ambient contracts. Only legacy local callers with neither bound authority nor
ambient contract remain unrestricted. Read authorization grants no writes.
require_read_path(path)
Require read authority before opening a service's persisted target.
Both explicit caller targets and implicit configured targets must pass here after canonical service resolution. Relative paths use the active contract's execution root, and symlinks use its canonical readable decision. Transport None explicitly denies reads and suppresses the ambient environment. Only legacy local callers with neither transport binding nor contract stay unrestricted; this helper never grants write authority.