executor

package
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

ceiling.go is about the difference between what a run asks for and what it is allowed to have.

Until this file existed cloop had only the first vocabulary. ResourceLimits is a *request*, and the drivers resolve it against the operator's config with one rule, stated in pkg/executor/container/container.go:

// Spec limits override the executor's configured defaults: the per-run
// request is more specific than the per-executor policy.

That is the right rule when the request comes from the operator, and the wrong one when it does not. A project's resource request is read from .cloop/sandbox.yaml — a file committed to the repository — so "the per-run request" is authored by whoever can push to that repo. Under the override rule such a file could name `memory: 900g` and receive it: the only thing in its way was config.ContainerMemoryMBUpper, a 1 TiB constant sized to catch a typo rather than a tenant. `executors.container.memory: 2g` did not bound it, because that setting was never a bound. It was a default.

So an operator had no way to write down the sentence this file exists to make expressible: *no workload on this executor gets more than this, whatever it asks for.*

Ceilings compose by getting tighter

There are two of them, and they answer different questions:

  • the fleet ceiling (config `executors.limits`) — what any workload on this hub may have;
  • the project ceiling (control-plane, operator-set) — what this one project may have, which is how a single noisy project gets held below the fleet's allowance without holding everything else down with it.

Neither is a request, so neither can raise anything. Apply them in sequence and the result is the minimum of the request and every ceiling that spoke, with each reduction attributed to the ceiling that caused it — which is the only way the developer reading "your task was given 2 GB" can find out who to ask for more.

ceilingpolicy.go holds the fleet-wide resource ceiling for the process.

It is the same shape as the min-agent-build switch in agentbuild.go, and for the same reason. A control plane reads many projects' config.yaml — every project it serves has one, and each is a file inside a repository that the project's own developers control. Installing those settings symmetrically would let one tenant's file raise a cap that applies to every other tenant in the process, which is not a configuration mistake but a privilege escalation with a YAML interface.

So the switch only ever tightens. Loosening a fleet ceiling means restarting with the looser config: deliberate, explicit, and performed by whoever controls the hub rather than by whoever controls a repository.

Package executor defines cloop's pluggable execution backend abstraction.

Historically the Web UI spawned harness processes directly on the host with raw exec.Command. That hard-wired three assumptions that no longer hold as cloop moves toward a hostable, multi-tenant product:

  1. the machine running the control plane is also the machine running the agent (no remote or edge executors),
  2. an agent run has the same privileges, filesystem, and network as the UI server itself (no isolation boundary), and
  3. 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

View Source
const (
	// CeilingSourceFleet is the hub-wide cap from config `executors.limits`.
	CeilingSourceFleet = "fleet"
	// CeilingSourceExecutor is the per-executor cap an admin set on the hub.
	//
	// It sits between the fleet's and the project's because it answers a
	// question neither of them can: *this machine* has 8 GB, whatever the fleet
	// allows and whatever any project asks for. The fleet ceiling cannot say it
	// without holding every other executor to the weakest device in the fleet,
	// and the project ceiling cannot say it because a project does not know
	// which executor it will land on.
	CeilingSourceExecutor = "executor"
	// CeilingSourceProject is the per-project cap an operator set on the hub.
	CeilingSourceProject = "project"
)

Ceiling source names. They appear in Clamp.Source, in the warnings surfaced on a run, and in the audit trail, so they are constants rather than literals scattered across the call sites that produce them.

View Source
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.

View Source
const (
	SandboxNetworkNone   = "none"
	SandboxNetworkBridge = "bridge"
)

SandboxNetworkNone and SandboxNetworkBridge are the two networks every engine has without an operator creating one. A named network is accepted too — that is how an operator attaches payloads to a segment they built — but only these two can be offered as choices, because the set of named networks lives on the device rather than here.

View Source
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.

View Source
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.

