awnode 0.3.2__tar.gz → 0.3.4__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.
Files changed (83) hide show
  1. awnode-0.3.4/CLAUDE.md +2 -0
  2. {awnode-0.3.2 → awnode-0.3.4}/PKG-INFO +40 -7
  3. {awnode-0.3.2 → awnode-0.3.4}/README.md +38 -5
  4. {awnode-0.3.2 → awnode-0.3.4}/awnode/__init__.py +1 -1
  5. {awnode-0.3.2 → awnode-0.3.4}/awnode/_doctor.py +2 -2
  6. {awnode-0.3.2 → awnode-0.3.4}/awnode/cli.py +51 -5
  7. {awnode-0.3.2 → awnode-0.3.4}/awnode/hub.py +20 -1
  8. {awnode-0.3.2 → awnode-0.3.4}/awnode/mcp_server.py +248 -88
  9. {awnode-0.3.2 → awnode-0.3.4}/awnode/proxy.py +14 -2
  10. {awnode-0.3.2 → awnode-0.3.4}/awnode/server.py +483 -70
  11. {awnode-0.3.2 → awnode-0.3.4}/awnode/sync.py +79 -6
  12. awnode-0.3.4/awnode/update_check.py +245 -0
  13. {awnode-0.3.2 → awnode-0.3.4}/docs/aither-manifest.json +4 -4
  14. awnode-0.3.4/docs/index.html +1360 -0
  15. awnode-0.3.4/docs/llms.txt +33 -0
  16. {awnode-0.3.2 → awnode-0.3.4}/pyproject.toml +9 -12
  17. awnode-0.3.4/tests/test_extension_contract.py +357 -0
  18. awnode-0.3.4/tests/test_hub_error_cooldown.py +29 -0
  19. awnode-0.3.4/tests/test_mcp_api_compat.py +60 -0
  20. awnode-0.3.4/tests/test_mcp_mode_reprobe.py +31 -0
  21. awnode-0.3.4/tests/test_mcp_sse_transport.py +142 -0
  22. awnode-0.3.4/tests/test_pair_mode_route.py +23 -0
  23. {awnode-0.3.2 → awnode-0.3.4}/tests/test_proxy_plane.py +18 -4
  24. awnode-0.3.4/tests/test_sync_enrollment_lifecycle.py +132 -0
  25. awnode-0.3.4/tests/test_update_check.py +209 -0
  26. awnode-0.3.2/docs/index.html +0 -1378
  27. {awnode-0.3.2 → awnode-0.3.4}/.github/workflows/publish.yml +0 -0
  28. {awnode-0.3.2 → awnode-0.3.4}/.gitignore +0 -0
  29. {awnode-0.3.2 → awnode-0.3.4}/Dockerfile +0 -0
  30. {awnode-0.3.2 → awnode-0.3.4}/LICENSE +0 -0
  31. {awnode-0.3.2 → awnode-0.3.4}/_smoke_check.py +0 -0
  32. {awnode-0.3.2 → awnode-0.3.4}/aither-constellation.js +0 -0
  33. {awnode-0.3.2 → awnode-0.3.4}/aither-manifest.json +0 -0
  34. {awnode-0.3.2 → awnode-0.3.4}/awnode/__main__.py +0 -0
  35. {awnode-0.3.2 → awnode-0.3.4}/awnode/comfyui_setup.py +0 -0
  36. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/__init__.py +0 -0
  37. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/autorig.yaml +0 -0
  38. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/canvas.yaml +0 -0
  39. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/comfyui-3d.yaml +0 -0
  40. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/comfyui.yaml +0 -0
  41. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/design.yaml +0 -0
  42. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/iris.yaml +0 -0
  43. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/muse.yaml +0 -0
  44. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/ollama.yaml +0 -0
  45. {awnode-0.3.2 → awnode-0.3.4}/awnode/components/sd-webui.yaml +0 -0
  46. {awnode-0.3.2 → awnode-0.3.4}/awnode/extensions.py +0 -0
  47. {awnode-0.3.2 → awnode-0.3.4}/awnode/license.py +0 -0
  48. {awnode-0.3.2 → awnode-0.3.4}/awnode/service_install.py +0 -0
  49. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/__init__.py +0 -0
  50. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/_http.py +0 -0
  51. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/_scopes.py +0 -0
  52. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/commands.py +0 -0
  53. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/context.py +0 -0
  54. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/documents.py +0 -0
  55. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/embeddings.py +0 -0
  56. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/extensions.py +0 -0
  57. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/filesystem.py +0 -0
  58. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/fleet_host.py +0 -0
  59. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/fleet_verbs.py +0 -0
  60. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/generation.py +0 -0
  61. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/git_tools.py +0 -0
  62. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/http_client.py +0 -0
  63. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/knowledge.py +0 -0
  64. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/memory.py +0 -0
  65. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/mermaid.py +0 -0
  66. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/notebooks.py +0 -0
  67. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/ollama.py +0 -0
  68. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/orchestrator.py +0 -0
  69. {awnode-0.3.2 → awnode-0.3.4}/awnode/tools/search.py +0 -0
  70. {awnode-0.3.2 → awnode-0.3.4}/dist_validate/aithernode-0.1.0-py3-none-any.whl +0 -0
  71. {awnode-0.3.2 → awnode-0.3.4}/docker-entrypoint.sh +0 -0
  72. {awnode-0.3.2 → awnode-0.3.4}/docs/aither-constellation.js +0 -0
  73. {awnode-0.3.2 → awnode-0.3.4}/packaging/aithernode-stub/README.md +0 -0
  74. {awnode-0.3.2 → awnode-0.3.4}/packaging/aithernode-stub/pyproject.toml +0 -0
  75. {awnode-0.3.2 → awnode-0.3.4}/tests/test_awnix_host_install.py +0 -0
  76. {awnode-0.3.2 → awnode-0.3.4}/tests/test_chat_endpoint_auth.py +0 -0
  77. {awnode-0.3.2 → awnode-0.3.4}/tests/test_component_image_env.py +0 -0
  78. {awnode-0.3.2 → awnode-0.3.4}/tests/test_custom_backend.py +0 -0
  79. {awnode-0.3.2 → awnode-0.3.4}/tests/test_fleet_host_tools.py +0 -0
  80. {awnode-0.3.2 → awnode-0.3.4}/tests/test_fleet_verbs_tools.py +0 -0
  81. {awnode-0.3.2 → awnode-0.3.4}/tests/test_mcp_server_auth.py +0 -0
  82. {awnode-0.3.2 → awnode-0.3.4}/tests/test_server_route_hygiene.py +0 -0
  83. {awnode-0.3.2 → awnode-0.3.4}/tests/test_server_security_hardening.py +0 -0
