Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/plans/2026-05-31-runtime-workload-contracts-design.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions docs/plans/2026-05-31-runtime-workload-contracts.md
Original file line number Diff line number Diff line change
@@ -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
```
105 changes: 105 additions & 0 deletions protocol/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
54 changes: 54 additions & 0 deletions protocol/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading