spec

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package spec defines the public task specification: the YAML/JSON document an operator writes and the validated Go type every Podium component codes against.

Index

Constants

View Source
const (
	DefaultWorkingDir       = "/workspace"
	DefaultTimeout          = time.Hour
	DefaultMaxAttempts      = 1
	DefaultPIDs             = 4096
	DefaultReadinessTimeout = time.Minute
	DefaultHTTPPort         = 80
	DefaultExposeTTL        = time.Hour
)

Defaults applied by ApplyDefaults.

View Source
const (
	ExposeViaTailnet = "tailnet"
	ExposeViaLAN     = "lan"
	// ExposeViaSandbox publishes through the sandbox provider, on URLs anyone holding
	// them can open from the internet.
	ExposeViaSandbox = "sandbox"
)

Ways a preview can be reached from outside its node.

View Source
const (
	// SecretTargetEnv puts the value in an environment variable named by Key.
	SecretTargetEnv = "env"
	// SecretTargetFile mounts the value read-only at the absolute path named by Key.
	SecretTargetFile = "file"
)

Secret targets. A ref says where in the container the value should appear, never what the value is.

View Source
const ConductorReservedPrefix = "podium.agent."

ConductorReservedPrefix is the conductor's own namespace. A personal secret cannot take it.

View Source
const ConnectionSecretPrefix = "podium.agent.connection."

ConnectionSecretPrefix is where the conductor keeps the credentials of its saved connections: the GitHub App's key and secrets, and the Slack tokens.

View Source
const DefaultSecretTarget = SecretTargetEnv

DefaultSecretTarget is what ApplyDefaults fills an unset target with.

View Source
const GitHubAccountSecret = "github.token"

GitHubAccountSecret is the personal secret a person's connected GitHub account is stored as. The conductor may write it for that person.

View Source
const LabelSandbox = "backend=sandbox"

LabelSandbox is the label every sandbox node carries, and every task that needs one gets. A sandbox node takes no task without it.

View Source
const LabelSandboxProvider = "sandbox.provider="

LabelSandboxProvider is the label a sandbox node carries, followed by its provider's name.

View Source
const ReservedSidecarName = "task"

ReservedSidecarName is the network alias the task container itself answers to, so no sidecar may take it.

View Source
const SecretsMount = "/podium/secrets"

SecretsMount is the tmpfs every task container gets for secret files. A file target outside it works, but this is the path the docs use and the only one guaranteed to be writable, non-executable and gone when the container is.

View Source
const SpecDocs = "docs/task-spec.md"

SpecDocs is where a validation error points a reader who wants the whole picture.

View Source
const UserGitTokenSecretPrefix = "podium.agent.git_user_token."

UserGitTokenSecretPrefix is where the conductor keeps the scoped GitHub tokens it issued a turn, so it can revoke them when the turn ends, even after a restart.

Variables

View Source
var (
	BoatTypes     = []string{"small", "default", "large", "xlarge"}
	ModalRuntimes = []string{"gvisor", "vm"}
)

BoatTypes and ModalRuntimes are the values those fields take.

View Source
var AllowedCapabilities = []string{
	"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID", "NET_BIND_SERVICE", "KILL",
}

AllowedCapabilities is the complete set of Linux capabilities a task may add back after the executor drops them all. Anything outside it is rejected at validation: a task that needs more is a node-level operator decision, not a spec field.

View Source
var RegistryHostRE = regexp.MustCompile(`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[0-9]{1,5})?$`)

RegistryHostRE is what a registry host may look like once normalised: a hostname, with an optional port. It is what an image reference's domain part is.