awnode-0.3.4/CLAUDE.md ADDED
@@ -0,0 +1,2 @@
1
+ See [llms.txt](llms.txt) for what this repo is, how to install it, and how it shows up in Claude Code.
2
+ Full ecosystem: https://aitherium.com/llms.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: awnode
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: awnode — lightweight local gateway for AitherOS
5
5
  Project-URL: Homepage, https://aitherium.com
6
6
  Author-email: Aitherium <hello@aitherium.com>
@@ -11,7 +11,7 @@ Requires-Python: >=3.10
11
11
  Requires-Dist: awdk>=1.3.0
12
12
  Requires-Dist: fastapi>=0.109.0
13
13
  Requires-Dist: httpx>=0.26.0
14
- Requires-Dist: mcp<2.0.0,>=1.0.0
14
+ Requires-Dist: mcp<3.0.0,>=1.0.0
15
15
  Requires-Dist: pyyaml>=6.0
16
16
  Requires-Dist: uvicorn[standard]>=0.27.0
17
17
  Provides-Extra: all
@@ -32,7 +32,7 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  **[Docs](https://aitherium.github.io/awnode/)** · [Source](https://github.com/Aitherium/awnode) · `pip install awnode` · [The Aither World](https://aitherium.github.io/)
34
34
 
35
- > **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awnode** is one of its 65 bricks — each installs on its own, runs offline, and needs no account.
35
+ > **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awnode** is one of its 67 bricks — each installs on its own, runs offline, and needs no account.
36
36
  >
37
37
  > **Start here:** Run it once and point one app at it instead of a vendor URL.
38
38
 
@@ -54,7 +54,7 @@ curl -fsSL https://launch.aitherium.com | bash
54
54
  ## Quick Start
55
55
 
56
56
  ```bash
57
- # Start with auto-detection (finds Genesis, vLLM, Ollama, or cloud)
57
+ # Start with auto-detection (finds Genesis, vLLM, llama.cpp, LM Studio, Ollama, or cloud)
58
58
  awnode start
59
59
 
60
60
  # Run the published container
@@ -77,11 +77,18 @@ awnode deploy my-service --target docker --strategy rolling
77
77
 
78
78
  ## Backend Priority (auto mode)
79
79
 
80
+ 0. **Custom** (`AITHERNODE_CUSTOM_BACKEND_URL`) — your own OpenAI-compatible backend, when it answers
80
81
  1. **Genesis** (localhost:8001) — full AitherOS pipeline (context, memory, agents)
81
82
  2. **vLLM** (localhost:8120) — direct GPU inference (OpenAI-compatible)
82
- 3. **Elysium** (cloud) — hosted AitherOS (requires API key)
83
- 4. **Ollama** (localhost:11434) — local CPU/GPU inference
84
- 5. **Standalone** — no LLM, tools-only mode
83
+ 3. **llama.cpp** (localhost:8200) / **Bonsai** (localhost:8092) — OpenAI-compatible llama.cpp servers
84
+ 4. **LM Studio** (127.0.0.1:1234) — LM Studio's local server (OpenAI-compatible; start it from LM Studio's Developer tab)
85
+ 5. **Elysium** (cloud) — hosted AitherOS (requires API key)
86
+ 6. **Ollama** (localhost:11434) — local CPU/GPU inference
87
+ 7. **Standalone** — no LLM, tools-only mode
88
+
89
+ OpenAI-compatible backends (vLLM, llama.cpp, Bonsai, LM Studio, custom) are detected by a
90
+ 200 on `/v1/models`, never by `/health`, so an unrelated service on the same port is not
91
+ mistaken for one.
85
92
 
86
93
  ## API
87
94
 
@@ -91,8 +98,25 @@ GET /status — Backend status (what's connected)
91
98
  POST /chat — Chat (proxied to best backend)
92
99
  POST /deploy — Deploy via AitherComet
93
100
  POST /connect — Register with Elysium
101
+ GET /v1/models — Models across the local OpenAI-compatible backends
102
+ POST /v1/chat/completions — OpenAI-compatible chat, routed to the best backend
103
+ GET /mcp/tools — Tool discovery
104
+ POST /mcp — MCP JSON-RPC 2.0: tools/list, tools/call
94
105
  ```
95
106
 
107
+ The awconnect browser extension (its published ids, plus any in `AWNODE_EXTENSION_IDS`)
108
+ may call `/health`, `/v1/models`, `/v1/chat/completions` and `POST /mcp`; every other
109
+ route refuses an extension origin. The node stays loopback-bound and the
110
+ loopback-or-enrolled-bearer gate still applies.
111
+
112
+ **Tool scope.** By default `POST /mcp` serves only `read_file`, `list_directory` (both
113
+ jailed to `AITHERNODE_FS_ROOT`) and `web_search`. `write_file`, `run_command` and every
114
+ other tool need a direct loopback caller (no proxy/tunnel headers) that is either a local
115
+ non-browser process sending `Authorization: Bearer <enrolled token or AITHER_MCP_KEY>`, or
116
+ any loopback caller when the node owner sets `AWNODE_BROWSER_FULL_TOOLS=1`. A browser or
117
+ extension origin, a tunnelled/remote request and an unauthenticated local process stay on
118
+ the read-only set; for full tool access from an IDE use `awnode mcp`.
119
+
96
120
  ## Environment Variables
97
121
 
98
122
  | Variable | Default | Description |
@@ -100,6 +124,9 @@ POST /connect — Register with Elysium
100
124
  | `AITHER_URL` | `http://localhost:8001` | Genesis URL |
101
125
  | `AITHER_VLLM_URL` | `http://localhost:8120` | vLLM URL |
102
126
  | `OLLAMA_HOST` | `http://localhost:11434` | Ollama URL |
127
+ | `AITHERNODE_LMSTUDIO_URL` | `http://127.0.0.1:1234` | LM Studio server URL (`LMSTUDIO_URL` also read) |
128
+ | `AWNODE_EXTENSION_IDS` | | Extra extension ids admitted on the extension routes (comma-separated) |
129
+ | `AWNODE_BROWSER_FULL_TOOLS` | | `1` lets direct loopback callers (browser/extension included) run every tool without a credential; tunnelled/remote callers stay read-only |
103
130
  | `AITHER_API_KEY` | | API key for cloud/auth |
104
131
  | `AITHERNODE_MODE` | `auto` | Force mode: auto/local/cloud/standalone |
105
132
  | `AITHERNODE_PORT` | `8090` | Server port |
@@ -132,9 +159,11 @@ Each installs on its own, works offline, and needs no account.
132
159
  | [awgit](https://github.com/Aitherium/awgit) | that no one else is editing this file | a lease, refused at commit time if you do not hold it |
133
160
  | [awdelphi](https://github.com/Aitherium/awdelphi) | one agent's confident take on a decision | the round trace, the anonymity, and who dissents |
134
161
  | [awclassify](https://github.com/Aitherium/awclassify) | a filename, a folder, or whoever last touched it | doc_type, visibility, audience and topics, with the evidence lines that decided each |
162
+ | [awdecide](https://github.com/Aitherium/awdecide) | a hosted classifier's probability that never learns whether it was right | the decision, its probability, and the calibration curve from your own resolved outcomes |
135
163
  | [awtoll](https://github.com/Aitherium/awtoll) | that your tooling is saving you context | the measured token cost of each tool call, and what the alternative cost |
136
164
  | [awseal](https://github.com/Aitherium/awseal) | that the artifact came from who you think | an Ed25519 seal — the key that verifies is not the key that forges |
137
165
  | [awshare](https://github.com/Aitherium/awshare) | that the download is intact | content-addressed bundles, verified on fetch |
166
+ | [awsuite](https://github.com/Aitherium/awsuite) | that an agent holding your mailbox will not send on its own | every send, draft, upload and create returns a dry-run until confirm is true |
138
167
  | [awnest](https://github.com/Aitherium/awnest) | that there is a person on the other end | a verdict with evidence, where "we could not tell" is not "yes" |
139
168
  | [awrena](https://github.com/Aitherium/awrena) | a leaderboard someone can edit, and votes nobody counted | a scored duel with both answers kept, and a result bound to them |
140
169
  | [awnboard](https://github.com/Aitherium/awnboard) | a share link anyone who sees it can use | an invitation addressed to one person, for one gate, revocable |
@@ -196,9 +225,11 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
196
225
  | [awgit](https://github.com/Aitherium/awgit) | Semantic version control on top of git — edit-ops and leases | [docs](https://aitherium.github.io/awgit/) |
197
226
  | [awdelphi](https://github.com/Aitherium/awdelphi) | Anonymous multi-round expert panels — a converged answer with a trace | [docs](https://aitherium.github.io/awdelphi/) |
198
227
  | [awclassify](https://github.com/Aitherium/awclassify) | Classify any document -- what it is, who may read it, who it is for, what it is about | — |
228
+ | [awdecide](https://github.com/Aitherium/awdecide) | One typed-decision contract -- choice / score / bool with a probability -- over a ladder of backends you already run (rules, tiny local models, an LLM's logprobs), fail-closed, with a Brier ledger that resolves every decision against its outcome | — |
199
229
  | [awtoll](https://github.com/Aitherium/awtoll) | What every tool call costs you in context, measured from your own transcripts | [docs](https://aitherium.github.io/awtoll/) |
200
230
  | [awseal](https://github.com/Aitherium/awseal) | Sign an artifact so a stranger can verify it | [docs](https://aitherium.github.io/awseal/) |
201
231
  | [awshare](https://github.com/Aitherium/awshare) | Publish an artifact and fetch it back verified | [docs](https://aitherium.github.io/awshare/) |
232
+ | [awsuite](https://github.com/Aitherium/awsuite) | Your Google Workspace as agent tools, and no write happens without a yes | — |
202
233
  | [awdit](https://github.com/Aitherium/awdit) | An append-only audit trail whose gaps are DETECTABLE | [docs](https://aitherium.github.io/awdit/) |
203
234
  | [awbac](https://github.com/Aitherium/awbac) | Role-based access control that fails closed and explains itself | [docs](https://aitherium.github.io/awbac/) |
204
235
  | [awiam](https://github.com/Aitherium/awiam) | Who is this caller? A directory and session store that fails honestly | [docs](https://aitherium.github.io/awiam/) |
@@ -251,6 +282,8 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
251
282
  | [awsettings](https://github.com/Aitherium/awsettings) | Your agent's permissions and config, following you to the next machine | [docs](https://aitherium.github.io/awsettings/) |
252
283
  | [awavatar](https://github.com/Aitherium/awavatar) | One character spec in, a rigged, animated, multi-style avatar pack out | [docs](https://aitherium.github.io/awavatar/) |
253
284
 
285
+ **Built on** [REA (Reverse Engineer Anything)](https://github.com/morluto/rea) · [llama.cpp](https://github.com/ggml-org/llama.cpp) · [vLLM](https://github.com/vllm-project/vllm) · [ComfyUI](https://github.com/comfyanonymous/ComfyUI) · [CentOS Stream](https://www.centos.org/centos-stream/) · [Podman](https://github.com/containers/podman) · [Docker](https://github.com/moby/moby) · [LanceDB](https://github.com/lancedb/lancedb) · [WireGuard](https://www.wireguard.com/) · [FFmpeg](https://ffmpeg.org/) · [Blender + Rigify](https://www.blender.org/) · [headroom](https://github.com/headroomlabs-ai/headroom) · [SANA](https://github.com/NVlabs/Sana) · [Hunyuan3D](https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1) · [repowise](https://github.com/repowise-dev/repowise) · [Playwright](https://github.com/microsoft/playwright) · [Chromium](https://www.chromium.org/) · [Next.js](https://github.com/vercel/next.js) · [React](https://github.com/facebook/react).
286
+
254
287
  <div id="aither-constellation" data-self="awnode"></div>
255
288
  <script src="aither-constellation.js"></script>
256
289
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  **[Docs](https://aitherium.github.io/awnode/)** · [Source](https://github.com/Aitherium/awnode) · `pip install awnode` · [The Aither World](https://aitherium.github.io/)
6
6
 
7
- > **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awnode** is one of its 65 bricks — each installs on its own, runs offline, and needs no account.
7
+ > **The Aither World** is an operating system for agents — a Linux you can hand to one, the runtimes it works in, and the tools it works with. [awnix](https://github.com/Aitherium/awnix) is the Linux underneath it; **awnode** is one of its 67 bricks — each installs on its own, runs offline, and needs no account.
8
8
  >
9
9
  > **Start here:** Run it once and point one app at it instead of a vendor URL.
10
10
 
@@ -26,7 +26,7 @@ curl -fsSL https://launch.aitherium.com | bash
26
26
  ## Quick Start
27
27
 
28
28
  ```bash
29
- # Start with auto-detection (finds Genesis, vLLM, Ollama, or cloud)
29
+ # Start with auto-detection (finds Genesis, vLLM, llama.cpp, LM Studio, Ollama, or cloud)
30
30
  awnode start
31
31
 
32
32
  # Run the published container
@@ -49,11 +49,18 @@ awnode deploy my-service --target docker --strategy rolling
49
49
 
50
50
  ## Backend Priority (auto mode)
51
51
 
52
+ 0. **Custom** (`AITHERNODE_CUSTOM_BACKEND_URL`) — your own OpenAI-compatible backend, when it answers
52
53
  1. **Genesis** (localhost:8001) — full AitherOS pipeline (context, memory, agents)
53
54
  2. **vLLM** (localhost:8120) — direct GPU inference (OpenAI-compatible)
54
- 3. **Elysium** (cloud) — hosted AitherOS (requires API key)
55
- 4. **Ollama** (localhost:11434) — local CPU/GPU inference
56
- 5. **Standalone** — no LLM, tools-only mode
55
+ 3. **llama.cpp** (localhost:8200) / **Bonsai** (localhost:8092) — OpenAI-compatible llama.cpp servers
56
+ 4. **LM Studio** (127.0.0.1:1234) — LM Studio's local server (OpenAI-compatible; start it from LM Studio's Developer tab)
57
+ 5. **Elysium** (cloud) — hosted AitherOS (requires API key)
58
+ 6. **Ollama** (localhost:11434) — local CPU/GPU inference
59
+ 7. **Standalone** — no LLM, tools-only mode
60
+
61
+ OpenAI-compatible backends (vLLM, llama.cpp, Bonsai, LM Studio, custom) are detected by a
62
+ 200 on `/v1/models`, never by `/health`, so an unrelated service on the same port is not
63
+ mistaken for one.
57
64
 
58
65
  ## API
59
66
 
@@ -63,8 +70,25 @@ GET /status — Backend status (what's connected)
63
70
  POST /chat — Chat (proxied to best backend)
64
71
  POST /deploy — Deploy via AitherComet
65
72
  POST /connect — Register with Elysium
73
+ GET /v1/models — Models across the local OpenAI-compatible backends
74
+ POST /v1/chat/completions — OpenAI-compatible chat, routed to the best backend
75
+ GET /mcp/tools — Tool discovery
76
+ POST /mcp — MCP JSON-RPC 2.0: tools/list, tools/call
66
77
  ```
67
78
 
79
+ The awconnect browser extension (its published ids, plus any in `AWNODE_EXTENSION_IDS`)
80
+ may call `/health`, `/v1/models`, `/v1/chat/completions` and `POST /mcp`; every other
81
+ route refuses an extension origin. The node stays loopback-bound and the
82
+ loopback-or-enrolled-bearer gate still applies.
83
+
84
+ **Tool scope.** By default `POST /mcp` serves only `read_file`, `list_directory` (both
85
+ jailed to `AITHERNODE_FS_ROOT`) and `web_search`. `write_file`, `run_command` and every
86
+ other tool need a direct loopback caller (no proxy/tunnel headers) that is either a local
87
+ non-browser process sending `Authorization: Bearer <enrolled token or AITHER_MCP_KEY>`, or
88
+ any loopback caller when the node owner sets `AWNODE_BROWSER_FULL_TOOLS=1`. A browser or
89
+ extension origin, a tunnelled/remote request and an unauthenticated local process stay on
90
+ the read-only set; for full tool access from an IDE use `awnode mcp`.
91
+
68
92
  ## Environment Variables
69
93
 
70
94
  | Variable | Default | Description |
@@ -72,6 +96,9 @@ POST /connect — Register with Elysium
72
96
  | `AITHER_URL` | `http://localhost:8001` | Genesis URL |
73
97
  | `AITHER_VLLM_URL` | `http://localhost:8120` | vLLM URL |
74
98
  | `OLLAMA_HOST` | `http://localhost:11434` | Ollama URL |
99
+ | `AITHERNODE_LMSTUDIO_URL` | `http://127.0.0.1:1234` | LM Studio server URL (`LMSTUDIO_URL` also read) |
100
+ | `AWNODE_EXTENSION_IDS` | | Extra extension ids admitted on the extension routes (comma-separated) |
101
+ | `AWNODE_BROWSER_FULL_TOOLS` | | `1` lets direct loopback callers (browser/extension included) run every tool without a credential; tunnelled/remote callers stay read-only |
75
102
  | `AITHER_API_KEY` | | API key for cloud/auth |
76
103
  | `AITHERNODE_MODE` | `auto` | Force mode: auto/local/cloud/standalone |
77
104
  | `AITHERNODE_PORT` | `8090` | Server port |
@@ -104,9 +131,11 @@ Each installs on its own, works offline, and needs no account.
104
131
  | [awgit](https://github.com/Aitherium/awgit) | that no one else is editing this file | a lease, refused at commit time if you do not hold it |
105
132
  | [awdelphi](https://github.com/Aitherium/awdelphi) | one agent's confident take on a decision | the round trace, the anonymity, and who dissents |
106
133
  | [awclassify](https://github.com/Aitherium/awclassify) | a filename, a folder, or whoever last touched it | doc_type, visibility, audience and topics, with the evidence lines that decided each |
134
+ | [awdecide](https://github.com/Aitherium/awdecide) | a hosted classifier's probability that never learns whether it was right | the decision, its probability, and the calibration curve from your own resolved outcomes |
107
135
  | [awtoll](https://github.com/Aitherium/awtoll) | that your tooling is saving you context | the measured token cost of each tool call, and what the alternative cost |
108
136
  | [awseal](https://github.com/Aitherium/awseal) | that the artifact came from who you think | an Ed25519 seal — the key that verifies is not the key that forges |
109
137
  | [awshare](https://github.com/Aitherium/awshare) | that the download is intact | content-addressed bundles, verified on fetch |
138
+ | [awsuite](https://github.com/Aitherium/awsuite) | that an agent holding your mailbox will not send on its own | every send, draft, upload and create returns a dry-run until confirm is true |
110
139
  | [awnest](https://github.com/Aitherium/awnest) | that there is a person on the other end | a verdict with evidence, where "we could not tell" is not "yes" |
111
140
  | [awrena](https://github.com/Aitherium/awrena) | a leaderboard someone can edit, and votes nobody counted | a scored duel with both answers kept, and a result bound to them |
112
141
  | [awnboard](https://github.com/Aitherium/awnboard) | a share link anyone who sees it can use | an invitation addressed to one person, for one gate, revocable |
@@ -168,9 +197,11 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
168
197
  | [awgit](https://github.com/Aitherium/awgit) | Semantic version control on top of git — edit-ops and leases | [docs](https://aitherium.github.io/awgit/) |
169
198
  | [awdelphi](https://github.com/Aitherium/awdelphi) | Anonymous multi-round expert panels — a converged answer with a trace | [docs](https://aitherium.github.io/awdelphi/) |
170
199
  | [awclassify](https://github.com/Aitherium/awclassify) | Classify any document -- what it is, who may read it, who it is for, what it is about | — |
200
+ | [awdecide](https://github.com/Aitherium/awdecide) | One typed-decision contract -- choice / score / bool with a probability -- over a ladder of backends you already run (rules, tiny local models, an LLM's logprobs), fail-closed, with a Brier ledger that resolves every decision against its outcome | — |
171
201
  | [awtoll](https://github.com/Aitherium/awtoll) | What every tool call costs you in context, measured from your own transcripts | [docs](https://aitherium.github.io/awtoll/) |
172
202
  | [awseal](https://github.com/Aitherium/awseal) | Sign an artifact so a stranger can verify it | [docs](https://aitherium.github.io/awseal/) |
173
203
  | [awshare](https://github.com/Aitherium/awshare) | Publish an artifact and fetch it back verified | [docs](https://aitherium.github.io/awshare/) |
204
+ | [awsuite](https://github.com/Aitherium/awsuite) | Your Google Workspace as agent tools, and no write happens without a yes | — |
174
205
  | [awdit](https://github.com/Aitherium/awdit) | An append-only audit trail whose gaps are DETECTABLE | [docs](https://aitherium.github.io/awdit/) |
175
206
  | [awbac](https://github.com/Aitherium/awbac) | Role-based access control that fails closed and explains itself | [docs](https://aitherium.github.io/awbac/) |
176
207
  | [awiam](https://github.com/Aitherium/awiam) | Who is this caller? A directory and session store that fails honestly | [docs](https://aitherium.github.io/awiam/) |
@@ -223,6 +254,8 @@ Every repository here is public. Each publishes an `aither-manifest.json` beside
223
254
  | [awsettings](https://github.com/Aitherium/awsettings) | Your agent's permissions and config, following you to the next machine | [docs](https://aitherium.github.io/awsettings/) |
224
255
  | [awavatar](https://github.com/Aitherium/awavatar) | One character spec in, a rigged, animated, multi-style avatar pack out | [docs](https://aitherium.github.io/awavatar/) |
225
256
 
257
+ **Built on** [REA (Reverse Engineer Anything)](https://github.com/morluto/rea) · [llama.cpp](https://github.com/ggml-org/llama.cpp) · [vLLM](https://github.com/vllm-project/vllm) · [ComfyUI](https://github.com/comfyanonymous/ComfyUI) · [CentOS Stream](https://www.centos.org/centos-stream/) · [Podman](https://github.com/containers/podman) · [Docker](https://github.com/moby/moby) · [LanceDB](https://github.com/lancedb/lancedb) · [WireGuard](https://www.wireguard.com/) · [FFmpeg](https://ffmpeg.org/) · [Blender + Rigify](https://www.blender.org/) · [headroom](https://github.com/headroomlabs-ai/headroom) · [SANA](https://github.com/NVlabs/Sana) · [Hunyuan3D](https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1) · [repowise](https://github.com/repowise-dev/repowise) · [Playwright](https://github.com/microsoft/playwright) · [Chromium](https://www.chromium.org/) · [Next.js](https://github.com/vercel/next.js) · [React](https://github.com/facebook/react).
258
+
226
259
  <div id="aither-constellation" data-self="awnode"></div>
227
260
  <script src="aither-constellation.js"></script>
228
261
 
@@ -17,4 +17,4 @@ Install:
17
17
  aithernode status # Check what's connected
18
18
  """
19
19
 
20
- __version__ = "0.3.2"
20
+ __version__ = "0.3.4"
@@ -23,7 +23,7 @@ import sys
23
23
  #: package cannot read the registry, and a doctor that guessed at the family
24
24
  #: would go stale in silence. Regenerate to update.
25
25
  SELF = 'awnode'
26
- FAMILY = ['awask', 'awavatar', 'awbac', 'awbrain', 'awbrowse', 'awclassify', 'awdecide', 'awdeck', 'awdelphi', 'awdit', 'awembed', 'awevolve', 'awfind', 'awflow', 'awfocus', 'awgit', 'awgraph', 'awgym', 'awiam', 'awkno', 'awm', 'awmail', 'awmine', 'awnboard', 'awnest', 'awnet', 'awpool', 'awpredict', 'awprism', 'awprove', 'awreason', 'awrecover', 'awrecurse', 'awrelay', 'awrena', 'awrepl', 'awreport', 'awresearch', 'awrise', 'awrouter', 'awrtifact', 'awrun', 'awscreen', 'awseal', 'awsettings', 'awshare', 'awsprite', 'awstorage', 'awswarm', 'awtax', 'awtoll', 'awtunnel', 'awvision', 'awvoice', 'awwall', 'gawbbonet']
26
+ FAMILY = ['awask', 'awavatar', 'awbac', 'awbrain', 'awbrowse', 'awcams', 'awclassify', 'awdecide', 'awdeck', 'awdelphi', 'awdit', 'awembed', 'awevolve', 'awfind', 'awflow', 'awfocus', 'awgit', 'awgraph', 'awgym', 'awiam', 'awkno', 'awm', 'awmail', 'awmine', 'awnboard', 'awnest', 'awnet', 'awpool', 'awpredict', 'awprism', 'awprove', 'awreason', 'awrecover', 'awrecurse', 'awrelay', 'awrena', 'awrepl', 'awreport', 'awresearch', 'awrise', 'awrouter', 'awrtifact', 'awrun', 'awscope', 'awscreen', 'awseal', 'awsettings', 'awshare', 'awsprite', 'awstorage', 'awsuite', 'awswarm', 'awtax', 'awtoll', 'awtunnel', 'awvision', 'awvoice', 'awwall', 'gawbbonet']
27
27
  PAIRS_WITH = ['adk']
28
28
 
29
29
  #: This brick's OWN config, read out of its source at generation time.
@@ -33,7 +33,7 @@ PAIRS_WITH = ['adk']
33
33
  #: platform-wide vars it also touches would be noise, and a doctor that floods
34
34
  #: gets ignored.
35
35
  ENV_REQUIRED = []
36
- ENV_OPTIONAL = ['AITHERNODE_ALLOWED_ORIGINS', 'AITHERNODE_BIND_HOST', 'AITHERNODE_CUSTOM_BACKEND_API_KEY', 'AITHERNODE_CUSTOM_BACKEND_MODEL', 'AITHERNODE_CUSTOM_BACKEND_URL', 'AITHERNODE_FS_ROOT', 'AITHERNODE_MODE', 'AITHERNODE_PORT', 'AITHERNODE_PROXY_MAP', 'AITHERNODE_PROXY_TOKEN', 'AITHERNODE_SERVICE_HOST', 'AITHERNODE_TRUSTED_PROXIES', 'AWNODE_EXTENSION_IDS']
36
+ ENV_OPTIONAL = ['AITHERNODE_ALLOWED_ORIGINS', 'AITHERNODE_BIND_HOST', 'AITHERNODE_CUSTOM_BACKEND_API_KEY', 'AITHERNODE_CUSTOM_BACKEND_MODEL', 'AITHERNODE_CUSTOM_BACKEND_URL', 'AITHERNODE_FS_ROOT', 'AITHERNODE_LMSTUDIO_URL', 'AITHERNODE_MODE', 'AITHERNODE_PORT', 'AITHERNODE_PROXY_MAP', 'AITHERNODE_PROXY_TOKEN', 'AITHERNODE_SERVICE_HOST', 'AITHERNODE_TRUSTED_PROXIES', 'AWNODE_EXTENSION_IDS', 'AWNODE_IN_CONTAINER']
37
37
 
38
38
 
39
39
  def _installed(mod: str) -> "str | None":
@@ -12,6 +12,7 @@ awnode CLI
12
12
  awnode mcp --transport sse # Start MCP server (HTTP SSE)
13
13
  awnode mcp-config # Output .mcp.json config snippet
14
14
  awnode deploy <service> # Deploy via AitherComet
15
+ awnode pair-mode # Let a device you own find this one on the Wi-Fi (5 min)
15
16
  """
16
17
 
17
18
  import argparse
@@ -41,6 +42,15 @@ def main():
41
42
  if sys.argv[1:2] == ["hub"]:
42
43
  from awnode.hub import main as hub_main
43
44
  return hub_main(sys.argv[2:])
45
+ # `awnode pair-mode ...` -- advertise this box on the LAN for 5 minutes so a device that
46
+ # is already the owner's can approve it (adk/lan_pair.py; Phase 2 of device join).
47
+ if sys.argv[1:2] == ["pair-mode"]:
48
+ try:
49
+ from adk.lan_pair import main as pair_mode_main
50
+ except ImportError as exc:
51
+ print(f"pair-mode needs a newer awdk (pip install -U awdk): {exc}")
52
+ return 1
53
+ return pair_mode_main(sys.argv[2:])
44
54
  # License entitlement gate (logs tier, wires metering quotas; never blocks unless
45
55
  # AITHER_LICENSE_REQUIRE=1). adk must be importable for this to apply.
46
56
  try:
@@ -166,6 +176,14 @@ def main():
166
176
 
167
177
  args = parser.parse_args()
168
178
 
179
+ # Once-a-day PyPI check for a newer awnode (awnode/update_check.py): a cached
180
+ # notice prints now, a stale cache refreshes on a daemon thread. Skipped for
181
+ # the stdio MCP server, whose client owns the terminal. AWNODE_NO_UPDATE_CHECK=1
182
+ # turns it off.
183
+ if args.command and args.command != "mcp":
184
+ from awnode.update_check import cli_start_check
185
+ cli_start_check()
186
+
169
187
  if args.command == "start":
170
188
  if args.cloud:
171
189
  os.environ["AITHERNODE_MODE"] = "cloud"
@@ -178,6 +196,7 @@ def main():
178
196
  if args.genesis_url:
179
197
  os.environ["AITHER_URL"] = args.genesis_url
180
198
  os.environ["AITHERNODE_PORT"] = str(args.port)
199
+ os.environ["AITHERNODE_BIND_HOST"] = str(args.host) # the startup bind warning reads this env
181
200
 
182
201
  import uvicorn
183
202
  uvicorn.run(
@@ -329,11 +348,15 @@ async def _doctor(*, emit_json: bool = False, timeout: float = 3.0) -> int:
329
348
  "genesis": os.environ.get("AITHER_URL", "http://localhost:8001"),
330
349
  "vllm": os.environ.get("AITHER_VLLM_URL", "http://localhost:8120"),
331
350
  "ollama": os.environ.get("OLLAMA_HOST", "http://localhost:11434"),
351
+ # LM Studio has no /health; its OpenAI-compatible /v1/models is the liveness surface.
352
+ "lmstudio": os.environ.get(
353
+ "AITHERNODE_LMSTUDIO_URL", os.environ.get("LMSTUDIO_URL", "http://127.0.0.1:1234")
354
+ ),
332
355
  "gateway": os.environ.get("AITHER_CLOUD_URL", "https://gateway.aitherium.com"),
333
356
  }
334
357
  async with httpx.AsyncClient(timeout=timeout) as client:
335
358
  for name, url in backends.items():
336
- health_path = "/api/tags" if name == "ollama" else "/health"
359
+ health_path = {"ollama": "/api/tags", "lmstudio": "/v1/models"}.get(name, "/health")
337
360
  try:
338
361
  r = await client.get(url.rstrip("/") + health_path)
339
362
  if r.status_code < 500:
@@ -397,6 +420,22 @@ async def _status():
397
420
  icon = "+" if status == "UP" else "-"
398
421
  print(f" [{icon}] {name:12s} {url:40s} {status}")
399
422
 
423
+ # Enrollment: a dead or expiring token is the most common silent failure (sync,
424
+ # register and heartbeat all 401), so say it here in plain words.
425
+ try:
426
+ from awnode.sync import WorkspaceSyncClient
427
+ sc = WorkspaceSyncClient()
428
+ sc._load_local_state()
429
+ if sc._tenant_scoped_token:
430
+ exp = sc._token_expires_at or "unknown"
431
+ if sc.token_expired():
432
+ print(f" [-] Enrollment token EXPIRED ({exp}) -- re-enroll: "
433
+ "awnode connect --enroll-token=TOKEN")
434
+ else:
435
+ print(f" [+] Enrollment token valid until {exp}")
436
+ except Exception:
437
+ pass
438
+
400
439
  # Check for running Node
401
440
  try:
402
441
  async with httpx.AsyncClient(timeout=2.0) as c:
@@ -578,9 +617,8 @@ async def _connect(
578
617
 
579
618
  # Resolve portal URL
580
619
  if not portal_url:
581
- portal_url = os.environ.get(
582
- "AITHER_CLOUD_URL", "https://portal.aitherium.com"
583
- )
620
+ # api.aitherium.com is the live app host (Veil); portal.* is retired.
621
+ portal_url = os.environ.get("AITHER_CLOUD_URL", "https://api.aitherium.com")
584
622
 
585
623
  aither_home = Path(os.environ.get("AITHER_HOME", str(Path.home() / ".aither")))
586
624
  state_file = aither_home / "sync_state.json"
@@ -610,7 +648,15 @@ async def _connect(
610
648
  pass
611
649
  state["tenant_scoped_token"] = workspace_token
612
650
  state["node_id"] = node_id
613
- state_file.write_text(json.dumps(state, indent=2))
651
+ # Recorded so the node can say "re-enroll" before the 30-day
652
+ # token dies, instead of 401ing silently afterwards.
653
+ if data.get("expires_at"):
654
+ state["tenant_scoped_token_expires_at"] = data["expires_at"]
655
+ # 0600 from the first byte: this file holds a bearer token,
656
+ # and write_text() created it at the default umask.
657
+ fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
658
+ with os.fdopen(fd, "w") as fh:
659
+ fh.write(json.dumps(state, indent=2))
614
660
  print(
615
661
  f"✓ Token saved for node {node_id} "
616
662
  f"(expires {data.get('expires_at', '?')})"
@@ -371,6 +371,9 @@ class Hub:
371
371
  self.cache_path = state_dir / "tools-cache.json"
372
372
  self.tool_cache: Dict[str, List[dict]] = self._load_cache()
373
373
  self.errors: Dict[str, str] = {}
374
+ # When each error was recorded. An error used to be permanent: one transient
375
+ # start failure hid that brick's tools in every session until the hub restarted.
376
+ self.error_at: Dict[str, float] = {}
374
377
  self.started_at = time.time()
375
378
  self.stop_event = threading.Event()
376
379
 
@@ -446,8 +449,10 @@ class Hub:
446
449
  c.start()
447
450
  except ChildError as exc:
448
451
  self.errors[brick] = str(exc)
452
+ self.error_at[brick] = time.monotonic()
449
453
  raise
450
454
  self.errors.pop(brick, None)
455
+ self.error_at.pop(brick, None)
451
456
  self.children[key] = c
452
457
  self._learn(brick, c.tools)
453
458
  log("started %s for %s (pid %s)" % (brick, project_dir,
@@ -510,10 +515,24 @@ class Hub:
510
515
  tools.append(t if exposed == orig else {**t, "name": exposed})
511
516
  return tools, table
512
517
 
518
+ #: How long a brick's start failure suppresses retries.
519
+ ERROR_COOLDOWN_S = 300.0
520
+
521
+ def _error_fresh(self, brick: str) -> bool:
522
+ """True while a recorded error is younger than the cooldown."""
523
+ if brick not in self.errors:
524
+ return False
525
+ at = self.error_at.get(brick)
526
+ if at is not None and time.monotonic() - at >= self.ERROR_COOLDOWN_S:
527
+ self.errors.pop(brick, None)
528
+ self.error_at.pop(brick, None)
529
+ return False
530
+ return True
531
+
513
532
  def ensure_tool_cache(self, project_dir: str, timeout: float = 90.0) -> None:
514
533
  """Learn the tool list of every brick not yet in the cache (in parallel)."""
515
534
  missing = [b for b in self.order if not self.tool_cache.get(b)
516
- and b not in self.errors]
535
+ and not self._error_fresh(b)]
517
536
  if not missing:
518
537
  return
519
538
  threads = []