c7325a20d4
Findings 1 and 2 of REVIEW-2026-07-30.md, which must land together: every workspace capability registered with enabled=false, so the only working provider could never execute. Fixing that alone would have turned a dead execution path into a reachable one on an unauthenticated port. Auth: a shared bearer token (HEXIS_API_TOKEN) is now required on the whole /api/v1/ surface, compared with crypto/subtle.ConstantTimeCompare. /health and /ready stay open for probes. It fails closed twice over — hexisd refuses to start with an empty token, and the middleware returns 503 rather than ever serving unauthenticated. Guards: `enabled` and `requires_confirmation` are no longer readable from the request body at all. Previously the handler derived the correct §4.3 default and then let the caller override it, which is worse than no guard because it reads as enforced. Both are now derived from the risk tier by shared helpers in domain, used by the HTTP and provider registration paths alike; unrecognised tiers fail closed to requiring confirmation. BuildCapabilities sets Enabled, RequiresConfirmation and TimeoutSeconds explicitly, and hexisd reconciles drifted rows on startup instead of skipping any capability whose ID already exists — without that, allowlist edits never reach an existing database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
231 lines
4.9 KiB
Go
231 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kami/hexis/pkg/client"
|
|
)
|
|
|
|
var cli *client.Client
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
baseURL := os.Getenv("HEXIS_URL")
|
|
if baseURL == "" {
|
|
baseURL = "http://localhost:9741"
|
|
}
|
|
if os.Args[1] == "--url" && len(os.Args) > 2 {
|
|
baseURL = os.Args[2]
|
|
os.Args = append(os.Args[:1], os.Args[3:]...)
|
|
}
|
|
|
|
cli = client.New(baseURL).WithToken(os.Getenv("HEXIS_API_TOKEN"))
|
|
|
|
cmd := os.Args[1]
|
|
args := os.Args[2:]
|
|
|
|
switch cmd {
|
|
case "capability":
|
|
handleCapability(args)
|
|
case "exec":
|
|
handleExec(args)
|
|
case "execution":
|
|
handleExecution(args)
|
|
case "health":
|
|
handleHealth()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func handleCapability(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Fprintln(os.Stderr, "usage: hexis capability <create|list|show|delete> [...]")
|
|
os.Exit(1)
|
|
}
|
|
|
|
sub := args[0]
|
|
rest := args[1:]
|
|
|
|
switch sub {
|
|
case "create":
|
|
capCreate(rest)
|
|
case "list":
|
|
capList(rest)
|
|
case "show":
|
|
capShow(rest)
|
|
case "delete":
|
|
capDelete(rest)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown capability subcommand: %s\n", sub)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func capCreate(args []string) {
|
|
flags := parseFlags(args)
|
|
req := client.CreateCapabilityRequest{
|
|
Name: flags["name"],
|
|
Description: flags["description"],
|
|
TargetEntityID: flags["target"],
|
|
Provider: flags["provider"],
|
|
Operation: flags["operation"],
|
|
Risk: flags["risk"],
|
|
ExpectedSideEffects: flags["side-effects"],
|
|
}
|
|
if flags["read-only"] == "true" {
|
|
req.ReadOnly = true
|
|
}
|
|
if typesStr := flags["types"]; typesStr != "" {
|
|
req.TargetTypes = strings.Split(typesStr, ",")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
cap, err := cli.CreateCapability(ctx, req)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
printJSON(cap)
|
|
}
|
|
|
|
func capList(args []string) {
|
|
flags := parseFlags(args)
|
|
entityID := flags["entity"]
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
caps, err := cli.Capabilities(ctx, entityID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
printJSON(caps)
|
|
}
|
|
|
|
func capShow(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Fprintln(os.Stderr, "usage: hexis capability show <id>")
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
cap, err := cli.GetCapability(ctx, args[0])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
printJSON(cap)
|
|
}
|
|
|
|
func capDelete(args []string) {
|
|
fmt.Fprintln(os.Stderr, "capability delete not yet implemented")
|
|
os.Exit(1)
|
|
}
|
|
|
|
func handleExec(args []string) {
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: hexis exec <capability-id> <target-entity-id> [--args JSON] [--idempotency KEY]")
|
|
os.Exit(1)
|
|
}
|
|
|
|
flags := parseFlags(args[2:])
|
|
req := client.ExecuteRequest{
|
|
CapabilityID: args[0],
|
|
TargetEntityID: args[1],
|
|
IdempotencyKey: flags["idempotency"],
|
|
}
|
|
if argsStr := flags["args"]; argsStr != "" {
|
|
json.Unmarshal([]byte(argsStr), &req.Arguments)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
result, err := cli.Execute(ctx, req)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
printJSON(result)
|
|
}
|
|
|
|
func handleExecution(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Fprintln(os.Stderr, "usage: hexis execution <id>")
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
exec, err := cli.GetExecution(ctx, args[0])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
printJSON(exec)
|
|
}
|
|
|
|
func handleHealth() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := cli.Health(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "unhealthy: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("ok")
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Fprintf(os.Stderr, `Usage: hexis [--url URL] <command> [args]
|
|
|
|
Commands:
|
|
capability create --name NAME --provider PROVIDER --operation OP [--target ENTITY] [--types TYPES] [--risk RISK] [--read-only true] [--side-effects TEXT]
|
|
capability list [--entity ENTITY_ID]
|
|
capability show <id>
|
|
exec <capability-id> <target-entity-id> [--args JSON] [--idempotency KEY]
|
|
execution <id>
|
|
health
|
|
`)
|
|
}
|
|
|
|
func parseFlags(args []string) map[string]string {
|
|
flags := map[string]string{}
|
|
for i := 0; i < len(args); i++ {
|
|
if strings.HasPrefix(args[i], "--") {
|
|
key := strings.TrimPrefix(args[i], "--")
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
|
|
flags[key] = args[i+1]
|
|
i++
|
|
} else {
|
|
flags[key] = "true"
|
|
}
|
|
}
|
|
}
|
|
return flags
|
|
}
|
|
|
|
func printJSON(v any) {
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
enc.Encode(v)
|
|
}
|