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,73 @@
|
|||||||
|
|
||||||
|
# Hexis — capability registry + guarded execution.
|
||||||
|
# Pure-Go build (modernc sqlite), so CGO is never required.
|
||||||
|
|
||||||
|
GO ?= go
|
||||||
|
BIN_DIR ?= bin
|
||||||
|
IMAGE ?= hexis:dev
|
||||||
|
DATA_DIR ?= $(HOME)/.local/share/hexis
|
||||||
|
HTTP_ADDR ?= localhost:9741
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := build
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## List targets
|
||||||
|
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \
|
||||||
|
awk -F':.*?## ' '{printf " %-14s %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: ## Build hexisd and hexisctl into bin/
|
||||||
|
CGO_ENABLED=0 $(GO) build -trimpath -o $(BIN_DIR)/hexisd ./cmd/hexisd
|
||||||
|
CGO_ENABLED=0 $(GO) build -trimpath -o $(BIN_DIR)/hexisctl ./cmd/hexisctl
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: ## Run the test suite
|
||||||
|
$(GO) test ./...
|
||||||
|
|
||||||
|
.PHONY: vet
|
||||||
|
vet: ## Run go vet
|
||||||
|
$(GO) vet ./...
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt: ## Format all Go sources
|
||||||
|
$(GO) fmt ./...
|
||||||
|
|
||||||
|
.PHONY: fmt-check
|
||||||
|
fmt-check: ## Fail if any Go source is unformatted
|
||||||
|
@out="$$(gofmt -l .)"; \
|
||||||
|
if [ -n "$$out" ]; then echo "unformatted files:"; echo "$$out"; exit 1; fi
|
||||||
|
|
||||||
|
.PHONY: check
|
||||||
|
check: fmt-check vet test ## fmt-check + vet + test
|
||||||
|
|
||||||
|
.PHONY: tidy
|
||||||
|
tidy: ## Tidy go.mod/go.sum
|
||||||
|
$(GO) mod tidy
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: build ## Run hexisd locally (HTTP API)
|
||||||
|
$(BIN_DIR)/hexisd -http $(HTTP_ADDR) -data $(DATA_DIR)
|
||||||
|
|
||||||
|
.PHONY: run-mcp
|
||||||
|
run-mcp: build ## Run hexisd as an MCP stdio server
|
||||||
|
$(BIN_DIR)/hexisd -mcp -data $(DATA_DIR)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Remove build output
|
||||||
|
rm -rf $(BIN_DIR)
|
||||||
|
|
||||||
|
.PHONY: docker-build
|
||||||
|
docker-build: ## Build the distroless container image
|
||||||
|
docker build -t $(IMAGE) .
|
||||||
|
|
||||||
|
# The compose file that actually runs Hexis lives with the ecosystem stack, not
|
||||||
|
# in this repo. Override COMPOSE_DIR if yours is elsewhere.
|
||||||
|
COMPOSE_DIR ?= ../Maven/deploy/ecosystem
|
||||||
|
|
||||||
|
.PHONY: compose-up
|
||||||
|
compose-up: ## Build + start the ecosystem stack (nexus, praxis, hexis)
|
||||||
|
docker compose -f $(COMPOSE_DIR)/docker-compose.yml up -d --build hexis
|
||||||
|
|
||||||
|
.PHONY: compose-logs
|
||||||
|
compose-logs: ## Tail hexis logs from the ecosystem stack
|
||||||
|
docker compose -f $(COMPOSE_DIR)/docker-compose.yml logs -f hexis
|
||||||
@@ -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`.
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Hexis — Senior Engineering Review
|
||||||
|
|
||||||
|
Date: 2026-07-30
|
||||||
|
Reviewed at: commit `945e4ba` (working tree had one uncommitted change in `cmd/hexisd/main.go`: default Nexus URL `8987` → `9740`)
|
||||||
|
Reference contract: `/home/kami/apps/ECOSYSTEM-SPEC.md` §4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## verdict
|
||||||
|
|
||||||
|
**Fragile + misaligned, but structurally sound and worth repairing incrementally.** No rewrite. The architecture matches the spec's shape; the *guards the spec exists to enforce* are missing, and the primary execution path is functionally dead at HEAD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## first assessment
|
||||||
|
|
||||||
|
```text
|
||||||
|
purpose: capability registry + guarded execution for the Nexus/Praxis/Hexis
|
||||||
|
ecosystem (ECOSYSTEM-SPEC.md §4)
|
||||||
|
intended users: Maven (voice + mavweb), Command Center, MCP clients
|
||||||
|
actual users: Maven only — mavweb reads /api/v1/capabilities; mavend/voice.go:1329-1381
|
||||||
|
resolves→matches→executes via a *vendored* copy of pkg/client
|
||||||
|
critical workflows: 1. list capabilities for an entity 2. confirm 3. execute 4. poll changes
|
||||||
|
current state: running on :9741 — but the live binary predates commit 89d8433
|
||||||
|
(its GET capability response has no `enabled`/`requires_confirmation`
|
||||||
|
fields), so deployed ≠ HEAD
|
||||||
|
known failures: every workspace_mcp capability is registered with enabled=false (verified)
|
||||||
|
maintenance burden: low — 3.6k LOC, 1 real dependency + pure-Go sqlite, builds clean, vet clean
|
||||||
|
technical constraints: single-user homesrv, single SQLite writer, distroless container
|
||||||
|
personal constraints: solo maintainer; spec is the contract, three services must agree
|
||||||
|
what still works well: confirmation lifecycle (args_hash + version + TTL) is correct and
|
||||||
|
genuinely well tested; storage layer is honest; timeout⇒unknown is right
|
||||||
|
what has become obsolete: systemd provider (no systemctl in distroless), ListenUnix, the
|
||||||
|
contract_test.go "bug documentation" tests, schema_migrations table
|
||||||
|
```
|
||||||
|
|
||||||
|
Classification: **fragile** (safety guards absent), **misaligned** (deployed ≠ HEAD; spec §4.3/§4.4 unmet), partly **underbuilt** (no auth, no arg validation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## main findings
|
||||||
|
|
||||||
|
### 1. Every workspace capability is registered disabled — the product's only working provider cannot execute
|
||||||
|
|
||||||
|
- **problem:** `BuildCapabilities()` never sets `Enabled`, so it defaults to `false`.
|
||||||
|
- **evidence:** `internal/provider/workspace_mcp.go:235-249` — the `domain.Capability` literal omits both `Enabled` and `TimeoutSeconds`, leaving them zero-valued. Note that the same function *skips* allowlist-disabled tools at `:226`, so it plainly intends everything it emits to be live: this is an omitted field, not a policy decision. A throwaway test at HEAD printed `Enabled=false RequiresConfirmation=false TimeoutSeconds=0`. `internal/execution/engine.go:41` then returns `ErrCapabilityDisabled` → 403 for all 19 capabilities.
|
||||||
|
- **impact:** Maven's voice execute path can never succeed against a fresh database. Nothing in the suite catches it — no test executes a workspace-provided capability end to end.
|
||||||
|
- **action:** **repair.** Set `Enabled` from risk tier and `TimeoutSeconds` explicitly. Must not land alone — see finding 2.
|
||||||
|
|
||||||
|
### 2. Fixing (1) naively arms unconfirmed docker stop/restart on a publicly-proxied, unauthenticated port
|
||||||
|
|
||||||
|
- **problem:** `BuildCapabilities` also leaves `RequiresConfirmation=false` for `risk: medium` docker start/stop/restart, and the HTTP API has no authentication whatsoever.
|
||||||
|
- **evidence:** `Maven/deploy/ecosystem/nginx.conf:32-42` proxies `hexis.kvmx.ru:80` → `127.0.0.1:9741` with no auth block; `internal/api/handler.go:37` exposes `POST /api/v1/execute`; `handler.go:115-166` lets any caller `POST /api/v1/capabilities` with `provider`+`operation` of their choosing. Precisely: `handler.go:142` derives the correct spec §4.3 default (`enabled := req.Risk != "destructive"`), and then `:143-145` lets an optional `enabled` field in the request body override it. The guard is implemented and then made opt-out by the untrusted caller — worse than simply absent, because it reads as enforced. `requires_confirmation` (`:159`) is copied from the body with no derivation at all.
|
||||||
|
- **concrete attack path:** `POST http://hexis.kvmx.ru/api/v1/capabilities {"name":"x","provider":"workspace_mcp","operation":"ssh.exec","enabled":true}` then `POST /api/v1/execute`. The allowlist blocks that specific one (`workspace_mcp.go:129-135` re-checks `ssh.exec` is disabled) — but `docker.stop_container` is allowed, and self-registration bypasses `requires_confirmation` entirely.
|
||||||
|
- **impact:** remote unauthenticated container control.
|
||||||
|
- **action:** **security, blocking.** Require a shared token on `/api/v1/`; derive `requires_confirmation` from risk tier server-side; and stop honouring `enabled` from the request body entirely rather than treating it as an override.
|
||||||
|
|
||||||
|
### 3. Hexis accepts free-text targets — the one thing the spec says it must never do
|
||||||
|
|
||||||
|
- **problem:** `target_entity_id` is accepted as any non-empty string. Nothing checks the `ent_` shape, existence in Nexus, or `capability.TargetTypes`.
|
||||||
|
- **evidence:** `handler.go:213-216` checks non-empty only; `engine.go:44-46` only compares against a *pinned* `TargetEntityID`, which is `""` for all 19 registered capabilities. Spec §4.3: "Hexis never accepts a free-text target. Ever."
|
||||||
|
- **impact:** the invariant "Maven must not invent mappings" is unenforced at the point it matters.
|
||||||
|
- **action:** **repair.** Validate the target against Nexus `GET /api/v1/entities/{id}` (that endpoint exists) plus a `TargetTypes` match, before creating the execution.
|
||||||
|
- **note:** the spec's stronger *blessing* guard (§4.3) is **not implementable today** — `grep -rni bless` across `nexus/` returns nothing. Nexus has no blessing concept. That's a cross-service gap, not a Hexis bug.
|
||||||
|
|
||||||
|
### 4. Audit fields are written but never read back, or never written at all
|
||||||
|
|
||||||
|
- **problem:** `confirmation_id` is INSERTed but absent from every SELECT, so it always reads back empty. `causation_id` isn't in the `executions` schema at all, yet `engine.go:79` sets it on the struct — the field is populated in memory with nowhere to land.
|
||||||
|
- **evidence:** `confirmation_id` appears in the INSERT at `sqlite.go:295` and in **none** of the three executions SELECTs (`:313`, `:369`, `:418`). `causation_id` exists only on `hexis_events` (`:119`, `:502`), never on `executions`. Also missing vs spec §4.1: `capability_version` on executions (it exists on `confirmations` at `:134`, so the precedent is there).
|
||||||
|
- **impact:** "which confirmation authorized this destructive act" is unanswerable from the API — the core audit question. Silent, not a crash.
|
||||||
|
- **action:** **repair.** Add the columns as a migration, persist and select them.
|
||||||
|
|
||||||
|
### 5. `contract_test.go` asserts the opposite of current behavior and passes anyway
|
||||||
|
|
||||||
|
- **problem:** three tests are prose, not verification. `TestContract_ChangesSinceAlwaysZero` documents a bug that commit `74b19e0` already fixed, and can only `t.Logf`. `TestContract_TimeoutGoroutineNotCancelled` is a bare `t.Log`. `TestContract_DestructiveCapabilityDisabledByDefault` re-implements `risk != "destructive"` inside the test and asserts on its own local variable — it would pass if the handler were deleted.
|
||||||
|
- **evidence:** `internal/execution/contract_test.go:212-299`.
|
||||||
|
- **impact:** the suite's green status overstates coverage; a misnamed test actively misleads about a fixed bug.
|
||||||
|
- **action:** **cleanup.** Delete the three; the real coverage gap is an end-to-end execute over a provider-registered capability.
|
||||||
|
|
||||||
|
### 6. Read-only failures are reported as `unknown`, and timed-out provider calls leak
|
||||||
|
|
||||||
|
- **problem:** `runWithTimeout` (`engine.go:161-186`) can't cancel — `Provider.Execute` takes no `context`. The goroutine runs to the HTTP client's 60s timeout, outliving the 30s capability timeout.
|
||||||
|
- **evidence:** a POST of a `read`/`read_only` capability against the live service while workspace-mcp was down (`:9930` → 503) hung past 5s. Per spec §4.3 that lands `outcome=unknown` — "may or may not have completed the side effect" — for a call that by definition has none, and `unknown` is never retried.
|
||||||
|
- **impact:** read failures are unretryable and indistinguishable from genuinely ambiguous mutations.
|
||||||
|
- **action:** **refactor.** Add `ctx` to the `Provider` interface (2 implementations), and let `read_only=true` timeouts resolve to `failed`.
|
||||||
|
|
||||||
|
### 7. Race: the one-in-flight-per-(capability,target) guard is check-then-insert
|
||||||
|
|
||||||
|
- **problem:** `engine.go:56-58` queries, then `engine.go:92` inserts, with no uniqueness constraint between them.
|
||||||
|
- **evidence:** no partial unique index on `(capability_id, target_entity_id)` where `status='started'`; the existing `idx_executions_inflight` is non-unique (`sqlite.go:561`).
|
||||||
|
- **impact:** two concurrent requests both proceed. Narrow window, single writer, low likelihood — but the spec states it as a hard guard and the existing test only covers the sequential case.
|
||||||
|
- **action:** **repair**, via a partial unique index.
|
||||||
|
|
||||||
|
### 8. Deployed binary is older than HEAD; registered capabilities never refresh
|
||||||
|
|
||||||
|
- **problem:** `cmd/hexisd/main.go:80-83` skips any capability whose ID already exists, so allowlist edits to risk/target_type/enabled never propagate to an existing database.
|
||||||
|
- **evidence:** live GET returns no `enabled` field → pre-`89d8433` build. Rows dated 2026-07-19.
|
||||||
|
- **impact:** config drift; even the correct fix for (1) won't reach the running instance's existing rows.
|
||||||
|
- **action:** **repair.** Reconcile on startup (update when the derived record differs), and redeploy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## keep
|
||||||
|
|
||||||
|
Confirmation lifecycle and its tests; the storage layer's explicitness; `timeout ⇒ unknown`; the provider registry (2 real implementations justify it); the allowlist-as-config design; `nexusclient`; pure-Go sqlite + distroless build.
|
||||||
|
|
||||||
|
## remove
|
||||||
|
|
||||||
|
- `internal/provider/systemd.go` — no `systemctl` in the distroless image and no capability ever registers it; it's the spec's §4.2 `systemd_dbus` slot filled with a shell-adjacent stub. Delete, or replace with D-Bus when a real need appears.
|
||||||
|
- `Server.ListenUnix` (dead; never called).
|
||||||
|
- `schema_migrations` table (unused; `PRAGMA user_version` is the real mechanism). **Caveat:** confirm no already-deployed database depends on it before dropping — finding 8 establishes the live instance is several commits behind, so its schema state is not HEAD's.
|
||||||
|
- `execution.ResolveRequest` / `ResolveResult` + `MarshalJSON` (superseded by `nexusclient`).
|
||||||
|
- `Engine.emitEvent`, `Registry.List`, `DiscoveredTools`, `Capability.IsDestructive`, `ifString`.
|
||||||
|
- `EventCapabilityUnavailable`, `EventExecutionDenied`, `ExecutionDenied`, and 8 unused `Err*` values.
|
||||||
|
- The three vacuous contract tests.
|
||||||
|
|
||||||
|
## repair now
|
||||||
|
|
||||||
|
1. Auth on `/api/v1/` + server-derived `requires_confirmation` (finding 2) — **before anything else**.
|
||||||
|
2. `Enabled` / `TimeoutSeconds` / `RequiresConfirmation` in `BuildCapabilities`, plus startup reconciliation (1, 8).
|
||||||
|
3. Target validation against Nexus + `TargetTypes` (3).
|
||||||
|
4. Persist/read `confirmation_id`, `causation_id`, `capability_version` (4).
|
||||||
|
5. Partial unique index for in-flight (7).
|
||||||
|
6. Delete the dead code and misleading tests (5, remove).
|
||||||
|
|
||||||
|
## refactor later
|
||||||
|
|
||||||
|
- `context` through `Provider.Execute` (6).
|
||||||
|
- Collapse the four competing wire shapes for a capability — `handler.go:96-108` (which emits both `id` and `capability_id`), `adapter.go:147-157`, `pkg/client`, and Maven's vendored copy — into one serializer.
|
||||||
|
- `GET /api/v1/executions?entity_id=&since=` (spec §4.5, Command Center needs it).
|
||||||
|
- A `Makefile` and a README: there is no onboarding doc at all.
|
||||||
|
|
||||||
|
## rewrite only if
|
||||||
|
|
||||||
|
Nexus gains blessing and versioned capabilities `(capability_id, version)` as the primary key, *and* multi-writer access becomes real. Neither is true; incremental repair is clearly cheaper. The confirmation logic is the hidden knowledge here and it is correct — do not throw it away.
|
||||||
|
|
||||||
|
## verification plan
|
||||||
|
|
||||||
|
- `go build ./... && go vet ./... && go test ./...` — all currently pass. `gofmt -l` flags `internal/provider/systemd.go`, `internal/provider/workspace_mcp.go`, `pkg/client/client.go` as unformatted at HEAD.
|
||||||
|
- New tests, each pinning a finding:
|
||||||
|
- execute a `BuildCapabilities`-registered capability end-to-end (1)
|
||||||
|
- unauthenticated `/api/v1/execute` rejected (2)
|
||||||
|
- non-`ent_` / unknown target rejected (3)
|
||||||
|
- round-trip `confirmation_id` through `GET /executions/{id}` (4)
|
||||||
|
- concurrent duplicate execute → exactly one row (7)
|
||||||
|
- Live: rebuild, redeploy, `curl` a read capability once workspace-mcp is back (it is 503 as of this review), confirm `succeeded` and non-empty `changes`.
|
||||||
|
|
||||||
|
## uncertainties
|
||||||
|
|
||||||
|
- **Unknown:** whether `hexis.kvmx.ru:80` is actually resolvable from outside — the nginx config was read, external reachability was not tested. The severity of finding 2 depends on this; the missing-auth defect stands either way.
|
||||||
|
- **Unknown:** whether blessing is planned for Nexus soon, or whether the target-existence check in finding 3 is the intended permanent guard.
|
||||||
|
- **Assumption:** Maven is the only consumer. Based on a repo-wide grep of `/home/kami/apps`; an out-of-tree MCP client would not appear.
|
||||||
|
- **Confirmed but unexplained:** the live instance is ~4 commits behind. Unclear whether that is a stalled deploy or deliberate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## second-reviewer verification (2026-07-30)
|
||||||
|
|
||||||
|
An independent pass re-checked the load-bearing claims against the code at HEAD rather than accepting them. **The verdict and all eight findings stand.** Confirmed directly:
|
||||||
|
|
||||||
|
- **Finding 1** — `workspace_mcp.go:235-249` omits `Enabled` and `TimeoutSeconds`; `engine.go:42` rejects on `!capability.Enabled`. The execution path is dead as described.
|
||||||
|
- **Finding 2** — no `auth`, `token`, or `Authorization` handling exists anywhere under `internal/api/`. The `hexis.kvmx.ru:80` server block proxies straight to `127.0.0.1:9741` with only `X-Forwarded-*` headers set and no auth directive.
|
||||||
|
- **Finding 4** — verified by comparing the INSERT column list against all three SELECT column lists; `confirmation_id` is write-only. `causation_id` is absent from the `executions` DDL.
|
||||||
|
- **Finding 5** — the three tests are as characterised. `TestContract_DestructiveCapabilityDisabledByDefault` computes `enabled := risk != "destructive"` as a local variable and asserts on that, touching no production code. `TestContract_TimeoutGoroutineNotCancelled` is a single `t.Log` with no assertions.
|
||||||
|
- **Finding 7** — `sqlite.go:143` creates `idx_executions_inflight` as a plain `CREATE INDEX`, not `CREATE UNIQUE INDEX`, and there is no partial unique index anywhere.
|
||||||
|
- **Finding 8** — `cmd/hexisd/main.go:76-79` `continue`s on any capability whose ID already resolves, so derived changes never reconcile.
|
||||||
|
- **Verification plan** — `gofmt -l` flags exactly the three files named.
|
||||||
|
|
||||||
|
Amendments made above, none of which change a conclusion: finding 2's description of `enabled` was corrected (it is a caller-supplied override of a correct server default, not "forced true"), finding 4's line references were corrected to the actual INSERT/SELECT sites, and the `schema_migrations` removal gained a deployed-schema caveat.
|
||||||
|
|
||||||
|
One emphasis difference: finding 4 reads as more serious than "silent, not a crash" suggests. The confirmation lifecycle is the part of this codebase the review rightly calls correct and worth keeping — but with `confirmation_id` unreadable, that correctness cannot be demonstrated after the fact from the API. A guard you cannot audit is hard to trust under incident review, which raises finding 4 close to the priority of the target-validation work in finding 3.
|
||||||
|
|
||||||
|
Agreement on sequencing is unqualified; see below.
|
||||||
|
|
||||||
|
## note on sequencing
|
||||||
|
|
||||||
|
Findings 1 and 2 are coupled. Fixing the dead-capability bug without adding auth and confirmation defaults would turn a broken execution path into a remotely reachable one. They should land together as one coherent change.
|
||||||
Reference in New Issue
Block a user