executor

package
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Overview

branchbundle.go describes how one git branch travels into a sandbox with a Spec (Task 20367).

A feature (pkg/feature) is a linked git worktree of a project's repository, on the hub. Its .git is a one-line pointer to the parent repository's .git/worktrees/<slug>, by absolute path, so the worktree is meaningless anywhere but on the hub: a container that mounts it sees a .git that names a directory it does not have, and a remote device never sees it at all. Mounting the parent's .git in as well would make it resolve — and would hand the sandbox the hub's own repository configuration and hooks, which run on the host at the next hub-side git operation. That is not an option.

So a feature does not travel as a worktree. It travels as a standalone checkout of its branch, built on the far side from a git bundle the hub produces:

  • on top of a git workspace (Kind git), when the parent repository has an https upstream that already holds the feature's base: the executor clones the upstream at the base exactly as it would for the project, and the bundle carries only the commits the feature has added;
  • on its own (Kind bundle), for a repository that exists only on the hub: the bundle carries the whole branch, or — when that is too large — a shallow slice of it, with the commits whose parents were left out named in Shallow so the receiver can record the boundary before it fetches.

Either way the tree ends up on the branch itself, attached, so a harness that commits commits to the feature's branch; and the work comes back as a write-back bundle onto the same branch, vetted on the hub before anything moves (see writeback.go and pkg/writeback).

The bytes never travel inside the Spec. A Spec is persisted, audited and re-read after a restart, and tens of megabytes of pack in any of those is an outage; the bundle rides beside it — Spec.BranchBundleFile on the hub, a stream of chunk frames on the wire — and BranchBundle is only the metadata that lets the receiver refuse anything other than the bytes the hub meant.

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 (
	// DefaultBranchBundleBytes is the cap a hub applies to a shipped branch
	// when its configuration names none.
	DefaultBranchBundleBytes int64 = DefaultWriteBackBundleBytes
	// MaxBranchBundleBytes is the hard ceiling: no configuration may raise a
	// shipped branch past it, and a receiver refuses anything larger whatever
	// the metadata says.
	MaxBranchBundleBytes int64 = MaxWriteBackBundleBytes
	// MaxBranchBundleShallow bounds the shallow boundary. A shallow slice of a
	// branch has one boundary commit per parent left out, which is one for a
	// linear history and a handful for a merge-heavy one; this is far above
	// any real slice and small enough that a receiver writing them to
	// .git/shallow is not an unbounded write.
	MaxBranchBundleShallow = 256
)

Bounds on a shipped branch. Both directions of a feature run move bundles, and the two ceilings are deliberately the same numbers as a write-back's: a project that may return 32 MiB of work should be able to receive as much.

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 (
	// DiskLimitFromSpec: the project's own request, .cloop/sandbox.yaml
	// resources.disk, within every ceiling.
	DiskLimitFromSpec = "spec"
	// DiskLimitFromCeiling: an operator's ceiling — the fleet's, the
	// executor's or the project's — lowered or filled in the request.
	DiskLimitFromCeiling = "ceiling"
)

Disk limit sources: where the limit a workload was held to came from, so the message that stops it can say whom to ask for more.

View Source
const (
	ChannelEdge   = "edge"
	ChannelStable = "stable"
)

ChannelEdge and ChannelStable are the channel names a device reports in its hello (provenance.ChannelEdge/ChannelStable, which this package does not import).

View Source
const (
	EgressRemedyPods = "Set executors.egress.advertise_addr to the address Pods reach the hub's proxy at — its " +
		"Service's cluster IP and port; a name is refused, since a Pod's NetworkPolicy opens addresses."
	EgressRemedyDevices = "Set executors.egress.advertise_addr to an address the device reaches the hub at, " +
		"with the proxy's port."
)

The remedies for an AdvertisedEgressRoute refusal, by who it refuses.

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 (
	MaxFirewallCIDRs     = 256
	MaxFirewallPorts     = 64
	MaxFirewallResolvers = 16
)

Firewall list bounds. They mirror pkg/netfilter's, which are the real limit — the rendered ruleset goes to nft(8) — and are restated here so a form is refused naming the field at the moment it is submitted, rather than a dispatch failing on a compile error minutes later on a device.

View Source
const (
	// SandboxGitCADir is where a sandbox with a filesystem of its own — a
	// container, a Pod, a container on an edge device — finds the CA,
	// mounted read-only.
	SandboxGitCADir = "/etc/cloop/hub-ca"
	// SandboxGitCAFile is the bundle inside SandboxGitCADir.
	SandboxGitCAFile = SandboxGitCADir + "/ca.crt"
	// SandboxGitCAFileName is SandboxGitCAFile's base name.
	SandboxGitCAFileName = "ca.crt"

	// MaxGitCAPEMBytes bounds the bundle. A CA certificate is a kilobyte;
	// anything near this is not one.
	MaxGitCAPEMBytes = 64 << 10
)
View Source
const (
	SelfUpgradeCommand   = "cloop upgrade"
	HelperUpgradeCommand = "cloop executor agent install --upgrade --remote-upgrade"
)

SelfUpgradeCommand and HelperUpgradeCommand are the on-device steps that move an agent from before the remote-upgrade helper onto the latest release, with its signature verified (Task 20421): every published release has `cloop upgrade`, which replaces the binary in place, and the new binary's `install --upgrade` — finding itself already installed — restarts the service on it and installs the helper.

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 (
	// DefaultPinnedWriteBackBytes is what one executor may have a hub hold.
	// Twice MaxWriteBackBundleBytes, so the largest bundle any spec may ask
	// for always fits with room for a second, and eight default-sized ones.
	DefaultPinnedWriteBackBytes int64 = 2 * MaxWriteBackBundleBytes
	// DefaultPinnedWriteBackTotalBytes is what every executor together may
	// have one hub process hold. It is the number that keeps a hub serving
	// its other tenants when several devices misbehave at once.
	DefaultPinnedWriteBackTotalBytes int64 = 1 << 30
	// MinPinnedWriteBackBytes and MaxPinnedWriteBackBytes bound both
	// settings. The floor catches a value written in the wrong unit — 256
	// meant as megabytes is 256 bytes, which no bundle fits — and the
	// ceiling one with a digit too many.
	MinPinnedWriteBackBytes int64 = 1 << 20
	MaxPinnedWriteBackBytes int64 = 64 << 30
)

How much returned work a hub process holds in memory while it waits to be collected (Task 20399): the write-back bundles devices are sending or have sent, and seeded runs' project-state documents. The per-handle bounds above are not a bound on the process — a hub tracks every running handle and hundreds of finished ones per device, and devices are many — so these two are. pkg/executor/remote enforces them; pkg/config validates the keys that set them (executors.remote.max_pinned_writeback_bytes and max_pinned_writeback_total_bytes).

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 DefaultMaxFailoverAttempts = 2

DefaultMaxFailoverAttempts is how many times a session is re-dispatched after the executor running it went unreachable, when nothing configures it (executors.failover.max_attempts). Two replacements: enough to ride out one flaky node and one unlucky placement, and few enough that a workload that takes down every node it lands on costs a fleet three nodes, not all of them.

View Source
const EdgeOptIn = "sudo cloop executor agent install --upgrade --channel edge"

EdgeOptIn is the command an operator runs on a device to put it on the edge channel. Mirrors agent.EdgeOptInCommand.

View Source
const EgressGatewayHost = "host.containers.internal"

EgressGatewayHost is the name a container sandbox's proxy URL uses for the hub. The container driver pins it to the gateway of whichever bridge the sandbox joins; podman already resolves it to the host in every container.

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 KindVirtual = "virtual"

KindVirtual is the kind of a virtual executor.

Its own kind rather than its parent's, because the actions an admin can take differ: a virtual executor has no binary to upgrade, no credential to revoke and no sandbox mode separate from its definition, and a panel that offered those because the kind said "remote" would offer actions that do nothing.

View Source
const LabelDiskLimitSource = "disk_limit_source"

LabelDiskLimitSource is the Spec label set when an operator's ceiling, rather than the project's own request, decided the disk limit a spec carries. A driver that cannot read the ceilings itself — a remote agent's — still names the right remedy when it stops or refuses the workload.

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 MaxFailoverAttemptsCeiling = 10

MaxFailoverAttemptsCeiling is the most executors.failover.max_attempts may be. Each attempt is one more node a node-killing workload can take down.

View Source
const MaxFilterDrivers = 256

MaxFilterDrivers bounds how many filter drivers HardenedGitConfig will neutralise. A repository configures a handful at most (git-lfs, a crypt filter); one configuring thousands is not one git should be run in.

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 MaxProjectResultErrBytes = 4 << 10

MaxProjectResultErrBytes bounds ProjectResult.Err as an executor carries it: the reason a run came back without a document. It is the same on every transport — a device's project_result frame, a Pod's log frame — so that no route home lets a workload say more than another would.

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 MaxUSBDevices = 128

MaxUSBDevices bounds an inventory. A host with more attached devices than this is a hub farm, not an executor, and an agent reporting one has a bug.

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 MetaRunID = "run_id"

MetaRunID is the HandleRecord.Meta key carrying the run id the hub minted for the dispatch (the LabelRunID Spec label), so the workload a hub process started can be found by it if that process died before recording the run (Task 20419). Not a secret: it is on the run's audit rows too.

View Source
const MinRemoteUpgradeVersion = 11

MinRemoteUpgradeVersion is the first protocol version whose agents accept the upgrade frame: remote.MinUpgradeVersion, mirrored here because this package sits below pkg/executor/remote and writes the advice that depends on it. TestMinRemoteUpgradeVersionMatchesTheWire in pkg/executor/remote keeps the two equal.

View Source
const PacketFilterGrantProcedure = "cloop executor agent install --upgrade --packet-filter"

PacketFilterGrantProcedure is the command that lets an installed device install sandbox firewalls (Task 20352): it writes the drop-in granting the agent CAP_NET_ADMIN and netlink sockets and restarts the agent, with no enrollment bundle and whether or not the binary is current. Here for the same reason as AgentUpgradeProcedure: the device's own probe, the hub's refusal and the CLI all name it, and cmd tests that it parses.