View Source
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`.

View Source
const AgentUpgradeProcedure = "cloop executor agent install --upgrade"

AgentUpgradeProcedure is the command that rolls a device forward.

It lives here, at the bottom of the import graph, because three layers name it: this package's placement refusals, the Executors panel, and the CLI's own help. It used to be a string in pkg/ui describing a flag that did not exist, which is how an operator came to run the one command the UI recommended and get an unknown-flag error. One definition, imported upward, is what keeps the advice and the flag from drifting apart again.

The flag now exists and is safe to recommend for a critical host: it verifies the binary before the swap and restores the previous one if the service does not come back. See pkg/executor/install.

View Source
const DefaultMaxAttachSessionsPerExecutor = 4

DefaultMaxAttachSessionsPerExecutor bounds simultaneous interactive sessions against one executor.

The ceiling is small on purpose. Each session is a live exec process inside the sandbox holding a pty and a transport, so the resource being protected is the *workload's* machine, not the hub's; a debugging tool that can be pointed at a struggling sandbox forty times over is a denial-of-service primitive against the thing it is meant to diagnose. Four is enough for an operator plus a colleague plus a stale session that has not timed out yet.

View Source
const HarnessProgram = "cloop"

HarnessProgram is the name cloop is installed under everywhere the control plane cannot see. The release tarball, the published sandbox image and the agent installer all put it on PATH under this name.

View Source
const LabelRunID = "run_id"

LabelRunID is the Spec label carrying the execution id the hub minted for a dispatch (Task 20282). A remote agent copies it into the placement record it writes beside a project seed, so the run on the device stamps its tasks with the same id the hub's lease rows carry (Task 20339). Bookkeeping only, like every label: nothing about how the workload runs depends on it.

View Source
const MaxDevices = 16

MaxDevices bounds how many device nodes one workload may be given.

The limit is not about runtime cost — a runtime will happily take hundreds — but about what a spec that asks for this many is describing. Past a handful the sandbox is no longer a confined environment with a piece of hardware in it; and every entry is another cgroup rule whose failure has to be diagnosed.

View Source
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.

View Source
const MaxInterfaces = 8

MaxInterfaces bounds how many interfaces one workload may be given.

Lower than MaxDevices because the resource is scarcer and the blast radius larger: each entry is a segment the host gives up for the life of the run, and a spec asking for more than a handful is describing a router rather than a sandbox with a wire in it.

View Source
const MaxProjectResultBytes = 640 << 10 // 640 KiB compressed

MaxProjectResultBytes bounds what an executor sends back after a seeded run — compressed (Task 20339).

A result is the run's changes, not the project: the tasks it touched, the steps, events and cost rows it recorded. One finished task is a few hundred bytes. The ceiling is set by the transport rather than by the content — a remote frame carries at most remote.MaxFrameBytes, and JSON encodes these bytes as base64 — so a device that produced more drops step output first, and says what it dropped, rather than sending a frame the hub must refuse.

View Source
const MaxProjectSeedBytes = 4 << 20 // 4 MiB compressed

MaxProjectSeedBytes bounds one seed as it travels — compressed.

The largest plan on this hub is 482 tasks and about 600 KiB of text, which gzips to well under a tenth of this. The cap is generous against that so it never becomes the thing an operator has to tune, and it is still a cap: an unbounded field here would be an unbounded allocation on a remote frame and an unbounded write on an edge device's disk.

View Source
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.

View Source
const MaxRuntimeNameLen = 64

MaxRuntimeNameLen bounds an OCI runtime name. Real names are well under 20 characters; the limit exists so a pathological value cannot produce an unreadable error or a surprising argv.

Exported so the container driver's own bound can be defined as this one rather than as a second 64 that happens to agree with it today.

View Source
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.

View Source
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.

View Source
const MaxWriteBackSentinelBytes = 8 << 10

MaxWriteBackSentinelBytes bounds the encoded line, so a workload cannot use the scanner as an unbounded allocator.

View Source
const WriteBackBranchPrefix = "cloop/"

WriteBackBranchPrefix namespaces every branch a sandbox may write back to. See ValidateWriteBackBranch for why the namespace is not optional.

View Source
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

View Source
var (
	// ErrAttachUnsupported means the driver has no interactive session
	// capability — it does not implement Attacher.
	ErrAttachUnsupported = errors.New("executor: attach not supported by this executor")

	// ErrAttachNoSandbox means the workload has no boundary to enter: it runs
	// as a process on the hub's own machine. Attaching would be a shell on the
	// control plane, which is the one thing this package promises never
	// happens.
	ErrAttachNoSandbox = errors.New("executor: attach refused: workload runs on the host, not in a sandbox")

	// ErrAttachReadOnly means the session was opened without write authority
	// and something tried to send stdin anyway.
	ErrAttachReadOnly = errors.New("executor: attach session is read-only")

	// ErrAttachHandleUnknown means the handle is not running on this executor.
	ErrAttachHandleUnknown = errors.New("executor: attach: unknown handle")

	// ErrAttachBusy means the per-executor concurrent-session ceiling is
	// reached. It is a capacity refusal, not an authorization one.
	ErrAttachBusy = errors.New("executor: attach: too many concurrent sessions on this executor")

	// ErrAttachClosed means the session ended — the workload exited, the
	// sandbox was removed, or the transport dropped.
	ErrAttachClosed = errors.New("executor: attach session closed")
)

Attach errors. Callers distinguish them with errors.Is: the hub maps the refusals to 403 and the absences to 501/404, and the CLI prints the remediation that fits.

View Source
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.

AllWriteBackReasons is every classification, for tests that assert the documentation and the dashboards cover the whole set.

View Source
var DefaultAttachCommand = []string{"/bin/sh"}

DefaultAttachCommand is what a session runs when the caller named no command.

`sh` rather than `bash`: the sandbox images cloop ships are minimal, and a session that fails with "bash: not found" on an alpine-based image is a worse default than a plainer shell that is always there.

View Source
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.

View Source
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.

View Source
var ErrProjectResultUnavailable = errors.New("executor: no project result was received")

ErrProjectResultUnavailable reports that no result arrived for a handle.

View Source
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.

View Source
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.

View Source
var ErrWorkspaceUnavailable = errors.New("executor: workspace could not be provisioned")

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.

View Source
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.

View Source
var ErrWriteBackUnavailable = errors.New("executor: work product could not be written back")

ErrWriteBackUnavailable: the work product could not be recovered from the executor. Distinct from a harness failure — nothing about the task's code is implicated, and the remedy is an operator's, not the model's.

Functions

func AgentBuild added in v0.0.2

func AgentBuild(ex Executor) (build string, reports bool)

AgentBuild reports the cloop build running on ex's node, and whether ex is a driver that has a build of its own at all.

The second return is the important one. A container or Kubernetes executor runs *this* binary — it has no separate build, and comparing it against the hub would be comparing the hub with itself. Those drivers must never be rejected for build skew, and reporting them as "build unknown" would reject every one of them the moment a floor was configured.

func ApplyHostExecutionPolicy

func ApplyHostExecutionPolicy(allowed bool)

ApplyHostExecutionPolicy installs a configured policy as a ratchet: it can only ever tighten. A config that permits host execution leaves the switch alone; only a config that forbids it moves the switch.

This is what every bootstrap path should call. SetAllowHostExecution is the raw switch, and outside of tests, calling it with true is almost always a bug — the asymmetry exists because the two directions are not symmetric in consequence:

  • A control plane manages many projects, each with its own config.yaml. Applying them symmetrically would mean one tenant's permissive config re-enables host execution for the whole process, which is a privilege escalation by way of a file the tenant controls.
  • Constructing a second Server (pkg/ui does this per instance, and tests do it constantly) would otherwise silently reset a security control that something earlier had deliberately set.

Loosening is therefore deliberate and explicit: restart the process with a permissive config, which is the same ceremony every other security-relevant setting requires.

func ApplyMinAgentBuild added in v0.0.2

func ApplyMinAgentBuild(v string)

ApplyMinAgentBuild installs a configured floor as a ratchet: it can only ever rise. A config naming an older floor than the one already in force, or naming none at all, leaves the switch alone.

This is what every bootstrap path should call, for the reason spelled out on ApplyHostExecutionPolicy: a control plane reads many projects' config.yaml, and applying them symmetrically would let one tenant's file lower a fleet-wide security floor for the whole process. Lowering is deliberate and explicit — restart with the looser config, the same ceremony every other security-relevant setting asks for.

An unparseable floor is ignored rather than treated as infinitely high. The safe reading of "I cannot understand this rule" is not "refuse the entire fleet" — that turns a typo into an outage. It is not silent either: config.ValidateExecutors rejects one arriving through `cloop config set`, and config.ExecutorWarnings surfaces a hand-edited one as a banner saying no floor is being enforced, which is the fact an operator must not be left to assume the other way round.

func ApplyResourceCeiling added in v0.0.2

func ApplyResourceCeiling(c ResourceCeiling)

ApplyResourceCeiling installs c as a ratchet: the result is the tighter of what was already in force and what c asks for, resource by resource.

This is what every bootstrap path should call. A ceiling naming a looser cap than the one already installed, or naming none at all, leaves the switch alone — so the order in which a hub happens to read its projects' config files cannot change the policy it ends up enforcing.

An invalid ceiling is ignored rather than installed. The safe reading of "I cannot understand this rule" is not "refuse every workload on the fleet", which would turn a typo into an outage; config.ValidateExecutorLimits rejects one arriving through `cloop config set`, and config.ExecutorLimitWarnings surfaces a hand-edited one as a banner saying no ceiling is being enforced — the fact an operator must not be left to assume the other way round.

func AttachSupported added in v0.0.2

func AttachSupported(ex Executor) bool

AttachSupported reports whether ex could be attached to right now, for a UI that wants to disable a button rather than explain a failure after the click.

func AuditWorkspace

func AuditWorkspace(ev WorkspaceEvent)

AuditWorkspace reports a provisioning event to the installed sink.

Best-effort and non-blocking on the caller's correctness: a sink that panics must not take down a workload that is otherwise fine, so the recover is here rather than in every sink.

func Bind

func Bind(projectPath, executorID string) error

Bind pins projectPath to executorID in the default registry.

func BlockedByBuildFloor added in v0.0.2

func BlockedByBuildFloor(ex Executor) (bool, string)

BlockedByBuildFloor reports whether the fleet's minimum agent build forbids dispatching to ex, and the sentence to show an operator when it does.

It exists so the Executors panel and the scheduler cannot disagree. A device that placement silently skips looks identical on the panel to one that is merely idle, and the operator's next move — waiting — is the one thing that will not help. Rendering the *same* decision function the scheduler runs means the card explains the refusal that is actually happening, rather than a second implementation of the rule that will drift from the first.

func BoundCPUs added in v0.0.2

func BoundCPUs(effective float64, capMillis int) float64

BoundCPUs is BoundLimit for a core allowance, whose units are the runtimes' fractional cores rather than milli-cores.

func BoundLimit added in v0.0.2

func BoundLimit(effective, cap int) int

BoundLimit returns the limit a driver should apply for one resource.

effective is what the driver resolved on its own — the Spec's request if it made one, otherwise the executor's configured default. cap is the ceiling; 0 means there is none.

The two zero cases are different and both matter. A cap of 0 is no ceiling, so effective stands. An *effective* of 0 is "nothing has bounded this yet", which is the case a ceiling exists for: a project that requested nothing on an executor configured with no default would otherwise run unbounded.

func CeilingUnenforceable added in v0.0.2

func CeilingUnenforceable(ex Executor, clamps []Clamp) bool

CeilingUnenforceable reports that a ceiling bounded this spec but ex cannot hold it to the result.

Remote agents advertise SupportsResourceLimits false — they report the device's capacity for placement and do not confine a workload to a share of it — so a spec dispatched to one carries limits nothing will apply. That is a pre-existing property of the driver, not something a ceiling introduces, but it changes what an operator is entitled to believe: having set a cap, they will assume it holds everywhere. It does not, and this is what lets the caller say so rather than leave them to assume.

func CheckSandboxSupport

func CheckSandboxSupport(ex Executor, req Requirements, projectPath string) error

CheckSandboxSupport verifies that the executor a project is *already bound to* can honour req, and returns a typed error naming the gap if it cannot.

Binding and placement are different questions — Registry.Resolve answers "which executor is this project pointed at", not "which executor should run this" — but they must not answer the capability question differently. So this runs the bound executor through the same reject() the scheduler uses, as a candidate list of one. A constraint added to Select is therefore enforced on the binding path for free, which is the opposite of how the two usually drift.

The error type is chosen by what the operator has to *do*:

  • a gap that is really "this workload cannot run beside the control plane" becomes *HostExecutionDeniedError, whose Remediation() already names the isolated executors to bind to instead. The UI renders that verbatim.
  • anything else becomes the *PlacementError from reject(), which names the single constraint the bound executor failed.

A nil executor, or a req nothing rejects, returns nil.

func ClampWarnings added in v0.0.2

func ClampWarnings(clamps []Clamp) []string

ClampWarnings renders clamps as operator-facing lines, deduplicated and ordered so the same set always reads the same way.

Deduplication matters because the fleet and project ceilings can both clamp the same resource to the same number, and telling someone twice that their memory was capped to 2 GB reads as two separate events.

func ContainerEngines added in v0.0.3

func ContainerEngines() []string

ContainerEngines lists the selectable container engines.

func Cordon

func Cordon(h Health, reason string, at time.Time) (Health, *Transition)

Cordon takes a node out of rotation without disturbing its in-flight work.

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 DeviceArgv added in v0.0.3

func DeviceArgv(spec *Spec, ex Executor)

DeviceArgv rewrites spec.Argv so its program can be found on ex.

Why IsolatesFromHost decides it

It is the same question pkg/ui already asks two lines from the call site, to decide whether a workload may inherit the hub's own process environment: a hub path is meaningful to a workload exactly when that workload runs on the hub, and an environment full of them is no more portable than an argv full of them.

The neighbouring SharesHostFilesystem would be the wrong gate, by exactly one driver. It answers "can host-side tooling read what the workload wrote", which the container driver answers yes to because the project directory really is a bind mount of a host path — and that same driver still cannot exec a binary belonging to the hub, because everything outside the mounts it was given is the image's filesystem. Gating on it would leave containers broken, which is where this defect was first measured: AuditRunArgv put the hub's own binary path after the image name.

A no-op on an unisolated executor, where the absolute path is the better answer: it pins the exact binary the hub is running rather than whichever one PATH happens to find, which matters on a host carrying more than one version — the same host, note, that produced this bug by carrying two.

Idempotent, and a no-op on an argv that is already relative: a caller that applies it twice, or that passed a bare name to begin with, gets the same spec back.

func DeviceNames added in v0.0.2

func DeviceNames(devices []HostDevice) []string

DeviceNames returns the grant-facing names of a device list, in order, for log lines and audit records.

func DeviceWorkDir

func DeviceWorkDir(workDir string) string

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

func Drain(h Health, reason string, at time.Time) (Health, *Transition)

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 Ensure

func Ensure(ex Executor) error

Ensure idempotently adds ex to the default registry.

func EnvKeys

func EnvKeys(bindings []SecretBinding) []string

EnvKeys returns every environment variable name the bindings contributed, deduplicated and sorted.

func ForgetHandle

func ForgetHandle(store HandleStore, handleID string)

ForgetHandle drops rec's row once the workload is terminal. Same best-effort contract as RecordHandle: a row that outlives its workload is swept by the orphan reaper, so failing loudly here would buy nothing.

func FormatCPUMillis added in v0.0.2

func FormatCPUMillis(millis int) string

FormatCPUMillis renders a milli-core count as cores when it divides evenly, which is how nearly every real setting is written.

func FormatMB added in v0.0.2

func FormatMB(mb int) string

FormatMB renders a megabyte count the way an operator wrote it, so a value that came from "8g" reads back as "8 GB" rather than "8192 MB".

func GitBaseEnv

func GitBaseEnv() []string

GitBaseEnv returns the environment every step of a plan runs with, credential or not.

It is a closed environment rather than an inherited one. A provisioning step that read the executor's own ~/.gitconfig could pick up a credential helper, an insteadOf rewrite pointing the fetch at another host, or a proxy — all decided by whoever last touched that machine rather than by the grant.

func GitCredentialEnv

func GitCredentialEnv(w Workspace, c GitCredential) ([]string, error)

GitCredentialEnv returns the additional environment for the authenticated step of a plan.

The credential is delivered as a URL-scoped http.extraHeader through git's GIT_CONFIG_COUNT protocol. That is the only delivery path here that satisfies all three constraints at once: nothing is written to disk, nothing appears in argv (which /proc publishes to every process with the same uid), and the header is scoped to the repository's own origin.

The scoping alone is not what keeps the header at its origin — baseGitConfig's refusal to follow redirects is, and the two only work together. The whole block is re-emitted here rather than added to GitBaseEnv's, because GIT_CONFIG_COUNT is a single count for the whole environment: a second block appended after the first overrides the count, and any key the second block did not repeat would be dropped along with it. Emitting one complete block means the credential cannot arrive with the redirect guard silently switched off.

The empty credential.helper entry is not redundant: without it, a helper configured in a location GIT_CONFIG_GLOBAL does not cover could still answer the challenge with a *different* credential, and the fetch would succeed using authority the grant never issued.

func HostExecutionAllowed

func HostExecutionAllowed() bool

HostExecutionAllowed reports whether un-isolated host execution is permitted.

func InspectWriteBack

func InspectWriteBack(branch, commitSHA string, entries []BundleEntry) error

InspectWriteBack vets a whole commit range's worth of changed entries and returns the first refusal, or nil when every entry is acceptable.

It takes the already-extracted entries rather than reading git itself so that this package stays free of I/O — pkg/executor is imported by the edge agent, which must not grow a dependency on a git binary to satisfy a type — and so the rules can be tested exhaustively against a table instead of against a repository.

func InterfaceNames added in v0.0.3

func InterfaceNames(ifaces []HostInterface) []string

InterfaceNames returns the grant-facing names of an interface list, in order, for log lines and audit records.

func IsKernelIsolatedRuntime added in v0.0.2

func IsKernelIsolatedRuntime(name string) bool

IsKernelIsolatedRuntime reports whether name identifies a runtime under which the workload's system calls are served by something other than the host kernel — a guest kernel inside a VM (Kata) or a userspace kernel in the Sentry (gVisor).

This is the property a project asks for when it says "do not let my task's syscalls reach the host kernel directly". It is strictly weaker than IsVirtualizedRuntime, and that is the point: demanding a hypervisor when a userspace kernel would do refuses placement on executors that met the actual requirement, and a refusal nobody needed is still an outage.

runc and crun report false. They are the case this exists to exclude: the container boundary is namespaces, cgroups and seccomp, and a kernel bug is a host bug.

func IsUserspaceKernelRuntime added in v0.0.2

func IsUserspaceKernelRuntime(name string) bool

IsUserspaceKernelRuntime reports whether name identifies gVisor, in either the OCI-runtime or the Kubernetes RuntimeClass spelling.

Recognised: "runsc" (the binary's own name, which is what both `--runtime runsc` and a RuntimeClass called runsc use), "gvisor", any "runsc-*" or "gvisor-*" variant an operator has registered with different Sentry flags (runsc-ptrace, runsc-kvm are the two in the wild), and the containerd shim spelling "io.containerd.runsc.v1".

gVisor is not a hypervisor and this never claims it is; see IsKernelIsolatedRuntime for what it is used to assert.

func IsVirtualizedRuntime

func IsVirtualizedRuntime(name string) bool

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

func IsolatesFromHost(ex Executor) bool

IsolatesFromHost reports whether ex puts any boundary between the workload and the control-plane host. It is the exported form of isolatesFromHost, for callers outside this package that have to treat the two cases differently — pkg/ui decides from it whether a workload may inherit the hub's own process environment, which is safe only when the workload runs on the hub anyway.

It fails closed the same way: a driver that declares no isolation is not isolated. See isolatesFromHost for why that direction is the safe one.

func MarshalWriteBackSentinel

func MarshalWriteBackSentinel(r WriteBackResult) (string, error)

MarshalWriteBackSentinel renders r as the single line a sandbox prints.

func MinAgentBuild added in v0.0.2

func MinAgentBuild() string

MinAgentBuild returns the configured minimum agent build, or "" for no floor.

func ObserveProbe

func ObserveProbe(h Health, probeErr error, at time.Time, policy HealthPolicy) (Health, *Transition)

ObserveProbe folds one probe result into h and reports the resulting health plus the transition it caused, if any.

It is a pure function of (health, result, time, policy) with no clock, no I/O, and no locking, which is what lets the state machine be tested exhaustively under a fake clock while the supervisor around it deals with timers and goroutines.

probeErr is nil for a successful probe.

func PolicyProjectPath added in v0.0.3

func PolicyProjectPath(projectPath string) string

PolicyProjectPath returns the project whose policy governs projectPath: projectPath itself unless the installed mapper says otherwise.

func RecordHandle

func RecordHandle(store HandleStore, rec HandleRecord)

RecordHandle persists rec, reporting failure without propagating it.

Persistence is best-effort by design. A workload that started successfully must not be reported as failed because the state database was momentarily locked: the caller would mark the task failed and retry it, producing the double execution the whole scheduling layer exists to prevent. A lost row degrades to exactly the behaviour that existed before this file — the workload becomes unobservable after a restart — which is strictly better than losing the workload now.

func RedactAttachOutput added in v0.0.2

func RedactAttachOutput(r io.Reader, set *redact.Set) io.Reader

RedactAttachOutput wraps r so the workload's own leased credentials never reach the operator's terminal.

It reuses redact.Writer rather than scrubbing each read, because a credential does not respect read boundaries: a 40-character token split across two reads matches neither half, and a per-chunk scrub would pass it through in two pieces that a terminal reassembles perfectly. redact.Writer already holds back a partial tail for exactly this, and routing attach output through it is what makes "the same redaction path as the log stream" true rather than aspirational.

A nil set returns r unchanged, so a driver can call this unconditionally.

func RedactSecrets

func RedactSecrets(s string, secrets []string) string

RedactSecrets replaces every occurrence of each secret in s with a fixed marker.

Provisioning output is streamed to the run panel and persisted as an artifact, and git is perfectly willing to quote a URL or a header back in an error message. Redacting at the point output leaves the provisioning step is the only place that catches every such path; filtering at the display layer would leave the artifact on disk.

The matching — including the short-value guard that keeps ordinary text from being mangled — lives in pkg/redact, so the harness output path and this one cannot drift apart in what they consider a secret.

func Register

func Register(ex Executor) error

Register adds ex to the default registry.

func RequireRevocable

func RequireRevocable(ex Executor, spec Spec) error

RequireRevocable refuses to place spec on ex when the spec carries material that ex cannot take back.

This is the rule the remote driver enforced for its own agents, applied to every driver. A spec with no revocable bindings passes on any executor — the refusal is about credentials, not about drivers — so a deployment that has never configured the secret broker is unaffected.

Drivers that implement Revoker still enforce their own, more specific refusals inside Start (a remote agent checks the negotiated protocol version against the live session). This is the outer gate, and the two are deliberately redundant: a security control with one enforcement point is a security control with a bypass.

func ResetResourceCeiling added in v0.0.2

func ResetResourceCeiling()

ResetResourceCeiling clears the switch. It exists for tests, which would otherwise leak a ceiling from one case into the next through process state — and because the ratchet means a test cannot undo itself by installing a looser value, which is exactly the property being tested.

func SandboxNetworks added in v0.0.3

func SandboxNetworks() []string

SandboxNetworks lists the networks an admin may pick from a menu, in UI order. Deny-by-default first: a payload that cannot reach the network cannot exfiltrate what it was given, and an admin choosing otherwise should be choosing, not accepting a pre-selected default.

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

func SetAllowHostExecution(allowed bool) bool

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

func SetBindingLookup(fn func(projectPath string) (string, bool))

SetBindingLookup installs the persistent binding resolver on the default registry.

func SetExecutorCeilingLookup added in v0.0.3

func SetExecutorCeilingLookup(fn func(executorID string) (ResourceCeiling, bool))

SetExecutorCeilingLookup installs the per-executor ceiling resolver.

func SetMinAgentBuild added in v0.0.2

func SetMinAgentBuild(v string) string

SetMinAgentBuild sets the minimum cloop build a remote agent may be running to receive work, returning the previous value so callers — tests especially — can restore it without keeping a separate copy.

An empty string clears the floor. This is the raw switch; bootstrap paths should call ApplyMinAgentBuild.

func SetPolicyProjectMapper added in v0.0.3

func SetPolicyProjectMapper(fn func(projectPath string) string)

SetPolicyProjectMapper installs the mapper. Pass nil to remove it.

func SetProjectCeilingLookup added in v0.0.2

func SetProjectCeilingLookup(fn func(projectPath string) (ResourceCeiling, bool))

SetProjectCeilingLookup installs the per-project ceiling resolver.

func SetWorkspaceAuditor

func SetWorkspaceAuditor(f func(WorkspaceEvent))

SetWorkspaceAuditor installs the sink provisioning events are reported to.

It is a process-wide hook rather than a field on every driver because the drivers must not depend on the hub's database: pkg/executor is imported by the edge agent, which has no audit store and no business linking one. The control plane installs the sink at bootstrap; anywhere else the events are dropped, which is correct — an agent's local provisioning is audited by the hub that dispatched it.

Passing nil disables auditing.

func ShortSHA

func ShortSHA(sha string) string

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

func SupportsRevocation(ex Executor) bool

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 ValidContainerEngine added in v0.0.3

func ValidContainerEngine(name string) bool

ValidContainerEngine reports whether name is a known container engine. Empty is valid and means "let the executor detect one".

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

func ValidateCommitSHA(s string) error

ValidateCommitSHA checks that s is a full 40-character lowercase hex object name.

Full, not abbreviated: an abbreviation is ambiguous by construction, and the hub compares this value against what it actually fetched. A prefix match would let a sandbox report a SHA that resolves to a different object than the one it pushed.

func ValidateDevices added in v0.0.2

func ValidateDevices(devices []HostDevice) error

ValidateDevices checks a whole device list: each entry, the count, and the two collision classes that would otherwise be resolved by argv order.

func ValidateHostDevice added in v0.0.2

func ValidateHostDevice(d HostDevice) error

ValidateHostDevice checks one device grant.

It is exported because three layers need the same answer: the broker when an operator creates a grant (so the mistake is caught at grant time, by the person who can fix it), the drivers before they render a runtime flag, and Spec.Validate on the dispatch path. One definition means a device that passes at grant time cannot be refused two layers down.

func ValidateHostInterface added in v0.0.3

func ValidateHostInterface(i HostInterface) error

ValidateHostInterface checks one interface grant.

Exported for the reason ValidateHostDevice is: the broker needs the same answer at grant time (so the mistake reaches the person who can fix it), the drivers need it before they touch a netlink socket, and Spec.Validate needs it on the dispatch path. One definition means an interface that passes at grant time cannot be refused two layers down.

What it deliberately does not check is whether the interface exists, or what it is attached to. Both are facts about one host, and a grant minted on the hub may be honoured by an executor on another machine — so they belong to Preflight and to the driver's attach path, which run on the right one.

func ValidateHostMounts

func ValidateHostMounts(mounts []HostMount) error

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 ValidateInterfaces added in v0.0.3

func ValidateInterfaces(ifaces []HostInterface) error

ValidateInterfaces checks a whole interface list: each entry, the count, and the two collision classes that would otherwise be resolved by ordering.

func ValidateNetworkName added in v0.0.3

func ValidateNetworkName(name string) error

ValidateNetworkName checks that name is a network an executor may be told to attach a payload to.

Empty is valid and means "unset", which every caller reads as the deny-by-default "none".

The two refusals are the point of the function, and neither is about shape:

  • "host" removes the network namespace altogether. The payload would then reach every service bound to the device's loopback — which on a machine running a cloop agent includes whatever else that machine is for. It is also the one value that makes the container's own address the host's, so an egress filter attached to the sandbox would filter nothing.
  • "container:<id>" joins another container's namespace, which is the same escape wearing a different name: the neighbour may have the network this one was refused.

This is the canonical definition; container.ValidateNetwork defers to it, so the rule cannot drift between the hub's config file and its per-executor API.

func ValidateProjectSeed added in v0.0.3

func ValidateProjectSeed(seed []byte) error

ValidateProjectSeed checks what every holder of a Spec can check: that the payload is bounded and, if present, actually compressed.

The gzip magic test is cheap and it is the difference between a corrupt seed failing here, naming itself, and failing inside a sandbox as a state file that will not parse — which surfaces as "no cloop project found", the exact message this whole mechanism exists to stop lying to people.

func ValidateRuntimeName added in v0.0.3

func ValidateRuntimeName(name string) error

ValidateRuntimeName checks that name is a plausible OCI runtime *name*.

Empty is valid and means "the engine's own default" — no --runtime flag is emitted at all, which is what every existing deployment gets.

The shape check is permissive on purpose. The set of legitimate names is open: an operator may register Kata under any name they like, and clusters do (kata, kata-qemu, kata-clh). What the check forbids is the shape that would stop the value being a name at all — a leading dash, which the engine would parse as another flag, and a path separator, which would turn config that names a runtime into config that names an arbitrary binary the engine runs as root. A bare name cannot do that: docker resolves it only against /etc/docker/daemon.json and podman only against containers.conf, both root-owned files an operator already controls, so the name is an indirection through a trusted table rather than a target.

This is the canonical definition; container.ValidateOCIRuntime and the per-executor settings below both defer to it, so the rule cannot drift between the hub's config file and its API.

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

func ValidateSpecMounts(mounts []SpecMount) error

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

func ValidateWriteBackBranch(branch string) error

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

func ValidateWriteBackPath(p string) error

ValidateWriteBackPath reports whether a repository-relative path may be written by an applied write-back.

The containment rules are mount.go's, reached through the same hasDotDot helper rather than restated, because "does this path escape its root" has one correct answer and two implementations of it is one more than the number that can be kept right. What this adds on top is the git-specific half: the .git directory is not an ordinary path, because writing into it is not writing a file — it is editing the configuration of the program that is about to run.

Types

type AttachConn added in v0.0.2

type AttachConn interface {
	io.ReadWriteCloser

	// Resize reports new terminal geometry. It is a no-op — not an error —
	// for a session without a TTY, so a caller that always sends the window
	// size does not need to know which kind it opened.
	Resize(rows, cols uint16) error
}

AttachConn is one live interactive session.

Read yields the sandbox's combined output, already scrubbed of the workload's own leased credentials. Write forwards stdin, or fails with ErrAttachReadOnly. Close is idempotent and must terminate the remote process, not merely detach from it: a session that left a shell running inside the sandbox after the operator closed the tab would be an unattributed process holding the project's credentials.

func NewAttachConn added in v0.0.2

func NewAttachConn(out io.Reader, in io.WriteCloser, resize func(rows, cols uint16) error, closeFn func() error) AttachConn

NewAttachConn assembles an AttachConn from a driver's pieces.

in may be nil, which is how a read-only session is expressed: Write then fails with ErrAttachReadOnly rather than blocking or silently discarding.

type AttachLimiter added in v0.0.2

type AttachLimiter struct {
	// contains filtered or unexported fields
}

AttachLimiter bounds simultaneous sessions per executor.

Per executor rather than per hub, per project or per user, because the resource at risk is the sandboxed machine: one operator opening four sessions on four different executors costs each of them one, while four operators converging on the one pod that is misbehaving is exactly the pile-on worth stopping. The limiter is also where the hub learns who is attached to what, so it doubles as the live-session inventory the UI and the CLI read.

func NewAttachLimiter added in v0.0.2

func NewAttachLimiter(max int) *AttachLimiter

NewAttachLimiter returns a limiter admitting at most max sessions per executor. A max below 1 falls back to the default rather than admitting nothing or everything — a misconfigured ceiling should not decide the policy.

func (*AttachLimiter) Acquire added in v0.0.2

func (l *AttachLimiter) Acquire(info AttachSessionInfo) (string, func(), error)

Acquire reserves a slot on info.ExecutorID and returns the session ID plus a release function. The release is idempotent, so a caller may defer it and also call it on an early error path.

A nil limiter admits everything: the ceiling is a hub concern, and a driver test or an embedded use with no limiter configured should not be silently capped at zero.

func (*AttachLimiter) Count added in v0.0.2

func (l *AttachLimiter) Count(executorID string) int

Count returns the number of live sessions on one executor.

func (*AttachLimiter) Live added in v0.0.2

func (l *AttachLimiter) Live() []AttachSessionInfo

Live returns every open session, ordered by session ID, for the inventory.

func (*AttachLimiter) Max added in v0.0.2

func (l *AttachLimiter) Max() int

Max returns the configured per-executor ceiling.

type AttachRequest added in v0.0.2

type AttachRequest struct {
	// HandleID identifies the running workload, as returned by Start.
	HandleID string

	// Command is the argv to run inside the sandbox. Empty means the driver's
	// default interactive shell.
	Command []string

	// TTY requests a pseudo-terminal. Without one the session is a pipe: fine
	// for `tail -f`, useless for anything that draws.
	TTY bool

	// Stdin reports that the caller holds write authority and that the driver
	// should wire an input channel.
	//
	// It is a field of the request rather than a property of the returned
	// session because it must reach the sandbox: `docker exec` without -i and
	// a Kubernetes exec without stdin=true give the remote process no input fd
	// at all. A read-only session is therefore read-only *in the sandbox*, not
	// merely at the hub — there is no open channel to smuggle bytes down.
	Stdin bool

	// Rows and Cols are the initial terminal geometry; ignored when !TTY.
	Rows, Cols uint16

	// Env carries extra environment for the exec'd command (TERM, mainly).
	// Entries are "K=V"; a driver that cannot set environment ignores them.
	Env []string
}

AttachRequest asks a driver to open an interactive session inside a running workload.

func (AttachRequest) Argv added in v0.0.2

func (r AttachRequest) Argv() []string

Argv returns the command to run, substituting the default when empty.

func (AttachRequest) CommandLine added in v0.0.2

func (r AttachRequest) CommandLine() string

CommandLine renders Argv for an audit record and an operator's eyes. It is not shell-quoted for re-execution — it exists to answer "what did they run", and a naive quote would imply a round-trip that is not offered.

type AttachSessionInfo added in v0.0.2

type AttachSessionInfo struct {
	SessionID  string `json:"session_id"`
	ExecutorID string `json:"executor_id"`
	HandleID   string `json:"handle_id"`
	Actor      string `json:"actor"`
	Command    string `json:"command"`
	Writable   bool   `json:"writable"`
}

AttachSessionInfo describes one live session, for the inventory.

type Attacher added in v0.0.2

type Attacher interface {
	Attach(ctx context.Context, req AttachRequest) (AttachConn, error)
}

Attacher is the optional capability: a driver that can open a session inside a workload it is already running.

Implemented by the three isolating drivers (container, kubernetes, remote) and deliberately not by localprocess — see AttachTarget.

func AsAttacher added in v0.0.2

func AsAttacher(ex Executor) (Attacher, bool)

AsAttacher reports whether ex can open interactive sessions, mirroring AsRevoker. It does no policy checking; callers want AttachTarget.

func AttachTarget added in v0.0.2

func AttachTarget(ex Executor) (Attacher, error)

AttachTarget is the one place that decides whether an executor may be entered.

Order matters, and it is the opposite of the cheap one. The sandbox question is asked *before* the capability question so that a localprocess workload is refused with "this runs on the host" rather than the milder "not supported": the two have the same effect today and very different meanings if someone later teaches localprocess to exec. A refusal that would silently become a grant is not a refusal.

type BuildReporter added in v0.0.2

type BuildReporter interface {
	Executor
	// AgentVersion returns the cloop build the connected device advertised,
	// or "" when there is no live session to ask.
	AgentVersion() string
}

BuildReporter is implemented by drivers whose workload runs on a machine with a cloop binary of its own, and which can therefore report a build distinct from the control plane's.

The interface, rather than a Capabilities bool, for the same reason Revoker is an interface (see revoke.go): a bool in a struct is a claim anybody can set, and the direction the two would drift is a driver asserting a build it cannot substantiate. Only a driver that genuinely has a separate binary to ask about can implement a method that returns its version.

remote.Executor satisfies this already — AgentVersion() has existed since the fleet inventory landed. Nothing had to change on the driver side; what was missing was a reader in the scheduler.

type BundleEntry

type BundleEntry struct {
	// Path is the repository-relative path, slash-separated.
	Path string
	// Mode is the resulting file mode, one of the Mode* constants. ModeAbsent
	// means the path was deleted.
	Mode string
	// LinkTarget is the symlink destination, for Mode ModeSymlink. The caller
	// reads it out of the blob; this package decides whether it is allowed.
	LinkTarget string
}

BundleEntry is one changed path in a write-back's commit range, as the hub reads it out of git.

type Candidate

type Candidate struct {
	// Executor is the driver instance. Select returns the Candidate, so the
	// caller gets this back to Start on.
	Executor Executor
	// Health is the supervisor's current view. A zero value normalizes to
	// ready, so a caller with no health store still gets sane placement.
	Health Health
	// Labels are operator-assigned scheduler selectors (region, tier, owner).
	Labels map[string]string
	// Harnesses are the agent CLIs available on the node, as detected by
	// pkg/executor/agent.Detect ("claude", "codex", "cloop", ...).
	Harnesses []string
	// ContainerRuntimes are the container runtimes the node can drive.
	ContainerRuntimes []string
	// MemoryMB is total system memory on the node; 0 means unknown, which is
	// never treated as "too small".
	MemoryMB int
	// InFlight is the number of sessions currently running on the node, and
	// InFlightKnown says whether that number is real. A driver that cannot
	// enumerate reports InFlightKnown=false, and capacity is then not
	// enforced — refusing to schedule because load is unknown would make
	// every non-enumerating driver unusable.
	InFlight      int
	InFlightKnown bool
}

Candidate is one executor offered to the scheduler, together with everything placement needs to judge it.

The caller assembles this from the registry, the health store, and (for remote agents) the capabilities the device advertised at enrollment. Bundling it into a value keeps Select pure and keeps this package independent of both storage and the remote protocol.

func Select

func Select(candidates []Candidate, req Requirements) (Candidate, error)

Select chooses the best candidate satisfying req, or returns a *PlacementError naming the constraint that could not be met.

Ranking among satisfying candidates, in order:

  1. ready before degraded — a node whose probes are landing is a better bet than one that is merely not yet given up on;
  2. more free capacity first, so a fleet fills evenly instead of piling every task onto whichever node sorts first alphabetically;
  3. isolated before un-isolated, so a deployment that permits host execution still prefers a sandbox when one is free;
  4. executor ID, purely so the result is deterministic and tests are not flaky.

func (Candidate) ID

func (c Candidate) ID() string

ID returns the candidate's executor ID, or "" for a malformed candidate.

type Capabilities

type Capabilities struct {
	// Isolation is the strength of the boundary between workload and host.
	Isolation Isolation `json:"isolation"`
	// Virtualized reports whether the workload runs behind a hypervisor — a
	// VM or microVM with a kernel of its own — rather than sharing the
	// executing machine's kernel.
	//
	// It is a separate field rather than a reading of Isolation because the
	// two answer different questions and a driver can need both. A Kata pod
	// on a remote cluster is IsolationRemote (the machine is not ours) *and*
	// virtualized (the kernel is not the node's); collapsing that into one
	// enum value would force every such driver to drop one of the two facts,
	// and Isolation is explicitly not a total order (see placement.go).
	//
	// It stays false unless the driver is certain. A requirement to run
	// virtualized is checked against this field, so a false positive places a
	// workload that must be behind a hypervisor onto one that is not.
	Virtualized bool `json:"virtualized,omitempty"`
	// KernelIsolated reports whether the workload's system calls are served
	// by something other than the executing machine's kernel — a guest
	// kernel in a VM, or gVisor's Sentry.
	//
	// Every virtualized executor is also kernel-isolated; the reverse does
	// not hold, and the gap between them is exactly gVisor. Before this
	// field existed a gVisor sandbox advertised the same capabilities as a
	// runc one, so the stronger boundary an operator had paid for was
	// invisible to placement and a project could not ask for it without
	// demanding a hypervisor it did not need.
	//
	// Like Virtualized it stays false unless the driver is certain, and for
	// the same reason: RequireKernelIsolation is checked against it, so a
	// false positive runs a workload on the host kernel it was told to stay
	// off.
	KernelIsolated bool `json:"kernel_isolated,omitempty"`
	// SupportsStream reports whether Stream returns live output. Drivers
	// that only persist output post-hoc set this false.
	SupportsStream bool `json:"supports_stream"`
	// SupportsSignal reports whether Signal can deliver signals to a
	// running handle.
	SupportsSignal bool `json:"supports_signal"`
	// SupportsResourceLimits reports whether Spec.ResourceLimits is
	// enforced. When false, limits in a Spec are ignored, not an error.
	SupportsResourceLimits bool `json:"supports_resource_limits"`
	// SharesHostFilesystem reports whether Spec.WorkDir refers to a path on
	// 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"`
	// SupportsProjectSeed reports whether this driver places Spec.ProjectSeed
	// into the workspace before the harness starts.
	//
	// Separate from SupportsWorkspaceProvisioning, which it always accompanies,
	// because the two failures look nothing alike. A driver that cannot
	// provision leaves an empty directory, and the harness says so loudly. A
	// driver that provisions but drops the seed produces a tree that is
	// entirely correct and a `cloop run` that exits on its first line with
	// "no cloop project found" — pointing at the project, which is fine, rather
	// than at the dispatch, which is not. A capability makes that a refusal at
	// placement time instead.
	SupportsProjectSeed bool `json:"supports_project_seed"`
	// ReturnsProjectState reports whether this driver brings a seeded run's
	// changes back — the tasks it finished or created, the steps, events and
	// cost rows it recorded — for the hub to merge into its own copy of the
	// project (Task 20339). See ProjectResultFetcher.
	//
	// It is SupportsProjectSeed's other half, and separate from it because a
	// driver can have one without the other: a v10–v12 remote agent places the
	// seed and returns nothing, which is exactly the gap that let a run finish
	// its task on a device while the dashboard kept showing it pending. The
	// hub reads this to tell "the run came back with no changes" from "this
	// executor cannot report any", and to say which in the project's journal.
	ReturnsProjectState bool `json:"returns_project_state"`
	// SupportsWriteBack reports whether this driver can return the files a
	// workload changed (Spec.WriteBack).
	//
	// Separate from SupportsWorkspaceProvisioning even though the same drivers
	// implement both, because they are two different halves of the run and a
	// driver can genuinely have one without the other — an executor that
	// shares the host filesystem needs neither, and one whose transport cannot
	// carry bytes back can fetch but not return. Every field a driver can
	// quietly drop needs a flag that says so, and this is the one whose
	// omission costs work rather than confusion.
	SupportsWriteBack bool `json:"supports_write_back"`
	// SupportsSandboxMounts reports whether Spec.Mounts is honoured.
	//
	// It is tracked separately from SupportsImageOverride even though the same
	// two drivers implement both, because the failure mode of getting it wrong
	// is silent: a driver that ignores Mounts produces a sandbox that starts,
	// runs, and cannot find the cache directory the project told it to expect.
	// Every field a driver can quietly drop needs a flag that says so.
	SupportsSandboxMounts bool `json:"supports_sandbox_mounts"`
	// SupportsHostMounts reports whether Spec.HostMounts is honoured — i.e.
	// whether this driver can bind a path from the control-plane host into
	// the sandbox.
	//
	// It is not implied by SharesHostFilesystem, and the two are genuinely
	// different answers. localprocess shares the filesystem and cannot bind
	// anything: a granted repository is simply already visible at its own
	// path there. A remote agent can bind, but not paths from *this* host,
	// which it has never seen. Only a driver that both runs here and has a
	// mount namespace to bind into can honour the field, and a driver that
	// ignored it would start a harness whose /repos is empty.
	SupportsHostMounts bool `json:"supports_host_mounts"`
	// SupportsDevices reports whether Spec.Devices is honoured — i.e.
	// whether this driver can expose a host device node inside the sandbox.
	//
	// Distinct from SupportsHostMounts because a device is not a file the
	// driver can bind and be done with. A container runtime needs a cgroup
	// rule permitting the major:minor pair as well as the node itself, and
	// the Kubernetes driver needs the node to advertise the device at all.
	// A driver that quietly dropped the field would produce a sandbox whose
	// /dev is missing exactly the hardware the task exists to talk to, so
	// the honest outcome is a placement refusal naming the executor.
	SupportsDevices bool `json:"supports_devices"`
	// SupportsInterfaces reports whether Spec.Interfaces is honoured — i.e.
	// whether this driver can move a host network interface into the
	// sandbox's network namespace.
	//
	// Separate from SupportsDevices because a netdev is not a node under
	// /dev: there is nothing to bind and no cgroup rule to write, and the
	// runtimes have no flag for it. The operation is a namespace move, which
	// needs the driver to be co-resident with the interface, privileged
	// enough to perform it, and — decisively — to be handing the workload a
	// kernel that will observe the new device. A Kata guest and a gVisor
	// Sentry both build their view of the network when the sandbox starts
	// and never see an interface that arrives afterwards, so on those
	// runtimes this is false even though the move itself would succeed.
	//
	// That last case is why a driver must not treat the field as
	// best-effort. The move appears to work, the host loses the interface,
	// and the workload sees no new link at all: a bench with no wire in it,
	// reported as a success. The honest outcome is a placement refusal
	// naming the executor.
	SupportsInterfaces bool `json:"supports_interfaces"`
	// SupportsEgressScope reports whether Spec.EgressScope is honoured —
	// whether this driver can confine one workload's IP-layer egress
	// independently of the other workloads on the same executor.
	//
	// Its absence is the reason the field cannot simply be best-effort. An
	// executor that ignored a project's request to be cut off from private
	// address space would hand a harness the reach into the operator's
	// internal network that the project had explicitly renounced, and would
	// report success doing it.
	SupportsEgressScope bool `json:"supports_egress_scope"`
	// SupportsSecretFiles reports whether the credential *files* a secret
	// lease produces actually reach the workload.
	//
	// It is the flag whose absence was silent, and expensively so. A lease's
	// environment travelled everywhere; its files were written into the hub's
	// own /dev/shm and named in Spec.Env, which is a delivery only for a
	// workload running on the hub's filesystem. Every isolating driver
	// forwarded the variable and dropped the file. A repo-scoped github_pat
	// delivers its *whole enforcement* as files — a credential helper, a token
	// file, a gitconfig exported through GIT_CONFIG_GLOBAL — and deliberately
	// exports no bare token unless the grant is unrestricted, so what arrived
	// in the sandbox was a path that did not exist and no credential at all.
	//
	// False here refuses such a workload at placement time (see
	// Requirements.RequireSecretFiles) instead of starting a sandbox whose git
	// authentication will fail for a reason nothing names.
	SupportsSecretFiles bool `json:"supports_secret_files"`
	// SecretFilesFromHostPath reports whether the workload reads those files
	// straight off the control-plane host's filesystem, at the path the hub
	// wrote them to.
	//
	// It decides *who materialises*, which is a different question from
	// whether files are supported at all, and the two cannot be collapsed.
	// localprocess reads hub paths, so the hub writes the plaintext and the
	// process opens it. A container also runs on the hub — SharesHostFilesystem
	// is true for it — and still cannot read that directory: it has a mount
	// namespace of its own, and the lease directory is 0700 owned by the hub
	// user while the sandbox runs as an unprivileged UID. So the container
	// driver takes the bytes and stages its own copy, and the hub writes no
	// plaintext at all. Kubernetes and remote are further out again.
	//
	// The rule this field encodes: the hub materialises a credential only for
	// a backend that will genuinely read it from there. Anywhere else, writing
	// it would create a plaintext file on the control plane that nothing ever
	// opens.
	SecretFilesFromHostPath bool `json:"secret_files_from_host_path,omitempty"`
	// MaxConcurrent is the advertised ceiling on simultaneously running
	// handles; 0 means unbounded/unknown.
	MaxConcurrent int `json:"max_concurrent,omitempty"`
	// Platform and Arch describe the execution target (GOOS/GOARCH style).
	Platform string `json:"platform,omitempty"`
	Arch     string `json:"arch,omitempty"`
}

