phase 3-4: get_execution_status tool, approval creation, delete bin/ + homelab.go, update AGENTS.md

This commit is contained in:
2026-07-07 23:13:13 +02:00
parent 7ac2521a22
commit 7f7d039e1b
5 changed files with 54 additions and 2098 deletions

View File

@@ -1,137 +0,0 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
type cliHost struct {
Name string
LanIP string `yaml:"lan_ip"`
Role string
Kind string
State string
Mesh map[string]any
Runs []string
Services []map[string]any `yaml:"services_hosted"`
}
func runHomelabSubcommand() {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: oikos homelab <list|whoami|ssh|secret>")
os.Exit(1)
}
ctxDir := os.Getenv("HOMELAB_CONTEXT_DIR")
if ctxDir == "" {
ctxDir = "."
}
switch os.Args[2] {
case "list":
inv := loadInventory(ctxDir)
fmt.Printf("%-25s %-8s %-18s %-6s %s\n", "HOST", "TYPE", "IP", "STATE", "ROLE")
fmt.Println(strings.Repeat("-", 80))
for name, host := range inv.Hosts {
state := host.State
if state == "" {
state = "active"
}
fmt.Printf("%-25s %-8s %-18s %-6s %s\n", name, host.Kind, host.LanIP, state, host.Role)
}
case "whoami":
hostname, _ := os.Hostname()
// Try short hostname and full hostname
hostnameShort := strings.Split(hostname, ".")[0]
inv := loadInventory(ctxDir)
h, ok := inv.Hosts[hostnameShort]
if !ok {
// Try with full hostname
h, ok = inv.Hosts[hostname]
}
if !ok {
fmt.Printf("host: %s (not enrolled — no entry in inventory.yaml)\n", hostname)
return
}
state := h.State
if state == "" {
state = "active"
}
fmt.Printf("host: %s kind: %s role: %s lan_ip: %s state: %s\n",
hostnameShort, h.Kind, h.Role, h.LanIP, state)
for _, svc := range h.Services {
fmt.Printf(" service: %v\n", svc["name"])
}
case "ssh":
if len(os.Args) < 4 {
fmt.Fprintln(os.Stderr, "usage: oikos homelab ssh <host>")
os.Exit(1)
}
target := os.Args[3]
inv := loadInventory(ctxDir)
h, ok := inv.Hosts[target]
if !ok {
fmt.Fprintf(os.Stderr, "unknown host: %s\n", target)
os.Exit(1)
}
addr := h.LanIP
if addr == "" {
for _, m := range []string{"netbird", "tailscale"} {
if mh, ok := h.Mesh[m].(map[string]any); ok {
if ip, ok := mh["ip"].(string); ok && ip != "" {
addr = ip
break
}
}
}
}
if addr == "" {
fmt.Fprintf(os.Stderr, "no address for %s\n", target)
os.Exit(1)
}
user := "root"
cmd := exec.Command("ssh", user+"@"+addr)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
case "secret":
if len(os.Args) < 4 {
fmt.Fprintln(os.Stderr, "usage: oikos homelab secret <name>")
os.Exit(1)
}
secretName := os.Args[3]
secretFile := filepath.Join(ctxDir, "secrets", secretName+".yaml")
// Shell out to sops for decryption
cmd := exec.Command("sops", "-d", secretFile)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "decrypt %s: %v\n", secretName, err)
os.Exit(1)
}
default:
fmt.Fprintf(os.Stderr, "unknown homelab command: %s\n", os.Args[2])
os.Exit(1)
}
}
func loadInventory(ctxDir string) inventoryFile {
data, _ := os.ReadFile(filepath.Join(ctxDir, "inventory.yaml"))
var inv inventoryFile
yaml.Unmarshal(data, &inv)
return inv
}
type inventoryFile struct {
Hosts map[string]cliHost `yaml:"hosts"`
}

View File

@@ -60,8 +60,6 @@ func main() {
slog.Error("export failed", "error", err)
os.Exit(1)
}
case "homelab":
runHomelabSubcommand()
case "api":
if err := runAPI(ctx, cfg); err != nil {
slog.Error("api failed", "error", err)
@@ -114,15 +112,15 @@ Roles:
migrate Run database migrations (forward-only, idempotent)
seed Ingest seed YAML files into the database
export Export DB state back to seed YAMLs (DR / version control)
homelab Operator CLI: list, whoami, ssh, secret
api Run the REST + MCP API server (Phase 2)
scheduler Run the observe + act loop (Phase 3)
notifier Run the notification service (Phase 3)
api Run the REST + MCP API server
scheduler Run the observe loop
notifier Run the notification service (Matrix alerts)
all Run all roles in one process (dev mode)
secret Secret management (Phase 5)
secret Secret management (Infisical)
knowledge Convert wiki to knowledge seed (one-shot)
version Print version info
The operator interface is Hermes (MCP agent) — no CLI needed.
Environment:
OIKOS_DATABASE_URL Postgres connection string
OIKOS_API_LISTEN API listen address (default :8090)