Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eba47d1
resources: share the workload spec field rules
shrutiyam-glitch Sep 30, 2026
ac77d06
atelet: validate Terminate requests declaratively
shrutiyam-glitch Sep 30, 2026
86f62b0
atelet: validate the Container tree of the WorkloadSpec
shrutiyam-glitch Sep 30, 2026
3877802
atelet: convert Volume and SystemInfoDataSource oneofs to flat fields
shrutiyam-glitch Sep 30, 2026
b11519a
atelet: validate the Volume tree of the WorkloadSpec
shrutiyam-glitch Sep 30, 2026
d33679e
atelet: validate the system-info volume of the WorkloadSpec
shrutiyam-glitch Sep 30, 2026
7c0b0ef
validation: allow at most one actor_metadata system-info data source
shrutiyam-glitch Sep 30, 2026
87e7068
atelet: validate Run requests declaratively
shrutiyam-glitch Sep 30, 2026
237acb7
atelet: validate the sandbox assets of Run requests
shrutiyam-glitch Sep 30, 2026
4755543
atelet: validate UploadPausedCheckpoint requests declaratively
shrutiyam-glitch Sep 30, 2026
ea21f20
atelet: validate Checkpoint requests declaratively
shrutiyam-glitch Sep 30, 2026
34669e0
atelet: validate Restore requests declaratively
shrutiyam-glitch Sep 30, 2026
d850362
Merge branch 'main' into ateom-herder-dv
shrutiyam-glitch Sep 30, 2026
686c550
Merge main and trim duplicated apivalidation edge-case tests
shrutiyam-glitch Sep 30, 2026
38f197d
Merge branch 'main' into ateom-herder-dv
shrutiyam-glitch Oct 2, 2026
bf3313f
Drop reserved 'pause' container name validation in atelet
shrutiyam-glitch Oct 2, 2026
6f69a7a
Merge branch 'main' into ateom-herder-dv
shrutiyam-glitch Oct 5, 2026
f0bd1b7
Merge branch 'main' into ateom-herder-dv
shrutiyam-glitch Oct 7, 2026
9002824
Address comments
shrutiyam-glitch Oct 7, 2026
18d6fe1
atelet: allow multi-name trust bundles and fix test fixtures for vali…
shrutiyam-glitch Oct 7, 2026
c705ef0
Merge branch 'main' into ateom-herder-dv
shrutiyam-glitch Oct 9, 2026
82c70bb
atelet: only require of requests what ateapi can guarantee it sends
shrutiyam-glitch Oct 9, 2026
91fe3a1
Merge branch 'main' into ateom-herder-dv
shrutiyam-glitch Oct 9, 2026
c8b8c1a
Merge branch 'main' into ateom-herder-dv
shrutiyam-glitch Oct 9, 2026
827029c
Bound every CSI map the same way, with headroom over the spec limit
shrutiyam-glitch Oct 9, 2026
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
35 changes: 7 additions & 28 deletions cmd/ateapi/internal/apivalidation/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ package apivalidation

import (
"context"
"strings"

"github.com/agent-substrate/substrate/internal/resources"
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/validate"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
)

Expand Down Expand Up @@ -134,34 +133,14 @@ func ValidateCustom_WorkerAssignment_WorkerPodIps(_ context.Context, _ operation
return validateDualStackIPs(fldPath, value)
}

// ValidateCustom_ExternalVolume_VolumeType checks that a volume type string is well-formed.
// It allows an optional "substrate.io/" prefix, followed by a valid DNS-1123 subdomain.
func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
if value == nil || *value == "" {
return nil
}
var errs field.ErrorList
valToValidate := strings.TrimPrefix(*value, "substrate.io/")
for _, msg := range validation.IsDNS1123Subdomain(valToValidate) {
errs = append(errs, field.Invalid(fldPath, *value, msg))
}
return errs
return resources.ValidateVolumeType(fldPath, *value)
}

// ValidateCustom_ExternalVolume_StorageVolumeId checks that an external volume's storage ID does not
// contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F).
func ValidateCustom_ExternalVolume_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
if value == nil || *value == "" {
return nil
}
for _, r := range *value {
if (r >= 0x0000 && r <= 0x0008) ||
r == 0x000B ||
r == 0x000C ||
(r >= 0x000E && r <= 0x001F) ||
(r >= 0x007F && r <= 0x009F) {
return field.ErrorList{field.Invalid(fldPath, *value, "must not contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F)")}
}
}
return nil
return resources.ValidateStorageVolumeID(fldPath, *value)
}

