# Hexis Capability registry and **guarded execution** service for the Nexus / Praxis / Hexis ecosystem (see `ECOSYSTEM-SPEC.md` §4 in the sibling repo root). Hexis is the only component allowed to *act* on infrastructure. It holds a registry of named capabilities (`workspace.docker.restart`, …), each bound to a registered provider implementation — never to a shell string — and mediates every execution behind a set of guards: - a capability must be `enabled`; - capabilities marked `requires_confirmation` need a valid, unexpired confirmation matching the capability, target and argument hash (default TTL applies); - one in-flight execution per `(capability, target_entity_id)` — a second attempt gets `409`; - a wall-clock timeout per capability; on timeout the outcome is `unknown` (never `failed`, never auto-retried). Its place in the ecosystem: - **Nexus** (`:9740`) owns entities. Hexis resolves free-text targets to entity IDs through Nexus rather than inventing them. - **Praxis** (`:8989`) owns items/incidents and consumes execution correlation. - **Hexis** (`:9741`) owns capabilities, confirmations and executions. - Consumers today: Maven (voice + mavweb) over the HTTP API, plus MCP clients via the stdio adapter. Actual work is performed by providers. The live one is **workspace_mcp**, which proxies an allowlisted set of tools from a Workspace MCP HTTP server (docker, filesystem, …). There is also a `systemd` provider stub, which no capability currently registers. > Status note: this repo has a candid engineering review in > `REVIEW-2026-07-30.md`. Read it before trusting the guards — several are > documented there as incomplete at the time of writing. ## Build Go 1.25+. No CGO (sqlite is the pure-Go `modernc.org/sqlite`). ```sh make build # -> bin/hexisd, bin/hexisctl make check # gofmt check + go vet + go test ``` Individual targets: `make test`, `make vet`, `make fmt`, `make fmt-check`, `make tidy`, `make clean`, `make help`. ## Run locally ```sh export HEXIS_API_TOKEN=dev-token # required; hexisd refuses to start without it make run # bin/hexisd -http localhost:9741 -data ~/.local/share/hexis ``` That runs registry + execution API with no workspace provider, so nothing is executable — useful for API work. To wire the real provider: ```sh bin/hexisd \ -http localhost:9741 \ -data ~/.local/share/hexis \ -workspace-url http://localhost:9930 \ -workspace-allowlist etc/workspace-allowlist.yaml ``` On startup Hexis discovers the workspace tools and registers a capability for each allowlisted entry. Discovery failure is a warning, not fatal; a missing or unreadable allowlist file **is** fatal when `-workspace-url` is set. MCP stdio mode (same storage, same engine, tools `hexis.list_capabilities`, `hexis.inspect_capability`, `hexis.resolve_target`, `hexis.execute`, `hexis.execution_status`): ```sh make run-mcp # bin/hexisd -mcp -data ~/.local/share/hexis ``` `bin/hexisctl` is a thin CLI over the HTTP API: ```sh bin/hexisctl health bin/hexisctl capability list [--entity ENTITY_ID] bin/hexisctl capability show bin/hexisctl exec [--args JSON] [--idempotency KEY] bin/hexisctl execution ``` ## Configuration Flags (all optional) on `hexisd`: | Flag | Default | Meaning | | --- | --- | --- | | `-http` | `localhost:9741` | HTTP listen address | | `-data` | `$HOME/.local/share/hexis` | Data dir; SQLite lives at `/hexis.db` | | `-mcp` | `false` | Run the MCP stdio adapter instead of the HTTP server | | `-workspace-url` | `$WORKSPACE_MCP_URL` | Workspace MCP HTTP API URL, e.g. `http://localhost:9930`. Empty ⇒ provider disabled | | `-workspace-allowlist` | `/workspace-allowlist.yaml` | Tool allowlist YAML (repo copy: `etc/workspace-allowlist.yaml`) | | `-nexus` | `$HEXIS_NEXUS_URL`, else `http://localhost:9740` | Nexus base URL, used by `hexis.resolve_target` | Environment: - `WORKSPACE_MCP_URL` — fallback for `-workspace-url`. - `HEXIS_NEXUS_URL` — fallback for `-nexus`. - `HEXIS_URL` — read by `hexisctl` only; defaults to `http://localhost:9741`. - `HEXIS_API_TOKEN` — shared bearer token; required by `hexisd`, and also read by `hexisctl` (and by `pkg/client` via `WithToken`) to authenticate its calls. **Authentication.** The whole `/api/v1/` surface sits behind a shared bearer token read from `HEXIS_API_TOKEN`; `/health` and `/ready` stay open for probes. It fails closed: `hexisd` refuses to start without the variable set, and the middleware answers `503` rather than serving unauthenticated if it is somehow empty. Callers send `Authorization: Bearer `; a missing or wrong token gets `401`. ```sh export HEXIS_API_TOKEN="$(openssl rand -hex 32)" curl -H "Authorization: Bearer $HEXIS_API_TOKEN" localhost:9741/api/v1/capabilities ``` (This landed while the README was being written — if the details drift, `internal/api/handler.go` `requireAuth` is the source of truth.) Ports: Hexis `9741`, Nexus `9740`, Praxis `8989`, Workspace MCP `9930`. ## API surface All `/api/v1/` responses are JSON. A request may send `X-Hexis-Version`; if present it must be `v1`, otherwise the request is rejected with `412`. Omitting the header is allowed. | Method | Path | Notes | | --- | --- | --- | | `GET` | `/health` | liveness | | `GET` | `/ready` | readiness | | `GET` | `/api/v1/capabilities` | optional `?entity_id=` filter | | `POST` | `/api/v1/capabilities` | register a capability | | `GET` | `/api/v1/capabilities/{id}` | | | `DELETE` | `/api/v1/capabilities/{id}` | | | `POST` | `/api/v1/confirmations` | `{capability_id, target_entity_id, arguments?, requester?}` → `201` with the confirmation | | `POST` | `/api/v1/execute` | `{capability_id, target_entity_id, arguments?, confirmation_id?, …}`; also reads `X-Correlation-ID` / `X-Causation-ID` | | `GET` | `/api/v1/executions` | history; `?entity_id=`, `?since=`, `?limit=` (≤100) | | `GET` | `/api/v1/executions/{id}` | | | `GET` | `/api/v1/changes` | event feed, `?since=`, up to 100 events | Execute status codes: `409` in-flight duplicate, `404` unknown capability, `403` confirmation/enablement failures, `400` otherwise. ### Pagination `/api/v1/executions` and `/api/v1/changes` share one cursor convention: `since` is an **exclusive integer sequence**, results come back in ascending order, and you page by passing the sequence of the last item you saw. Executions carry it as `seq`, events as `sequence`. A full page means "call again"; there is no separate next-page token, and there is no timestamp cursor. ### Capability wire shape Every producer — the HTTP handler, the MCP adapter and `pkg/client` — serializes a capability through the single shape defined in `pkg/client/capability.go`, converted by `internal/wire`. Notable points: - `capability_id` (the spec §4.1 name) and `id` are **both** emitted and always carry the same value. `id` is a deprecated alias retained for Maven's client; see the compatibility note in `pkg/client/capability.go` before removing it. - `enabled` and `requires_confirmation` are always present, including when false. They are derived server-side from the risk tier and are never settable by a caller. ## Tests ```sh make test # go test ./... go test ./internal/execution/ -run TestConfirmation -v ``` Tests are table-driven Go tests colocated with their packages — currently `internal/api/handler_test.go`, `internal/execution/{engine,contract}_test.go`, `internal/provider/workspace_mcp_test.go`. They use temporary SQLite files and need no running services. ## Container and deploy ```sh make docker-build # distroless image, static CGO_ENABLED=0 binary ``` The image entrypoint is `/hexisd -data /data -http 0.0.0.0:9741`, so the data volume must be mounted at `/data`, `HEXIS_API_TOKEN` must be in the container environment, and — when `WORKSPACE_MCP_URL` is set — the allowlist must be present at `/data/workspace-allowlist.yaml`, or the container exits immediately. Hexis is deployed as part of the ecosystem compose stack, which lives outside this repo (`Maven/deploy/ecosystem/docker-compose.yml`, alongside nexus and praxis; host nginx fronts it). Convenience wrappers: ```sh make compose-up # build + start just the hexis service make compose-logs ``` Override `COMPOSE_DIR` if your checkout layout differs from `../Maven/deploy/ecosystem`.