Files
hexis/internal/wire/capability.go
T
kami dda4acfbb6 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
2026-07-30 23:40:20 +04:00

55 lines
1.7 KiB
Go

// Package wire converts internal domain objects into the public wire shapes
// defined in pkg/client. It is the single serialization point: the HTTP
// handler and the MCP adapter both go through it, so a capability looks the
// same on every surface Hexis exposes.
package wire
import (
"github.com/kami/hexis/internal/domain"
"github.com/kami/hexis/pkg/client"
)
// Capability converts a domain capability into the public wire shape.
//
// Both `capability_id` and `id` are populated with the same value; see the
// compatibility note on client.Capability for why the alias is retained.
func Capability(c *domain.Capability) client.Capability {
if c == nil {
return client.Capability{}
}
targetTypes := c.TargetTypes
if targetTypes == nil {
targetTypes = []string{}
}
return client.Capability{
CapabilityID: c.ID,
ID: c.ID,
Name: c.Name,
Description: c.Description,
TargetTypes: targetTypes,
TargetEntityID: c.TargetEntityID,
Provider: c.Provider,
Operation: c.Operation,
Risk: c.Risk,
ReadOnly: c.ReadOnly,
ExpectedSideEffects: c.ExpectedSideEffects,
RequiresConfirmation: c.RequiresConfirmation,
Enabled: c.Enabled,
TimeoutSeconds: c.TimeoutSeconds,
Attributes: c.Attributes,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
Version: c.Version,
}
}
// Capabilities converts a slice, never returning nil so the JSON encoding is
// `[]` rather than `null`.
func Capabilities(caps []*domain.Capability) []client.Capability {
out := make([]client.Capability, 0, len(caps))
for _, c := range caps {
out = append(out, Capability(c))
}
return out
}