Skip to main content

Process-isolated capability executor for axor-core

Project description

axor-daemon

CI PyPI Python License: Apache 2.0

Process-isolated capability executor for axor-core.

axor-core governs what agents are allowed to do. axor-daemon enforces it from outside the agent process.


The Problem with Library-Only Governance

When governance runs as a library in the same process as the agent, the enforcement boundary is Python-level. A compromised dependency, a monkey-patched import, or a hostile extension can bypass CapabilityExecutor without touching the governance logic at all.

axor-daemon moves tool execution across a process boundary:

Agent process                      AxorDaemon process
────────────────────────────       ─────────────────────────────────
GovernedSession                    DaemonServer (mode 0600 socket)
IntentLoop                           DaemonEnforcer
DaemonCapabilityClient  ──────►        operator_policy (ceiling)
  (no tool impls here)   socket         path normalization
                        ◄──────        exec timeout per handler
                                       approved result | DENIED

Tool implementations live only in the daemon. The agent process cannot call them directly — it has no code to do so. The Unix socket is the only path, and it is only accessible to the process owner.


Enforcement Model

Every tool call passes independent checks in the daemon:

1. Socket access — the socket is created 0600. Only the owner process may connect. Any other local process is rejected by the OS before the handshake begins.

2. Protocol version — the handshake validates PROTOCOL_VERSION on both sides. A version mismatch is rejected immediately with an explicit error, not a silent protocol confusion.

3. Operator ceilingoperator_policy is set at daemon startup by the operator and never modified per-connection. The daemon derives allowed tools from it independently — it does not trust the client's claim.

4. Client ceilingallowed_tools reported by the client's GovernedSession. Both the operator ceiling and the client ceiling must approve the tool. The client can only narrow below the operator ceiling, never escalate above it.

5. Arg normalization — path-like args (path, file, target, etc.) are normalized with os.path.normpath by the daemon independently before being passed to any handler. A ../ traversal sequence in a client-supplied path cannot reach a handler unnormalized.

6. Exec timeout — every handler execution is bounded by exec_timeout (default: 60s). A handler that exceeds the timeout returns DENIED — it does not hang the daemon session.

Client sends: tool="bash", allowed_tools=["bash", "read"]

Operator policy = focused_readonly (allow_bash=False)
  → DENIED  operator ceiling  "bash not permitted by operator policy"

Client sends: tool="read", args={"path": "../../etc/passwd"}, allowed_tools=["read"]
Operator policy = focused_readonly (allow_read=True)
  → daemon normalizes path → "/etc/passwd"
  → handler receives normalized args, never raw client string

Client sends: tool="read", allowed_tools=[]   ← excluded read
  → DENIED  session ceiling  "read not in session allowed_tools"

A client that sends an inflated allowed_tools list or crafted path args cannot bypass or escalate — both checks are evaluated daemon-side on independently derived state.


Installation

pip install axor-daemon

Requires axor-core >= 0.5.0, < 0.6. Zero additional dependencies — stdlib asyncio only.


Quick Start

1. Start the daemon

axor-daemon start --policy focused_generative

The daemon loads the operator policy, creates ~/.axor/daemon.sock with permissions 0600, and begins accepting connections.

2. Use DaemonCapabilityClient instead of CapabilityExecutor

from axor_core.capability.daemon_client import DaemonCapabilityClient
import axor_claude

session = axor_claude.make_session(
    api_key="sk-ant-...",
    capability_executor=DaemonCapabilityClient(
        socket_path="~/.axor/daemon.sock",
        mode="production",
    ),
)

result = await session.run("Write tests for the auth module")

No other changes. DaemonCapabilityClient exposes the same interface as CapabilityExecutor.


Operator Policies

The operator policy is the capability ceiling. Clients cannot exceed it.

axor-daemon start --policy focused_readonly     # read + search only, no writes
axor-daemon start --policy focused_generative   # read + write, no bash (default)
axor-daemon start --policy focused_mutative     # read + write + bash
axor-daemon start --policy moderate_mutative    # broad context, bash, shallow children
axor-daemon start --policy expansive            # full capability surface
Policy read write bash search children
focused_readonly
focused_generative
focused_mutative
moderate_mutative shallow
expansive

CLI Reference

axor-daemon start [options]

  --socket PATH      Unix socket path (default: ~/.axor/daemon.sock)
  --policy NAME      Operator policy ceiling (default: focused_generative)
  --log-level LEVEL  DEBUG | INFO | WARNING | ERROR (default: INFO)

Wire Protocol

Communication is length-prefixed JSON over a Unix domain socket. Every message carries a protocol version field "v". Version mismatches are rejected at handshake — there is no silent fallback.

Client → {"v": 1, "type": "hello", "mode": "production"}
Server → {"v": 1, "type": "ready"}

Client → {"v": 1, "type": "tool_call", "call_id": "a1b2c3", "tool": "read",
           "args": {"path": "auth.py"}, "allowed_tools": ["read", "search"]}
