diff --git a/protocol/types.go b/protocol/types.go index 7a2846e..f95861e 100644 --- a/protocol/types.go +++ b/protocol/types.go @@ -2307,10 +2307,14 @@ func (w WASMWorkload) Validate() error { } func validateProviderComponentRef(name, value string) error { - value = strings.TrimSpace(value) - if value == "" { + raw := value + trimmed := strings.TrimSpace(value) + if trimmed == "" { return fmt.Errorf("%s is required", name) } + if raw != trimmed { + return fmt.Errorf("%s must not contain surrounding whitespace", name) + } if strings.ContainsAny(value, " \t\r\n\x00") { return fmt.Errorf("%s must not contain whitespace or NUL", name) } @@ -4856,10 +4860,14 @@ func validateScopedRef(name, value, scheme string) error { } func validateComponentRef(name, value string) error { - value = strings.TrimSpace(value) - if value == "" { + raw := value + trimmed := strings.TrimSpace(value) + if trimmed == "" { return fmt.Errorf("%s is required", name) } + if raw != trimmed { + return fmt.Errorf("%s must not contain surrounding whitespace", name) + } for _, prefix := range []string{"artifact://", "content://", "provider://"} { if strings.HasPrefix(value, prefix) && !strings.ContainsAny(value, " \t\r\n\x00") { return nil diff --git a/protocol/types_test.go b/protocol/types_test.go index b293f40..2cc6747 100644 --- a/protocol/types_test.go +++ b/protocol/types_test.go @@ -152,6 +152,20 @@ func TestServiceWorkloadRejectsUnsafeShape(t *testing.T) { } } +func TestServiceWorkloadRejectsComponentRefWhitespace(t *testing.T) { + workload := protocol.ServiceWorkload{ + ComponentRef: " provider://workflow-plugin-compute-service/service-runtime", + ComponentDigest: "sha256:" + strings.Repeat("a", 64), + Command: []string{"serve", "--port", "8080"}, + Ports: []protocol.ServicePort{{Name: "http", Port: 8080, Protocol: "http"}}, + HealthCheck: protocol.HealthCheck{Kind: "http", Path: "/healthz", IntervalSeconds: 5, TimeoutSeconds: 2}, + Ingress: protocol.IngressPolicy{Mode: "private", AuthRequired: true}, + } + if err := workload.Validate(); err == nil || !strings.Contains(err.Error(), "surrounding whitespace") { + t.Fatalf("service workload accepted whitespace-padded component_ref: %v", err) + } +} + func TestWorkloadSpecValidatesNodeServiceWorkload(t *testing.T) { endpoint := "node.example.invalid:30303" workload := protocol.WorkloadSpec{ @@ -184,6 +198,27 @@ func TestWorkloadSpecValidatesNodeServiceWorkload(t *testing.T) { } } +func TestNodeServiceWorkloadRejectsComponentRefWhitespace(t *testing.T) { + endpoint := "node.example.invalid:30303" + workload := protocol.NodeServiceWorkload{ + ComponentRef: "provider://workflow-plugin-compute-service/node-runtime ", + ComponentDigest: "sha256:" + strings.Repeat("b", 64), + Chain: "ethereum", + Network: "sepolia", + DataDirRef: "volume://nodes/sepolia", + RPCSecretRef: "secret://nodes/sepolia/rpc", + PeerPolicy: protocol.PeerPolicy{ + Mode: "allowlist", + AllowedPeers: []string{endpoint}, + EgressAllowlist: []string{endpoint}, + }, + HealthCheck: protocol.HealthCheck{Kind: "command", Command: []string{"node", "status"}, IntervalSeconds: 10, TimeoutSeconds: 3}, + } + if err := workload.Validate(); err == nil || !strings.Contains(err.Error(), "surrounding whitespace") { + t.Fatalf("node-service workload accepted whitespace-padded component_ref: %v", err) + } +} + func TestNodeServiceWorkloadRejectsMissingHealthAndUnsafePeers(t *testing.T) { workload := protocol.NodeServiceWorkload{ ImageRef: "ghcr.io/gocodealone/node@sha256:" + strings.Repeat("c", 64),