AXIS

A snapshot-first Go CLI that discovers hardware facts across your cluster via SSH, builds a ClusterSnapshot, and makes deterministic reservation-aware placement decisions. Optional chat, HTTP, and MCP surfaces are subordinate to observed state, not sources of truth.
Quick Start
# Install
go install github.com/toasterbook88/axis/cmd/axis@latest
# Inspect the local machine
axis facts
# Inspect the full cluster (requires ~/.axis/nodes.yaml)
axis status
# Ask where to run a task
axis task place "run ollama inference on a 7b model"
Truth Boundary
No generated output may present itself as cluster truth unless it is backed by a real snapshot or live probe.
- The stable operator path is
axis facts, axis status, axis task place, axis task context, and the daemon cache commands.
axis chat is experimental and non-authoritative.
Command Surface
Stable operator path
axis version — print the current AXIS build version
axis facts — local hardware/tool snapshot
axis status / axis status --cached — live or daemon-cached cluster snapshot
axis task place / axis task place --cached — advisory placement with reasoning
axis task context / axis task context --cached — compact context block backed by live or cached snapshot data
axis daemon status / refresh / invalidate / restart — inspect and manage the local snapshot cache seam
axis context show / clear — inspect or reset persisted placement memory
Optional or secondary surfaces
axis task run — explicit execution surface layered on top of placement, with optional observed-state-gated Nix helper wrapping when the selected node proves it supports nix
axis serve — optional local HTTP API surface
axis mcp serve — optional read-only MCP server over stdio
axis scripts list — list built-in helper scripts
axis skills — show learned local skills/failures
axis chat — cluster-aware chat assistant using Ollama /api/chat; advisory only
axis agent — agentic tool-calling assistant with read-only cluster tools and operator-confirmed shell execution
axis completion — Cobra-generated shell completion
Available Commands (Strict Config Enforced)
axis version
axis facts — local facts only
axis status — full cluster snapshot (uses strict nodes.yaml)
axis task place <description> — deterministic placement
Removed: axis discover (fully replaced by axis status + snapshot assembly).
Execution Safety (hardened)
- CLI execution requires
axis task run --script ... or axis task run --exec ...; HTTP /run and axis_execute require explicit mode=script|exec plus confirm: "YES"
- Hardened safety blocker with allow-list, cluster-aware RAM/GPU checks, and learned-bad fast path
- Per-node reservation caps enforced before any command runs (
RAMTotalMB - 1024 headroom)
- Live and cached read paths both overlay local reservations before placement, context generation, or execution
axis task run / /run now export AXIS_TURBOQUANT_* env hints to executed commands when a TurboQuant-capable node is selected
- Corrupt local
~/.axis/state.json / ~/.axis/skills.json files are quarantined to .corrupt-* backups and surfaced as warnings instead of crashing read paths
- Reservations auto-clean on every daemon refresh:
- guarded executions now write explicit per-exec heartbeats while they are alive
- reservation entries record their owning local AXIS PID, internal caller provenance (
task-run, http-run, agent-run-shell), and the observed local AXIS execution origin (node name / hostname / stable ID when available)
- HTTP execution surfaces can also accept a signed forwarded upstream origin when the caller proves possession of the shared AXIS API token
- heartbeat-stale exec reservations are reclaimed after about 2 minutes
- legacy no-heartbeat state still falls back to the older 45-minute/24-hour cleanup path
- no-exec entries are discarded immediately
- All cached paths (
task place --cached, task context --cached, mcp serve --cached) use the single internal/daemon/client
- Visible in
/snapshot/meta as reserved_mb
axis serve now watches ~/.axis/nodes.yaml, ~/.axis/state.json, and ~/.axis/skills.json for semantic/content changes and surfaces the resulting last_refresh_trigger in daemon metadata/health; heartbeat-only state.json writes are ignored so active executions do not force full cache refresh churn
axis task run and HTTP /run now emit best-effort execution-reserved / execution-finished refresh signals so the default local daemon cache can react immediately to real execution-state changes
axis agent now routes approved run_shell calls through the same guarded AXIS execution path as other execution surfaces, preferring the local AXIS /run daemon/API hop when available so signed upstream origin survives that boundary; agent-triggered commands still participate in placement, reservation tracking, heartbeats, caller provenance, and refresh signaling
axis task run now also prefers the local AXIS daemon/API /run hop when available, using a streamed NDJSON execution contract so placement readiness and live stdout/stderr survive the boundary instead of forcing a local-only execution path
- The streamed
/run contract now also carries explicit execution state-change events, and early runtime/load failures now surface as the normal final result event instead of unexpectedly falling back to buffered JSON
- Local daemon
/run clients now use a dedicated long-lived execution transport instead of inheriting the short snapshot/meta HTTP timeout, so daemon-hop executions can run for the full caller context
- When UDP discovery is enabled,
axis serve also runs a long-lived beacon watcher so new/changed/expired beacon-derived nodes can trigger beacon-change cache refreshes without waiting for the normal snapshot timer
- Live and cached snapshots now carry a typed
freshness contract describing the discovery source, expected/observed beacon window, additive beacon-node count, completion state, and any degraded-confidence warning
- Guarded execution now records exact-scope empirical observations (
node, workload, backend, tool) separately from failure memory so later placement can prefer fresh successful history without treating "slow" as "broken"
Degraded-State Behavior
| Local condition |
CLI behavior |
API/MCP behavior |
File outcome |
Corrupt ~/.axis/state.json |
axis context show warns on stderr and still prints valid JSON |
live snapshot-bearing reads keep working; state warning appears in snapshot warnings or placement reasoning |
original file is renamed to state.json.corrupt-<UTCSTAMP> and a clean in-memory state is used |
Corrupt ~/.axis/skills.json |
axis skills warns on stderr and still prints valid JSON |
live API/MCP reads keep working; skills warning appears in snapshot warnings or placement reasoning |
original file is renamed to skills.json.corrupt-<UTCSTAMP> and a clean in-memory skills store is used |
These files are local operator memory, not authoritative cluster truth. AXIS now prefers warn + quarantine + continue over failing shut on read paths.
Features
| Feature |
Details |
| Local fact collection |
OS, kernel, arch, CPU cores/model, RAM (total/free + load averages + pressure), disk (total/free + storage class: nvme/ssd/hdd), structured GPU info (vendor/model/VRAM/capabilities), network addresses (with interface name, subnet, heuristic speed class), battery %, thermal state, and additive memory-topology / pressure-source metadata where available |
| Tool inventory |
go, python3, git, docker, ollama, mlx_lm, llama-cli, llama-server, node, swift, cargo, gcc, plus probe-verified local apple-foundation-models on eligible Apple Silicon hosts running macOS 26 or later |
| SSH cluster sweep |
Concurrent fan-out over all configured nodes; per-node timeout |
| ClusterSnapshot |
Structured JSON/YAML with per-node status (complete / partial / unreachable / error), cluster-level aggregates, and additive discovery freshness metadata for both live and cached reads |
| Advisory task placement |
axis task place ranks nodes deterministically by allocatable RAM first, then fresh exact-scope empirical history, resident-model locality, backend/GPU/headroom tie-breaks, and reservation spread; heavy AI tasks still avoid nodes under critical runtime pressure signals |
| Optional local control surfaces |
axis serve, axis daemon invalidate, axis mcp serve, axis task run, axis chat, and axis agent are available when explicitly invoked |
| Single-binary operation |
No required daemon, database, or background process; local server/MCP surfaces are opt-in |
| Structured output |
axis facts and axis status support JSON/YAML; axis task place supports human output and JSON |
Installation
1. Quick Install (macOS / Linux)
The fastest way to install AXIS is using our install script. It automatically detects your OS/Arch, downloads the latest release tarball securely verified against checksums.txt, and places it in ~/.local/bin.
curl -fsSL -o install.sh https://raw.githubusercontent.com/toasterbook88/axis/main/install.sh
less install.sh
bash install.sh
rm -f install.sh
2. Nix Flakes (NixOS / macOS)
AXIS offers native, reproducible Nix support.
# Install to your profile
nix profile install github:toasterbook88/axis
# Or run instantly without installing
nix run github:toasterbook88/axis
Note for contributors: Run nix develop to enter a reproducible devShell containing the matching Go toolchain and required utilities.
3. Homebrew (macOS / Linux)
Note: Homebrew Tap automation is currently pending. For now, please use the Quick Install script above.
For Developers (Build from Source)
If you need unreleased main-branch changes or specifically want to use the Go ecosystem, you can compile from source. Requirements: Go 1.26.1+ (use the latest 1.26 patch release), SSH key-based auth for remote nodes.
Using go install:
go install github.com/toasterbook88/axis/cmd/axis@latest
Manual Compilation:
git clone https://github.com/toasterbook88/axis.git
cd axis
go build -o axis ./cmd/axis/
# Optional: move to $PATH
mv axis /usr/local/bin/axis
Updating AXIS
AXIS includes a built-in self-updater via the axis update command.
[!WARNING]
If you installed AXIS via a package manager (like Nix or Homebrew), DO NOT use axis update to upgrade the binary. Doing so attempts an in-place executable swap which violates immutable system paths and desyncs your package manager's state. AXIS is package-manager aware and will politely refuse to upgrade itself, instructing you to use your respective tool (e.g. nix profile upgrade, brew upgrade).
To safely check if there is a newer version available without triggering an upgrade, run:
axis update --check
Important Notes for axis update on Quick Install / Source builds:
By default, axis update safely scopes its upgrade only to the currently executing binary to prevent cross-contamination in mixed dev/prod PATH environments. If you want it to automatically upgrade all other axis binaries it finds in your $PATH, pass the --all flag.
Release Pipeline & Security
Tagged release pipeline:
v* tags are published through GitHub Actions and GoReleaser
- Release artifacts are configured for
darwin/linux on amd64/arm64
- The release workflow refuses to publish if the pushed tag and
internal/buildinfo/version.go disagree
- Published releases are listed on the GitHub Releases page
Security hygiene:
- Weekly Dependabot updates cover Go modules and GitHub Actions
govulncheck runs on pull requests, pushes to main, and a weekly schedule
- Private vulnerability reporting and automated security fixes are enabled on GitHub
- Security reporting guidance lives in SECURITY.md
Usage
axis facts — local machine snapshot
axis facts # JSON (default)
axis facts --format yaml # YAML
axis status — cluster snapshot
Create ~/.axis/nodes.yaml (see nodes.example.yaml):
nodes:
- name: node-a
hostname: node-a.local
# stable_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
ssh_user: alice
role: primary
- name: node-b
hostname: node-b.local
ssh_user: alice
role: worker
# ssh_port: 22
# timeout_sec: 10
Then:
axis status # JSON cluster snapshot
axis status --format yaml
axis status --cached # read explicit daemon cache instead of live SSH sweep
axis task place — advisory placement
axis task place "analyze a git repo"
# → Selected node: node-b (remote, fit 82/100)
# Tool: git
# Reason:
# - has required tool: git
# - free RAM: 14336 MB
axis task place "run ollama inference on a 7b model" --format json
axis task place --cached "run ollama inference on a 7b model"
Placement uses keyword matching against the task description (no ML). It infers the required tool (ollama, git, go, docker) and minimum free RAM from specific keywords (model, 7b, inference, heavy, etc.), then scores each reachable node. Tool presence is a hard requirement, and eligible nodes are ranked by allocatable RAM first, then fresh exact-scope empirical history, resident-model locality, backend/GPU/headroom tie-breaks, reservation spread, storage class (HDD penalized for heavy loads), and stable name ordering.
AXIS now treats allocatable RAM as min(live free RAM, total RAM minus a protected 1GB system reserve) - local reservations, so cached reads, placement, and last-second execution admission use the same headroom model. When allocatable and per-node reservation ratios tie, placement also prefers the node holding a smaller share of the current cluster reservation pool, which makes RAM spreading more explicit instead of repeatedly picking the same machine on late ties.
When UDP discovery is enabled, both live and cached snapshot surfaces now also expose a typed freshness contract instead of only a free-form warning string. That contract reports whether the expected beacon window completed, how much of it was actually observed, and how many additive beacon-derived nodes were included.
For Ollama-shaped tasks, observed resident_models from ollama ps now provide a truth-backed warm-runtime locality hint, and guarded execution now feeds back exact-scope observations (wall time always, RAM/VRAM peaks only when directly observed) into later placement decisions.
Long-context hints such as 128k, book-length, or million-token also trigger a TurboQuant-aware preference when a node exposes mlx or llama.cpp-style backends, with stronger RAM reduction and fit bonuses reserved for recognizable backend help/probe responses. For heavy inference tasks, AXIS filters out nodes showing critical runtime pressure, thermal throttling, or low battery (< 20%), and respects tombstone blacklists for task+node combinations that have repeatedly crashed.
When axis task run selects a TurboQuant-capable node, AXIS exports AXIS_TURBOQUANT, AXIS_TURBOQUANT_STATUS, AXIS_TURBOQUANT_BACKENDS, AXIS_TURBOQUANT_CAPABILITIES, and long-context hints into the execution environment. For probe-verified llama.cpp commands with --ctx-size support, AXIS can also inject safe additive flags such as --ctx-size and --flash-attn when they are absent.
Experimental local Apple Foundation Models execution is available on eligible Apple Silicon machines running macOS 26 or later through the source-visible Swift helper in hack/apple-foundation-models.swift:
axis task run --exec 'xcrun swift hack/apple-foundation-models.swift --prompt "Summarize this text"'
AXIS treats apple-foundation-models as a local-only verified capability. Remote nodes are excluded for that backend instead of being treated as fallback candidates.
With --cached, placement uses the explicit daemon snapshot cache instead of a fresh SSH sweep. JSON output includes a source wrapper so you can tell whether the decision came from daemon-cache or live fallback.
axis task context — compact operator/agent prompt block
axis task context "test inference"
axis task context --cached "test inference"
--cached uses the explicit daemon snapshot cache and includes a Source: line in the rendered context block so you can tell where the prompt data came from.
axis serve — optional local HTTP API
axis serve
Starts the local AXIS HTTP API and execution surface on 127.0.0.1:42425 by default, plus a background snapshot refresh loop that powers the explicit cached-read path.
axis daemon invalidate — clear local daemon cache
axis daemon invalidate
axis daemon invalidate --cache-addr 127.0.0.1:42425
Clears the daemon-backed snapshot cache explicitly. This does not change the default axis status live path; it only affects cached reads and other daemon-backed surfaces.
axis daemon refresh — force a fresh daemon snapshot now
axis daemon refresh
axis daemon refresh --cache-addr 127.0.0.1:42425
Forces the daemon to rebuild its cached snapshot immediately. This is the fastest way to ensure axis status --cached and axis task place --cached use fresh cluster state without waiting for the next background tick.
axis daemon status — inspect local daemon freshness
axis daemon status
axis daemon restart
axis daemon status reports cache readiness, age, and version metadata. axis daemon restart restarts the local cache seam from the current binary when you need to refresh stale daemon state explicitly.
axis update — self-update to the latest release
axis update # download and install the latest release (only replaces current binary)
axis update --all # replace ALL discovered `axis` binaries in your $PATH
axis update --check # report whether an update is available (no download)
Checks GitHub Releases for a newer version, downloads the matching platform
binary, verifies its SHA-256 checksum against the release's checksums.txt,
and replaces the current binary in-place. The checksum is always verified —
the release workflow produces checksums.txt alongside every archive.
Note: if your copy of axis was installed using a package manager like nix
or homebrew, axis update will refuse to perform an in-place replacement
and will direct you to update using your package manager natively instead.
Configuration Reference
~/.axis/nodes.yaml fields:
Unknown YAML keys are rejected at load time so config typos fail fast instead of being silently ignored.
| Field |
Required |
Default |
Description |
name |
yes |
— |
Logical node name |
hostname |
yes |
— |
Resolvable hostname or IP |
stable_id |
no |
— |
Optional observed machine identity (machine-id / platform UUID) used for locality and discovery dedupe |
ssh_user |
yes |
— |
SSH username |
role |
no |
— |
primary or worker |
ssh_port |
no |
22 |
SSH port |
timeout_sec |
no |
10 |
Per-node collection timeout (seconds) |
Optional discovery block used by experimental UDP-assisted discovery:
| Field |
Required |
Default |
Description |
discovery.enabled |
no |
false |
Enable UDP beacon discovery alongside configured nodes |
discovery.udp_port |
no |
42424 |
UDP beacon port |
discovery.beacon_interval_sec |
no |
3 |
Beacon broadcast interval |
discovery.secret |
no |
empty |
Shared discovery secret for filtering beacons |
When a host exposes a stable identity, AXIS includes it in UDP beacons and uses it only to avoid duplicate nodes or preserve an explicit config binding when names or IPs drift.
Architecture
┌─────────────────────┬─────────────────────────────────────────────────────────────────────────────────┐
│ Package │ Role │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ cmd/axis/ │ Cobra CLI entry — chat, facts, status, task, serve, context, scripts, skills │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/config/ │ Loads ~/.axis/nodes.yaml (node list, SSH user/port/timeout) │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/facts/ │ SSH into each node, collects RAM/CPU/GPU/tools │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/placement/ │ Filter + rank nodes by free RAM, pressure, GPU capability, storage class, thermal state, tombstones, locality; ComputeFitScore 0–100 │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/chat/ │ Structured /api/chat client with rolling context window and system prompt │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/agent/ │ Tool-calling agent loop with read-only tools and safety-gated shell execution │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/snapshot/ │ Assembles `ClusterSnapshot` from `[]NodeFacts` │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/daemon/ │ Background snapshot refresh, in-memory cache, and explicit invalidation │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/state/ │ Persists local placement memory and execution state │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/api/ │ Local HTTP API and execution surface │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/mcp/ │ Read-only MCP server over stdio │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/transport/ │ Raw SSH execution layer │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/discovery/ │ Node discovery │
├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ internal/models/ │ Shared types: NodeFacts, TaskRequirements, Locality │
└─────────────────────┴─────────────────────────────────────────────────────────────────────────────────┘
Key design notes
- Config lives at
~/.axis/nodes.yaml — no cluster IPs hardcoded in code
- Placement is deterministic: RAM pressure → GPU → effective headroom → allocatable RAM → reservation ratio → name
- ComputeFitScore factors in GPU (+25pts) and local-node bonus (+10pts) — M1↔M3 RAM sharing would be relevant here
- Chat uses the Ollama
/api/chat endpoint with structured messages and a rolling context window; defaults to localhost:11434
axis agent provides a tool-calling loop with read-only cluster tools (status, facts, place) and safety-gated shell execution; --auto-approve enables automatic execution for low-risk commands
axis serve hosts an optional daemon-backed cache; axis status --cached, axis task place --cached, axis task context --cached, axis daemon refresh, and axis daemon invalidate use it explicitly
- The daemon cache now refreshes immediately on config/state/skills content changes, explicit execution-state signals, and long-lived UDP beacon changes, in addition to its normal timer loop
axis serve and axis mcp serve are optional local surfaces, not required infrastructure
axis chat and axis agent are advisory helpers and must not outrank snapshot-backed truth
- Placement memory lives locally in
~/.axis/state.json
Current phase: The observability and placement core is stable. Chat and agent surfaces use structured /api/chat with tool calling and safety gates, but remain subordinate to observed state and must not present model output as authoritative cluster truth.
See Phase 1 Spec and White Paper for detailed design notes.
Roadmap
The following are planned directions beyond the functionality shipped in v0.7.0:
- Mesh networking / peer discovery beyond a static seed file
- Future roadmap phases beyond the current runtime-hardening wave — see
white paper and
future roadmap
Phase 3 (Shipped in v0.4.0) · Phases 4–7 (Shipped in v0.7.0)
The following features were delivered in Phase 3:
- nodes.yaml hot-reload — daemon detects config changes and re-discovers nodes without restart
- Daemon refresh metrics —
/health reports refresh_count, last_refresh_duration_ms, stale_nodes
- Graceful shutdown —
axis serve drains in-flight work before exit
axis task context --format json — machine-readable context block with fit score, skills, and recent decisions
- HMAC-SHA256 beacon auth — UDP discovery signs beacons instead of transmitting the shared secret
Phases 4–7 are now shipped in v0.7.0, including professional CLI UX,
structured chat and agent surfaces, transport and execution hardening, and the
trust-and-foundations work tracked in the roadmap docs.
Contributing
See CONTRIBUTING.md. Keep PRs small and focused; open an
issue before adding major new roadmap phases or control surfaces.
License
MIT — see LICENSE.