View Source
const RollbackOptIn = "sudo cloop executor agent install --upgrade --to <target> --force"

RollbackOptIn is what an operator runs on a device to move it back on main deliberately, the one way that remains.

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")
	// ErrFailoverExhausted: a session's executor went unreachable after it
	// had already been re-dispatched as often as executors.failover.
	// max_attempts allows (Task 20391). The claim that decided it closed the
	// session; nothing re-dispatches it.
	ErrFailoverExhausted = errors.New("executor: failover attempts exhausted")
	// 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 ErrCredentialRefused = errors.New("executor: workspace credential will not be renewed")

ErrCredentialRefused: a workspace credential with an expiry the hub knows could not be renewed, and will not be on a later attempt either — its grant was revoked or expired, the lease behind it released, or the forge refused (Task 20375). Whatever presents the credential stops doing so.

View Source
var ErrDiskLimit = errors.New("workspace over its disk limit")

ErrDiskLimit marks a workload refused because its workspace was already over its disk limit when it was asked to start.

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 ErrUSBDeviceNotFound = fmt.Errorf("executor: selected USB device is not attached")

ErrUSBDeviceNotFound reports that no attached device matches a selection.

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.

View Source
var HubBuild = version.String

HubBuild reports the control plane's own build: version.String(), unless a test pins it.

The remedy for a trailing device depends on it — a hub that is a release can hand its devices that release, one that is not cannot — and pkg/ui compares devices against the same value, so pinning it in a test moves both.

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 AgentUpgradeAdvice added in v0.0.5

func AgentUpgradeAdvice() string

AgentUpgradeAdvice is the remedy for a message that knows neither the device's protocol nor the one it needs: a static sentence in a remediation field, or a skew note about builds rather than protocols.

func AgentUpgradePath added in v0.0.5

func AgentUpgradePath(have, need int) string

AgentUpgradePath is the remedy for a device whose agent speaks protocol have when the hub needs need, chosen by the hub's own build.

A hub that is a release names its own release, installed by the Executors panel's Upgrade button — or by hand, once, when the device predates remote upgrade (v11). A hub that is not says so, says that no published release may carry what it needs, and gives the path that works: build cloop from the hub's source and install it on the device with --insecure-skip-verify. Where the newest published release does satisfy need, and the device can be upgraded remotely, the Upgrade button is offered as well.

Either number may be zero for "not known"; the sentence then leaves out what depends on it.

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 BuildFloorPath added in v0.0.5

func BuildFloorPath(floor string) string

BuildFloorPath is the remedy for a device refused by the fleet's minimum agent build (executors.min_agent_build).

A floor is a *release* version, so unlike a protocol shortfall it can only be met by a published release: a build made from source carries no version to order against it. A hub whose own release meets the floor names that; any other hub names the floor.

func CeilingUnenforceable added in v0.0.2

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

CeilingUnenforceable reports that a ceiling in force for this workload binds something ex will not hold it to.

Two ways, and they differ in what triggers them. A driver that enforces no limits at all — the host-process driver, a remote agent running payloads on its host — carries whatever a ceiling lowered on the spec and applies none of it; that is reported when a ceiling actually lowered something (clamps). A disk ceiling is reported whenever one is in force on a driver that does not bound disk (Task 20405), clamp or no clamp: the ceiling exists for the workload that asked for nothing, which is the one no clamp is ever recorded for — and before this an executor capped at 20 GB was capped at nothing and told nothing.

It changes nothing about the run. Having set a cap, an operator will assume it holds everywhere; this is what lets the caller say where it does not.

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 ClaimHandle added in v0.0.5

func ClaimHandle(store HandleStore, rec HandleRecord) bool

ClaimHandle claims rec on store before a driver adopts it, and reports whether the driver may: true for a store no other process shares, and for one whose claim could not be written — adopting without it is what every adoption did before, and refusing would leave a running workload unwatched. False only when another process claimed the row first.

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 DiskCeilingUnenforceable added in v0.0.5

func DiskCeilingUnenforceable(caps Capabilities, ceiling ResourceCeiling) bool

DiskCeilingUnenforceable reports a disk ceiling in force on a driver that does not bound disk.

func DiskEnforcementRemedy added in v0.0.5

func DiskEnforcementRemedy(ex Executor) string

DiskEnforcementRemedy asks the driver why it cannot hold a workload to a disk limit, falling back to the generic advice: where to run instead.

func DiskLimitAdvice added in v0.0.5

func DiskLimitAdvice(source string) string

DiskLimitAdvice names how to get a workload back under a disk limit that came from source, or how to raise it.

func DiskLimitSourceOf added in v0.0.5

func DiskLimitSourceOf(spec Spec, resolvedMB int, ceiling ResourceCeiling) string

DiskLimitSourceOf reads where spec's disk limit came from, given the ceiling the reader can see (zero when it can see none).

func DiskLimitSourcePhrase added in v0.0.5

func DiskLimitSourcePhrase(source string) string

DiskLimitSourcePhrase names where a disk limit from source came from, in the parenthesis a message gives it.

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 EdgeChannelRefusal added in v0.0.5

func EdgeChannelRefusal(subject, target string) string

EdgeChannelRefusal is the refusal for an edge target on a device that follows the stable channel.

func EdgeProtocolUnknown added in v0.0.5

func EdgeProtocolUnknown(subject, target, why string) string

EdgeProtocolUnknown is the refusal for an edge target whose protocol the hub could not read, so could not judge against the device's.

func EdgeUpgradeOffer added in v0.0.5

func EdgeUpgradeOffer(subject string, dev DeviceBuild, hubProtocol int, edge *EdgeBuild) (target, label, note string)

EdgeUpgradeOffer is UpgradeOffer for a device on the edge channel: this hub's own build when CI has published it and it would not lower the device's protocol or move it back on main; otherwise what UpgradeOffer would offer, with the reason the hub's build is not offered in front of it. edge is nil when the hub has no edge build to speak of — it runs a release, which UpgradeOffer already offers.

Builds are ordered by sequence (Task 20380): a device whose build is later on main than the hub's is never offered the hub's, because its installer would refuse it — nor a release the hub knows to be earlier.

label names the target for the dialog ("this hub's build (a0f3870)"), empty for a release, which names itself.

func EgressAdvertised added in v0.0.5

func EgressAdvertised(advertise string, boundPort int) (string, error)

EgressAdvertised resolves executors.egress.advertise_addr against the port the proxy is bound to: a bare host, or a port of 0, takes the bound port, which is what lets an operator bind an ephemeral port and still advertise a name. "" when advertise is unset.

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 FormatUsedMB added in v0.0.5

func FormatUsedMB(mb int64) string

FormatUsedMB renders a measured size in MiB, never rounded to whole gigabytes; see formatUsedMB.

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 GitCredentialConfig added in v0.0.5

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

GitCredentialConfig is the configuration GitCredentialEnv delivers, as pairs, for a caller that has more configuration to put in the same block — see GitEnv for why there can only be one. Nil for an empty credential.

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 GitEnv added in v0.0.5

func GitEnv(extra ...[2]string) []string

GitEnv is GitBaseEnv with extra configuration folded into the same GIT_CONFIG_COUNT block.

One block, not two appended: the count is a single variable for the whole environment, so a second block would silently drop every key of the first that it did not repeat — including the redirect guard in baseGitConfig.

func GuardReleaseOffer added in v0.0.5

func GuardReleaseOffer(subject string, have int, target, note string) (string, string)

GuardReleaseOffer withdraws a release offer the device would refuse as earlier on main, putting the reason in its place. A target that is empty, not a release, or not refused is returned unchanged.

func HardenedGitConfig added in v0.0.5

func HardenedGitConfig(filterDrivers []string) [][2]string

HardenedGitConfig is the configuration that stops a repository's own settings from making git run a program, for a git command run *outside* the sandbox that wrote the repository (Task 20367).

A sandbox that can write a repository can write its .git: hooks, and config keys whose values are commands. Every git command run in that repository afterwards by anything with more authority than the sandbox — the device's agent committing a write-back, the hub fast-forwarding a feature — would execute them with that authority. Configuration passed through the environment outranks the repository's own file, so these pairs close each path for the commands cloop runs there:

  • core.hooksPath=/dev/null: no hook runs, including the ones --no-verify does not cover (post-commit, post-checkout, reference-transaction);
  • core.fsmonitor=false: `git status` and `git add` would otherwise run the configured monitor program;
  • filter.<driver>.{clean,smudge,process} emptied for every driver the repository configures, which git treats as no filter at all; clean filters run on `git add`, smudge filters on checkout;
  • signing off, so a configured gpg.program is never invoked;
  • automatic gc and maintenance off, so nothing is spawned in the background of a command that is meant to finish;
  • submodules ignored, so `git status` does not descend into a nested repository and run git there under that repository's configuration.

filterDrivers are the driver names the repository configures; see gitprovision.SandboxedRepoEnv, which reads them.

func HostExecutionAllowed

func HostExecutionAllowed() bool

HostExecutionAllowed reports whether un-isolated host execution is permitted.

func HubLocalHost added in v0.0.5

func HubLocalHost(host string) string

HubLocalHost says why a host only means something on the hub's own machine or network, or "" when it may be reachable from elsewhere.

A device cannot be asked whether it can reach an address before the run starts, so this is a judgement — and it errs on the side of saying so: an address it lets through that the device cannot reach fails the run's first request, which the journal row at dispatch names.

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 MarkDiskLimitFromCeiling added in v0.0.5

func MarkDiskLimitFromCeiling(spec *Spec)

MarkDiskLimitFromCeiling labels spec's disk limit as the ceiling's. The labels map is copied rather than written through, because the caller's may be shared with a Spec that is persisted or reused.

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 NeedsProtocol added in v0.0.5

func NeedsProtocol(subject string, have, need int, purpose, instead string) string

NeedsProtocol composes a refusal for a device whose agent speaks too old a protocol:

"<subject> speaks protocol v<have>, and the hub needs v<need> <purpose>."

