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
8.3 KiB
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_confirmationneed 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 gets409; - a wall-clock timeout per capability; on timeout the outcome is
unknown(neverfailed, 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).
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
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:
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):
make run-mcp # bin/hexisd -mcp -data ~/.local/share/hexis
bin/hexisctl is a thin CLI over the HTTP API:
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 byhexisctlonly; defaults tohttp://localhost:9741.HEXIS_API_TOKEN— shared bearer token; required byhexisd, and also read byhexisctl(and bypkg/clientviaWithToken) 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.
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) andidare both emitted and always carry the same value.idis a deprecated alias retained for Maven's client; see the compatibility note inpkg/client/capability.gobefore removing it.enabledandrequires_confirmationare always present, including when false. They are derived server-side from the risk tier and are never settable by a caller.
Tests
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
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:
make compose-up # build + start just the hexis service
make compose-logs
Override COMPOSE_DIR if your checkout layout differs from
../Maven/deploy/ecosystem.