Documentation
¶
Overview ¶
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 ApplyHostExecutionPolicy(allowed bool)
- func AuditWorkspace(ev WorkspaceEvent)
- func Bind(projectPath, executorID string) error
- func CheckSandboxSupport(ex Executor, req Requirements, projectPath string) error
- func Cordon(h Health, reason string, at time.Time) (Health, *Transition)
- func DescribeBindings(bindings []SecretBinding) 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 GitBaseEnv() []string
- func GitCredentialEnv(w Workspace, c GitCredential) ([]string, error)
- func HostExecutionAllowed() bool
- func InspectWriteBack(branch, commitSHA string, entries []BundleEntry) error
- func IsVirtualizedRuntime(name string) bool
- func IsolatedIDs() []string
- func IsolatesFromHost(ex Executor) bool
- func MarshalWriteBackSentinel(r WriteBackResult) (string, error)
- func ObserveProbe(h Health, probeErr error, at time.Time, policy HealthPolicy) (Health, *Transition)
- func RecordHandle(store HandleStore, rec HandleRecord)
- func RedactSecrets(s string, secrets []string) string
- func Register(ex Executor) error
- func RequireRevocable(ex Executor, spec Spec) error
- func SecretFileDirs(files []SecretFile) []string
- func SetAllowHostExecution(allowed bool) bool
- func SetBindingLookup(fn func(projectPath string) (string, 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 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 BundleEntry
- type Candidate
- type Capabilities
- type Clock
- type Constraint
- 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 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 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
- 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 ( // 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 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 ( // 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 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 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 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 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 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 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 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 so a redirect cannot carry it to a third party.
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 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 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 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.
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 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 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 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 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"`
// 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"`
// 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 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" 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" ConstraintWorkspace Constraint = "workspace" ConstraintWriteBack Constraint = "write_back" ConstraintSecretFiles Constraint = "secret_files" ConstraintRevocation Constraint = "revocation" )
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 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
// RequireContainerRuntime demands a node that can drive containers.
RequireContainerRuntime bool
// RequireNetworkEgress demands a node whose workloads can reach the
// network.
RequireNetworkEgress 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 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"`
// 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) 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.
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
|
|
|
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. |
|
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. |