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
55 changes: 55 additions & 0 deletions docs/plans/2026-05-24-runtime-adapter-contract-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Runtime Adapter Contract Design

## Goal

Define the public, host-independent contract a runtime execution plugin can
advertise before `workflow-compute` moves concrete executors into plugin repos.
The contract should let hosts compare adapter capability, workspace policy,
runtime descriptor identity, workload kinds, and conformance evidence without
leaking server-owned task, lease, mount, network binding, or verifier state.

## Design

`RuntimeAdapterContract` is a declaration, not an execution API. It includes
the compute protocol version, adapter ID, runtime descriptor, adapter kinds,
supported workload kinds, optional runtime profiles, workspace policy,
conformance profiles, residue policy, optional provider config, and metadata.

`RuntimeAdapterKind` separates short-lived execution, one-shot service run, and
long-lived service session support. `RuntimeWorkspacePolicy` is explicit because
some workloads, especially WASM-style adapters, may not have a workspace.
Service adapter kinds must declare `service` or `node-service` workload support
so a plugin cannot advertise long-lived service capability for only command-like
workloads.

`RuntimeDescriptor.Validate` is added so adapter contracts can validate runtime
identity, security tier, proof tier, and non-native image/rootfs digest
requirements before a host accepts a plugin declaration.

## Assumptions

- A declaration contract is the right next step before moving executor process
code out of `workflow-compute`.
- Runtime plugins can describe capability with workload kinds and adapter kinds
while the host still decides task authorization, lease state, workspace
mounts, network capability, and proof verification.
- Residue policy may describe reusable non-workspace state, so workspace policy
must not blindly reject all residue for workspace-less adapters.

## Rollback

Rollback is reverting the additive adapter contract types and keeping
`workflow-compute` executor capability discovery local. No persisted state or
wire format migration is introduced by this slice.

## Self-Challenge

- Laziest solution: rely on `ProviderRuntimeContract` only. Rejected because it
describes provider runtime profiles, not what an executor plugin adapter can
register or whether it supports service sessions.
- Fragile assumption: adapter kinds are enough for the first plugin boundary.
The mitigation is that execution payloads remain in `RuntimeExecutionRequest`
and host-only capability handles stay out of compute-core.
- Security risk: plugin metadata could be mistaken for authorization. The
contract is declarative only; hosts must keep authorization and lease checks
local.
177 changes: 177 additions & 0 deletions protocol/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ const (
ProofZKReplay ProofTier = "zk-replay"
)

type RuntimeAdapterKind string

const (
RuntimeAdapterExecution RuntimeAdapterKind = "execution"
RuntimeAdapterServiceRun RuntimeAdapterKind = "service-run"
RuntimeAdapterServiceSession RuntimeAdapterKind = "service-session"
)

type RuntimeWorkspacePolicy string

const (
RuntimeWorkspaceUnavailable RuntimeWorkspacePolicy = "unavailable"
RuntimeWorkspaceOptional RuntimeWorkspacePolicy = "optional"
RuntimeWorkspaceRequired RuntimeWorkspacePolicy = "required"
)