Capabilities advertises what a driver can and cannot do. Callers use it to degrade gracefully (e.g. hide a "Stop" button for a driver that cannot signal) rather than to attempt an operation and handle the error.

type Clamp added in v0.0.2

type Clamp struct {
	// Resource is the human name of the field: "cpu", "memory", "disk", "pids".
	Resource string `json:"resource"`
	// Requested is what the run asked for; 0 means it asked for no limit.
	Requested int `json:"requested"`
	// Effective is what it was given.
	Effective int `json:"effective"`
	// Source names the ceiling that bound it — one of the CeilingSource
	// constants.
	Source string `json:"source"`
}

Clamp records one reduction a ceiling made, so it can be explained.

func BoundSpec added in v0.0.2

func BoundSpec(spec *Spec, projectPath, executorID string) []Clamp

BoundSpec lowers spec's *stated* resource requests to the ceilings in force for projectPath, and bounds the workspace fetch. It returns what it changed.

Every dispatch site calls it, and it lives here rather than beside any one of them because there are five: the Web UI's detached and synchronous paths, the failover supervisor, the reproduction runner and the REST API.

Why this does not fill in an unstated request

It is tempting to write the ceiling into a limit the project left at zero — zero means *no limit*, so leaving it alone looks like leaving the hole open. Doing that is wrong, and subtly so: it converts "this project stated nothing" into "this project explicitly requests the ceiling", and the drivers resolve a stated request as *more specific* than the executor's configured default. A hub with `executors.container.memory: 4g` under an 8 GB fleet ceiling would then give an unconfigured project 8 GB — the ceiling would have *raised* the limit, which is the one thing a ceiling may never do.

