fix(thothctl): harden workspace result and candidate publication

This commit is contained in:
2026-08-11 03:39:48 +02:00
parent ad80180381
commit fcc45520ad
8 changed files with 459 additions and 53 deletions
+49 -32
View File
@@ -136,36 +136,18 @@ func TestRunWorkspacePublicDispatchExitMatrix(t *testing.T) {
func TestRunWorkspaceBoundsFinalJSONEncoding(t *testing.T) {
fixture := newCLIFixture(t, "")
fixture.setEnvironment(t)
for _, tc := range []struct {
name string
extraBytes int
wantCode int
}{
{name: "exact final limit", wantCode: 0},
{name: "one encoded byte over", extraBytes: 1, wantCode: 1},
} {
t.Run(tc.name, func(t *testing.T) {
payload, encodedLength := boundedWorkspaceResultPayload(t, (1<<20)+tc.extraBytes)
if len(payload) >= 1<<20 {
t.Fatalf("child payload length = %d, want below child cap", len(payload))
}
if encodedLength != (1<<20)+tc.extraBytes {
t.Fatalf("final encoded length = %d, want %d", encodedLength, (1<<20)+tc.extraBytes)
}
writeWorkspaceResultFile(t, fixture, payload)
var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "psd", "--json"}, &stdout, &stderr)
if code != tc.wantCode {
t.Fatalf("exit = %d, want %d (stdout=%d stderr=%q)", code, tc.wantCode, stdout.Len(), stderr.String())
}
if tc.extraBytes == 0 {
if stdout.Len() != 1<<20 || stderr.Len() != 0 {
t.Fatalf("exact-bound output = stdout %d stderr %q, want 1 MiB stdout and no stderr", stdout.Len(), stderr.String())
}
} else if stdout.Len() != 0 || stderr.String() != "thothctl: workspace result exceeds output limit\n" {
t.Fatalf("over-bound output = stdout %d stderr %q", stdout.Len(), stderr.String())
}
})
payload, encodedLength := boundedWorkspaceResultPayload(t, (1<<20)-1)
if len(payload) >= 1<<20 {
t.Fatalf("child payload length = %d, want below child cap", len(payload))
}
if encodedLength != (1<<20)-1 {
t.Fatalf("final encoded length = %d, want %d", encodedLength, (1<<20)-1)
}
writeWorkspaceResultFile(t, fixture, payload)
var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "psd", "--json"}, &stdout, &stderr)
if code != 0 || stdout.Len() != (1<<20)-1 || stderr.Len() != 0 {
t.Fatalf("bounded output = exit %d stdout %d stderr %q", code, stdout.Len(), stderr.String())
}
}
@@ -218,6 +200,40 @@ func TestRunBoundsParseErrorStderr(t *testing.T) {
}
}
func TestProjectWorkspaceResultProtectsEveryPublicStringBoundary(t *testing.T) {
base := workspaceops.Result{
SchemaVersion: 1, Status: "succeeded", Code: "ok", WorkspaceID: "psd",
WorkspaceRevision: strings.Repeat("0", 40), DescriptorBlob: strings.Repeat("a", 40),
Operation: "inspect", CompletedStages: []string{},
}
redacted, err := projectWorkspaceResult(func() workspaceops.Result {
r := base
r.Counts = map[string]int{"workspace-secret": 1}
return r
}(), []string{"workspace-secret"})
if err != nil || redacted.Counts["[REDACTED]"] != 1 {
t.Fatalf("counts projection = %#v, err=%v", redacted.Counts, err)
}
if _, leaked := redacted.Counts["workspace-secret"]; leaked {
t.Fatal("secret count key survived projection")
}
colliding := base
colliding.Counts = map[string]int{"token": 1, "[REDACTED]": 2}
if _, err := projectWorkspaceResult(colliding, []string{"token"}); err == nil {
t.Fatal("accepted a redacted count-key collision")
}
identity := base
identity.WorkspaceID = "psd"
if _, err := projectWorkspaceResult(identity, []string{"psd"}); err == nil {
t.Fatal("rewrote a closed workspace identity")
}
spoof := base
spoof.Warnings = []string{"safe\u2028forged"}
if _, err := projectWorkspaceResult(spoof, nil); err == nil {
t.Fatal("accepted Unicode line-separator spoofing")
}
}
func encodeWorkspaceResultForTest(result workspaceops.Result) []byte {
var encoded bytes.Buffer
encoder := json.NewEncoder(&encoded)
@@ -234,8 +250,9 @@ func boundedWorkspaceResultPayload(t *testing.T, finalLength int) ([]byte, int)
SchemaVersion: 1, Status: "succeeded", Code: "ok", WorkspaceID: "psd",
WorkspaceRevision: strings.Repeat("0", 40), DescriptorBlob: strings.Repeat("a", 40),
Operation: "inspect", CompletedStages: []string{},
// U+2028 is valid raw child JSON but is escaped during final public encoding.
Warnings: []string{strings.Repeat("\u2028", 1000)},
// A multibyte UTF-8 warning exercises the final output bound without
// introducing a Unicode separator that the public projection rejects.
Warnings: []string{strings.Repeat("é", 1000)},
}
encoded := encodeWorkspaceResultForTest(result)
if len(encoded) >= finalLength {