spec

package
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: Apache-2.0 Imports: 16 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 (
	// MaxMounts is how many directories one task may be given.
	MaxMounts = 16
	// MaxMountFiles is how many files one task may be given, across every mount.
	MaxMountFiles = 1024
	// MaxMountBytes caps the content of every file of every mount, together.
	MaxMountBytes = 80 << 20
	// MaxMountPathLen and MaxMountPathDepth bound one path inside a mount.
	MaxMountPathLen   = 255
	MaxMountPathDepth = 8
)

The caps on Mounts.

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 FilesMount = "/podium/files"

FilesMount is where every FileMount lands. A target anywhere else is refused: a task run as a process has these written by root, so a target is a place root writes to.

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 ModalImagePrefix = "modal:"

ModalImagePrefix marks an image Modal holds rather than a registry: `modal:<name>` is one published with Modal's own SDK (Image.publish), `modal:im-…` one by its ID. Modal builds and stores it, so nothing is pulled from a registry when a sandbox starts — which is also how a task uses an image that lives in a registry Modal cannot reach. Such an image runs only in a Modal sandbox, so a task that names one is placed there.

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.

View Source
const WorkspaceExcludeEnv = "PODIUM_WORKSPACE_EXCLUDE"

WorkspaceExcludeEnv names, in a task's environment, the directories its workspace snapshot leaves out: a comma-separated list of names, matched against every path component. What is in them is rebuilt by the next task — node_modules, build caches — and is most of what a snapshot weighs, stored again for every conversation.

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 CheckMountPath added in v0.7.3

func CheckMountPath(rel string) error

CheckMountPath is the traversal guard for a path inside a mount, or a target below FilesMount: every component an ordinary name.

func CheckMountTarget added in v0.7.3

func CheckMountTarget(target string) error

CheckMountTarget reports whether target is a directory a FileMount may be mounted at.

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, their MCP servers' tokens and sign-ins, and their own model provider credential. 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 IsPersonalModelKeySecret added in v0.8.0

func IsPersonalModelKeySecret(name string) bool

IsPersonalModelKeySecret reports whether name is a person's model provider credential or its refresh token. The conductor may write one of these for a person.

func ModalImage added in v0.8.0

func ModalImage(image string) (string, bool)

ModalImage is the Modal image name or ID an image reference names, if it names 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 PersonalModelKeySecretName added in v0.8.0

func PersonalModelKeySecretName(provider string) string

PersonalModelKeySecretName is where one person keeps their own credential for a model provider — anthropic, openai, xai, google: an API key, or the access token of their own subscription sign-in. A person sets it under Settings → Models (Yours), or by hand on the Secrets page as a personal secret; the conductor writes it for them from Models, and spends it rather than the company's key on the turns that person asks for. A playbook may not name it.

func PersonalModelRefreshSecretName added in v0.8.0

func PersonalModelRefreshSecretName(provider string) string

PersonalModelRefreshSecretName is where one person's subscription sign-in keeps its refresh token. The conductor writes and reads it; it never reaches a turn.

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.

func WorkspaceConversation added in v0.8.0

func WorkspaceConversation(key string) (string, bool)

WorkspaceConversation is the conversation a playbook's workspace key belongs to, when key is one. A conversation's workspace was keyed by the conversation alone before each playbook kept its own, so a playbook workspace with no snapshot yet continues from that one.

func WorkspacePlaybookKey added in v0.8.0

func WorkspacePlaybookKey(session, playbook string) string

WorkspacePlaybookKey is the WorkspaceSession of a playbook's own workspace in a conversation: each playbook of a conversation keeps one.

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 FileMount added in v0.7.3

type FileMount struct {
	// Target is the absolute directory it is mounted at, under FilesMount.
	Target string `yaml:"target" json:"target"`
	// Files maps a path relative to Target onto that file's content. Text only: nothing
	// here is executable, and nothing can be a link.
	Files map[string]string `yaml:"files" json:"files"`
}

FileMount is one read-only directory a task gets, carried whole on the spec.

It is how content too large for an environment variable reaches a task: Linux caps a single environment string at MAX_ARG_STRLEN, 128 KiB, and past it the container cannot exec at all. A file has no such cap, so the limit here is MaxMountBytes, which is about what a spec can reasonably carry through the control plane.

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) ProviderName added in v0.8.0

func (s *Sandbox) ProviderName() string

ProviderName is the provider this sandbox is pinned to, empty for none — or for no sandbox.

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"`
	// WorkspaceReadOnly restores the session's snapshot and stores nothing when the task
	// exits: what it changed is thrown away with the volume. It requires WorkspaceSession,
	// and it does not publish a base.
	WorkspaceReadOnly bool `yaml:"workspace_readonly,omitempty" json:"workspace_readonly,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"`
	// Mounts are directories of text files the node writes before the task starts and
	// mounts read-only. See FileMount.
	Mounts []FileMount `yaml:"mounts,omitempty" json:"mounts,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