fix: recover failed task launches safely (#3437)

This commit is contained in:
Carlos Florêncio
2026-09-07 11:19:09 +01:00
committed by GitHub
parent 3c6af41063
commit 3fba828e2f
69 changed files with 2280 additions and 277 deletions
@@ -2,6 +2,7 @@ package routingerr
import (
"regexp"
"strings"
"unicode/utf8"
)
@@ -40,20 +41,67 @@ var redactions = []redaction{
{regexp.MustCompile(`/home/[^/\s]+/`), "/home/<redacted>/"},
}
// Redact applies the same credential and home-path rules as Sanitize but
// performs no truncation, so a caller that needs to bound the result by a
// different rule (e.g. keeping the newest bytes of a longer budget cut) sees
// every credential intact and redacted rather than racing a head-truncation
// that runs before it can see the whole input. Redact is idempotent:
// applying it twice equals applying it once.
var (
localUnixPathPattern = regexp.MustCompile(`/(?:[^/\s"']+/)+[^\r\n\s"'<>]+`)
localWindowsPathPattern = regexp.MustCompile(`(?i)[A-Z]:[\\/][^\r\n"']+`)
)
func redactLocalPaths(s string) string {
s = localWindowsPathPattern.ReplaceAllStringFunc(s, func(path string) string {
if len(path) >= 4 && path[2:4] == "//" {
return path
}
return "[path-redacted]"
})
matches := localUnixPathPattern.FindAllStringIndex(s, -1)
if len(matches) == 0 {
return s
}
var redacted strings.Builder
redacted.Grow(len(s))
last := 0
for _, match := range matches {
start, end := match[0], match[1]
redacted.WriteString(s[last:start])
path := s[start:end]
if start >= 2 && s[start-2:start] == ":/" {
redacted.WriteString(path)
last = end
continue
}
redacted.WriteString(redactUnixPath(path))
last = end
}
redacted.WriteString(s[last:])
return redacted.String()
}
func redactUnixPath(path string) string {
if strings.Contains(path, "[path-redacted]") {
return path
}
switch {
case strings.HasPrefix(path, "/Users/"):
return "/Users/<redacted>/[path-redacted]"
case strings.HasPrefix(path, "/home/"):
return "/home/<redacted>/[path-redacted]"
default:
return "[path-redacted]"
}
}
// Redact applies the credential and local-path rules without truncation.
// The function is idempotent: applying it twice equals applying it once.
func Redact(s string) string {
for _, r := range redactions {
s = r.pattern.ReplaceAllString(s, r.replace)
}
s = redactLocalPaths(s)
return s
}
// Sanitize redacts likely credentials, normalizes home paths, and truncates
// Sanitize redacts likely credentials and local paths, then truncates
// to MaxRawExcerptBytes on a rune boundary so a multi-byte character (e.g.
// Vietnamese, CJK) is never split into invalid UTF-8. The function is
// idempotent: applying it twice equals applying it once.
@@ -69,6 +69,30 @@ func TestSanitize_RedactionsGolden(t *testing.T) {
mustNotHave: []string{"/home/bob/"},
mustHave: []string{"/home/<redacted>/"},
},
{
name: "temporary workspace path",
in: "file at /tmp/kandev/task/repo/main.go failed",
mustNotHave: []string{"/tmp/kandev/task/repo/main.go"},
mustHave: []string{"[path-redacted]"},
},
{
name: "workspace root path",
in: "checkout failed in /workspace/kandev/repo/main.go",
mustNotHave: []string{"/workspace/kandev/repo/main.go"},
mustHave: []string{"[path-redacted]"},
},
{
name: "path redaction does not consume a following endpoint",
in: "cannot read /workspace/.env while connecting to https://endpoint.example.test",
mustNotHave: []string{"/workspace/.env"},
mustHave: []string{"https://endpoint.example.test"},
},
{
name: "windows workspace path",
in: `checkout failed in C:\Users\alice\workspace\repo\main.go`,
mustNotHave: []string{`C:\Users\alice\workspace\repo\main.go`},
mustHave: []string{"[path-redacted]"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -96,6 +120,7 @@ func TestSanitize_Idempotent(t *testing.T) {
"--api-key=ABCDEFGHIJKLMNOPQRSTUV --rest",
"password: hunter2 token: foobar secret=abc",
"/Users/me/projects/x /home/me/x",
"/tmp/kandev/repo/main.go C:\\workspace\\repo\\main.go",
}
for _, in := range inputs {
first := Sanitize(in)