diff --git a/protocol/types.go b/protocol/types.go index d51ff7c..992eb72 100644 --- a/protocol/types.go +++ b/protocol/types.go @@ -1033,6 +1033,12 @@ func (r ProviderUpstreamClientRequirement) Validate() error { errs = append(errs, fmt.Errorf("%s is required", field.name)) } } + if strings.TrimSpace(r.Version) != r.Version || strings.ContainsAny(r.Version, "\t\r\n\x00") { + errs = append(errs, errors.New("version is required")) + } + if strings.TrimSpace(r.UpstreamClientName) != r.UpstreamClientName || strings.ContainsAny(r.UpstreamClientName, "\t\r\n\x00") { + errs = append(errs, errors.New("upstream_client_name is required")) + } if r.ProtocolVersion != Version { errs = append(errs, fmt.Errorf("protocol_version must be %q", Version)) } @@ -1048,6 +1054,21 @@ func (r ProviderUpstreamClientRequirement) Validate() error { if err := r.ImagePolicy.Validate(); err != nil { errs = append(errs, err) } + for i, value := range r.VersionProbeCommand { + if strings.TrimSpace(value) == "" || strings.ContainsAny(value, "\t\r\n\x00") { + errs = append(errs, fmt.Errorf("version_probe_command[%d] is required", i)) + } + } + for i, value := range r.RequiredEvidence { + if strings.TrimSpace(value) == "" || strings.ContainsAny(value, "\t\r\n\x00") { + errs = append(errs, fmt.Errorf("required_evidence[%d] is required", i)) + } + } + for i, value := range r.Notes { + if strings.TrimSpace(value) == "" || strings.ContainsAny(value, "\t\r\n\x00") { + errs = append(errs, fmt.Errorf("notes[%d] is required", i)) + } + } return errors.Join(errs...) } diff --git a/protocol/types_test.go b/protocol/types_test.go index b8c01fa..df12bf6 100644 --- a/protocol/types_test.go +++ b/protocol/types_test.go @@ -47,6 +47,48 @@ func TestProviderUpstreamImagePolicyRequiresRecommendedImageUnlessOperatorSuppli } } +func TestProviderUpstreamClientRequirementRejectsControlWhitespaceLists(t *testing.T) { + req := protocol.ProviderUpstreamClientRequirement{ + ProtocolVersion: protocol.Version, + PluginID: "workflow-plugin-crypto", + ProviderID: "ethereum-full-node", + ContractID: "ethereum-full-node.v1", + Version: "v1.0.0", + RuntimeProfileID: "sandboxed-container-runtime", + ConformanceProfile: "upstream-client-v1", + DefaultConformance: protocol.UpstreamClientConformanceShapeOnly, + RealClientConformance: protocol.UpstreamClientConformanceRealClient, + UpstreamClientName: "geth", + VersionProbeCommand: []string{"geth version"}, + ImagePolicy: protocol.ProviderUpstreamImagePolicy{ + DigestPinnedImageRequired: true, + RecommendedImageRef: "ethereum/client-go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + RequiredEvidence: []string{"artifact://provider/evidence"}, + Notes: []string{"operator may provide a digest-pinned image"}, + } + if err := req.Validate(); err != nil { + t.Fatalf("requirement invalid: %v", err) + } + + req.VersionProbeCommand = []string{"geth\nversion"} + if err := req.Validate(); err == nil || !strings.Contains(err.Error(), "version_probe_command") { + t.Fatalf("expected version_probe_command error, got %v", err) + } + req.VersionProbeCommand = []string{"geth version"} + + req.RequiredEvidence = []string{""} + if err := req.Validate(); err == nil || !strings.Contains(err.Error(), "required_evidence") { + t.Fatalf("expected required_evidence error, got %v", err) + } + req.RequiredEvidence = []string{"artifact://provider/evidence"} + + req.Notes = []string{" "} + if err := req.Validate(); err == nil || !strings.Contains(err.Error(), "notes") { + t.Fatalf("expected notes error, got %v", err) + } +} + func TestProviderRuntimeProfileRejectsReusableResidueWithoutWorkspace(t *testing.T) { contract := validBatchProviderContract() profile := &contract.RuntimeContract.Profiles[0]