feat(builder): bounded model requests, a safety check, and a clearer PR summary
- The drafter no longer grows one conversation. Each request is the rules and facts, the model's latest template, what is wrong now and one line per earlier round, so long builds can't push the facts out of the model's 32K context (Ollama drops the oldest part silently). The image's scripts in the facts are capped at 24000 characters in all, and diagnosis check results at 16000. - Every model call logs its prompt and answer tokens, with a warning near the context limit. - unsafeCommands: a draft whose startup command or installConfig pipes a download into a shell, decodes base64, uses eval, raw network tools or /dev/tcp, sudo/su, setuid bits, writes /etc, touches cron/systemd/firewall, or looks like a miner goes back to the model. AUTHORING.md has the rule. - The PR summary lists every address nodes would download from under "Review carefully", marks the measured checks as measured, and labels the research notes as the model's reading, not checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a80a6ebc5
commit
00cc61aba2
7 files changed
+249
-24
No files matched your search
@@ -323,3 +323,79 @@ func launchOptions(cmd string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Things a game server's startup or install never needs, and that a prompt-injected or careless model
|
||||
// could write. Templates run on players' own machines, so a draft with any of them goes back to the model.
|
||||
var unsafePatterns = []struct {
|
||||
re *regexp.Regexp
|
||||
what string
|
||||
}{
|
||||
{regexp.MustCompile(`(?i)(curl|wget)[^|;&\n]*\|\s*(sudo\s+)?(ba|z|da)?sh\b`), "pipes a download straight into a shell"},
|
||||
{regexp.MustCompile(`(?i)base64\s+(-d|--decode)`), "decodes hidden (base64) content"},
|
||||
{regexp.MustCompile(`(^|[^\w-])eval\s`), "runs text as a command (eval)"},
|
||||
{regexp.MustCompile(`/dev/(tcp|udp)/`), "opens raw network connections"},
|
||||
{regexp.MustCompile(`(^|[^\w-])(nc|ncat|netcat|socat)\s`), "uses a raw network tool"},
|
||||
{regexp.MustCompile(`(^|[^\w-])(sudo|su)\s`), "tries to become another user"},
|
||||
{regexp.MustCompile(`chmod\s+([0-7]*[4-7][0-7]{3}\b|[ugoa]*\+[rwx]*s)`), "sets setuid/setgid bits"},
|
||||
{regexp.MustCompile(`(>|\btee\s+(-a\s+)?)\s*/etc/`), "writes to /etc"},
|
||||
{regexp.MustCompile(`(?i)(^|[^\w-])(crontab|systemctl|iptables|mount|insmod|modprobe)\s`), "changes the machine itself"},
|
||||
{regexp.MustCompile(`(?i)xmrig|minerd|stratum\+tcp|cryptonight`), "looks like a cryptocurrency miner"},
|
||||
}
|
||||
|
||||
// unsafeCommands checks the commands a node runs for a template: the startup command and anything in
|
||||
// installConfig (scripts, file downloads).
|
||||
func unsafeCommands(game map[string]any) []string {
|
||||
var problems []string
|
||||
for _, text := range commandTexts(game) {
|
||||
for _, p := range unsafePatterns {
|
||||
if m := p.re.FindString(text); m != "" {
|
||||
problems = append(problems, fmt.Sprintf("the template %s (%q). Templates run on players' own machines: a game "+
|
||||
"server never needs that. Remove it; if the server needs a file, install it with installMethod or "+
|
||||
"installConfig.files from the game's official source", p.what, strings.TrimSpace(m)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
var urlRe = regexp.MustCompile(`https?://[^\s"'<>)]+`)
|
||||
|
||||
// downloadURLs lists the addresses a template makes nodes download from, for the reviewer to check.
|
||||
func downloadURLs(game map[string]any) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, text := range commandTexts(game) {
|
||||
for _, u := range urlRe.FindAllString(text, -1) {
|
||||
if !seen[u] {
|
||||
seen[u] = true
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// commandTexts is everything in a template that ends up being run or fetched on a node.
|
||||
func commandTexts(game map[string]any) []string {
|
||||
var out []string
|
||||
if s, _ := game["defaultStartupCommand"].(string); s != "" {
|
||||
out = append(out, unwrapShell(s))
|
||||
}
|
||||
var walk func(v any)
|
||||
walk = func(v any) {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
out = append(out, t)
|
||||
case map[string]any:
|
||||
for _, k := range sortedKeys(t) {
|
||||
walk(t[k])
|
||||
}
|
||||
case []any:
|
||||
for _, e := range t {
|
||||
walk(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(game["installConfig"])
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user