Serve capabilities through one serializer and add GET /api/v1/executions
The two "refactor later" items from REVIEW-2026-07-30.md; they share the wire types, so they land together. A capability had four divergent wire shapes — the HTTP handler, the MCP adapter, pkg/client, and Maven's vendored copy of it. There is now a single definition in pkg/client, mapped from domain by internal/wire and used by the HTTP list/create/get paths and all four MCP surfaces. It lives in pkg/client rather than internal so external consumers need not vendor internal/domain, and so producer and consumer are literally the same type. The unified shape is a strict superset of all four predecessors; nothing was dropped. It adds enabled and requires_confirmation to the list responses (never omitempty — an absent bool reads as unknown, not false), capability_id to the MCP and client shapes, and the timing/attribute/version fields previously only on get-by-ID. target_types and the list itself now serialize as [] rather than null. Both `id` and `capability_id` are deliberately kept, carrying the same value. Maven decodes `id`; the spec and the rest of the API say `capability_id`. Bearer auth is already a breaking change for that consumer, and stacking a second silent one is the wrong trade — the redundancy stays until every consumer is confirmed on capability_id, then `id` goes in an announced removal. A test pins this and says so. GET /api/v1/executions?entity_id=&since=&limit= implements spec §4.5, which the Command Center needs. `since` reuses the changes-feed cursor convention rather than inventing a second paging idiom. That cursor is the row's implicit SQLite rowid, which is safe only while nothing deletes executions and nothing VACUUMs — both would renumber and silently invalidate outstanding cursors. If retention is ever added, this must become an explicit monotonic column first; the constraint is documented at the query site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
This commit is contained in:
+51
-9
@@ -206,7 +206,7 @@ func (h *Handler) createCapability(w http.ResponseWriter, r *http.Request) {
|
||||
Payload: map[string]any{"capability_id": cap.ID, "name": cap.Name},
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusCreated, cap)
|
||||
writeJSON(w, http.StatusCreated, wire.Capability(cap))
|
||||
}
|
||||
|
||||
func (h *Handler) handleCapabilityByID(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -232,7 +232,7 @@ func (h *Handler) getCapability(w http.ResponseWriter, r *http.Request, id strin
|
||||
writeJSON(w, http.StatusNotFound, errorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cap)
|
||||
writeJSON(w, http.StatusOK, wire.Capability(cap))
|
||||
}
|
||||
|
||||
func (h *Handler) deleteCapability(w http.ResponseWriter, r *http.Request, id string) {
|
||||
@@ -327,19 +327,61 @@ func (h *Handler) handleConfirmations(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
conf, err := h.engine.CreateConfirmation(req.CapabilityID, req.TargetEntityID, req.Requester, req.Arguments)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, domain.ErrCapabilityNotFound) {
|
||||
status = http.StatusNotFound
|
||||
} else if errors.Is(err, domain.ErrCapabilityNotBound) {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
writeJSON(w, status, errorResponse(err.Error()))
|
||||
writeJSON(w, executeErrorStatus(err), errorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, conf)
|
||||
}
|
||||
|
||||
// handleExecutions serves GET /api/v1/executions?entity_id=&since=&limit=
|
||||
// (ECOSYSTEM-SPEC.md §4.5) — the execution history the Command Center's
|
||||
// Overview and Executions surfaces render.
|
||||
//
|
||||
// `since` follows the same cursor convention as /api/v1/changes: an integer
|
||||
// sequence, exclusive, with results ordered ascending. Callers page by passing
|
||||
// the `seq` of the last execution they saw. This deliberately reuses the
|
||||
// changes-feed style rather than introducing a timestamp cursor, so a client
|
||||
// only has to learn one paging idiom against Hexis.
|
||||
func (h *Handler) handleExecutions(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errorResponse("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
|
||||
var since int64
|
||||
if s := q.Get("since"); s != "" {
|
||||
parsed, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil || parsed < 0 {
|
||||
writeJSON(w, http.StatusBadRequest, errorResponse("since must be a non-negative integer sequence"))
|
||||
return
|
||||
}
|
||||
since = parsed
|
||||
}
|
||||
|
||||
limit := storage.MaxExecutionPageSize
|
||||
if l := q.Get("limit"); l != "" {
|
||||
parsed, err := strconv.Atoi(l)
|
||||
if err != nil || parsed <= 0 {
|
||||
writeJSON(w, http.StatusBadRequest, errorResponse("limit must be a positive integer"))
|
||||
return
|
||||
}
|
||||
limit = parsed
|
||||
}
|
||||
|
||||
execs, err := h.store.ListExecutions(q.Get("entity_id"), since, limit)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
if execs == nil {
|
||||
execs = []*domain.Execution{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, execs)
|
||||
}
|
||||
|
||||
func (h *Handler) handleExecutionByID(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errorResponse("method not allowed"))
|
||||
|
||||
Reference in New Issue
Block a user