fix: make Nomos actually provision LXCs from chat (pct_create + web fetch)
Root cause of "asks permission but never acts": the approved pct_create execution failed to parse because the LLM emitted `"privileged":0` / `"nesting":1` (numbers) into strict `bool` fields, so the container was never created. Compounded by a hardcoded template name (debian-13.0-1) that no longer exists on the host, and no way for the agent to read the web. - flexBool: accept 0/1, "true", bool for privileged/nesting (the exact prod failure) - pct_create template pre-flight: list host cache, validate/auto-pick newest debian - pct_create services[] + post_install: one approval provisions a working service - new http_get MCP tool (sanitized, size-capped, SSRF-guarded) — agent can read repos/sites - request_execution description: target=host, full JSON schema + example - SOUL.md: agent CAN fetch the web; prefer one-step provisioning - default model deepseek-v4-flash -> v4-pro; maxIterations 15 -> 25 - unit tests for flexBool, template resolve, pkg sanitize, HTML sanitize + SSRF block Verified live on host:strong with a throwaway VMID 999: template auto-resolved, container created + booted, services installed, post_install ran, then destroyed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
83
internal/httpapi/pct_create_test.go
Normal file
83
internal/httpapi/pct_create_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFlexBoolUnmarshal covers the exact production failure: the LLM emitted
|
||||
// `"privileged":0` / `"nesting":1` (numbers) and the strict bool field made the
|
||||
// already-approved pct_create execution fail to parse, so the LXC was never
|
||||
// created.
|
||||
func TestFlexBoolUnmarshal(t *testing.T) {
|
||||
type cfg struct {
|
||||
Privileged flexBool `json:"privileged"`
|
||||
Nesting flexBool `json:"nesting"`
|
||||
}
|
||||
cases := []struct {
|
||||
in string
|
||||
privileged bool
|
||||
nesting bool
|
||||
wantErr bool
|
||||
}{
|
||||
{`{"privileged":0,"nesting":1}`, false, true, false}, // the prod payload
|
||||
{`{"privileged":false,"nesting":true}`, false, true, false}, // canonical
|
||||
{`{"privileged":"1","nesting":"0"}`, true, false, false}, // stringified
|
||||
{`{"privileged":"true","nesting":"no"}`, true, false, false},
|
||||
{`{}`, false, false, false}, // absent → zero
|
||||
{`{"privileged":"maybe"}`, false, false, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
var out cfg
|
||||
err := json.Unmarshal([]byte(c.in), &out)
|
||||
if (err != nil) != c.wantErr {
|
||||
t.Fatalf("%s: err=%v wantErr=%v", c.in, err, c.wantErr)
|
||||
}
|
||||
if c.wantErr {
|
||||
continue
|
||||
}
|
||||
if bool(out.Privileged) != c.privileged || bool(out.Nesting) != c.nesting {
|
||||
t.Errorf("%s: got priv=%v nest=%v want priv=%v nest=%v",
|
||||
c.in, bool(out.Privileged), bool(out.Nesting), c.privileged, c.nesting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTemplate(t *testing.T) {
|
||||
avail := []string{
|
||||
"debian-12-standard_12.7-1_amd64.tar.zst",
|
||||
"debian-13-standard_13.0-1_amd64.tar.zst",
|
||||
"ubuntu-24.04-standard_24.04-2_amd64.tar.zst",
|
||||
}
|
||||
cases := []struct {
|
||||
requested string
|
||||
want string
|
||||
}{
|
||||
{"debian-13-standard_13.0-1_amd64.tar.zst", "debian-13-standard_13.0-1_amd64.tar.zst"}, // exact
|
||||
{"debian-13", "debian-13-standard_13.0-1_amd64.tar.zst"}, // prefix
|
||||
{"", "debian-13-standard_13.0-1_amd64.tar.zst"}, // auto newest debian
|
||||
{"debian-99", "debian-13-standard_13.0-1_amd64.tar.zst"}, // miss prefix → auto debian
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := resolveTemplate(c.requested, avail); got != c.want {
|
||||
t.Errorf("resolveTemplate(%q): got %q want %q", c.requested, got, c.want)
|
||||
}
|
||||
}
|
||||
if got := resolveTemplate("debian-13", nil); got != "" {
|
||||
t.Errorf("empty cache should yield empty, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizePkgs(t *testing.T) {
|
||||
in := []string{"docker.io", "git", "rm -rf /", "curl;wget", "python3-pip", ""}
|
||||
got := sanitizePkgs(in)
|
||||
want := map[string]bool{"docker.io": true, "git": true, "python3-pip": true}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %v want keys %v", got, want)
|
||||
}
|
||||
for _, g := range got {
|
||||
if !want[g] {
|
||||
t.Errorf("unexpected package survived sanitize: %q", g)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user