From 58e9abd8eef2c4467c4ce5d1dfe7fa3e42a9a6cc Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Mon, 25 May 2026 02:28:04 -0400 Subject: [PATCH] feat: share runtime result preview validation --- protocol/types.go | 23 ++++++++++++++++------- protocol/types_test.go | 12 ++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/protocol/types.go b/protocol/types.go index 0633a25..3c6221a 100644 --- a/protocol/types.go +++ b/protocol/types.go @@ -460,6 +460,20 @@ func (c RuntimeAdapterContract) SupportsAdapterKind(kind RuntimeAdapterKind) boo const MaxRuntimeResultPreviewBytes = 16 * 1024 +func ValidateRuntimeResultPreview(preview map[string]any) error { + if len(preview) == 0 { + return nil + } + data, err := json.Marshal(preview) + if err != nil { + return fmt.Errorf("result_preview must be JSON-serializable: %w", err) + } + if len(data) > MaxRuntimeResultPreviewBytes { + return fmt.Errorf("result_preview must be at most %d bytes", MaxRuntimeResultPreviewBytes) + } + return nil +} + type RuntimeExecutionResult struct { StartedAt time.Time `json:"started_at,omitempty"` FinishedAt time.Time `json:"finished_at,omitempty"` @@ -480,13 +494,8 @@ func (r RuntimeExecutionResult) Validate() error { if r.ArtifactHash != "" && !validSHA256Ref(r.ArtifactHash) { errs = append(errs, errors.New("artifact_hash must be sha256:<64 hex chars>")) } - if len(r.ResultPreview) > 0 { - data, err := json.Marshal(r.ResultPreview) - if err != nil { - errs = append(errs, fmt.Errorf("result_preview must be JSON-serializable: %w", err)) - } else if len(data) > MaxRuntimeResultPreviewBytes { - errs = append(errs, fmt.Errorf("result_preview must be at most %d bytes", MaxRuntimeResultPreviewBytes)) - } + if err := ValidateRuntimeResultPreview(r.ResultPreview); err != nil { + errs = append(errs, err) } return errors.Join(errs...) } diff --git a/protocol/types_test.go b/protocol/types_test.go index bdd1532..137f07a 100644 --- a/protocol/types_test.go +++ b/protocol/types_test.go @@ -109,6 +109,18 @@ func TestRuntimeExecutionResultValidatesTimingAndPreview(t *testing.T) { } } +func TestValidateRuntimeResultPreview(t *testing.T) { + if err := protocol.ValidateRuntimeResultPreview(map[string]any{"ok": true}); err != nil { + t.Fatalf("bounded preview rejected: %v", err) + } + if err := protocol.ValidateRuntimeResultPreview(map[string]any{"payload": strings.Repeat("x", protocol.MaxRuntimeResultPreviewBytes+1)}); err == nil || !strings.Contains(err.Error(), "result_preview") { + t.Fatalf("expected oversized preview error, got %v", err) + } + if err := protocol.ValidateRuntimeResultPreview(map[string]any{"bad": func() {}}); err == nil || !strings.Contains(err.Error(), "JSON-serializable") { + t.Fatalf("expected JSON serialization error, got %v", err) + } +} + func TestRuntimeServiceResultValidatesSLOEvidence(t *testing.T) { result := protocol.RuntimeServiceResult{ StartedAt: time.Unix(1, 0).UTC(),