So the unstated case is closed one layer down, by the driver, which applies BoundLimit after it has resolved the request against its own default. This function handles what the driver cannot see: a stated request, which it lowers here so that the Spec persisted for failover, recorded in the provenance file and shown in the audit trail is the one that actually ran.

It never fails. A ceiling is a bound, not an admission decision: the outcome of asking for too much is a smaller sandbox, not a refused run.

func (Clamp) String added in v0.0.2

func (c Clamp) String() string

String renders the clamp the way an operator should read it.

The unstated case gets its own wording. "requested 0" would be a lie about what the project asked for — it asked for nothing, which meant everything — and a developer told their 0 became 2048 has been given a puzzle rather than an explanation.

type Clock

type Clock interface {
	// Now returns the current time.
	Now() time.Time
	// After returns a channel that fires once after d.
	After(d time.Duration) <-chan time.Time
}

Clock abstracts time so tests can drive a supervisor without sleeping.

var SystemClock Clock = realClock{}

SystemClock is the production Clock.

type Constraint

type Constraint string

Constraint identifies which requirement eliminated a candidate. It is a string rather than an int so it can cross the API boundary into a JSON error body and still mean something to whoever reads it.

const (
	ConstraintNoCandidates     Constraint = "no_candidates"
	ConstraintExecutorID       Constraint = "executor_id"
	ConstraintHealth           Constraint = "health"
	ConstraintHostPolicy       Constraint = "host_execution_policy"
	ConstraintIsolation        Constraint = "isolation"
	ConstraintVirtualization   Constraint = "virtualization"
	ConstraintKernelIsolation  Constraint = "kernel_isolation"
	ConstraintLabels           Constraint = "labels"
	ConstraintPlatform         Constraint = "platform"
	ConstraintArch             Constraint = "arch"
	ConstraintHarness          Constraint = "harness"
	ConstraintContainerRuntime Constraint = "container_runtime"
	ConstraintNetworkEgress    Constraint = "network_egress"
	ConstraintResourceLimits   Constraint = "resource_limits"
	ConstraintStream           Constraint = "stream"
	ConstraintSignal           Constraint = "signal"
	ConstraintMemory           Constraint = "memory"
	ConstraintCapacity         Constraint = "capacity"
	ConstraintImageOverride    Constraint = "image_override"
	ConstraintSandboxBuild     Constraint = "sandbox_build"
	ConstraintSandboxMounts    Constraint = "sandbox_mounts"
	ConstraintHostMounts       Constraint = "host_mounts"
	ConstraintDevices          Constraint = "devices"
	ConstraintInterfaces       Constraint = "interfaces"
	ConstraintEgressScope      Constraint = "egress_scope"
	ConstraintWorkspace        Constraint = "workspace"
	ConstraintWriteBack        Constraint = "write_back"
	ConstraintSecretFiles      Constraint = "secret_files"
	ConstraintRevocation       Constraint = "revocation"
	ConstraintAgentBuild       Constraint = "agent_build"
)

type DevicePermissions added in v0.0.2

type DevicePermissions string

DevicePermissions is the access a workload gets to a device node, in the runtime's own rwm vocabulary.

It is a string rather than a set of bools because that is the form both container runtimes and the Linux device cgroup take, and because the mistake worth preventing — silently widening "r" to "rwm" — is easier to see in a value that round-trips unchanged.

What this does and does not guarantee

The value becomes the third field of the runtime's `--device src:dst:perms`, which programs the container's *device cgroup*. On cgroup v1 that is the devices controller; on cgroup v2 it is an eBPF program attached to the container's cgroup, and whether it is attached at all depends on the kernel, the runtime build and the cgroup delegation — none of which cloop can interrogate. On a host where it is not attached, a device granted "r" is readable *and* writable, because the node inside the sandbox carries the host node's file mode and /dev/zero is 0666 nearly everywhere.

So this field is defence in depth and not a boundary cloop can promise. The control that does hold on every host is the node's own ownership and mode: a device at crw-rw---- root:dialout is unopenable by a sandbox whose UID is not in that group, regardless of cgroups. An operator who needs read-only access to be enforced sets it there. Preflight says so rather than leaving the stronger reading of this field to be assumed; see preflightDeviceMode.

It is still emitted, and still defaults to rw rather than the runtime's rwm, because on the hosts where the cgroup rule *is* enforced it is enforced exactly, and withholding mknod by default costs nothing.

const (
	// DeviceRead allows read only. Correct for a sensor or a capture device.
	DeviceRead DevicePermissions = "r"
	// DeviceReadWrite allows read and write. The common case: a serial line,
	// a GPU, a TPM.
	DeviceReadWrite DevicePermissions = "rw"
	// DeviceReadWriteMknod additionally allows mknod on the device's
	// major:minor. Needed by workloads that create their own nodes, and
	// worth naming separately because it is the one that lets a sandbox
	// fabricate a second reference to the same hardware.
	DeviceReadWriteMknod DevicePermissions = "rwm"
)

func (DevicePermissions) Valid added in v0.0.2

func (p DevicePermissions) Valid() bool

Valid reports whether p is one of the three permitted access strings.

type EgressScope added in v0.0.2

type EgressScope string

EgressScope is a project's request to confine its own IP-layer egress.

const (
	// EgressScopeUnset leaves the executor's own configuration in force. It
	// is the zero value, so every deployment that predates this field keeps
	// exactly the behaviour it had.
	EgressScopeUnset EgressScope = ""

	// EgressScopePublic allows the public Internet and drops everything in
	// the non-public block set — RFC1918 and ULA private space, link-local
	// (including the cloud metadata endpoint at 169.254.169.254),
	// carrier-grade NAT, multicast and loopback beyond the sandbox's own.
	//
	// This is the "let it out to the Internet, keep it off our network"
	// policy. It is the useful default for a task on a host that also has
	// routes an operator would rather a harness never discovered, and it is
	// the only scope that leaves the sandbox able to fetch dependencies.
	EgressScopePublic EgressScope = "public"

	// EgressScopeNone removes the sandbox's network interfaces entirely.
	//
	// It overlaps Spec.DisableNetwork and exists so that one field can carry
	// the whole decision: a project that writes `egress: none` and a project
	// that omits capabilities.network end up in the same place, and a reader
	// of the spec does not have to know that the absence of one key means the
	// same thing as the presence of another.
	EgressScopeNone EgressScope = "none"
)

func EgressScopes added in v0.0.2

func EgressScopes() []EgressScope

