0.29.0 — code-quality refactor (plan E1–E5): file splits, sqlc migration, SSH unification, test coverage
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

E1: split monolithic files — cmd/nomos (main.go → server.go + mcp.go + workers.go),
    internal/mcp/tools.go → entity_tools/ops_tools/knowledge_tools/analysis_tools,
    internal/httpapi/impl.go → domain files (entities, events, signals, ontology,
    fleet_health, client_context, client_lifecycle, entity_mutations, query_audit).
E2: migrate raw pool.Exec queries to sqlc (entities/relationships queries + generated).
E3: unify SSH — consolidate crypto/ssh dial into actuator/client.go (+client_test).
E4/E5: add tests — db/lifecycle, checkdefaults/build, ontology/preconditions, policy/risk.
This commit is contained in:
2026-08-08 22:38:47 +02:00
parent 712b66422b
commit 75c0848a6f
34 changed files with 4544 additions and 3745 deletions

View File

@@ -17,7 +17,6 @@ import (
"github.com/dtoro/oikos/internal/db"
"github.com/dtoro/oikos/internal/db/sqlcgen"
"github.com/google/uuid"
"golang.org/x/crypto/ssh"
)
// Run starts the actuator loop. Blocks until ctx is cancelled.
@@ -403,7 +402,7 @@ func ProvisionVM(ctx context.Context, pool *db.Pool, entityID uuid.UUID, attrs m
return nil
}
// sshExecSimple runs a command over SSH with a simple client setup.
// sshExecSimple runs a command over SSH using the shared dial/run primitives.
// Uses the default SSH key from SSH_KEY_PATH or ~/.ssh/id_rsa.
func sshExecSimple(ctx context.Context, host, user, command string) (string, error) {
keyPath := os.Getenv("SSH_KEY_PATH")
@@ -411,55 +410,19 @@ func sshExecSimple(ctx context.Context, host, user, command string) (string, err
keyPath = os.Getenv("HOME") + "/.ssh/id_rsa"
}
keyBytes, err := os.ReadFile(keyPath)
signer, err := LoadSigner(keyPath)
if err != nil {
return "", fmt.Errorf("read ssh key: %w", err)
return "", err
}
signer, err := ssh.ParsePrivateKey(keyBytes)
client, err := Dial(ctx, DialOptions{Host: host, User: user, Signer: signer})
if err != nil {
return "", fmt.Errorf("parse ssh key: %w", err)
}
clientCfg := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: HostKeyCallback(),
Timeout: 10 * time.Second,
}
client, err := ssh.Dial("tcp", host+":22", clientCfg)
if err != nil {
return "", fmt.Errorf("ssh dial %s: %w", host, err)
return "", err
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return "", fmt.Errorf("create session: %w", err)
}
defer session.Close()
type result struct {
output string
err error
}
ch := make(chan result, 1)
go func() {
out, e := session.CombinedOutput(command)
ch <- result{output: string(out), err: e}
}()
select {
case <-ctx.Done():
session.Close()
return "", ctx.Err()
case res := <-ch:
if res.err != nil {
return res.output, res.err
}
return res.output, nil
}
out, err := RunCombinedOutput(ctx, client, command)
return string(out), err
}
// resolveHost resolves a host entity slug to (address, user) for SSH.

101
internal/actuator/client.go Normal file
View File

@@ -0,0 +1,101 @@
package actuator
import (
"context"
"fmt"
"net"
"os"
"time"
"golang.org/x/crypto/ssh"
)
// defaultDialTimeout bounds an SSH dial when the caller leaves Timeout unset.
// 10s matches the previous hardcoded value at every dial site.
const defaultDialTimeout = 10 * time.Second
// LoadSigner reads and parses the private key at keyPath.
func LoadSigner(keyPath string) (ssh.Signer, error) {
key, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("read ssh key: %w", err)
}
return LoadSignerFromBytes(key)
}
// LoadSignerFromBytes parses an in-memory private key into an ssh.Signer.
func LoadSignerFromBytes(key []byte) (ssh.Signer, error) {
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, fmt.Errorf("parse ssh key: %w", err)
}
return signer, nil
}
// DialOptions configures an SSH dial.
type DialOptions struct {
Host string
Port int // 0 means 22
User string
Signer ssh.Signer
Timeout time.Duration // dial timeout; <=0 means defaultDialTimeout
}
// Dial opens a crypto/ssh connection through the centralized HostKeyCallback.
// The connection itself is bounded by Timeout; ctx is respected by callers
// via RunCombinedOutput once the session is running.
func Dial(ctx context.Context, opts DialOptions) (*ssh.Client, error) {
port := opts.Port
if port <= 0 {
port = 22
}
timeout := opts.Timeout
if timeout <= 0 {
timeout = defaultDialTimeout
}
cfg := &ssh.ClientConfig{
User: opts.User,
Auth: []ssh.AuthMethod{ssh.PublicKeys(opts.Signer)},
HostKeyCallback: HostKeyCallback(),
Timeout: timeout,
}
addr := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", port))
client, err := ssh.Dial("tcp", addr, cfg)
if err != nil {
return nil, fmt.Errorf("ssh dial %s:%d: %w", opts.Host, port, err)
}
return client, nil
}
// RunCombinedOutput runs cmd on an established client and returns its combined
// stdout/stderr. Context cancellation closes the session to abort the remote
// command instead of blocking until it finishes — the same goroutine+select
// pattern the actuator, mcp, and scheduler each reimplemented before.
func RunCombinedOutput(ctx context.Context, client *ssh.Client, cmd string) ([]byte, error) {
session, err := client.NewSession()
if err != nil {
return nil, fmt.Errorf("create session: %w", err)
}
defer session.Close()
type result struct {
out []byte
err error
}
ch := make(chan result, 1)
go func() {
out, err := session.CombinedOutput(cmd)
ch <- result{out: out, err: err}
}()
select {
case <-ctx.Done():
session.Close()
return nil, ctx.Err()
case res := <-ch:
if res.err != nil {
return res.out, fmt.Errorf("command: %w", res.err)
}
return res.out, nil
}
}