followed by AgentUpgradePath(have, need) and, when instead is not empty, the alternative remedy it states ("Or remove the grant from this project."). purpose completes "the hub needs vN ___", e.g. "for firewall rules stored in the hub" or "to revoke <bindings> mid-run".

A have of zero or less means the device's protocol is not known — an offline device, or a driver that does not report one — and the sentence says only that it is older than what is needed, which is what the caller established.

func NormalizeGitCAURL added in v0.0.5

func NormalizeGitCAURL(raw string) (string, error)

NormalizeGitCAURL checks one URL and renders it the way git matches http.<url>.* keys: scheme, host, port, and a path ending in "/" so that https://hub:8443/ matches every repository beneath it and nothing at https://hub:8443.evil.example.

func NormalizeUSBID added in v0.0.5

func NormalizeUSBID(s string) (string, error)

NormalizeUSBID lowercases and validates a vendor or product ID. "0x1050" and "1050" are both accepted, because both are how people write them.

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 ParseFirewallPrefix added in v0.0.5

func ParseFirewallPrefix(s string) (netip.Prefix, error)

ParseFirewallPrefix accepts a CIDR or a bare address and returns it masked.

A bare address is read as a single host because "block 203.0.113.9" is a thing an admin means, and forcing them to write /32 invites the typo /3 — eight million times wider, and nearly identical on screen. Masking matters for more than tidiness: netip.Prefix.Contains is false for every address when bits are set below the length, so an unmasked 10.8.0.5/24 would be a rule that silently matches nothing.

func ParseGroupID added in v0.0.5

func ParseGroupID(s string) (int, bool, error)

ParseGroupID reads a numeric group ID, refusing root.

Root's group is refused outright rather than accepted with a warning: on a rootful engine GID 0 inside the sandbox is GID 0 on the host for every file the sandbox can see through a bind mount, and the device-access problem it would "solve" has a narrow fix — a udev rule giving the node a group of its own.

func ParseResolver added in v0.0.5

func ParseResolver(s string) (netip.AddrPort, error)

ParseResolver accepts "addr" or "addr:port" ("[v6]:port" for IPv6), with the port defaulting to 53.

Address literals only, never names: a packet filter matches addresses, and resolving a name here would pin it silently at configuration time — a DNS rebinding hazard dressed up as a convenience.

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 ProtocolDrop added in v0.0.5

func ProtocolDrop(subject string, have int, target string, targetProtocol int) string

ProtocolDrop is the refusal for an upgrade target that would lower the device's protocol: the device speaks have, target speaks targetProtocol (or at most that, when the release table can only bound it), and installing it would take away whatever needs the protocols in between. The path that moves the device forward instead follows.

func ProtocolMismatchAdvice added in v0.0.5

func ProtocolMismatchAdvice() string

ProtocolMismatchAdvice is the remedy for a device and a hub that share no protocol version at all. It is read on both sides — a device prints it when the hub refuses its hello — so unlike AgentUpgradePath it names no build: on the device, HubBuild is the device's own.

func ProtocolShortfall added in v0.0.5

func ProtocolShortfall(subject string, have, need int, purpose string) string

ProtocolShortfall is NeedsProtocol's first sentence alone, for a caller that carries the remedy in a field of its own (a preflight finding's Fix).

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 RedactAttachOutputFollowing added in v0.0.5

func RedactAttachOutputFollowing(r io.Reader, current func() *redact.Set) io.Reader

RedactAttachOutputFollowing is RedactAttachOutput over the set current returns as the session goes on — a handle's log bus's, which a credential refreshed while the workload runs joins (Task 20375). A terminal open across the refresh scrubs the new token as the log stream does. It pipes even when the set is empty now: a rehydrated handle's bus starts empty and gains the first token it is sent.

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 ReleaseRollbackRefusal added in v0.0.5

func ReleaseRollbackRefusal(subject string, have int, tag string) string

ReleaseRollbackRefusal is RollbackRefusal for a release target, judged from what the hub knows of the release's sequence; "" when it would not be refused or the hub cannot tell.

func ReleaseSequence added in v0.0.5

func ReleaseSequence(tag string) (seq int, known bool)

ReleaseSequence returns the sequence the release tag's binaries carry and whether the hub knows it: from the published release table, or — for this hub's own release — from the hub's own stamp. Unknown for a release newer than anything this hub knows of, and for anything that is not a release.

func ReplaceSecretFile added in v0.0.5

func ReplaceSecretFile(path string, content []byte, opts ReplaceOptions) error

ReplaceSecretFile atomically replaces the credential file at path, which must already exist as a regular file inside a lease-owned directory.

Everything is done relative to the directory, opened once without following a symlink in its place: the new content is written to a fresh file created exclusively in it, given its mode and owner through its own descriptor, and renamed over the old name in it. Nothing is ever done by path to a name someone else could have swapped — which matters because a container's lease directory belongs to the sandbox user, and a hub running as root that chowned "the temp file" by path could be steered into chowning /etc/shadow. rename(2) replaces a symlink planted at the old name rather than writing through it, and a reader sees the old content or the new, never a mixture. A failure zeroes and removes the new file.

The old content is not overwritten: a process may be reading it at this moment, and the credential it holds is destroyed at its source once the workload has had time to move to the new one, which is what makes the stale bytes worthless.

func RequestingExecutor added in v0.0.5

func RequestingExecutor(ctx context.Context, fallback string) string

RequestingExecutor returns the identity WithRequestingExecutor put on ctx, or fallback when there is none.

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 RollbackRefusal added in v0.0.5

func RollbackRefusal(subject string, have int, target string, targetSeq int) string

RollbackRefusal is the sentence for a target the device would refuse because it is earlier on main than the device's build: targetSeq below have, or zero — a build that carries no sequence — while have is not. It returns "" when the target would not be refused, or when have is unknown (zero).

func RollbackUnprotected added in v0.0.5

func RollbackUnprotected(subject, build, channel string, remoteUpgrade bool) (note, remedy string)

RollbackUnprotected is the warning for a device whose build carries no sequence (Task 20421), and the way to cover it: its installer has nothing to order an upgrade by, so rollback protection does not hold for it until it runs a build that carries one — every published release before v0.0.5, and any build stamped by hand. The doctor's executors.rollback_protection and executors.edge_lag findings and the Executors panel say the same thing through it.

build is what the device reports ("" when it reported nothing), channel the channel it follows ("" for an agent too old to say, which follows releases), and remoteUpgrade whether its agent said it can carry out an upgrade the hub asks for — which no build from before the remote-upgrade helper can.

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 SanitizeDiskOutcome added in v0.0.5

func SanitizeDiskOutcome(o Outcome, b *DiskLimitBreach) (Outcome, *DiskLimitBreach)

SanitizeDiskOutcome bounds a status's disk-limit account where the hub did not produce it — a remote agent's status frame. An outcome the hub does not know, a limit it could never have sent, or a breach that does not describe a tree over its limit is dropped, and the run is then judged by its State and exit code like any other. What is kept has a known source, a measurement of bounded size, and a path of bounded length with no control or bidirectional formatting characters to rewrite the line it is printed on.

func SanitizeUSBString added in v0.0.5

func SanitizeUSBString(s string) string

SanitizeUSBString bounds a descriptor string and removes anything that is not a printable character. See the file comment for why this is not optional.

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 StableChannelHint added in v0.0.5

func StableChannelHint() string

StableChannelHint is appended to the Upgrade dialog's note for a device on the stable channel when the hub runs an unreleased build of a commit: the way to let the dialog offer the hub's own build is on the device.

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 USBNodePath added in v0.0.5

func USBNodePath(bus, dev int) string

USBNodePath is where usbfs puts a device's node.

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 UnpublishedTarget added in v0.0.5

func UnpublishedTarget(subject, target string, have int) string

UnpublishedTarget is the refusal for an upgrade target that is not a release tag at all — most often the hub's own unreleased version, which the Upgrade dialog used to prefill. A device resolves the target through its own release channel and verifies the archive's signature, so a name with no published release behind it cannot be installed that way; the path that does put the hub's code on the device follows.

func UpgradeOffer added in v0.0.5

func UpgradeOffer(subject string, have, hubProtocol int) (target, note string)

UpgradeOffer is what the Executors panel's Upgrade dialog offers a device whose agent speaks protocol have, on a hub speaking hubProtocol: the release to prefill, and the sentence explaining it.

The target is the hub's own release when the hub is one; otherwise the newest published release this hub knows of, provided it does not lower the device's protocol; otherwise "" — there is no release the button could install that would not move the device backwards, and the note says what to do instead. A have of zero (an offline device) judges nothing about the device.

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 ValidateUSBInventory added in v0.0.5

func ValidateUSBInventory(devs []USBDevice) error

ValidateUSBInventory checks a whole inventory as received.

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.

func WithGitConfig added in v0.0.5

func WithGitConfig(env []string, pairs [][2]string) ([]string, error)

WithGitConfig adds pairs to the GIT_CONFIG_COUNT block in env (K=V strings), after any entries already there, and returns the new list.

The count is one variable for the whole environment, so appending a second block would silently replace the first — a lease's or the review gate's — rather than join it. A count that does not parse is refused rather than overwritten: whoever set it meant something by the entries it numbers.

func WithRequestingExecutor added in v0.0.5

func WithRequestingExecutor(ctx context.Context, id string) context.Context

WithRequestingExecutor marks ctx as a dispatch made for executor id when that is not the executor whose driver runs it: a virtual executor, whose work is carried out by its parent device.

A credential source keys the lease on the executor it was built for, which for a device's virtual executors is the device. The grant chosen before the dispatch, though, was matched against the virtual executor — so a grant issued to the virtual executor was chosen and then missing from the lease, and one issued to the device was leased but never chosen (Task 20349). A source that honours this makes both steps ask about the same identity.

func WithRunID added in v0.0.5

func WithRunID(meta map[string]string, spec Spec) map[string]string

WithRunID records spec's run id in meta, which it returns.

Types

type Abandoner added in v0.0.5

type Abandoner interface {
	Abandon(ctx context.Context, handleID, reason string) error
}

