fix: harden workspace preprocessing contract

This commit is contained in:
2026-08-11 00:59:15 +02:00
parent 9df7c38069
commit 05a6e8cc2d
11 changed files with 494 additions and 32 deletions
+12 -1
View File
@@ -95,7 +95,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
}
result, operationErr := workspaceops.Run(ctx, installation, runner, workspaceCommand, nil)
if operationErr != nil {
if errors.Is(operationErr, compose.ErrOutputLimit) || strings.Contains(operationErr.Error(), "unsafe") || strings.Contains(operationErr.Error(), "exceeds") || strings.Contains(operationErr.Error(), "invalid workspace") {
if workspaceUsageError(operationErr) {
return commandUsageError(stderr, operationErr.Error())
}
fmt.Fprintf(stderr, "thothctl: %s\n", output.Sanitize(operationErr.Error(), secretValues))
@@ -210,6 +210,17 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
return writeResult(result, err, secretValues, stdout, stderr)
}
func workspaceUsageError(err error) bool {
if err == nil || errors.Is(err, compose.ErrOutputLimit) {
return false
}
message := err.Error()
// These are host-side grammar/local-file failures. Child envelope/result and
// bounded-stream failures are operational and deliberately remain exit 1.
return strings.Contains(message, "unsafe") || strings.Contains(message, "request exceeds") ||
strings.Contains(message, "SQL input exceeds") || strings.Contains(message, "annotation input exceeds")
}
func renderWorkspaceHuman(w io.Writer, result workspaceops.Result) {
if result.Code == workspaceops.CodeRegistryBootstrapRecoveryConflict {
fmt.Fprintln(w, "Bootstrap recovery is ambiguous or corrupt; inspect the installation registry jobs.")
+25
View File
@@ -3,6 +3,7 @@ package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
@@ -104,6 +105,17 @@ func TestUsageDocumentsClosedConfigureUpdateSourcesAndMaintenanceRecovery(t *tes
}
}
func TestRunWorkspaceRejectsInvalidCommandBeforeDocker(t *testing.T) {
fixture := newCLIFixture(t, "")
fixture.setEnvironment(t)
var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "PSd"}, &stdout, &stderr)
if code != 2 || !strings.Contains(stderr.String(), "workspace") {
t.Fatalf("exit=%d stderr=%q", code, stderr.String())
}
assertDockerNotInvoked(t, fixture)
}
func TestRunSessionsMigrateRequiresExplicitConfirmationBeforeDocker(t *testing.T) {
fixture := newCLIFixture(t, "")
fixture.setProfile(t, "server")
@@ -855,3 +867,16 @@ func TestRenderWorkspaceHumanHidesRecoveryIdentity(t *testing.T) {
t.Fatalf("human output = %q", out.String())
}
}
func TestWorkspaceOperationalEnvelopeFailuresAreExitOne(t *testing.T) {
for _, err := range []error{compose.ErrOutputLimit, errors.New("invalid workspace result"), errors.New("invalid workspace request"), errors.New("invalid host export")} {
if workspaceUsageError(err) {
t.Errorf("classified operational error %q as usage", err)
}
}
for _, err := range []error{errors.New("unsafe output file"), errors.New("request exceeds limit"), errors.New("unsafe SQL input")} {
if !workspaceUsageError(err) {
t.Errorf("classified host error %q as operational", err)
}
}
}