Documentation
¶
Overview ¶
ceiling.go is about the difference between what a run asks for and what it is allowed to have.
Until this file existed cloop had only the first vocabulary. ResourceLimits is a *request*, and the drivers resolve it against the operator's config with one rule, stated in pkg/executor/container/container.go:
// Spec limits override the executor's configured defaults: the per-run // request is more specific than the per-executor policy.
That is the right rule when the request comes from the operator, and the wrong one when it does not. A project's resource request is read from .cloop/sandbox.yaml — a file committed to the repository — so "the per-run request" is authored by whoever can push to that repo. Under the override rule such a file could name `memory: 900g` and receive it: the only thing in its way was config.ContainerMemoryMBUpper, a 1 TiB constant sized to catch a typo rather than a tenant. `executors.container.memory: 2g` did not bound it, because that setting was never a bound. It was a default.
So an operator had no way to write down the sentence this file exists to make expressible: *no workload on this executor gets more than this, whatever it asks for.*
Ceilings compose by getting tighter ¶
There are two of them, and they answer different questions:
- the fleet ceiling (config `executors.limits`) — what any workload on this hub may have;
- the project ceiling (control-plane, operator-set) — what this one project may have, which is how a single noisy project gets held below the fleet's allowance without holding everything else down with it.
Neither is a request, so neither can raise anything. Apply them in sequence and the result is the minimum of the request and every ceiling that spoke, with each reduction attributed to the ceiling that caused it — which is the only way the developer reading "your task was given 2 GB" can find out who to ask for more.
ceilingpolicy.go holds the fleet-wide resource ceiling for the process.
It is the same shape as the min-agent-build switch in agentbuild.go, and for the same reason. A control plane reads many projects' config.yaml — every project it serves has one, and each is a file inside a repository that the project's own developers control. Installing those settings symmetrically would let one tenant's file raise a cap that applies to every other tenant in the process, which is not a configuration mistake but a privilege escalation with a YAML interface.
So the switch only ever tightens. Loosening a fleet ceiling means restarting with the looser config: deliberate, explicit, and performed by whoever controls the hub rather than by whoever controls a repository.
Package executor defines cloop's pluggable execution backend abstraction.
Historically the Web UI spawned harness processes directly on the host with raw exec.Command. That hard-wired three assumptions that no longer hold as cloop moves toward a hostable, multi-tenant product:
- the machine running the control plane is also the machine running the agent (no remote or edge executors),
- an agent run has the same privileges, filesystem, and network as the UI server itself (no isolation boundary), and
- process lifecycle is expressible only as an *os.Process (no container, no sandbox, no RPC to a remote agent).
This package replaces those assumptions with a driver interface. A driver ("executor") knows how to start a process-shaped workload somewhere, stream its output back, signal it, and report its status. Where "somewhere" is — this host, a container on this host, a sandbox on a remote edge device — is the driver's business, not the caller's.
Drivers live in sub-packages (pkg/executor/localprocess is the first) and register themselves into a Registry. Callers never construct a driver directly; they call Resolve(projectPath) and get whichever executor that project is bound to.
Security model: Resolve deliberately fails closed. When a project is bound to an executor that is not currently registered, Resolve returns ErrExecutorNotFound rather than silently falling back to the local host. A project pinned to an isolated executor must never be downgraded to host execution because the isolated backend happened to be unreachable.
workspace.go describes how a workload's source tree gets *into* the place the workload runs.
Until this file existed, cloop had no answer to that question for any executor that does not share the control plane's filesystem. The container driver worked because it bind-mounts a host path; every other driver started the harness in a directory that was created and then left empty — the Kubernetes driver mounted an emptyDir at /workspace and its own comment conceded that "the workload is expected to populate it", and the remote agent MkdirAll'd a directory beneath its root. Nothing populated either. The result was a run that started cleanly, produced a plausible-looking transcript, and operated on no code at all.
So a Spec now says which of the three possible answers applies:
- bind: the tree is already at WorkDir because the executor shares the control plane's filesystem. Nothing to do, and nothing may be done — cloning over a bind mount would rewrite the operator's own checkout.
- git: the executor must fetch the tree before the harness starts.
- none: the workload genuinely wants an empty directory (a smoke test, a scratch build). Stated, so it cannot be confused with the bug.
Credentials ¶
A private repository needs authentication, and the whole point of the secret broker is that the credential is short-lived, narrowly scoped, and never durable on the executor. Two rules follow, and they shape every function here:
- the credential is never part of a Spec. A Spec is persisted (see pkg/executorstore, which records the dispatched spec), logged, and shipped across the remote-executor boundary. Workspace names a *grant*; the driver leases the material at dispatch time and carries it out of band. There is deliberately no field on this struct that could hold a token, so no future caller can put one there.
- the credential never reaches a command line or a file. It travels in the environment of the single git invocation that talks to the remote, as an http.<base>.extraHeader — which is what GitHub's own actions/checkout does, for the same reason: argv is world-readable through /proc and a credential file outlives the process that needed it.
writeback.go is the other half of workspace.go: how the work a harness produced gets *out* of the place it ran.
Until this file existed there was no answer. RunResult carried stdout and stderr and nothing else, and both isolated drivers discard their filesystem when the workload ends — a Kubernetes Pod's /workspace is an emptyDir that dies with the Pod, a remote agent's tree stays on a device the hub cannot read. So a task that ran on a non-local executor produced a transcript describing edits that no longer exist anywhere. The executor architecture could dispatch work but could not deliver it.
Both delivery paths here are git-native, for one reason: the hub already has a merge story for parallel work (pkg/worktree plus pkg/mergequeue, Task 20136), and remote work has to arrive in a shape that story accepts. That means a *branch at a commit*, not a diff — so both paths converge on "sandbox commits to a per-task branch" and differ only in how the objects travel:
- push: the sandbox pushes the branch to the project's own origin with the same brokered credential that cloned it, and the hub fetches and verifies the branch is at the SHA the sandbox reported. Preferred, because the objects never pass through the control plane at all.
- bundle: the sandbox writes `git bundle` of base..branch and streams it back over the remote protocol. For sandboxes with no egress, where a push is not merely disabled but impossible.
The trust boundary ¶
The sandbox runs model-authored code. Everything it sends back is hostile input, and that is true of *both* paths — a compromised sandbox holding a push credential can push a branch containing .git/hooks/post-checkout just as easily as it can put one in a bundle. So the vetting in this file gates the commit range on the way to the merge queue, not the transport: Inspect* is called for pushed branches and applied bundles alike. Treating only the bundle as dangerous would secure the path that requires no credential and leave open the one that does.
The rules are deliberately about *paths and modes*, which is all a git tree can express, and they reuse the containment checks in mount.go rather than restating them — a second implementation of "does this path escape" is a second thing to get wrong, and the one that drifts is always the copy.
Index ¶
- Constants
- Variables
- func AgentBuild(ex Executor) (build string, reports bool)
- func ApplyHostExecutionPolicy(allowed bool)
- func ApplyMinAgentBuild(v string)
- func ApplyResourceCeiling(c ResourceCeiling)
- func AttachSupported(ex Executor) bool
- func AuditWorkspace(ev WorkspaceEvent)
- func Bind(projectPath, executorID string) error
- func BlockedByBuildFloor(ex Executor) (bool, string)
- func BoundCPUs(effective float64, capMillis int) float64
- func BoundLimit(effective, cap int) int
- func CeilingUnenforceable(ex Executor, clamps []Clamp) bool
- func CheckSandboxSupport(ex Executor, req Requirements, projectPath string) error
- func ClampWarnings(clamps []Clamp) []string
- func Cordon(h Health, reason string, at time.Time) (Health, *Transition)
- func DescribeBindings(bindings []SecretBinding) string
- func DeviceNames(devices []HostDevice) []string
- func DeviceWorkDir(workDir string) string
- func Drain(h Health, reason string, at time.Time) (Health, *Transition)
- func Ensure(ex Executor) error
- func EnvKeys(bindings []SecretBinding) []string
- func ForgetHandle(store HandleStore, handleID string)
- func FormatCPUMillis(millis int) string
- func FormatMB(mb int) string
- func GitBaseEnv() []string
- func GitCredentialEnv(w Workspace, c GitCredential) ([]string, error)
- func HostExecutionAllowed() bool
- func InspectWriteBack(branch, commitSHA string, entries []BundleEntry) error
- func IsKernelIsolatedRuntime(name string) bool
- func IsUserspaceKernelRuntime(name string) bool
- func IsVirtualizedRuntime(name string) bool
- func IsolatedIDs() []string
- func IsolatesFromHost(ex Executor) bool
- func MarshalWriteBackSentinel(r WriteBackResult) (string, error)
- func MinAgentBuild() string
- func ObserveProbe(h Health, probeErr error, at time.Time, policy HealthPolicy) (Health, *Transition)
- func RecordHandle(store HandleStore, rec HandleRecord)
- func RedactAttachOutput(r io.Reader, set *redact.Set) io.Reader
- func RedactSecrets(s string, secrets []string) string
- func Register(ex Executor) error
- func RequireRevocable(ex Executor, spec Spec) error
- func ResetResourceCeiling()
- func SecretFileDirs(files []SecretFile) []string
- func SetAllowHostExecution(allowed bool) bool
- func SetBindingLookup(fn func(projectPath string) (string, bool))
- func SetMinAgentBuild(v string) string
- func SetProjectCeilingLookup(fn func(projectPath string) (ResourceCeiling, bool))
- func SetWorkspaceAuditor(f func(WorkspaceEvent))
- func ShortSHA(sha string) string
- func SortedHandles(m map[string][]SecretBinding) []string
- func SupportsRevocation(ex Executor) bool
- func Uncordon(h Health, at time.Time, policy HealthPolicy) (Health, *Transition)
- func ValidateBundleEntry(e BundleEntry) error
- func ValidateCommitSHA(s string) error
- func ValidateDevices(devices []HostDevice) error
- func ValidateHostDevice(d HostDevice) error
- func ValidateHostMounts(mounts []HostMount) error
- func ValidateSecretFiles(files []SecretFile) error
- func ValidateSpecMounts(mounts []SpecMount) error
- func ValidateWriteBackBranch(branch string) error
- func ValidateWriteBackPath(p string) error
- type AttachConn
- type AttachLimiter
- type AttachRequest
- type AttachSessionInfo
- type Attacher
- type BuildReporter
- type BundleEntry
- type Candidate
- type Capabilities
- type Clamp
- type Clock
- type Constraint
- type DevicePermissions
- type EgressScope
- type EgressScopeExplainer
- type EventSink
- type Executor
- type FailoverEvent
- type FailoverHandler
- type GitCredential
- type GitStep
- type Handle
- type HandleRecord
- type HandleStore
- type Health
- type HealthPolicy
- type HealthStore
- type HostDevice
- type HostExecutionDeniedError
- type HostMount
- type Isolation
- type LeaseIndex
- func (li *LeaseIndex) Adopt(rec HandleRecord)
- func (li *LeaseIndex) Bind(handleID string, bindings []SecretBinding)
- func (li *LeaseIndex) Handles(req RevokeRequest) map[string][]SecretBinding
- func (li *LeaseIndex) Holds(leaseID string) bool
- func (li *LeaseIndex) Leases() []string
- func (li *LeaseIndex) MarkUnresolved(handleID, reason string)
- func (li *LeaseIndex) Release(handleID string)
- func (li *LeaseIndex) Unresolved() map[string]string
- func (li *LeaseIndex) UnresolvedError() error
- type Lister
- type LogLine
- type MemoryHandleStore
- type NodeState
- type PlacementError
- type Registry
- func (r *Registry) Bind(projectPath, executorID string) error
- func (r *Registry) Binding(projectPath string) (string, bool)
- func (r *Registry) Default() (Executor, error)
- func (r *Registry) DefaultID() string
- func (r *Registry) Ensure(ex Executor) error
- func (r *Registry) Get(id string) (Executor, error)
- func (r *Registry) IsolatedIDs() []string
- func (r *Registry) List() []Executor
- func (r *Registry) Register(ex Executor) error
- func (r *Registry) Resolve(projectPath string) (Executor, error)
- func (r *Registry) SetBindingLookup(fn func(projectPath string) (string, bool))
- func (r *Registry) SetDefault(id string) error
- func (r *Registry) Unbind(projectPath string)
- func (r *Registry) Unregister(id string)
- type Rejection
- type Requirements
- type ResourceCeiling
- type ResourceLimits
- type RevocationDeniedError
- type RevocationLog
- func (rl *RevocationLog) Fail(leaseID, grantID string, state RevokeState, err error)
- func (rl *RevocationLog) Pending() []RevokeOutcome
- func (rl *RevocationLog) Record(res RevokeOutcome) RevokeOutcome
- func (rl *RevocationLog) Settle(leaseID, grantID string, ack RevokeReport, at time.Time)
- func (rl *RevocationLog) Snapshot() []RevokeOutcome
- type RevokeAction
- type RevokeOutcome
- type RevokeReport
- type RevokeRequest
- type RevokeState
- type Revoker
- type RunResult
- type SecretBinding
- type SecretFile
- type Session
- type SessionStore
- type Signal
- type Spec
- func (s Spec) NeedsSecretFiles() bool
- func (s Spec) Redactor() *redact.Set
- func (s *Spec) RelocateSecrets(from, to string)
- func (s Spec) RevocableSecrets() []SecretBinding
- func (s Spec) SandboxRequirements() Requirements
- func (s Spec) SecretValues() []string
- func (s Spec) Timeout() time.Duration
- func (s Spec) Validate() error
- type SpecMount
- type State
- type Status
- type StreamName
- type Supervisor
- func (sv *Supervisor) Cordon(executorID, reason string) (Health, error)
- func (sv *Supervisor) Drain(executorID, reason string) (Health, error)
- func (sv *Supervisor) Health(executorID string) Health
- func (sv *Supervisor) ProbeOnce(ctx context.Context) []Transition
- func (sv *Supervisor) Start(ctx context.Context) (stop func())
- func (sv *Supervisor) Uncordon(executorID string) (Health, error)
- func (sv *Supervisor) WaitForDrain(ctx context.Context, executorID string, timeout, poll time.Duration) (int, error)
- type SupervisorConfig
- type SupervisorOption
- func WithCandidateSource(fn func() []Candidate) SupervisorOption
- func WithClock(c Clock) SupervisorOption
- func WithEventSink(s EventSink) SupervisorOption
- func WithFailoverHandler(h FailoverHandler) SupervisorOption
- func WithHealthStore(s HealthStore) SupervisorOption
- func WithSessionStore(s SessionStore) SupervisorOption
- type Transition
- type Workspace
- func (w Workspace) BaseURL() string
- func (w Workspace) Describe() string
- func (w Workspace) GitPlan(dir string) ([]GitStep, error)
- func (w Workspace) Host() string
- func (w Workspace) IsZero() bool
- func (w Workspace) NeedsProvisioning() bool
- func (w Workspace) RepoPath() (string, bool)
- func (w Workspace) RequiresCredential() bool
- func (w Workspace) Validate() error
- type WorkspaceAccess
- type WorkspaceCredentialSource
- type WorkspaceEvent
- type WorkspaceEventPhase
- type WorkspaceGrantError
- type WorkspaceKind
- type WriteBack
- type WriteBackFetcher
- type WriteBackMode
- type WriteBackReason
- type WriteBackRejection
- type WriteBackResult
Constants ¶
const ( // CeilingSourceFleet is the hub-wide cap from config `executors.limits`. CeilingSourceFleet = "fleet" // CeilingSourceProject is the per-project cap an operator set on the hub. CeilingSourceProject = "project" )
Ceiling source names. They appear in Clamp.Source, in the warnings surfaced on a run, and in the audit trail, so they are constants rather than literals scattered across the call sites that produce them.
const ( // KindLocalProcess runs workloads as child processes of the cloop // control plane, on the same host, with the same privileges. This is // the legacy behavior and offers no isolation. KindLocalProcess = "localprocess" // KindContainer runs workloads in a container runtime (Docker/Podman) // on the control-plane host. KindContainer = "container" // KindRemoteAgent runs workloads on a remote cloop executor agent that // enrolled itself with this control plane over an outbound connection. KindRemoteAgent = "remote" // KindKubernetes runs each workload as an ephemeral Pod in a Kubernetes // cluster, using a kubeconfig brokered as a short-lived lease rather than // a file on the control-plane host. KindKubernetes = "kubernetes" )
Well-known executor kinds. Kind identifies the *driver implementation*; ID identifies a particular configured instance of that driver.
const ( // MaxWorkspaceRepoLen bounds the clone URL. Real ones are under 200 bytes; // this is generous and still keeps an unbounded string out of an argv, a // label, and an audit row. MaxWorkspaceRepoLen = 2048 // MaxWorkspaceRefLen matches git's own limit on a single refname component // budget closely enough to reject nonsense without rejecting reality. MaxWorkspaceRefLen = 255 // MaxWorkspaceDepth bounds a shallow fetch. Past this the "shallow" is // doing nothing but the request is still a full history walk on the // server, so it is better read as a typo. MaxWorkspaceDepth = 100000 // MaxWorkspaceGrantLen bounds the grant reference. MaxWorkspaceGrantLen = 128 )
Bounds on the fields, all of which arrive from outside this process.
const ( // MaxWriteBackBranchLen bounds the branch name. It becomes a refspec, an // argv element, a label and an audit field. MaxWriteBackBranchLen = 200 // MaxWriteBackMessageLen bounds the commit message. Generous for a subject // plus a body; short enough that it cannot be used as a smuggling channel // for a megabyte of anything. MaxWriteBackMessageLen = 4096 // DefaultWriteBackBundleBytes is the cap applied when a spec names none. // A task's worth of source changes is kilobytes; 32 MiB accommodates a // vendored dependency or a checked-in fixture and still cannot exhaust a // hub's memory, which is what a stream with no ceiling eventually does. DefaultWriteBackBundleBytes int64 = 32 << 20 // MaxWriteBackBundleBytes is the hard ceiling a spec may not raise past. // A spec is derived from repo-committed configuration, so the limit a // project can ask for has to be bounded by one it cannot edit. MaxWriteBackBundleBytes int64 = 128 << 20 // MaxWriteBackCommits bounds how many commits one write-back may carry. // The inspection walks every changed path in every commit; an unbounded // range is an unbounded walk on the control plane. MaxWriteBackCommits = 1000 // MaxWriteBackFiles bounds how many changed paths one write-back may // carry, for the same reason. MaxWriteBackFiles = 20000 // MaxWriteBackPathLen bounds a single path inside the commit range. MaxWriteBackPathLen = 4096 )
Bounds. Every one of these is a ceiling on attacker-influenced input: the sandbox chooses the tree, the branch name and the bundle contents.
const ( ModeAbsent = "000000" ModeFile = "100644" ModeExec = "100755" ModeSymlink = "120000" ModeSubmodule = "160000" ModeTree = "040000" )
Git file modes, as they appear in `git diff --raw` and `git ls-tree`.
const AgentUpgradeProcedure = "cloop executor agent install --upgrade"
AgentUpgradeProcedure is the command that rolls a device forward.
It lives here, at the bottom of the import graph, because three layers name it: this package's placement refusals, the Executors panel, and the CLI's own help. It used to be a string in pkg/ui describing a flag that did not exist, which is how an operator came to run the one command the UI recommended and get an unknown-flag error. One definition, imported upward, is what keeps the advice and the flag from drifting apart again.
The flag now exists and is safe to recommend for a critical host: it verifies the binary before the swap and restores the previous one if the service does not come back. See pkg/executor/install.
const DefaultMaxAttachSessionsPerExecutor = 4
DefaultMaxAttachSessionsPerExecutor bounds simultaneous interactive sessions against one executor.
The ceiling is small on purpose. Each session is a live exec process inside the sandbox holding a pty and a transport, so the resource being protected is the *workload's* machine, not the hub's; a debugging tool that can be pointed at a struggling sandbox forty times over is a denial-of-service primitive against the thing it is meant to diagnose. Four is enough for an operator plus a colleague plus a stale session that has not timed out yet.
const MaxDevices = 16
MaxDevices bounds how many device nodes one workload may be given.
The limit is not about runtime cost — a runtime will happily take hundreds — but about what a spec that asks for this many is describing. Past a handful the sandbox is no longer a confined environment with a piece of hardware in it; and every entry is another cgroup rule whose failure has to be diagnosed.
const MaxHostMounts = 32
MaxHostMounts bounds how many host repositories one lease may open.
It matches secretbroker.MaxLocalRepos rather than MaxSpecMounts because the two limits answer different questions: MaxSpecMounts bounds what a repo-committed file may rearrange, this bounds what a human-issued grant may open. The value is duplicated rather than imported because pkg/secretbroker imports nothing from here and the dependency should not start now.
const MaxRetainedRevocations = 512
MaxRetainedRevocations bounds a driver's revocation log. A control plane runs for months and an operator can revoke as often as they like, so without a ceiling this would grow forever. Completed entries are evicted oldest-first; pending ones never are, because forgetting a pending revocation would silently drop the replay that is the whole point of retaining it.
The log is live state, not the record of what happened. Durability belongs to the audit trail, which the hub writes per outcome — a process restart must not be able to erase the evidence that a credential was withdrawn.
const MaxSecretFileBytes = 256 << 10 // 256 KiB
MaxSecretFileBytes bounds one credential file. Everything a grant produces is a token, a script or a kubeconfig — kilobytes at most. The cap exists so a malformed or hostile secret store cannot make a driver allocate, or a remote frame carry, an unbounded payload.
const MaxSpecMounts = 16
MaxSpecMounts bounds how many mounts one spec may request. The limit is not about resource exhaustion — it is that a spec approaching it has stopped describing a sandbox and started describing a filesystem, and every entry is another chance to shadow something the harness needs.
const MaxWriteBackSentinelBytes = 8 << 10
MaxWriteBackSentinelBytes bounds the encoded line, so a workload cannot use the scanner as an unbounded allocator.
const WriteBackBranchPrefix = "cloop/"
WriteBackBranchPrefix namespaces every branch a sandbox may write back to. See ValidateWriteBackBranch for why the namespace is not optional.
const WriteBackSentinel = "##cloop-writeback-v1##"
WriteBackSentinel prefixes the line a sandbox prints to report its write-back. Chosen to be something no compiler, test runner or harness emits.
Variables ¶
var ( // ErrAttachUnsupported means the driver has no interactive session // capability — it does not implement Attacher. ErrAttachUnsupported = errors.New("executor: attach not supported by this executor") // ErrAttachNoSandbox means the workload has no boundary to enter: it runs // as a process on the hub's own machine. Attaching would be a shell on the // control plane, which is the one thing this package promises never // happens. ErrAttachNoSandbox = errors.New("executor: attach refused: workload runs on the host, not in a sandbox") // ErrAttachReadOnly means the session was opened without write authority // and something tried to send stdin anyway. ErrAttachReadOnly = errors.New("executor: attach session is read-only") // ErrAttachHandleUnknown means the handle is not running on this executor. ErrAttachHandleUnknown = errors.New("executor: attach: unknown handle") // ErrAttachBusy means the per-executor concurrent-session ceiling is // reached. It is a capacity refusal, not an authorization one. ErrAttachBusy = errors.New("executor: attach: too many concurrent sessions on this executor") // ErrAttachClosed means the session ended — the workload exited, the // sandbox was removed, or the transport dropped. ErrAttachClosed = errors.New("executor: attach session closed") )
Attach errors. Callers distinguish them with errors.Is: the hub maps the refusals to 403 and the absences to 501/404, and the CLI prints the remediation that fits.
var ( // ErrHandleNotFound: the handle ID is unknown to this executor. ErrHandleNotFound = errors.New("executor: handle not found") // ErrExecutorNotFound: no executor with the requested ID is registered. ErrExecutorNotFound = errors.New("executor: executor not registered") // ErrAlreadyRegistered: an executor with this ID is already registered. ErrAlreadyRegistered = errors.New("executor: executor already registered") // ErrNoDefault: the registry has no default executor to fall back to. ErrNoDefault = errors.New("executor: no default executor configured") // ErrInvalidSpec: the Spec failed validation. ErrInvalidSpec = errors.New("executor: invalid spec") // ErrInvalidSignal: the requested signal is not one of the known kinds. ErrInvalidSignal = errors.New("executor: invalid signal") // ErrUnsupported: the driver does not implement this operation; check // Capabilities first. ErrUnsupported = errors.New("executor: operation not supported by this executor") // ErrHostExecutionDenied: policy forbids running this workload on the // control-plane host. Reserved for strict no-host-execution mode. ErrHostExecutionDenied = errors.New("executor: host execution denied by policy") // ErrNoPlacement: no registered executor satisfies the task's // requirements. Always carried by a *PlacementError, which names the // unsatisfied constraint; match the sentinel, read the typed error. ErrNoPlacement = errors.New("executor: no executor satisfies placement requirements") // ErrSessionClaimLost: a session was requeued by someone else first. // This is the exactly-once latch reporting that it worked, not a // failure — a supervisor that sees it must do nothing further. ErrSessionClaimLost = errors.New("executor: session claim lost to another requeue") // ErrDrainTimeout: a drain did not reach zero in-flight sessions before // its deadline. ErrDrainTimeout = errors.New("executor: drain timed out with sessions still in flight") )
Sentinel errors. Callers use errors.Is against these; drivers wrap them with %w and add detail.
var AllWriteBackReasons = []WriteBackReason{ WriteBackReasonUnspecified, WriteBackReasonBadBranch, WriteBackReasonBadCommit, WriteBackReasonEmptyRange, WriteBackReasonUnknownBase, WriteBackReasonFsck, WriteBackReasonRefMoved, WriteBackReasonNotDescendant, WriteBackReasonTooManyCommits, WriteBackReasonPath, WriteBackReasonMode, }
AllWriteBackReasons is every classification, for tests that assert the documentation and the dashboards cover the whole set.
var DefaultAttachCommand = []string{"/bin/sh"}
DefaultAttachCommand is what a session runs when the caller named no command.
`sh` rather than `bash`: the sandbox images cloop ships are minimal, and a session that fails with "bash: not found" on an alpine-based image is a worse default than a plainer shell that is always there.
var DefaultRegistry = NewRegistry()
DefaultRegistry is the process-wide registry. Drivers register into it at startup (see pkg/executor/localprocess.Ensure) and the package-level helpers below operate on it.
var ErrBindingsUnresolved = errors.New("executor cannot account for a running workload's secret leases")
ErrBindingsUnresolved reports that an executor is running a workload whose leased material it cannot enumerate, so a revocation aimed at that executor cannot be confirmed.
It is a failure and not a warning. "I do not know whether this credential is still in use" has to reach the operator as something other than the word revoked, or the panel is telling them an incident is closed when it is not.
var ErrRevocationUnsupported = errors.New("executor cannot revoke secret leases")
ErrRevocationUnsupported is returned when a workload carrying revocable credentials is aimed at an executor that cannot give them back.
var ErrWorkspaceGrantMissing = errors.New("executor: no secret grant can authenticate this workspace")
ErrWorkspaceGrantMissing: a git workspace needs a credential the project does not hold. Match the sentinel; read the *WorkspaceGrantError for the detail.
ErrWorkspaceUnavailable: the source tree could not be materialised. It is distinct from a harness failure because the remedy is entirely different — nothing about the task's code is implicated.
var ErrWriteBackRejected = errors.New("executor: write-back rejected by the hub's content policy")
ErrWriteBackRejected: the returned commit range contains something the hub refuses to apply. Match the sentinel; read the *WriteBackRejection for which path and why.
ErrWriteBackUnavailable: the work product could not be recovered from the executor. Distinct from a harness failure — nothing about the task's code is implicated, and the remedy is an operator's, not the model's.
Functions ¶
func AgentBuild ¶ added in v0.0.2
AgentBuild reports the cloop build running on ex's node, and whether ex is a driver that has a build of its own at all.
The second return is the important one. A container or Kubernetes executor runs *this* binary — it has no separate build, and comparing it against the hub would be comparing the hub with itself. Those drivers must never be rejected for build skew, and reporting them as "build unknown" would reject every one of them the moment a floor was configured.
func ApplyHostExecutionPolicy ¶
func ApplyHostExecutionPolicy(allowed bool)
ApplyHostExecutionPolicy installs a configured policy as a ratchet: it can only ever tighten. A config that permits host execution leaves the switch alone; only a config that forbids it moves the switch.
This is what every bootstrap path should call. SetAllowHostExecution is the raw switch, and outside of tests, calling it with true is almost always a bug — the asymmetry exists because the two directions are not symmetric in consequence:
- A control plane manages many projects, each with its own config.yaml. Applying them symmetrically would mean one tenant's permissive config re-enables host execution for the whole process, which is a privilege escalation by way of a file the tenant controls.
- Constructing a second Server (pkg/ui does this per instance, and tests do it constantly) would otherwise silently reset a security control that something earlier had deliberately set.
Loosening is therefore deliberate and explicit: restart the process with a permissive config, which is the same ceremony every other security-relevant setting requires.
func ApplyMinAgentBuild ¶ added in v0.0.2
func ApplyMinAgentBuild(v string)
ApplyMinAgentBuild installs a configured floor as a ratchet: it can only ever rise. A config naming an older floor than the one already in force, or naming none at all, leaves the switch alone.
This is what every bootstrap path should call, for the reason spelled out on ApplyHostExecutionPolicy: a control plane reads many projects' config.yaml, and applying them symmetrically would let one tenant's file lower a fleet-wide security floor for the whole process. Lowering is deliberate and explicit — restart with the looser config, the same ceremony every other security-relevant setting asks for.
An unparseable floor is ignored rather than treated as infinitely high. The safe reading of "I cannot understand this rule" is not "refuse the entire fleet" — that turns a typo into an outage. It is not silent either: config.ValidateExecutors rejects one arriving through `cloop config set`, and config.ExecutorWarnings surfaces a hand-edited one as a banner saying no floor is being enforced, which is the fact an operator must not be left to assume the other way round.
func ApplyResourceCeiling ¶ added in v0.0.2
func ApplyResourceCeiling(c ResourceCeiling)
ApplyResourceCeiling installs c as a ratchet: the result is the tighter of what was already in force and what c asks for, resource by resource.
This is what every bootstrap path should call. A ceiling naming a looser cap than the one already installed, or naming none at all, leaves the switch alone — so the order in which a hub happens to read its projects' config files cannot change the policy it ends up enforcing.
An invalid ceiling is ignored rather than installed. The safe reading of "I cannot understand this rule" is not "refuse every workload on the fleet", which would turn a typo into an outage; config.ValidateExecutorLimits rejects one arriving through `cloop config set`, and config.ExecutorLimitWarnings surfaces a hand-edited one as a banner saying no ceiling is being enforced — the fact an operator must not be left to assume the other way round.
func AttachSupported ¶ added in v0.0.2
AttachSupported reports whether ex could be attached to right now, for a UI that wants to disable a button rather than explain a failure after the click.
func AuditWorkspace ¶
func AuditWorkspace(ev WorkspaceEvent)
AuditWorkspace reports a provisioning event to the installed sink.
Best-effort and non-blocking on the caller's correctness: a sink that panics must not take down a workload that is otherwise fine, so the recover is here rather than in every sink.
func BlockedByBuildFloor ¶ added in v0.0.2
BlockedByBuildFloor reports whether the fleet's minimum agent build forbids dispatching to ex, and the sentence to show an operator when it does.
It exists so the Executors panel and the scheduler cannot disagree. A device that placement silently skips looks identical on the panel to one that is merely idle, and the operator's next move — waiting — is the one thing that will not help. Rendering the *same* decision function the scheduler runs means the card explains the refusal that is actually happening, rather than a second implementation of the rule that will drift from the first.
func BoundCPUs ¶ added in v0.0.2
BoundCPUs is BoundLimit for a core allowance, whose units are the runtimes' fractional cores rather than milli-cores.
func BoundLimit ¶ added in v0.0.2
BoundLimit returns the limit a driver should apply for one resource.
effective is what the driver resolved on its own — the Spec's request if it made one, otherwise the executor's configured default. cap is the ceiling; 0 means there is none.
The two zero cases are different and both matter. A cap of 0 is no ceiling, so effective stands. An *effective* of 0 is "nothing has bounded this yet", which is the case a ceiling exists for: a project that requested nothing on an executor configured with no default would otherwise run unbounded.
func CeilingUnenforceable ¶ added in v0.0.2
CeilingUnenforceable reports that a ceiling bounded this spec but ex cannot hold it to the result.
Remote agents advertise SupportsResourceLimits false — they report the device's capacity for placement and do not confine a workload to a share of it — so a spec dispatched to one carries limits nothing will apply. That is a pre-existing property of the driver, not something a ceiling introduces, but it changes what an operator is entitled to believe: having set a cap, they will assume it holds everywhere. It does not, and this is what lets the caller say so rather than leave them to assume.
func CheckSandboxSupport ¶
func CheckSandboxSupport(ex Executor, req Requirements, projectPath string) error
CheckSandboxSupport verifies that the executor a project is *already bound to* can honour req, and returns a typed error naming the gap if it cannot.
Binding and placement are different questions — Registry.Resolve answers "which executor is this project pointed at", not "which executor should run this" — but they must not answer the capability question differently. So this runs the bound executor through the same reject() the scheduler uses, as a candidate list of one. A constraint added to Select is therefore enforced on the binding path for free, which is the opposite of how the two usually drift.
The error type is chosen by what the operator has to *do*:
- a gap that is really "this workload cannot run beside the control plane" becomes *HostExecutionDeniedError, whose Remediation() already names the isolated executors to bind to instead. The UI renders that verbatim.
- anything else becomes the *PlacementError from reject(), which names the single constraint the bound executor failed.
A nil executor, or a req nothing rejects, returns nil.
func ClampWarnings ¶ added in v0.0.2
ClampWarnings renders clamps as operator-facing lines, deduplicated and ordered so the same set always reads the same way.
Deduplication matters because the fleet and project ceilings can both clamp the same resource to the same number, and telling someone twice that their memory was capped to 2 GB reads as two separate events.
func DescribeBindings ¶
func DescribeBindings(bindings []SecretBinding) string
DescribeBindings names the credentials in a refusal, so the operator reads "the GitHub PAT github-ci" rather than "a secret". A lease ID is the last resort: "lease_7f3a" means nothing to the person who granted "prod-kubeconfig".
func DeviceNames ¶ added in v0.0.2
func DeviceNames(devices []HostDevice) []string
DeviceNames returns the grant-facing names of a device list, in order, for log lines and audit records.
func DeviceWorkDir ¶
DeviceWorkDir maps a control-plane path onto a directory name an executor that does not share the hub's filesystem can place beneath its own root.
The two sides mean different things by a path. The hub's Spec.WorkDir is a real directory on the hub — /var/lib/cloop/projects/api — while a remote agent treats a workload's WorkDir as attacker-controlled input from a control plane that might be compromised: it confines every workload beneath its configured root and refuses an absolute path from outside it. Dispatching the hub's own path unchanged therefore fails every real run to a device enrolled with a --workdir-root, which is the enrollment operators are told to use, and fails with an error about escaping a root nobody asked to escape.
So the caller that builds the spec sends a *relative* name and lets the device decide where it lands. That is the same decision applyWorkspace already makes from SharesHostFilesystem — "my paths mean nothing to you" — and it belongs on the same side of the wire, not in a driver: a driver that silently rewrote an absolute path would also rewrite one a compromised hub aimed at /etc, turning the agent's refusal into a quiet remap.
The name is derived rather than random for one reason: it has to be the same next time. A device given a fresh directory per run re-clones the repository every run, which on a large project over a slow link is the difference between an incremental fetch and a full one. The hash covers the whole path because two projects can share a base name — "api" under two different roots must not collide on the device and pull each other's history.
A relative path is returned unchanged: it is already device-relative. An empty one stays empty and lands on the executor's root.
func Drain ¶
Drain takes a node out of rotation and marks it for retirement. It differs from Cordon only in intent, which the UI and `cloop executor drain` surface: draining implies someone is waiting for in-flight to reach zero.
func EnvKeys ¶
func EnvKeys(bindings []SecretBinding) []string
EnvKeys returns every environment variable name the bindings contributed, deduplicated and sorted.
func ForgetHandle ¶
func ForgetHandle(store HandleStore, handleID string)
ForgetHandle drops rec's row once the workload is terminal. Same best-effort contract as RecordHandle: a row that outlives its workload is swept by the orphan reaper, so failing loudly here would buy nothing.
func FormatCPUMillis ¶ added in v0.0.2
FormatCPUMillis renders a milli-core count as cores when it divides evenly, which is how nearly every real setting is written.
func FormatMB ¶ added in v0.0.2
FormatMB renders a megabyte count the way an operator wrote it, so a value that came from "8g" reads back as "8 GB" rather than "8192 MB".
func GitBaseEnv ¶
func GitBaseEnv() []string
GitBaseEnv returns the environment every step of a plan runs with, credential or not.
It is a closed environment rather than an inherited one. A provisioning step that read the executor's own ~/.gitconfig could pick up a credential helper, an insteadOf rewrite pointing the fetch at another host, or a proxy — all decided by whoever last touched that machine rather than by the grant.
func GitCredentialEnv ¶
func GitCredentialEnv(w Workspace, c GitCredential) ([]string, error)
GitCredentialEnv returns the additional environment for the authenticated step of a plan.
The credential is delivered as a URL-scoped http.extraHeader through git's GIT_CONFIG_COUNT protocol. That is the only delivery path here that satisfies all three constraints at once: nothing is written to disk, nothing appears in argv (which /proc publishes to every process with the same uid), and the header is scoped to the repository's own origin.
The scoping alone is not what keeps the header at its origin — baseGitConfig's refusal to follow redirects is, and the two only work together. The whole block is re-emitted here rather than added to GitBaseEnv's, because GIT_CONFIG_COUNT is a single count for the whole environment: a second block appended after the first overrides the count, and any key the second block did not repeat would be dropped along with it. Emitting one complete block means the credential cannot arrive with the redirect guard silently switched off.
The empty credential.helper entry is not redundant: without it, a helper configured in a location GIT_CONFIG_GLOBAL does not cover could still answer the challenge with a *different* credential, and the fetch would succeed using authority the grant never issued.
func HostExecutionAllowed ¶
func HostExecutionAllowed() bool
HostExecutionAllowed reports whether un-isolated host execution is permitted.
func InspectWriteBack ¶
func InspectWriteBack(branch, commitSHA string, entries []BundleEntry) error
InspectWriteBack vets a whole commit range's worth of changed entries and returns the first refusal, or nil when every entry is acceptable.
It takes the already-extracted entries rather than reading git itself so that this package stays free of I/O — pkg/executor is imported by the edge agent, which must not grow a dependency on a git binary to satisfy a type — and so the rules can be tested exhaustively against a table instead of against a repository.
func IsKernelIsolatedRuntime ¶ added in v0.0.2
IsKernelIsolatedRuntime reports whether name identifies a runtime under which the workload's system calls are served by something other than the host kernel — a guest kernel inside a VM (Kata) or a userspace kernel in the Sentry (gVisor).
This is the property a project asks for when it says "do not let my task's syscalls reach the host kernel directly". It is strictly weaker than IsVirtualizedRuntime, and that is the point: demanding a hypervisor when a userspace kernel would do refuses placement on executors that met the actual requirement, and a refusal nobody needed is still an outage.
runc and crun report false. They are the case this exists to exclude: the container boundary is namespaces, cgroups and seccomp, and a kernel bug is a host bug.
func IsUserspaceKernelRuntime ¶ added in v0.0.2
IsUserspaceKernelRuntime reports whether name identifies gVisor, in either the OCI-runtime or the Kubernetes RuntimeClass spelling.
Recognised: "runsc" (the binary's own name, which is what both `--runtime runsc` and a RuntimeClass called runsc use), "gvisor", any "runsc-*" or "gvisor-*" variant an operator has registered with different Sentry flags (runsc-ptrace, runsc-kvm are the two in the wild), and the containerd shim spelling "io.containerd.runsc.v1".
gVisor is not a hypervisor and this never claims it is; see IsKernelIsolatedRuntime for what it is used to assert.
func IsVirtualizedRuntime ¶
IsVirtualizedRuntime reports whether name identifies a Kata Containers runtime, in either the OCI-runtime or the Kubernetes RuntimeClass spelling.
Recognised: "kata", "katacontainers", any "kata-*" or "kata.*" name (kata-runtime, kata-qemu, kata-clh, kata-fc), and the containerd shim spelling "io.containerd.kata.v2". Everything else, including an empty name, reports false.
func IsolatedIDs ¶
func IsolatedIDs() []string
IsolatedIDs returns the isolating executors registered in the default registry.
func IsolatesFromHost ¶
IsolatesFromHost reports whether ex puts any boundary between the workload and the control-plane host. It is the exported form of isolatesFromHost, for callers outside this package that have to treat the two cases differently — pkg/ui decides from it whether a workload may inherit the hub's own process environment, which is safe only when the workload runs on the hub anyway.
It fails closed the same way: a driver that declares no isolation is not isolated. See isolatesFromHost for why that direction is the safe one.
func MarshalWriteBackSentinel ¶
func MarshalWriteBackSentinel(r WriteBackResult) (string, error)
MarshalWriteBackSentinel renders r as the single line a sandbox prints.
func MinAgentBuild ¶ added in v0.0.2
func MinAgentBuild() string
MinAgentBuild returns the configured minimum agent build, or "" for no floor.
func ObserveProbe ¶
func ObserveProbe(h Health, probeErr error, at time.Time, policy HealthPolicy) (Health, *Transition)
ObserveProbe folds one probe result into h and reports the resulting health plus the transition it caused, if any.
It is a pure function of (health, result, time, policy) with no clock, no I/O, and no locking, which is what lets the state machine be tested exhaustively under a fake clock while the supervisor around it deals with timers and goroutines.
probeErr is nil for a successful probe.
func RecordHandle ¶
func RecordHandle(store HandleStore, rec HandleRecord)
RecordHandle persists rec, reporting failure without propagating it.
Persistence is best-effort by design. A workload that started successfully must not be reported as failed because the state database was momentarily locked: the caller would mark the task failed and retry it, producing the double execution the whole scheduling layer exists to prevent. A lost row degrades to exactly the behaviour that existed before this file — the workload becomes unobservable after a restart — which is strictly better than losing the workload now.
func RedactAttachOutput ¶ added in v0.0.2
RedactAttachOutput wraps r so the workload's own leased credentials never reach the operator's terminal.
It reuses redact.Writer rather than scrubbing each read, because a credential does not respect read boundaries: a 40-character token split across two reads matches neither half, and a per-chunk scrub would pass it through in two pieces that a terminal reassembles perfectly. redact.Writer already holds back a partial tail for exactly this, and routing attach output through it is what makes "the same redaction path as the log stream" true rather than aspirational.
A nil set returns r unchanged, so a driver can call this unconditionally.
func RedactSecrets ¶
RedactSecrets replaces every occurrence of each secret in s with a fixed marker.
Provisioning output is streamed to the run panel and persisted as an artifact, and git is perfectly willing to quote a URL or a header back in an error message. Redacting at the point output leaves the provisioning step is the only place that catches every such path; filtering at the display layer would leave the artifact on disk.
The matching — including the short-value guard that keeps ordinary text from being mangled — lives in pkg/redact, so the harness output path and this one cannot drift apart in what they consider a secret.
func RequireRevocable ¶
RequireRevocable refuses to place spec on ex when the spec carries material that ex cannot take back.
This is the rule the remote driver enforced for its own agents, applied to every driver. A spec with no revocable bindings passes on any executor — the refusal is about credentials, not about drivers — so a deployment that has never configured the secret broker is unaffected.
Drivers that implement Revoker still enforce their own, more specific refusals inside Start (a remote agent checks the negotiated protocol version against the live session). This is the outer gate, and the two are deliberately redundant: a security control with one enforcement point is a security control with a bypass.
func ResetResourceCeiling ¶ added in v0.0.2
func ResetResourceCeiling()
ResetResourceCeiling clears the switch. It exists for tests, which would otherwise leak a ceiling from one case into the next through process state — and because the ratchet means a test cannot undo itself by installing a looser value, which is exactly the property being tested.
func SecretFileDirs ¶
func SecretFileDirs(files []SecretFile) []string
SecretFileDirs returns the distinct directories a set of files needs, in first-appearance order. A driver creates one mount, volume or directory per entry; in practice a lease produces exactly one.
func SetAllowHostExecution ¶
SetAllowHostExecution sets whether workloads may run on the control-plane host with no isolation boundary (the localprocess driver).
It returns the previous value so callers — tests especially — can restore it without keeping a separate copy.
func SetBindingLookup ¶
SetBindingLookup installs the persistent binding resolver on the default registry.
func SetMinAgentBuild ¶ added in v0.0.2
SetMinAgentBuild sets the minimum cloop build a remote agent may be running to receive work, returning the previous value so callers — tests especially — can restore it without keeping a separate copy.
An empty string clears the floor. This is the raw switch; bootstrap paths should call ApplyMinAgentBuild.
func SetProjectCeilingLookup ¶ added in v0.0.2
func SetProjectCeilingLookup(fn func(projectPath string) (ResourceCeiling, bool))
SetProjectCeilingLookup installs the per-project ceiling resolver.
func SetWorkspaceAuditor ¶
func SetWorkspaceAuditor(f func(WorkspaceEvent))
SetWorkspaceAuditor installs the sink provisioning events are reported to.
It is a process-wide hook rather than a field on every driver because the drivers must not depend on the hub's database: pkg/executor is imported by the edge agent, which has no audit store and no business linking one. The control plane installs the sink at bootstrap; anywhere else the events are dropped, which is correct — an agent's local provisioning is audited by the hub that dispatched it.
Passing nil disables auditing.
func ShortSHA ¶
ShortSHA abbreviates a commit SHA the way git does, without pretending a non-SHA is one.
func SortedHandles ¶
func SortedHandles(m map[string][]SecretBinding) []string
SortedHandles returns the keys of Handles in a stable order.
func SupportsRevocation ¶
SupportsRevocation reports whether ex could honour a revocation issued now.
This is the one function the UI and placement should both call. The Executors panel previously answered it with "is this driver remote?", and treated every other driver as revocable on the theory that the hub owns their filesystem — which says nothing about whether any code wipes it. A driver that has not implemented Revoker now reports false, which is what it always was.
func Uncordon ¶
func Uncordon(h Health, at time.Time, policy HealthPolicy) (Health, *Transition)
Uncordon returns an admin-held node to the state its probe history justifies.
It deliberately does not return to ready unconditionally. A node cordoned *because* it was misbehaving, then uncordoned, should come back as degraded or unreachable if its probes are still failing — otherwise uncordon becomes a way to launder a broken node into the schedulable set, and the next task placed on it fails for reasons the operator just told the system to ignore.
func ValidateBundleEntry ¶
func ValidateBundleEntry(e BundleEntry) error
ValidateBundleEntry vets one changed path in a write-back's commit range: the path itself, the file mode, and — for a symlink — where it points.
The three refusals beyond ValidateWriteBackPath are each a way to escape a path check that has already passed:
- a submodule (gitlink) entry names a URL, not a file. Checking it out and running `git submodule update` fetches attacker-chosen code into the tree, and no path rule sees it because the gitlink's own path is benign.
- a symlink whose target escapes the project root turns every *subsequent* legitimate-looking write through that link into a write anywhere on the control plane. The path "config/x" is fine; it is fine right up until "config" is a symlink to /etc.
- an unexpected mode is refused rather than ignored, because the set of things a git tree can hold is small and closed, and "some mode we did not think about" is not a category worth admitting on this boundary.
func ValidateCommitSHA ¶
ValidateCommitSHA checks that s is a full 40-character lowercase hex object name.
Full, not abbreviated: an abbreviation is ambiguous by construction, and the hub compares this value against what it actually fetched. A prefix match would let a sandbox report a SHA that resolves to a different object than the one it pushed.
func ValidateDevices ¶ added in v0.0.2
func ValidateDevices(devices []HostDevice) error
ValidateDevices checks a whole device list: each entry, the count, and the two collision classes that would otherwise be resolved by argv order.
func ValidateHostDevice ¶ added in v0.0.2
func ValidateHostDevice(d HostDevice) error
ValidateHostDevice checks one device grant.
It is exported because three layers need the same answer: the broker when an operator creates a grant (so the mistake is caught at grant time, by the person who can fix it), the drivers before they render a runtime flag, and Spec.Validate on the dispatch path. One definition means a device that passes at grant time cannot be refused two layers down.
func ValidateHostMounts ¶
ValidateHostMounts checks a whole host-mount list: each entry, the list length, and that no two entries claim the same target.
Duplicate targets are rejected for the same reason as in ValidateSpecMounts — the runtimes resolve the collision differently — and additionally because two grants shadowing each other would make "which repository is at /repos/api" depend on grant iteration order.
func ValidateSecretFiles ¶
func ValidateSecretFiles(files []SecretFile) error
ValidateSecretFiles checks a whole set, including the cross-entry invariant no single file can see: two files claiming the same path would mean the workload reads whichever the driver happened to write last.
func ValidateSpecMounts ¶
ValidateSpecMounts checks a whole mount list: each entry individually, the list length, and that no two entries claim the same target.
Duplicate targets are an error rather than last-one-wins because the two drivers resolve the collision differently — the container runtimes take the last -v, the kubelet refuses the Pod — and a spec that works on one executor and is rejected by the other is exactly the portability break this whole feature exists to remove.
func ValidateWriteBackBranch ¶
ValidateWriteBackBranch applies the naming rules a write-back branch must satisfy.
It is stricter than git's own check-ref-format in two ways that matter here. The name may not start with a dash, because it becomes an argv element in a push refspec and git would read it as a flag. And it must live under a "cloop/" prefix, because the hub force-updates this ref when the branch arrives: without a namespace, a sandbox could name its write-back branch "main" and have the hub overwrite the project's trunk with sandbox-authored history before anyone looked at it.
func ValidateWriteBackPath ¶
ValidateWriteBackPath reports whether a repository-relative path may be written by an applied write-back.
The containment rules are mount.go's, reached through the same hasDotDot helper rather than restated, because "does this path escape its root" has one correct answer and two implementations of it is one more than the number that can be kept right. What this adds on top is the git-specific half: the .git directory is not an ordinary path, because writing into it is not writing a file — it is editing the configuration of the program that is about to run.
Types ¶
type AttachConn ¶ added in v0.0.2
type AttachConn interface {
io.ReadWriteCloser
// Resize reports new terminal geometry. It is a no-op — not an error —
// for a session without a TTY, so a caller that always sends the window
// size does not need to know which kind it opened.
Resize(rows, cols uint16) error
}
AttachConn is one live interactive session.
Read yields the sandbox's combined output, already scrubbed of the workload's own leased credentials. Write forwards stdin, or fails with ErrAttachReadOnly. Close is idempotent and must terminate the remote process, not merely detach from it: a session that left a shell running inside the sandbox after the operator closed the tab would be an unattributed process holding the project's credentials.
func NewAttachConn ¶ added in v0.0.2
func NewAttachConn(out io.Reader, in io.WriteCloser, resize func(rows, cols uint16) error, closeFn func() error) AttachConn
NewAttachConn assembles an AttachConn from a driver's pieces.
in may be nil, which is how a read-only session is expressed: Write then fails with ErrAttachReadOnly rather than blocking or silently discarding.
type AttachLimiter ¶ added in v0.0.2
type AttachLimiter struct {
// contains filtered or unexported fields
}
AttachLimiter bounds simultaneous sessions per executor.
Per executor rather than per hub, per project or per user, because the resource at risk is the sandboxed machine: one operator opening four sessions on four different executors costs each of them one, while four operators converging on the one pod that is misbehaving is exactly the pile-on worth stopping. The limiter is also where the hub learns who is attached to what, so it doubles as the live-session inventory the UI and the CLI read.
func NewAttachLimiter ¶ added in v0.0.2
func NewAttachLimiter(max int) *AttachLimiter
NewAttachLimiter returns a limiter admitting at most max sessions per executor. A max below 1 falls back to the default rather than admitting nothing or everything — a misconfigured ceiling should not decide the policy.
func (*AttachLimiter) Acquire ¶ added in v0.0.2
func (l *AttachLimiter) Acquire(info AttachSessionInfo) (string, func(), error)
Acquire reserves a slot on info.ExecutorID and returns the session ID plus a release function. The release is idempotent, so a caller may defer it and also call it on an early error path.
A nil limiter admits everything: the ceiling is a hub concern, and a driver test or an embedded use with no limiter configured should not be silently capped at zero.
func (*AttachLimiter) Count ¶ added in v0.0.2
func (l *AttachLimiter) Count(executorID string) int
Count returns the number of live sessions on one executor.
func (*AttachLimiter) Live ¶ added in v0.0.2
func (l *AttachLimiter) Live() []AttachSessionInfo
Live returns every open session, ordered by session ID, for the inventory.
func (*AttachLimiter) Max ¶ added in v0.0.2
func (l *AttachLimiter) Max() int
Max returns the configured per-executor ceiling.
type AttachRequest ¶ added in v0.0.2
type AttachRequest struct {
// HandleID identifies the running workload, as returned by Start.
HandleID string
// Command is the argv to run inside the sandbox. Empty means the driver's
// default interactive shell.
Command []string
// TTY requests a pseudo-terminal. Without one the session is a pipe: fine
// for `tail -f`, useless for anything that draws.
TTY bool
// Stdin reports that the caller holds write authority and that the driver
// should wire an input channel.
//
// It is a field of the request rather than a property of the returned
// session because it must reach the sandbox: `docker exec` without -i and
// a Kubernetes exec without stdin=true give the remote process no input fd
// at all. A read-only session is therefore read-only *in the sandbox*, not
// merely at the hub — there is no open channel to smuggle bytes down.
Stdin bool
// Rows and Cols are the initial terminal geometry; ignored when !TTY.
Rows, Cols uint16
// Env carries extra environment for the exec'd command (TERM, mainly).
// Entries are "K=V"; a driver that cannot set environment ignores them.
Env []string
}
AttachRequest asks a driver to open an interactive session inside a running workload.
func (AttachRequest) Argv ¶ added in v0.0.2
func (r AttachRequest) Argv() []string
Argv returns the command to run, substituting the default when empty.
func (AttachRequest) CommandLine ¶ added in v0.0.2
func (r AttachRequest) CommandLine() string
CommandLine renders Argv for an audit record and an operator's eyes. It is not shell-quoted for re-execution — it exists to answer "what did they run", and a naive quote would imply a round-trip that is not offered.
type AttachSessionInfo ¶ added in v0.0.2
type AttachSessionInfo struct {
SessionID string `json:"session_id"`
ExecutorID string `json:"executor_id"`
HandleID string `json:"handle_id"`
Actor string `json:"actor"`
Command string `json:"command"`
Writable bool `json:"writable"`
}
AttachSessionInfo describes one live session, for the inventory.
type Attacher ¶ added in v0.0.2
type Attacher interface {
Attach(ctx context.Context, req AttachRequest) (AttachConn, error)
}
Attacher is the optional capability: a driver that can open a session inside a workload it is already running.
Implemented by the three isolating drivers (container, kubernetes, remote) and deliberately not by localprocess — see AttachTarget.
func AsAttacher ¶ added in v0.0.2
AsAttacher reports whether ex can open interactive sessions, mirroring AsRevoker. It does no policy checking; callers want AttachTarget.
func AttachTarget ¶ added in v0.0.2
AttachTarget is the one place that decides whether an executor may be entered.
Order matters, and it is the opposite of the cheap one. The sandbox question is asked *before* the capability question so that a localprocess workload is refused with "this runs on the host" rather than the milder "not supported": the two have the same effect today and very different meanings if someone later teaches localprocess to exec. A refusal that would silently become a grant is not a refusal.
type BuildReporter ¶ added in v0.0.2
type BuildReporter interface {
Executor
// AgentVersion returns the cloop build the connected device advertised,
// or "" when there is no live session to ask.
AgentVersion() string
}
BuildReporter is implemented by drivers whose workload runs on a machine with a cloop binary of its own, and which can therefore report a build distinct from the control plane's.
The interface, rather than a Capabilities bool, for the same reason Revoker is an interface (see revoke.go): a bool in a struct is a claim anybody can set, and the direction the two would drift is a driver asserting a build it cannot substantiate. Only a driver that genuinely has a separate binary to ask about can implement a method that returns its version.
remote.Executor satisfies this already — AgentVersion() has existed since the fleet inventory landed. Nothing had to change on the driver side; what was missing was a reader in the scheduler.
type BundleEntry ¶
type BundleEntry struct {
// Path is the repository-relative path, slash-separated.
Path string
// Mode is the resulting file mode, one of the Mode* constants. ModeAbsent
// means the path was deleted.
Mode string
// LinkTarget is the symlink destination, for Mode ModeSymlink. The caller
// reads it out of the blob; this package decides whether it is allowed.
LinkTarget string
}
BundleEntry is one changed path in a write-back's commit range, as the hub reads it out of git.
type Candidate ¶
type Candidate struct {
// Executor is the driver instance. Select returns the Candidate, so the
// caller gets this back to Start on.
Executor Executor
// Health is the supervisor's current view. A zero value normalizes to
// ready, so a caller with no health store still gets sane placement.
Health Health
// Labels are operator-assigned scheduler selectors (region, tier, owner).
Labels map[string]string
// Harnesses are the agent CLIs available on the node, as detected by
// pkg/executor/agent.Detect ("claude", "codex", "cloop", ...).
Harnesses []string
// ContainerRuntimes are the container runtimes the node can drive.
ContainerRuntimes []string
// MemoryMB is total system memory on the node; 0 means unknown, which is
// never treated as "too small".
MemoryMB int
// InFlight is the number of sessions currently running on the node, and
// InFlightKnown says whether that number is real. A driver that cannot
// enumerate reports InFlightKnown=false, and capacity is then not
// enforced — refusing to schedule because load is unknown would make
// every non-enumerating driver unusable.
InFlight int
InFlightKnown bool
}
Candidate is one executor offered to the scheduler, together with everything placement needs to judge it.
The caller assembles this from the registry, the health store, and (for remote agents) the capabilities the device advertised at enrollment. Bundling it into a value keeps Select pure and keeps this package independent of both storage and the remote protocol.
func Select ¶
func Select(candidates []Candidate, req Requirements) (Candidate, error)
Select chooses the best candidate satisfying req, or returns a *PlacementError naming the constraint that could not be met.
Ranking among satisfying candidates, in order:
- ready before degraded — a node whose probes are landing is a better bet than one that is merely not yet given up on;
- more free capacity first, so a fleet fills evenly instead of piling every task onto whichever node sorts first alphabetically;
- isolated before un-isolated, so a deployment that permits host execution still prefers a sandbox when one is free;
- executor ID, purely so the result is deterministic and tests are not flaky.
type Capabilities ¶
type Capabilities struct {
// Isolation is the strength of the boundary between workload and host.
Isolation Isolation `json:"isolation"`
// Virtualized reports whether the workload runs behind a hypervisor — a
// VM or microVM with a kernel of its own — rather than sharing the
// executing machine's kernel.
//
// It is a separate field rather than a reading of Isolation because the
// two answer different questions and a driver can need both. A Kata pod
// on a remote cluster is IsolationRemote (the machine is not ours) *and*
// virtualized (the kernel is not the node's); collapsing that into one
// enum value would force every such driver to drop one of the two facts,
// and Isolation is explicitly not a total order (see placement.go).
//
// It stays false unless the driver is certain. A requirement to run
// virtualized is checked against this field, so a false positive places a
// workload that must be behind a hypervisor onto one that is not.
Virtualized bool `json:"virtualized,omitempty"`
// KernelIsolated reports whether the workload's system calls are served
// by something other than the executing machine's kernel — a guest
// kernel in a VM, or gVisor's Sentry.
//
// Every virtualized executor is also kernel-isolated; the reverse does
// not hold, and the gap between them is exactly gVisor. Before this
// field existed a gVisor sandbox advertised the same capabilities as a
// runc one, so the stronger boundary an operator had paid for was
// invisible to placement and a project could not ask for it without
// demanding a hypervisor it did not need.
//
// Like Virtualized it stays false unless the driver is certain, and for
// the same reason: RequireKernelIsolation is checked against it, so a
// false positive runs a workload on the host kernel it was told to stay
// off.
KernelIsolated bool `json:"kernel_isolated,omitempty"`
// SupportsStream reports whether Stream returns live output. Drivers
// that only persist output post-hoc set this false.
SupportsStream bool `json:"supports_stream"`
// SupportsSignal reports whether Signal can deliver signals to a
// running handle.
SupportsSignal bool `json:"supports_signal"`
// SupportsResourceLimits reports whether Spec.ResourceLimits is
// enforced. When false, limits in a Spec are ignored, not an error.
SupportsResourceLimits bool `json:"supports_resource_limits"`
// the control-plane host. False for remote and for containers with a
// copied (rather than bind-mounted) workspace.
SharesHostFilesystem bool `json:"shares_host_filesystem"`
// NetworkEgress reports whether workloads can reach the network.
NetworkEgress bool `json:"network_egress"`
// FilteredEgress reports whether that reach is bounded at the IP layer
// by a policy cloop installs — an nftables ruleset on the sandbox
// bridge, an --internal runtime network, or a Kubernetes NetworkPolicy.
//
// It is separate from NetworkEgress because the two answer different
// questions, and conflating them loses both. NetworkEgress says whether
// the workload has an interface at all, which is what placement needs;
// this says whether what it reaches through that interface is
// constrained, which is what an operator auditing the fleet needs.
//
// False does not mean "nothing filters this" — a cluster may run its own
// NetworkPolicy, an operator may firewall the host — it means cloop is
// not the thing doing it and will not claim credit for it.
FilteredEgress bool `json:"filtered_egress"`
// SupportsImageOverride reports whether Spec.Image is honoured. It is
// false for drivers with no image concept at all (localprocess) and for
// drivers whose image is fixed by the operator, and it is what lets a
// project carrying a .cloop/sandbox.yaml be refused *before* it runs
// with the wrong toolchain rather than after.
SupportsImageOverride bool `json:"supports_image_override"`
// SupportsSandboxBuild reports whether Spec.SetupCommands can be baked
// into a derived image. Building needs a builder on the executor, which
// a driver that only schedules pre-built images does not have.
SupportsSandboxBuild bool `json:"supports_sandbox_build"`
// SupportsWorkspaceProvisioning reports whether this driver can
// materialise a source tree itself (Spec.Workspace.Kind == "git").
//
// It is what makes a non-local executor honest. A driver that ignored the
// field would start the harness in an empty directory, and the run would
// look like a working run producing inexplicable output — which is the
// exact failure this capability exists to turn into a refusal at
// placement time. Drivers that share the host filesystem report false and
// are given Kind "bind" instead, because their tree is already there.
SupportsWorkspaceProvisioning bool `json:"supports_workspace_provisioning"`
// SupportsWriteBack reports whether this driver can return the files a
// workload changed (Spec.WriteBack).
//
// Separate from SupportsWorkspaceProvisioning even though the same drivers
// implement both, because they are two different halves of the run and a
// driver can genuinely have one without the other — an executor that
// shares the host filesystem needs neither, and one whose transport cannot
// carry bytes back can fetch but not return. Every field a driver can
// quietly drop needs a flag that says so, and this is the one whose
// omission costs work rather than confusion.
SupportsWriteBack bool `json:"supports_write_back"`
// SupportsSandboxMounts reports whether Spec.Mounts is honoured.
//
// It is tracked separately from SupportsImageOverride even though the same
// two drivers implement both, because the failure mode of getting it wrong
// is silent: a driver that ignores Mounts produces a sandbox that starts,
// runs, and cannot find the cache directory the project told it to expect.
// Every field a driver can quietly drop needs a flag that says so.
SupportsSandboxMounts bool `json:"supports_sandbox_mounts"`
// SupportsHostMounts reports whether Spec.HostMounts is honoured — i.e.
// whether this driver can bind a path from the control-plane host into
// the sandbox.
//
// It is not implied by SharesHostFilesystem, and the two are genuinely
// different answers. localprocess shares the filesystem and cannot bind
// anything: a granted repository is simply already visible at its own
// path there. A remote agent can bind, but not paths from *this* host,
// which it has never seen. Only a driver that both runs here and has a
// mount namespace to bind into can honour the field, and a driver that
// ignored it would start a harness whose /repos is empty.
SupportsHostMounts bool `json:"supports_host_mounts"`
// SupportsDevices reports whether Spec.Devices is honoured — i.e.
// whether this driver can expose a host device node inside the sandbox.
//
// Distinct from SupportsHostMounts because a device is not a file the
// driver can bind and be done with. A container runtime needs a cgroup
// rule permitting the major:minor pair as well as the node itself, and
// the Kubernetes driver needs the node to advertise the device at all.
// A driver that quietly dropped the field would produce a sandbox whose
// /dev is missing exactly the hardware the task exists to talk to, so
// the honest outcome is a placement refusal naming the executor.
SupportsDevices bool `json:"supports_devices"`
// SupportsEgressScope reports whether Spec.EgressScope is honoured —
// whether this driver can confine one workload's IP-layer egress
// independently of the other workloads on the same executor.
//
// Its absence is the reason the field cannot simply be best-effort. An
// executor that ignored a project's request to be cut off from private
// address space would hand a harness the reach into the operator's
// internal network that the project had explicitly renounced, and would
// report success doing it.
SupportsEgressScope bool `json:"supports_egress_scope"`
// SupportsSecretFiles reports whether the credential *files* a secret
// lease produces actually reach the workload.
//
// It is the flag whose absence was silent, and expensively so. A lease's
// environment travelled everywhere; its files were written into the hub's
// own /dev/shm and named in Spec.Env, which is a delivery only for a
// workload running on the hub's filesystem. Every isolating driver
// forwarded the variable and dropped the file. A repo-scoped github_pat
// delivers its *whole enforcement* as files — a credential helper, a token
// file, a gitconfig exported through GIT_CONFIG_GLOBAL — and deliberately
// exports no bare token unless the grant is unrestricted, so what arrived
// in the sandbox was a path that did not exist and no credential at all.
//
// False here refuses such a workload at placement time (see
// Requirements.RequireSecretFiles) instead of starting a sandbox whose git
// authentication will fail for a reason nothing names.
SupportsSecretFiles bool `json:"supports_secret_files"`
// SecretFilesFromHostPath reports whether the workload reads those files
// straight off the control-plane host's filesystem, at the path the hub
// wrote them to.
//
// It decides *who materialises*, which is a different question from
// whether files are supported at all, and the two cannot be collapsed.
// localprocess reads hub paths, so the hub writes the plaintext and the
// process opens it. A container also runs on the hub — SharesHostFilesystem
// is true for it — and still cannot read that directory: it has a mount
// namespace of its own, and the lease directory is 0700 owned by the hub
// user while the sandbox runs as an unprivileged UID. So the container
// driver takes the bytes and stages its own copy, and the hub writes no
// plaintext at all. Kubernetes and remote are further out again.
//
// The rule this field encodes: the hub materialises a credential only for
// a backend that will genuinely read it from there. Anywhere else, writing
// it would create a plaintext file on the control plane that nothing ever
// opens.
SecretFilesFromHostPath bool `json:"secret_files_from_host_path,omitempty"`
// MaxConcurrent is the advertised ceiling on simultaneously running
// handles; 0 means unbounded/unknown.
MaxConcurrent int `json:"max_concurrent,omitempty"`
// Platform and Arch describe the execution target (GOOS/GOARCH style).
Platform string `json:"platform,omitempty"`
Arch string `json:"arch,omitempty"`
}
Capabilities advertises what a driver can and cannot do. Callers use it to degrade gracefully (e.g. hide a "Stop" button for a driver that cannot signal) rather than to attempt an operation and handle the error.
type Clamp ¶ added in v0.0.2
type Clamp struct {
// Resource is the human name of the field: "cpu", "memory", "disk", "pids".
Resource string `json:"resource"`
// Requested is what the run asked for; 0 means it asked for no limit.
Requested int `json:"requested"`
// Effective is what it was given.
Effective int `json:"effective"`
// Source names the ceiling that bound it — one of the CeilingSource
// constants.
Source string `json:"source"`
}
Clamp records one reduction a ceiling made, so it can be explained.
func BoundSpec ¶ added in v0.0.2
BoundSpec lowers spec's *stated* resource requests to the ceilings in force for projectPath, and bounds the workspace fetch. It returns what it changed.
Every dispatch site calls it, and it lives here rather than beside any one of them because there are five: the Web UI's detached and synchronous paths, the failover supervisor, the reproduction runner and the REST API.
Why this does not fill in an unstated request ¶
It is tempting to write the ceiling into a limit the project left at zero — zero means *no limit*, so leaving it alone looks like leaving the hole open. Doing that is wrong, and subtly so: it converts "this project stated nothing" into "this project explicitly requests the ceiling", and the drivers resolve a stated request as *more specific* than the executor's configured default. A hub with `executors.container.memory: 4g` under an 8 GB fleet ceiling would then give an unconfigured project 8 GB — the ceiling would have *raised* the limit, which is the one thing a ceiling may never do.
So the unstated case is closed one layer down, by the driver, which applies BoundLimit after it has resolved the request against its own default. This function handles what the driver cannot see: a stated request, which it lowers here so that the Spec persisted for failover, recorded in the provenance file and shown in the audit trail is the one that actually ran.
It never fails. A ceiling is a bound, not an admission decision: the outcome of asking for too much is a smaller sandbox, not a refused run.
func (Clamp) String ¶ added in v0.0.2
String renders the clamp the way an operator should read it.
The unstated case gets its own wording. "requested 0" would be a lie about what the project asked for — it asked for nothing, which meant everything — and a developer told their 0 became 2048 has been given a puzzle rather than an explanation.
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// After returns a channel that fires once after d.
After(d time.Duration) <-chan time.Time
}
Clock abstracts time so tests can drive a supervisor without sleeping.
var SystemClock Clock = realClock{}
SystemClock is the production Clock.
type Constraint ¶
type Constraint string
Constraint identifies which requirement eliminated a candidate. It is a string rather than an int so it can cross the API boundary into a JSON error body and still mean something to whoever reads it.
const ( ConstraintNoCandidates Constraint = "no_candidates" ConstraintExecutorID Constraint = "executor_id" ConstraintHealth Constraint = "health" ConstraintHostPolicy Constraint = "host_execution_policy" ConstraintIsolation Constraint = "isolation" ConstraintVirtualization Constraint = "virtualization" ConstraintKernelIsolation Constraint = "kernel_isolation" ConstraintLabels Constraint = "labels" ConstraintPlatform Constraint = "platform" ConstraintArch Constraint = "arch" ConstraintHarness Constraint = "harness" ConstraintContainerRuntime Constraint = "container_runtime" ConstraintNetworkEgress Constraint = "network_egress" ConstraintResourceLimits Constraint = "resource_limits" ConstraintStream Constraint = "stream" ConstraintSignal Constraint = "signal" ConstraintMemory Constraint = "memory" ConstraintCapacity Constraint = "capacity" ConstraintImageOverride Constraint = "image_override" ConstraintSandboxBuild Constraint = "sandbox_build" ConstraintSandboxMounts Constraint = "sandbox_mounts" ConstraintHostMounts Constraint = "host_mounts" ConstraintDevices Constraint = "devices" ConstraintEgressScope Constraint = "egress_scope" ConstraintWorkspace Constraint = "workspace" ConstraintWriteBack Constraint = "write_back" ConstraintSecretFiles Constraint = "secret_files" ConstraintRevocation Constraint = "revocation" ConstraintAgentBuild Constraint = "agent_build" )
type DevicePermissions ¶ added in v0.0.2
type DevicePermissions string
DevicePermissions is the access a workload gets to a device node, in the runtime's own rwm vocabulary.
It is a string rather than a set of bools because that is the form both container runtimes and the Linux device cgroup take, and because the mistake worth preventing — silently widening "r" to "rwm" — is easier to see in a value that round-trips unchanged.
What this does and does not guarantee ¶
The value becomes the third field of the runtime's `--device src:dst:perms`, which programs the container's *device cgroup*. On cgroup v1 that is the devices controller; on cgroup v2 it is an eBPF program attached to the container's cgroup, and whether it is attached at all depends on the kernel, the runtime build and the cgroup delegation — none of which cloop can interrogate. On a host where it is not attached, a device granted "r" is readable *and* writable, because the node inside the sandbox carries the host node's file mode and /dev/zero is 0666 nearly everywhere.
So this field is defence in depth and not a boundary cloop can promise. The control that does hold on every host is the node's own ownership and mode: a device at crw-rw---- root:dialout is unopenable by a sandbox whose UID is not in that group, regardless of cgroups. An operator who needs read-only access to be enforced sets it there. Preflight says so rather than leaving the stronger reading of this field to be assumed; see preflightDeviceMode.
It is still emitted, and still defaults to rw rather than the runtime's rwm, because on the hosts where the cgroup rule *is* enforced it is enforced exactly, and withholding mknod by default costs nothing.
const ( // DeviceRead allows read only. Correct for a sensor or a capture device. DeviceRead DevicePermissions = "r" // DeviceReadWrite allows read and write. The common case: a serial line, // a GPU, a TPM. DeviceReadWrite DevicePermissions = "rw" // DeviceReadWriteMknod additionally allows mknod on the device's // major:minor. Needed by workloads that create their own nodes, and // worth naming separately because it is the one that lets a sandbox // fabricate a second reference to the same hardware. DeviceReadWriteMknod DevicePermissions = "rwm" )
func (DevicePermissions) Valid ¶ added in v0.0.2
func (p DevicePermissions) Valid() bool
Valid reports whether p is one of the three permitted access strings.
type EgressScope ¶ added in v0.0.2
type EgressScope string
EgressScope is a project's request to confine its own IP-layer egress.
const ( // EgressScopeUnset leaves the executor's own configuration in force. It // is the zero value, so every deployment that predates this field keeps // exactly the behaviour it had. EgressScopeUnset EgressScope = "" // EgressScopePublic allows the public Internet and drops everything in // the non-public block set — RFC1918 and ULA private space, link-local // (including the cloud metadata endpoint at 169.254.169.254), // carrier-grade NAT, multicast and loopback beyond the sandbox's own. // // This is the "let it out to the Internet, keep it off our network" // policy. It is the useful default for a task on a host that also has // routes an operator would rather a harness never discovered, and it is // the only scope that leaves the sandbox able to fetch dependencies. EgressScopePublic EgressScope = "public" // EgressScopeNone removes the sandbox's network interfaces entirely. // // It overlaps Spec.DisableNetwork and exists so that one field can carry // the whole decision: a project that writes `egress: none` and a project // that omits capabilities.network end up in the same place, and a reader // of the spec does not have to know that the absence of one key means the // same thing as the presence of another. EgressScopeNone EgressScope = "none" )
func EgressScopes ¶ added in v0.0.2
func EgressScopes() []EgressScope
EgressScopes returns every nameable scope, sorted, for CLI help and validation messages. EgressScopeUnset is omitted: it is spelled by leaving the key out, not by naming it.
func ParseEgressScope ¶ added in v0.0.2
func ParseEgressScope(raw string) (EgressScope, error)
ParseEgressScope validates and normalises a user-supplied scope string.
func (EgressScope) Describe ¶ added in v0.0.2
func (s EgressScope) Describe() string
Describe returns the operator-facing sentence for this scope.
func (EgressScope) NeedsFilter ¶ added in v0.0.2
func (s EgressScope) NeedsFilter() bool
NeedsFilter reports whether honouring this scope requires the driver to install a per-workload IP-layer policy.
EgressScopeNone does not: taking the interfaces away is not a filter, and it needs neither nft nor CAP_NET_ADMIN. That distinction is what lets a project on an unprivileged host still express the strongest of these scopes.
func (EgressScope) RemovesNetwork ¶ added in v0.0.2
func (s EgressScope) RemovesNetwork() bool
RemovesNetwork reports whether this scope leaves the sandbox with no interfaces, in which case no filter has to be installed for it.
func (EgressScope) Valid ¶ added in v0.0.2
func (s EgressScope) Valid() bool
Valid reports whether s is a known scope. The unset scope is valid: it means "no opinion", which is a legitimate thing for a spec not to have.
type EgressScopeExplainer ¶ added in v0.0.2
type EgressScopeExplainer interface {
// ExplainEgressScopeRefusal returns a sentence completing "this executor
// cannot confine one project's egress independently of its neighbours; ...".
ExplainEgressScopeRefusal() string
}
EgressScopeExplainer is implemented by a driver that can say, in its own vocabulary, why it is not carrying per-project egress scopes right now.
It exists because the remedy is driver-specific and the refusal is not. A container executor needs nft(8) and CAP_NET_ADMIN; a Kubernetes executor needs proof that the cluster's CNI enforces the NetworkPolicy it already creates, which is a different problem with a different command. Printing the container remedy at a Kubernetes operator sends them to a config key their deployment does not have — and a refusal whose remedy does not apply is one people resolve by deleting the capability from sandbox.yaml, which is exactly the outcome the refusal existed to prevent.
Deliberately an interface rather than a field on Capabilities, for the reason stated at SupportsRevocation: a string in a struct is a claim anyone can set and nobody keeps current, while a method is answered by the driver that knows.
type EventSink ¶
type EventSink interface {
// ExecutorTransition is called once per state change.
ExecutorTransition(t Transition)
// ExecutorFailover is called once per session moved off a dead node,
// including when no replacement could be found (ev.Err is then set).
ExecutorFailover(ev FailoverEvent)
}
EventSink receives supervisor observations for the event log and the UI. Implementations must not block: they are called from the probe goroutines.
type Executor ¶
type Executor interface {
// ID returns the stable, unique identifier of this executor instance.
ID() string
// Kind returns the driver implementation name (one of the Kind* consts).
Kind() string
// Capabilities describes what this executor supports.
Capabilities() Capabilities
// Start launches a workload and returns its handle. The workload
// outlives ctx.
Start(ctx context.Context, spec Spec) (Handle, error)
// Signal delivers sig to a running handle. Signalling an
// already-finished handle returns nil (the desired end state already
// holds); an unknown handle returns ErrHandleNotFound.
Signal(ctx context.Context, handleID string, sig Signal) error
// Status reports the current state of a handle.
Status(ctx context.Context, handleID string) (Status, error)
// Stream returns a channel of output chunks for a handle. The channel
// is closed when the workload finishes and all output has been
// delivered. Output produced between Start and Stream is replayed
// (bounded) so callers do not race the workload's first writes.
// Cancelling ctx unsubscribes the caller without affecting the
// workload or other subscribers.
Stream(ctx context.Context, handleID string) (<-chan LogLine, error)
// HealthCheck reports whether this executor is able to accept work.
HealthCheck(ctx context.Context) error
}
Executor is a pluggable execution backend.
Implementations must be safe for concurrent use by multiple goroutines.
Context handling: the ctx passed to each method governs *that call*, not the lifetime of the workload. Cancelling the ctx passed to Start does not kill the started workload — it only aborts the act of starting it. This matters because the Web UI starts long-lived runs from short-lived HTTP request handlers; tying workload lifetime to the request context would kill every run the moment its originating request returned. Callers that *do* want ctx-bound lifetime should use Run.
func Resolve ¶
Resolve returns the executor bound to projectPath in the default registry, falling back to the default executor.
func ResolveBinding ¶
ResolveBinding is Resolve without the host-execution policy check: it answers "which executor is this project pointing at", which the Executors panel needs even for a binding that policy currently forbids running.
type FailoverEvent ¶
type FailoverEvent struct {
// Session is the claimed session, carrying its *new* claim token.
Session Session `json:"session"`
// From is the executor that went unreachable.
From string `json:"from"`
// To is the replacement executor, or "" when none was found.
To string `json:"to,omitempty"`
// Err is set when placement or re-dispatch failed. The task is still
// marked failed-with-retry; it just has nowhere to go right now.
Err error `json:"-"`
// Reason renders Err for the event payload.
Reason string `json:"reason,omitempty"`
At time.Time `json:"at"`
}
FailoverEvent describes one session being moved off an unreachable node.
type FailoverHandler ¶
type FailoverHandler func(ctx context.Context, ev FailoverEvent) error
FailoverHandler re-dispatches a claimed session. It is supplied by the control plane (pkg/ui) because only that layer knows how to mark a task failed-with-retry and start a fresh run.
It is called *after* the claim succeeds, so it is guaranteed to run at most once per session per failure. Returning an error records the failover as unplaced; it does not release the claim, because releasing it would re-arm the double-execution risk the claim exists to prevent.
type GitCredential ¶
type GitCredential struct {
// Username and Password are HTTP basic credentials. For a GitHub PAT the
// broker's convention is username "x-access-token" and the token as the
// password.
Username string
Password string
// LeaseID, GrantID and SecretName are bookkeeping for the audit trail and
// for releasing the lease afterwards. None of them is secret.
LeaseID string
GrantID string
SecretName string
// ExpiresAt is the lease deadline, so a driver can refuse to start a
// provisioning step whose credential will expire mid-fetch.
ExpiresAt time.Time
}
GitCredential is the material one authenticated fetch needs.
It is *not* part of Spec and must never be put there: a Spec is persisted by pkg/executorstore, echoed into audit rows, and marshalled across the remote boundary. Drivers lease this at dispatch time and carry it out of band.
func (GitCredential) AuthorizationHeader ¶
func (c GitCredential) AuthorizationHeader() string
AuthorizationHeader renders the credential as an HTTP basic header value.
func (GitCredential) Empty ¶
func (c GitCredential) Empty() bool
Empty reports whether there is no credential to deliver.
func (GitCredential) Secrets ¶
func (c GitCredential) Secrets() []string
Secrets returns every string that must be scrubbed from output before it leaves the executor: the token itself and its base64 encoding, since a git error message may echo either.
type GitStep ¶
type GitStep struct {
// Name identifies the step for logs and audit records.
Name string
// Argv is the command. Argv[0] is always "git" and no element is ever
// passed through a shell.
Argv []string
// Authenticated marks the single step that contacts the remote and so
// receives the credential in its environment. Exactly one step in a plan
// has it set, which is what lets a driver deliver the token to one child
// process instead of to the whole provisioning sequence.
Authenticated bool
}
GitStep is one command in a workspace provisioning plan.
type Handle ¶
type Handle struct {
// ID is unique within the executor that produced it.
ID string `json:"id"`
// ExecutorID is the ID of the executor that owns this handle, so a
// caller holding only a Handle can find its way back to the driver.
ExecutorID string `json:"executor_id"`
// PID is the operating-system process ID *on the executor*. It is 0
// for drivers where a PID is meaningless or not reported. Host-local
// tooling (the /proc scan in pkg/multiui that powers the Stop button)
// relies on this being the real PID for KindLocalProcess.
PID int `json:"pid,omitempty"`
// StartedAt is when the workload began, in the control plane's clock.
StartedAt time.Time `json:"started_at"`
// Image is the fully-resolved image reference the workload actually ran
// from — digest-pinned where the driver could resolve one, so the run
// stays reproducible after the tag moves. Empty for drivers with no image.
//
// It is reported here rather than echoed from Spec.Image because the two
// differ in exactly the case that matters: the spec asks for `python:3.12`
// and the handle records `python@sha256:…`, which is the only one of the
// two that will still mean the same thing next month.
Image string `json:"image,omitempty"`
}
Handle identifies one started workload. It is the token every subsequent operation (Signal, Status, Stream) is keyed by.
type HandleRecord ¶
type HandleRecord struct {
// HandleID is the driver-side handle, unique within ExecutorID. It is the
// key callers hold, so it is the primary key here too.
HandleID string `json:"handle_id"`
// ExecutorID is the executor instance that owns the workload. Rehydration
// is scoped by it: a driver adopts only its own rows, so two container
// executors configured against the same runtime never steal each other's
// containers.
ExecutorID string `json:"executor_id"`
// Driver is the Kind* constant of the owning driver, recorded so a sweep
// can reason about rows whose executor is no longer registered (a
// container executor removed from config still left containers behind).
Driver string `json:"driver"`
// ExternalID is the name the *runtime* knows the workload by, and is the
// whole point of the record. Its shape is driver-specific:
//
// localprocess the OS process ID, rendered as decimal
// container the container name
// kubernetes "namespace/podname"
// remote the agent-side handle ID (same as HandleID; the agent
// offers it back on reconnect)
//
// It is a string rather than a typed union because nothing outside the
// owning driver interprets it — the sweep only needs to know a row exists.
ExternalID string `json:"external_id"`
// ProjectPath and TaskID identify the work, so an operator reading the
// table can tell what an orphan was doing and a sweep can scope itself to
// one project.
ProjectPath string `json:"project_path,omitempty"`
TaskID int `json:"task_id,omitempty"`
// PID is the OS process ID where one is meaningful (localprocess), 0
// elsewhere. Duplicated from ExternalID for localprocess because a
// typed field is what the liveness check wants.
PID int `json:"pid,omitempty"`
// Image is the resolved image reference, for drivers that have one.
Image string `json:"image,omitempty"`
// StartedAt is when the workload began, in the control plane's clock. The
// orphan sweep compares it against a grace period, so it must be the
// dispatch time and not the time the row was written.
StartedAt time.Time `json:"started_at"`
// Deadline is when Spec.TimeoutMinutes expires, or the zero time for an
// unbounded workload.
//
// It is stored as an absolute instant rather than as the original
// duration because the two disagree in exactly the case it exists for: a
// hub down for twenty minutes must resume a one-hour timeout with forty
// minutes left, not restart the hour. A duration would silently extend
// every timeout by the length of the outage.
//
// Drivers whose backend enforces the deadline itself leave this zero and
// say so — the Kubernetes driver hands the API server
// activeDeadlineSeconds, which already survives a control-plane restart
// and is the reason a client-side timer was the wrong mechanism there in
// the first place. It is the drivers holding a time.AfterFunc in this
// process (container, localprocess) that lose their timer on a restart
// and need this to re-arm.
Deadline time.Time `json:"deadline,omitempty"`
// Meta carries driver-specific extras that do not deserve a column —
// a Kubernetes NetworkPolicy name, a container's runtime. Never secrets:
// this map is persisted verbatim.
Meta map[string]string `json:"meta,omitempty"`
// Secrets are the lease bindings the workload was started with, so a
// revocation arriving after a control-plane restart can still find it.
//
// This is the one field that is *material-adjacent* and still safe to
// persist, and the reason is a property of the type rather than of this
// call site: SecretBinding carries lease ids, variable names and paths
// and no values — which is what already lets it be written to
// executor_sessions and into audit rows. The Spec next to it is not safe
// and is deliberately absent; see the file header.
//
// Without it the driver's lease→handle index was in-memory only, so a
// rehydrated workload answered HoldsLease with false and a revocation
// aimed at it reported nothing-to-revoke — a *success* for a credential
// that was still in use.
Secrets []SecretBinding `json:"secrets,omitempty"`
// SecretsRecorded reports whether Secrets is an authoritative answer.
//
// It exists because nil is ambiguous in the one direction that matters:
// "this workload held no leases" and "nobody wrote down which leases this
// workload held" are the same empty slice, and reading the second as the
// first is how a revocation silently misses a holder. A row written
// before bindings were persisted at all decodes with this false, and its
// driver marks the adopted handle unresolved rather than unbound — see
// LeaseIndex.Adopt.
//
// False is therefore the safe zero value: a writer that forgets to set it
// produces an over-cautious revocation report, never an over-confident
// one.
SecretsRecorded bool `json:"secrets_recorded,omitempty"`
}
HandleRecord is the durable identity of one dispatched workload: enough to find it again after the control plane restarts, and nothing more.
It is a value type with no pointers so a driver can hand one to a store without worrying about the store retaining a reference into live state.
func LoadHandles ¶
func LoadHandles(store HandleStore, executorID string) []HandleRecord
LoadHandles reads executorID's rows for rehydration, reporting failure without propagating it.
A driver that cannot read its rows must still construct: refusing to would take down a control plane whose only real problem is a stale row, and leave the operator with no hub at all rather than a hub that has forgotten some workloads. The returned slice is empty in that case, which is the pre-existing behaviour.
func (HandleRecord) Validate ¶
func (r HandleRecord) Validate() error
Validate reports whether the record carries enough identity to be useful. A row with no external ID cannot be reattached to, so storing one would create an orphan that the sweep can see but never clean up.
type HandleStore ¶
type HandleStore interface {
// PutHandle inserts or replaces the row for rec.HandleID.
PutHandle(rec HandleRecord) error
// ListHandles returns the rows owned by executorID, oldest first.
// Passing "" returns every row, which is what a cross-driver sweep wants.
ListHandles(executorID string) ([]HandleRecord, error)
// DeleteHandle forgets one row. Deleting a row that is not there is not
// an error: a terminal transition can be recorded twice (once by the log
// pump, once by an explicit reap) and neither call should fail.
DeleteHandle(handleID string) error
}
HandleStore persists handle identity across control-plane restarts.
It is deliberately three methods. Drivers write one row when they dispatch, drop it when the workload reaches a terminal state, and read their own rows back at construction; anything richer would be a query surface for the UI, which reads executor_sessions instead.
Implementations must be safe for concurrent use: Start is called from request goroutines and DeleteHandle from log-pump goroutines.
type Health ¶
type Health struct {
// ExecutorID is the registry key this health record describes.
ExecutorID string `json:"executor_id"`
// State is the current scheduling state.
State NodeState `json:"state"`
// Reason is a short human-readable explanation of the current state:
// the probe error for unreachable, the operator's note for cordoned.
Reason string `json:"reason,omitempty"`
// ConsecutiveFailures counts probe failures since the last success. It
// keeps counting while a node is cordoned so that uncordoning restores
// the state the probes actually justify rather than optimistically
// assuming ready.
ConsecutiveFailures int `json:"consecutive_failures"`
// LastSeen is the most recent time a probe succeeded. Zero means the
// node has never been observed healthy by this control plane.
LastSeen time.Time `json:"last_seen,omitempty"`
// LastProbe is the most recent time a probe ran, successful or not. The
// gap between LastProbe and LastSeen is how long a node has been failing.
LastProbe time.Time `json:"last_probe,omitempty"`
// StateChangedAt is when State last took a different value, which is what
// "cordoned 20m ago" in the UI is computed from.
StateChangedAt time.Time `json:"state_changed_at,omitempty"`
}
Health is the supervisor's view of one executor. It is the value persisted by a HealthStore and the value placement filters on.
func (Health) Normalize ¶
Normalize fills in a usable zero value. A node with no persisted health record is ready: refusing to schedule on an executor merely because the supervisor has not reached its first tick would make every fresh control plane briefly unable to run anything.
type HealthPolicy ¶
type HealthPolicy struct {
// DegradeAfter is the consecutive-failure count at which a ready node
// becomes degraded. Must be >= 1.
DegradeAfter int
// UnreachableAfter is the consecutive-failure count at which a node is
// declared unreachable and its work is failed over. Must be >=
// DegradeAfter.
UnreachableAfter int
}
HealthPolicy tunes how many failed probes it takes to give up on a node.
The two thresholds exist because the failure modes are different. One missed probe is usually a blip — a wifi roam, a GC pause on a Raspberry Pi — and reacting to it by failing over in-flight work would cause more damage than it prevents. Sustained failure means the device is genuinely gone, and holding its sessions hostage any longer just delays the user's task.
func DefaultHealthPolicy ¶
func DefaultHealthPolicy() HealthPolicy
DefaultHealthPolicy degrades on the first failure and gives up after three. With the default 30s probe interval that is roughly a 90-second window before failover, which is long enough to ride out a reboot and short enough that a user watching a stalled run does not give up first.
type HealthStore ¶
type HealthStore interface {
// LoadHealth returns the persisted health for an executor. An unknown
// executor yields the zero Health and a nil error — "never probed" is a
// normal state, not a lookup failure.
LoadHealth(executorID string) (Health, error)
// SaveHealth persists a health record, creating it if absent.
SaveHealth(h Health) error
// ListHealth returns every persisted health record.
ListHealth() ([]Health, error)
}
HealthStore persists the supervisor's view across control-plane restarts.
It is an interface, not a *statedb.DB, for the same reason the rest of this package avoids storage: pkg/executor is linked into the agent binary that runs on edge devices, and an executor driver has no business pulling in a SQLite engine. pkg/executorstore implements this over statedb.
type HostDevice ¶ added in v0.0.2
type HostDevice struct {
// Name identifies the device within its grant, for diagnostics and for
// the `devices:` selector in a sandbox spec.
Name string `json:"name"`
// Source is the absolute path of the device node on the executor's host.
Source string `json:"source"`
// Target is the absolute path it appears at inside the sandbox. Empty
// means the same path as Source, which is what a workload looking for
// well-known hardware expects.
Target string `json:"target,omitempty"`
// Permissions is the access granted. Empty means DeviceReadWrite, which
// is what a device is useful with and is still short of mknod.
Permissions DevicePermissions `json:"permissions,omitempty"`
// LeaseID is the secret lease that authorised this device, so revocation
// can find it and the audit trail can say who opened it. Empty is
// permitted only for a device the operator configured directly on the
// executor, which is not a path any project-scoped grant takes.
LeaseID string `json:"lease_id,omitempty"`
// KubernetesResource optionally names the extended resource a Kubernetes
// node advertises for this hardware ("nvidia.com/gpu", "squat.ai/fuse").
//
// It exists because Kubernetes does not take device paths. A device
// plugin owns the node and hands the container the right one in response
// to a resource request, so on that driver the path is not the thing
// asked for — the resource name is. A grant that omits it is honestly
// undeliverable on Kubernetes rather than silently ignored there; see
// Capabilities.SupportsDevices.
KubernetesResource string `json:"kubernetes_resource,omitempty"`
}
HostDevice is one device node exposed from the executor's host into the sandbox.
Source and Target are separate because the sandbox-visible name is part of what makes a grant portable. A project told to open /dev/accel0 should not have to know that on this particular host the hardware happens to be /dev/nvidia3; the operator maps it, and the project's code does not change when it moves to a node that enumerates its hardware differently.
func (HostDevice) EffectivePermissions ¶ added in v0.0.2
func (d HostDevice) EffectivePermissions() DevicePermissions
EffectivePermissions is the access to apply, resolving the empty default.
func (HostDevice) EffectiveTarget ¶ added in v0.0.2
func (d HostDevice) EffectiveTarget() string
EffectiveTarget is where the device appears inside the sandbox.
type HostExecutionDeniedError ¶
type HostExecutionDeniedError struct {
// ExecutorID is the un-isolated executor that was resolved.
ExecutorID string
// ProjectPath is the project whose workload was refused; may be empty
// for workloads not tied to a project.
ProjectPath string
// Alternatives are the IDs of registered executors that *do* isolate,
// ordered for stable output. Empty means none are configured, which is a
// materially different (and much more urgent) situation.
Alternatives []string
}
HostExecutionDeniedError explains a refusal to run a workload on the host and, critically, names what the operator can do instead.
A bare "denied by policy" is the kind of error that generates a support ticket: the person who hits it in the UI is usually not the person who wrote the config. Carrying the alternatives in the error means the 409 the browser receives already contains the remediation, and `cloop run` prints the same sentence in a terminal.
func (*HostExecutionDeniedError) Error ¶
func (e *HostExecutionDeniedError) Error() string
Error implements error.
func (*HostExecutionDeniedError) Remediation ¶
func (e *HostExecutionDeniedError) Remediation() string
Remediation returns just the "what to do about it" half of the message, for UI surfaces that render the cause and the fix in separate elements.
func (*HostExecutionDeniedError) Unwrap ¶
func (e *HostExecutionDeniedError) Unwrap() error
Unwrap lets callers match with errors.Is(err, ErrHostExecutionDenied) without caring whether they got the rich form or a plain wrap.
type HostMount ¶
type HostMount struct {
// Name is the granted repository's handle, for diagnostics and audit.
Name string `json:"name,omitempty"`
// Source is the absolute path on the executor's host.
Source string `json:"source"`
// Target is the absolute path it appears at inside the sandbox.
Target string `json:"target"`
// ReadOnly binds the source read-only. The zero value is read-write, so
// callers building one of these by hand get the same default as the
// runtime flag they render into — the safe default lives in the grant
// (secretbroker.Constraints.Writable), which is where an operator sets it.
ReadOnly bool `json:"read_only,omitempty"`
}
HostMount binds an absolute path on the executor's host into the sandbox.
It is the deliberate counterpart to SpecMount, and the difference between the two types is the trust boundary, not the mechanism. A SpecMount comes from .cloop/sandbox.yaml — repo-committed, therefore whatever a pull request says it is — so its source is workspace-relative and cannot escape. A HostMount comes from a secret grant: a human with secret.grant named this path, named the project it goes to, and the broker recorded who did it. That is the only provenance under which an absolute host path may enter a Spec.
Keeping them as separate types rather than one type with an "absolute allowed" flag is the point. There is no code path that turns a SpecMount into a HostMount, so a future change to the sandbox parser cannot widen into host access by setting a bool: it would have to construct a different type, in a package that parses untrusted YAML, and that is a visible thing to review.
func (HostMount) Validate ¶
Validate enforces the containment rules for a host mount.
Unlike SpecMount.Validate this cannot check containment — the whole point is that the source is outside the workspace — so what it checks instead is that the path cannot be *reinterpreted*: no colon to append runtime mount options, no relative segment for a driver to resolve against a directory this code cannot see, nothing that changes meaning between here and a -v flag.
type Isolation ¶
type Isolation string
Isolation describes how strongly a driver separates a workload from the control-plane host. It is advisory metadata used by the UI and by policy checks such as strict no-host-execution mode.
const ( // IsolationNone: the workload shares the host's filesystem, network, // and user with the control plane. IsolationNone Isolation = "none" // IsolationContainer: the workload runs in a container with its own // filesystem and (optionally) network namespace. IsolationContainer Isolation = "container" // IsolationVM: the workload runs in a virtual machine or microVM. IsolationVM Isolation = "vm" // IsolationRemote: the workload runs on a different machine entirely. IsolationRemote Isolation = "remote" )
type LeaseIndex ¶
type LeaseIndex struct {
// contains filtered or unexported fields
}
LeaseIndex maps a lease to the handles started with its material, so a revocation knows which workloads to reach into.
Every driver needs exactly this and nothing more, so it is written once. The zero value is not usable; call NewLeaseIndex.
func (*LeaseIndex) Adopt ¶
func (li *LeaseIndex) Adopt(rec HandleRecord)
Adopt restores a rehydrated handle's lease bindings from its persisted record, or marks the handle unresolved when the record cannot say.
Every driver's adopt path calls exactly this, which is the point: the decision of what an unrecorded binding *means* is a security decision, and four copies of it would eventually become four different answers — with the permissive one silently losing a revocation.
A record with SecretsRecorded true is authoritative, including when it names no bindings: the workload genuinely held nothing, and binding nothing is correct. A record with it false predates binding persistence (its row was written by an older binary), so the honest answer is that this driver cannot account for the handle — recorded as unresolved, and reported as a failure by any revocation that follows. That state clears on its own when the workload finishes and Release runs.
func (*LeaseIndex) Bind ¶
func (li *LeaseIndex) Bind(handleID string, bindings []SecretBinding)
Bind records that handleID was started with material from spec's bindings.
Only the bindings are retained, never the Spec: SecretBinding carries names and paths and no values, which is exactly what makes it safe to keep for the lifetime of a handle.
func (*LeaseIndex) Handles ¶
func (li *LeaseIndex) Handles(req RevokeRequest) map[string][]SecretBinding
Handles lists the handle IDs holding material matching req, each with the bindings that request covers. Handles are returned sorted so a revocation processes them deterministically.
func (*LeaseIndex) Holds ¶
func (li *LeaseIndex) Holds(leaseID string) bool
Holds reports whether any tracked handle carries material from leaseID.
It answers true when any handle is unresolved, whatever the lease. That is deliberately blunt and deliberately conservative: an unresolved handle might hold this lease and nothing here can tell, so the two available answers are "possibly" and a lie. True routes the revocation to RevokeLease, which is where the doubt is reported as a failure — a false here would skip this executor entirely and leave the fleet aggregate reading revoked.
func (*LeaseIndex) Leases ¶
func (li *LeaseIndex) Leases() []string
Leases lists the lease IDs currently held, sorted.
Unlike Holds this reports only what is *known*, because there is no honest way to name a lease an unresolved handle might be holding. Callers that enumerate leases to revoke them — draining an executor, say — must therefore also consult UnresolvedError, or they will drain an executor and report it clean while a workload on it holds a credential nobody listed.
func (*LeaseIndex) MarkUnresolved ¶
func (li *LeaseIndex) MarkUnresolved(handleID, reason string)
MarkUnresolved records that handleID may hold leased material this index cannot enumerate.
Callers use it for a row whose bindings could not be decoded as well as for one that never carried any. Both mean the same thing to a revocation, and the reason string is what tells the operator which it was.
func (*LeaseIndex) Release ¶
func (li *LeaseIndex) Release(handleID string)
Release forgets a handle's lease bindings once it is gone.
func (*LeaseIndex) Unresolved ¶
func (li *LeaseIndex) Unresolved() map[string]string
Unresolved returns handleID → reason for every handle whose bindings this index cannot enumerate. A copy, so a caller cannot mutate live state; nil when nothing is in doubt. UnresolvedError is what renders it in a stable order for an operator.
func (*LeaseIndex) UnresolvedError ¶
func (li *LeaseIndex) UnresolvedError() error
UnresolvedError describes the handles this index cannot account for, or nil when there are none.
A driver puts this on the revocation's report rather than swallowing it. The alternative — reporting the leases it *could* resolve and staying quiet about the rest — is the exact shape of the bug this exists to prevent: an answer that looks like a successful revocation and is not one.
type Lister ¶
type Lister interface {
// HandleStatuses returns a status snapshot per handle the driver
// retains, including recently-finished ones. Ordering is unspecified.
//
// It is not called Handles because the drivers already expose a
// diagnostic Handles() []string, and one method name meaning two
// different things across the same types is how a caller ends up
// counting strings and believing it counted running workloads.
HandleStatuses(ctx context.Context) ([]Status, error)
}
Lister is an optional interface for drivers that can enumerate the handles they currently know about.
It is separate from Executor because not every driver can answer cheaply — a remote agent would have to round-trip to a device that may be offline — and a UI panel refreshing a load column must never be the reason a page hangs. Callers use LiveHandles, which degrades to "unknown" rather than to an error.
type LogLine ¶
type LogLine struct {
HandleID string `json:"handle_id"`
Stream StreamName `json:"stream"`
Text string `json:"text"`
Time time.Time `json:"time"`
// Seq numbers chunks per handle starting at 1. Because drivers must
// never block a workload's writes waiting for a slow consumer, a
// subscriber that falls too far behind has chunks dropped. A gap in
// Seq is how a consumer detects that; see Run, which reports it.
Seq uint64 `json:"seq"`
}
LogLine is one chunk of workload output.
Despite the name, Text is *not* guaranteed to be exactly one line: drivers forward output as it arrives, so a chunk may span several lines or stop mid-line. This is deliberate — a harness that prints a progress line without a trailing newline must still reach the live log panel promptly, which line-buffering would prevent. Consumers needing whole lines should buffer on '\n' themselves.
type MemoryHandleStore ¶
type MemoryHandleStore struct {
// contains filtered or unexported fields
}
MemoryHandleStore is an in-memory HandleStore for tests and for embedders with no state database. It is concurrency-safe and, being a map, forgets everything on process exit — which makes it useful for asserting that a driver *writes* the right rows, and useless for surviving a restart.
func NewMemoryHandleStore ¶
func NewMemoryHandleStore() *MemoryHandleStore
NewMemoryHandleStore returns an empty store.
func (*MemoryHandleStore) DeleteHandle ¶
func (m *MemoryHandleStore) DeleteHandle(handleID string) error
DeleteHandle implements HandleStore.
func (*MemoryHandleStore) Len ¶
func (m *MemoryHandleStore) Len() int
Len reports how many rows are stored, for test assertions.
func (*MemoryHandleStore) ListHandles ¶
func (m *MemoryHandleStore) ListHandles(executorID string) ([]HandleRecord, error)
ListHandles implements HandleStore, returning rows oldest first.
func (*MemoryHandleStore) PutHandle ¶
func (m *MemoryHandleStore) PutHandle(rec HandleRecord) error
PutHandle implements HandleStore.
type NodeState ¶
type NodeState string
NodeState is the scheduling state of a registered executor.
const ( // NodeReady: probes are succeeding and the node accepts new work. NodeReady NodeState = "ready" // NodeDegraded: probes are failing but the node has not yet been given // up on. It still accepts work — a node that is slow to answer a health // check is usually still able to run one — but placement treats it as a // last resort. NodeDegraded NodeState = "degraded" // NodeUnreachable: probes have failed past the threshold. The node // accepts no new work and its in-flight sessions are failed over. NodeUnreachable NodeState = "unreachable" // NodeCordoned: an operator removed the node from rotation. In-flight // work continues to completion; only new placement is refused. NodeCordoned NodeState = "cordoned" // NodeDraining: an operator is retiring the node. Like cordoned, but the // intent is to reach zero in-flight and stay there. NodeDraining NodeState = "draining" )
func (NodeState) AdminHeld ¶
AdminHeld reports whether the state was set by an operator rather than by a probe. Probe results never move a node out of an admin-held state.
func (NodeState) Schedulable ¶
Schedulable reports whether new work may be placed on a node in this state.
Degraded is schedulable on purpose. A health probe is a much weaker signal than an actual workload: a node whose probe timed out once may be perfectly able to run a task, and refusing to schedule on the first missed probe would make a fleet of flaky edge devices permanently unusable. Placement breaks the tie by preferring ready nodes, so degraded only ever gets work when nothing better exists.
type PlacementError ¶
type PlacementError struct {
// Constraint is the headline reason — the requirement that eliminated
// the most candidates, which is nearly always the one to fix.
Constraint Constraint
// Rejections is the per-candidate detail, ordered by executor ID.
Rejections []Rejection
// Considered is how many candidates were offered.
Considered int
}
PlacementError is returned when no candidate satisfies the requirements.
It is a typed error carrying structured rejections rather than a formatted string because three different surfaces need to render it differently: the CLI prints a table, the Web UI shows a per-node badge, and the failover path logs it as an event payload.
func (*PlacementError) Unwrap ¶
func (e *PlacementError) Unwrap() error
Unwrap lets callers match with errors.Is(err, ErrNoPlacement).
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is a thread-safe set of registered executors plus the policy for deciding which one a given project runs on.
Two layers of binding exist:
- an in-memory binding map, authoritative and cheap, used for tests and for bindings applied at runtime; and
- an optional persistent lookup (SetBindingLookup) that reads the project→executor binding out of statedb. The registry takes a function rather than a *statedb.DB so this package stays free of storage dependencies and remains importable from anywhere.
Resolution order for Resolve(projectPath):
in-memory binding → persistent binding → default executor
A binding that names an unregistered executor is a hard error, never a silent fallback to the default. See the package doc for why.
func (*Registry) Bind ¶
Bind pins a project path to a specific executor ID. The executor need not be registered yet — bindings are configuration and may be restored from storage before drivers finish connecting — but Resolve will fail until it is. Pass an empty executorID to clear the binding.
func (*Registry) Binding ¶
Binding returns the executor ID bound to projectPath and whether one exists, consulting the in-memory map first and then the persistent lookup.
func (*Registry) DefaultID ¶
DefaultID returns the current default executor ID, or "" when none is set.
func (*Registry) Ensure ¶
Ensure registers ex unless an executor with the same ID is already present, in which case it is a no-op. Useful for idempotent bootstrap paths that may run from several entry points (CLI, UI server, tests).
func (*Registry) IsolatedIDs ¶
IsolatedIDs returns the IDs of registered executors that put some boundary between a workload and the control-plane host, ordered by ID.
It exists so a host-execution refusal can name concrete alternatives rather than telling the operator to go read the docs.
func (*Registry) Register ¶
Register adds ex to the registry. The first executor registered also becomes the default, so a process that only ever configures one backend needs no extra wiring. Returns ErrAlreadyRegistered if the ID is taken.
Under strict no-host-execution mode a driver that offers no isolation is refused outright, with a *HostExecutionDeniedError. Resolve and the driver's own Start both refuse such an executor too, so this is defense in depth rather than the only gate — but it is the layer that matters most, because an executor that was never registered cannot become the registry's default. Without it, a project with no explicit binding falls back to Default() and gets the host driver, which then refuses at Start: a confusing late failure where the honest answer is that no executor is available at all.
func (*Registry) Resolve ¶
Resolve returns the executor that should run work for projectPath.
It fails closed in two ways:
- if the project is bound to an executor that is not registered, the error is ErrExecutorNotFound. Downgrading such a project to the default (typically the local host) would defeat the entire point of binding it to an isolated backend; and
- if strict no-host-execution mode is on and the resolved executor offers no isolation, the error is *HostExecutionDeniedError. Resolve is the chokepoint every UI code path funnels through, which is what lets one config flag turn "the UI may not spawn harnesses on the host" from a convention into an invariant.
func (*Registry) SetBindingLookup ¶
SetBindingLookup installs the persistent project→executor resolver. Pass nil to remove it.
func (*Registry) SetDefault ¶
SetDefault marks id as the fallback executor for projects with no binding. The executor must already be registered.
func (*Registry) Unregister ¶
Unregister removes the executor with the given ID. If it was the default, the default is reassigned deterministically (lowest remaining ID) so the registry never silently becomes defaultless while executors remain.
type Rejection ¶
type Rejection struct {
ExecutorID string `json:"executor_id"`
Constraint Constraint `json:"constraint"`
Detail string `json:"detail"`
}
Rejection records why one candidate was not chosen.
type Requirements ¶
type Requirements struct {
// ExecutorID pins placement to one node. It is still checked for health
// and policy: a pin is a statement about *where*, not a license to run on
// a dead or forbidden node.
ExecutorID string
// Labels is a selector; every key must be present on the candidate with
// the same value.
Labels map[string]string
// Harnesses lists agent CLIs the task will invoke. A candidate must
// advertise all of them, unless it advertises none at all (see
// hasHarness).
Harnesses []string
// Platform and Arch constrain the execution target (GOOS/GOARCH style).
Platform string
Arch string
// MinMemoryMB rejects nodes that report less memory than this. Nodes
// reporting 0 (unknown) are not rejected.
MinMemoryMB int
// RequireIsolation rejects any node that shares the control-plane host.
RequireIsolation bool
// AllowedIsolations, when non-empty, restricts placement to these
// isolation kinds. Isolation is not a total order (a container and a
// remote device are differently, not more or less, isolated), so this is
// a set rather than a minimum.
AllowedIsolations []Isolation
// RequireVirtualization demands a node whose workloads run behind a
// hypervisor — a Kata sandbox, locally or on a remote cluster.
//
// Separate from AllowedIsolations because it cuts across it: both a local
// Kata container (IsolationVM) and a Kata Pod on a remote cluster
// (IsolationRemote) satisfy it, and no set of Isolation values selects
// exactly those two without also admitting the non-Kata remote executors
// that share their isolation value.
RequireVirtualization bool
// RequireKernelIsolation demands a node on which the workload's system
// calls are not served by the executing machine's kernel — either a
// hypervisor-backed sandbox (Kata) or a userspace kernel (gVisor).
//
// Weaker than RequireVirtualization and separate from it because the two
// are different asks, and conflating them costs real placements. A project
// whose actual requirement is "a kernel bug in my task must not be a
// kernel bug on the host" is satisfied by gVisor; making it say
// `virtualized: true` to express that would refuse every gVisor executor
// in the fleet, so an operator who had deliberately standardised on gVisor
// would watch their nodes rejected for a property they had in fact
// provided.
RequireKernelIsolation bool
// RequireContainerRuntime demands a node that can drive containers.
RequireContainerRuntime bool
// RequireNetworkEgress demands a node whose workloads can reach the
// network.
RequireNetworkEgress bool
// RequireDevices demands a node that honours Spec.Devices — one that can
// expose a host device node inside the sandbox.
//
// The refusal it produces is the point. A granted /dev/ttyUSB0 is a
// statement about one machine's hardware, so a project holding that grant
// and bound to a Kubernetes or remote executor has asked for something no
// amount of retrying will produce. Saying so at placement time names both
// facts; the alternative is a harness that opens the path, gets ENOENT,
// and reports that the device is broken.
RequireDevices bool
// RequireEgressScope demands a node that can confine one workload's
// IP-layer egress independently of its neighbours (Spec.EgressScope).
RequireEgressScope bool
// RequireResourceLimits demands a node that actually enforces
// Spec.ResourceLimits rather than ignoring them.
RequireResourceLimits bool
// RequireImageOverride demands a node that honours Spec.Image, i.e. that
// a per-project sandbox can choose its own toolchain there.
//
// This is the constraint that makes per-project sandboxes honest. Without
// it a project pinning `image: rust:1.79` placed on the host driver would
// run against whatever toolchain the control plane happens to have, get a
// plausible-looking build failure, and send its author hunting through
// their own code. Refusing placement and naming the reason is the only
// outcome that points at the deployment instead.
RequireImageOverride bool
// RequireSandboxBuild demands a node that can bake Spec.SetupCommands
// into a derived image.
RequireSandboxBuild bool
// RequireSandboxMounts demands a node that honours Spec.Mounts.
RequireSandboxMounts bool
// RequireHostMounts demands a node that honours Spec.HostMounts — a
// driver that can bind a path from the control-plane host into the
// sandbox, which is what a local_repo grant needs.
//
// This is the constraint that makes "local repositories" and "remote
// sandbox" an honest either/or rather than a silent failure. A project
// granted three checkouts on the hub and bound to a Kubernetes or remote
// executor is refused here, naming both facts, instead of running against
// an empty /repos on a machine that has never seen those files.
RequireHostMounts bool
// RequireWorkspaceProvisioning demands a node that can materialise the
// source tree itself, because this workload's tree is not already there.
//
// Without this constraint the failure mode is the worst kind: the harness
// starts, finds an empty directory, and reports back on a repository it
// never saw. Refusing placement converts that into a message naming the
// executor that cannot fetch.
RequireWorkspaceProvisioning bool
// RequireHostFilesystemWorkspace demands a node whose WorkDir really is a
// path on the control-plane host, because the workload was dispatched with
// Workspace.Kind "bind" — i.e. with the tree assumed to be there already.
RequireHostFilesystemWorkspace bool
// RequireWriteBack demands a node that can return the files the workload
// changes, because this one's tree does not survive the run.
//
// It is the workspace constraint's mirror image, and its failure is the
// worse of the two. An executor that cannot fetch produces a harness that
// reports on a repository it never saw; one that cannot write back produces
// a harness that reports, accurately, on work that is then discarded with
// the sandbox. The first is confusing on arrival, the second is invisible
// until someone goes looking for the commit.
RequireWriteBack bool
// RequireSecretFiles demands a node that delivers a secret lease's
// credential *files* to the workload, not only its environment.
//
// The failure it prevents is the quietest one in this list. A node that
// drops the files still starts, still runs, and still receives the
// environment variables naming them — so the workload holds
// GIT_CONFIG_GLOBAL pointing at a directory that does not exist, gets no
// token (a narrow github_pat deliberately exports none), and fails to
// authenticate with nothing in the transcript connecting that to the
// grant. Refusing placement is what turns it into a sentence.
RequireSecretFiles bool
// RequireRevocation demands a node that can take a secret lease's material
// back from a workload that is already running.
//
// It is the constraint that makes "revoking a lease takes the credential
// away" a property of the system rather than of which backend a project
// happens to be bound to. A driver that cannot honour a revocation still
// starts the workload and still receives the credential — it simply has no
// way to give it back, so the TTL and the push revocation become advice.
// Refusing placement is the only point at which that is visible.
RequireRevocation bool
// RequireStream and RequireSignal demand live output and the ability to
// stop a workload — the two capabilities the Web UI's run panel needs.
RequireStream bool
RequireSignal bool
// AllowDegraded permits placement on a degraded node. Default true via
// the zero value being "not forbidden"; set ForbidDegraded to tighten.
ForbidDegraded bool
// IgnoreCapacity skips the MaxConcurrent check.
IgnoreCapacity bool
}
Requirements describes what a task needs from a node. The zero value means "anything healthy", so a caller with no special needs still benefits from health filtering and capacity-aware tie-breaking.
type ResourceCeiling ¶ added in v0.0.2
type ResourceCeiling struct {
// CPUMillis caps the CPU allowance in thousandths of a core.
CPUMillis int `json:"cpu_millis,omitempty"`
// MemoryMB caps resident memory in megabytes.
MemoryMB int `json:"memory_mb,omitempty"`
// DiskMB caps the writable-layer / scratch ceiling in megabytes.
DiskMB int `json:"disk_mb,omitempty"`
// PIDs caps the number of processes/threads.
PIDs int `json:"pids,omitempty"`
}
ResourceCeiling bounds what a workload may be given. Zero on any field means "this ceiling says nothing about that resource" — not "zero of it" — which is what lets a ceiling constrain memory alone and leave CPU to whatever the executor would otherwise have done.
It mirrors ResourceLimits field for field on purpose. The two are separate types precisely because they are not interchangeable: assigning one to the other is the bug this file was written to remove, and the compiler now refuses it.
func CeilingFor ¶ added in v0.0.2
func CeilingFor(projectPath string) ResourceCeiling
CeilingFor returns the single ceiling in force for projectPath: the fleet's and the project's, tightened together.
This is what a driver calls. The drivers need it because they, not this package, hold the last word on what a workload is given — see BoundSpec for why the ceiling cannot simply be written onto the Spec ahead of them.
func FleetResourceCeiling ¶ added in v0.0.2
func FleetResourceCeiling() ResourceCeiling
FleetResourceCeiling returns the ceiling in force, or the zero ceiling if none was installed.
func ProjectResourceCeiling ¶ added in v0.0.2
func ProjectResourceCeiling(projectPath string) (ResourceCeiling, bool)
ProjectResourceCeiling returns the ceiling recorded for projectPath.
func (ResourceCeiling) Describe ¶ added in v0.0.2
func (c ResourceCeiling) Describe() string
Describe renders the ceiling for a UI or a log line, tightest fields first in a stable order. An empty ceiling renders as "unlimited" rather than as the empty string, because a blank field where a policy was expected reads as a rendering bug.
func (ResourceCeiling) IsZero ¶ added in v0.0.2
func (c ResourceCeiling) IsZero() bool
IsZero reports whether the ceiling constrains nothing.
func (ResourceCeiling) Tighten ¶ added in v0.0.2
func (c ResourceCeiling) Tighten(other ResourceCeiling) ResourceCeiling
Tighten returns the stricter of two ceilings, resource by resource.
"Stricter" is the smaller *non-zero* value, because zero is silence rather than a bound of nothing: a ceiling naming only memory must not erase a CPU cap the other one set.
This is the operation the fleet ceiling is accumulated with, and it is one-directional for the same reason executor.ApplyMinAgentBuild is. A hub reads many projects' config.yaml; combining them symmetrically would let one tenant's file raise a fleet-wide cap for every other tenant in the process. Loosening a fleet ceiling is deliberate and explicit — restart with the looser config, the ceremony every other security-relevant setting asks for.
func (ResourceCeiling) Validate ¶ added in v0.0.2
func (c ResourceCeiling) Validate() error
Validate rejects negative caps. Zero (unset) is always valid.
Negative is rejected rather than normalised because in ResourceLimits a negative PIDs value is the runtimes' "unlimited" sentinel, and a *ceiling* that means unlimited is a ceiling that should not have been written. Silently reading it as zero would turn a cap an operator believed they had set into no cap at all, which is the one failure mode this type may not have.
type ResourceLimits ¶
type ResourceLimits struct {
// CPUMillis is the CPU allowance in thousandths of a core (1000 = 1 CPU).
CPUMillis int `json:"cpu_millis,omitempty"`
// MemoryMB is the resident memory ceiling in megabytes.
MemoryMB int `json:"memory_mb,omitempty"`
// DiskMB is the writable-layer / scratch-space ceiling in megabytes.
DiskMB int `json:"disk_mb,omitempty"`
// PIDs is the maximum number of processes/threads.
PIDs int `json:"pids,omitempty"`
}
ResourceLimits bounds a single workload. Zero means "no limit requested". Drivers that do not advertise SupportsResourceLimits ignore this struct entirely rather than failing the Start.
func (ResourceLimits) IsZero ¶
func (rl ResourceLimits) IsZero() bool
IsZero reports whether no limit at all was requested.
func (ResourceLimits) Validate ¶
func (rl ResourceLimits) Validate() error
Validate rejects negative limits. Zero (unset) is always valid.
type RevocationDeniedError ¶
type RevocationDeniedError struct {
// ExecutorID and Kind name the driver that was refused.
ExecutorID string
Kind string
// Bindings are the revocable bindings that made the spec ineligible.
Bindings []SecretBinding
// Detail is the driver's own reason, when it implements Revoker but
// cannot honour a revocation at this moment (an offline agent, a
// protocol too old). Empty when the driver does not implement Revoker
// at all.
Detail string
}
RevocationDeniedError explains a refused placement in the terms the operator needs: which executor, which credentials, and what to do about it.
func (*RevocationDeniedError) Error ¶
func (e *RevocationDeniedError) Error() string
func (*RevocationDeniedError) Unwrap ¶
func (e *RevocationDeniedError) Unwrap() error
type RevocationLog ¶
type RevocationLog struct {
// contains filtered or unexported fields
}
RevocationLog is a driver's record of the leases it has been told to take back, so they can be replayed and reported.
It is shared by every Revoker rather than reimplemented per driver: the eviction rule above is subtle enough that three copies would eventually become three different rules, and the one that quietly dropped a pending entry would lose a revocation rather than a log line.
The zero value is not usable; call NewRevocationLog.
func NewRevocationLog ¶
func NewRevocationLog() *RevocationLog
NewRevocationLog returns an empty log.
func (*RevocationLog) Fail ¶
func (rl *RevocationLog) Fail(leaseID, grantID string, state RevokeState, err error)
Fail marks an entry undelivered, without discarding it: it stays in the log so a later replay can retry it.
func (*RevocationLog) Pending ¶
func (rl *RevocationLog) Pending() []RevokeOutcome
Pending returns the revocations still owed, oldest first.
func (*RevocationLog) Record ¶
func (rl *RevocationLog) Record(res RevokeOutcome) RevokeOutcome
Record inserts or refreshes an entry, returning a copy of the live state.
func (*RevocationLog) Settle ¶
func (rl *RevocationLog) Settle(leaseID, grantID string, ack RevokeReport, at time.Time)
Settle applies a driver's report to an entry.
func (*RevocationLog) Snapshot ¶
func (rl *RevocationLog) Snapshot() []RevokeOutcome
Snapshot returns every entry, newest first.
type RevokeAction ¶
type RevokeAction string
RevokeAction is what a driver should do about a workload still using the material being taken away.
const ( // RevokeScrub invalidates the material and lets the task keep running. // It will fail naturally the next time it reaches for the credential. // // This is the default because a long autonomous run is usually doing // many things, only one of which needed the revoked credential. Killing // it to revoke a kubeconfig it used an hour ago throws away hours of // work to close a window that scrubbing already closes. // // A driver that cannot scrub a particular material without terminating // the workload escalates and reports the kill. Silently doing nothing // would be the only worse answer than killing. RevokeScrub RevokeAction = "scrub" // RevokeKill scrubs and then terminates every task holding the lease. // // It exists because scrubbing has one hard limit: a credential handed to // a process as an environment variable is in that process's own memory, // and no control plane can reach into another process's heap. When the // credential itself is compromised rather than merely over-granted, // killing the task is the only thing that actually stops its use. RevokeKill RevokeAction = "kill" )
func (RevokeAction) Valid ¶
func (a RevokeAction) Valid() bool
Valid reports whether the action is one a driver knows.
type RevokeOutcome ¶
type RevokeOutcome struct {
LeaseID string `json:"lease_id"`
GrantID string `json:"grant_id,omitempty"`
ExecutorID string `json:"executor_id"`
State RevokeState `json:"state"`
// Action is what was asked for, echoed so a caller reading only the
// outcome knows whether a kill was requested.
Action RevokeAction `json:"action,omitempty"`
Reason string `json:"reason,omitempty"`
// SentAt / AckedAt bound how long the material was still live after the
// operator pressed the button. Operators ask this after an incident.
SentAt time.Time `json:"sent_at"`
AckedAt time.Time `json:"acked_at,omitempty"`
// Ack is the driver's report, present once State is RevokeStateRevoked.
Ack *RevokeReport `json:"ack,omitempty"`
// Error explains a failed or unreachable outcome.
Error string `json:"error,omitempty"`
}
RevokeOutcome is one executor's result for one lease revocation.
func (RevokeOutcome) Pending ¶
func (r RevokeOutcome) Pending() bool
Pending reports whether this outcome still needs to be delivered.
type RevokeReport ¶
type RevokeReport struct {
LeaseID string `json:"lease_id"`
GrantID string `json:"grant_id,omitempty"`
Action RevokeAction `json:"action,omitempty"`
// Known reports whether the driver was holding this lease at all. False
// is a success, not a failure: the material is not here, which is the
// end state the revocation asked for.
Known bool `json:"known"`
// EnvScrubbed names the variables whose values were dropped. Names only
// — echoing a revoked credential back to the control plane would be a
// fine way to write it into a log.
//
// A name here means the *control plane's* copy is gone. It does not mean
// a process that already forked with that variable has lost it; see
// Killed for the material that needed stronger treatment.
EnvScrubbed []string `json:"env_scrubbed,omitempty"`
// FilesRemoved counts credential files wiped.
FilesRemoved int `json:"files_removed,omitempty"`
// EgressDropped reports that the lease's network allowlist entry is gone
// *on this executor*.
//
// Only the remote driver sets it, and that is correct rather than an
// omission. A remote agent keeps an allowlist of its own on the device, so
// it has something local to drop. The hub's own drivers do not: the
// allowlist lives in the egress broker's proxy session, which the hub tears
// down with the lease itself before any driver is asked — reported as
// WipedLocally, not here. A container or Kubernetes driver claiming to have
// dropped it would be taking credit for another component's work, and would
// read as a second, independent guarantee where there is one.
EgressDropped bool `json:"egress_dropped,omitempty"`
// Killed lists the handles terminated — for RevokeKill, and for a
// RevokeScrub the driver could not honour without terminating.
Killed []string `json:"killed,omitempty"`
// Error is non-empty when part of the revocation failed. The report is
// still returned: "I tried and this is what went wrong" is far more
// actionable than silence, which a hub can only read as unreachable.
Error string `json:"error,omitempty"`
}
RevokeReport is what a driver actually did.
It is deliberately specific rather than a bare "ok". An operator revoking a kubeconfig needs to know whether the file is gone or whether the driver merely forgot about it, and a report that cannot distinguish the two would let a UI claim a guarantee the system did not deliver.
type RevokeRequest ¶
type RevokeRequest struct {
// LeaseID is the lease being revoked. Required.
LeaseID string `json:"lease_id"`
// GrantID narrows the revocation to one grant within the lease. Empty
// revokes everything the lease delivered.
GrantID string `json:"grant_id,omitempty"`
// Reason is operator-facing text carried into the driver's log and the
// audit trail, so "why did my run lose its token" has an answer on both
// sides of the link.
Reason string `json:"reason,omitempty"`
// Action is scrub or kill. An empty or unknown action is treated as
// scrub: the conservative reading of a request from a possibly-newer
// control plane is the one that does not destroy work.
Action RevokeAction `json:"action,omitempty"`
}
RevokeRequest takes one lease's material back from a running executor.
func (RevokeRequest) Effective ¶
func (r RevokeRequest) Effective() RevokeAction
Effective returns the action to apply, defaulting an absent or unknown value to RevokeScrub.
func (RevokeRequest) Matches ¶
func (r RevokeRequest) Matches(b SecretBinding) bool
Matches reports whether a binding is in scope for this request.
A request naming only a lease covers every grant under it; one naming a grant covers just that grant. Getting this backwards in either direction is a real failure — too narrow leaves material behind, too wide takes away credentials the operator did not revoke — so it is written once here rather than re-derived per driver.
type RevokeState ¶
type RevokeState string
RevokeState is how far one lease's revocation has got.
const ( // RevokeStatePending: the revocation is on its way, or is queued for a // driver that is not currently reachable but is expected back. RevokeStatePending RevokeState = "revoke_pending" // RevokeStateRevoked: the driver reported what it took back. RevokeStateRevoked RevokeState = "revoked" // RevokeStateUnreachable: the executor is offline, so the material is // still out there. The revocation is queued and will be replayed, but // until then this is the honest state and the UI must show it as such. RevokeStateUnreachable RevokeState = "unreachable" // RevokeStateFailed: the driver answered, and the answer was an error. RevokeStateFailed RevokeState = "failed" )
func (RevokeState) Terminal ¶
func (s RevokeState) Terminal() bool
Terminal reports whether the state can still change on its own. Only RevokeStateRevoked is final; a failure is retried on the next sweep and an unreachable executor is retried when it reconnects.
type Revoker ¶
type Revoker interface {
Executor
// SupportsRevocation reports whether a revocation issued *right now*
// would be honoured.
//
// It is separate from implementing the interface because the two answer
// different questions. Implementing Revoker is a property of the driver
// and is known at compile time; this is a property of the moment — a
// remote agent that is offline, or one speaking a protocol too old for
// the frame, cannot honour anything even though its driver can.
SupportsRevocation() bool
// HoldsLease reports whether any workload this executor tracks was
// started with material from leaseID.
HoldsLease(leaseID string) bool
// Leases lists the lease IDs this executor is currently holding material
// for, sorted.
Leases() []string
// RevokeLease takes one lease's material back.
//
// It returns an outcome rather than an error because the caller's next
// move depends on *which* failure: an error alone cannot say whether the
// material is gone, in doubt, or definitely still out there. A driver
// that was not holding the lease returns RevokeStateRevoked with
// Ack.Known false — "not here" is the end state a revocation wants.
RevokeLease(ctx context.Context, req RevokeRequest) RevokeOutcome
// Revocations reports this executor's revocation log, newest first, so
// the Secrets panel can show what landed and what is still owed.
Revocations() []RevokeOutcome
}
Revoker is an executor that can take a secret lease's material back from a workload it has already started.
It is optional in the Go sense and mandatory in the policy sense: a driver that does not implement it is refused any spec carrying revocable bindings (see RequireRevocable), so the omission surfaces as a named refusal at placement time rather than as a credential nobody can withdraw.
Implementations must be safe for concurrent use.
type RunResult ¶
type RunResult struct {
Handle Handle
Status Status
// Output is the workload's combined stdout+stderr.
Output []byte
// Dropped is true when the consumer fell behind and chunks were
// discarded, so Output is incomplete. Detected via LogLine.Seq gaps.
Dropped bool
// WriteBack is what the driver recovered of the workload's file changes,
// copied from the terminal Status. Nil when the Spec asked for none.
WriteBack *WriteBackResult
// Bundle is the git bundle the driver received, for a bundle-mode
// write-back. Empty for every other mode — a push leaves the objects at
// the origin, so there is nothing for the hub to carry.
Bundle []byte
}
RunResult is the outcome of a synchronous Run.
func Run ¶
Run starts spec on ex, collects its combined output, and blocks until the workload finishes.
Unlike Start, Run *does* tie the workload to ctx: if ctx is cancelled or its deadline expires before the workload exits, the handle is killed and the ctx error is returned alongside whatever output was collected. This gives callers the familiar exec.CommandContext semantics on top of the lifetime-independent primitives.
The returned error is non-nil when the workload could not be started, when ctx fired, or when the workload exited non-zero; RunResult is still populated in the latter two cases so callers can show the output.
type SecretBinding ¶
type SecretBinding struct {
// LeaseID is the broker lease this material was issued under.
LeaseID string `json:"lease_id"`
// GrantID is the specific grant within the lease, when the binding
// describes one grant rather than every material in the lease.
GrantID string `json:"grant_id,omitempty"`
// SecretName is the operator-facing name, for diagnostics.
SecretName string `json:"secret_name,omitempty"`
// Kind is the credential kind ("github_pat", "kubeconfig", "egress", …).
Kind string `json:"kind,omitempty"`
// EnvKeys names the environment variables this lease contributed. Keys
// only, never "K=V" — a value here would defeat the whole point.
EnvKeys []string `json:"env_keys,omitempty"`
// Files are the credential file paths as the *executor* sees them.
Files []string `json:"files,omitempty"`
// Dir is the lease directory holding Files.
Dir string `json:"dir,omitempty"`
// Egress marks a binding that also opened a network path (an egress
// proxy session), so revoking it must drop the allowlist entry and not
// only the credential.
Egress bool `json:"egress,omitempty"`
// ExpiresAt is the lease TTL, so a driver can expire material locally
// even while it cannot reach the control plane.
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
SecretBinding says which parts of a Spec came from one secret lease.
It carries no credential material — only the *names* of the environment variables and the *paths* of the files a lease contributed. The material itself is already in Env and on disk; duplicating it here would put plaintext into a struct that gets persisted (executorstore records the dispatched spec) and logged.
The binding exists because revocation needs attribution. Without it an executor holding GITHUB_TOKEN has no way to know that variable came from lease_abc rather than from the operator's own environment, so "revoke lease_abc" could only be honoured by killing the whole workload or by doing nothing. With it, a driver scrubs exactly what that lease delivered and leaves everything else alone.
func (SecretBinding) Revocable ¶
func (b SecretBinding) Revocable() bool
Revocable reports whether this binding describes material that can meaningfully be taken back mid-run: it must name a lease and something that lease actually delivered.
type SecretFile ¶
type SecretFile struct {
// LeaseID and GrantID attribute the file, so a driver can tie what it
// wrote back to the binding that must later scrub it.
LeaseID string
GrantID string
// Dir is the absolute directory the workload expects the file in.
Dir string
// Name is a bare file name: no separators, no "." or "..".
Name string
// Mode is the permission bits to create it with. Zero means 0600.
Mode fs.FileMode
// Content is the plaintext. This is the credential; everything else in
// this struct is bookkeeping.
Content []byte
}
SecretFile is one credential file plus the directory the workload expects to find it in.
Dir is the path *as the workload sees it*, which the broker has already baked into the environment (GIT_CONFIG_GLOBAL, KUBECONFIG, CLOOP_LEASE_DIR). A driver that can honour it verbatim — a bind mount, a projected volume — leaves the environment alone. A driver that must place the files somewhere else calls Spec.RelocateSecrets so the environment follows them.
func (SecretFile) FileMode ¶
func (f SecretFile) FileMode() fs.FileMode
FileMode returns the mode to create the file with, defaulting to 0600.
func (SecretFile) GoString ¶
func (f SecretFile) GoString() string
GoString mirrors String so %#v cannot print the content either.
func (SecretFile) Path ¶
func (f SecretFile) Path() string
Path is where this file belongs, as the workload sees it.
func (SecretFile) String ¶
func (f SecretFile) String() string
String renders the file without its content, so a `%v` on a Spec — or on a driver's own debug log — cannot write a live credential into a transcript. Marshalling is unaffected; only fmt goes through here.
func (SecretFile) Validate ¶
func (f SecretFile) Validate() error
Validate checks the invariants a driver relies on before it writes anything.
The name check is the important one and it is duplicated from secretbroker.Mount.writeFile deliberately: this struct crosses a process boundary on the remote path, so the receiving agent must re-derive the guarantee rather than inherit the sender's word for it. A name that is not a bare file name is an arbitrary-file-write primitive aimed at whatever host the driver runs on.
type Session ¶
type Session struct {
// ID is unique across the control plane.
ID string `json:"id"`
// ExecutorID is the node the session was dispatched to.
ExecutorID string `json:"executor_id"`
// HandleID is the driver-side handle, when one was obtained.
HandleID string `json:"handle_id,omitempty"`
// ProjectPath and TaskID identify the work, so failover can mark the
// task failed-with-retry and re-dispatch it.
ProjectPath string `json:"project_path,omitempty"`
TaskID int `json:"task_id,omitempty"`
// ClaimToken is the exactly-once guard. Requeueing a session requires
// presenting the token the session currently carries; the store rotates
// it atomically, so a second requeue attempt — from a concurrent
// supervisor, or from this one after a restart — finds a token that no
// longer matches and is refused with ErrSessionClaimLost.
ClaimToken string `json:"claim_token"`
// Attempt counts dispatches, starting at 1.
Attempt int `json:"attempt"`
StartedAt time.Time `json:"started_at"`
// Spec is the workload the session was dispatched with, so a failover
// can re-dispatch it verbatim. It is persisted rather than held in
// memory precisely because the control plane that requeues a session is
// often not the one that started it.
Spec Spec `json:"spec"`
}
Session is one workload dispatched to an executor, as the scheduler sees it.
It is deliberately thinner than a Handle: failover needs to know what to re-run and who currently owns the right to re-run it, not how to stream its output.
type SessionStore ¶
type SessionStore interface {
// RunningSessions returns the sessions currently in flight on an
// executor. Passing "" returns every running session.
RunningSessions(executorID string) ([]Session, error)
// ClaimRequeue atomically transfers ownership of a session away from its
// current claim token, returning the session with its new token.
//
// It must be a single conditional write ("UPDATE ... WHERE id = ? AND
// claim_token = ?"): that is the entire double-execution guard. It
// returns ErrSessionClaimLost when the token no longer matches.
ClaimRequeue(sessionID, claimToken string, at time.Time) (Session, error)
// CountRunning returns the number of in-flight sessions on an executor,
// which drain waits on and the UI displays.
CountRunning(executorID string) (int, error)
}
SessionStore tracks in-flight work per executor and arbitrates requeues.
type Signal ¶
type Signal string
Signal is a driver-independent termination request. Concrete OS signal numbers are a host-execution detail that a container or remote driver may not be able to express, so the interface speaks in intent.
const ( // SignalInterrupt asks the workload to stop gracefully (SIGINT). // cloop runs trap this and checkpoint before exiting. SignalInterrupt Signal = "interrupt" // SignalTerminate asks the workload to stop (SIGTERM). SignalTerminate Signal = "terminate" // SignalKill stops the workload immediately and unconditionally. SignalKill Signal = "kill" )
type Spec ¶
type Spec struct {
// WorkDir is the directory the workload runs in. For host-local drivers
// this is a real path; for remote drivers it is the path *on the
// executor*, which the driver is responsible for provisioning.
WorkDir string `json:"work_dir"`
// Argv is the command and its arguments. Argv[0] is the program. It is
// never passed through a shell — no quoting or injection surface.
Argv []string `json:"argv"`
// Env is the environment in "K=V" form. A nil Env means "inherit the
// control plane's environment", matching os/exec semantics; an empty
// non-nil slice means "start with an empty environment".
Env []string `json:"env,omitempty"`
// Labels carry routing and bookkeeping metadata (project path, task ID,
// requesting user). Drivers may surface them for observability; they
// never affect execution.
Labels map[string]string `json:"labels,omitempty"`
// ResourceLimits bounds the workload where the driver supports it.
ResourceLimits ResourceLimits `json:"resource_limits,omitempty"`
// TimeoutMinutes kills the workload after this long. 0 means no
// timeout — cloop runs are long-lived by design (Task 20148 removed the
// implicit task timeout), so this must stay opt-in.
TimeoutMinutes int `json:"timeout_minutes,omitempty"`
// Image overrides the executor's configured sandbox image for this run.
// Empty means "use the executor's image". A driver that does not
// advertise SupportsImageOverride must reject a non-empty value rather
// than run the wrong toolchain silently.
Image string `json:"image,omitempty"`
// SetupCommands are shell commands baked into a derived image once per
// unique (base image, command list) pair — not re-run per workload.
// Requires SupportsSandboxBuild.
SetupCommands []string `json:"setup_commands,omitempty"`
// Mounts re-expose sub-paths of WorkDir elsewhere in the sandbox. See
// SpecMount: sources are workspace-relative and cannot escape it.
Mounts []SpecMount `json:"mounts,omitempty"`
// HostMounts bind absolute paths on the executor's host into the
// sandbox. Unlike Mounts they may point anywhere, which is why nothing
// that parses a repo-committed file is allowed to set them: the only
// writer is a secret lease (secretbroker.KindLocalRepo), where a human
// named the path and the broker recorded who. See HostMount.
HostMounts []HostMount `json:"host_mounts,omitempty"`
// Devices expose host device nodes inside the sandbox. Like HostMounts
// and for the same reason, nothing that parses a repo-committed file may
// set them: the only writer is a secret lease
// (secretbroker.KindHostDevice), where a human named the device and the
// broker recorded who. A sandbox spec may select among what the project
// was granted, never add to it. See HostDevice.
Devices []HostDevice `json:"devices,omitempty"`
// EgressScope confines this workload's IP-layer network reach
// independently of the other workloads on the same executor.
//
// One-directional like DisableNetwork: a scope can only take reach away.
// A project that needs to reach private address space says so with an
// egress grant, which an operator issues; see egressscope.go.
EgressScope EgressScope `json:"egress_scope,omitempty"`
// DisableNetwork forces this workload off the network regardless of how
// the executor is configured.
//
// It is deliberately one-directional. A sandbox spec is repo-committed
// input, so it may *narrow* what the operator granted and may never widen
// it; there is no corresponding EnableNetwork field, and a spec that wants
// egress gets it only by the executor already having it (see
// Requirements.RequireNetworkEgress, which refuses placement instead of
// quietly turning the network on).
DisableNetwork bool `json:"disable_network,omitempty"`
// SandboxHash identifies the sandbox spec this workload was built from,
// for the audit trail. Drivers surface it as a label; it never affects
// execution.
SandboxHash string `json:"sandbox_hash,omitempty"`
// Secrets attributes parts of Env and the filesystem to the secret
// leases that produced them, so a driver can take an individual
// credential back mid-run. See SecretBinding.
Secrets []SecretBinding `json:"secrets,omitempty"`
// SecretFiles carries the *contents* of those credential files, for a
// driver that has to place them itself because the workload cannot read
// the control plane's filesystem. Empty when the hub materialised them
// (see Capabilities.SecretFilesFromHostPath) — then Secrets[].Files
// already names paths the workload can open.
//
// json:"-" is a hard rule, not a default. This is the one field of a Spec
// that holds plaintext, and a Spec is persisted by pkg/executorstore,
// echoed into the audit trail, and re-read by reconcile after a restart.
// A wire format that legitimately needs the bytes opts in with a type of
// its own; see remote.StartPayload.SecretFiles.
SecretFiles []SecretFile `json:"-"`
// Workspace says how the source tree gets into WorkDir. It carries no
// credential — only the name of a grant — for the reasons set out in
// workspace.go. The zero value is "unspecified", which leaves a driver's
// pre-existing behaviour alone.
Workspace Workspace `json:"workspace,omitempty"`
// WriteBack says how the file changes the workload produces get back to
// the hub. It carries no credential either — a push reuses the grant
// Workspace named — for the reasons set out in writeback.go. The zero
// value means nothing is returned, which is correct for a driver that
// shares the hub's filesystem and wrong for every other one; the caller
// that builds the Spec is responsible for saying so.
WriteBack WriteBack `json:"write_back,omitempty"`
}
Spec fully describes one workload to start. It is intentionally serializable: a remote driver marshals it and ships it to an agent.
func (Spec) NeedsSecretFiles ¶
NeedsSecretFiles reports whether this Spec depends on credential files reaching the workload — either because the hub already wrote them and the bindings name them, or because the bytes are travelling with the Spec.
It is what SandboxRequirements derives RequireSecretFiles from, so both delivery modes are gated by the same question.
func (Spec) Redactor ¶ added in v0.0.2
Redactor returns a redact.Set over SecretValues, or nil when this Spec carries no credential — which lets a driver install it unconditionally.
func (*Spec) RelocateSecrets ¶
RelocateSecrets rewrites every reference to the lease directory from so it names to instead: the environment the broker built, the bindings the vault indexes, and the files themselves.
A driver needs this when it cannot place the files where the hub said. The remote agent is the case: the hub has no idea what is writable on an edge device, so it declares a nominal directory, and the agent picks a real one under its own tmpfs and calls this. Without it the workload would find GIT_CONFIG_GLOBAL pointing at a path the agent never created — the exact failure the whole file exists to remove, moved one hop.
Prefix matching is safe because a lease directory name carries 96 bits of randomness: no unrelated value in the environment can begin with it.
func (Spec) RevocableSecrets ¶
func (s Spec) RevocableSecrets() []SecretBinding
RevocableSecrets returns the bindings in this spec that carry revocable material. A driver that cannot honour revocation must refuse such a spec rather than run it and silently drop the guarantee.
func (Spec) SandboxRequirements ¶
func (s Spec) SandboxRequirements() Requirements
SandboxRequirements returns the placement constraints implied by the sandbox-shaped fields of this Spec.
It exists so the one mapping from "what the spec asks for" to "what an executor must therefore support" lives beside the fields themselves, instead of being re-derived (and eventually re-derived differently) at each of the several call sites that place work.
func (Spec) SecretValues ¶ added in v0.0.2
SecretValues returns the plaintext this Spec carries into the workload, for a driver to scrub out of the output it captures.
Two sources, matching the two ways material reaches a workload:
- SecretFiles, when the driver places the bytes itself (a container's staged tmpfs, a projected Kubernetes Secret, a remote agent's vault).
- Env, for the variables the lease declared credential-bearing.
A localprocess workload has neither: the hub materialised the files on the shared filesystem, so Spec.SecretFiles is empty and only the paths travel. That case is covered on the other side of the boundary, where the workload reads its own lease directory — see redact.FromEnviron. Both halves exist because output is captured on both sides.
type SpecMount ¶
type SpecMount struct {
// Source is a slash-separated path relative to Spec.WorkDir. It may not
// be absolute and may not contain a ".." element.
Source string `json:"source"`
// Target is the absolute path the source appears at inside the sandbox.
Target string `json:"target"`
// ReadOnly maps the source read-only.
ReadOnly bool `json:"read_only,omitempty"`
}
SpecMount re-exposes a path inside the workload's workspace at another absolute path inside the sandbox.
The canonical use is a cache: `source: .cache/pip, target: /home/agent/.cache/pip` lets a project keep its dependency cache in the repo tree — where it is already bind-mounted, already size-bounded, and already discarded with the workspace — while the toolchain finds it at the fixed path it insists on.
type State ¶
type State string
State is the lifecycle phase of a handle.
const ( // StatePending: accepted by the driver but not yet running (queued, // image pulling, remote agent dispatch in flight). StatePending State = "pending" // StateRunning: the workload is executing. StateRunning State = "running" // StateExited: the workload finished on its own; ExitCode is valid. StateExited State = "exited" // StateFailed: the workload could not be run, or the driver lost track // of it; Error explains why. StateFailed State = "failed" // StateKilled: the workload was terminated by a signal or by its // timeout rather than exiting on its own. StateKilled State = "killed" // StateUnknown: the driver cannot determine the state (e.g. a remote // executor is currently unreachable). StateUnknown State = "unknown" )
type Status ¶
type Status struct {
HandleID string `json:"handle_id"`
ExecutorID string `json:"executor_id"`
State State `json:"state"`
PID int `json:"pid,omitempty"`
ExitCode int `json:"exit_code"`
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at,omitempty"`
// Error carries the driver-side failure detail for StateFailed. It is
// a string rather than an error so Status stays serializable across
// the remote-executor boundary.
Error string `json:"error,omitempty"`
// WriteBack reports what the driver recovered of the workload's file
// changes, once the workload is terminal. Nil means the driver returned
// nothing — either the Spec asked for no write-back, or the workload is
// still running. It is metadata only; the bundle bytes, when there are
// any, are collected through WriteBackFetcher.
WriteBack *WriteBackResult `json:"write_back,omitempty"`
}
Status is a point-in-time snapshot of a handle.
func LiveHandles ¶
LiveHandles returns the currently-running handles on ex and whether ex could report at all. A false second return means "this driver does not enumerate", which a UI should render as "—", not as zero: showing an idle executor when it is actually saturated is worse than admitting ignorance.
type StreamName ¶
type StreamName string
StreamName identifies which output stream a LogLine came from.
const ( StreamStdout StreamName = "stdout" StreamStderr StreamName = "stderr" StreamCombined StreamName = "combined" )
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor probes registered executors, maintains their scheduling state, and fails work over when a node dies holding it.
func NewSupervisor ¶
func NewSupervisor(reg *Registry, cfg SupervisorConfig, opts ...SupervisorOption) *Supervisor
NewSupervisor builds a supervisor over reg. A nil registry uses DefaultRegistry.
func (*Supervisor) Cordon ¶
func (sv *Supervisor) Cordon(executorID, reason string) (Health, error)
Cordon takes an executor out of rotation, persisting and announcing it.
func (*Supervisor) Drain ¶
func (sv *Supervisor) Drain(executorID, reason string) (Health, error)
Drain takes an executor out of rotation and marks it for retirement.
func (*Supervisor) Health ¶
func (sv *Supervisor) Health(executorID string) Health
Health returns the current health of one executor.
func (*Supervisor) ProbeOnce ¶
func (sv *Supervisor) ProbeOnce(ctx context.Context) []Transition
ProbeOnce probes every executor that is currently due and returns the transitions it caused, ordered by executor ID.
It is exported because it is the whole supervisor minus the timer: tests drive fleets through arbitrary state sequences by calling it, and `cloop executor ls` uses it to refresh before printing rather than showing an operator a stale table.
func (*Supervisor) Start ¶
func (sv *Supervisor) Start(ctx context.Context) (stop func())
Start launches the probe loop and returns immediately.
The returned stop function cancels the loop and waits for in-flight probes to finish, so a caller shutting down does not race a probe writing to a closed store. Calling Start twice is a no-op on the second call.
func (*Supervisor) Uncordon ¶
func (sv *Supervisor) Uncordon(executorID string) (Health, error)
Uncordon returns an executor to the state its probe history justifies.
func (*Supervisor) WaitForDrain ¶
func (sv *Supervisor) WaitForDrain(ctx context.Context, executorID string, timeout, poll time.Duration) (int, error)
WaitForDrain blocks until an executor has no in-flight sessions, ctx fires, or timeout elapses. A zero timeout waits indefinitely (bounded by ctx).
It returns ErrDrainTimeout with the remaining count when work is still in flight at the deadline, so `cloop executor drain --timeout` can tell an operator how many sessions they would be abandoning with --force.
type SupervisorConfig ¶
type SupervisorConfig struct {
// Interval is the probe period for a healthy node.
Interval time.Duration
// ProbeTimeout bounds one HealthCheck call. It must be well under
// Interval so a hung probe cannot stack up rounds.
ProbeTimeout time.Duration
// BackoffBase is the first retry delay after a failure; each subsequent
// consecutive failure doubles it up to BackoffMax.
BackoffBase time.Duration
BackoffMax time.Duration
// JitterFraction is the proportional jitter applied to every delay
// (0.2 = ±20%). Clamped to [0, 0.9].
JitterFraction float64
// MaxConcurrentProbes bounds how many nodes are probed at once.
MaxConcurrentProbes int
// Policy sets the degrade/unreachable thresholds.
Policy HealthPolicy
// Rand returns a value in [0,1) for jitter. Nil uses a package-level
// source; tests inject a deterministic one.
Rand func() float64
}
SupervisorConfig tunes probing. The zero value is usable; DefaultSupervisorConfig spells out the defaults.
func DefaultSupervisorConfig ¶
func DefaultSupervisorConfig() SupervisorConfig
DefaultSupervisorConfig returns production defaults: probe every 30s, give up after three consecutive failures (~90s), back off from 5s to 5m while down.
type SupervisorOption ¶
type SupervisorOption func(*Supervisor)
SupervisorOption configures a Supervisor.
func WithCandidateSource ¶
func WithCandidateSource(fn func() []Candidate) SupervisorOption
WithCandidateSource overrides how the failover placement pool is assembled.
func WithEventSink ¶
func WithEventSink(s EventSink) SupervisorOption
WithEventSink routes transitions and failovers to the event log.
func WithFailoverHandler ¶
func WithFailoverHandler(h FailoverHandler) SupervisorOption
WithFailoverHandler installs the re-dispatch callback.
func WithHealthStore ¶
func WithHealthStore(s HealthStore) SupervisorOption
WithHealthStore persists health across restarts. Without one the supervisor keeps state only in the store's absence — that is, nowhere — and every restart re-learns the fleet.
func WithSessionStore ¶
func WithSessionStore(s SessionStore) SupervisorOption
WithSessionStore enables failover. Without one, an unreachable node still transitions and emits events, but no work is moved.
type Transition ¶
type Transition struct {
ExecutorID string `json:"executor_id"`
From NodeState `json:"from"`
To NodeState `json:"to"`
Reason string `json:"reason,omitempty"`
At time.Time `json:"at"`
}
Transition records one state change, for the event log and the UI.
func (Transition) String ¶
func (t Transition) String() string
String renders a transition as "id: ready -> unreachable (reason)".
type Workspace ¶
type Workspace struct {
// Kind selects the strategy.
Kind WorkspaceKind `json:"kind,omitempty"`
// Repo is the clone URL for Kind git. https only: a token delivered over
// cleartext http is not a short-lived credential, it is a published one.
Repo string `json:"repo,omitempty"`
// Ref is the branch, tag or commit to check out. Empty means the remote's
// default branch.
Ref string `json:"ref,omitempty"`
// Depth is the shallow-fetch depth; 0 fetches full history.
Depth int `json:"depth,omitempty"`
// CredentialGrant names the secret grant whose material authenticates the
// fetch — never the material itself. Empty means an unauthenticated
// fetch, which only works for a public repository.
CredentialGrant string `json:"credential_grant,omitempty"`
// SizeLimitMB bounds the provisioned tree, from the project's
// .cloop/sandbox.yaml resources.disk. 0 means the executor's own default.
SizeLimitMB int `json:"size_limit_mb,omitempty"`
}
Workspace describes how to materialise the project's source tree inside the executor. The zero value means "unspecified"; see WorkspaceUnspecified.
func (Workspace) BaseURL ¶
BaseURL returns "https://host/", the key prefix an http.<base>.extraHeader setting is scoped to.
Scoping matters: an unscoped http.extraHeader is sent to *every* host git contacts, including whatever a redirect points at. Scoping it to the origin means a repository that redirects elsewhere gets a fetch failure rather than a leaked Authorization header.
func (Workspace) Describe ¶
Describe renders the workspace for a log line or an audit row. It can never contain a credential — Workspace has no field that could hold one — so it is safe to emit anywhere.
func (Workspace) GitPlan ¶
GitPlan returns the ordered commands that materialise w into dir.
It is init + fetch + checkout rather than `git clone`, because clone can only name a branch or tag with --branch while a fetch can name any ref including a bare commit SHA. A plan that works for "main" and silently fails for a pinned commit would be the sort of thing discovered in production.
The plan is pure: no I/O, no clock, no environment. That is what lets both the Kubernetes init container and the remote agent render the *same* sequence, and lets a test assert on it without a git binary.
func (Workspace) NeedsProvisioning ¶
NeedsProvisioning reports whether the executor must do work before the harness can start. This is the predicate that turns into a placement requirement, so it is deliberately narrow: only git needs anything.
func (Workspace) RepoPath ¶
RepoPath returns the "owner/name" form used to match a GitHub grant's repository allowlist, and whether the URL had that shape at all.
func (Workspace) RequiresCredential ¶
RequiresCredential reports whether provisioning needs a leased credential.
func (Workspace) Validate ¶
Validate checks the driver-independent invariants.
Every rule here is about the fields becoming an argv, an environment variable, or a URL a moment later. A ref that starts with "-" would be read by git as a flag; a repo URL carrying userinfo would smuggle a credential into a struct that gets persisted and logged; a non-https scheme would put a brokered token on the wire in cleartext.
type WorkspaceAccess ¶
type WorkspaceAccess struct {
// Credential authenticates the git commands. It may be empty, for the
// unauthenticated fetch of a public repository.
Credential GitCredential
// Repo, when non-empty, replaces Workspace.Repo for every git command the
// executor runs against this workspace — the fetch that provisions it and
// the push that writes back. Empty means no redirection.
Repo string
}
WorkspaceAccess is what one git workspace gets: the material, and the repository URL that material is good against.
The two travel together because a brokered credential only means anything against the URL it was brokered for. With no interception the credential is the forge token and Repo is empty, meaning "leave Workspace.Repo alone". When the hub interposes a git proxy the credential is an ephemeral session token and Repo is the proxy's URL for that session — the forge token stays on the hub, and the sandbox's git talks to the proxy instead.
Returning the URL alongside the credential, rather than leaving the driver to discover the redirection some other way, is what makes the interception impossible to skip by accident: a driver that ignores Repo sends the sandbox at the forge holding a token the forge has never heard of, which fails immediately and loudly instead of quietly restoring the un-proxied path.
func (WorkspaceAccess) Apply ¶
func (a WorkspaceAccess) Apply(w Workspace) Workspace
Apply returns w routed according to the access.
Drivers call this once and pass the result everywhere they would have passed the original: the spec shipped to a remote agent, the plan rendered into an init container, the write-back request. Rewriting Repo in one place keeps the fetch and the push pointed at the same host, which matters because the credential is scoped to exactly one origin — split them and the push authenticates against a URL its header does not cover.
type WorkspaceCredentialSource ¶
type WorkspaceCredentialSource interface {
// ForWorkspace leases material authorising a fetch of w on behalf of
// projectID. A workspace that needs no credential yields the zero
// WorkspaceAccess, a no-op release, and a nil error.
//
// When no grant authorises the fetch the error is a
// *WorkspaceGrantError, which names the missing grant. Drivers surface it
// unchanged rather than starting a workload against an empty tree.
ForWorkspace(ctx context.Context, projectID string, w Workspace) (WorkspaceAccess, func(), error)
}
WorkspaceCredentialSource leases the access one git workspace fetch needs.
It is an interface with no secret-broker types in it so that pkg/executor stays a leaf package — the remote agent imports it and must not drag the hub's secret store onto an edge device — and so a driver test can supply a fake credential without a database, a sealing key, or a grant.
The release function returned alongside the access is always non-nil, including on error paths, so callers can defer it unconditionally.
type WorkspaceEvent ¶
type WorkspaceEvent struct {
Phase WorkspaceEventPhase
ExecutorID string
ExecutorKind string
HandleID string
ProjectPath string
Workspace Workspace
GrantID string
LeaseID string
// DurationMS is set on the end event only.
DurationMS int64
// Err is the failure, already redacted, or "" on success.
Err string
}
WorkspaceEvent is one provisioning observation for the compliance trail.
Provisioning is worth its own rows rather than being folded into the run's because it is the moment a brokered credential is used against an external service. "Which grant fetched which repository onto which executor, and did it work" is the question an operator has after an incident, and the run's own record cannot answer it — the run may look identical whether the tree arrived or not, which is the whole reason this subsystem exists.
It carries no credential. GrantID and LeaseID are identifiers; the token is never in scope here.
type WorkspaceEventPhase ¶
type WorkspaceEventPhase string
WorkspaceEventPhase distinguishes the two rows one provisioning produces.
const ( // WorkspaceProvisionStart is emitted before the first fetch byte moves. WorkspaceProvisionStart WorkspaceEventPhase = "start" // WorkspaceProvisionEnd is emitted once, whether it succeeded or not. WorkspaceProvisionEnd WorkspaceEventPhase = "end" )
type WorkspaceGrantError ¶
type WorkspaceGrantError struct {
// Repo is the clone URL that could not be authenticated.
Repo string
// RepoPath is the owner/name form, when the URL had one.
RepoPath string
// Grant is the grant the spec named, or "" when none was named and none
// could be inferred.
Grant string
// ExecutorID is the executor the workload was bound to.
ExecutorID string
// ProjectPath is the project whose run was refused.
ProjectPath string
// Reason distinguishes "no grant exists" from "a grant exists but excludes
// this repository" — different fixes, so they must not read alike.
Reason string
}
WorkspaceGrantError names the grant a workload needed and did not have.
It is typed, and it names the missing grant, because the alternative that this whole task exists to remove is a run that proceeds against an empty directory. "Missing credential" as a bare string would be indistinguishable from a dozen other refusals; the operator's next question is always "which repository, on which executor, and what do I create" — so the error answers all three and Remediation() prints the command.
func (*WorkspaceGrantError) Error ¶
func (e *WorkspaceGrantError) Error() string
Error implements error.
func (*WorkspaceGrantError) Remediation ¶
func (e *WorkspaceGrantError) Remediation() string
Remediation returns the command that fixes this, or "" when there is nothing specific to suggest.
func (*WorkspaceGrantError) Unwrap ¶
func (e *WorkspaceGrantError) Unwrap() error
Unwrap lets callers match errors.Is(err, ErrWorkspaceGrantMissing).
type WorkspaceKind ¶
type WorkspaceKind string
WorkspaceKind selects how a workload's source tree is materialised.
const ( // WorkspaceUnspecified is the zero value: the Spec says nothing, and the // driver keeps whatever behaviour it had. It exists so a caller that has // no workspace concern at all (a smoke test, `cloop executor test`) is not // forced to make a declaration it has no basis for. WorkspaceUnspecified WorkspaceKind = "" // WorkspaceBind: WorkDir already holds the tree because the executor // shares the control plane's filesystem. Only valid on a driver whose // Capabilities report SharesHostFilesystem. WorkspaceBind WorkspaceKind = "bind" // WorkspaceGit: the executor clones Repo at Ref into WorkDir before // starting the harness. WorkspaceGit WorkspaceKind = "git" // WorkspaceNone: an intentionally empty working tree. WorkspaceNone WorkspaceKind = "none" )
func (WorkspaceKind) Valid ¶
func (k WorkspaceKind) Valid() bool
Valid reports whether k is one of the known kinds.
type WriteBack ¶
type WriteBack struct {
// Mode selects the strategy.
Mode WriteBackMode `json:"mode,omitempty"`
// Branch is the per-task branch the sandbox commits to, e.g.
// "cloop/task-42-add-retry". It must not already carry a refs/ prefix.
Branch string `json:"branch,omitempty"`
// Message is the commit subject. Empty gets a generated one.
Message string `json:"message,omitempty"`
// MaxBundleBytes caps the bundle for Mode bundle. 0 means
// DefaultWriteBackBundleBytes; values above MaxWriteBackBundleBytes are a
// validation error rather than being silently clamped, because a project
// that asked for 1 GiB and got 128 MiB would fail later, further away.
MaxBundleBytes int64 `json:"max_bundle_bytes,omitempty"`
}
WriteBack describes how a workload's file changes should be returned.
Like Workspace it names a grant and never carries material, and for the same reasons: a Spec is persisted by pkg/executorstore, echoed into audit rows and marshalled across the remote boundary. There is deliberately no field here that could hold a token.
There is also deliberately no Remote field. A push goes to the workspace's own Repo and nowhere else — a sandbox that could name its own push target would be able to exfiltrate the tree to a host of its choosing using a credential the operator scoped to one repository.
func (WriteBack) Describe ¶
Describe renders the write-back for a log line or an audit row. It can never contain a credential — WriteBack has no field that could hold one.
type WriteBackFetcher ¶
type WriteBackFetcher interface {
// WriteBackBundle returns the bundle received for handleID. It returns
// ErrHandleNotFound for an unknown handle and ErrWriteBackUnavailable when
// the handle produced no bundle.
WriteBackBundle(handleID string) ([]byte, error)
}
WriteBackFetcher is the optional interface a driver implements when it can hand the hub the bundle bytes it received.
It is separate from Executor for the same reason Lister is: not every driver has one. The Kubernetes driver never carries a bundle (its work product goes out by push), and the local driver has nothing to carry — its changes are already on the hub's filesystem.
type WriteBackMode ¶
type WriteBackMode string
WriteBackMode selects how a workload's result travels back to the hub.
const ( // WriteBackNone is the zero value: the workload produces no durable file // changes, or the executor shares the hub's filesystem and the changes are // already there. Nothing is committed and nothing is sent. WriteBackNone WriteBackMode = "" // WriteBackPush: the sandbox commits to Branch and pushes it to the // workspace's own origin. Requires network egress and a credential grant // whose scope covers the repository. WriteBackPush WriteBackMode = "push" // WriteBackBundle: the sandbox commits to Branch and produces a git bundle // of the new commits, which travels back over the executor's own transport. // Requires a driver that can carry bytes back — today, the remote agent. WriteBackBundle WriteBackMode = "bundle" )
func (WriteBackMode) Valid ¶
func (m WriteBackMode) Valid() bool
Valid reports whether m is one of the known modes.
type WriteBackReason ¶
type WriteBackReason string
WriteBackReason is the closed set of write-back refusal classifications.
const ( // WriteBackReasonUnspecified is the zero value: a refusal that was not // classified. Its presence in a scrape is a bug in the hub, not in the // sandbox. WriteBackReasonUnspecified WriteBackReason = "unspecified" // WriteBackReasonBadBranch: the reported branch name is not a valid or // permitted ref. WriteBackReasonBadBranch WriteBackReason = "bad_branch" // WriteBackReasonBadCommit: a reported SHA is not a well-formed object // name. WriteBackReasonBadCommit WriteBackReason = "bad_commit" // WriteBackReasonEmptyRange: the reported head is the base it was built // on, or the range holds no commits. WriteBackReasonEmptyRange WriteBackReason = "empty_range" // WriteBackReasonUnknownBase: the base commit is not an object this // repository has, so the work is not built on anything the hub knows. WriteBackReasonUnknownBase WriteBackReason = "unknown_base" // WriteBackReasonFsck: git refused the bundled objects. WriteBackReasonFsck WriteBackReason = "fsck" // WriteBackReasonRefMoved: the quarantine ref is not where the executor // said it would be. WriteBackReasonRefMoved WriteBackReason = "ref_moved" // WriteBackReasonNotDescendant: the head does not descend from the base // it claims. WriteBackReasonNotDescendant WriteBackReason = "not_descendant" // WriteBackReasonTooManyCommits: the range is longer than the hub // applies. WriteBackReasonTooManyCommits WriteBackReason = "too_many_commits" // WriteBackReasonPath: an entry's path is not one the hub will write — // an escape, or a write into .git. WriteBackReasonPath WriteBackReason = "path" // WriteBackReasonMode: an entry's file mode is not permitted. WriteBackReasonMode WriteBackReason = "mode" )
func WriteBackReasonOf ¶
func WriteBackReasonOf(err error) (WriteBackReason, bool)
WriteBackReasonOf extracts the classification from any error in a write-back's chain, returning ("", false) when the error is not a rejection. Callers use it to separate a policy refusal from an infrastructure outage without type-asserting at every site.
type WriteBackRejection ¶
type WriteBackRejection struct {
// Path is the offending repository-relative path.
Path string
// Mode is the file mode that was refused, when the mode is the problem.
Mode string
// Target is the symlink destination, when that is the problem.
Target string
// Reason is the human-readable refusal.
Reason string
// Code classifies the refusal into a closed set, for metrics and for
// alerting.
//
// It exists because Reason cannot serve that purpose and must not be
// made to: it interpolates branch names, commit SHAs and paths, all of
// which come from the sandbox. Counting rejections by Reason would let
// a sandbox mint an unbounded number of Prometheus series by varying
// the branch it pushes — an out-of-memory the isolated workload gets to
// trigger from inside the box. Code is chosen by the hub from
// WriteBackReason*, so a sandbox can influence which bucket it lands in
// but never how many buckets exist.
//
// The zero value is WriteBackReasonUnspecified rather than a panic: a
// rejection with no code is still a valid rejection, and losing the
// refusal because its classification was forgotten would be strictly
// worse than losing the classification.
Code WriteBackReason
// Branch and CommitSHA locate the refusal in the sandbox's output.
Branch string
CommitSHA string
}
WriteBackRejection names the entry that was refused.
It is typed and it names the path because the operator's first question is always "what exactly did the sandbox try to write" — and because a rejection here is a security event, not a build failure. A bare "invalid bundle" would be indistinguishable from a corrupt download.
func (*WriteBackRejection) Error ¶
func (e *WriteBackRejection) Error() string
Error implements error.
func (*WriteBackRejection) ReasonCode ¶
func (e *WriteBackRejection) ReasonCode() WriteBackReason
ReasonCode returns the rejection's classification, normalising the zero value so a caller never has to special-case an unclassified refusal.
func (*WriteBackRejection) Unwrap ¶
func (e *WriteBackRejection) Unwrap() error
Unwrap lets callers match errors.Is(err, ErrWriteBackRejected).
type WriteBackResult ¶
type WriteBackResult struct {
// Mode is the mode that actually ran, which may be None when the spec
// asked for one and the tree turned out to be clean.
Mode WriteBackMode `json:"mode,omitempty"`
// Branch is the branch the sandbox committed to.
Branch string `json:"branch,omitempty"`
// CommitSHA is the tip the sandbox produced. Full 40-hex.
CommitSHA string `json:"commit_sha,omitempty"`
// BaseSHA is the commit the sandbox started from — the tip the workspace
// was provisioned at. The hub verifies the range base..commit rather than
// the whole branch, so a bundle cannot smuggle history that predates the
// checkout past the inspection.
BaseSHA string `json:"base_sha,omitempty"`
// Pushed reports whether the branch reached the origin.
Pushed bool `json:"pushed,omitempty"`
// BundleBytes and BundleSHA256 describe the bundle, for mode bundle. The
// digest is what lets the hub detect a truncated or altered stream before
// handing the file to git.
BundleBytes int64 `json:"bundle_bytes,omitempty"`
BundleSHA256 string `json:"bundle_sha256,omitempty"`
// Commits and FilesChanged are for the operator reading a task record.
Commits int `json:"commits,omitempty"`
FilesChanged int `json:"files_changed,omitempty"`
// Skipped reports that there was nothing to write back: the harness ran and
// changed no tracked file. It is distinct from a failure, and distinct from
// a write-back that never ran, because "the agent produced no edits" is a
// real and reportable outcome.
Skipped bool `json:"skipped,omitempty"`
// SkipReason explains Skipped in one phrase.
SkipReason string `json:"skip_reason,omitempty"`
// Err is the failure, already redacted, or "" on success.
Err string `json:"error,omitempty"`
}
WriteBackResult is what a driver reports about the work product it recovered.
It is metadata only. The bundle bytes, when there are any, stay in the driver and are collected through WriteBackFetcher — a Status is JSON-marshalled into audit rows and the executor store, and 32 MiB of base64 in an audit row is not a record, it is an outage.
func ScanWriteBackSentinel ¶
func ScanWriteBackSentinel(output string) (WriteBackResult, bool)
ScanWriteBackSentinel finds a sandbox's reported write-back in a workload's output.
It returns the *last* well-formed sentinel, and that is a security decision rather than a tie-break. The harness shares this stdout: model-authored code can print a line claiming any branch and commit it likes. What it cannot do is print one after it has exited — and the wrapper that performs the write-back emits its line only then, once the harness's stream is closed. So taking the last occurrence means a forged line can only ever be overwritten by the real one.
The forgery that survives is a workload with no genuine write-back at all, which can name any commit it wants. That is why nothing downstream trusts this value: pkg/writeback fetches the named branch, checks it is at the named SHA, checks it descends from the base the hub recorded, and inspects every path in the range before anything merges. The sentinel says where to look, never what is true.
func (*WriteBackResult) Delivered ¶
func (r *WriteBackResult) Delivered() bool
Delivered reports whether there is a commit the hub can act on.
func (*WriteBackResult) Describe ¶
func (r *WriteBackResult) Describe() string
Describe renders the result for a log line or an event row.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package agent is the device side of cloop's remote executor: the process that runs on an edge device, dials out to a control plane, and executes the workloads it is sent.
|
Package agent is the device side of cloop's remote executor: the process that runs on an edge device, dials out to a control plane, and executes the workloads it is sent. |
|
Package container implements the executor.Executor interface on top of the Docker/Podman command line, running each workload in a resource- and network-isolated sandbox.
|
Package container implements the executor.Executor interface on top of the Docker/Podman command line, running each workload in a resource- and network-isolated sandbox. |
|
Package gitcreds leases the credential a git workspace fetch needs.
|
Package gitcreds leases the credential a git workspace fetch needs. |
|
Package gitprovision materialises a workload's source tree where the workload is about to run.
|
Package gitprovision materialises a workload's source tree where the workload is about to run. |
|
Package gitproxycreds interposes a git interception proxy between a sandbox and the forge.
|
Package gitproxycreds interposes a git interception proxy between a sandbox and the forge. |
|
Package gitwriteback turns the files a harness changed into something the hub can merge.
|
Package gitwriteback turns the files a harness changed into something the hub can merge. |
|
Package install turns an enrollment bundle into a running, restart-on-boot, hardened cloop executor agent on an edge device.
|
Package install turns an enrollment bundle into a running, restart-on-boot, hardened cloop executor agent on an edge device. |
|
internal
|
|
|
gitforge
Package gitforge stands up a real, hermetic git remote for tests.
|
Package gitforge stands up a real, hermetic git remote for tests. |
|
logbus
Package logbus fans one workload's output out to any number of live subscribers, with a bounded replay backlog for late joiners.
|
Package logbus fans one workload's output out to any number of live subscribers, with a bounded replay backlog for late joiners. |
|
ptyshell
Package ptyshell runs a command attached to a pseudo-terminal.
|
Package ptyshell runs a command attached to a pseudo-terminal. |
|
Package kubernetes implements the executor.Executor interface by running each workload as an ephemeral Pod in a Kubernetes cluster.
|
Package kubernetes implements the executor.Executor interface by running each workload as an ephemeral Pod in a Kubernetes cluster. |
|
Package localprocess implements the executor.Executor interface by forking child processes on the control-plane host.
|
Package localprocess implements the executor.Executor interface by forking child processes on the control-plane host. |
|
Package reconcile turns the executors section of a config file into registered, preflighted executors — once, in one place, for every entry point that hosts a control plane (Task 20170).
|
Package reconcile turns the executors section of a config file into registered, preflighted executors — once, in one place, for every entry point that hosts a control plane (Task 20170). |
|
Package remote implements the control-plane half of cloop's remote executor: a driver that satisfies executor.Executor by proxying frames to a cloop agent running on another machine.
|
Package remote implements the control-plane half of cloop's remote executor: a driver that satisfies executor.Executor by proxying frames to a cloop agent running on another machine. |