From a25a3f429ae02f58f4e5c68c1b455788b48a8ab7 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 24 May 2026 13:51:21 -0400 Subject: [PATCH] feat: add runtime executor contracts --- ...05-24-runtime-executor-contracts-design.md | 54 ++++++++ protocol/types.go | 118 ++++++++++++++++++ protocol/types_test.go | 72 +++++++++++ 3 files changed, 244 insertions(+) create mode 100644 docs/plans/2026-05-24-runtime-executor-contracts-design.md diff --git a/docs/plans/2026-05-24-runtime-executor-contracts-design.md b/docs/plans/2026-05-24-runtime-executor-contracts-design.md new file mode 100644 index 0000000..5182505 --- /dev/null +++ b/docs/plans/2026-05-24-runtime-executor-contracts-design.md @@ -0,0 +1,54 @@ +# Runtime Executor Contracts Design + +## Goal + +Move the smallest runtime execution boundary that provider plugins need into +`workflow-plugin-compute-core`: executor identity, proof-relevant runtime +metadata, and resource usage/limit evidence. This supports future runtime +plugins without forcing `workflow-compute` host internals such as leases, +workspace paths, network bindings, or process supervision into the public core +contract. + +## Design + +`RuntimeDescriptor` is the public descriptor shape a runtime provider can +advertise. It carries provider name, version, execution security tier, proof +tier, and optional image/rootfs digests. It can produce an `ExecutorRef`, which +is the proof-facing executor identity already used by `workflow-compute`. + +`ExecutorRef` moves with its `ValidateForProof` and attestation requirement +helpers so proof validation and runtime advertisement share one contract. + +`ResourceUsage` and `ResourceLimits` become compute-core contracts because +runtime plugins need to report measured usage and receive hard limits without +depending on `workflow-compute` task execution structs. + +## Assumptions + +- Runtime plugins need shared metadata and evidence contracts before they need + shared process execution APIs. +- `workflow-compute` remains responsible for host-only execution context: + workspace preparation, lease authorization, network binding, and supervisor + integration. +- Existing runtime providers can alias these contracts without changing + behavior. + +## Rollback + +Rollback is reverting this contract addition and restoring local +`workflow-compute` type ownership. No stored data format changes are introduced +because the JSON field names intentionally match the existing +`workflow-compute` structs. + +## Self-Challenge + +- Laziest solution: leave these structs local until the runtime plugin repos + exist. Rejected because provider plugins already need a stable advertised + runtime and proof evidence shape. +- Fragile assumption: `RuntimeDescriptor` may need workload-kind or network + mode declarations later. Those are intentionally left in + `ProviderRuntimeProfile` and provider contracts for now to avoid duplicating + catalog policy. +- Partial failure risk: moving execution request structs too early would expose + host workspace and network internals as public API. This slice explicitly + avoids that. diff --git a/protocol/types.go b/protocol/types.go index 6678556..a5ebe1e 100644 --- a/protocol/types.go +++ b/protocol/types.go @@ -125,6 +125,124 @@ const ( ProofZKReplay ProofTier = "zk-replay" ) +type RuntimeDescriptor struct { + Name string `json:"name"` + Version string `json:"version"` + ExecutionSecurityTier ExecutionSecurityTier `json:"execution_security_tier,omitempty"` + ProofTier ProofTier `json:"proof_tier,omitempty"` + ImageDigest string `json:"image_digest,omitempty"` + RootFSDigest string `json:"rootfs_digest,omitempty"` +} + +func (d RuntimeDescriptor) ExecutorRef(defaultProvider string) ExecutorRef { + provider := d.Name + if provider == "" { + provider = defaultProvider + } + version := d.Version + if version == "" { + version = "dev" + } + return ExecutorRef{ + Provider: provider, + Version: version, + ExecutionSecurityTier: d.ExecutionSecurityTier, + ProofTier: d.ProofTier, + ImageDigest: d.ImageDigest, + RootFSDigest: d.RootFSDigest, + } +} + +type ExecutorRef struct { + Provider string `json:"provider"` + Version string `json:"version"` + ExecutionSecurityTier ExecutionSecurityTier `json:"execution_security_tier,omitempty"` + ProofTier ProofTier `json:"proof_tier,omitempty"` + ImageDigest string `json:"image_digest,omitempty"` + RootFSDigest string `json:"rootfs_digest,omitempty"` +} + +func (e ExecutorRef) ValidateForProof() error { + var errs []error + if e.Provider == "" { + errs = append(errs, errors.New("executor.provider is required")) + } + if e.Version == "" { + errs = append(errs, errors.New("executor.version is required")) + } + if e.ExecutionSecurityTier == "" { + errs = append(errs, errors.New("executor.execution_security_tier is required")) + } + if e.ProofTier == "" { + errs = append(errs, errors.New("executor.proof_tier is required")) + } + if e.ExecutionSecurityTier != "" && e.ExecutionSecurityTier != ExecutionTrustedNative && e.ExecutionSecurityTier != ExecutionWASMCapability { + if e.ImageDigest == "" { + errs = append(errs, errors.New("executor.image_digest is required for non-native executor")) + } + if e.RootFSDigest == "" { + errs = append(errs, errors.New("executor.rootfs_digest is required for non-native executor")) + } + } + return errors.Join(errs...) +} + +func (e ExecutorRef) RequiresAttestation() bool { + switch e.ExecutionSecurityTier { + case ExecutionConfidentialCPU, ExecutionConfidentialGPU: + return true + } + switch e.ProofTier { + case ProofAttestedReceipt, ProofAttestedQuorum: + return true + default: + return false + } +} + +type ResourceUsage struct { + CPUMillis int64 `json:"cpu_millis,omitempty"` + GPUMillis int64 `json:"gpu_millis,omitempty"` + MaxMemoryBytes int64 `json:"max_memory_bytes,omitempty"` + NetworkRxBytes int64 `json:"network_rx_bytes,omitempty"` + NetworkTxBytes int64 `json:"network_tx_bytes,omitempty"` + WorkspaceBytes int64 `json:"workspace_bytes,omitempty"` + OutputBytes int64 `json:"output_bytes,omitempty"` + LimitHit string `json:"limit_hit,omitempty"` +} + +type ResourceLimits struct { + CPUPercent int `json:"cpu_percent,omitempty"` + MemoryBytes int64 `json:"memory_bytes,omitempty"` + WorkspaceBytes int64 `json:"workspace_bytes,omitempty"` + RuntimeSeconds int `json:"runtime_seconds,omitempty"` + NetworkEgressBytes int64 `json:"network_egress_bytes,omitempty"` + OutputBytes int64 `json:"output_bytes,omitempty"` +} + +func (l ResourceLimits) Validate() error { + var errs []error + if l.CPUPercent < 0 { + errs = append(errs, errors.New("cpu_percent cannot be negative")) + } + if l.MemoryBytes < 0 { + errs = append(errs, errors.New("memory_bytes cannot be negative")) + } + if l.WorkspaceBytes < 0 { + errs = append(errs, errors.New("workspace_bytes cannot be negative")) + } + if l.RuntimeSeconds < 0 { + errs = append(errs, errors.New("runtime_seconds cannot be negative")) + } + if l.NetworkEgressBytes < 0 { + errs = append(errs, errors.New("network_egress_bytes cannot be negative")) + } + if l.OutputBytes < 0 { + errs = append(errs, errors.New("output_bytes cannot be negative")) + } + return errors.Join(errs...) +} + type NetworkMode string const ( diff --git a/protocol/types_test.go b/protocol/types_test.go index 0e3686b..ca2b52e 100644 --- a/protocol/types_test.go +++ b/protocol/types_test.go @@ -15,6 +15,78 @@ func TestProviderContractAcceptsBatchSandboxedOCI(t *testing.T) { } } +func TestRuntimeDescriptorProducesExecutorRef(t *testing.T) { + descriptor := protocol.RuntimeDescriptor{ + Name: "sandboxed-command", + Version: "v1.2.3", + ExecutionSecurityTier: protocol.ExecutionSandboxedContainer, + ProofTier: protocol.ProofArtifactHash, + ImageDigest: "sha256:image", + RootFSDigest: "sha256:rootfs", + } + + ref := descriptor.ExecutorRef("fallback") + + if ref.Provider != "sandboxed-command" || ref.Version != "v1.2.3" { + t.Fatalf("executor identity = %+v", ref) + } + if ref.ExecutionSecurityTier != protocol.ExecutionSandboxedContainer || ref.ProofTier != protocol.ProofArtifactHash { + t.Fatalf("executor proof metadata = %+v", ref) + } + if ref.ImageDigest != "sha256:image" || ref.RootFSDigest != "sha256:rootfs" { + t.Fatalf("executor digests = %+v", ref) + } +} + +func TestRuntimeDescriptorFallsBackToProviderNameAndDevVersion(t *testing.T) { + ref := (protocol.RuntimeDescriptor{}).ExecutorRef("command") + + if ref.Provider != "command" || ref.Version != "dev" { + t.Fatalf("fallback executor ref = %+v", ref) + } +} + +func TestExecutorRefValidateForProofRequiresDigestsForNonNativeExecutors(t *testing.T) { + ref := protocol.ExecutorRef{ + Provider: "sandboxed-command", + Version: "dev", + ExecutionSecurityTier: protocol.ExecutionSandboxedContainer, + ProofTier: protocol.ProofArtifactHash, + } + + err := ref.ValidateForProof() + if err == nil { + t.Fatal("expected non-native executor without image digests to fail") + } + if !strings.Contains(err.Error(), "image_digest") || !strings.Contains(err.Error(), "rootfs_digest") { + t.Fatalf("expected digest errors, got %v", err) + } + + ref.ImageDigest = "sha256:image" + ref.RootFSDigest = "sha256:rootfs" + if err := ref.ValidateForProof(); err != nil { + t.Fatalf("executor ref invalid with digests: %v", err) + } +} + +func TestResourceLimitsRejectNegativeValues(t *testing.T) { + limits := protocol.ResourceLimits{ + CPUPercent: -1, + RuntimeSeconds: -1, + OutputBytes: -1, + } + + err := limits.Validate() + if err == nil { + t.Fatal("expected negative resource limits to fail") + } + if !strings.Contains(err.Error(), "cpu_percent") || + !strings.Contains(err.Error(), "runtime_seconds") || + !strings.Contains(err.Error(), "output_bytes") { + t.Fatalf("expected resource limit errors, got %v", err) + } +} + func TestProviderContractRejectsMalformedConfigSchemaDigest(t *testing.T) { contract := validBatchProviderContract() contract.ConfigSchemaDigest = "sha256:not-hex"