func ValidateCustom_ExternalVolume_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList {
return resources.ValidateCSIMap(fldPath, value)
}
146 changes: 22 additions & 124 deletions cmd/ateapi/internal/apivalidation/actor_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,8 @@ package apivalidation
import (
"context"
"fmt"
"regexp"
"strings"

"github.com/distribution/reference"

"github.com/agent-substrate/substrate/internal/resources"
"github.com/agent-substrate/substrate/internal/volumepath"
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/resource"
Expand Down Expand Up @@ -85,91 +80,40 @@ func ValidateCustom_CreateActorTemplateRequest_ActorTemplate(_ context.Context,
return errs
}

// httpGetPathRE constrains wakeup probe paths to RFC 3986 path-segment
// characters only, with well-formed percent-escapes, and no query string
// or fragment.
var httpGetPathRE = regexp.MustCompile(`^/([A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$`)

func ValidateCustom_HTTPGetAction_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
if !httpGetPathRE.MatchString(*value) {
return field.ErrorList{field.Invalid(fldPath, *value, "must be a URL path starting with '/', using only RFC 3986 path-segment characters, without query or fragment")}
}
return nil
return resources.ValidateHTTPGetPath(fldPath, *value)
}

// mountPathBadSegmentRE matches '.' or '..' path segments.
var mountPathBadSegmentRE = regexp.MustCompile(`(^|/)[.][.]?(/|$)`)

// ValidateCustom_VolumeMount_MountPath requires a clean absolute Unix path
// that starts with '/', is not '/', and contains no ':', '.' or '..'
// segments, '//', trailing '/', or control characters.
func ValidateCustom_VolumeMount_MountPath(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
p := *value
bad := !strings.HasPrefix(p, "/") || len(p) == 1 ||
strings.HasSuffix(p, "/") || strings.Contains(p, "//") ||
strings.Contains(p, ":") || mountPathBadSegmentRE.MatchString(p)
if !bad {
for _, r := range p {
if r < 0x20 || r == 0x7f {
bad = true
break
}
}
}
if bad {
return field.ErrorList{field.Invalid(fldPath, p, "must be a clean absolute Unix path: must start with '/', not be '/', and contain no ':', '..', '.', '//', trailing '/', or control characters")}
}
return nil
return resources.ValidateMountPath(fldPath, *value)
}

// ValidateCustom_Container_VolumeMounts rejects mounts that nest under one
// another.
// another. Mount-path uniqueness is enforced by the list key.
func ValidateCustom_Container_VolumeMounts(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateapipb.VolumeMount) field.ErrorList {
var errs field.ErrorList
paths := make([]string, len(value))
for i, m := range value {
path := m.GetMountPath()
if path == "" {
continue // required is enforced by tags
}
// Nested mounts are unsupported (volumes cannot mount onto
// other volumes).
for j := 0; j < i; j++ {
prior := value[j].GetMountPath()
if prior == "" || prior == path {
continue
}
if strings.HasPrefix(path, prior+"/") || strings.HasPrefix(prior, path+"/") {
errs = append(errs, field.Invalid(fldPath.Index(i).Child("mount_path"), path,
fmt.Sprintf("must not nest under or over another mount (%q)", prior)))
}
}
}
return errs
}

// validateProjectedPath applies the projected-path rule shared with atelet,
// which re-checks it before writing to the host.
func validateProjectedPath(fldPath *field.Path, p string) field.ErrorList {
if err := volumepath.ValidateProjected(p); err != nil {
return field.ErrorList{field.Invalid(fldPath, p, err.Error())}
paths[i] = m.GetMountPath()
}
return nil
return resources.ValidateNestedMountPaths(fldPath, paths)
}

func ValidateCustom_ActorMetadataItem_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
return validateProjectedPath(fldPath, *value)
return resources.ValidateProjectedPath(fldPath, *value)
}

func ValidateCustom_TrustBundleDataSource_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
return validateProjectedPath(fldPath, *value)
return resources.ValidateProjectedPath(fldPath, *value)
}