Abandoner is implemented by a driver that can give up a workload the control plane failed over elsewhere (Task 20396).

When the supervisor declares a node lost it claims the sessions stranded on it, and from that moment their workloads are no longer the runs of their projects: the tasks they held went back to pending, and a replacement may already be running them somewhere else. The stranded workload itself is out of reach — that is why it was failed over — but it is not necessarily dead. A device that was only cut off comes back with it still running, and a driver that resumes it puts a second harness on the same tasks.

Abandon is how the hub says so to the driver: stop the workload now if it can still be reached; end its output stream and report it terminal, so that everything on the hub waiting on it lets go — the run's own watchers, and with them the credential lease it held; and refuse it when its executor comes back offering to resume it. reason is operator-facing and ends up in the workload's last status.

A driver without it is asked to kill the workload instead, which is all the hub can do for one it cannot reach.

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 BootstrapSource added in v0.0.5

type BootstrapSource struct {
	// Edge reports that the command installs this hub's own build from the
	// edge channel: `install.sh --channel edge`.
	Edge bool
	// Commit is the full commit of the hub's published edge build: what
	// `--channel edge` installs, whether or not the command uses it. Empty when
	// there is none, and EdgeWhyNot then says why.
	Commit string
	// EdgeWhyNot says why `--channel edge` has nothing to install, for the
	// script's refusal of it. Empty when Commit is set.
	EdgeWhyNot string
	// Note is the sentence shown beside the command; empty when there is
	// nothing to add to it.
	Note string
	// Stopped reports that, as far as the hub knows, no signed build speaks
	// its protocol: the command's installer will stop at the protocol check on
	// a device without a newer cloop, and Note says what to do instead.
	Stopped bool
}

BootstrapSource is what the installer a hub hands out installs.

func ChooseBootstrapSource added in v0.0.5

func ChooseBootstrapSource(hubProtocol int, edge *EdgeBuild) BootstrapSource

ChooseBootstrapSource decides what the installer installs for a hub that speaks hubProtocol, from the hub's own build (HubBuild) and what it learned about that build's edge build. edge is nil when the hub runs a release, or when it could not be asked.

type BranchBundle added in v0.0.5

type BranchBundle struct {
	// Branch is the branch the provisioned tree is left on, attached, so the
	// harness's commits land on it. It must live under the cloop/ namespace —
	// the same rule a write-back branch obeys, and for the same reason: the
	// receiver force-creates it.
	Branch string `json:"branch"`
	// Head is the commit Branch points at in the bundle, full 40-hex. The
	// receiver verifies the checkout lands exactly here.
	Head string `json:"head"`
	// Bytes is the bundle's size. Zero means nothing was shipped: on a git
	// workspace the feature has no commits of its own yet, and Branch is
	// created at Workspace.Ref, which must then equal Head.
	Bytes int64 `json:"bytes,omitempty"`
	// SHA256 is the hex digest of the bundle, so a truncated or substituted
	// stream is refused before git reads a byte of it.
	SHA256 string `json:"sha256,omitempty"`
	// Shallow lists the commits at the bundle's lower edge whose parents were
	// deliberately left out, for a Kind bundle workspace shipped as a shallow
	// slice. The receiver records them as shallow before fetching; without
	// that git refuses the bundle as incomplete.
	Shallow []string `json:"shallow,omitempty"`
}

BranchBundle is the metadata of a git branch shipped with a Spec.

It carries no credential and no bytes: the bundle itself travels beside the Spec, and this is what lets the receiver check that what arrived is what the control plane sent.

func (*BranchBundle) Describe added in v0.0.5

func (b *BranchBundle) Describe() string

Describe renders the bundle for a log line. It can never contain a credential — BranchBundle has no field that could hold one.

func (*BranchBundle) VerifyFile added in v0.0.5

func (b *BranchBundle) VerifyFile(path string) error

VerifyFile checks that the file at path is exactly the bundle b describes: a regular file, of the stated size, with the stated digest. A bundle that ships no bytes has nothing to verify.

Lstat, not Stat: on a machine where a workload has had write access near this path, a symbolic link standing in for the bundle would make git read a file of the workload's choosing.

type BranchBundleExplainer added in v0.0.5

type BranchBundleExplainer interface {
	// ExplainBranchBundleRefusal returns a sentence completing "this executor
	// cannot receive a branch shipped from the control plane; ...", or "" when
	// the driver has nothing more specific than the generic remedy.
	ExplainBranchBundleRefusal() string
}

BranchBundleExplainer is implemented by a driver that can say why it cannot receive a branch shipped from the hub right now: a remote agent knows the protocol its device speaks and whether the device has git, and those have different remedies. Like EgressScopeExplainer, a method rather than a field, so the answer comes from the driver that knows it.

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"`
	// DiskEnforcement says how ResourceLimits.DiskMB is held, and is empty
	// where it is not (Task 20405). It is separate from SupportsResourceLimits
	// because a driver can enforce the other three and not this one: a remote
	// agent older than protocol v20 runs a container that bounds CPU and
	// memory and refuses a disk limit outright.
	DiskEnforcement DiskEnforcement `json:"disk_enforcement,omitempty"`
	// 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"`
	// SupportsBranchBundle reports whether this driver can receive a git
	// branch shipped beside the Spec (Workspace.Branch and
	// Spec.BranchBundleFile) and build the workload's tree from it, leaving
	// the checkout on that branch (Task 20367).
	//
	// It is what lets a feature — a branch of a repository on the hub — run
	// in a sandbox at all, and it is separate from the provisioning and
	// write-back flags because a driver can have those and still have no way
	// to carry the bytes in: the Kubernetes driver clones and pushes, but a Pod
	// has no channel from the hub to receive a bundle through. A driver that
	// ignored the field would start the harness on the base commit, without
	// any of the feature's work, which is the run-on-the-wrong-code failure
	// this whole contract exists to refuse.
	SupportsBranchBundle bool `json:"supports_branch_bundle"`
	// 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"
	ConstraintDiskLimit        Constraint = "disk_limit"
	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 DeviceBuild added in v0.0.5

type DeviceBuild struct {
	Protocol int
	Sequence int
}

DeviceBuild is what an offer needs to know about a device's build: the protocol its agent speaks and its place on main, each zero when unknown.

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 DeviceSelector added in v0.0.5

type DeviceSelector struct {
	// Name identifies the device in labels, audit rows and error messages.
	Name string `json:"name"`
	// USB selects a USB device by vendor, product and optionally serial or
	// port.
	USB *USBMatch `json:"usb,omitempty"`
	// Path is a device node on the executor's host.
	Path string `json:"path,omitempty"`
	// Permissions is the access granted; empty means read-write. See
	// DevicePermissions for what the container runtime can and cannot enforce.
	Permissions DevicePermissions `json:"permissions,omitempty"`
	// Group is a supplementary group the sandbox user is given so the node's
	// own permissions let it open the device — the group a udev rule assigns
	// to it, by name or number. Empty lets the device infer it from the node
	// when it can see it. Never root's.
	Group string `json:"group,omitempty"`
	// SkipInterfaceNodes passes a USB device's usbfs node alone, without the
	// tty or hidraw nodes its interfaces created. The default passes both,
	// because which of them a tool opens is the tool's business.
	SkipInterfaceNodes bool `json:"skip_interface_nodes,omitempty"`
}

DeviceSelector names one host device a virtual executor passes through.

Exactly one of USB and Path is set. USB selects by identity and is resolved on the device at dispatch (see usb.go for why); Path names a fixed node for hardware that is not USB — an on-board serial port, a GPIO chip.

func (DeviceSelector) Describe added in v0.0.5

func (d DeviceSelector) Describe() string

Describe renders a selector for a card or an audit row.

func (DeviceSelector) Normalize added in v0.0.5

func (d DeviceSelector) Normalize() (DeviceSelector, error)

Normalize canonicalises and validates one selector.

type DiskEnforcement added in v0.0.5

type DiskEnforcement string

DiskEnforcement says how a driver holds a workload to ResourceLimits.DiskMB. The empty value means it does not, and the limit is not applied — placement refuses a spec that states one (RequireDiskLimit), and a disk ceiling on such an executor is reported as unenforced.

const (
	// DiskEnforcementNone: the driver does not bound disk use.
	DiskEnforcementNone DiskEnforcement = ""
	// DiskEnforcementSampled: the driver measures the workspace while the
	// workload runs and stops it once it is over the limit. It is enforcement
	// by measurement, not a quota: a burst can overshoot by the write rate
	// times the sampling interval before the next sample sees it.
	DiskEnforcementSampled DiskEnforcement = "sampled"
	// DiskEnforcementEviction: the cluster holds the limit — the kubelet
	// evicts a Pod whose ephemeral storage or emptyDir outgrows it.
	DiskEnforcementEviction DiskEnforcement = "eviction"
)

func (DiskEnforcement) Describe added in v0.0.5

func (d DiskEnforcement) Describe() string

Describe renders the mode the way the executor card shows it.

func (DiskEnforcement) Enforced added in v0.0.5

func (d DiskEnforcement) Enforced() bool

Enforced reports whether the mode bounds disk use at all.

type DiskLimitBreach added in v0.0.5

type DiskLimitBreach struct {
	// UsedBytes is the workspace's allocated size when it was measured: the
	// blocks its files occupy, hard links counted once, .cloop/ excluded.
	UsedBytes int64 `json:"used_bytes"`
	// LimitMB is the limit it was held to, in MiB.
	LimitMB int `json:"limit_mb"`
	// Source is DiskLimitFromSpec or DiskLimitFromCeiling.
	Source string `json:"source,omitempty"`
	// Path is the tree that was measured, on the executor's own host.
	Path string `json:"path,omitempty"`
	// MeasuredAt is when the measurement finished.
	MeasuredAt time.Time `json:"measured_at"`
}

DiskLimitBreach is the measurement that put a workload over its disk limit.

func (DiskLimitBreach) Describe added in v0.0.5

func (b DiskLimitBreach) Describe() string

Describe is the sentence fragment the journal, the task and the pause give: "the workspace grew to 70 MB, over its disk limit of 64 MB (from .cloop/sandbox.yaml resources.disk)".

