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>
57 lines
1.6 KiB
Go
57 lines
1.6 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// sprintResult extracts the concatenated text content from a tool result.
|
|
func sprintResult(r *mcp.CallToolResult) string {
|
|
var b strings.Builder
|
|
for _, c := range r.Content {
|
|
if tc, ok := c.(*mcp.TextContent); ok {
|
|
b.WriteString(tc.Text)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func TestSanitizeBodyStripsHTML(t *testing.T) {
|
|
raw := `<html><head><style>.x{color:red}</style><script>alert(1)</script></head>` +
|
|
`<body><h1>Hello & Welcome</h1><p>Deploy with docker compose up -d</p></body></html>`
|
|
out := sanitizeBody("text/html; charset=utf-8", raw)
|
|
if strings.Contains(out, "<script") || strings.Contains(out, "alert(1)") {
|
|
t.Errorf("script not stripped: %q", out)
|
|
}
|
|
if strings.Contains(out, ".x{color:red}") {
|
|
t.Errorf("style not stripped: %q", out)
|
|
}
|
|
if !strings.Contains(out, "Hello & Welcome") {
|
|
t.Errorf("expected unescaped heading text, got: %q", out)
|
|
}
|
|
if !strings.Contains(out, "docker compose up -d") {
|
|
t.Errorf("expected body text preserved, got: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestHTTPGetBlocksPrivateAndBadScheme(t *testing.T) {
|
|
cases := []string{
|
|
"http://127.0.0.1:8080/",
|
|
"http://localhost/admin",
|
|
"http://192.168.8.77/",
|
|
"http://10.0.0.5/",
|
|
"file:///etc/passwd",
|
|
"ftp://example.com/x",
|
|
"",
|
|
}
|
|
for _, c := range cases {
|
|
out := sprintResult(httpGet(context.Background(), c))
|
|
if !strings.Contains(strings.ToLower(out), "error") && !strings.Contains(strings.ToLower(out), "refus") {
|
|
t.Errorf("%q: expected rejection, got %q", c, out)
|
|
}
|
|
}
|
|
}
|