type RuntimeDescriptor struct {
Name string `json:"name"`
Version string `json:"version"`
Expand All @@ -134,6 +150,37 @@ type RuntimeDescriptor struct {
RootFSDigest string `json:"rootfs_digest,omitempty"`
}

func (d RuntimeDescriptor) Validate() error {
var errs []error
if err := validateIdentifier("name", d.Name); err != nil {
errs = append(errs, err)
}
if strings.TrimSpace(d.Version) == "" {
errs = append(errs, errors.New("version is required"))
} else if strings.ContainsAny(d.Version, " \t\r\n\x00") {
errs = append(errs, errors.New("version must not contain whitespace"))
}
if !validExecutionSecurityTier(d.ExecutionSecurityTier) {
errs = append(errs, fmt.Errorf("execution_security_tier %q is unsupported", d.ExecutionSecurityTier))
}
if !validProofTier(d.ProofTier) {
errs = append(errs, fmt.Errorf("proof_tier %q is unsupported", d.ProofTier))
}
if d.ExecutionSecurityTier != ExecutionTrustedNative && d.ExecutionSecurityTier != ExecutionWASMCapability {
if d.ImageDigest == "" {
errs = append(errs, errors.New("image_digest is required for non-native runtime"))
} else if !validSHA256Ref(d.ImageDigest) {
errs = append(errs, errors.New("image_digest must be sha256:<64 hex chars>"))
}
if d.RootFSDigest == "" {
errs = append(errs, errors.New("rootfs_digest is required for non-native runtime"))
} else if !validSHA256Ref(d.RootFSDigest) {
errs = append(errs, errors.New("rootfs_digest must be sha256:<64 hex chars>"))
}
}
return errors.Join(errs...)
}

func (d RuntimeDescriptor) ExecutorRef(defaultProvider string) ExecutorRef {
provider := d.Name
if provider == "" {
Expand Down Expand Up @@ -286,6 +333,118 @@ func (r RuntimeExecutionRequest) Validate() error {
return errors.Join(errs...)
}

type RuntimeAdapterContract struct {
ProtocolVersion string `json:"protocol_version"`
AdapterID string `json:"adapter_id"`
Descriptor RuntimeDescriptor `json:"descriptor,omitzero"`
Kinds []RuntimeAdapterKind `json:"kinds"`
WorkloadKinds []WorkloadKind `json:"workload_kinds"`
RuntimeProfiles []RuntimeProfile `json:"runtime_profiles,omitempty"`
WorkspacePolicy RuntimeWorkspacePolicy `json:"workspace_policy"`
ConformanceProfiles []string `json:"conformance_profiles"`
ResiduePolicy ResiduePolicy `json:"residue_policy,omitzero"`
ProviderConfig ProviderConfig `json:"provider_config,omitzero"`
Metadata map[string]string `json:"metadata,omitempty"`
}

func (c RuntimeAdapterContract) Validate() error {
var errs []error
if c.ProtocolVersion != Version {
errs = append(errs, fmt.Errorf("protocol_version must be %q", Version))
}
if err := validateIdentifier("adapter_id", c.AdapterID); err != nil {
errs = append(errs, err)
}
if err := c.Descriptor.Validate(); err != nil {
errs = append(errs, fmt.Errorf("descriptor: %w", err))
}
if len(c.Kinds) == 0 {
errs = append(errs, errors.New("kinds is required"))
}
seenKinds := map[RuntimeAdapterKind]struct{}{}
for i, kind := range c.Kinds {
if !validRuntimeAdapterKind(kind) {
errs = append(errs, fmt.Errorf("kinds[%d] %q is unsupported", i, kind))
continue
}
if _, exists := seenKinds[kind]; exists {
errs = append(errs, fmt.Errorf("kinds[%d] %q is duplicated", i, kind))
}
seenKinds[kind] = struct{}{}
}
if len(c.WorkloadKinds) == 0 {
errs = append(errs, errors.New("workload_kinds is required"))
}
seenWorkloads := map[WorkloadKind]struct{}{}
for i, kind := range c.WorkloadKinds {
if !validWorkloadKind(kind) {
errs = append(errs, fmt.Errorf("workload_kinds[%d] %q is unsupported", i, kind))
continue
}
if _, exists := seenWorkloads[kind]; exists {
errs = append(errs, fmt.Errorf("workload_kinds[%d] %q is duplicated", i, kind))
}
seenWorkloads[kind] = struct{}{}
}
if c.SupportsAdapterKind(RuntimeAdapterServiceRun) || c.SupportsAdapterKind(RuntimeAdapterServiceSession) {
if !c.Supports(WorkloadService) && !c.Supports(WorkloadNodeService) {
errs = append(errs, errors.New("service adapter kinds require service workload kind"))
}
}
for i, profile := range c.RuntimeProfiles {
if !validRuntimeProfile(profile) {
errs = append(errs, fmt.Errorf("runtime_profiles[%d] %q is unsupported", i, profile))
}
}
if !validRuntimeWorkspacePolicy(c.WorkspacePolicy) {
errs = append(errs, fmt.Errorf("workspace_policy %q is unsupported", c.WorkspacePolicy))
}
if len(c.ConformanceProfiles) == 0 {
errs = append(errs, errors.New("conformance_profiles is required"))
}
seenProfiles := map[string]struct{}{}
for i, profile := range c.ConformanceProfiles {
if err := validateIdentifier(fmt.Sprintf("conformance_profiles[%d]", i), profile); err != nil {
errs = append(errs, err)
continue
}
if _, exists := seenProfiles[profile]; exists {
errs = append(errs, fmt.Errorf("conformance_profiles[%d] %q is duplicated", i, profile))
}
seenProfiles[profile] = struct{}{}
}
if err := c.ResiduePolicy.Validate(ResiduePolicyValidation{RequirePolicyHash: true}); err != nil {
errs = append(errs, fmt.Errorf("residue_policy: %w", err))
}
if err := c.ProviderConfig.Validate(); err != nil {
errs = append(errs, fmt.Errorf("provider_config: %w", err))
}
for key := range c.Metadata {
if err := validateIdentifier("metadata key", key); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}

func (c RuntimeAdapterContract) Supports(kind WorkloadKind) bool {
for _, supported := range c.WorkloadKinds {
if supported == kind {
return true
}
}
return false
}

func (c RuntimeAdapterContract) SupportsAdapterKind(kind RuntimeAdapterKind) bool {
for _, supported := range c.Kinds {
if supported == kind {
return true
}
}
return false
}

const MaxRuntimeResultPreviewBytes = 16 * 1024

type RuntimeExecutionResult struct {
Expand Down Expand Up @@ -1938,6 +2097,24 @@ func validProofTier(tier ProofTier) bool {
}
}

func validRuntimeAdapterKind(kind RuntimeAdapterKind) bool {
switch kind {
case RuntimeAdapterExecution, RuntimeAdapterServiceRun, RuntimeAdapterServiceSession:
return true
default:
return false
}
}

func validRuntimeWorkspacePolicy(policy RuntimeWorkspacePolicy) bool {
switch policy {
case RuntimeWorkspaceUnavailable, RuntimeWorkspaceOptional, RuntimeWorkspaceRequired:
return true
default:
return false
}
}

func normalizeNetworkMode(mode NetworkMode) NetworkMode {
mode = NetworkMode(strings.TrimSpace(string(mode)))
if mode == "" {
Expand Down
83 changes: 83 additions & 0 deletions protocol/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,89 @@ func TestRuntimeServiceResultValidatesStatusEvidenceHashes(t *testing.T) {
}
}

func TestRuntimeAdapterContractValidatesHostIndependentBoundary(t *testing.T) {
contract := protocol.RuntimeAdapterContract{
ProtocolVersion: protocol.Version,
AdapterID: "sandboxed-command",
Descriptor: protocol.RuntimeDescriptor{
Name: "sandboxed-command",
Version: "v1.2.3",
ExecutionSecurityTier: protocol.ExecutionSandboxedContainer,
ProofTier: protocol.ProofArtifactHash,
ImageDigest: protocol.CanonicalHash("image"),
RootFSDigest: protocol.CanonicalHash("rootfs"),
},
Kinds: []protocol.RuntimeAdapterKind{protocol.RuntimeAdapterExecution},
WorkloadKinds: []protocol.WorkloadKind{protocol.WorkloadCommand, protocol.WorkloadContainerBuild},
RuntimeProfiles: []protocol.RuntimeProfile{protocol.RuntimeProfileSandboxedOCI},
WorkspacePolicy: protocol.RuntimeWorkspaceRequired,
ConformanceProfiles: []string{"protected-command-v1"},
ResiduePolicy: protocol.ResiduePolicy{
Mode: protocol.ResidueModeProviderBound,
PolicyHash: protocol.CanonicalHash("residue"),
MaxReuseCount: 2,
},
}

if err := contract.Validate(); err != nil {
t.Fatalf("adapter contract invalid: %v", err)
}
}

func TestRuntimeAdapterContractRejectsAmbiguousRuntimeBoundary(t *testing.T) {
contract := protocol.RuntimeAdapterContract{
ProtocolVersion: "wrong",
AdapterID: "bad adapter",
Descriptor: protocol.RuntimeDescriptor{
Name: "adapter",
Version: "v1.0.0",
ExecutionSecurityTier: protocol.ExecutionSandboxedContainer,
ProofTier: protocol.ProofArtifactHash,
},
Kinds: []protocol.RuntimeAdapterKind{protocol.RuntimeAdapterExecution, protocol.RuntimeAdapterExecution},
WorkloadKinds: []protocol.WorkloadKind{protocol.WorkloadKind("unknown")},
WorkspacePolicy: protocol.RuntimeWorkspacePolicy("maybe"),
ResiduePolicy: protocol.ResiduePolicy{
Mode: protocol.ResidueModeSessionBound,
SessionKey: "session",
},
}

err := contract.Validate()
if err == nil {
t.Fatal("expected malformed adapter contract to fail")
}
for _, want := range []string{"protocol_version", "adapter_id", "descriptor", "kinds", "workload_kinds", "workspace_policy", "conformance_profiles", "residue_policy"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("expected %q in error, got %v", want, err)
}
}
}

func TestRuntimeAdapterContractRequiresServiceWorkloadForServiceAdapters(t *testing.T) {
contract := protocol.RuntimeAdapterContract{
ProtocolVersion: protocol.Version,
AdapterID: "service-adapter",
Descriptor: protocol.RuntimeDescriptor{
Name: "service-adapter",
Version: "v1.0.0",
ExecutionSecurityTier: protocol.ExecutionSandboxedContainer,
ProofTier: protocol.ProofArtifactHash,
ImageDigest: protocol.CanonicalHash("image"),
RootFSDigest: protocol.CanonicalHash("rootfs"),
},
Kinds: []protocol.RuntimeAdapterKind{protocol.RuntimeAdapterServiceSession},
WorkloadKinds: []protocol.WorkloadKind{protocol.WorkloadCommand},
WorkspacePolicy: protocol.RuntimeWorkspaceOptional,
ConformanceProfiles: []string{"service-session-v1"},
}

err := contract.Validate()
if err == nil || !strings.Contains(err.Error(), "service adapter kinds require service workload kind") {
t.Fatalf("expected service workload error, got %v", err)
}
}

func TestRuntimeDescriptorFallsBackToProviderNameAndDevVersion(t *testing.T) {
ref := (protocol.RuntimeDescriptor{}).ExecutorRef("command")

Expand Down
Loading