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
- Variables
- func CheckMountPath(rel string) error
- func CheckMountTarget(target string) error
- func ConductorMayRead(name, owner string) bool
- func IsPersonalMCPOAuthSecret(name string) bool
- func IsPersonalMCPSecret(name string) bool
- func IsPersonalModelKeySecret(name string) bool
- func ModalImage(image string) (string, bool)
- func NormalizeCapability(c string) string
- func NormalizeRegistryHost(host string) string
- func PersonalMCPOAuthSecretName(server string) string
- func PersonalMCPSecretName(server string) string
- func PersonalModelKeySecretName(provider string) string
- func PersonalModelRefreshSecretName(provider string) string
- func RegistryHost(image string) (string, error)
- func URLEnv(name string) string
- func ValidateRegistryHost(host string) (string, error)
- func WorkspaceConversation(key string) (string, bool)
- func WorkspacePlaybookKey(session, playbook string) string
- type BoatSandbox
- type Duration
- type Expose
- type ExposedPort
- type FileMount
- type Hardening
- type ModalSandbox
- type Readiness
- type Resources
- type Sandbox
- type SecretRef
- type Sidecar
- type TaskSpec
Constants ¶
const ( DefaultWorkingDir = "/workspace" DefaultTimeout = time.Hour DefaultMaxAttempts = 1 DefaultPIDs = 4096 DefaultReadinessTimeout = time.Minute DefaultHTTPPort = 80 DefaultExposeTTL = time.Hour )
Defaults applied by ApplyDefaults.
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.
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.
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.
const ConductorReservedPrefix = "podium.agent."
ConductorReservedPrefix is the conductor's own namespace. A personal secret cannot take it.
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.
const DefaultSecretTarget = SecretTargetEnv
DefaultSecretTarget is what ApplyDefaults fills an unset target with.
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.
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.
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.
const LabelSandboxProvider = "sandbox.provider="
LabelSandboxProvider is the label a sandbox node carries, followed by its provider's name.
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.
const ReservedSidecarName = "task"
ReservedSidecarName is the network alias the task container itself answers to, so no sidecar may take it.
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.
const SpecDocs = "docs/task-spec.md"
SpecDocs is where a validation error points a reader who wants the whole picture.
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.
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 ¶
var ( BoatTypes = []string{"small", "default", "large", "xlarge"} ModalRuntimes = []string{"gvisor", "vm"} )
BoatTypes and ModalRuntimes are the values those fields take.
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.
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.
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
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
CheckMountTarget reports whether target is a directory a FileMount may be mounted at.
func ConductorMayRead ¶ added in v0.6.0
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
IsPersonalMCPOAuthSecret reports whether name is a personal MCP sign-in's secrets.
func IsPersonalMCPSecret ¶ added in v0.6.0
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
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
ModalImage is the Modal image name or ID an image reference names, if it names one.
func NormalizeCapability ¶
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 ¶
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
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
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
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
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 ¶
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
URLEnv is the variable a task reads a port's URL from: PODIUM_URL_ and the name, uppercased, with dashes as underscores.
func ValidateRegistryHost ¶
ValidateRegistryHost normalises host and rejects anything that could not be the domain of an image reference.
func WorkspaceConversation ¶ added in v0.8.0
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
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 ¶
Duration is a time.Duration that marshals to and from a Go duration string ("30s", "1h") in both YAML and JSON.
func (Duration) MarshalJSON ¶
MarshalJSON encodes the duration as a string.
func (Duration) MarshalYAML ¶
MarshalYAML encodes the duration as a string.
func (*Duration) UnmarshalJSON ¶
UnmarshalJSON accepts a duration string ("30s") or a raw nanosecond count.
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.
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.
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
ProviderName is the provider this sandbox is pinned to, empty for none — or for no sandbox.
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"`
// 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 ParseTaskSpec ¶
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 ¶
Images is every image reference a spec pulls: the task's own and each sidecar's.
func (*TaskSpec) UsesSandbox ¶ added in v0.7.0
UsesSandbox reports whether only a sandbox node can run the task.