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 ConductorMayRead(name, owner string) bool
- func IsPersonalMCPOAuthSecret(name string) bool
- func IsPersonalMCPSecret(name string) bool
- func NormalizeCapability(c string) string
- func NormalizeRegistryHost(host string) string
- func PersonalMCPOAuthSecretName(server string) string
- func PersonalMCPSecretName(server string) string
- func RegistryHost(image string) (string, error)
- func URLEnv(name string) string
- func ValidateRegistryHost(host string) (string, error)
- type Duration
- type Expose
- type ExposedPort
- type Hardening
- type Readiness
- type Resources
- 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 ( ExposeViaTailnet = "tailnet" ExposeViaLAN = "lan" )
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 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 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.
Variables ¶
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 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, and their MCP servers' tokens and sign-ins. 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 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 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.
Types ¶
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 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 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 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"`
// 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"`
}
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.