EgressScopes returns every nameable scope, sorted, for CLI help and validation messages. EgressScopeUnset is omitted: it is spelled by leaving the key out, not by naming it.

func ParseEgressScope added in v0.0.2

func ParseEgressScope(raw string) (EgressScope, error)

ParseEgressScope validates and normalises a user-supplied scope string.

func (EgressScope) Describe added in v0.0.2

func (s EgressScope) Describe() string

Describe returns the operator-facing sentence for this scope.

func (EgressScope) NeedsFilter added in v0.0.2

func (s EgressScope) NeedsFilter() bool

NeedsFilter reports whether honouring this scope requires the driver to install a per-workload IP-layer policy.

EgressScopeNone does not: taking the interfaces away is not a filter, and it needs neither nft nor CAP_NET_ADMIN. That distinction is what lets a project on an unprivileged host still express the strongest of these scopes.

func (EgressScope) RemovesNetwork added in v0.0.2

func (s EgressScope) RemovesNetwork() bool

RemovesNetwork reports whether this scope leaves the sandbox with no interfaces, in which case no filter has to be installed for it.

func (EgressScope) Valid added in v0.0.2

func (s EgressScope) Valid() bool

Valid reports whether s is a known scope. The unset scope is valid: it means "no opinion", which is a legitimate thing for a spec not to have.

type EgressScopeExplainer added in v0.0.2

type EgressScopeExplainer interface {
	// ExplainEgressScopeRefusal returns a sentence completing "this executor
	// cannot confine one project's egress independently of its neighbours; ...".
	ExplainEgressScopeRefusal() string
}

EgressScopeExplainer is implemented by a driver that can say, in its own vocabulary, why it is not carrying per-project egress scopes right now.

It exists because the remedy is driver-specific and the refusal is not. A container executor needs nft(8) and CAP_NET_ADMIN; a Kubernetes executor needs proof that the cluster's CNI enforces the NetworkPolicy it already creates, which is a different problem with a different command. Printing the container remedy at a Kubernetes operator sends them to a config key their deployment does not have — and a refusal whose remedy does not apply is one people resolve by deleting the capability from sandbox.yaml, which is exactly the outcome the refusal existed to prevent.

Deliberately an interface rather than a field on Capabilities, for the reason stated at SupportsRevocation: a string in a struct is a claim anyone can set and nobody keeps current, while a method is answered by the driver that knows.

type EventSink

type EventSink interface {
	// ExecutorTransition is called once per state change.
	ExecutorTransition(t Transition)
	// ExecutorFailover is called once per session moved off a dead node,
	// including when no replacement could be found (ev.Err is then set).
	ExecutorFailover(ev FailoverEvent)
}

EventSink receives supervisor observations for the event log and the UI. Implementations must not block: they are called from the probe goroutines.

type Executor

type Executor interface {
	// ID returns the stable, unique identifier of this executor instance.
	ID() string
	// Kind returns the driver implementation name (one of the Kind* consts).
	Kind() string
	// Capabilities describes what this executor supports.
	Capabilities() Capabilities
	// Start launches a workload and returns its handle. The workload
	// outlives ctx.
	Start(ctx context.Context, spec Spec) (Handle, error)
	// Signal delivers sig to a running handle. Signalling an
	// already-finished handle returns nil (the desired end state already
	// holds); an unknown handle returns ErrHandleNotFound.
	Signal(ctx context.Context, handleID string, sig Signal) error
	// Status reports the current state of a handle.
	Status(ctx context.Context, handleID string) (Status, error)
	// Stream returns a channel of output chunks for a handle. The channel
	// is closed when the workload finishes and all output has been
	// delivered. Output produced between Start and Stream is replayed
	// (bounded) so callers do not race the workload's first writes.
	// Cancelling ctx unsubscribes the caller without affecting the
	// workload or other subscribers.
	Stream(ctx context.Context, handleID string) (<-chan LogLine, error)
	// HealthCheck reports whether this executor is able to accept work.
	HealthCheck(ctx context.Context) error
}

Executor is a pluggable execution backend.

Implementations must be safe for concurrent use by multiple goroutines.

Context handling: the ctx passed to each method governs *that call*, not the lifetime of the workload. Cancelling the ctx passed to Start does not kill the started workload — it only aborts the act of starting it. This matters because the Web UI starts long-lived runs from short-lived HTTP request handlers; tying workload lifetime to the request context would kill every run the moment its originating request returned. Callers that *do* want ctx-bound lifetime should use Run.

func Get

func Get(id string) (Executor, error)

Get returns the executor with the given ID from the default registry.

func List

func List() []Executor

List returns every executor in the default registry.

func Resolve

func Resolve(projectPath string) (Executor, error)

Resolve returns the executor bound to projectPath in the default registry, falling back to the default executor.

func ResolveBinding

func ResolveBinding(projectPath string) (Executor, error)

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
	// Branches is the authorising grant's branch allowlist, when it has one:
	// branch-name globs every push made with this credential must match
	// (Task 20340). Not secret, and not enforceable by anything that holds
	// the credential — a git proxy between the holder and the forge is what
	// applies it (see pkg/executor/gitproxycreds).
	Branches []string
}

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

func (h Health) Normalize() Health

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.

func (Health) Stale

func (h Health) Stale(ref time.Time, ttl time.Duration) bool

Stale reports whether the node has gone longer than ttl without a successful probe. A node that has never been seen is stale only once ttl has elapsed since ref, so a control plane that just started does not declare its whole fleet stale before the first probe lands.

type HealthPolicy

type HealthPolicy struct {
	// DegradeAfter is the consecutive-failure count at which a ready node
	// becomes degraded. Must be >= 1.
	DegradeAfter int
	// UnreachableAfter is the consecutive-failure count at which a node is
	// declared unreachable and its work is failed over. Must be >=
	// DegradeAfter.
	UnreachableAfter int
}

HealthPolicy tunes how many failed probes it takes to give up on a node.

The two thresholds exist because the failure modes are different. One missed probe is usually a blip — a wifi roam, a GC pause on a Raspberry Pi — and reacting to it by failing over in-flight work would cause more damage than it prevents. Sustained failure means the device is genuinely gone, and holding its sessions hostage any longer just delays the user's task.

func DefaultHealthPolicy

func DefaultHealthPolicy() HealthPolicy

DefaultHealthPolicy degrades on the first failure and gives up after three. With the default 30s probe interval that is roughly a 90-second window before failover, which is long enough to ride out a reboot and short enough that a user watching a stalled run does not give up first.

type HealthStore

type HealthStore interface {
	// LoadHealth returns the persisted health for an executor. An unknown
	// executor yields the zero Health and a nil error — "never probed" is a
	// normal state, not a lookup failure.
	LoadHealth(executorID string) (Health, error)
	// SaveHealth persists a health record, creating it if absent.
	SaveHealth(h Health) error
	// ListHealth returns every persisted health record.
	ListHealth() ([]Health, error)
}

HealthStore persists the supervisor's view across control-plane restarts.

It is an interface, not a *statedb.DB, for the same reason the rest of this package avoids storage: pkg/executor is linked into the agent binary that runs on edge devices, and an executor driver has no business pulling in a SQLite engine. pkg/executorstore implements this over statedb.

type HostDevice added in v0.0.2

type HostDevice struct {
	// Name identifies the device within its grant, for diagnostics and for
	// the `devices:` selector in a sandbox spec.
	Name string `json:"name"`
	// Source is the absolute path of the device node on the executor's host.
	Source string `json:"source"`
	// Target is the absolute path it appears at inside the sandbox. Empty
	// means the same path as Source, which is what a workload looking for
	// well-known hardware expects.
	Target string `json:"target,omitempty"`
	// Permissions is the access granted. Empty means DeviceReadWrite, which
	// is what a device is useful with and is still short of mknod.
	Permissions DevicePermissions `json:"permissions,omitempty"`
	// LeaseID is the secret lease that authorised this device, so revocation
	// can find it and the audit trail can say who opened it. Empty is
	// permitted only for a device the operator configured directly on the
	// executor, which is not a path any project-scoped grant takes.
	LeaseID string `json:"lease_id,omitempty"`
	// KubernetesResource optionally names the extended resource a Kubernetes
	// node advertises for this hardware ("nvidia.com/gpu", "squat.ai/fuse").
	//
	// It exists because Kubernetes does not take device paths. A device
	// plugin owns the node and hands the container the right one in response
	// to a resource request, so on that driver the path is not the thing
	// asked for — the resource name is. A grant that omits it is honestly
	// undeliverable on Kubernetes rather than silently ignored there; see
	// Capabilities.SupportsDevices.
	KubernetesResource string `json:"kubernetes_resource,omitempty"`
}

HostDevice is one device node exposed from the executor's host into the sandbox.

Source and Target are separate because the sandbox-visible name is part of what makes a grant portable. A project told to open /dev/accel0 should not have to know that on this particular host the hardware happens to be /dev/nvidia3; the operator maps it, and the project's code does not change when it moves to a node that enumerates its hardware differently.

func (HostDevice) EffectivePermissions added in v0.0.2

func (d HostDevice) EffectivePermissions() DevicePermissions

EffectivePermissions is the access to apply, resolving the empty default.

func (HostDevice) EffectiveTarget added in v0.0.2

func (d HostDevice) EffectiveTarget() string

EffectiveTarget is where the device appears inside the sandbox.

type HostExecutionDeniedError

type HostExecutionDeniedError struct {
	// ExecutorID is the un-isolated executor that was resolved.
	ExecutorID string
	// ProjectPath is the project whose workload was refused; may be empty
	// for workloads not tied to a project.
	ProjectPath string
	// Alternatives are the IDs of registered executors that *do* isolate,
	// ordered for stable output. Empty means none are configured, which is a
	// materially different (and much more urgent) situation.
	Alternatives []string
}

HostExecutionDeniedError explains a refusal to run a workload on the host and, critically, names what the operator can do instead.

A bare "denied by policy" is the kind of error that generates a support ticket: the person who hits it in the UI is usually not the person who wrote the config. Carrying the alternatives in the error means the 409 the browser receives already contains the remediation, and `cloop run` prints the same sentence in a terminal.

func (*HostExecutionDeniedError) Error

func (e *HostExecutionDeniedError) Error() string

Error implements error.

func (*HostExecutionDeniedError) Remediation

func (e *HostExecutionDeniedError) Remediation() string

Remediation returns just the "what to do about it" half of the message, for UI surfaces that render the cause and the fix in separate elements.

func (*HostExecutionDeniedError) Unwrap

func (e *HostExecutionDeniedError) Unwrap() error

Unwrap lets callers match with errors.Is(err, ErrHostExecutionDenied) without caring whether they got the rich form or a plain wrap.

type HostInterface added in v0.0.3

type HostInterface struct {
	// Name identifies the interface within its grant, for diagnostics and for
	// the `interfaces:` selector in a sandbox spec. It is not an interface
	// name: it is a handle, and may be longer and more descriptive than the
	// kernel would allow a netdev to be.
	Name string `json:"name"`
	// Source is the interface's name on the executor's host.
	Source string `json:"source"`
	// Target is the name it takes inside the sandbox. Empty means keep
	// Source's name.
	//
	// Renaming is worth supporting because the host's name is frequently
	// unusable as a contract: a veth end is named for the pair that created
	// it, and a predictable-network-name NIC is named for its PCI path.
	Target string `json:"target,omitempty"`
	// Address is an optional CIDR to configure on the interface inside the
	// sandbox ("172.31.99.200/24", "fd00::10/64").
	//
	// Optional because the two sensible regimes are both real: a bench with a
	// DHCP server on it wants the sandbox to ask, and a bench without one
	// wants the operator to have decided. Empty means the interface arrives
	// up and unaddressed, which is also what a workload doing raw L2 wants.
	Address string `json:"address,omitempty"`
	// Gateway is an optional default route to install inside the sandbox via
	// this interface. Meaningless without Address and refused without it.
	//
	// Note what this does: it makes the sandbox's default route point at the
	// bench rather than at the runtime's own network. That is usually the
	// intent for an isolated segment and never the intent for a workload that
	// also needs the Internet, so it is opt-in per interface.
	Gateway string `json:"gateway,omitempty"`
	// MTU optionally overrides the interface's MTU inside the sandbox. Zero
	// means leave the kernel's value alone. Jumbo frames on a capture segment
	// are the common reason to set it.
	MTU int `json:"mtu,omitempty"`
	// LeaseID is the secret lease that authorised this interface, so
	// revocation can find it and the audit trail can say who opened it. Empty
	// is permitted only for an interface an operator configured directly on
	// the executor, which is not a path any project-scoped grant takes.
	LeaseID string `json:"lease_id,omitempty"`
}

HostInterface is one network interface moved from the executor's host into the sandbox's network namespace.

Source and Target are separate for the reason HostDevice's are: the sandbox-visible name is part of what makes a grant portable. A project told to bind "eth1" should not have to know that this particular bench enumerates the adapter as enp0s31f6, and its code should not change when it moves to a bench that enumerates it differently.

func (HostInterface) EffectiveTarget added in v0.0.3

func (i HostInterface) EffectiveTarget() string

EffectiveTarget is the interface's name inside the sandbox.

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

func (m HostMount) Validate() error

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 NewLeaseIndex

func NewLeaseIndex() *LeaseIndex

NewLeaseIndex returns an empty index.

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

func (s NodeState) AdminHeld() bool

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

func (s NodeState) Schedulable() bool

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.

func (NodeState) Valid

func (s NodeState) Valid() bool

Valid reports whether s is one of the known states.

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) Error

func (e *PlacementError) Error() string

Error implements error.

func (*PlacementError) Unwrap

func (e *PlacementError) Unwrap() error

Unwrap lets callers match with errors.Is(err, ErrNoPlacement).

type ProjectResult added in v0.0.3

type ProjectResult struct {
	// Data is the compressed result document. Empty when Err is set.
	Data []byte
	// Err is the executor's own account of why it could not read the run back
	// — the workload removed its project, the database was written by a newer
	// cloop than the device's. Operator-facing.
	Err string
	// Redact removes the credentials the hub leased to the workload from a
	// string. The hub applies it to everything it takes from Data rather than
	// trusting the device to have scrubbed: a result is written by the party a
	// redaction exists to protect against. Never nil.
	Redact func(string) string
}

ProjectResult is what an executor brought back from a seeded run (Task 20339): the compressed document pkg/executor/projectseed decodes and merges, or the device's reason for having none.

type ProjectResultFetcher added in v0.0.3

type ProjectResultFetcher interface {
	// ProjectResult returns what the device sent back for handleID, once: the
	// result is released to the caller and forgotten, because merging the same
	// run twice would count its spend twice. It returns
	// ErrProjectResultUnavailable when nothing arrived.
	//
	// Call it after the handle's output stream has closed. A device sends the
	// result before the terminal status, and the terminal status is what
	// closes the stream, so by then everything that is coming has come.
	ProjectResult(handleID string) (ProjectResult, error)
}

ProjectResultFetcher is implemented by drivers whose Capabilities report ReturnsProjectState.

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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) Bind

func (r *Registry) Bind(projectPath, executorID string) error

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

func (r *Registry) Binding(projectPath string) (string, bool)

