ae6bd08ad8
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
205 lines
8.3 KiB
Markdown
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`.
|