View Source
var SecretNameRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.-]*$`)

SecretNameRE is the shape of a secret name. It is deliberately narrow: a name reaches the node as a filename on the way to a file-target mount, and it is what appears in a redaction marker in stored logs.

Functions

func ConductorMayRead added in v0.6.0

func ConductorMayRead(name, owner string) bool

ConductorMayRead reports whether the conductor may read a value back: a global in its own namespace, or a credential it keeps on a person's list — their connected GitHub account, and their MCP servers' tokens and sign-ins. No person may read any value.

func IsPersonalMCPOAuthSecret added in v0.6.0

func IsPersonalMCPOAuthSecret(name string) bool

IsPersonalMCPOAuthSecret reports whether name is a personal MCP sign-in's secrets.

func IsPersonalMCPSecret added in v0.6.0

func IsPersonalMCPSecret(name string) bool

IsPersonalMCPSecret reports whether name is a personal MCP credential. The conductor may write one of these for a person. A playbook may not name one.

func NormalizeCapability

func NormalizeCapability(c string) string

NormalizeCapability is the spelling the executor passes to Docker: uppercase, with the optional CAP_ prefix stripped, so both "chown" and "CAP_CHOWN" name the same capability.

func NormalizeRegistryHost

func NormalizeRegistryHost(host string) string

NormalizeRegistryHost reduces the ways people spell a registry to the domain an image reference carries: lower-case, no scheme, no path, and Docker Hub's legacy index address folded into "docker.io".

func PersonalMCPOAuthSecretName added in v0.6.0

func PersonalMCPOAuthSecretName(server string) string

PersonalMCPOAuthSecretName is where one person's MCP sign-in keeps its client secret and refresh token. The conductor writes and reads it. A playbook may not name it.

func PersonalMCPSecretName added in v0.6.0

func PersonalMCPSecretName(server string) string

PersonalMCPSecretName is the secret one person's MCP server token is stored as. Two people who both register the same server share this name; scope and owner separate the values.

func RegistryHost

func RegistryHost(image string) (string, error)

RegistryHost is the registry an image reference is pulled from, normalised the same way: "alpine:3" is docker.io, "us-docker.pkg.dev/acme/images/app" is us-docker.pkg.dev.

func URLEnv added in v0.6.0

func URLEnv(name string) string

URLEnv is the variable a task reads a port's URL from: PODIUM_URL_ and the name, uppercased, with dashes as underscores.

func ValidateRegistryHost

func ValidateRegistryHost(host string) (string, error)

ValidateRegistryHost normalises host and rejects anything that could not be the domain of an image reference.

Types

type BoatSandbox added in v0.7.0

type BoatSandbox struct {
	// Type is the machine size: small, default, large or xlarge.
	Type string `yaml:"type,omitempty" json:"type,omitempty"`
	// Template is the named snapshot an image task starts from.
	Template string `yaml:"template,omitempty" json:"template,omitempty"`
	// Org bills the sandbox to this boat organization.
	Org string `yaml:"org,omitempty" json:"org,omitempty"`
}

BoatSandbox is what a task may set about its boat sandbox.

type Duration

type Duration time.Duration

Duration is a time.Duration that marshals to and from a Go duration string ("30s", "1h") in both YAML and JSON.

func (Duration) MarshalJSON

func (d Duration) MarshalJSON() ([]byte, error)

MarshalJSON encodes the duration as a string.

func (Duration) MarshalYAML

func (d Duration) MarshalYAML() (any, error)

MarshalYAML encodes the duration as a string.

func (Duration) Std

func (d Duration) Std() time.Duration

Std returns the underlying time.Duration.

func (Duration) String

func (d Duration) String() string

String renders the duration the way time.Duration does.

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts a duration string ("30s") or a raw nanosecond count.

func (*Duration) UnmarshalYAML

func (d *Duration) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML accepts a duration string ("30s").

type Expose added in v0.6.0

type Expose struct {
	TTL Duration `yaml:"ttl,omitempty" json:"ttl,omitempty"`
	// Via is ExposeViaTailnet or ExposeViaLAN; empty lets the node choose.
	Via   string                 `yaml:"via,omitempty" json:"via,omitempty"`
	Ports map[string]ExposedPort `yaml:"ports,omitempty" json:"ports,omitempty"`
}

Expose is a task that stays up as a preview once its command has exited. Every port is published on one address per task under its own number, so an app built to call localhost:5011 from localhost:3000 works unchanged once it is told the address.

With no ports it only holds: the containers stay up for the ttl and nothing is published. That is for a task that publishes itself, through a tunnel it opens, say, which needs its processes to outlive the command and no address from the node.

func (*Expose) Publishes added in v0.6.0

func (x *Expose) Publishes() bool

Publishes reports whether the node has to publish anything: false for a hold-only expose.

type ExposedPort added in v0.6.0

type ExposedPort struct {
	Port int    `yaml:"port" json:"port"`
	From string `yaml:"from,omitempty" json:"from,omitempty"`
}

ExposedPort is one published port. From names the sidecar listening on it; empty is the task container.

type Hardening

type Hardening struct {
	ReadOnlyRootfs bool     `yaml:"read_only_rootfs,omitempty" json:"read_only_rootfs,omitempty"`
	Capabilities   []string `yaml:"capabilities,omitempty" json:"capabilities,omitempty"`
}

Hardening relaxes or tightens the task container's sandbox. The defaults — every capability dropped, no new privileges, the engine's seccomp profile — are not negotiable; these two fields are the only dials.

type ModalSandbox added in v0.7.0

type ModalSandbox struct {
	// Runtime is gvisor or vm; vm has Docker, for sidecars.
	Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"`
	// Environment is the Modal environment the sandbox is made in.
	Environment string `yaml:"environment,omitempty" json:"environment,omitempty"`
}