Binding returns the executor ID bound to projectPath and whether one exists, consulting the in-memory map first and then the persistent lookup.

The path is first mapped through PolicyProjectPath, so a feature worktree resolves to its parent project's binding and can never land somewhere the parent was pinned away from.

func (*Registry) Default

func (r *Registry) Default() (Executor, error)

Default returns the fallback executor, or ErrNoDefault.

func (*Registry) DefaultID

func (r *Registry) DefaultID() string

DefaultID returns the current default executor ID, or "" when none is set.

func (*Registry) Ensure

func (r *Registry) Ensure(ex Executor) error

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) Get

func (r *Registry) Get(id string) (Executor, error)

Get returns the executor with the given ID, or ErrExecutorNotFound.

func (*Registry) IsolatedIDs

func (r *Registry) IsolatedIDs() []string

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) List

func (r *Registry) List() []Executor

List returns every registered executor, ordered by ID for stable output.

func (*Registry) Register

func (r *Registry) Register(ex Executor) error

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

func (r *Registry) Resolve(projectPath string) (Executor, error)

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

func (r *Registry) SetBindingLookup(fn func(projectPath string) (string, bool))

SetBindingLookup installs the persistent project→executor resolver. Pass nil to remove it.

func (*Registry) SetDefault

func (r *Registry) SetDefault(id string) error

SetDefault marks id as the fallback executor for projects with no binding. The executor must already be registered.

func (*Registry) Unbind

func (r *Registry) Unbind(projectPath string)

Unbind removes any in-memory binding for projectPath.

func (*Registry) Unregister

func (r *Registry) Unregister(id string)

Unregister removes the executor with the given ID. If it was the default, the default is reassigned deterministically (lowest remaining ID) so the registry never silently becomes defaultless while executors remain.

type Rejection

type Rejection struct {
	ExecutorID string     `json:"executor_id"`
	Constraint Constraint `json:"constraint"`
	Detail     string     `json:"detail"`
}

Rejection records why one candidate was not chosen.

type Requirements

type Requirements struct {
	// ExecutorID pins placement to one node. It is still checked for health
	// and policy: a pin is a statement about *where*, not a license to run on
	// a dead or forbidden node.
	ExecutorID string
	// Labels is a selector; every key must be present on the candidate with
	// the same value.
	Labels map[string]string
	// Harnesses lists agent CLIs the task will invoke. A candidate must
	// advertise all of them, unless it advertises none at all (see
	// hasHarness).
	Harnesses []string
	// Platform and Arch constrain the execution target (GOOS/GOARCH style).
	Platform string
	Arch     string
	// MinMemoryMB rejects nodes that report less memory than this. Nodes
	// reporting 0 (unknown) are not rejected.
	MinMemoryMB int
	// RequireIsolation rejects any node that shares the control-plane host.
	RequireIsolation bool
	// AllowedIsolations, when non-empty, restricts placement to these
	// isolation kinds. Isolation is not a total order (a container and a
	// remote device are differently, not more or less, isolated), so this is
	// a set rather than a minimum.
	AllowedIsolations []Isolation
	// RequireVirtualization demands a node whose workloads run behind a
	// hypervisor — a Kata sandbox, locally or on a remote cluster.
	//
	// Separate from AllowedIsolations because it cuts across it: both a local
	// Kata container (IsolationVM) and a Kata Pod on a remote cluster
	// (IsolationRemote) satisfy it, and no set of Isolation values selects
	// exactly those two without also admitting the non-Kata remote executors
	// that share their isolation value.
	RequireVirtualization bool
	// RequireKernelIsolation demands a node on which the workload's system
	// calls are not served by the executing machine's kernel — either a
	// hypervisor-backed sandbox (Kata) or a userspace kernel (gVisor).
	//
	// Weaker than RequireVirtualization and separate from it because the two
	// are different asks, and conflating them costs real placements. A project
	// whose actual requirement is "a kernel bug in my task must not be a
	// kernel bug on the host" is satisfied by gVisor; making it say
	// `virtualized: true` to express that would refuse every gVisor executor
	// in the fleet, so an operator who had deliberately standardised on gVisor
	// would watch their nodes rejected for a property they had in fact
	// provided.
	RequireKernelIsolation bool
	// RequireContainerRuntime demands a node that can drive containers.
	RequireContainerRuntime bool
	// RequireNetworkEgress demands a node whose workloads can reach the
	// network.
	RequireNetworkEgress bool
	// RequireDevices demands a node that honours Spec.Devices — one that can
	// expose a host device node inside the sandbox.
	//
	// The refusal it produces is the point. A granted /dev/ttyUSB0 is a
	// statement about one machine's hardware, so a project holding that grant
	// and bound to a Kubernetes or remote executor has asked for something no
	// amount of retrying will produce. Saying so at placement time names both
	// facts; the alternative is a harness that opens the path, gets ENOENT,
	// and reports that the device is broken.
	RequireDevices bool
	// RequireInterfaces demands a node that honours Spec.Interfaces — one
	// that can move a host network interface into the sandbox.
	//
	// Narrower than RequireDevices, and the extra narrowness is the point. A
	// device grant needs an executor on the machine with the hardware; an
	// interface grant needs that *and* a runtime whose kernel will notice a
	// link that appears after the sandbox has started, which rules out Kata
	// and gVisor on the very machine that has the wire. Placement is the only
	// layer that can say so before the host has already given the interface
	// away.
	RequireInterfaces bool
	// RequireEgressScope demands a node that can confine one workload's
	// IP-layer egress independently of its neighbours (Spec.EgressScope).
	RequireEgressScope bool
	// RequireResourceLimits demands a node that actually enforces
	// Spec.ResourceLimits rather than ignoring them.
	RequireResourceLimits bool
	// RequireImageOverride demands a node that honours Spec.Image, i.e. that
	// a per-project sandbox can choose its own toolchain there.
	//
	// This is the constraint that makes per-project sandboxes honest. Without
	// it a project pinning `image: rust:1.79` placed on the host driver would
	// run against whatever toolchain the control plane happens to have, get a
	// plausible-looking build failure, and send its author hunting through
	// their own code. Refusing placement and naming the reason is the only
	// outcome that points at the deployment instead.
	RequireImageOverride bool
	// RequireSandboxBuild demands a node that can bake Spec.SetupCommands
	// into a derived image.
	RequireSandboxBuild bool
	// RequireSandboxMounts demands a node that honours Spec.Mounts.
	RequireSandboxMounts bool
	// RequireHostMounts demands a node that honours Spec.HostMounts — a
	// driver that can bind a path from the control-plane host into the
	// sandbox, which is what a local_repo grant needs.
	//
	// This is the constraint that makes "local repositories" and "remote
	// sandbox" an honest either/or rather than a silent failure. A project
	// granted three checkouts on the hub and bound to a Kubernetes or remote
	// executor is refused here, naming both facts, instead of running against
	// an empty /repos on a machine that has never seen those files.
	RequireHostMounts bool
	// RequireWorkspaceProvisioning demands a node that can materialise the
	// source tree itself, because this workload's tree is not already there.
	//
	// Without this constraint the failure mode is the worst kind: the harness
	// starts, finds an empty directory, and reports back on a repository it
	// never saw. Refusing placement converts that into a message naming the
	// executor that cannot fetch.
	RequireWorkspaceProvisioning bool
	// RequireHostFilesystemWorkspace demands a node whose WorkDir really is a
	// path on the control-plane host, because the workload was dispatched with
	// Workspace.Kind "bind" — i.e. with the tree assumed to be there already.
	RequireHostFilesystemWorkspace bool
	// RequireWriteBack demands a node that can return the files the workload
	// changes, because this one's tree does not survive the run.
	//
	// It is the workspace constraint's mirror image, and its failure is the
	// worse of the two. An executor that cannot fetch produces a harness that
	// reports on a repository it never saw; one that cannot write back produces
	// a harness that reports, accurately, on work that is then discarded with
	// the sandbox. The first is confusing on arrival, the second is invisible
	// until someone goes looking for the commit.
	RequireWriteBack bool
	// RequireProjectSeed demands a node that places the hub's `.cloop/` into
	// the workspace it fetched, because a cloned tree carries a source
	// repository and not a cloop project.
	//
	// Its failure is loud but misdirected, which is its own hazard: the harness
	// exits on its first line with "no cloop project found (run 'cloop init'
	// first)", so the operator goes looking at the project — which is intact —
	// instead of at the executor that dropped its state.
	RequireProjectSeed bool
	// RequireSecretFiles demands a node that delivers a secret lease's
	// credential *files* to the workload, not only its environment.
	//
	// The failure it prevents is the quietest one in this list. A node that
	// drops the files still starts, still runs, and still receives the
	// environment variables naming them — so the workload holds
	// GIT_CONFIG_GLOBAL pointing at a directory that does not exist, gets no
	// token (a narrow github_pat deliberately exports none), and fails to
	// authenticate with nothing in the transcript connecting that to the
	// grant. Refusing placement is what turns it into a sentence.
	RequireSecretFiles bool
	// RequireRevocation demands a node that can take a secret lease's material
	// back from a workload that is already running.
	//
	// It is the constraint that makes "revoking a lease takes the credential
	// away" a property of the system rather than of which backend a project
	// happens to be bound to. A driver that cannot honour a revocation still
	// starts the workload and still receives the credential — it simply has no
	// way to give it back, so the TTL and the push revocation become advice.
	// Refusing placement is the only point at which that is visible.
	RequireRevocation bool
	// RequireStream and RequireSignal demand live output and the ability to
	// stop a workload — the two capabilities the Web UI's run panel needs.
	RequireStream bool
	RequireSignal bool
	// AllowDegraded permits placement on a degraded node. Default true via
	// the zero value being "not forbidden"; set ForbidDegraded to tighten.
	ForbidDegraded bool
	// IgnoreCapacity skips the MaxConcurrent check.
	IgnoreCapacity bool
}

Requirements describes what a task needs from a node. The zero value means "anything healthy", so a caller with no special needs still benefits from health filtering and capacity-aware tie-breaking.

type ResourceCeiling added in v0.0.2

type ResourceCeiling struct {
	// CPUMillis caps the CPU allowance in thousandths of a core.
	CPUMillis int `json:"cpu_millis,omitempty"`
	// MemoryMB caps resident memory in megabytes.
	MemoryMB int `json:"memory_mb,omitempty"`
	// DiskMB caps the writable-layer / scratch ceiling in megabytes.
	DiskMB int `json:"disk_mb,omitempty"`
	// PIDs caps the number of processes/threads.
	PIDs int `json:"pids,omitempty"`
}

ResourceCeiling bounds what a workload may be given. Zero on any field means "this ceiling says nothing about that resource" — not "zero of it" — which is what lets a ceiling constrain memory alone and leave CPU to whatever the executor would otherwise have done.

It mirrors ResourceLimits field for field on purpose. The two are separate types precisely because they are not interchangeable: assigning one to the other is the bug this file was written to remove, and the compiler now refuses it.

func CeilingFor added in v0.0.2

func CeilingFor(projectPath, executorID string) ResourceCeiling

CeilingFor returns the single ceiling in force for a workload: the fleet's, the executor's and the project's, tightened together.

This is what a driver calls, passing its own ID as executorID. The drivers need it because they, not this package, hold the last word on what a workload is given — see BoundSpec for why the ceiling cannot simply be written onto the Spec ahead of them.

An empty executorID asks only the fleet and the project. That is the honest answer for a caller that does not know where the workload will land, and it errs in the safe direction: a ceiling this function did not apply is one the driver still applies for itself, because the driver always knows its own ID.

func ExecutorResourceCeiling added in v0.0.3

func ExecutorResourceCeiling(executorID string) (ResourceCeiling, bool)

ExecutorResourceCeiling returns the ceiling recorded for executorID.

func FleetResourceCeiling added in v0.0.2

func FleetResourceCeiling() ResourceCeiling

FleetResourceCeiling returns the ceiling in force, or the zero ceiling if none was installed.

func ProjectResourceCeiling added in v0.0.2

func ProjectResourceCeiling(projectPath string) (ResourceCeiling, bool)

ProjectResourceCeiling returns the ceiling recorded for projectPath — for a feature worktree, its parent project's (see PolicyProjectPath).

func (ResourceCeiling) Describe added in v0.0.2

func (c ResourceCeiling) Describe() string

Describe renders the ceiling for a UI or a log line, tightest fields first in a stable order. An empty ceiling renders as "unlimited" rather than as the empty string, because a blank field where a policy was expected reads as a rendering bug.

func (ResourceCeiling) IsZero added in v0.0.2

func (c ResourceCeiling) IsZero() bool

IsZero reports whether the ceiling constrains nothing.

func (ResourceCeiling) Tighten added in v0.0.2

Tighten returns the stricter of two ceilings, resource by resource.

"Stricter" is the smaller *non-zero* value, because zero is silence rather than a bound of nothing: a ceiling naming only memory must not erase a CPU cap the other one set.

This is the operation the fleet ceiling is accumulated with, and it is one-directional for the same reason executor.ApplyMinAgentBuild is. A hub reads many projects' config.yaml; combining them symmetrically would let one tenant's file raise a fleet-wide cap for every other tenant in the process. Loosening a fleet ceiling is deliberate and explicit — restart with the looser config, the ceremony every other security-relevant setting asks for.

func (ResourceCeiling) Validate added in v0.0.2

func (c ResourceCeiling) Validate() error

Validate rejects negative caps. Zero (unset) is always valid.

Negative is rejected rather than normalised because in ResourceLimits a negative PIDs value is the runtimes' "unlimited" sentinel, and a *ceiling* that means unlimited is a ceiling that should not have been written. Silently reading it as zero would turn a cap an operator believed they had set into no cap at all, which is the one failure mode this type may not have.

type ResourceLimits

type ResourceLimits struct {
	// CPUMillis is the CPU allowance in thousandths of a core (1000 = 1 CPU).
	CPUMillis int `json:"cpu_millis,omitempty"`
	// MemoryMB is the resident memory ceiling in megabytes.
	MemoryMB int `json:"memory_mb,omitempty"`
	// DiskMB is the writable-layer / scratch-space ceiling in megabytes.
	DiskMB int `json:"disk_mb,omitempty"`
	// PIDs is the maximum number of processes/threads.
	PIDs int `json:"pids,omitempty"`
}

ResourceLimits bounds a single workload. Zero means "no limit requested". Drivers that do not advertise SupportsResourceLimits ignore this struct entirely rather than failing the Start.

func (ResourceLimits) IsZero

func (rl ResourceLimits) IsZero() bool

IsZero reports whether no limit at all was requested.

func (ResourceLimits) Validate

func (rl ResourceLimits) Validate() error

Validate rejects negative limits. Zero (unset) is always valid.

type RevocationDeniedError

type RevocationDeniedError struct {
	// ExecutorID and Kind name the driver that was refused.
	ExecutorID string
	Kind       string
	// Bindings are the revocable bindings that made the spec ineligible.
	Bindings []SecretBinding
	// Detail is the driver's own reason, when it implements Revoker but
	// cannot honour a revocation at this moment (an offline agent, a
	// protocol too old). Empty when the driver does not implement Revoker
	// at all.
	Detail string
}

