Closes the two remaining open points from the auto-continuation work.
1. Atomic pct_create (observability, the bigger of the two):
pct_create used to bundle create + apt install + post_install script into
one black-box multi-minute SSH call — the agent got back a single opaque
success/fail with no way to see (or fix) which step actually broke.
Removed the whole post-create provisioning block (and the now-dead
provisionScript/sanitizePkgs helpers + their tests). pct_create is now
create + start + register ONLY — fast, and its result is fed back to the
agent via auto-continuation almost immediately. The agent installs
packages and runs setup as its OWN sequence of `run` calls against the new
lxc:<hostname>, observing each command's real output and able to diagnose
and retry exactly the step that failed — the same recovery loop already
proven for the general case, now applied to installs too, instead of
requiring a separate black-box mechanism.
- services/post_install removed from the pct_create params struct and
from the MCP tool schema/SOUL.md docs.
- SOUL.md: explains the new flow, moves the Docker CLI gotcha and DNS
troubleshooting guidance to be steps the agent runs itself.
2. Scoped destructive window (targeted autonomy for recovery):
Verified live in the previous session that a destructive recovery (a
failed destroy needing stop-then-destroy on the same container) required
TWO separate typed confirmations for what was clearly one recovery
action. Added a narrow, TARGET-scoped 15-minute grant
(destructive_window.agent:<id>.target:<slug> in autonomy_settings,
shared key format across cmd/nomos and internal/mcp) that opens only
after an EXPLICIT typed confirmation (never loose assent) or an explicit
button-approval of a destructive step, and only ever covers further
destructive commands against that SAME target. A different target always
needs its own fresh confirmation — this narrows risk instead of loosening
it globally, unlike broadening the general assent window to cover
destructive actions would have.
- cmd/nomos/store.go: openDestructiveWindow/destructiveWindowActive/
executionTarget.
- cmd/nomos/agent.go: opens the window when a typed confirmation grants a
destructive chat-assent execution.
- internal/mcp/server.go: `run` tool checks the window before gating a
destructive command; auto-runs if active.
- internal/httpapi/phase3.go: DecideApproval opens the same window when a
destructive execution is approved via the button/API, for parity with
the chat-assent path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
4.2 KiB
Go
120 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
// TestJSONErrValidForNastyOutput guards the bug where command output with
|
|
// quotes/backslashes/newlines produced invalid JSON, failing the ::jsonb cast
|
|
// and silently dropping the execution's final status update.
|
|
func TestJSONErrValidForNastyOutput(t *testing.T) {
|
|
nasty := "CT 132 already exists on node \"hubris\"\n\tpath C:\\x\r\n\x00 100%"
|
|
for _, payload := range [][]byte{
|
|
jsonErr("%s", nasty),
|
|
jsonErr("list templates on %s: %s", "host:strong", nasty),
|
|
} {
|
|
var m map[string]any
|
|
if err := json.Unmarshal(payload, &m); err != nil {
|
|
t.Fatalf("jsonErr produced invalid JSON: %v\npayload=%s", err, payload)
|
|
}
|
|
if _, ok := m["error"]; !ok {
|
|
t.Errorf("missing error key: %s", payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGatewayPreflightPassed guards the exact bug found live: "UNREACHABLE"
|
|
// contains "REACHABLE" as a substring, so a strings.Contains(out,"REACHABLE")
|
|
// check is true for BOTH outcomes and can never fail. Exact-match only.
|
|
func TestGatewayPreflightPassed(t *testing.T) {
|
|
cases := []struct {
|
|
out string
|
|
want bool
|
|
}{
|
|
{"PREFLIGHT_OK", true},
|
|
{"PREFLIGHT_OK\n", true},
|
|
{" PREFLIGHT_OK ", true},
|
|
{"PREFLIGHT_FAIL", false},
|
|
{"PREFLIGHT_FAIL\n", false},
|
|
{"", false},
|
|
{"some garbage output", false},
|
|
// the specific historical bug: a naive substring check on the old
|
|
// REACHABLE/UNREACHABLE markers would have called this true.
|
|
{"UNREACHABLE", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := gatewayPreflightPassed(c.out); got != c.want {
|
|
t.Errorf("gatewayPreflightPassed(%q) = %v, want %v", c.out, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// provisionScript and sanitizePkgs were removed when pct_create was made
|
|
// atomic (create + start + register only) — installing packages and running
|
|
// setup scripts is now the agent's own job via follow-up `run` calls, which
|
|
// already has its own classifier/sanitization tests in internal/policy.
|