ModalSandbox is what a task may set about its Modal sandbox.

type Readiness

type Readiness struct {
	TCPPort  int      `yaml:"tcp_port,omitempty" json:"tcp_port,omitempty"`
	HTTPPath string   `yaml:"http_path,omitempty" json:"http_path,omitempty"`
	HTTPPort int      `yaml:"http_port,omitempty" json:"http_port,omitempty"`
	Command  []string `yaml:"command,omitempty" json:"command,omitempty"`
	Timeout  Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
}

Readiness is how the node decides a sidecar is usable. At most one probe may be set; a sidecar with none is ready as soon as its container is running.

func (Readiness) Probes

func (r Readiness) Probes() int

Probes reports how many readiness probes are configured. Validation allows 0 or 1.

type Resources

type Resources struct {
	CPU      float64 `yaml:"cpu,omitempty" json:"cpu,omitempty"`
	MemoryMB int     `yaml:"memory_mb,omitempty" json:"memory_mb,omitempty"`
	PIDs     int     `yaml:"pids,omitempty" json:"pids,omitempty"`
}

Resources caps one container. The node applies them blindly; refusing a task that asks for more than a node has is the scheduler's job.

type Sandbox added in v0.7.0

type Sandbox struct {
	Snapshot string `yaml:"snapshot,omitempty" json:"snapshot,omitempty"`
	// Provider pins the task to that provider's sandbox nodes. Empty is any of them.
	Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
	// Boat and Modal override that provider's settings for this task's sandbox, when it
	// runs on one.
	Boat  *BoatSandbox  `yaml:"boat,omitempty" json:"boat,omitempty"`
	Modal *ModalSandbox `yaml:"modal,omitempty" json:"modal,omitempty"`
}

Sandbox is a task's cloud sandbox. With a Snapshot the task's command runs directly in a sandbox made from that provider snapshot, and Image is not used.

func (*Sandbox) Settings added in v0.7.0

func (s *Sandbox) Settings(provider string) map[string]string

Settings is what the task overrides of provider's settings, under the keys the provider's adapter reads. Empty when it overrides nothing there.

type SecretRef

type SecretRef struct {
	Name   string `yaml:"name" json:"name"`
	Target string `yaml:"target,omitempty" json:"target,omitempty"`
	Key    string `yaml:"key" json:"key"`
	// Owner is the login a personal secret belongs to. Empty means the secret is global.
	// A stored task from before this field existed has an empty owner and still resolves
	// as global.
	Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`
}

SecretRef names a stored secret and says where the task wants it. It carries the name of a value, never the value: the server resolves it immediately before assignment.

type Sidecar

type Sidecar struct {
	Image     string            `yaml:"image" json:"image"`
	Command   []string          `yaml:"command,omitempty" json:"command,omitempty"`
	Env       map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
	Readiness Readiness         `yaml:"readiness,omitempty" json:"readiness,omitempty"`
	Resources Resources         `yaml:"resources,omitempty" json:"resources,omitempty"`
	// Privileged runs the sidecar with every capability and no device restriction, which
	// is root on the node's kernel. It exists for one thing — a Docker daemon beside the
	// task, so a turn can run `docker compose` and testcontainers — and the spec only
	// *asks*: a node started without --allow-privileged-sidecars refuses the task at
	// provisioning. Validation lets it through on purpose, because whether a machine will
	// host such a container is an operator's answer and not a parser's.
	Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"`
	// ShareWorkspace mounts the task's workspace volume in the sidecar at the same path
	// the task sees it. A nested daemon resolves a bind-mount source in its OWN
	// filesystem, so a `docker run -v /workspace/...` or a build context under /workspace
	// only works when the daemon sees that path too.
	ShareWorkspace bool `yaml:"share_workspace,omitempty" json:"share_workspace,omitempty"`
}