View File

@@ -0,0 +1,88 @@
package actuator
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"os"
"path/filepath"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
func TestLoadSignerRejectsBadInput(t *testing.T) {
if _, err := LoadSignerFromBytes([]byte("not a private key")); err == nil {
t.Error("LoadSignerFromBytes should reject a non-key input")
}
if _, err := LoadSigner("/nonexistent/key"); err == nil {
t.Error("LoadSigner should fail on a missing file")
}
}
func TestLoadSignerRoundTrip(t *testing.T) {
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
block, err := ssh.MarshalPrivateKey(priv, "")
if err != nil {
t.Fatalf("marshal private key: %v", err)
}
pemBytes := pem.EncodeToMemory(block)
signer, err := LoadSignerFromBytes(pemBytes)
if err != nil {
t.Fatalf("LoadSignerFromBytes on a valid key: %v", err)
}
if signer == nil {
t.Fatal("signer is nil")
}
dir := t.TempDir()
path := filepath.Join(dir, "id_ed25519")
if err := os.WriteFile(path, pemBytes, 0o600); err != nil {
t.Fatalf("write key file: %v", err)
}
fromFile, err := LoadSigner(path)
if err != nil {
t.Fatalf("LoadSigner(%s): %v", path, err)
}
if !bytes.Equal(fromFile.PublicKey().Marshal(), signer.PublicKey().Marshal()) {
t.Error("file and in-memory signers resolved to different public keys")
}
}
// Dial needs a real SSH server to run a command, but its option normalization
// is verifiable without one: a zero Port must default to 22 (so the dial error
// references host:22, not host:0), and a closed port yields a dial error rather
// than panicking.
func TestDialDefaultsPort(t *testing.T) {
_, err := Dial(context.Background(), DialOptions{Host: "127.0.0.1", Signer: mustSigner(t)})
if err == nil {
t.Fatal("Dial to a closed port should fail")
}
if !strings.Contains(err.Error(), "127.0.0.1:22") {
t.Errorf("Dial error = %q, want it to reference 127.0.0.1:22", err)
}
}
func mustSigner(t *testing.T) ssh.Signer {
t.Helper()
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
block, err := ssh.MarshalPrivateKey(priv, "")
if err != nil {
t.Fatalf("marshal key: %v", err)
}
s, err := LoadSignerFromBytes(pem.EncodeToMemory(block))
if err != nil {
t.Fatalf("parse key: %v", err)
}
return s
}

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"log/slog"
"net"
"os"
"strings"
"sync"
"time"
@@ -131,37 +130,19 @@ func ExecuteProcedure(
start := time.Now()
// Parse the SSH key
key, err := os.ReadFile(cfg.KeyPath)
signer, err := LoadSigner(cfg.KeyPath)
if err != nil {
return SSHResult{
Err: fmt.Errorf("read ssh key: %w", err),
Err: err,
Duration: time.Since(start),
Verified: false,
}
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return SSHResult{
Err: fmt.Errorf("parse ssh key: %w", err),
Duration: time.Since(start),
Verified: false,
}
}
addr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port))
if cfg.Port == 0 {
addr = net.JoinHostPort(cfg.Host, "22")
}
clientCfg := &ssh.ClientConfig{
User: cfg.User,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: HostKeyCallback(),
Timeout: cfg.Timeout,
}
client, err := ssh.Dial("tcp", addr, clientCfg)
client, err := Dial(ctx, DialOptions{
Host: cfg.Host, Port: cfg.Port, User: cfg.User,
Signer: signer, Timeout: cfg.Timeout,
})
if err != nil {
class := classifySSHError(err)
return SSHResult{
@@ -229,38 +210,11 @@ func ExecuteProcedure(
}
}
// runSSHCommand executes a single command over an established SSH session.
// Uses context-aware goroutines: ctx.Done() closes the session.
// runSSHCommand executes a single command over an established SSH session via
// the shared RunCombinedOutput primitive (context-aware abort + combined output).
func runSSHCommand(ctx context.Context, client *ssh.Client, command string) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", fmt.Errorf("create session: %w", err)
}
defer session.Close()
// Wrap in goroutine so we can abort on ctx.Done()
type result struct {
output string
err error
}
ch := make(chan result, 1)
go func() {
out, err := session.CombinedOutput(command)
ch <- result{output: string(out), err: err}
}()
select {
case <-ctx.Done():
// Close the session to abort the SSH command
session.Close()
return "", ctx.Err()
case res := <-ch:
if res.err != nil {
return res.output, fmt.Errorf("command: %w", res.err)
}
return res.output, nil
}
out, err := RunCombinedOutput(ctx, client, command)
return string(out), err
}
// ─── Procedure parsing ────────────────────────────────────────────────────