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 }