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
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user