Propagate correlation_id/causation_id through execution events, add API version header

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
This commit is contained in:
kami
2026-07-20 11:11:49 +04:00
parent 89d8433d17
commit ed593efb71
5 changed files with 390 additions and 14 deletions
+36 -6
View File
@@ -21,15 +21,39 @@ func NewHandler(store storage.Interface, engine *execution.Engine) *Handler {
return &Handler{store: store, engine: engine}
}
// SupportedAPIVersion is the version this server implements. A request
// carrying X-Hexis-Version set to anything else is rejected — clients that
// don't send the header at all are allowed through unversioned, to avoid
// breaking callers mid-rollout.
const SupportedAPIVersion = "v1"
func (h *Handler) Register(mux *http.ServeMux) {
mux.HandleFunc("/health", h.health)
mux.HandleFunc("/ready", h.ready)
mux.HandleFunc("/api/v1/capabilities", h.handleCapabilities)
mux.HandleFunc("/api/v1/capabilities/", h.handleCapabilityByID)
mux.HandleFunc("/api/v1/execute", h.handleExecute)
mux.HandleFunc("/api/v1/confirmations", h.handleConfirmations)
mux.HandleFunc("/api/v1/executions/", h.handleExecutionByID)
mux.HandleFunc("/api/v1/changes", h.handleChanges)
api := http.NewServeMux()
api.HandleFunc("/api/v1/capabilities", h.handleCapabilities)
api.HandleFunc("/api/v1/capabilities/", h.handleCapabilityByID)
api.HandleFunc("/api/v1/execute", h.handleExecute)
api.HandleFunc("/api/v1/confirmations", h.handleConfirmations)
api.HandleFunc("/api/v1/executions/", h.handleExecutionByID)
api.HandleFunc("/api/v1/changes", h.handleChanges)
mux.Handle("/api/v1/", versionCheck(api))
}
func versionCheck(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if v := r.Header.Get("X-Hexis-Version"); v != "" && v != SupportedAPIVersion {
writeJSON(w, http.StatusPreconditionFailed, map[string]string{
"error": "unsupported API version",
"requested_version": v,
"supported_version": SupportedAPIVersion,
})
return
}
next.ServeHTTP(w, r)
})
}
func (h *Handler) health(w http.ResponseWriter, r *http.Request) {
@@ -210,6 +234,12 @@ func (h *Handler) handleExecute(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, errorResponse("capability_id and target_entity_id are required"))
return
}
if req.CorrelationID == "" {
req.CorrelationID = r.Header.Get("X-Correlation-ID")
}
if req.CausationID == "" {
req.CausationID = r.Header.Get("X-Causation-ID")
}
result, err := h.engine.Execute(&req)
if err != nil {