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:
kami
2026-07-30 23:40:20 +04:00
parent 9e6b995538
commit dda4acfbb6
10 changed files with 926 additions and 67 deletions
+10 -19
View File
@@ -14,6 +14,7 @@ import (
"github.com/kami/hexis/internal/execution"
"github.com/kami/hexis/internal/nexusclient"
"github.com/kami/hexis/internal/storage"
"github.com/kami/hexis/internal/wire"
)
type Adapter struct {
@@ -145,22 +146,12 @@ func (a *Adapter) handleListCapabilities(ctx context.Context, req mcp.CallToolRe
return mcp.NewToolResultError(fmt.Sprintf("list capabilities: %v", err)), nil
}
var result []map[string]any
for _, c := range caps {
result = append(result, map[string]any{
"id": c.ID,
"name": c.Name,
"description": c.Description,
"target_types": c.TargetTypes,
"target_entity_id": c.TargetEntityID,
"provider": c.Provider,
"operation": c.Operation,
"risk": c.Risk,
"read_only": c.ReadOnly,
})
}
data, _ := json.MarshalIndent(result, "", " ")
// Same serializer as the HTTP API, so an MCP client and an HTTP client see
// an identical capability — including `enabled` and
// `requires_confirmation`, which this tool previously hid. Tool
// availability is not permission (spec §4.5), but a client that can see
// the guard fields can at least explain a 403 instead of guessing.
data, _ := json.MarshalIndent(wire.Capabilities(caps), "", " ")
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{Type: "text", Text: string(data)},
@@ -179,7 +170,7 @@ func (a *Adapter) handleInspectCapability(ctx context.Context, req mcp.CallToolR
return mcp.NewToolResultError(fmt.Sprintf("capability not found: %v", err)), nil
}
data, _ := json.MarshalIndent(cap, "", " ")
data, _ := json.MarshalIndent(wire.Capability(cap), "", " ")
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{Type: "text", Text: string(data)},
@@ -317,7 +308,7 @@ func (a *Adapter) handleCapabilitiesResource(ctx context.Context, req mcp.ReadRe
if err != nil {
return nil, err
}
data, _ := json.MarshalIndent(caps, "", " ")
data, _ := json.MarshalIndent(wire.Capabilities(caps), "", " ")
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: "hexis://capabilities",
@@ -339,7 +330,7 @@ func (a *Adapter) handleCapabilityResourceTemplate(ctx context.Context, req mcp.
return nil, fmt.Errorf("capability %s: %w", id, err)
}
data, _ := json.MarshalIndent(cap, "", " ")
data, _ := json.MarshalIndent(wire.Capability(cap), "", " ")
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: uri,