Server → {"v": 1, "type": "tool_result", "call_id": "a1b2c3",
           "decision": "approved", "result": "...", "denial_reason": null}

Client → {"v": 1, "type": "bye"}

Framing: 4-byte big-endian unsigned int (payload length) + JSON bytes. Maximum message size: 8 MB.

Backpressure: the server enforces a maximum of 64 concurrent connections. Connections beyond this limit receive an immediate rejected response. Each connection has a 30s read timeout — a stalled client does not hold a session indefinitely.

One connection per session. If the connection is lost mid-session, DaemonCapabilityClient raises DaemonUnavailableError — fail-closed by design.


Fail-Closed Guarantee

If the daemon is unreachable, DaemonCapabilityClient.execute() raises DaemonUnavailableError. Execution stops. It never silently falls back to direct tool execution.

from axor_core.errors.exceptions import DaemonUnavailableError

try:
    result = await session.run("audit the auth module")
except DaemonUnavailableError as e:
    # daemon not running — do not proceed
    raise

Registering Tool Handlers

Tool handlers live in the daemon, not in the client. Extend the daemon at startup:

from axor_daemon.enforcer import DaemonEnforcer
from axor_daemon.server import DaemonServer

enforcer = DaemonEnforcer(
    operator_policy=operator_policy,
    exec_timeout=30.0,          # seconds per handler call, default 60
    handlers={
        "read":   MyReadHandler(),
        "write":  MyWriteHandler(),
        "bash":   MyBashHandler(),
        "search": MySearchHandler(),
    },
)
server = DaemonServer(enforcer=enforcer)
await server.start("~/.axor/daemon.sock")
await server.serve_forever()

axor-claude ships ready-made handlers for Claude tool use. Register them with the daemon at startup — not with the session.


Known Limitations

Socket access is OS-level, not cryptographic. Any process running as the same OS user may connect to the socket. For multi-user or container environments, run the agent in a separate OS user or apply additional access controls (e.g., systemd socket activation with User=).

Path normalization is not an allowlist. os.path.normpath resolves ../ sequences so handlers always receive clean paths. It does not enforce which paths are allowed — that remains the handler's or operator's responsibility. Use CapabilityLease with allowed_paths for path allowlists.

Exec timeout kills slow handlers, not hanging syscalls. asyncio.wait_for cancels the coroutine. If a handler blocks on a non-async call (e.g., a synchronous subprocess), the cancel will not interrupt it immediately. Use asyncio.create_subprocess_exec for shell commands inside handlers.

Trace is client-side. Audit traces are written by the agent process, not the daemon. A compromised worker could suppress or mutate trace writes. Daemon-side audit logging is planned for a future release.

For stronger guarantees, combine axor-daemon with OS-level sandboxing (seccomp, Landlock, container isolation) to restrict what the agent process can do even beyond the governance boundary. That is Level 2.


Requirements

  • Python 3.11+
  • axor-core >= 0.5.0
  • No additional dependencies — stdlib asyncio only

Ecosystem

Package Role
axor-core Governance kernel — defines the contracts axor-daemon implements
axor-daemon Process-isolated capability executor — this package
axor-claude Claude / Claude Code adapter — provides tool handlers
axor-cli Governed terminal runtime
axor-memory-sqlite Cross-session memory (SQLite)
axor-classifier-simple ML task signal derivation (optional)
axor-benchmarks Governance proof layer

License

Apache-2.0 — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

axor_daemon-0.3.0.tar.gz (17.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

axor_daemon-0.3.0-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file axor_daemon-0.3.0.tar.gz.

File metadata

  • Download URL: axor_daemon-0.3.0.tar.gz
  • Upload date:
  • Size: 17.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for axor_daemon-0.3.0.tar.gz
Algorithm Hash digest
SHA256 57e90bb0cea3d649b29530ddfb0f2a1322b7098a6f5267fca0c235d2edc9e687
MD5 aebd87e362eb6289cf851b214950dad3
BLAKE2b-256 1547965cfbdfad39f17d49ff1fe86d0848b0faccfa1ca8052d2f249a523e9c52

See more details on using hashes here.

Provenance

The following attestation bundles were made for axor_daemon-0.3.0.tar.gz:

Publisher: ci.yml on Bucha11/axor-daemon

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file axor_daemon-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: axor_daemon-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 16.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for axor_daemon-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1fc9a1d72464751ae3aa4497a8e7f88be0d1be09def6e56872ffe22d14631df6
MD5 bec471831af0c2bfed393b57def1fedf
BLAKE2b-256 8087db32a6c6b7be53f99f4387fd35f90ee67b92d8d8427907798d17c3c0e428

See more details on using hashes here.

Provenance

The following attestation bundles were made for axor_daemon-0.3.0-py3-none-any.whl:

Publisher: ci.yml on Bucha11/axor-daemon

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page