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:
kami
2026-07-20 00:50:37 +04:00
commit cca63269e1
20 changed files with 2461 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
package provider
import (
"fmt"
"sync"
"github.com/kami/hexis/internal/domain"
)
type Provider interface {
Name() string
Execute(capability *domain.Capability, req *domain.ExecuteRequest) (map[string]any, error)
}
type Registry struct {
mu sync.RWMutex
providers map[string]Provider
}
func NewRegistry() *Registry {
return &Registry{
providers: make(map[string]Provider),
}
}
func (r *Registry) Register(p Provider) {
r.mu.Lock()
defer r.mu.Unlock()
r.providers[p.Name()] = p
}
func (r *Registry) Get(name string) (Provider, error) {
r.mu.RLock()
defer r.mu.RUnlock()
p, ok := r.providers[name]
if !ok {
return nil, fmt.Errorf("provider %q not found", name)
}
return p, nil
}
func (r *Registry) List() []string {
r.mu.RLock()
defer r.mu.RUnlock()
var names []string
for n := range r.providers {
names = append(names, n)
}
return names
}
+68
View File
@@ -0,0 +1,68 @@
package provider
import (
"fmt"
"os/exec"
"strings"
"github.com/kami/hexis/internal/domain"
)
type SystemdProvider struct{}
func NewSystemdProvider() *SystemdProvider {
return &SystemdProvider{}
}
func (p *SystemdProvider) Name() string { return "systemd" }
func (p *SystemdProvider) Execute(capability *domain.Capability, req *domain.ExecuteRequest) (map[string]any, error) {
unitName := req.TargetEntityID
if unitName == "" {
unitName = capability.TargetEntityID
}
if !isValidUnitName(unitName) {
return nil, fmt.Errorf("invalid unit name: %q", unitName)
}
var cmd *exec.Cmd
switch capability.Operation {
case "restart":
cmd = exec.Command("systemctl", "restart", unitName)
case "start":
cmd = exec.Command("systemctl", "start", unitName)
case "stop":
cmd = exec.Command("systemctl", "stop", unitName)
case "status":
cmd = exec.Command("systemctl", "status", unitName)
case "reload":
cmd = exec.Command("systemctl", "reload", unitName)
case "enable":
cmd = exec.Command("systemctl", "enable", unitName)
case "disable":
cmd = exec.Command("systemctl", "disable", unitName)
default:
return nil, fmt.Errorf("unsupported systemd operation: %s", capability.Operation)
}
output, err := cmd.CombinedOutput()
if err != nil {
return map[string]any{
"stdout": string(output),
"exit_code": cmd.ProcessState.ExitCode(),
}, fmt.Errorf("systemctl %s failed: %w\n%s", capability.Operation, err, string(output))
}
return map[string]any{
"stdout": string(output),
"exit_code": 0,
}, nil
}
func isValidUnitName(name string) bool {
if name == "" || strings.Contains(name, "..") || strings.Contains(name, "/") || strings.Contains(name, ";") || strings.Contains(name, "|") || strings.Contains(name, "$") || strings.Contains(name, "`") || strings.Contains(name, "'") || strings.Contains(name, `"`) || strings.Contains(name, "\\") {
return false
}
return true
}
+228
View File
@@ -0,0 +1,228 @@
package provider
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/kami/hexis/internal/domain"
"gopkg.in/yaml.v3"
)
func LoadToolAllowlist(path string) (ToolAllowlist, error) {
data, err := os.ReadFile(path)
if err != nil {
return ToolAllowlist{}, fmt.Errorf("read allowlist: %w", err)
}
var allowlist ToolAllowlist
if err := yaml.Unmarshal(data, &allowlist); err != nil {
return ToolAllowlist{}, fmt.Errorf("parse allowlist: %w", err)
}
if allowlist.Tools == nil {
allowlist.Tools = map[string]ToolMapping{}
}
return allowlist, nil
}
type WorkspaceMCPProvider struct {
mu sync.RWMutex
baseURL string
httpClient *http.Client
tools []WorkspaceTool
allowlist ToolAllowlist
}
type WorkspaceTool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema any `json:"inputSchema"`
}
type ToolAllowlist struct {
Tools map[string]ToolMapping `yaml:"tools" json:"tools"`
}
type ToolMapping struct {
Capability string `yaml:"capability" json:"capability"`
Risk string `yaml:"risk" json:"risk"`
TargetType string `yaml:"target_type,omitempty" json:"target_type,omitempty"`
ReadOnly bool `yaml:"read_only" json:"read_only"`
SideEffects string `yaml:"side_effects,omitempty" json:"side_effects,omitempty"`
Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
AllowParams []string `yaml:"allow_params,omitempty" json:"allow_params,omitempty"`
}
func (r *ToolAllowlist) IsEnabled(name string) bool {
m, ok := r.Tools[name]
if !ok {
return false
}
if m.Enabled != nil && !*m.Enabled {
return false
}
return true
}
func (r *ToolAllowlist) Mapping(name string) (ToolMapping, bool) {
m, ok := r.Tools[name]
return m, ok
}
func NewWorkspaceMCPProvider(baseURL string, allowlist ToolAllowlist) *WorkspaceMCPProvider {
return &WorkspaceMCPProvider{
baseURL: strings.TrimRight(baseURL, "/"),
httpClient: &http.Client{
Timeout: 60 * time.Second,
},
allowlist: allowlist,
}
}
func (p *WorkspaceMCPProvider) Name() string {
return "workspace_mcp"
}
func (p *WorkspaceMCPProvider) DiscoverTools() ([]WorkspaceTool, error) {
resp, err := p.httpClient.Get(fmt.Sprintf("%s/api/tools", p.baseURL))
if err != nil {
return nil, fmt.Errorf("discover workspace tools: %w", err)
}
defer resp.Body.Close()
var result struct {
Tools []WorkspaceTool `json:"tools"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode workspace tools: %w", err)
}
p.mu.Lock()
p.tools = result.Tools
p.mu.Unlock()
return result.Tools, nil
}
func (p *WorkspaceMCPProvider) DiscoveredTools() []WorkspaceTool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.tools
}
func (p *WorkspaceMCPProvider) Execute(capability *domain.Capability, req *domain.ExecuteRequest) (map[string]any, error) {
toolName := p.capabilityToTool(capability.Name)
if toolName == "" {
return nil, fmt.Errorf("no workspace tool mapped for capability %q", capability.Name)
}
mapping, ok := p.allowlist.Mapping(toolName)
if !ok {
return nil, fmt.Errorf("tool %q not in allowlist", toolName)
}
if !p.allowlist.IsEnabled(toolName) {
return nil, fmt.Errorf("tool %q is disabled in allowlist", toolName)
}
args := map[string]any{}
if req.Arguments != nil {
if len(mapping.AllowParams) > 0 {
for k, v := range req.Arguments {
for _, allowed := range mapping.AllowParams {
if k == allowed {
args[k] = v
break
}
}
}
} else {
args = req.Arguments
}
}
body, _ := json.Marshal(args)
resp, err := p.httpClient.Post(
fmt.Sprintf("%s/api/tool/%s", p.baseURL, toolName),
"application/json",
bytes.NewReader(body),
)
if err != nil {
return nil, fmt.Errorf("call workspace tool %q: %w", toolName, err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read workspace tool response: %w", err)
}
if resp.StatusCode >= 400 {
return map[string]any{
"error": string(respBody),
"http_status": resp.StatusCode,
}, fmt.Errorf("workspace tool %q returned %d: %s", toolName, resp.StatusCode, string(respBody))
}
var result any
if err := json.Unmarshal(respBody, &result); err != nil {
return map[string]any{
"raw": string(respBody),
}, nil
}
return map[string]any{
"result": result,
}, nil
}
func (p *WorkspaceMCPProvider) capabilityToTool(capName string) string {
for toolName, mapping := range p.allowlist.Tools {
if mapping.Capability == capName {
return toolName
}
}
return ""
}
func (p *WorkspaceMCPProvider) BuildCapabilities() []domain.Capability {
var caps []domain.Capability
for toolName, mapping := range p.allowlist.Tools {
if !p.allowlist.IsEnabled(toolName) {
continue
}
readOnly := mapping.ReadOnly
if mapping.Risk == "read" {
readOnly = true
}
now := time.Now().UTC()
caps = append(caps, domain.Capability{
ID: fmt.Sprintf("cap_ws_%s", strings.ReplaceAll(mapping.Capability, ".", "_")),
Name: mapping.Capability,
Description: fmt.Sprintf("Workspace tool: %s", toolName),
TargetTypes: ifString(mapping.TargetType != "", []string{mapping.TargetType}, nil),
TargetEntityID: "",
Provider: "workspace_mcp",
Operation: toolName,
Risk: mapping.Risk,
ReadOnly: readOnly,
ExpectedSideEffects: mapping.SideEffects,
CreatedAt: now,
UpdatedAt: now,
Version: 1,
})
}
return caps
}
func ifString(cond bool, a, b []string) []string {
if cond {
return a
}
return b
}