RevocationDeniedError explains a refused placement in the terms the operator needs: which executor, which credentials, and what to do about it.

func (*RevocationDeniedError) Error

func (e *RevocationDeniedError) Error() string

func (*RevocationDeniedError) Unwrap

func (e *RevocationDeniedError) Unwrap() error

type RevocationLog

type RevocationLog struct {
	// contains filtered or unexported fields
}

RevocationLog is a driver's record of the leases it has been told to take back, so they can be replayed and reported.

It is shared by every Revoker rather than reimplemented per driver: the eviction rule above is subtle enough that three copies would eventually become three different rules, and the one that quietly dropped a pending entry would lose a revocation rather than a log line.

The zero value is not usable; call NewRevocationLog.

func NewRevocationLog

func NewRevocationLog() *RevocationLog

NewRevocationLog returns an empty log.

func (*RevocationLog) Fail

func (rl *RevocationLog) Fail(leaseID, grantID string, state RevokeState, err error)

Fail marks an entry undelivered, without discarding it: it stays in the log so a later replay can retry it.

func (*RevocationLog) Pending

func (rl *RevocationLog) Pending() []RevokeOutcome

Pending returns the revocations still owed, oldest first.

func (*RevocationLog) Record

func (rl *RevocationLog) Record(res RevokeOutcome) RevokeOutcome

Record inserts or refreshes an entry, returning a copy of the live state.

func (*RevocationLog) Settle

func (rl *RevocationLog) Settle(leaseID, grantID string, ack RevokeReport, at time.Time)

Settle applies a driver's report to an entry.

func (*RevocationLog) Snapshot

func (rl *RevocationLog) Snapshot() []RevokeOutcome

Snapshot returns every entry, newest first.

type RevokeAction

type RevokeAction string

RevokeAction is what a driver should do about a workload still using the material being taken away.

const (
	// RevokeScrub invalidates the material and lets the task keep running.
	// It will fail naturally the next time it reaches for the credential.
	//
	// This is the default because a long autonomous run is usually doing
	// many things, only one of which needed the revoked credential. Killing
	// it to revoke a kubeconfig it used an hour ago throws away hours of
	// work to close a window that scrubbing already closes.
	//
	// A driver that cannot scrub a particular material without terminating
	// the workload escalates and reports the kill. Silently doing nothing
	// would be the only worse answer than killing.
	RevokeScrub RevokeAction = "scrub"
	// RevokeKill scrubs and then terminates every task holding the lease.
	//
	// It exists because scrubbing has one hard limit: a credential handed to
	// a process as an environment variable is in that process's own memory,
	// and no control plane can reach into another process's heap. When the
	// credential itself is compromised rather than merely over-granted,
	// killing the task is the only thing that actually stops its use.
	RevokeKill RevokeAction = "kill"
)

func (RevokeAction) Valid

func (a RevokeAction) Valid() bool

Valid reports whether the action is one a driver knows.

type RevokeOutcome

type RevokeOutcome struct {
	LeaseID    string      `json:"lease_id"`
	GrantID    string      `json:"grant_id,omitempty"`
	ExecutorID string      `json:"executor_id"`
	State      RevokeState `json:"state"`
	// Action is what was asked for, echoed so a caller reading only the
	// outcome knows whether a kill was requested.
	Action RevokeAction `json:"action,omitempty"`
	Reason string       `json:"reason,omitempty"`
	// SentAt / AckedAt bound how long the material was still live after the
	// operator pressed the button. Operators ask this after an incident.
	SentAt  time.Time `json:"sent_at"`
	AckedAt time.Time `json:"acked_at,omitempty"`
	// Ack is the driver's report, present once State is RevokeStateRevoked.
	Ack *RevokeReport `json:"ack,omitempty"`
	// Error explains a failed or unreachable outcome.
	Error string `json:"error,omitempty"`
}

RevokeOutcome is one executor's result for one lease revocation.

func (RevokeOutcome) Pending

func (r RevokeOutcome) Pending() bool

Pending reports whether this outcome still needs to be delivered.

type RevokeReport

type RevokeReport struct {
	LeaseID string       `json:"lease_id"`
	GrantID string       `json:"grant_id,omitempty"`
	Action  RevokeAction `json:"action,omitempty"`
	// Known reports whether the driver was holding this lease at all. False
	// is a success, not a failure: the material is not here, which is the
	// end state the revocation asked for.
	Known bool `json:"known"`
	// EnvScrubbed names the variables whose values were dropped. Names only
	// — echoing a revoked credential back to the control plane would be a
	// fine way to write it into a log.
	//
	// A name here means the *control plane's* copy is gone. It does not mean
	// a process that already forked with that variable has lost it; see
	// Killed for the material that needed stronger treatment.
	EnvScrubbed []string `json:"env_scrubbed,omitempty"`
	// FilesRemoved counts credential files wiped.
	FilesRemoved int `json:"files_removed,omitempty"`
	// EgressDropped reports that the lease's network allowlist entry is gone
	// *on this executor*.
	//
	// Only the remote driver sets it, and that is correct rather than an
	// omission. A remote agent keeps an allowlist of its own on the device, so
	// it has something local to drop. The hub's own drivers do not: the
	// allowlist lives in the egress broker's proxy session, which the hub tears
	// down with the lease itself before any driver is asked — reported as
	// WipedLocally, not here. A container or Kubernetes driver claiming to have
	// dropped it would be taking credit for another component's work, and would
	// read as a second, independent guarantee where there is one.
	EgressDropped bool `json:"egress_dropped,omitempty"`
	// Killed lists the handles terminated — for RevokeKill, and for a
	// RevokeScrub the driver could not honour without terminating.
	Killed []string `json:"killed,omitempty"`
	// Error is non-empty when part of the revocation failed. The report is
	// still returned: "I tried and this is what went wrong" is far more
	// actionable than silence, which a hub can only read as unreachable.
	Error string `json:"error,omitempty"`
}

RevokeReport is what a driver actually did.

It is deliberately specific rather than a bare "ok". An operator revoking a kubeconfig needs to know whether the file is gone or whether the driver merely forgot about it, and a report that cannot distinguish the two would let a UI claim a guarantee the system did not deliver.

type RevokeRequest

type RevokeRequest struct {
	// LeaseID is the lease being revoked. Required.
	LeaseID string `json:"lease_id"`
	// GrantID narrows the revocation to one grant within the lease. Empty
	// revokes everything the lease delivered.
	GrantID string `json:"grant_id,omitempty"`
	// Reason is operator-facing text carried into the driver's log and the
	// audit trail, so "why did my run lose its token" has an answer on both
	// sides of the link.
	Reason string `json:"reason,omitempty"`
	// Action is scrub or kill. An empty or unknown action is treated as
	// scrub: the conservative reading of a request from a possibly-newer
	// control plane is the one that does not destroy work.
	Action RevokeAction `json:"action,omitempty"`
}

RevokeRequest takes one lease's material back from a running executor.

func (RevokeRequest) Effective

func (r RevokeRequest) Effective() RevokeAction

Effective returns the action to apply, defaulting an absent or unknown value to RevokeScrub.

func (RevokeRequest) Matches

func (r RevokeRequest) Matches(b SecretBinding) bool

Matches reports whether a binding is in scope for this request.

A request naming only a lease covers every grant under it; one naming a grant covers just that grant. Getting this backwards in either direction is a real failure — too narrow leaves material behind, too wide takes away credentials the operator did not revoke — so it is written once here rather than re-derived per driver.

type RevokeState

type RevokeState string

RevokeState is how far one lease's revocation has got.

const (
	// RevokeStatePending: the revocation is on its way, or is queued for a
	// driver that is not currently reachable but is expected back.
	RevokeStatePending RevokeState = "revoke_pending"
	// RevokeStateRevoked: the driver reported what it took back.
	RevokeStateRevoked RevokeState = "revoked"
	// RevokeStateUnreachable: the executor is offline, so the material is
	// still out there. The revocation is queued and will be replayed, but
	// until then this is the honest state and the UI must show it as such.
	RevokeStateUnreachable RevokeState = "unreachable"
	// RevokeStateFailed: the driver answered, and the answer was an error.
	RevokeStateFailed RevokeState = "failed"
)

func (RevokeState) Terminal

func (s RevokeState) Terminal() bool

Terminal reports whether the state can still change on its own. Only RevokeStateRevoked is final; a failure is retried on the next sweep and an unreachable executor is retried when it reconnects.

type Revoker

type Revoker interface {
	Executor

	// SupportsRevocation reports whether a revocation issued *right now*
	// would be honoured.
	//
	// It is separate from implementing the interface because the two answer
	// different questions. Implementing Revoker is a property of the driver
	// and is known at compile time; this is a property of the moment — a
	// remote agent that is offline, or one speaking a protocol too old for
	// the frame, cannot honour anything even though its driver can.
	SupportsRevocation() bool

	// HoldsLease reports whether any workload this executor tracks was
	// started with material from leaseID.
	HoldsLease(leaseID string) bool

	// Leases lists the lease IDs this executor is currently holding material
	// for, sorted.
	Leases() []string

	// RevokeLease takes one lease's material back.
	//
	// It returns an outcome rather than an error because the caller's next
	// move depends on *which* failure: an error alone cannot say whether the
	// material is gone, in doubt, or definitely still out there. A driver
	// that was not holding the lease returns RevokeStateRevoked with
	// Ack.Known false — "not here" is the end state a revocation wants.
	RevokeLease(ctx context.Context, req RevokeRequest) RevokeOutcome

	// Revocations reports this executor's revocation log, newest first, so
	// the Secrets panel can show what landed and what is still owed.
	Revocations() []RevokeOutcome
}

Revoker is an executor that can take a secret lease's material back from a workload it has already started.

It is optional in the Go sense and mandatory in the policy sense: a driver that does not implement it is refused any spec carrying revocable bindings (see RequireRevocable), so the omission surfaces as a named refusal at placement time rather than as a credential nobody can withdraw.

Implementations must be safe for concurrent use.

func AsRevoker

func AsRevoker(ex Executor) (Revoker, bool)

AsRevoker returns ex as a Revoker, reporting whether it is one.

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

func Run(ctx context.Context, ex Executor, spec Spec) (RunResult, error)

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 SandboxMode added in v0.0.3

type SandboxMode string

SandboxMode says where a payload runs on a given executor.

const (
	// SandboxModeDefault is the zero value: the executor keeps whatever it
	// would have done without this configuration. It exists so that adding
	// the field changed no deployment's behaviour, and so that "unset" is
	// distinguishable from "host" — an admin who never opened the panel has
	// not thereby asserted that host execution is acceptable.
	SandboxModeDefault SandboxMode = ""
	// SandboxModeHost runs the payload directly on the executor's host, with
	// the executor's own privileges and filesystem.
	SandboxModeHost SandboxMode = "host"
	// SandboxModeContainer runs the payload in a container on the executor's
	// host, with the workspace bind-mounted in.
	SandboxModeContainer SandboxMode = "container"
)

func SandboxModes added in v0.0.3

func SandboxModes() []SandboxMode

SandboxModes lists the modes an admin may select, in UI order. The default is omitted: it is what a record's absence already means, so offering it as a choice would be offering "leave this alone" as an action.

func (SandboxMode) Isolates added in v0.0.3

func (m SandboxMode) Isolates() bool

Isolates reports whether m puts a boundary between the payload and the executor's host.

Note that SandboxModeDefault answers false. That is deliberate and is the conservative direction: an unset mode means the executor does whatever it did before, which for the one driver that reads this field was host execution. A caller asking "is this contained" must not be told yes by a value that means "nobody has said".

func (SandboxMode) Valid added in v0.0.3

func (m SandboxMode) Valid() bool

Valid reports whether m is a mode this build understands.

type SandboxSettings added in v0.0.3

type SandboxSettings struct {
	// Mode is where payloads run: on the executor's host, or in a container
	// on it.
	Mode SandboxMode `json:"mode,omitempty"`
	// Engine is the container CLI to drive ("docker", "podman", "nerdctl").
	// Empty lets the executor detect one. Ignored unless Mode is container.
	Engine string `json:"engine,omitempty"`
	// Runtime is the OCI runtime the engine hands each container to — the
	// `--runtime` value. Empty means the engine's default, which is runc or
	// crun. This is the field that decides whether the payload sits behind a
	// hypervisor (kata) or a userspace kernel (runsc); see IsVirtualized.
	Runtime string `json:"runtime,omitempty"`
	// Image is the container image payloads run in. Empty means the
	// executor's own configured default. Ignored unless Mode is container.
	Image string `json:"image,omitempty"`
	// Network is the container network payloads are attached to: "none"
	// (the default), "bridge", or a network the operator created on the
	// device. Ignored unless Mode is container.
	//
	// It exists because "none" is both the right default and unusable for the
	// flow this product is built around. A sandbox reaches its repositories
	// through the hub's git interception proxy, its cluster through the
	// Kubernetes access monitor and its Internet through the egress broker —
	// and all three are *network services*, addressed by URL. Without a
	// network the payload cannot resolve or connect to any of them, so a
	// container-mode remote executor could be granted credentials it had no
	// way to use, and the failure surfaced inside the sandbox as a DNS error
	// rather than at the hub as a refusal.
	//
	// Its own field rather than a derived one, because the answer is a
	// property of the device: which network exists, and whether it reaches
	// the hub, is something the admin who enrolled it knows and the hub does
	// not. Deriving it from "does this dispatch need the proxy" would also
	// silently widen a payload's reach every time a grant was added.
	Network string `json:"network,omitempty"`
}

SandboxSettings is the per-executor sandbox configuration an admin sets.

Every field's zero value means "unset", and an unset field is never an assertion — it defers to whatever the executor would otherwise have done. That is what lets an admin pin the OCI runtime of one executor without thereby choosing an image for it.

func (SandboxSettings) Describe added in v0.0.3

func (s SandboxSettings) Describe() string

Describe renders the settings for a log line or a diagnostic. It names the mode even when nothing else is set, because "which mode" is the question this type exists to answer and "default" is a real answer to it.

func (SandboxSettings) IsKernelIsolated added in v0.0.3

func (s SandboxSettings) IsKernelIsolated() bool

IsKernelIsolated reports whether these settings keep the payload's syscalls off the host kernel — Kata's guest kernel or gVisor's userspace one.

func (SandboxSettings) IsVirtualized added in v0.0.3

func (s SandboxSettings) IsVirtualized() bool

IsVirtualized reports whether these settings put payloads behind a hypervisor. It is false unless the mode is container: a runtime name is not a boundary on its own, and host execution with `runtime: kata` recorded against it is contained by nothing.

func (SandboxSettings) IsZero added in v0.0.3

func (s SandboxSettings) IsZero() bool

IsZero reports whether the settings configure nothing, which is what an executor with no record has.

func (SandboxSettings) Normalize added in v0.0.3

func (s SandboxSettings) Normalize() SandboxSettings

Normalize trims whitespace and drops fields that the selected mode makes meaningless, so a record cannot claim an engine for host execution.

