cca63269e1
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.
154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
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)
|
|
}
|