Finding 3 of REVIEW-2026-07-30.md. target_entity_id was accepted as any
non-empty string; the engine only compared it against a pinned TargetEntityID,
which is empty for every registered capability. Spec §4.3: "Hexis never
accepts a free-text target. Ever."
Targets are now checked in order: ent_ shape (free, never touches the
network), pinned target, existence in Nexus, entity still active, and a match
against the capability's TargetTypes. Validation runs before a confirmation is
consumed, so a bad target cannot burn one, and at confirmation-mint time too,
since a confirmation binds a target.
Two deliberate calls:
Nexus unreachable fails closed (503, ErrTargetUnverifiable). Failing open
would reinstate exactly this hole the moment Nexus blips, and hand it to
anyone able to degrade Nexus. Hexis holds no entity table, so "unreachable"
and "I cannot tell if this target is real" are the same statement. The cost is
that executes now require Nexus liveness; the lookup is bounded at 5s so a
hung Nexus fails fast rather than consuming the capability timeout.
An empty TargetTypes means no type constraint, not a bypass — the entity must
still exist, be canonical and be active. Rejecting empty outright would
disable 16 of the 19 registered capabilities, since only the docker.* entries
declare a target type.
The spec's stronger blessing guard is not implementable: Nexus has no blessing
concept at all. This is the achievable guard, and strictly weaker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
Findings 4 and 7 of REVIEW-2026-07-30.md, as a single user_version step
(11 -> 12) because they alter the same table.
confirmation_id was written by the INSERT and absent from all three executions
SELECTs, so it always read back empty: "which confirmation authorized this
destructive act" was unanswerable from the API. causation_id was set on the
struct by the engine with no column to land in, and capability_version was
missing entirely despite spec §4.1 and the precedent on confirmations. All
three are now persisted and selected back.
The one-in-flight-per-(capability, target) rule was check-then-insert with no
constraint between, so two concurrent requests could both proceed. It is now a
partial UNIQUE index; the engine's pre-insert query is demoted to an advisory
fast path and a constraint violation maps to ErrExecutionInFlight (409), kept
distinct from the idempotency-key replay case.
Note the migration also rewrites pre-existing duplicate in-flight rows to
status='unknown', keeping the lowest id per pair — creating the index would
otherwise fail outright on any database holding stale started rows, which the
old non-unique index permitted indefinitely. That is a write to existing audit
rows, not just DDL. Rows predating this migration get capability_version=0,
which is indistinguishable from a genuine 0; backfill is not possible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
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.