Dropping rather than rejecting is the right behaviour for a UI that keeps a form's fields visible when the mode selector changes: an admin who picks container, types a runtime, then switches back to host has not made an error, they have changed their mind. What must not happen is the settings *retaining* the runtime, because then a later switch back to container would silently resurrect a value nobody re-confirmed.

func (SandboxSettings) Validate added in v0.0.3

func (s SandboxSettings) Validate() error

Validate reports whether the settings are admissible.

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"
)

func (Signal) Valid

func (sig Signal) Valid() bool

Valid reports whether sig is one of the known signals.

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. See LabelRunID for the one a driver reads.
	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"`
	// Harness names the agent CLI this workload will invoke once it is running
	// — "claude" for the claudecode provider, and empty for a provider that
	// talks to an HTTP API and so needs no binary on the machine.
	//
	// Argv does not carry it: argv is `cloop run`, and which harness that
	// process goes on to exec is a property of the *project's* configuration,
	// resolved inside the workload long after placement is over. So the one
	// thing an executor must have for the run to get anywhere is the one thing
	// nothing downstream could see, and a device that advertised no `claude`
	// was dispatched to anyway — failing on the far side with
	//
	//	exec: "claude": executable file not found in $PATH
	//
	// after the hub had leased a credential, delivered it to the device and
	// provisioned a workspace. Every input to that refusal was on the hub
	// before any of it happened.
	//
	// It is a requirement, not an instruction: no driver execs this. It exists
	// so SandboxRequirements can state it and placement can refuse on it, which
	// is why an unset value is not "no harness" but "no opinion" — a caller
	// with no basis for the claim (`cloop executor test`, a smoke run) must not
	// be forced to invent one.
	Harness string `json:"harness,omitempty"`

	// Image overrides the executor's configured sandbox image for this run.
	// Empty means "use the executor's image". A driver that does not
	// advertise SupportsImageOverride must reject a non-empty value rather
	// than run the wrong toolchain silently.
	Image string `json:"image,omitempty"`
	// SetupCommands are shell commands baked into a derived image once per
	// unique (base image, command list) pair — not re-run per workload.
	// Requires SupportsSandboxBuild.
	SetupCommands []string `json:"setup_commands,omitempty"`
	// Mounts re-expose sub-paths of WorkDir elsewhere in the sandbox. See
	// SpecMount: sources are workspace-relative and cannot escape it.
	Mounts []SpecMount `json:"mounts,omitempty"`
	// HostMounts bind absolute paths on the executor's host into the
	// sandbox. Unlike Mounts they may point anywhere, which is why nothing
	// that parses a repo-committed file is allowed to set them: the only
	// writer is a secret lease (secretbroker.KindLocalRepo), where a human
	// named the path and the broker recorded who. See HostMount.
	HostMounts []HostMount `json:"host_mounts,omitempty"`
	// Devices expose host device nodes inside the sandbox. Like HostMounts
	// and for the same reason, nothing that parses a repo-committed file may
	// set them: the only writer is a secret lease
	// (secretbroker.KindHostDevice), where a human named the device and the
	// broker recorded who. A sandbox spec may select among what the project
	// was granted, never add to it. See HostDevice.
	Devices []HostDevice `json:"devices,omitempty"`
	// Interfaces move host network interfaces into the sandbox's network
	// namespace, giving the workload a seat on the segment they are attached
	// to rather than a route through the host.
	//
	// Same authority rule as Devices, and nothing that parses a
	// repo-committed file may set them: the only writer is a secret lease
	// (secretbroker.KindHostInterface). A sandbox spec may select among what
	// the project was granted, never add to it. See HostInterface.
	//
	// Unlike every other field here, honouring this one takes something away
	// from the host: an interface belongs to exactly one network namespace,
	// so for the life of the workload the executor cannot use it.
	Interfaces []HostInterface `json:"interfaces,omitempty"`
	// EgressScope confines this workload's IP-layer network reach
	// independently of the other workloads on the same executor.
	//
	// One-directional like DisableNetwork: a scope can only take reach away.
	// A project that needs to reach private address space says so with an
	// egress grant, which an operator issues; see egressscope.go.
	EgressScope EgressScope `json:"egress_scope,omitempty"`
	// DisableNetwork forces this workload off the network regardless of how
	// the executor is configured.
	//
	// It is deliberately one-directional. A sandbox spec is repo-committed
	// input, so it may *narrow* what the operator granted and may never widen
	// it; there is no corresponding EnableNetwork field, and a spec that wants
	// egress gets it only by the executor already having it (see
	// Requirements.RequireNetworkEgress, which refuses placement instead of
	// quietly turning the network on).
	DisableNetwork bool `json:"disable_network,omitempty"`
	// SandboxHash identifies the sandbox spec this workload was built from,
	// for the audit trail. Drivers surface it as a label; it never affects
	// execution.
	SandboxHash string `json:"sandbox_hash,omitempty"`

	// Secrets attributes parts of Env and the filesystem to the secret
	// leases that produced them, so a driver can take an individual
	// credential back mid-run. See SecretBinding.
	Secrets []SecretBinding `json:"secrets,omitempty"`

	// SecretFiles carries the *contents* of those credential files, for a
	// driver that has to place them itself because the workload cannot read
	// the control plane's filesystem. Empty when the hub materialised them
	// (see Capabilities.SecretFilesFromHostPath) — then Secrets[].Files
	// already names paths the workload can open.
	//
	// json:"-" is a hard rule, not a default. This is the one field of a Spec
	// that holds plaintext, and a Spec is persisted by pkg/executorstore,
	// echoed into the audit trail, and re-read by reconcile after a restart.
	// A wire format that legitimately needs the bytes opts in with a type of
	// its own; see remote.StartPayload.SecretFiles.
	SecretFiles []SecretFile `json:"-"`

	// ProjectSeed carries the hub's `.cloop/` project state — goal,
	// instructions, provider selection and plan — for a workload whose
	// workspace had to be fetched rather than shared. Empty for a bind
	// workspace, where the real `.cloop/` is already at WorkDir.
	//
	// json:"-" for size rather than for secrecy; see seed.go.
	ProjectSeed []byte `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

func (s Spec) NeedsSecretFiles() bool

NeedsSecretFiles reports whether this Spec depends on credential files reaching the workload — either because the hub already wrote them and the bindings name them, or because the bytes are travelling with the Spec.

It is what SandboxRequirements derives RequireSecretFiles from, so both delivery modes are gated by the same question.

func (Spec) Redactor added in v0.0.2

func (s Spec) Redactor() *redact.Set

Redactor returns a redact.Set over SecretValues, or nil when this Spec carries no credential — which lets a driver install it unconditionally.

func (*Spec) RelocateSecrets

func (s *Spec) RelocateSecrets(from, to string)

RelocateSecrets rewrites every reference to the lease directory from so it names to instead: the environment the broker built, the bindings the vault indexes, and the files themselves.

A driver needs this when it cannot place the files where the hub said. The remote agent is the case: the hub has no idea what is writable on an edge device, so it declares a nominal directory, and the agent picks a real one under its own tmpfs and calls this. Without it the workload would find GIT_CONFIG_GLOBAL pointing at a path the agent never created — the exact failure the whole file exists to remove, moved one hop.

Prefix matching is safe because a lease directory name carries 96 bits of randomness: no unrelated value in the environment can begin with it.

func (Spec) RevocableSecrets

func (s Spec) RevocableSecrets() []SecretBinding

RevocableSecrets returns the bindings in this spec that carry revocable material. A driver that cannot honour revocation must refuse such a spec rather than run it and silently drop the guarantee.

func (Spec) SandboxRequirements

func (s Spec) SandboxRequirements() Requirements

SandboxRequirements returns the placement constraints implied by the sandbox-shaped fields of this Spec.

It exists so the one mapping from "what the spec asks for" to "what an executor must therefore support" lives beside the fields themselves, instead of being re-derived (and eventually re-derived differently) at each of the several call sites that place work.

func (Spec) SecretValues added in v0.0.2

func (s Spec) SecretValues() []string

SecretValues returns the plaintext this Spec carries into the workload, for a driver to scrub out of the output it captures.

Two sources, matching the two ways material reaches a workload:

  • SecretFiles, when the driver places the bytes itself (a container's staged tmpfs, a projected Kubernetes Secret, a remote agent's vault).
  • Env, for the variables the lease declared credential-bearing.

A localprocess workload has neither: the hub materialised the files on the shared filesystem, so Spec.SecretFiles is empty and only the paths travel. That case is covered on the other side of the boundary, where the workload reads its own lease directory — see redact.FromEnviron. Both halves exist because output is captured on both sides.

func (Spec) Timeout

func (s Spec) Timeout() time.Duration

Timeout returns the spec's wall-clock ceiling as a duration, or 0 when unbounded.

func (Spec) Validate

func (s Spec) Validate() error

Validate checks the driver-independent invariants of a Spec. Drivers add their own checks on top (e.g. localprocess requires an existing WorkDir).

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.

func (SpecMount) Validate

func (m SpecMount) Validate() error

Validate enforces the containment rules.

Rejections are stated in terms of the offending field rather than as a generic "invalid mount", because the author of the file is a developer writing YAML in a repo, not the operator who will read the log.

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"
)

func (State) Terminal

func (s State) Terminal() bool

Terminal reports whether no further transitions are expected.

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

func LiveHandles(ctx context.Context, ex Executor) ([]Status, bool)

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 WithClock

func WithClock(c Clock) SupervisorOption

WithClock injects a clock, for tests.

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

func (w Workspace) BaseURL() string

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

func (w Workspace) Describe() string

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

func (w Workspace) GitPlan(dir string) ([]GitStep, error)

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) Host

func (w Workspace) Host() string

Host returns the repo's host, or "" when the workspace is not a git one.

func (Workspace) IsZero

func (w Workspace) IsZero() bool

IsZero reports whether the workspace says nothing at all.

func (Workspace) NeedsProvisioning

func (w Workspace) NeedsProvisioning() bool

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

func (w Workspace) RepoPath() (string, bool)

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

func (w Workspace) RequiresCredential() bool

RequiresCredential reports whether provisioning needs a leased credential.

func (Workspace) Validate

func (w Workspace) Validate() error

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"
	// WorkspaceExecutor: the working directory belongs to the executor and
	// persists there between dispatches. The hub sends no source tree at all —
	// only the project's control state, as Spec.ProjectSeed.
	//
	// This is the answer for a project that has no source of its own to fetch:
	// no git remote, and often nothing in the directory but `.cloop/`. That
	// shape is not a misconfiguration, it is the ordinary one for the flow this
	// product is built around — a project is a unit of work whose *code* is the
	// set of repositories granted to it, which the harness clones for itself
	// through the git proxy once it is running. Before this kind existed such a
	// project could not run off-host at all: applyWorkspace found no remote and
	// refused the dispatch, telling the operator to invent a git remote for a
	// project that legitimately has none.
	//
	// Three properties distinguish it from WorkspaceNone, which is otherwise
	// the closest fit:
	//
	//   - the directory is *kept*. WorkDir is derived deterministically from
	//     the hub's project path (see DeviceWorkDir), so every dispatch of the
	//     same project lands in the same place and a tree the harness cloned
	//     for task 1 is still there for task 2. "none" promises the opposite —
	//     an empty tree — and a reader who believed it would be wrong.
	//   - a seed is mandatory rather than best-effort. On the git path `.cloop/`
	//     may already be committed in the fetched repository, so a missing seed
	//     degrades. Here there is no other source for it: without the seed the
	//     directory holds no project and the harness exits on its first line.
	//   - it says the project path names a location on the *executor*. That is
	//     the whole point, and it is the thing the operator has to understand
	//     to reason about where their work lives.
	WorkspaceExecutor WorkspaceKind = "executor"
	// WorkspaceNone: an intentionally empty working tree.
	WorkspaceNone WorkspaceKind = "none"
)

func (WorkspaceKind) KeepsWorkDir added in v0.0.3

func (k WorkspaceKind) KeepsWorkDir() bool

KeepsWorkDir reports whether the executor is expected to preserve WorkDir between dispatches of the same project.

Only WorkspaceExecutor makes that promise. A bind workspace is the hub's own directory and is not the executor's to keep or discard; a git workspace is re-fetched; "none" is empty by definition. Callers use this to decide whether reusing a directory is correct or is the stale-state bug projectseed's package comment describes.

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) BundleCap

func (w WriteBack) BundleCap() int64

BundleCap returns the effective byte ceiling for a bundle.

func (WriteBack) Describe

func (w WriteBack) Describe() string

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.

func (WriteBack) Enabled

func (w WriteBack) Enabled() bool

Enabled reports whether anything is written back at all.

func (WriteBack) IsZero

func (w WriteBack) IsZero() bool

IsZero reports whether the write-back says nothing.

func (WriteBack) Validate

func (w WriteBack) Validate() error

Validate checks the driver-independent invariants.

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.

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 autoupdate decides which devices in the fleet should be rolled forward, and when (Task 20331).
Package autoupdate decides which devices in the fleet should be rolled forward, and when (Task 20331).
Package container implements the executor.Executor interface on top of the Docker/Podman command line, running each workload in a resource- and network-isolated sandbox.
Package container implements the executor.Executor interface on top of the Docker/Podman command line, running each workload in a resource- and network-isolated sandbox.
Package gitcreds leases the credential a git workspace fetch needs.
Package gitcreds leases the credential a git workspace fetch needs.
Package gitprovision materialises a workload's source tree where the workload is about to run.
Package gitprovision materialises a workload's source tree where the workload is about to run.
Package gitproxycreds interposes a git interception proxy between a sandbox and the forge.
Package gitproxycreds interposes a git interception proxy between a sandbox and the forge.
Package gitwriteback turns the files a harness changed into something the hub can merge.
Package gitwriteback turns the files a harness changed into something the hub can merge.
Package install turns an enrollment bundle into a running, restart-on-boot, hardened cloop executor agent on an edge device.
Package install turns an enrollment bundle into a running, restart-on-boot, hardened cloop executor agent on an edge device.
internal
gitforge
Package gitforge stands up a real, hermetic git remote for tests.
Package gitforge stands up a real, hermetic git remote for tests.
logbus
Package logbus fans one workload's output out to any number of live subscribers, with a bounded replay backlog for late joiners.
Package logbus fans one workload's output out to any number of live subscribers, with a bounded replay backlog for late joiners.
ptyshell
Package ptyshell runs a command attached to a pseudo-terminal.
Package ptyshell runs a command attached to a pseudo-terminal.
Package kubernetes implements the executor.Executor interface by running each workload as an ephemeral Pod in a Kubernetes cluster.
Package kubernetes implements the executor.Executor interface by running each workload as an ephemeral Pod in a Kubernetes cluster.
Package localprocess implements the executor.Executor interface by forking child processes on the control-plane host.
Package localprocess implements the executor.Executor interface by forking child processes on the control-plane host.
Package projectseed carries a cloop project into a sandbox that had to fetch its own source tree.
Package projectseed carries a cloop project into a sandbox that had to fetch its own source tree.
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).
harness.go answers one question about a dispatch: once `cloop run` is going on the far side, will the binary it execs be there.
harness.go answers one question about a dispatch: once `cloop run` is going on the far side, will the binary it execs be there.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL