Files
hexis/README.md
T
kami ae6bd08ad8 Add a Makefile, a README and the review this branch works from
There was no onboarding doc of any kind. The Makefile covers build, test, vet,
fmt, a fmt-check gate, tidy, run, the MCP stdio mode, docker-build and the
ecosystem compose targets. The README covers what Hexis is and where it sits
between Nexus, Praxis and workspace-mcp, how to build and run it, the flag and
environment table including the new HEXIS_API_TOKEN, the API surface, and the
tests.

REVIEW-2026-07-30.md is the engineering review the preceding commits address,
kept in-tree as the rationale for them. It includes an independent
second-reviewer pass, and its "uncertainties" section has since been resolved:
the service is not reachable from the public internet (the internet-exposed
nginx config in the Maven repo is a template, not what is deployed), Nexus has
no blessing concept and none is planned, and the running image was built from
an uncommitted working tree hours before the first commit existed — which is
why the deployed binary never matched any revision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
2026-07-30 23:40:45 +04:00

205 lines
8.3 KiB
Markdown

# 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 <id>
bin/hexisctl exec <capability-id> <target-entity-id> [--args JSON] [--idempotency KEY]
bin/hexisctl execution <id>
```
## 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 `<data>/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` | `<data>/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 <token>`; 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=<seq>`, `?limit=` (≤100) |
| `GET` | `/api/v1/executions/{id}` | |
| `GET` | `/api/v1/changes` | event feed, `?since=<sequence>`, 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`.