hexis: re-vendor the client so a configured token is actually sent
The vendored copy of github.com/kami/hexis predated Client.WithToken: no token field, no setter, no header hook, and an unexported httpClient, so there was no way to attach auth from outside the package. wireEcosystem handled that by refusing to wire Hexis at all when a token was configured, which was the honest reading of the code but left the deployment silently without its executing service. go.mod already replaces the module with /home/kami/apps/hexis, and that source has had WithToken and the Bearer header for a while. Only the checked-in vendor/ copy was stale. Refreshed it (client.go plus the new capability.go) and wired Hexis like Nexus and Praxis. Two tests cover the outcome the refusal was standing in for: a configured token reaches the wire as Authorization, and no token still wires unauthed, because Hexis without auth is a valid deployment on a trusted box. Also corrected the discoverCapabilities comment. It claimed the client stamped the correlation header on Execute only; do() stamps it on every request, and did before the re-vendor too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
+62
@@ -0,0 +1,62 @@
|
||||
package client
|
||||
|
||||
import "time"
|
||||
|
||||
// Capability is THE wire shape for a Hexis capability.
|
||||
//
|
||||
// There is exactly one definition of it, here, and every producer in this
|
||||
// repository serializes through it: the HTTP handler (GET/POST
|
||||
// /api/v1/capabilities, GET /api/v1/capabilities/{id}), the MCP adapter
|
||||
// (hexis.list_capabilities), and this client's decode path. It lives in
|
||||
// pkg/client rather than internal/ so that external consumers get the shape
|
||||
// without vendoring internal packages; internal/wire holds the
|
||||
// domain.Capability -> Capability conversion.
|
||||
//
|
||||
// Compatibility note — `id` and `capability_id` are BOTH emitted, deliberately.
|
||||
// They always carry the same value. Maven's vendored consumer decodes `id`
|
||||
// (cmd/mavend/voice.go matches on Capability.ID); the ECOSYSTEM-SPEC.md §4.1
|
||||
// schema and the rest of the Hexis API name the column `capability_id`. Hexis
|
||||
// is mid-rollout of bearer auth on /api/v1/, which is already one breaking
|
||||
// change for that consumer; dropping either alias here would stack a second,
|
||||
// silent one on top. Both stay until every consumer is confirmed to read
|
||||
// `capability_id`, at which point `id` can be removed in a deliberate,
|
||||
// announced change. Do not "clean this up" incidentally.
|
||||
type Capability struct {
|
||||
// CapabilityID is the canonical field (ECOSYSTEM-SPEC.md §4.1).
|
||||
CapabilityID string `json:"capability_id"`
|
||||
// ID is a deprecated alias for CapabilityID, kept for wire compatibility.
|
||||
// Always identical to CapabilityID. Prefer CapabilityID in new code.
|
||||
ID string `json:"id"`
|
||||
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TargetTypes []string `json:"target_types"`
|
||||
TargetEntityID string `json:"target_entity_id,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
Operation string `json:"operation"`
|
||||
Risk string `json:"risk,omitempty"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
ExpectedSideEffects string `json:"expected_side_effects,omitempty"`
|
||||
|
||||
// RequiresConfirmation and Enabled are server-derived from the risk tier
|
||||
// and are never settable by a caller. listCapabilities used to omit both,
|
||||
// which left clients unable to tell a callable capability from one that
|
||||
// would be rejected with 403; the unified shape always carries them.
|
||||
RequiresConfirmation bool `json:"requires_confirmation"`
|
||||
Enabled bool `json:"enabled"`
|
||||
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
|
||||
|
||||
Attributes map[string]any `json:"attributes,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
Version int64 `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// EffectiveID returns the capability ID, tolerating a peer that sends only one
|
||||
// of the two aliases.
|
||||
func (c Capability) EffectiveID() string {
|
||||
if c.CapabilityID != "" {
|
||||
return c.CapabilityID
|
||||
}
|
||||
return c.ID
|
||||
}
|
||||
+71
-39
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -42,6 +44,7 @@ func causationIDFrom(ctx context.Context) string {
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
token string
|
||||
}
|
||||
|
||||
func New(baseURL string) *Client {
|
||||
@@ -51,6 +54,12 @@ func New(baseURL string) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// WithToken sets the shared bearer token sent on every /api/v1/ request.
|
||||
func (c *Client) WithToken(token string) *Client {
|
||||
c.token = token
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, body, result any) error {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
@@ -67,6 +76,9 @@ func (c *Client) do(ctx context.Context, method, path string, body, result any)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Hexis-Version", APIVersion)
|
||||
if c.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
if id := correlationIDFrom(ctx); id != "" {
|
||||
req.Header.Set("X-Correlation-ID", id)
|
||||
}
|
||||
@@ -97,8 +109,37 @@ func (c *Client) do(ctx context.Context, method, path string, body, result any)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Capability struct {
|
||||
ID string `json:"id"`
|
||||
// Capability is defined in capability.go — the single wire shape shared by
|
||||
// the HTTP handler, the MCP adapter and this client.
|
||||
|
||||
type Execution struct {
|
||||
// Seq is the pagination cursor for Executions; see the `since` parameter.
|
||||
// Zero on single-execution reads.
|
||||
Seq int64 `json:"seq,omitempty"`
|
||||
ID string `json:"id"`
|
||||
CapabilityID string `json:"capability_id"`
|
||||
TargetEntityID string `json:"target_entity_id"`
|
||||
Status string `json:"status"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
RequestedBy map[string]string `json:"requested_by,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
CausationID string `json:"causation_id,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
}
|
||||
|
||||
type ExecuteRequest struct {
|
||||
CapabilityID string `json:"capability_id"`
|
||||
TargetEntityID string `json:"target_entity_id"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
RequestedBy map[string]string `json:"requested_by,omitempty"`
|
||||
Origin map[string]string `json:"origin,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
CausationID string `json:"causation_id,omitempty"`
|
||||
}
|
||||
|
||||
type CreateCapabilityRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TargetTypes []string `json:"target_types"`
|
||||
@@ -110,47 +151,11 @@ type Capability struct {
|
||||
ExpectedSideEffects string `json:"expected_side_effects,omitempty"`
|
||||
}
|
||||
|
||||
type Execution struct {
|
||||
ID string `json:"id"`
|
||||
CapabilityID string `json:"capability_id"`
|
||||
TargetEntityID string `json:"target_entity_id"`
|
||||
Status string `json:"status"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
RequestedBy map[string]string `json:"requested_by,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
CausationID string `json:"causation_id,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
}
|
||||
|
||||
type ExecuteRequest struct {
|
||||
CapabilityID string `json:"capability_id"`
|
||||
TargetEntityID string `json:"target_entity_id"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
RequestedBy map[string]string `json:"requested_by,omitempty"`
|
||||
Origin map[string]string `json:"origin,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
CausationID string `json:"causation_id,omitempty"`
|
||||
}
|
||||
|
||||
type CreateCapabilityRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TargetTypes []string `json:"target_types"`
|
||||
TargetEntityID string `json:"target_entity_id,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
Operation string `json:"operation"`
|
||||
Risk string `json:"risk,omitempty"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
ExpectedSideEffects string `json:"expected_side_effects,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) Capabilities(ctx context.Context, entityID string) ([]Capability, error) {
|
||||
var result []Capability
|
||||
path := "/api/v1/capabilities"
|
||||
if entityID != "" {
|
||||
path += "?entity_id=" + entityID
|
||||
path += "?entity_id=" + url.QueryEscape(entityID)
|
||||
}
|
||||
if err := c.do(ctx, http.MethodGet, path, nil, &result); err != nil {
|
||||
return nil, err
|
||||
@@ -190,6 +195,33 @@ func (c *Client) GetExecution(ctx context.Context, id string) (*Execution, error
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Executions returns execution history, newest last, ordered by ascending
|
||||
// `seq` (ECOSYSTEM-SPEC.md §4.5).
|
||||
//
|
||||
// entityID, when non-empty, filters to executions against that target entity.
|
||||
// since is an exclusive cursor: pass 0 for the first page, then the Seq of the
|
||||
// last element returned. The server caps a page at 100 rows, so a full page
|
||||
// means "call again with the new cursor".
|
||||
func (c *Client) Executions(ctx context.Context, entityID string, since int64) ([]Execution, error) {
|
||||
q := url.Values{}
|
||||
if entityID != "" {
|
||||
q.Set("entity_id", entityID)
|
||||
}
|
||||
if since > 0 {
|
||||
q.Set("since", strconv.FormatInt(since, 10))
|
||||
}
|
||||
path := "/api/v1/executions"
|
||||
if len(q) > 0 {
|
||||
path += "?" + q.Encode()
|
||||
}
|
||||
|
||||
var result []Execution
|
||||
if err := c.do(ctx, http.MethodGet, path, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) Health(ctx context.Context) error {
|
||||
return c.do(ctx, http.MethodGet, "/health", nil, nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user