diff --git a/docs/plans/2026-05-31-runtime-workload-contracts-design.md b/docs/plans/2026-05-31-runtime-workload-contracts-design.md new file mode 100644 index 0000000..53203c7 --- /dev/null +++ b/docs/plans/2026-05-31-runtime-workload-contracts-design.md @@ -0,0 +1,43 @@ +# Runtime Workload Contracts Design + +## Goal + +Expose the command and container-build workload payload contracts in +`workflow-plugin-compute-core/protocol` so public runtime plugins can decode and +validate the same input shape that `workflow-compute` currently sends through +`RuntimeExecutionRequest.Input`. + +This is the prerequisite phase for workflow-compute RTE-1 +`workflow-plugin-compute-container`: without these contracts, the public plugin +would have to copy host-local workload structs and validation before the +in-core runtime fallback can be deleted. + +## Boundary + +Compute-core owns only host-independent payload contracts: + +- `EnvRef` +- `ConfidentialPayloadRef` +- `CommandWorkload` +- `ContainerBuildWorkload` + +The host keeps task admission, lease authorization, secret resolution, +workspace path resolution, network policy binding, registry allowlists, proof +verification, and reward/proof mutation. + +## Validation + +Validation mirrors the current host contract: + +- command workloads require args, validate env refs, scoped artifact refs, and + optional confidential payload metadata; +- container-build workloads require context directory and tags, validate env + refs, and carry registry target refs without deciding whether they are + allowed; +- env refs may point to a value or a secret, not both. + +## Rollback + +Rollback removes the additive compute-core types and keeps the equivalent +payload contracts local in `workflow-compute`. No persisted data migration is +introduced. diff --git a/docs/plans/2026-05-31-runtime-workload-contracts.md b/docs/plans/2026-05-31-runtime-workload-contracts.md new file mode 100644 index 0000000..a1375e8 --- /dev/null +++ b/docs/plans/2026-05-31-runtime-workload-contracts.md @@ -0,0 +1,30 @@ +# Runtime Workload Contracts Plan + +## Scope + +Add the public command/container-build workload contracts needed before +`workflow-compute` can migrate RTE-1 runtime adapters into +`workflow-plugin-compute-container`. + +## Tasks + +1. Add failing protocol tests for command and container-build workload payloads. +2. Add public protocol types and validation in `protocol/types.go`. +3. Prove the tests fail when the production types are reverted and pass when + restored. +4. Run the full compute-core suite. + +## Deferred Issues + +- `workflow-compute` still needs a follow-up PR to alias its local workload + structs to compute-core after this module is released. +- `workflow-plugin-compute-container` repo creation and in-core fallback + deletion remain in the RTE-1 phase after workflow-compute consumes the + released compute-core contract. + +## Verification + +```bash +GOWORK=off go test ./protocol -run 'Test(CommandWorkloadContractUsesResolvedRefs|ContainerBuildWorkloadContractUsesRegistryRefs)' -count=1 +GOWORK=off go test ./... -count=1 +``` diff --git a/protocol/types.go b/protocol/types.go index 26449ea..71e9434 100644 --- a/protocol/types.go +++ b/protocol/types.go @@ -1588,6 +1588,111 @@ type RuntimeExecutionRequest struct { Limits ResourceLimits `json:"limits,omitzero"` } +type EnvRef struct { + Name string `json:"name"` + ValueRef string `json:"value_ref,omitempty"` + SecretRef string `json:"secret_ref,omitempty"` +} + +func (r EnvRef) Validate() error { + var errs []error + if r.Name == "" { + errs = append(errs, errors.New("name is required")) + } + if r.ValueRef == "" && r.SecretRef == "" { + errs = append(errs, errors.New("requires value_ref or secret_ref")) + } + if r.ValueRef != "" && r.SecretRef != "" { + errs = append(errs, errors.New("cannot set both value_ref and secret_ref")) + } + return errors.Join(errs...) +} + +type ConfidentialPayloadRef struct { + CiphertextRef string `json:"ciphertext_ref"` + CiphertextHash string `json:"ciphertext_hash"` + KeyRefHash string `json:"key_ref_hash"` + Algorithm string `json:"algorithm"` + KBSPolicyID string `json:"kbs_policy_id"` +} + +func (r ConfidentialPayloadRef) Validate() error { + var errs []error + if err := validateScopedRef("ciphertext_ref", r.CiphertextRef, "artifact://"); err != nil { + errs = append(errs, err) + } + if !validSHA256Digest(r.CiphertextHash) { + errs = append(errs, errors.New("ciphertext_hash must be sha256:<64 hex chars>")) + } + if !validSHA256Digest(r.KeyRefHash) { + errs = append(errs, errors.New("key_ref_hash must be sha256:<64 hex chars>")) + } + if r.Algorithm == "" { + errs = append(errs, errors.New("algorithm is required")) + } + if r.KBSPolicyID == "" { + errs = append(errs, errors.New("kbs_policy_id is required")) + } + return errors.Join(errs...) +} + +type CommandWorkload struct { + Args []string `json:"args"` + WorkingDirectory string `json:"working_directory,omitempty"` + Env []EnvRef `json:"env,omitempty"` + ArtifactRefs []string `json:"artifact_refs,omitempty"` + ArtifactAllowlist []string `json:"artifact_allowlist,omitempty"` + ConfidentialPayload *ConfidentialPayloadRef `json:"confidential_payload,omitempty"` +} + +func (w CommandWorkload) Validate() error { + var errs []error + if len(w.Args) == 0 { + errs = append(errs, errors.New("command args are required")) + } + for i, ref := range w.Env { + if err := ref.Validate(); err != nil { + errs = append(errs, fmt.Errorf("env[%d]: %w", i, err)) + } + } + for i, ref := range w.ArtifactRefs { + if err := validateScopedRef(fmt.Sprintf("artifact_refs[%d]", i), ref, "artifact://"); err != nil { + errs = append(errs, err) + } + } + if w.ConfidentialPayload != nil { + if err := w.ConfidentialPayload.Validate(); err != nil { + errs = append(errs, fmt.Errorf("confidential_payload: %w", err)) + } + } + return errors.Join(errs...) +} + +type ContainerBuildWorkload struct { + ContextDirectory string `json:"context_directory"` + Dockerfile string `json:"dockerfile,omitempty"` + Tags []string `json:"tags,omitempty"` + PushTargetRef string `json:"push_target_ref,omitempty"` + PullTargetRef string `json:"pull_target_ref,omitempty"` + Env []EnvRef `json:"env,omitempty"` +} + +func (w ContainerBuildWorkload) Validate() error { + var errs []error + if w.ContextDirectory == "" { + errs = append(errs, errors.New("context_directory is required")) + } + if len(w.Tags) == 0 { + errs = append(errs, errors.New("tags is required")) + } + for i, ref := range w.Env { + if err := ref.Validate(); err != nil { + errs = append(errs, fmt.Errorf("env[%d]: %w", i, err)) + } + } + return errors.Join(errs...) +} + func (r RuntimeExecutionRequest) Validate() error { var errs []error if r.ProtocolVersion != Version { diff --git a/protocol/types_test.go b/protocol/types_test.go index bb2417b..8b76d56 100644 --- a/protocol/types_test.go +++ b/protocol/types_test.go @@ -389,6 +389,60 @@ func TestRuntimeDescriptorFallsBackToProviderNameAndDevVersion(t *testing.T) { } } +func TestCommandWorkloadContractUsesResolvedRefs(t *testing.T) { + workload := protocol.CommandWorkload{ + Args: []string{"go", "test", "./..."}, + Env: []protocol.EnvRef{ + {Name: "GOPRIVATE", ValueRef: "config:goprivate"}, + {Name: "GITHUB_TOKEN", SecretRef: "secret:github-token"}, + }, + ArtifactRefs: []string{"artifact://pool-1/input.tar"}, + ConfidentialPayload: &protocol.ConfidentialPayloadRef{ + CiphertextRef: "artifact://pool-1/payload.cose", + CiphertextHash: protocol.CanonicalHash("ciphertext"), + KeyRefHash: protocol.CanonicalHash("key-ref"), + Algorithm: "trustee-envelope-v1", + KBSPolicyID: "policy-1", + }, + } + + if err := workload.Validate(); err != nil { + t.Fatalf("valid command workload rejected: %v", err) + } + + workload.Env[0].SecretRef = "secret:goprivate" + if err := workload.Validate(); err == nil || !strings.Contains(err.Error(), "cannot set both value_ref and secret_ref") { + t.Fatalf("env ref with both value and secret refs accepted: %v", err) + } + + workload.Env[0].SecretRef = "" + workload.ConfidentialPayload.CiphertextRef = "https://example.invalid/payload.cose" + if err := workload.Validate(); err == nil || !strings.Contains(err.Error(), "ciphertext_ref") { + t.Fatalf("origin URL confidential payload accepted: %v", err) + } +} + +func TestContainerBuildWorkloadContractUsesRegistryRefs(t *testing.T) { + workload := protocol.ContainerBuildWorkload{ + ContextDirectory: ".", + Tags: []string{"example:latest"}, + PushTargetRef: "registry:ghcr", + PullTargetRef: "registry:dockerhub", + Env: []protocol.EnvRef{ + {Name: "DOCKER_CONFIG_JSON", SecretRef: "secret://pool-1/ghcr-docker-config"}, + }, + } + + if err := workload.Validate(); err != nil { + t.Fatalf("valid container-build workload rejected: %v", err) + } + + workload.Env[0].ValueRef = "config:docker-config" + if err := workload.Validate(); err == nil || !strings.Contains(err.Error(), "cannot set both value_ref and secret_ref") { + t.Fatalf("container-build env ref with both value and secret refs accepted: %v", err) + } +} + func TestExecutorRefValidateForProofRequiresDigestsForNonNativeExecutors(t *testing.T) { ref := protocol.ExecutorRef{ Provider: "sandboxed-command",