Sidecar is a sibling container started before the task and reachable from it by the name it is keyed under, which becomes its DNS alias on the task network.

type TaskSpec

type TaskSpec struct {
	Image       string             `yaml:"image" json:"image"`
	Command     []string           `yaml:"command,omitempty" json:"command,omitempty"`
	WorkingDir  string             `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
	Env         map[string]string  `yaml:"env,omitempty" json:"env,omitempty"`
	Secrets     []SecretRef        `yaml:"secrets,omitempty" json:"secrets,omitempty"`
	Sidecars    map[string]Sidecar `yaml:"sidecars,omitempty" json:"sidecars,omitempty"`
	Resources   Resources          `yaml:"resources,omitempty" json:"resources,omitempty"`
	Hardening   Hardening          `yaml:"hardening,omitempty" json:"hardening,omitempty"`
	Labels      []string           `yaml:"labels,omitempty" json:"labels,omitempty"`
	Timeout     Duration           `yaml:"timeout,omitempty" json:"timeout,omitempty"`
	MaxAttempts int                `yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"`
	// RetryOnNodeLoss requeues the task as a new attempt when the node running it goes
	// offline mid-run, instead of marking it lost. It is off by default because a task
	// that is not idempotent must not be silently run twice.
	RetryOnNodeLoss bool `yaml:"retry_on_node_loss,omitempty" json:"retry_on_node_loss,omitempty"`
	// WorkspaceSession is the conversation this volume belongs to. Empty keeps the volume
	// ephemeral: nothing is restored and nothing is uploaded.
	WorkspaceSession string `yaml:"workspace_session,omitempty" json:"workspace_session,omitempty"`
	// WorkspaceRepo names the repository whose base snapshot may seed a session that has
	// none of its own. With WorkspacePublishBase it is also where this task's workspace
	// is published.
	WorkspaceRepo string `yaml:"workspace_repo,omitempty" json:"workspace_repo,omitempty"`
	// WorkspacePublishBase stores this task's workspace as the shared base for
	// WorkspaceRepo. It requires WorkspaceRepo.
	WorkspacePublishBase bool `yaml:"workspace_publish_base,omitempty" json:"workspace_publish_base,omitempty"`
	// WorkspaceWarm is how long a runtime should stay up after a turn, waiting for the
	// next instruction. Zero means leave when the turn ends. The node publishes it as
	// PODIUM_WORKSPACE_WARM_SECONDS; the runtime is what waits.
	WorkspaceWarm Duration `yaml:"workspace_warm,omitempty" json:"workspace_warm,omitempty"`
	// Expose keeps the task's containers up after its command exits and publishes the named
	// ports outside the node, so people can use what the task built. See docs/task-spec.md.
	Expose *Expose `yaml:"expose,omitempty" json:"expose,omitempty"`
	// Sandbox runs the task in a cloud sandbox instead of a container on the node. See
	// docs/task-spec.md.
	Sandbox *Sandbox `yaml:"sandbox,omitempty" json:"sandbox,omitempty"`
}

TaskSpec is what a task runs. It mirrors podium.v1.TaskSpec on the wire.

func FromProto

func FromProto(p *podiumv1.TaskSpec) *TaskSpec

FromProto converts a wire TaskSpec back to the public type.

func ParseTaskSpec

func ParseTaskSpec(r io.Reader) (*TaskSpec, error)

ParseTaskSpec decodes a YAML task spec, applies defaults and validates it.

func (*TaskSpec) ApplyDefaults

func (s *TaskSpec) ApplyDefaults()

ApplyDefaults fills unset fields. It never overwrites a value the caller set, so an invalid one (a negative timeout, say) survives for Validate to reject.

func (*TaskSpec) Images

func (s *TaskSpec) Images() []string

Images is every image reference a spec pulls: the task's own and each sidecar's.

func (*TaskSpec) ToProto

func (s *TaskSpec) ToProto() *podiumv1.TaskSpec

ToProto converts a TaskSpec to its wire form.

func (*TaskSpec) UsesSandbox added in v0.7.0

func (s *TaskSpec) UsesSandbox() bool

UsesSandbox reports whether only a sandbox node can run the task.

func (*TaskSpec) Validate

func (s *TaskSpec) Validate() error

Validate reports every problem with the spec at once.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL