Findings 1 and 2 of REVIEW-2026-07-30.md, which must land together: every
workspace capability registered with enabled=false, so the only working
provider could never execute. Fixing that alone would have turned a dead
execution path into a reachable one on an unauthenticated port.
Auth: a shared bearer token (HEXIS_API_TOKEN) is now required on the whole
/api/v1/ surface, compared with crypto/subtle.ConstantTimeCompare. /health
and /ready stay open for probes. It fails closed twice over — hexisd refuses
to start with an empty token, and the middleware returns 503 rather than ever
serving unauthenticated.
Guards: `enabled` and `requires_confirmation` are no longer readable from the
request body at all. Previously the handler derived the correct §4.3 default
and then let the caller override it, which is worse than no guard because it
reads as enforced. Both are now derived from the risk tier by shared helpers
in domain, used by the HTTP and provider registration paths alike;
unrecognised tiers fail closed to requiring confirmation.
BuildCapabilities sets Enabled, RequiresConfirmation and TimeoutSeconds
explicitly, and hexisd reconciles drifted rows on startup instead of skipping
any capability whose ID already exists — without that, allowlist edits never
reach an existing database.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
Decision: workspace-mcp's /api/tool, /api/tools direct-HTTP endpoints are
a first-class, server-sanctioned transport (see transport.py's "Direct
HTTP API for non-MCP clients"), not a workaround — Hexis's REST provider
is the right integration, no migration to the SSE/JSON-RPC MCP transport
needed.
The actual gap: workspace-mcp catches tool-handler exceptions and reports
them as an ERROR-coded warning inside a 200 response envelope rather than
an HTTP error status, so Execute()'s status-code check alone let every
such failure through as a successful execution. Detect the ERROR-coded
warning and return it as an error so it maps to Hexis's failed status.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
resolve_target previously returned a hardcoded "requires_nexus_resolution"
placeholder; it now calls Nexus's /api/v1/resolve via a new minimal
internal/nexusclient, configurable with -nexus (default localhost:8987).
/api/v1/changes ignored the since query param and always returned from
sequence 0 (`since = 0` regardless of what was parsed) — fixed to actually
parse and use it, so change-cursor polling works.
The MCP hexis.execute tool only forwarded capability_id/target_entity_id/
arguments/idempotency_key, silently dropping entity_version, requested_by,
origin, correlation_id, causation_id, resolution_evidence, and
confirmation_id even though the native HTTP API and domain.ExecuteRequest
already supported all of them — MCP callers now get full parity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
Execution events (started/failed/succeeded) never carried the
correlation/causation IDs from the ExecuteRequest, even though the
fields existed on Execution. Added CausationID to ExecuteRequest and
Execution, and split emitEvent into a correlated variant used
throughout the execution lifecycle. handleExecute falls back to
X-Correlation-ID/X-Causation-ID headers when the body omits them.
Also add X-Hexis-Version negotiation on /api/v1/*, matching Nexus.
Part of Vikunja #273.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
Closes the biggest gap between the running execution engine and
ECOSYSTEM-SPEC.md §4.3: confirmations were entirely unmodeled, so any
capability could execute unconfirmed regardless of requires_confirmation.
- New confirmations table + Confirmation domain type; POST
/api/v1/confirmations mints a TTL-bound (120s) confirmation binding
capability id+version, target entity, and a sorted-key args hash.
- Execute() now requires a valid pending confirmation when the
capability demands one: rejects missing, expired, consumed, or
args/version-mismatched confirmations; consumes on success.
- Capabilities gain enabled (destructive risk defaults to disabled,
matching "must be turned on explicitly") and timeout_seconds.
- One in-flight execution per (capability_id, target_entity_id); a
second concurrent attempt is rejected (surfaced as 409 over HTTP).
- Wall-clock timeout per capability now wraps the provider call; on
timeout the outcome is "unknown" (new ExecutionStatus), never
"failed", and the run is never auto-retried.
- 9 new engine tests cover each guard from the spec's Phase 6 gate.
Vikunja #274.
Go daemon (hexisd/hexisctl) implementing capability registry, guarded
execution (confirmations, blessed-entity checks), systemd/workspace-mcp
providers per ECOSYSTEM-SPEC.md. Snapshotting existing working state
before further development.