axor-daemon 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ name: CI/CD
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*.*.*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ test:
12
+ name: Test (Python ${{ matrix.python-version }})
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.11", "3.12"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ cache: pip
25
+
26
+ - name: Install
27
+ run: |
28
+ pip install -e ".[dev]"
29
+
30
+ - name: Run tests
31
+ run: pytest tests/ -v --tb=short
32
+
33
+ publish:
34
+ name: Publish to PyPI
35
+ needs: test
36
+ runs-on: ubuntu-latest
37
+ if: startsWith(github.ref, 'refs/tags/v')
38
+ environment: pypi
39
+
40
+ permissions:
41
+ id-token: write # for trusted publishing
42
+
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ - uses: actions/setup-python@v5
47
+ with:
48
+ python-version: "3.12"
49
+
50
+ - name: Verify tag matches package version
51
+ run: |
52
+ python - << 'EOF'
53
+ import pathlib
54
+ import re
55
+ import sys
56
+ import tomllib
57
+
58
+ ref = "${{ github.ref_name }}"
59
+ m = re.fullmatch(r"v(\d+\.\d+\.\d+)", ref)
60
+ if not m:
61
+ print(f"Tag {ref!r} must match vX.Y.Z")
62
+ sys.exit(1)
63
+
64
+ tag_version = m.group(1)
65
+ data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
66
+ pkg_version = data["project"]["version"]
67
+
68
+ if tag_version != pkg_version:
69
+ print(f"Version mismatch: tag={tag_version}, pyproject={pkg_version}")
70
+ sys.exit(1)
71
+
72
+ print(f"Version check passed: {pkg_version}")
73
+ EOF
74
+
75
+ - name: Build
76
+ run: |
77
+ pip install hatchling build
78
+ python -m build
79
+
80
+ - name: Publish to PyPI
81
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ dist/
6
+ *.egg-info/
7
+ .venv/
8
+ venv/
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.4
2
+ Name: axor-daemon
3
+ Version: 0.2.0
4
+ Summary: Process-isolated capability executor for axor-core
5
+ Project-URL: Repository, https://github.com/Bucha11/axor-daemon
6
+ License: MIT
7
+ Keywords: agents,ai,governance,llm,security
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: axor-core<0.7,>=0.6.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
12
+ Requires-Dist: pytest>=8.0; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # axor-daemon
16
+
17
+ [![CI](https://github.com/Bucha11/axor-daemon/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Bucha11/axor-daemon/actions/workflows/ci.yml)
18
+ [![PyPI](https://img.shields.io/pypi/v/axor-daemon?cacheSeconds=300)](https://pypi.org/project/axor-daemon/)
19
+ [![Python](https://img.shields.io/pypi/pyversions/axor-daemon?cacheSeconds=300)](https://pypi.org/project/axor-daemon/)
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
21
+
22
+ **Process-isolated capability executor for [axor-core](https://github.com/Bucha11/axor-core).**
23
+
24
+ axor-core governs what agents are *allowed* to do. axor-daemon enforces it from *outside the agent process*.
25
+
26
+ ---
27
+
28
+ ## The Problem with Library-Only Governance
29
+
30
+ 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.
31
+
32
+ axor-daemon moves tool execution across a process boundary:
33
+
34
+ ```
35
+ Agent process AxorDaemon process
36
+ ──────────────────────────── ─────────────────────────────────
37
+ GovernedSession DaemonServer (mode 0600 socket)
38
+ IntentLoop DaemonEnforcer
39
+ DaemonCapabilityClient ──────► operator_policy (ceiling)
40
+ (no tool impls here) socket path normalization
41
+ ◄────── exec timeout per handler
42
+ approved result | DENIED
43
+ ```
44
+
45
+ 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.
46
+
47
+ ---
48
+
49
+ ## Enforcement Model
50
+
51
+ Every tool call passes independent checks in the daemon:
52
+
53
+ **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.
54
+
55
+ **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.
56
+
57
+ **3. Operator ceiling** — `operator_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.
58
+
59
+ **4. Client ceiling** — `allowed_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.
60
+
61
+ **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.
62
+
63
+ **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.
64
+
65
+ ```
66
+ Client sends: tool="bash", allowed_tools=["bash", "read"]
67
+
68
+ Operator policy = focused_readonly (allow_bash=False)
69
+ → DENIED operator ceiling "bash not permitted by operator policy"
70
+
71
+ Client sends: tool="read", args={"path": "../../etc/passwd"}, allowed_tools=["read"]
72
+ Operator policy = focused_readonly (allow_read=True)
73
+ → daemon normalizes path → "/etc/passwd"
74
+ → handler receives normalized args, never raw client string
75
+
76
+ Client sends: tool="read", allowed_tools=[] ← excluded read
77
+ → DENIED session ceiling "read not in session allowed_tools"
78
+ ```
79
+
80
+ 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.
81
+
82
+ ---
83
+
84
+ ## Installation
85
+
86
+ ```bash
87
+ pip install axor-daemon
88
+ ```
89
+
90
+ Requires `axor-core >= 0.5.0, < 0.6`. Zero additional dependencies — stdlib `asyncio` only.
91
+
92
+ ---
93
+
94
+ ## Quick Start
95
+
96
+ **1. Start the daemon**
97
+
98
+ ```bash
99
+ axor-daemon start --policy focused_generative
100
+ ```
101
+
102
+ The daemon loads the operator policy, creates `~/.axor/daemon.sock` with permissions `0600`, and begins accepting connections.
103
+
104
+ **2. Use `DaemonCapabilityClient` instead of `CapabilityExecutor`**
105
+
106
+ ```python
107
+ from axor_core.capability.daemon_client import DaemonCapabilityClient
108
+ import axor_claude
109
+
110
+ session = axor_claude.make_session(
111
+ api_key="sk-ant-...",
112
+ capability_executor=DaemonCapabilityClient(
113
+ socket_path="~/.axor/daemon.sock",
114
+ mode="production",
115
+ ),
116
+ )
117
+
118
+ result = await session.run("Write tests for the auth module")
119
+ ```
120
+
121
+ No other changes. `DaemonCapabilityClient` exposes the same interface as `CapabilityExecutor`.
122
+
123
+ ---
124
+
125
+ ## Operator Policies
126
+
127
+ The operator policy is the capability ceiling. Clients cannot exceed it.
128
+
129
+ ```bash
130
+ axor-daemon start --policy focused_readonly # read + search only, no writes
131
+ axor-daemon start --policy focused_generative # read + write, no bash (default)
132
+ axor-daemon start --policy focused_mutative # read + write + bash
133
+ axor-daemon start --policy moderate_mutative # broad context, bash, shallow children
134
+ axor-daemon start --policy expansive # full capability surface
135
+ ```
136
+
137
+ | Policy | read | write | bash | search | children |
138
+ |--------|------|-------|------|--------|----------|
139
+ | `focused_readonly` | ✓ | — | — | ✓ | — |
140
+ | `focused_generative` | ✓ | ✓ | — | ✓ | — |
141
+ | `focused_mutative` | ✓ | ✓ | ✓ | ✓ | — |
142
+ | `moderate_mutative` | ✓ | ✓ | ✓ | ✓ | shallow |
143
+ | `expansive` | ✓ | ✓ | ✓ | ✓ | ✓ |
144
+
145
+ ---
146
+
147
+ ## CLI Reference
148
+
149
+ ```
150
+ axor-daemon start [options]
151
+
152
+ --socket PATH Unix socket path (default: ~/.axor/daemon.sock)
153
+ --policy NAME Operator policy ceiling (default: focused_generative)
154
+ --log-level LEVEL DEBUG | INFO | WARNING | ERROR (default: INFO)
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Wire Protocol
160
+
161
+ 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.
162
+
163
+ ```
164
+ Client → {"v": 1, "type": "hello", "mode": "production"}
165
+ Server → {"v": 1, "type": "ready"}
166
+
167
+ Client → {"v": 1, "type": "tool_call", "call_id": "a1b2c3", "tool": "read",
168
+ "args": {"path": "auth.py"}, "allowed_tools": ["read", "search"]}
169
+ Server → {"v": 1, "type": "tool_result", "call_id": "a1b2c3",
170
+ "decision": "approved", "result": "...", "denial_reason": null}
171
+
172
+ Client → {"v": 1, "type": "bye"}
173
+ ```
174
+
175
+ **Framing:** 4-byte big-endian unsigned int (payload length) + JSON bytes. Maximum message size: 8 MB.
176
+
177
+ **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.
178
+
179
+ One connection per session. If the connection is lost mid-session, `DaemonCapabilityClient` raises `DaemonUnavailableError` — fail-closed by design.
180
+
181
+ ---
182
+
183
+ ## Fail-Closed Guarantee
184
+
185
+ If the daemon is unreachable, `DaemonCapabilityClient.execute()` raises `DaemonUnavailableError`. Execution stops. It never silently falls back to direct tool execution.
186
+
187
+ ```python
188
+ from axor_core.errors.exceptions import DaemonUnavailableError
189
+
190
+ try:
191
+ result = await session.run("audit the auth module")
192
+ except DaemonUnavailableError as e:
193
+ # daemon not running — do not proceed
194
+ raise
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Registering Tool Handlers
200
+
201
+ Tool handlers live in the daemon, not in the client. Extend the daemon at startup:
202
+
203
+ ```python
204
+ from axor_daemon.enforcer import DaemonEnforcer
205
+ from axor_daemon.server import DaemonServer
206
+
207
+ enforcer = DaemonEnforcer(
208
+ operator_policy=operator_policy,
209
+ exec_timeout=30.0, # seconds per handler call, default 60
210
+ handlers={
211
+ "read": MyReadHandler(),
212
+ "write": MyWriteHandler(),
213
+ "bash": MyBashHandler(),
214
+ "search": MySearchHandler(),
215
+ },
216
+ )
217
+ server = DaemonServer(enforcer=enforcer)
218
+ await server.start("~/.axor/daemon.sock")
219
+ await server.serve_forever()
220
+ ```
221
+
222
+ `axor-claude` ships ready-made handlers for Claude tool use. Register them with the daemon at startup — not with the session.
223
+
224
+ ---
225
+
226
+ ## Known Limitations
227
+
228
+ **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=`).
229
+
230
+ **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.
231
+
232
+ **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.
233
+
234
+ **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.
235
+
236
+ 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.
237
+
238
+ ---
239
+
240
+ ## Requirements
241
+
242
+ - Python 3.11+
243
+ - [`axor-core`](https://github.com/Bucha11/axor-core) >= 0.5.0
244
+ - No additional dependencies — stdlib `asyncio` only
245
+
246
+ ---
247
+
248
+ ## Ecosystem
249
+
250
+ | Package | Role |
251
+ |---------|------|
252
+ | [`axor-core`](https://github.com/Bucha11/axor-core) | Governance kernel — defines the contracts axor-daemon implements |
253
+ | [`axor-daemon`](https://github.com/Bucha11/axor-daemon) | Process-isolated capability executor — this package |
254
+ | [`axor-claude`](https://github.com/Bucha11/axor-claude) | Claude / Claude Code adapter — provides tool handlers |
255
+ | [`axor-cli`](https://github.com/Bucha11/axor-cli) | Governed terminal runtime |
256
+ | [`axor-memory-sqlite`](https://github.com/Bucha11/axor-memory-sqlite) | Cross-session memory (SQLite) |
257
+ | [`axor-classifier-simple`](https://github.com/Bucha11/axor-classifier-simple) | ML task signal derivation (optional) |
258
+ | [`axor-benchmarks`](https://github.com/Bucha11/axor-benchmarks) | Governance proof layer |
259
+
260
+ ---
261
+
262
+ ## License
263
+
264
+ MIT
@@ -0,0 +1,250 @@
1
+ # axor-daemon
2
+
3
+ [![CI](https://github.com/Bucha11/axor-daemon/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Bucha11/axor-daemon/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/axor-daemon?cacheSeconds=300)](https://pypi.org/project/axor-daemon/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/axor-daemon?cacheSeconds=300)](https://pypi.org/project/axor-daemon/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ **Process-isolated capability executor for [axor-core](https://github.com/Bucha11/axor-core).**
9
+
10
+ axor-core governs what agents are *allowed* to do. axor-daemon enforces it from *outside the agent process*.
11
+
12
+ ---
13
+
14
+ ## The Problem with Library-Only Governance
15
+
16
+ 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.
17
+
18
+ axor-daemon moves tool execution across a process boundary:
19
+
20
+ ```
21
+ Agent process AxorDaemon process
22
+ ──────────────────────────── ─────────────────────────────────
23
+ GovernedSession DaemonServer (mode 0600 socket)
24
+ IntentLoop DaemonEnforcer
25
+ DaemonCapabilityClient ──────► operator_policy (ceiling)
26
+ (no tool impls here) socket path normalization
27
+ ◄────── exec timeout per handler
28
+ approved result | DENIED
29
+ ```
30
+
31
+ 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.
32
+
33
+ ---
34
+
35
+ ## Enforcement Model
36
+
37
+ Every tool call passes independent checks in the daemon:
38
+
39
+ **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.
40
+
41
+ **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.
42
+
43
+ **3. Operator ceiling** — `operator_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.
44
+
45
+ **4. Client ceiling** — `allowed_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.
46
+
47
+ **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.
48
+
49
+ **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.
50
+
51
+ ```
52
+ Client sends: tool="bash", allowed_tools=["bash", "read"]
53
+
54
+ Operator policy = focused_readonly (allow_bash=False)
55
+ → DENIED operator ceiling "bash not permitted by operator policy"
56
+
57
+ Client sends: tool="read", args={"path": "../../etc/passwd"}, allowed_tools=["read"]
58
+ Operator policy = focused_readonly (allow_read=True)
59
+ → daemon normalizes path → "/etc/passwd"
60
+ → handler receives normalized args, never raw client string
61
+
62
+ Client sends: tool="read", allowed_tools=[] ← excluded read
63
+ → DENIED session ceiling "read not in session allowed_tools"
64
+ ```
65
+
66
+ 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.
67
+
68
+ ---
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install axor-daemon
74
+ ```
75
+
76
+ Requires `axor-core >= 0.5.0, < 0.6`. Zero additional dependencies — stdlib `asyncio` only.
77
+
78
+ ---
79
+
80
+ ## Quick Start
81
+
82
+ **1. Start the daemon**
83
+
84
+ ```bash
85
+ axor-daemon start --policy focused_generative
86
+ ```
87
+
88
+ The daemon loads the operator policy, creates `~/.axor/daemon.sock` with permissions `0600`, and begins accepting connections.
89
+
90
+ **2. Use `DaemonCapabilityClient` instead of `CapabilityExecutor`**
91
+
92
+ ```python
93
+ from axor_core.capability.daemon_client import DaemonCapabilityClient
94
+ import axor_claude
95
+
96
+ session = axor_claude.make_session(
97
+ api_key="sk-ant-...",
98
+ capability_executor=DaemonCapabilityClient(
99
+ socket_path="~/.axor/daemon.sock",
100
+ mode="production",
101
+ ),
102
+ )
103
+
104
+ result = await session.run("Write tests for the auth module")
105
+ ```
106
+
107
+ No other changes. `DaemonCapabilityClient` exposes the same interface as `CapabilityExecutor`.
108
+
109
+ ---
110
+
111
+ ## Operator Policies
112
+
113
+ The operator policy is the capability ceiling. Clients cannot exceed it.
114
+
115
+ ```bash
116
+ axor-daemon start --policy focused_readonly # read + search only, no writes
117
+ axor-daemon start --policy focused_generative # read + write, no bash (default)
118
+ axor-daemon start --policy focused_mutative # read + write + bash
119
+ axor-daemon start --policy moderate_mutative # broad context, bash, shallow children
120
+ axor-daemon start --policy expansive # full capability surface
121
+ ```
122
+
123
+ | Policy | read | write | bash | search | children |
124
+ |--------|------|-------|------|--------|----------|
125
+ | `focused_readonly` | ✓ | — | — | ✓ | — |
126
+ | `focused_generative` | ✓ | ✓ | — | ✓ | — |
127
+ | `focused_mutative` | ✓ | ✓ | ✓ | ✓ | — |
128
+ | `moderate_mutative` | ✓ | ✓ | ✓ | ✓ | shallow |
129
+ | `expansive` | ✓ | ✓ | ✓ | ✓ | ✓ |
130
+
131
+ ---
132
+
133
+ ## CLI Reference
134
+
135
+ ```
136
+ axor-daemon start [options]
137
+
138
+ --socket PATH Unix socket path (default: ~/.axor/daemon.sock)
139
+ --policy NAME Operator policy ceiling (default: focused_generative)
140
+ --log-level LEVEL DEBUG | INFO | WARNING | ERROR (default: INFO)
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Wire Protocol
146
+
147
+ 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.
148
+
149
+ ```
150
+ Client → {"v": 1, "type": "hello", "mode": "production"}
151
+ Server → {"v": 1, "type": "ready"}
152
+
153
+ Client → {"v": 1, "type": "tool_call", "call_id": "a1b2c3", "tool": "read",
154
+ "args": {"path": "auth.py"}, "allowed_tools": ["read", "search"]}
155
+ Server → {"v": 1, "type": "tool_result", "call_id": "a1b2c3",
156
+ "decision": "approved", "result": "...", "denial_reason": null}
157
+
158
+ Client → {"v": 1, "type": "bye"}
159
+ ```
160
+
161
+ **Framing:** 4-byte big-endian unsigned int (payload length) + JSON bytes. Maximum message size: 8 MB.
162
+
163
+ **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.
164
+
165
+ One connection per session. If the connection is lost mid-session, `DaemonCapabilityClient` raises `DaemonUnavailableError` — fail-closed by design.
166
+
167
+ ---
168
+
169
+ ## Fail-Closed Guarantee
170
+
171
+ If the daemon is unreachable, `DaemonCapabilityClient.execute()` raises `DaemonUnavailableError`. Execution stops. It never silently falls back to direct tool execution.
172
+
173
+ ```python
174
+ from axor_core.errors.exceptions import DaemonUnavailableError
175
+
176
+ try:
177
+ result = await session.run("audit the auth module")
178
+ except DaemonUnavailableError as e:
179
+ # daemon not running — do not proceed
180
+ raise
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Registering Tool Handlers
186
+
187
+ Tool handlers live in the daemon, not in the client. Extend the daemon at startup:
188
+
189
+ ```python
190
+ from axor_daemon.enforcer import DaemonEnforcer
191
+ from axor_daemon.server import DaemonServer
192
+
193
+ enforcer = DaemonEnforcer(
194
+ operator_policy=operator_policy,
195
+ exec_timeout=30.0, # seconds per handler call, default 60
196
+ handlers={
197
+ "read": MyReadHandler(),
198
+ "write": MyWriteHandler(),
199
+ "bash": MyBashHandler(),
200
+ "search": MySearchHandler(),
201
+ },
202
+ )
203
+ server = DaemonServer(enforcer=enforcer)
204
+ await server.start("~/.axor/daemon.sock")
205
+ await server.serve_forever()
206
+ ```
207
+
208
+ `axor-claude` ships ready-made handlers for Claude tool use. Register them with the daemon at startup — not with the session.
209
+
210
+ ---
211
+
212
+ ## Known Limitations
213
+
214
+ **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=`).
215
+
216
+ **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.
217
+
218
+ **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.
219
+
220
+ **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.
221
+
222
+ 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.
223
+
224
+ ---
225
+
226
+ ## Requirements
227
+
228
+ - Python 3.11+
229
+ - [`axor-core`](https://github.com/Bucha11/axor-core) >= 0.5.0
230
+ - No additional dependencies — stdlib `asyncio` only
231
+
232
+ ---
233
+
234
+ ## Ecosystem
235
+
236
+ | Package | Role |
237
+ |---------|------|
238
+ | [`axor-core`](https://github.com/Bucha11/axor-core) | Governance kernel — defines the contracts axor-daemon implements |
239
+ | [`axor-daemon`](https://github.com/Bucha11/axor-daemon) | Process-isolated capability executor — this package |
240
+ | [`axor-claude`](https://github.com/Bucha11/axor-claude) | Claude / Claude Code adapter — provides tool handlers |
241
+ | [`axor-cli`](https://github.com/Bucha11/axor-cli) | Governed terminal runtime |
242
+ | [`axor-memory-sqlite`](https://github.com/Bucha11/axor-memory-sqlite) | Cross-session memory (SQLite) |
243
+ | [`axor-classifier-simple`](https://github.com/Bucha11/axor-classifier-simple) | ML task signal derivation (optional) |
244
+ | [`axor-benchmarks`](https://github.com/Bucha11/axor-benchmarks) | Governance proof layer |
245
+
246
+ ---
247
+
248
+ ## License
249
+
250
+ MIT
@@ -0,0 +1,4 @@
1
+ """axor-daemon — process-isolated capability executor for axor-core."""
2
+ from axor_daemon._version import get_version
3
+
4
+ __version__ = get_version("axor-daemon")