func (DiskLimitBreach) LimitBytes added in v0.0.5

func (b DiskLimitBreach) LimitBytes() int64

LimitBytes is the limit in bytes.

func (DiskLimitBreach) Over added in v0.0.5

func (b DiskLimitBreach) Over() bool

Over reports whether the measurement exceeds the limit.

func (DiskLimitBreach) Remedy added in v0.0.5

func (b DiskLimitBreach) Remedy() string

Remedy names what to do about it.

func (DiskLimitBreach) UsedMB added in v0.0.5

func (b DiskLimitBreach) UsedMB() int64

UsedMB is UsedBytes in MiB, rounded up: a tree one byte over a 64 MB limit must not read back as "64 MB, over its limit of 64 MB".

type DiskLimitError added in v0.0.5

type DiskLimitError struct {
	Breach DiskLimitBreach
	// Executor is the executor that refused.
	Executor string
}

DiskLimitError is the refusal Start returns for a tree that is already over the limit — the meaning the provisioner's post-fetch check gives a fetched tree, applied to a tree that was already there. It names both sizes and how to raise the limit, because the run never started and this message is all its author gets.

func (*DiskLimitError) Error added in v0.0.5

func (e *DiskLimitError) Error() string

Error implements error.

func (*DiskLimitError) Unwrap added in v0.0.5

func (e *DiskLimitError) Unwrap() error

Unwrap lets errors.Is(err, ErrDiskLimit) match.

type DiskLimitExplainer added in v0.0.5

type DiskLimitExplainer interface {
	// ExplainDiskLimitRefusal returns a sentence completing "this executor
	// cannot hold a workload to a disk limit; ...", or "" when the driver has
	// nothing more specific than the generic remedy.
	ExplainDiskLimitRefusal() string
}

DiskLimitExplainer is implemented by a driver that can say why it cannot hold a workload to a disk limit right now — a remote agent knows whether its device runs payloads in a container and which protocol its agent speaks, and those have different remedies. A method rather than a field, for the reason EgressScopeExplainer gives.

type EdgeBuild added in v0.0.5

type EdgeBuild struct {
	// Short is the hub's commit as its version carries it, e.g. "a0f3870".
	Short string
	// Target is "edge:<full commit>" once the commit is resolved.
	Target string
	// Published reports that CI published the build; Protocol is what its
	// manifest says it speaks, and Sequence its place on main (Task 20380) —
	// zero when the manifest names none, i.e. a build from before builds were
	// stamped with one, which a device whose build carries one refuses.
	Published bool
	Protocol  int
	Sequence  int
	// WhyNot is the sentence saying why it is not published, when it is not
	// (pkg/upgrade.EdgeBuild.Reason).
	WhyNot string
}

EdgeBuild is what the hub knows about its own build's edge build.

func (EdgeBuild) Label added in v0.0.5

func (b EdgeBuild) Label() string

Label is how the dialog names the build.

type EgressPosture added in v0.0.5

type EgressPosture struct {
	// DeviceID is the executor whose stored rule set is the superset for this
	// one: its own ID, or for a virtual executor its device's.
	DeviceID string
	// Config is the bound the executor's configuration file sets
	// (executors.*.egress_filter) — the level above the device's stored rules.
	// nil when the configuration does not filter.
	Config *FirewallRules
	// Own is the executor's level between the device's rules and the
	// project's: a virtual executor's firewall, or the network a device's own
	// sandboxes are given. nil when it adds no narrowing of its own; a zero
	// rule set when its sandboxes get no network.
	Own *FirewallRules
	// OwnName names Own for a sentence ("virtual executor vx-… 's firewall").
	OwnName string
	// Enforceable reports whether the executor can install a firewall built
	// from rules for one workload — a filtered bridge, a NetworkPolicy.
	Enforceable bool
	// RemovesNetwork reports whether it can start a workload with no network
	// at all, which is how a rule set that reaches nothing is honoured without
	// a packet filter.
	RemovesNetwork bool
	// Reason says why not Enforceable, for a refusal and for the panel.
	Reason string
}

EgressPosture is what an executor's own configuration says about the network its sandboxes get, in the terms the firewall levels compose in.

func PostureOf added in v0.0.5

func PostureOf(ex Executor) EgressPosture

PostureOf asks an executor for its posture. A driver that does not implement EgressPostured cannot install rules for a workload — that is the conservative reading, and it is the true one for the host-process driver.

type EgressPostured added in v0.0.5

type EgressPostured interface {
	EgressPosture() EgressPosture
}

EgressPostured is implemented by drivers that can state their posture.

type EgressProxyRoute added in v0.0.5

type EgressProxyRoute struct {
	// Host is the host the workload's proxy variables name: a name the driver
	// pins (Gateway), or the address or name the workload dials as it is.
	Host string `json:"host"`
	// Port is the proxy's port.
	Port int `json:"port"`
	// Gateway asks the driver to resolve Host to the gateway of the network
	// the sandbox joins, and to pin that resolution inside the sandbox.
	//
	// It is how a container on the hub's own engine reaches a proxy that
	// listens on every host interface: the host is at the gateway address of
	// whichever bridge the sandbox lands on, and on an --internal bridge that
	// address is the only one off the sandbox it can route to at all.
	Gateway bool `json:"gateway,omitempty"`
}

EgressProxyRoute is how one workload reaches the hub's egress proxy.

func AdvertisedEgressRoute added in v0.0.5

func AdvertisedEgressRoute(advertised, bound, who string) (EgressProxyRoute, string)

AdvertisedEgressRoute is advertised — EgressAdvertised's result — as the route a workload on another machine is given, or why it cannot be one. who names those workloads in the reason ("Pods", "a device"), and bound is the proxy's bound address, named when nothing is advertised.

func ContainerEgressRoute added in v0.0.5

func ContainerEgressRoute(bound *net.TCPAddr) (route EgressProxyRoute, why, remedy string)

ContainerEgressRoute is how a container sandbox on the hub's own engine reaches a proxy bound to bound — or why it cannot, the reason first, then the remedy.

func (EgressProxyRoute) Addr added in v0.0.5

func (r EgressProxyRoute) Addr() (netip.Addr, bool)

Addr returns the route's host as an address, when it is one.

func (EgressProxyRoute) String added in v0.0.5

func (r EgressProxyRoute) String() string

String renders the route as host:port, for messages.

func (EgressProxyRoute) Validate added in v0.0.5

func (r EgressProxyRoute) Validate() error

Validate checks the route's shape. It is called by Spec.Validate, so a route arriving at a driver — possibly over the wire from another machine — is checked where it is used rather than trusted from the sender.

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"`
	// Unreachable is when From was found unreachable: the transition's time.
	Unreachable time.Time `json:"unreachable_at,omitempty"`
	// To is the replacement executor, or "" when none was found or the
	// session is exhausted.
	To string `json:"to,omitempty"`
	// Exhausted reports that the claim found the session had already been
	// re-dispatched MaxAttempts times (Task 20391): nothing re-dispatches it,
	// and the store closed it as failover_exhausted.
	Exhausted bool `json:"exhausted,omitempty"`
	// MaxAttempts is the failover cap the claim was decided under.
	MaxAttempts int `json:"max_attempts"`
	// Err is set when the session is exhausted, or placement or re-dispatch
	// failed. The handler still settles the session's tasks; the work just
	// has nowhere to go.
	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 settles 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, fail one for good, and start a fresh run.

It is called for every claimed session — placed (ev.To set, ev.Err nil), unplaced (ev.To empty, ev.Err the placement failure) and exhausted (ev.Exhausted) — and it re-dispatches only the first. 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 FileIdentity added in v0.0.5

type FileIdentity struct{ Dev, Ino uint64 }

FileIdentity names one directory on one filesystem, so a replacement can insist that the directory it writes into is still the one the lease created.

func IdentityOf added in v0.0.5

func IdentityOf(path string) (FileIdentity, error)

IdentityOf returns the identity of the directory at path, without following a symlink there.

type FileOwner added in v0.0.5

type FileOwner struct{ UID, GID int }

FileOwner is who a replaced credential file belongs to.

type FirewallRules added in v0.0.5

type FirewallRules struct {
	// AllowPublicInternet opens every address outside the hard-block set.
	AllowPublicInternet bool `json:"allow_public_internet,omitempty"`
	// AllowCIDRs is the IP allowlist: ranges the sandbox may reach directly,
	// including private ones. A bare address is read as a single host.
	AllowCIDRs []string `json:"allow_cidrs,omitempty"`
	// DenyCIDRs is the IP denylist: ranges the sandbox may never reach,
	// whatever else is allowed.
	DenyCIDRs []string `json:"deny_cidrs,omitempty"`
	// AllowPorts bounds every allow above to these destination ports. Empty
	// means any port.
	AllowPorts []int `json:"allow_ports,omitempty"`
	// Resolvers are DNS servers the sandbox may query and is configured to
	// use, as address literals with an optional port (default 53).
	Resolvers []string `json:"resolvers,omitempty"`
}

FirewallRules is an editable IP-layer egress policy.

The zero value allows nothing: a sandbox under it has an interface and no destination. That is the deliberate default for a stored rule set, and the opposite of an absent one — a nil *FirewallRules means "no firewall", which is a different decision (see VirtualSpec.Firewall).

func (FirewallRules) CheckMetadata added in v0.0.5

func (r FirewallRules) CheckMetadata() error

CheckMetadata is the rule a save applies: ErrInvalidSpec naming each allowlist entry that opens a metadata service it does not name, with both explicit alternatives.

Normalize does not apply it, deliberately. Normalize is also how a stored rule set is read back, and one saved before the rule existed has to keep loading — every filter compiled from it keeps the service closed — so that its card and `cloop hub doctor` can say what is wrong with it, rather than every sandbox under it being refused as "unreadable".

func (FirewallRules) Describe added in v0.0.5

func (r FirewallRules) Describe() string

Describe renders the rules for a log line, an audit row or a card.

func (FirewallRules) HasDestination added in v0.0.5

func (r FirewallRules) HasDestination() bool

