Initial commit: Hexis capability registry + execution service baseline
Go daemon (hexisd/hexisctl) implementing capability registry, guarded execution (confirmations, blessed-entity checks), systemd/workspace-mcp providers per ECOSYSTEM-SPEC.md. Snapshotting existing working state before further development.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kami/hexis/internal/domain"
|
||||
"github.com/kami/hexis/internal/provider"
|
||||
"github.com/kami/hexis/internal/storage"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
store storage.Interface
|
||||
registry *provider.Registry
|
||||
}
|
||||
|
||||
func New(store storage.Interface, registry *provider.Registry) *Engine {
|
||||
return &Engine{store: store, registry: registry}
|
||||
}
|
||||
|
||||
type ExecuteResult struct {
|
||||
Execution *domain.Execution `json:"execution"`
|
||||
}
|
||||
|
||||
func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) {
|
||||
if req.IdempotencyKey != "" {
|
||||
existing, err := e.store.GetExecutionByIdempotencyKey(req.IdempotencyKey)
|
||||
if err == nil {
|
||||
return &ExecuteResult{Execution: existing}, nil
|
||||
}
|
||||
}
|
||||
|
||||
capability, err := e.store.GetCapability(req.CapabilityID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("capability: %w", err)
|
||||
}
|
||||
|
||||
if capability.TargetEntityID != "" && capability.TargetEntityID != req.TargetEntityID {
|
||||
return nil, domain.ErrCapabilityNotBound
|
||||
}
|
||||
|
||||
prov, err := e.registry.Get(capability.Provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("provider: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
exec := &domain.Execution{
|
||||
ID: domain.NewExecutionID(),
|
||||
CapabilityID: req.CapabilityID,
|
||||
TargetEntityID: req.TargetEntityID,
|
||||
EntityVersion: req.EntityVersion,
|
||||
Arguments: req.Arguments,
|
||||
RequestedBy: req.RequestedBy,
|
||||
Origin: req.Origin,
|
||||
IdempotencyKey: req.IdempotencyKey,
|
||||
Status: domain.ExecutionStarted,
|
||||
CorrelationID: req.CorrelationID,
|
||||
ResolutionEvidence: req.ResolutionEvidence,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if exec.Arguments == nil {
|
||||
exec.Arguments = map[string]any{}
|
||||
}
|
||||
if exec.RequestedBy == nil {
|
||||
exec.RequestedBy = map[string]string{}
|
||||
}
|
||||
if exec.Origin == nil {
|
||||
exec.Origin = map[string]string{}
|
||||
}
|
||||
|
||||
if err := e.store.CreateExecution(exec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.emitEvent(domain.EventExecutionStarted, exec.ID, map[string]any{
|
||||
"capability_id": req.CapabilityID,
|
||||
"target_entity_id": req.TargetEntityID,
|
||||
"provider": capability.Provider,
|
||||
"correlation_id": req.CorrelationID,
|
||||
})
|
||||
|
||||
result, execErr := prov.Execute(capability, req)
|
||||
exec.UpdatedAt = time.Now().UTC()
|
||||
|
||||
if execErr != nil {
|
||||
exec.Status = domain.ExecutionFailed
|
||||
exec.Error = execErr.Error()
|
||||
exec.Result = map[string]any{"error": execErr.Error()}
|
||||
e.emitEvent(domain.EventExecutionFailed, exec.ID, map[string]any{
|
||||
"capability_id": req.CapabilityID,
|
||||
"error": execErr.Error(),
|
||||
})
|
||||
} else {
|
||||
exec.Status = domain.ExecutionSucceeded
|
||||
exec.Result = result
|
||||
e.emitEvent(domain.EventExecutionSucceeded, exec.ID, map[string]any{
|
||||
"capability_id": req.CapabilityID,
|
||||
})
|
||||
}
|
||||
|
||||
if err := e.store.UpdateExecution(exec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ExecuteResult{Execution: exec}, nil
|
||||
}
|
||||
|
||||
func (e *Engine) ValidateTarget(capabilityID, entityID string) error {
|
||||
cap, err := e.store.GetCapability(capabilityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cap.TargetEntityID != "" && cap.TargetEntityID != entityID {
|
||||
return domain.ErrCapabilityNotBound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) emitEvent(evtType domain.HexisEventType, entityID string, payload map[string]any) {
|
||||
e.store.AppendEvent(&domain.Event{
|
||||
ID: domain.NewEventID(),
|
||||
Type: evtType,
|
||||
Timestamp: time.Now().UTC(),
|
||||
Payload: payload,
|
||||
})
|
||||
}
|
||||
|
||||
type ResolveRequest struct {
|
||||
Query string `json:"query"`
|
||||
Types []string `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
type ResolveResult struct {
|
||||
Status string `json:"status"`
|
||||
Candidates []map[string]any `json:"candidates,omitempty"`
|
||||
EntityID string `json:"entity_id,omitempty"`
|
||||
}
|
||||
|
||||
var _ json.Marshaler = (*ResolveResult)(nil)
|
||||
|
||||
func (r *ResolveResult) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{"status": r.Status}
|
||||
if r.Candidates != nil {
|
||||
m["candidates"] = r.Candidates
|
||||
}
|
||||
if r.EntityID != "" {
|
||||
m["entity_id"] = r.EntityID
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
Reference in New Issue
Block a user