// ValidateCustom_SystemInfoVolumeSource_DataSources requires every projected
// file path to be unique across all data sources: atelet writes them in
// order into one tree, so a repeated path silently clobbers the earlier file.
// ValidateCustom_SystemInfoVolumeSource_DataSources allows at most one
// actor_metadata entry and requires every projected file path to be unique
// across all data sources: atelet writes them in order into one tree, so a
// repeated path silently clobbers the earlier file.
func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateapipb.SystemInfoDataSource) field.ErrorList {
var errs field.ErrorList
seen := sets.New[string]()
sawMetadata := false
for i, ds := range value {
switch {
case ds == nil:
Expand All @@ -179,6 +123,10 @@ func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ oper
}
seen.Insert(ds.TrustBundle.Path)
case ds.ActorMetadata != nil:
if sawMetadata {
errs = append(errs, field.Forbidden(fldPath.Index(i).Child("actor_metadata"), "at most one actor_metadata entry may appear"))
}
sawMetadata = true
for j, item := range ds.ActorMetadata.Items {
if item == nil {
continue
Expand All @@ -193,31 +141,12 @@ func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ oper
return errs
}

// validatePinnedImage requires a well-formed OCI image reference pinned by
// digest (e.g. "name@sha256:..."): changing the image content under a fixed
// reference invalidates snapshots. It parses with the same grammar the
// container runtimes use, so a malformed digest is rejected rather than
// treated as pinned.
func validatePinnedImage(fldPath *field.Path, value string) field.ErrorList {
if value == "" {
return nil // required is enforced by tags
}
ref, err := reference.ParseNormalizedNamed(value)
if err != nil {
return field.ErrorList{field.Invalid(fldPath, value, fmt.Sprintf("must be a well-formed image reference: %v", err))}
}
if _, ok := ref.(reference.Digested); !ok {
return field.ErrorList{field.Invalid(fldPath, value, "must be pinned by digest (changing the image invalidates snapshots)")}
}
return nil
}

func ValidateCustom_ImageVolumeSource_Reference(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
return validatePinnedImage(fldPath, *value)
return resources.ValidatePinnedImage(fldPath, *value)
}

func ValidateCustom_Container_Image(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
return validatePinnedImage(fldPath, *value)
return resources.ValidatePinnedImage(fldPath, *value)
}

func ValidateCustom_ExternalVolumeTemplate_Capacity(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
Expand Down Expand Up @@ -246,45 +175,14 @@ func ValidateCustom_SnapshotConfig_PreferredFidelity(_ context.Context, _ operat
return nil
}

// envVarNameRE constrains env var names to any printable ASCII character
// except '='.
var envVarNameRE = regexp.MustCompile(`^[ -<>-~]+$`)

func ValidateCustom_EnvVar_Name(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
if !envVarNameRE.MatchString(*value) {
return field.ErrorList{field.Invalid(fldPath, *value, "may contain any printable ASCII character except '='")}
}
return nil
}

// capabilityRE constrains Linux capability names: uppercase, without the
// "CAP_" prefix (which is added when the OCI spec is written; the prefixed
// spelling would silently grant nothing).
var capabilityRE = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`)

func validateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList {
var errs field.ErrorList
for i, c := range caps {
p := fldPath.Index(i)
switch {
case c == "ALL" && !allowAll:
errs = append(errs, field.Invalid(p, c, "add does not accept 'ALL'; name the individual capabilities the container needs"))
case c == "ALL":
case len(c) > 63:
errs = append(errs, field.TooLong(p, nil, 63))
case strings.HasPrefix(c, "CAP_"):
errs = append(errs, field.Invalid(p, c, "must be named without the 'CAP_' prefix (e.g. 'NET_BIND_SERVICE')"))
case !capabilityRE.MatchString(c):
errs = append(errs, field.Invalid(p, c, "must be an uppercase capability name like 'NET_BIND_SERVICE'"))
}
}
return errs
return resources.ValidateEnvVarName(fldPath, *value)
}

func ValidateCustom_Capabilities_Add(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList {
return validateCapabilities(fldPath, value, false)
return resources.ValidateCapabilities(fldPath, value, false)
}

func ValidateCustom_Capabilities_Drop(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList {
return validateCapabilities(fldPath, value, true)
return resources.ValidateCapabilities(fldPath, value, true)
}
Loading
Loading