HasDestination reports whether the rules make anything reachable.

func (FirewallRules) MetadataFindings added in v0.0.5

func (r FirewallRules) MetadataFindings() []cloudmeta.Finding

MetadataFindings reports every allowlist entry that contains a cloud metadata service without naming it — and without the denylist closing it (Task 20397). An entry that does not parse is Normalize's to refuse, and is skipped here.

func (FirewallRules) MetadataNotes added in v0.0.5

func (r FirewallRules) MetadataNotes() []string

MetadataNotes says, for a rule set that is already stored, what MetadataFindings found and what becomes of it: one sentence per entry, for the rule set's card and for `cloop hub doctor`.

func (FirewallRules) Normalize added in v0.0.5

func (r FirewallRules) Normalize() (FirewallRules, error)

Normalize returns the canonical form: trimmed, parsed, masked, deduplicated and sorted. Every comparison and every rendered rule downstream works on this form, so "10.1.2.3/8" and "10.0.0.0/8" are the same range everywhere.

Errors name the field and the zero-based index, because that is what a form can attach to an input.

func (FirewallRules) Validate added in v0.0.5

func (r FirewallRules) Validate() error

Validate reports whether the rules are admissible.

type ForeignHandleLister added in v0.0.5

type ForeignHandleLister interface {
	ListForeignHandles(executorID string) ([]HandleRecord, error)
}

ForeignHandleLister is implemented by a HandleStore that hides the rows another live control-plane member tracks (Task 20354).

A hub member must not *adopt* a workload a live peer is streaming — two members watching one container would both settle it — so its drivers rehydrate from a store that leaves those rows out. But an orphan sweep asks the opposite question, "is anyone tracking this?", and answering it from the same filtered view would reap a peer's live workload as an orphan. This is how a sweep sees the rows it may not adopt.

type GitCA added in v0.0.5

type GitCA struct {
	// URLs are https base URLs, rendered the way git matches http.<url>.*:
	// scheme, host, port and a path ending in "/".
	URLs []string `json:"urls"`
	// PEM holds one or more CA certificates. Public.
	PEM []byte `json:"pem"`
}

GitCA is a certificate authority a workload's git trusts for particular https URLs.

func (*GitCA) Clone added in v0.0.5

func (c *GitCA) Clone() *GitCA

Clone returns a deep copy, so a driver can rewrite one Spec's without touching another's.

func (*GitCA) ConfigPairs added in v0.0.5

func (c *GitCA) ConfigPairs(path string) [][2]string

ConfigPairs is the git configuration that trusts the bundle at path for the URLs and nothing else: one http.<url>.sslCAInfo per URL.

func (*GitCA) Covers added in v0.0.5

func (c *GitCA) Covers(rawURL string) bool

Covers reports whether a request to rawURL is one git would verify against the bundle: the same scheme, host and port as one of the URLs, under its path at a "/" boundary.

func (*GitCA) Validate added in v0.0.5

func (c *GitCA) Validate() error

Validate checks the bundle as a driver is about to write it: every URL an https base, and the PEM nothing but parseable CA certificates.

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
	// TokenExpiresAt is when the forge stops honouring Password, for a
	// credential the hub minted — a GitHub App installation token's hour.
	// Zero for a PAT, whose expiry the hub does not know. A git proxy that
	// keeps the credential longer than that renews it through the source that
	// leased it (Task 20375). Not secret.
	TokenExpiresAt time.Time
	// SessionID names the git proxy session this credential is, when a proxy
	// minted one in the forge credential's place (pkg/executor/gitproxycreds):
	// the session's id, which is also its Username. Not secret. Empty for a
	// forge credential handed over as is.
	SessionID 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 HandleClaimer added in v0.0.5

type HandleClaimer interface {
	ClaimHandle(rec HandleRecord) (bool, error)
}

HandleClaimer is implemented by a HandleStore several control-plane processes share (Task 20419). ClaimHandle records that the process asking now tracks rec — the one adopting it — provided the row still names the owner rec was read with, and reports whether it does.

Adoption used to leave a row naming the process that started the workload. A process that started later found the row of a member that had died, and adopted the workload again: two log pumps, two kill timers, two Kubernetes lease renewers for one workload, each settling it. A claimed row names its adopter, so it is visible to that adopter alone — and so is everything that follows the workload: its wipe, its credential refresh, its watchers.

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 TrackedElsewhere added in v0.0.5

func TrackedElsewhere(store HandleStore, executorID string) []HandleRecord

TrackedElsewhere returns executorID's rows that another live member tracks, or nil when store is not member-scoped. A read failure yields nil and is logged: the sweeps that call this also hold a grace period, which is the guard that holds when this one cannot.

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 HandleRedactor added in v0.0.5

type HandleRedactor interface {
	// AddHandleRedactions extends the redaction of handleID's output with
	// values, from its next chunk on, and reports whether the handle is known.
	AddHandleRedactions(handleID string, values ...string) bool
}

HandleRedactor is implemented by a driver that scrubs a workload's output on the hub and can be handed more values to scrub after the workload started.

A hub process that adopts a run after the one that dispatched it stopped needs it (Task 20382): the handle it rehydrated carries no redaction set, because a dispatched Spec's secrets are never persisted, so the process that takes the run's lease over re-derives the values from the lease's grants and installs them here.

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 HeldWorkspaceCredential added in v0.0.5

type HeldWorkspaceCredential struct {
	// LeaseID is the secret lease the credential stands on.
	LeaseID string
	// GrantID is the grant that authorised it.
	GrantID string
	// SessionID is the git proxy session it is, when one was minted.
	SessionID string
}

HeldWorkspaceCredential names, without its secret, a workspace credential a driver keeps on a handle past dispatch — for a push write-back, which presents it when the workload finishes (Task 20390).

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 USBHostDevices added in v0.0.5

func USBHostDevices(sel DeviceSelector, d USBDevice) []HostDevice

USBHostDevices turns a resolved USB device into the device nodes a sandbox is given: the usbfs node, at the same path so libusb's sysfs-derived lookup finds it, and — unless the selection opts out — every interface node.

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 Outcome added in v0.0.5

type Outcome string

Outcome classifies how a workload ended, when a driver knows more than its State says. Empty for every ending a State and an exit code describe.

const OutcomeDiskLimit Outcome = "disk_limit"

OutcomeDiskLimit: the driver stopped the workload because its workspace grew past its disk limit. Status.DiskLimit carries the measurement.

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 ReplaceOptions added in v0.0.5

type ReplaceOptions struct {
	// Mode of the new file. Zero means 0600; group or world bits are refused.
	Mode fs.FileMode
	// Owner, when set, is given the new file — through its descriptor, never
	// by path. Nil leaves it owned by this process.
	Owner *FileOwner
	// Dir, when set, is the identity the lease directory must still have.
	// A directory renamed away and replaced — by anyone able to rename an
	// entry in its parent, such as the sandbox user who owns it in a sticky
	// /dev/shm — is refused rather than written into.
	Dir *FileIdentity
}

ReplaceOptions shapes a ReplaceSecretFile.

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
	// RequireDiskLimit demands a node that holds a workload to
	// ResourceLimits.DiskMB (Task 20405). Separate from RequireResourceLimits
	// because the two come apart: a remote agent below protocol v20 enforces
	// CPU and memory in its container and refuses a disk limit, so routing a
	// disk-limited spec there would be a dispatch the device turns down.
	RequireDiskLimit 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
	// RequireBranchBundle demands a node that can receive a git branch the
	// control plane ships with the Spec, which is how a feature reaches a
	// sandbox (Task 20367). Refusing is the only alternative to a harness
	// that runs on the feature's base commit with none of its work.
	RequireBranchBundle 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 workspace's disk use in megabytes (Task 20405).
	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 bounds the workspace — the tree the workload works in — in
	// megabytes. Held where Capabilities.DiskEnforcement says how (Task 20405).
	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 ResultReleaser added in v0.0.5

type ResultReleaser interface {
	ReleaseResults(handleID string)
}

ResultReleaser is implemented by a driver that keeps a finished workload's returned work on the control plane's disk until it is collected — the container driver's feature mode stages a tree and an output directory per workload (Task 20367). The hub calls ReleaseResults once it has taken what it needs; a driver must also release on its own after a bounded time, because a hub that crashed between the workload ending and the collection never will.

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.

func (*RevocationLog) Widen added in v0.0.5

func (rl *RevocationLog) Widen(leaseID, grantID, why string)

Widen records on an entry why its revocation went out for the whole lease rather than the one grant it named.

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"`
	// Widened explains why a revocation naming one grant took the whole lease
	// back on this executor — an agent too old to narrow it (Task 20403).
	// Empty when the executor took back what was asked. It is said rather than
	// hidden because the workload lost credentials nobody revoked, and its next
	// failure should not be a mystery.
	Widened string `json:"widened,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"`
	// Eventual reports that files were withdrawn from the object a running
	// workload reads them through rather than from the workload's own
	// filesystem, so they disappear inside it when the platform next syncs
	// that object — a Kubernetes Pod's projected Secret volume, within the
	// kubelet's sync period (about a minute) — not at the moment of this
	// report. Only the Kubernetes driver sets it (Task 20403).
	Eventual bool `json:"eventual,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 RunReaper added in v0.0.5

type RunReaper interface {
	ReapRun(ctx context.Context, runID string) (reaped []string, err error)
}

RunReaper is implemented by a driver that can find and stop the workloads it started for a run by the run id alone (Task 20419): what a hub process that died between starting a workload and recording its handle leaves behind, which no row names. reaped lists what was stopped; none found is not an error.

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) RunsProjectImages added in v0.0.5

func (s SandboxSettings) RunsProjectImages() bool

RunsProjectImages reports whether these settings run payloads in a container — the one mode in which a device honours the image a project names in its .cloop/sandbox.yaml (and its setup: block), and so the one in which the hub's image policy decides what a device runs. The device driver advertises SupportsImageOverride from this, and `cloop hub doctor` counts devices by it.

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 SecretRefreshReport added in v0.0.5

type SecretRefreshReport struct {
	LeaseID string `json:"lease_id"`
	// Known reports whether the executor holds the lease at all. False with no
	// Error is "nothing here to refresh", not a failure.
	Known bool `json:"known"`
	// FilesRewritten counts files replaced, across every workload holding the
	// lease.
	FilesRewritten int `json:"files_rewritten,omitempty"`
	// Handles names the workloads reached.
	Handles []string `json:"handles,omitempty"`
	// Eventual: the files went to something that delivers them later — the
	// kubelet syncing a Secret volume — so the workload reads the new content
	// within that sync, not at once. The hub then leaves the superseded
	// credential to lapse on its own rather than destroying it early.
	Eventual bool `json:"eventual,omitempty"`
	// Unsupported: the executor cannot rewrite files in a running workload at
	// all — a remote agent speaking too old a protocol. The workload keeps the
	// credential it was given until that lapses; Error says why.
	Unsupported bool `json:"unsupported,omitempty"`
	// Error is non-empty when part of the refresh failed.
	Error string `json:"error,omitempty"`
}

SecretRefreshReport is what one executor did with a refresh.

func (SecretRefreshReport) Delivered added in v0.0.5

func (r SecretRefreshReport) Delivered() bool

Delivered reports whether every holder on this executor has the new files.

type SecretRefreshRequest added in v0.0.5

type SecretRefreshRequest struct {
	// LeaseID is the lease whose files change. Required.
	LeaseID string
	// Files carry the replacements: Dir is the directory as the workload's
	// environment names it (the lease's declared directory), Name the bare
	// name the file was delivered under, Content the new plaintext.
	Files []SecretFile
	// Reason is operator-facing, for the driver's log.
	Reason string
}

SecretRefreshRequest replaces files one lease already placed for the workloads holding it.

func (SecretRefreshRequest) Validate added in v0.0.5

func (r SecretRefreshRequest) Validate() error

Validate checks the request before a driver writes anything.

func (SecretRefreshRequest) Values added in v0.0.5

func (r SecretRefreshRequest) Values() []string

Values returns the plaintext the request carries, for the holder's output redaction.

type SecretRefresher added in v0.0.5

type SecretRefresher interface {
	Executor
	// RefreshSecretFiles rewrites req's files in every workload holding
	// req.LeaseID and extends each holder's output redaction to the new
	// content. A driver holding nothing for the lease answers Known false.
	RefreshSecretFiles(ctx context.Context, req SecretRefreshRequest) SecretRefreshReport
}

SecretRefresher is an executor that can rewrite a lease's files inside workloads it already started.

Optional: a driver that does not implement it keeps the files it delivered for the life of the workload, and a token among them lapses when GitHub says so — the behaviour every driver had before Task 20375.

func AsSecretRefresher added in v0.0.5

func AsSecretRefresher(ex Executor) (SecretRefresher, bool)

AsSecretRefresher returns ex as a SecretRefresher, reporting whether it is one.

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: a session that has been
	// re-dispatched n times carries n+1. The failover cap is checked against
	// it inside the claim (see SessionStore.ClaimRequeue).
	Attempt   int       `json:"attempt"`
	StartedAt time.Time `json:"started_at"`
	// RunningTasks are the tasks the workload announced it was working on,
	// as last observed by the control plane (Task 20391). A run on an
	// executor that does not share the hub's filesystem works on a copy of
	// the project, so the hub's own plan cannot say which task a node died
	// under; this can. Empty when nothing was observed.
	RunningTasks []int `json:"running_tasks,omitempty"`
	// 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, and
	// decides in the same write whether the failover may re-dispatch it.
	//
	// 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.
	//
	// maxAttempts is the failover cap (Task 20391): how many times one
	// dispatch may be started again elsewhere. A session whose Attempt
	// already exceeds it — it has been re-dispatched maxAttempts times — is
	// claimed as exhausted rather than requeued, and exhausted reports so.
	// The cap belongs in the claim and not before it: decided outside, two
	// supervisors could each read the session as eligible, or one could close
	// it while another re-dispatched it, and the session's terminal state
	// would record whichever wrote last rather than what was decided.
	ClaimRequeue(sessionID, claimToken string, maxAttempts int, at time.Time) (claimed Session, exhausted bool, err 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"`
	// EgressRules is the IP-layer firewall this workload runs under when a
	// rule set stored in the hub applies to it: a device's, a project's, or
	// both, composed with the executor's own by pkg/fwpolicy (Task 20363).
	//
	// It is the opposite kind of input from EgressScope. It can name
	// 10.0.0.0/8, so it is never read from the repository: it comes from rule
	// sets an admin or the project's maintainers saved through the hub, and it
	// has already been proven, at dispatch, to fit inside every level above it.
	// Drivers prove it again before installing it, because a guarantee only the
	// caller enforces holds until the second caller.
	//
	// nil means no stored rule set applies, and the executor's own
	// configuration decides exactly as before. A zero rule set means the
	// workload reaches nothing — the opposite of nil.
	EgressRules *FirewallRules `json:"egress_rules,omitempty"`
	// EgressBound is the device-level rule set EgressRules was proven against —
	// the admin's superset for the machine the workload lands on — carried so
	// that a driver on that machine can check the containment itself. nil when
	// the device has no rule set; never set without EgressRules.
	EgressBound *FirewallRules `json:"egress_bound,omitempty"`
	// EgressProxy is the route this workload takes to the hub's egress proxy,
	// when the hub redeemed it a proxy session (Task 20378). It carries no
	// credential — the session's URL travels in Env, declared sensitive — only
	// what a driver needs to make that URL work from inside its sandbox: a
	// name to pin to the sandbox network's gateway, and an address to open in
	// the sandbox's firewall. See EgressProxyRoute.
	//
	// nil means the workload was given no session, and no driver opens
	// anything for one.
	EgressProxy *EgressProxyRoute `json:"egress_proxy,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"`

	// GitCA is the hub's own certificate authority, which this workload's git
	// — and cloop's provisioning and write-back git on its behalf — trusts
	// for the URLs it names and nothing else (Task 20409). The hub sets it
	// when its git proxy serves a certificate it issued itself
	// (executors.git_proxy.tls: auto); each driver writes the bundle where
	// the workload can read it and adds http.<url>.sslCAInfo to its
	// GIT_CONFIG_COUNT block. A CA certificate is public, so unlike
	// SecretFiles this travels in the Spec. nil delivers nothing. See
	// gitca.go.
	GitCA *GitCA `json:"git_ca,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:"-"`

	// BranchBundleFile names the git bundle Workspace.Branch describes, as a
	// path on the control plane's host, for the driver to read while it
	// starts the workload: the container driver unpacks it into the tree it
	// stages, the remote driver streams it to the device ahead of the start
	// frame. Empty when the branch ships no bytes.
	//
	// json:"-" for the reason ProjectSeed is: a bundle is megabytes, and a
	// Spec is persisted, audited and re-read after a restart. The caller owns
	// the file and removes it once Start has returned.
	BranchBundleFile string `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) NeedsGitCA added in v0.0.5

func (s Spec) NeedsGitCA(workspaceRepo string) bool

NeedsGitCA reports whether the workload will speak to a URL the bundle covers: its lease rewrites GitHub to a covered proxy (CLOOP_GIT_PROXY_URL), or its workspace is fetched from one. A driver that cannot deliver the bundle refuses exactly the workloads for which this is true.

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

func (Spec) WriteBackBase added in v0.0.5

func (s Spec) WriteBackBase() string

WriteBackBase returns the commit a write-back of this Spec is measured against: the head of a shipped branch, else the commit a git workspace is pinned to. Empty when the Spec has neither.

It exists so the drivers that run the write-back inside the sandbox (the Kubernetes wrapper, the container driver's feature mode) cannot disagree with the ones that read the base off the provisioned tree.

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"`
	// Outcome classifies an ending State alone does not: OutcomeDiskLimit for
	// a workload its driver stopped at its disk limit, with DiskLimit holding
	// the measurement (Task 20405). Both cross the remote boundary in the
	// status frame, so a device's stop reads the same on the hub as one on
	// the hub's own host.
	Outcome   Outcome          `json:"outcome,omitempty"`
	DiskLimit *DiskLimitBreach `json:"disk_limit,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 WithCandidateFilter added in v0.0.5

func WithCandidateFilter(fn func(Executor) bool) SupervisorOption

WithCandidateFilter keeps only the executors fn accepts in the failover placement pool, whichever source assembled it (Task 20396). The control plane uses it for what this package cannot see: an executor restricted to an access list is no place for a run the supervisor moves on nobody's behalf.

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 WithFailoverLimit added in v0.0.5

func WithFailoverLimit(fn func() int) SupervisorOption

WithFailoverLimit supplies the failover cap — executors.failover.max_attempts — read at each claim rather than once at start (Task 20391), so an operator who lowers it sees the next failover honour it without a restart.

A value outside [0, MaxFailoverAttemptsCeiling] is clamped into it: the cap is a bound, and no reading of a bad value may be "no bound".

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 WithProbeFilter added in v0.0.5

func WithProbeFilter(fn func(Executor) bool) SupervisorOption

WithProbeFilter restricts probing to the executors fn accepts (Task 20354).

For control planes served by several hub processes. Each process has every executor in its registry, but only one of them can observe a given edge agent — the one holding its socket — and to every other process that agent looks unreachable. Letting them all probe would have each non-holder demote a healthy device within seconds and fail its work over onto another executor: two agents on one task. The filter lets exactly one process speak for each executor. Executors skipped are neither probed nor transitioned; their persisted health is whatever the process that does probe them wrote.

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 USBDevice added in v0.0.5

type USBDevice struct {
	// Port is the kernel's topology name for where the device is plugged in
	// ("1-1", "3-2.4"). Stable across re-enumeration for as long as the device
	// stays in the same port, which is why a selection may pin it.
	Port string `json:"port"`
	// Bus and Dev are the current bus and device numbers. Dev changes on every
	// re-enumeration; see the file comment.
	Bus int `json:"bus"`
	Dev int `json:"dev"`
	// VendorID and ProductID are the four-digit lowercase hex IDs lsusb shows.
	VendorID  string `json:"vendor_id"`
	ProductID string `json:"product_id"`
	// Manufacturer, Product and Serial are the device's own descriptor
	// strings, sanitised. Serial is empty for the many devices that have none.
	Manufacturer string `json:"manufacturer,omitempty"`
	Product      string `json:"product,omitempty"`
	Serial       string `json:"serial,omitempty"`
	// Class is bDeviceClass as two hex digits; "09" is a hub.
	Class string `json:"class,omitempty"`
	// Speed is the link speed in Mbit/s as the kernel reports it ("12", "480").
	Speed string `json:"speed,omitempty"`
	// Node is the usbfs character device a userspace driver such as libusb
	// opens: /dev/bus/usb/BBB/DDD.
	Node string `json:"node"`
	// Nodes are the other device nodes the kernel created for this device's
	// interfaces — a CDC-ACM serial line, a HID endpoint — which is how most
	// hardware is actually talked to when no userspace driver is involved.
	Nodes []USBNode `json:"nodes,omitempty"`
	// Mode, UID and GID describe Node's permissions as the agent saw them.
	// Empty when the agent cannot see /dev at all, which is the norm under
	// the hardened service unit (PrivateDevices=yes): the container engine
	// resolves the node in the real /dev, but the agent never does.
	Mode string `json:"mode,omitempty"`
	UID  *int   `json:"uid,omitempty"`
	GID  *int   `json:"gid,omitempty"`
	// Group is GID's name on the device, when it has one.
	Group string `json:"group,omitempty"`
}

USBDevice is one device in an executor host's USB inventory.

func ResolveUSB added in v0.0.5

func ResolveUSB(inventory []USBDevice, m USBMatch) (USBDevice, error)

ResolveUSB finds the one device in an inventory that m describes.

Zero matches and several matches are both refusals. Zero is the device being unplugged, which must fail the start rather than run a hardware task without its hardware; several is the selection being ambiguous, and picking one would be guessing which physical unit a task talks to.

func (USBDevice) IsHub added in v0.0.5

func (d USBDevice) IsHub() bool

IsHub reports whether the device is a USB hub. Passing a hub's node through does not pass what is plugged into it, so the dashboard does not offer it.

func (USBDevice) Label added in v0.0.5

func (d USBDevice) Label() string

Label renders the device the way an admin recognises it.

func (USBDevice) Validate added in v0.0.5

func (d USBDevice) Validate() error

Validate checks an inventory entry that arrived from a device. The control plane stores and renders these, so an entry that is not the shape the agent is supposed to produce is refused rather than displayed.

type USBMatch added in v0.0.5

type USBMatch struct {
	VendorID  string `json:"vendor_id"`
	ProductID string `json:"product_id"`
	// Serial narrows the match to one physical unit. Empty matches any serial,
	// which is fine for the common case of one such device per host and is
	// refused as ambiguous when there are two.
	Serial string `json:"serial,omitempty"`
	// Port narrows the match to the device plugged into one port — the only
	// way to tell apart two identical devices that report no serial.
	Port string `json:"port,omitempty"`
}

USBMatch identifies one USB device independently of where it enumerated.

func (USBMatch) Matches added in v0.0.5

func (m USBMatch) Matches(d USBDevice) bool

Matches reports whether d is the device m describes.

func (USBMatch) Normalize added in v0.0.5

func (m USBMatch) Normalize() (USBMatch, error)

Normalize canonicalises and validates the match.

func (USBMatch) String added in v0.0.5

func (m USBMatch) String() string

String renders the match for a message.

type USBNode added in v0.0.5

type USBNode struct {
	// Path is the node under /dev ("/dev/ttyACM0", "/dev/hidraw2").
	Path string `json:"path"`
	// Subsystem is the kernel class that created it ("tty", "hidraw").
	Subsystem string `json:"subsystem"`
}

USBNode is one interface device node belonging to a USB device.

type VirtualSpec added in v0.0.5

type VirtualSpec struct {
	// Sandbox is where and how payloads run. Mode must be container.
	Sandbox SandboxSettings `json:"sandbox"`
	// Firewall is the IP-layer egress policy for every sandbox this executor
	// starts. Nil means no firewall: the sandbox gets Sandbox.Network as-is —
	// "none" by default, which is no network at all.
	//
	// Non-nil puts each sandbox on a bridge of this executor's own, filtered
	// on the host side before any sandbox can join it; see
	// container.EgressFilter. The host needs nft(8) and the agent
	// CAP_NET_ADMIN for that, and a device that cannot filter refuses the
	// work rather than running it unfiltered.
	Firewall *FirewallRules `json:"firewall,omitempty"`
	// Devices are the host devices every sandbox of this executor is given.
	Devices []DeviceSelector `json:"devices,omitempty"`
}

VirtualSpec is the configuration of one virtual executor.

func (VirtualSpec) CheckMetadata added in v0.0.5

func (v VirtualSpec) CheckMetadata() error

CheckMetadata is the save-time rule for the configuration's firewall: see FirewallRules.CheckMetadata, and why neither Normalize nor Validate applies it — a device re-validates every configuration the hub sends it, and one stored before the rule has to keep starting sandboxes, which keep the metadata service closed.

func (VirtualSpec) Describe added in v0.0.5

func (v VirtualSpec) Describe() string

Describe renders the configuration for an audit row or a log line.

func (VirtualSpec) FilteredEgress added in v0.0.5

func (v VirtualSpec) FilteredEgress() bool

FilteredEgress reports whether this configuration bounds egress at the IP layer.

func (VirtualSpec) NetworkEgress added in v0.0.5

func (v VirtualSpec) NetworkEgress() bool

NetworkEgress reports whether sandboxes under this configuration have a network to reach anything through.

func (VirtualSpec) Normalize added in v0.0.5

func (v VirtualSpec) Normalize() (VirtualSpec, error)

Normalize canonicalises and validates the whole configuration.

func (VirtualSpec) Validate added in v0.0.5

func (v VirtualSpec) Validate() error

Validate reports whether the configuration is admissible.

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"`
	// Upstream is the repository Repo was routed away from, set by
	// WorkspaceAccess.Apply when a git proxy redirected the fetch: the forge
	// URL, where Repo is now the proxy's. Only the shipped copy of a Spec
	// carries it. A device keeps its checkout between runs and uses this to
	// recognise that checkout after the route changed — the proxy turned on
	// or off, or its advertise_url moved — rather than refusing it as some
	// other repository (Task 20349).
	Upstream string `json:"upstream,omitempty"`
	// Branch is a git branch the control plane ships with the Spec to put on
	// top of — or, for Kind bundle, in place of — the fetched tree, leaving
	// the checkout on that branch (Task 20367). Nil for every workload but a
	// feature's. Metadata only: the bundle's bytes travel beside the Spec, as
	// Spec.BranchBundleFile on the hub and as chunk frames on the wire.
	Branch *BranchBundle `json:"branch_bundle,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) NeedsNetwork added in v0.0.5

func (w Workspace) NeedsNetwork() bool

NeedsNetwork reports whether provisioning has to reach a remote. A bundle workspace is built from bytes the control plane sent, so it is the one provisioned kind a workload with no egress can still have.

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 a tree the executor has to build — fetched with git, or unpacked from a shipped bundle — 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) UpstreamRepo added in v0.0.5

func (w Workspace) UpstreamRepo() string

UpstreamRepo returns the repository the workspace is a checkout of: Upstream when a proxy routed it, Repo otherwise.

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 WorkspaceCredentialHolder added in v0.0.5

type WorkspaceCredentialHolder interface {
	HeldWorkspaceCredential(handleID string) (HeldWorkspaceCredential, bool)
}

WorkspaceCredentialHolder is implemented by a driver that keeps a workload's workspace credential on its handle past dispatch. The hub names what it holds in the run's owner row, so the hub process that adopts the run after this one stops takes the lease over and restores the session, and the workload's write-back push is served rather than refused (Task 20390).

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"
	// WorkspaceBundle: the executor builds the tree from a git bundle the
	// control plane ships beside the Spec, described by Workspace.Branch
	// (Task 20367). It is how a feature — a branch of a repository that may
	// exist only on the hub — reaches a sandbox: there is no remote to clone
	// from, and the hub's own worktree is not something to mount. See
	// branchbundle.go.
	WorkspaceBundle WorkspaceKind = "bundle"
	// 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.

type WriteBackScanner added in v0.0.5

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

WriteBackScanner finds the write-back line in a workload's output stream.

The stream arrives as arbitrary chunks — a line may be split across two of them, and several lines may share one — so the scanner buffers a partial line rather than assuming chunk boundaries mean anything. Only text that could still become a sentinel is retained: the buffer is dropped the moment the current line is longer than a sentinel could be, so a workload that prints a gigabyte without a newline costs nothing. The zero value is ready to use.

func (*WriteBackScanner) Buffered added in v0.0.5

func (s *WriteBackScanner) Buffered() int

Buffered reports how many bytes of an unfinished line the scanner is holding, so a test can pin the memory bound.

func (*WriteBackScanner) Observe added in v0.0.5

func (s *WriteBackScanner) Observe(text string)

Observe feeds one chunk of output to the scanner.

func (*WriteBackScanner) Snapshot added in v0.0.5

func (s *WriteBackScanner) Snapshot() *WriteBackResult

Snapshot returns the last sentinel seen, or nil.

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 featurehub is the hub's half of running a feature on an executor that isolates from the hub's filesystem (Task 20367).
Package featurehub is the hub's half of running a feature on an executor that isolates from the hub's filesystem (Task 20367).
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
diskwatch
Package diskwatch measures a sandbox's workspace and holds a running workload to its disk limit by sampling it (Task 20405).
Package diskwatch measures a sandbox's workspace and holds a running workload to its disk limit by sampling it (Task 20405).
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.
Package resultframe carries a seeded run's project result out of a sandbox whose only channel home is its own output stream (Task 20402).
Package resultframe carries a seeded run's project result out of a sandbox whose only channel home is its own output stream (Task 20402).

Jump to

Keyboard shortcuts

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