Component and security architecture¶
This page describes all of cloop in one place: the processes it runs as, the components inside each process, the data they keep, and the path a task takes from a click to a commit. Wherever one part talks to another, it also says what is trusted, how each side proves who it is, and what stops a compromised part from doing more damage.
The other pages under Architecture and Security each cover one piece in depth.
This page shows how the pieces fit together and links to the page that covers
each one. The package paths named here are real, and a test keeps them that
way: tests/docs/architecture_overview_test.go fails if a path named here no
longer exists, or if a package is added under pkg/ or a suite under tests/
without being named here (a package in the component map, a
suite in how the architecture is kept true).
- Two ways to run cloop
- The system at a glance
- Processes
- Inside the hub
- What is kept, and where
- A run, end to end
- Executor pools
- Security architecture
- Design rules
- Trust boundaries
- Identity and access
- Secrets and keys
- Sandbox containment
- Credential guards
- Transport and the agent channel
- Supply chain
- Audit and evidence
- Availability as a security property
- Component map
- How the architecture is kept true
- Reading on
Two ways to run cloop¶
cloop is a single Go binary (main.go hands over to cmd, a Cobra command
tree) that you can use in two quite different ways.
On a checkout. You run cloop run in a repository. The orchestrator
(pkg/orchestrator) turns a goal into a plan of tasks and runs a harness once
per task. The harness is Claude Code's claude, GitHub Copilot's copilot, or
a model API that cloop calls directly. After each task the orchestrator reads
the outcome and moves on to the next one. The plan, the history and the costs
are stored in the checkout's .cloop/state.db. The harness runs as you, with
your files and credentials, so the only trust boundary is the machine itself.
Your first project and
how cloop works describe this mode.
As a hub. cloop ui serves the dashboard, its JSON API and its WebSockets,
and hosts many projects for many people. In this mode the hub decides what runs
and records what happened, and executors do the running. The hub starts each
run as a cloop run workload on an executor: a container on a host, an
enrolled edge device, or a Kubernetes Pod. The run gets a lease on the
credentials its project was granted. Where cloop can arrange it, those
credentials never enter the sandbox: the sandbox holds a session token, and a
proxy on the hub uses the real credential on its behalf.
You can see the split in the import graph: only cmd imports
pkg/orchestrator. The hub doesn't contain the task loop; it starts one
somewhere else. The rest of this page is about the hub, because that is where
the trust boundaries are.
The system at a glance¶
flowchart TB
subgraph callers["Callers"]
browser["Browser<br/>dashboard, glasses page"]
terminal["Terminal<br/>cloop login, cloop remote"]
pipeline["CI job<br/>GitHub Actions OIDC"]
end
idp["Identity provider<br/>OIDC sign-in, SCIM"]
hub["Hub: cloop ui, one or more members<br/>state.db and the root key"]
services["GitHub, model API,<br/>Confluence, Jira, KMS,<br/>SIEM, registries"]
subgraph fleet["Executors"]
ctr["container driver<br/>docker or podman"]
agent["cloop executor agent<br/>an enrolled device"]
kube["kubernetes driver<br/>one Pod per run"]
pool["executor controller<br/>runner Pods on demand"]
end
forwarder["cloop-forward<br/>a USB device or interface"]
sandbox["Sandbox<br/>cloop run and its harness"]
browser --> hub
terminal --> hub
pipeline --> hub
idp <-->|"sign-in, SCIM"| hub
hub --> services
hub --> ctr
hub --> kube
hub ---|"dials out"| agent
hub ---|"dials out"| forwarder
hub ---|"dials out"| pool
ctr --> sandbox
agent --> sandbox
kube --> sandbox
pool --> sandbox
sandbox -->|"session tokens only"| hub
%% Tints, not opaque fills, and no text colour:
%% see tests/docs/diagram_contrast_test.go.
style hub fill:#4674d126,stroke:#4674d1,stroke-width:2px
style sandbox fill:#cc339926,stroke:#c39
Three facts shape the rest of this page:
- The hub is the only trusted component. It holds the database, the root key and every credential. Every other part is assumed to be able to lie: a browser session can be a stolen cookie, an enrolled device can be compromised, and a workload is treated as hostile, because it runs code the hub didn't write, chosen by a model.
- Devices, forwarders and executor controllers dial out. The hub never opens a connection to an edge device or into a cluster that an executor controller serves. An agent behind NAT keeps one outbound WebSocket open and receives its work over it, and a controller asks the hub for its pool's demand the same way.
- Where a guard is on, sandboxes talk to the hub instead of the services.
With the git proxy and the Kubernetes monitor on (
cloop hub bootstrapturns on both), a sandbox's git and Kubernetes requests go to listeners on the hub. The hub decides each request and adds the real credential on the way out. The same goes for the sandbox's knowledge searches, for its Internet traffic when it has an egress lease, and for its model calls when the model relay is on. The relay is opt-in and serves Claude Code and Anthropic API runs.
Processes¶
| Process | Started as | Runs on | Holds | Code |
|---|---|---|---|---|
| Hub member | cloop ui |
the control-plane host; several share one database as a cluster | the database, the root key, sessions, the credential guards' listeners | pkg/ui |
| REST server | cloop serve |
wherever it is run | one project's API, without the dashboard | pkg/apiserver |
| Run | cloop run |
in a sandbox on an executor, or on a checkout | one project's plan, and the harness it starts | pkg/orchestrator |
| Harness | claude, copilot |
as a child of the run | session tokens the hub minted for the run; where no guard is on, the credential itself (by default its Claude or Copilot sign-in) | pkg/provider/claudecode, pkg/provider/copilot |
| Device agent | cloop executor agent |
an enrolled device, as a hardened service | its agent credential and, if it was enrolled with one, the hub's SPKI pin | pkg/executor/agent |
| Executor controller | cloop executor controller |
a Kubernetes cluster, installed by its Helm chart | its pool's controller credential, read from a Secret, and a Role in its own namespace | pkg/kubecontroller, deploy/helm |
| Pool runner | cloop executor agent --ephemeral |
a Pod the controller starts for one run | a single-use runner token, then a runner credential in memory | pkg/executor/agent |
| Upgrade helper | cloop-executor-upgrade.service |
the same device | no credential; it runs as root to verify and install the signed builds the agent asks for | pkg/executor/install |
| Device forwarder | cloop-forward |
a machine wired to hardware | a forwarder credential and, if it was enrolled with one, the hub's pin | cmd/cloop-forward, pkg/deviceforward |
| Terminal client | cloop login, cloop remote |
a developer's machine | a terminal session approved from the browser | pkg/hubclient |
| Cluster share | cloop remote kube share |
a developer's machine | runs the kubeconfig's exec plugin locally and uploads a narrow ServiceAccount token | pkg/kubeexec, pkg/kubeshare |
| MCP server | cloop mcp |
beside a harness | the plan, as Model Context Protocol tools; in a run of a project with knowledge sources, also the run's knowledge search | pkg/mcp, pkg/knowledgerun |
cloop-forward is a separate binary. Its main imports pkg/deviceforward
and links no orchestrator, dashboard or hub code, so the machine wired to the
hardware runs only a program that relays bytes and holds a credential
(device forwarders).
Inside the hub¶
A hub member is a single process, and most of the code described here is in
pkg/ui. Its work falls into six areas:
flowchart TB
serve["Serve<br/>one route table<br/>WebSocket updates"]
decide["Decide<br/>authentication, roles<br/>quotas, placement"]
dispatch["Dispatch<br/>secret broker<br/>executor registry<br/>supervision, failover"]
guard["Guard<br/>git proxy<br/>Kubernetes monitor<br/>egress proxy<br/>model and CI relay<br/>knowledge search"]
duty["Leader duties<br/>retention, checkpoints<br/>backups, auto-resume<br/>sweeps, knowledge sync"]
keep[("Keep: statedb<br/>plans, runs, fleet<br/>sessions, secrets<br/>grants, leases<br/>the audit trail")]
serve --> decide --> dispatch --> guard
decide --> keep
dispatch --> keep
guard --> keep
duty --> keep
style guard fill:#4674d126,stroke:#4674d1
Serve. Every route is declared in one route table, together with the
permission it requires, the scope that permission is checked against, and
whether it needs a recent sign-in. gate() wraps each route when it is
registered, and require() is the single place where permissions are enforced,
so a handler can't forget the check. A route that declares no permission and
isn't marked public stops the hub at startup. The
HTTP API reference is generated from that table,
and a test checks that the two match. Dashboards get live state as WebSocket
pushes, scoped per project, and members pass it to each other over the
cluster's event bus.
Decide. Authentication works out which identity a caller is. pkg/authz
gives that identity a role in each scope and decides whether it may do
something, pkg/quota decides how much, and placement (pkg/executor)
decides where a run may go. See identity and access
and executors: placement.
Keep. Each control plane has one SQLite database (pkg/statedb, with the
schema in numbered migrations). It holds the fleet, sessions, grants and leases,
sealed secrets, proxy sessions, the knowledge index and the hub's own audit
chain. Each project keeps its plan and its own audit chain in its
.cloop/state.db. See what is kept, and where.
Dispatch. The secret broker leases a project's grants to one run. The executor registry turns the project's executor binding into a driver. Supervision watches the running workload, moves the run to another executor if its executor dies, and brings back what the run produced. See a run, end to end.
Guard. The credential guards are listeners served by the hub process itself, because the process that mints a session token has to be the one that accepts it. There is no separate proxy daemon that could be compromised or fall out of step with the hub. See credential guards.
Leader duties. Some work must happen once per control plane: retention and
the audit prune, audit head checkpoints, scheduled backups, auto-resume of
paused projects, the orphan sweep and knowledge syncs. It runs on the member
that holds the control-plane lease (pkg/hublease, pkg/hubcluster). A hub
running on its own is always the leader.
What is kept, and where¶
| Store | What it holds | Protection |
|---|---|---|
<hub>/.cloop/state.db |
executors and their sessions, dashboard sessions, API tokens, role bindings, project members, teams, quotas, provisioned users and groups, secrets, grants, leases, grant requests, proxy sessions, cluster membership and bus, knowledge sources and chunks, telemetry, the hub's audit chain | SQLite in WAL mode with a bounded log; credential payloads sealed under the key hierarchy; credentials the hub issues kept only as hashes |
<project>/.cloop/state.db |
the plan and its tasks, run state, events, costs, requirements and verdicts, the project's audit chain | the same schema and rules: migrations only add to the schema; a binary refuses a newer database unless every migration it lacks is recorded as harmless to it; a save never erases a column that an older binary doesn't know about |
<project>/.cloop/ files |
task artifacts, live output, checkpoints, attachments, the sandbox spec, config | crash-safe writes (pkg/atomicfile), size-limited reads (pkg/boundedread), and size kept in check by the janitor (pkg/janitor) |
| Root key | CLOOP_SECRET_KEY, from a file, a systemd credential or the environment; or KEKs held by a KMS |
kept outside every .cloop/ directory; can be escrowed under a passphrase (pkg/keyescrow) |
| Audit checkpoints | the head of each audit chain, every five minutes by default | written outside the database, so a truncated trail can be detected |
| Per-identity homes | each person's Claude Code and Copilot configuration on an SSO hub | one directory per identity, destroyed when the person is offboarded |
| Device side | the agent's credential, agent.json in its state directory (mode 0600), and the hub's pin if it was enrolled with one |
the hub can revoke the credential, and then sends bye so the agent stops reconnecting |
No database holds the plaintext of a credential the hub issues: session cookies, API tokens, enrollment tokens, agent credentials, executor pool credentials, runner tokens and proxy session tokens. The hub stores a hash instead (SHA-256, or a salted HMAC-SHA256 for API tokens, glasses links and SCIM credentials) and compares it in constant time. A hash is enough to check a token that someone presents, and it can't be turned back into the token.
A hub process that adopts a run another process started never saw that run's
tokens, but it still has to scrub them from the run's output. So every token
the hub mints for a run starts with a prefix that names its kind
(cloop_git1_, cloop_kube1_, cloop_egress1_, cloop_relay_,
cloop_know1_, and GitHub's ghs_ for App installation tokens). The hub picks
out anything with one of these prefixes and confirms it against the recorded
hash (pkg/redact).
A run, end to end¶
This is what happens between a click on Start and the task's commit
landing, in the order the hub does it (startWorkloadAs in
pkg/ui/executor.go):
sequenceDiagram
actor person as Person
participant hub as Hub
participant driver as Executor
participant sbx as Sandbox
participant world as Services
person->>hub: Start
hub->>hub: admit, place, preflight
hub->>hub: lease grants, shape the spec
hub->>hub: last gate, record intent
hub->>driver: Start(spec): cloop run
driver->>sbx: create, fetch the source
loop every task
sbx->>hub: git, model, cluster calls<br/>with session tokens
hub->>world: the same calls<br/>with the real credential
sbx-->>hub: output, scrubbed on arrival
end
sbx-->>driver: exit, returned work
driver-->>hub: status, bundle
hub->>hub: land, end sessions, release lease
- Admission. The request goes through the front door: a same-origin check, a JSON body, an authenticated identity, the route's permission at the project's scope, the identity's quota, and a free concurrency slot for the tenant.
- Where. Before anything is chosen, the hub checks the project's directory
again against the confinement rule (
pkg/projectconfine). Then it resolves the project's executor binding, and the host-execution policy refuses a driver with no isolation. The run also has to pass a harness preflight: if the executor can't give the harness a Claude or Copilot sign-in, the run is refused before the hub mints anything for it. - What it may hold. The secret broker leases to this run, with a TTL,
every grant whose subject matches the project and the executor. Repository
grants become mounts, device grants become
--devicearguments, and interface grants are moved into the sandbox's network namespace. The rest become environment variables and files, except a credential that a guard will hold: that one is left out of the spec entirely. - What it runs in. The project's
.cloop/sandbox.yamlnarrows the spec (image, environment allowlist, ceilings, mounts). If the executor can't provide everything the file asks for, the run is refused; cloop never applies only part of it. The image is checked against the trust policy and pinned to a digest. Then the hub applies, in this order, the firewall levels, forwarded devices, the egress session, the workspace (for a feature, its branch bundle), trust in the hub CA, the model relay session, the knowledge session and the operator's resource ceilings. - Last gate.
executor.RequireRevocablerefuses an executor that couldn't take back a credential it is about to receive. This check comes after every step that can add a credential, because an earlier check would see an incomplete spec. The hub records its intent to dispatch before the workload exists. If a member dies at the wrong moment, the next member finds that record and acts on it, so no run is left unsupervised. - Run. The driver starts the workload. Every value the run was given is scrubbed from its output before the output reaches the hub's log bus, the database or a dashboard. The run's pushes go through the git proxy's policy and are scanned for the same values. Connections that its firewall refuses are reported to the run.
- Return. An executor that doesn't share the hub's filesystem sends back the run's commits as a write-back bundle, or a seeded project's outcome as a result frame. The hub lands them, ends the run's sessions, releases its lease and destroys credentials that were delivered as files. If the executor is lost instead, supervision dispatches the run again, up to a limit, and quarantines a task if two or more executors went down while running it.
Executors: end to end follows the same path and shows where each driver differs.
Executor pools¶
An executor pool lets a Kubernetes cluster run the hub's work when the hub
cannot reach the cluster and holds no credential for it. The cluster runs
cloop executor controller, which starts runners as runs queue for the pool.
Three parties take part, each with a credential of its own:
flowchart TB
hub["Hub<br/>the queue, max_runners,<br/>credential hashes"]
ctl["executor controller<br/>a Role in one namespace"]
api["Kubernetes API server"]
runner["Runner Pod<br/>cloop executor agent<br/>--ephemeral"]
ctl -->|"pool credential:<br/>feed, mint, retire"| hub
ctl -->|"its ServiceAccount:<br/>Pods, Secrets, PVCs"| api
api -->|"starts"| runner
runner -->|"runner token once,<br/>then its credential"| hub
%% Tints, not opaque fills, and no text colour:
%% see tests/docs/diagram_contrast_test.go.
style hub fill:#4674d126,stroke:#4674d1,stroke-width:2px
style ctl fill:#4c9a2a26,stroke:#4c9a2a
style runner fill:#cc339926,stroke:#c39
- The pool credential (
cloop_pool1_…) belongs to the controller. The hub keeps its hash and accepts it only on the four pool-controller routes, for its own pool. With it the controller reads the pool's demand feed: how many queued runs the pool may take, split by a digest of each run's project, and the state of each of its runners. It reads no project and starts no run. - Just-in-time enrollment. For each runner it needs, the controller starts
a Pod and asks the hub for a single-use runner token (
clrt1.…, good for ten minutes at most), which the hub counts against the pool'smax_runnersin the transaction that mints it. The controller writes the token to a Secret the Pod owns. The runner redeems it for a runner credential (clrc1.…) that it keeps in memory, and enrolls as an ephemeral executor. - One run per runner. The hub picks the run: the oldest queued run the pool may see by its access list, its owner and the scheduler's matching rules. It starts the run on behalf of the person who queued it, with that person's grants. When the run is settled, the hub retires the runner and the controller deletes its Pod. The controller scales down only by asking the hub, which retires an idle runner only while it is still idle and never a busy one.
- Revoking a pool, or one of its credentials, retires its runners, refuses its runner tokens and revokes every lease their runs hold, on every hub member. The hub reaches nothing in the cluster, so the pool's volumes stay there until the cluster's operator deletes them.
Executors: executor pools has the details, and the Kubernetes executor controller guide covers installing and running the controller.
Security architecture¶
Design rules¶
The code follows these rules wherever it has a choice to make. Each rule says where it is enforced.
- The hub never runs a harness itself. No request can fork a process on
the hub's host when the configuration has
executors.allow_host_process: false— whichcloop hub bootstrapwrites for you. Once host execution is off, nothing in the running process can switch it back on. The policy is enforced at registration, resolution and placement, in the local driver and in every handler that touches the host. A call-graph test follows every route and background duty to prove that none of them reachesos/execwithout going through the policy check (the no-host-execution guarantee). - Workloads are treated as hostile. The hub checks everything a sandbox sends it. Its output is scrubbed and its pushes are scanned. The work it returns is capped in size and accepted only for the handle that asked for it. What it claims about its own runtime is checked by a probe that the image can't replace.
- Credentials stay on the hub's side of the boundary. Guards on the hub
can use a forge token, a cluster credential or a Claude token on a
sandbox's behalf. The guards are the git proxy and the Kubernetes monitor,
which
cloop hub bootstrapturns on, and the opt-in model relay. A knowledge source's credential never leaves the hub at all. Behind a guard, a sandbox holds only a session token that is good for one run's traffic through that guard, and a revocation ends it at the next request. While a guard is off, a grant with a limit that only the guard can enforce is refused or narrowed to read-only, unless an administrator records on the grant why it may go out without that limit. - Claimed capabilities are checked. The isolation probe checks a runtime's claimed isolation, and the hub refuses a claim that the evidence contradicts. Image tags are resolved to digests, and signatures are verified where the policy asks for them. Release artifacts are verified against the workflow that must have signed them. Devices report their build, and the hub refuses builds below a minimum.
- Refuse instead of degrading. A driver that can't honour a spec field refuses the workload. A guard that is configured but not running refuses its traffic and never hands the credential over instead. A grant constraint that can't be parsed or satisfied, or a payload that minimises to nothing, produces a denial and an audit row, never a wider credential.
- Deny by default, and say so where the default is open. With RBAC in
force, an identity that matches no role binding gets role
none.cloop hub bootstrapwrites thedefault_role: nonethat puts RBAC in force. A route without a declared permission stops the hub at startup. A listener with no sign-in in front of it binds to loopback unless an operator explicitly configures otherwise. One configuration stays wide open: single sign-on without a role policy, which is allowed so that an upgrade doesn't lock anyone out. The startup banner,cloop hub doctor, Settings and/api/meall report it as open, next to a one-click fix. - Every important decision is recorded. Authorisation outcomes, grants, leases, guard decisions and fleet changes are written as rows of a hash-chained audit trail. The head of the chain is checkpointed outside the database, and the trail can be forwarded off the machine as it is written.
- Everything has a limit. Request rates, WebSocket connections, request bodies, log replay, returned work, file reads, the write-ahead log, disk use and retention all have ceilings, because any unbounded resource would let a single tenant deny service to everyone else.
Trust boundaries¶
The security model numbers the four boundaries between the hub and what it controls, and the threat model goes through each one with STRIDE. The table lists those four and the boundaries that cut across them:
| Boundary | Far side | How the far side proves who it is | The hub assumes it can | Detail |
|---|---|---|---|---|
| ① Caller ↔ hub | browsers, terminals, CI jobs, SCIM clients | an SSO session, an API token, a terminal session, the static token, a federated CI token or a SCIM credential | replay a stolen cookie, forge cross-site requests, rebind DNS | ① Browser ↔ hub |
| ② Hub ↔ device agent | enrolled devices | a single-use enrollment token, exchanged for an agent credential; a device enrolled with the hub's key pins it | be compromised, lie about its runtime or build, or send back more than it was asked for | ② Hub ↔ remote agent |
| ③ Hub ↔ container runtime | the local docker or podman socket | nothing: the socket is equivalent to root, and the hardening is in the argv the hub builds | run whatever the argv says | ③ Hub ↔ container runtime |
| ④ Hub ↔ Kubernetes | a cluster's API server | the brokered kubeconfig or projected ServiceAccount token | run other workloads beside the Pod, and let a workload reach other services in the cluster | ④ Hub ↔ Kubernetes cluster |
| Sandbox ↔ guards | every workload | session tokens minted per run and per guard | hold the token, misuse it within its scope, and try to exfiltrate data through it | credential guards |
| Hub ↔ hub | other members of a cluster | an HMAC over each forwarded request, keyed by a secret in the shared database | nothing a member couldn't already do, since anyone who can read the key can also rewrite the database the hub trusts; the owning member still re-checks the original caller | hub members |
| Hub ↔ executor controller | a controller in a cluster the hub need not reach, and the runner Pods it starts | a pool credential, kept as a hash; each runner a single-use runner token, then a credential of its own kept in memory | ask for runners up to the pool's max_runners, see what the runs handed to its runners bring with them, and tamper with those runs, as a compromised device could |
executor pools |
| Hub ↔ forwarder | on-prem machines wired to hardware | a forwarder enrollment token, exchanged for a credential; pins the hub like an agent does | be compromised | device forwarder |
| Hub ↔ services | IdP, GitHub, model API, knowledge sources, KMS, SIEM | TLS to the service, and the service's own signing keys where it signs | answer slowly or not at all, or return text that tries to instruct an agent | knowledge sources, SCIM |
Identity and access¶
Every request to the hub goes through the same layers, outermost first
(buildHandler in pkg/ui/server.go):
flowchart LR
subgraph connection["Connection"]
direction TB
listen["Listener<br/>loopback unless sign-in<br/>or acknowledged"]
tls["TLS 1.2 or later<br/>ECDHE and AEAD only"]
listen --> tls
end
subgraph early["Before identity"]
direction TB
recover["Request ID<br/>panic recovery"]
peer["Peer signature<br/>on forwarded requests"]
limit["Rate limit<br/>per address"]
hostg["Host guard<br/>421 on an open hub"]
origin["Origin guard<br/>403 cross-site, 415 not JSON"]
recover --> peer --> limit --> hostg --> origin
end
subgraph identity["Identity and permission"]
direction TB
who["Authentication<br/>session, token, static token"]
gate["Route gate<br/>permission, claims,<br/>recent sign-in"]
handler["Handler<br/>quota, then the work"]
who --> gate --> handler
end
trail[("Audit trail")]
connection --> early --> identity
identity -.-> trail
The health probes skip every layer after panic recovery. An edge agent, a pool runner or a forwarder that connects with its own credential, an executor controller presenting its pool credential, and a member's signed call to the cluster's internal endpoints skip session authentication and the route gate, because each of them authenticates itself.
Who a caller can be.
| Credential | Shape | Issued by | Kept as | Ends |
|---|---|---|---|---|
| Dashboard session | cloop_session cookie: HttpOnly; Secure and SameSite=Strict under TLS |
OIDC sign-in, authorization code with PKCE (pkg/oidcauth) |
a hash, with the IdP refresh token sealed (pkg/sessionstore) |
idle timeout (8 h), absolute ceiling (24 h), sign-out, or revocation by an admin or the IdP, which also closes every stream the session opened |
| API token | cloop_pat_<id>_<secret> |
a person or an admin, scoped and expiring (pkg/apitoken) |
a salted HMAC | expiry or revocation |
| Terminal session | cloop_cli_… |
cloop login (pkg/hubclient): a device code approved from a signed-in browser, issued by pkg/oidcauth |
a hash | its own lifetime and idle timeout, sign-out or revocation |
| Static admin token | CLOOP_UI_TOKEN |
the operator | never stored; identified by a fingerprint (pkg/statictoken) |
retired at runtime on every member, which closes everything it opened |
| Glasses link | a cloop_pat_ token named Display glasses link, carried in the URL |
a person, for their own display glasses | a salted HMAC | 30 days, rotation, revocation or offboarding |
| CI session | cloop_ci_… |
the hub, for a GitHub Actions OIDC token that matched an allowlist rule; checked by pkg/ciauth and pkg/celmatch, minted by pkg/claudeproxy |
a hash | the session's lifetime, or revocation |
| SCIM credential | cloop_scim_… |
an admin, for the IdP's provisioning client (pkg/provision) |
a salted HMAC | revocation; rotated with an overlap |
| Agent enrollment, credential | clet1.…, then clac1.… |
cloop executor enroll, then the first connection |
hashes | single use; revocation |
| Forwarder enrollment, credential | clft1…, then clfc1… |
cloop forward enroll, then the first connection |
hashes | single use; revocation |
| Executor pool credential | cloop_pool1_<id>.<secret> |
an admin, when a pool is created or its credential rotated (pkg/executorpool) |
a SHA-256 | revocation of the credential or of its pool |
| Runner token, credential | clrt1.…, then clrc1.… |
the hub, at a pool controller's request, then the runner's first connection | hashes | single use within ten minutes; the runner's retirement |
What a caller may do. The roles, from least to most privileged, are
none, viewer, operator, maintainer and admin. With RBAC in force, an
identity's role is decided per scope from three sources: role bindings in the
config, mapped from the IdP's claims; bindings written at runtime during an
incident (pkg/rolestore); and a project's own member list
(pkg/projectmember), whose members are people and teams (pkg/hubteam). A
project somebody creates is private: its owner is its administrator, and a role
held hub-wide reaches it only for the hub's administrators
(project members). RBAC is in force when single sign-on is on and an
operator has written a policy (a role mapping or a default_role). The request
gate and everything that reports on RBAC decide this with the same predicate,
authz.Enforced
(when RBAC is in force). Above
the operator role, two more conditions apply. The session's claims must have
been re-checked at the IdP within ui.oidc.max_claim_age_minutes (5 minutes by
default). A privileged route marked sudo also needs an interactive sign-in
within ui.oidc.sudo_max_age_minutes (2 hours); otherwise the hub answers
401 REAUTH_REQUIRED and the dashboard sends the person back to the IdP
(identity, roles and permissions).
How much. Quotas cap projects, concurrent tasks and reproductions,
executors and sessions, and set a daily token and cost budget per identity
(pkg/quota, pkg/quotastore). Spending counts against the identity that
started the run. With the model relay on, the relay counts it from the API's
own usage figures, so the figure doesn't depend on what the run reports.
Joining and leaving. A person gets an account by signing in or through SCIM
provisioning (pkg/scim, pkg/provision). They ask for access with grant
requests, which approvers see in the approvals inbox (pkg/approvalnotify).
They leave through offboarding (pkg/offboard), which ends their sessions,
tokens and glasses links, revokes their grants and leases, stops their tasks,
destroys their personal secrets and their Claude Code home, and writes a deny
binding. When a person is disabled at the identity provider, SCIM suspends them
in one request, without an administrator; once they are deleted there, they are
offboarded (SCIM provisioning).
What a page cannot do. The dashboard's content security policy is
script-src 'self', without 'unsafe-inline'. The markup contains no code,
only data-action names that a delegated dispatcher looks up. Requests that
change state need the hub's own origin or a bearer token, and a JSON body.
WebSocket connections need an exact origin match. Files a run produced are
served in a way that stops the browser from rendering any of them as a page of
the hub's origin (① Browser ↔ hub).
Secrets and keys¶
The key hierarchy. Each credential's payload is sealed (encrypted) with a data key of its own. The data key is sealed with a key-encryption key (KEK), and the KEK comes from a root key that the database never holds:
flowchart TB
root["Root key CLOOP_SECRET_KEY<br/>from a file, a systemd credential or the environment"]
kmsroot["Or a KMS root<br/>Kubernetes KMS v2 plugin, Vault or OpenBao Transit"]
escrow["Escrow<br/>the root wrapped under an operator passphrase"]
kek["KEKs in broker_keks<br/>derived from the root and a salt, or random and KMS-wrapped"]
dek["A data key per row<br/>random, never stored in plaintext"]
payload["Sealed payload<br/>AES-256-GCM, bound to its row"]
root --> kek
kmsroot --> kek
root -.-> escrow
kek --> dek --> payload
pkg/rootkeyis the only code that loads the root key. It tries, in this order, a file the process owns (mode 0400 or 0600, outside every.cloop/), a systemd credential (which is how a TPM-sealedLoadCredentialEncrypted=reaches the hub), and the environment.- A KEK is derived from the root key with a per-key salt (200,000 rounds of
SHA-256) and verified against a check value, so at load time the hub can tell
a wrong root key from a corrupt row. With
secrets.kms, each KEK is random and only the KMS's ciphertext of it is stored (pkg/kms). The KMS sees the 32-byte KEK but never a secret, and the store can't be opened without the KMS. - Both layers bind their ciphertext to its row with GCM associated data. A sealed payload copied onto another row fails to open, so it can't deliver someone else's credential under the wrong name.
- Rotation re-wraps the data keys and never touches the payloads, so it runs
online while the hub is serving. Changing the root key runs online too.
pkg/keyescrowkeeps a copy of the root key under a passphrase, so losing the root key file doesn't mean losing every sealed credential (key rotation, the root key).
From a secret to a run. A secret belongs to the organisation or, on a
multi-user hub, to one person. A grant gives a secret to a subject (a
project, an executor, or every executor whose recorded labels match a
selector), with constraints that depend on the secret's kind and a TTL. When a
run is dispatched, the broker turns the matching grants into one lease for
that run. The lease is short-lived, and it holds each secret cut down to what
the grants allow. It is renewed while the run lasts, and every renewal
recomputes it from the current grants, so a revoked grant is never issued
again. Everything the workload receives comes from the lease, and revoking the
lease takes it back at once: the hub sends a device a revoke frame, wipes the
files a container reads, empties a Pod's lease Secret, and ends the guard
sessions that depend on the lease. Values that went into a process's
environment can't be taken back, so a container or Pod holding some is
stopped, and the revocation report says so
(the lease-revocation guarantee).
| Kind | What it is | How it reaches a run |
|---|---|---|
github_pat |
a GitHub token | held by the git proxy when it runs; otherwise a file that a credential helper reads, answering only for the granted repositories on the grant's forge host |
github_app |
an App installation: app id, installation id, private key | the private key never leaves the hub; each lease gets an installation token scoped to the grant, renewed before its hour is up and held by the git proxy when the proxy runs |
kubeconfig |
a cluster credential | held by the Kubernetes monitor when it runs; otherwise rewritten to the allowed contexts and namespaces |
registry |
a registry login | a docker config filtered to the allowed registries, as DOCKER_CONFIG |
env |
opaque variables | the environment, filtered to the allowed keys and narrowed again by the sandbox spec's allowlist |
egress_proxy |
an outbound proxy, possibly with credentials | HTTPS_PROXY and HTTP_PROXY; the executor's network policy enforces its host allowlist |
local_repo |
a directory of repositories on the hub | a bind mount of the matching repositories, read-only unless granted otherwise |
host_device |
device nodes on the executor's host | --device |
host_interface |
network interfaces on the executor's host | moved into the sandbox's network namespace for the run |
Values a run was given are scrubbed from its output by exact match
(pkg/redact). Where the exact values aren't known, text that matches a
credential shape from a single pattern registry is scrubbed instead;
credential patterns lists every pattern.
Credentials delivered as files are destroyed when the run ends, and a sweep at
startup destroys any that a crash left behind (pkg/securewipe).
Sandbox containment¶
Isolation. Each driver declares the isolation it provides, and a probe checks the claim.
| Driver | Isolation | Kernel of its own | Notes |
|---|---|---|---|
localprocess |
none |
no | only where allow_host_process is true; never on a strict hub |
container |
container, or vm with Kata |
with gVisor or Kata | docker or podman; --cap-drop=ALL, no-new-privileges, a read-only root filesystem, the uid of the project directory's owner, no network unless egress is arranged, and memory, CPU and process limits |
remote |
remote |
depends on the device's runtime | an enrolled device that runs work directly, in a container runtime of its own, or in virtual executors, each with its own engine, runtime and firewall |
kubernetes |
remote |
with a gVisor or Kata RuntimeClass | one Pod per run, running as non-root with a read-only root filesystem and no service account token; its own NetworkPolicy when egress is filtered |
A project can require a VM or a kernel of its own, and placement doesn't accept
a runtime's name as proof. From inside a sandbox, cloop isolation-probe
reads what only the kernel serving the sandbox can answer: its boot ID, uptime,
hypervisor flag, DMI data and gVisor's markers. A container or device executor
mounts the probe read-only from its own binary, so an image can't replace it.
Kubernetes can't bind a file into a Pod, so there the probe is the cloop that
ships in the executor's own configured image. A claim the evidence contradicts
is refused under the isolation_evidence constraint. An executor that was
never probed is admitted with a warning on the run, unless
executors.isolation_evidence is required (pkg/isolationprobe,
pkg/isolationverify).
Images. sandbox.image_policy lists the registries and repositories that
are allowed. It can require a digest and a cosign signature (by key or by
keyless identity), and it pins each image it admits to a digest
(pkg/imagepolicy). Before a sandbox runs one of cloop's own images, the hub
verifies it by digest against the workflow that publishes it. By default an
unverified image is flagged on the run; under
executors.image_provenance: required it is refused. An image signed by
anyone other than that workflow is refused in both modes. On Kubernetes, a tag
is resolved to a digest at the registry before the Pod is built, so the Pod
runs the image the policy evaluated. If the registry won't resolve a tag, the
image is refused under require_digest or require_signature, and otherwise
admitted with a warning.
Network. By default a container has no network at all. When it is given
egress, it gets one of two mechanisms, depending on the kind of authorisation.
Either it joins an --internal network with no route off the host, where the
hub's egress proxy is the only way out, or, if it must connect to named
addresses directly, it gets a host nftables ruleset scoped to its bridge,
installed before the workload starts. A Pod gets its own NetworkPolicy.
Firewall rules nest. A device's rule set covers everything any sandbox on it
may reach, a virtual executor's rules can only narrow the device's, and a
project's rules can only narrow those (pkg/fwpolicy). No allow list can
include a cloud metadata service by accident (pkg/cloudmeta). A refused
connection fails at once unless the rules say to drop it, and it is reported to
the run with the rule that refused it. There are two exceptions: Kubernetes,
where a NetworkPolicy keeps no log, and devices older than protocol v24
(pkg/netfilter, pkg/denylog; firewall rules,
sandbox network isolation).
Hardware. A sandbox gets device nodes and network interfaces only through
a grant (host_device, host_interface), through a virtual executor that an
administrator gave them to, or through a forwarder binding. A path in the
sandbox spec can't give it hardware, because the spec is committed to the
repository and any pull request can change it. On-prem hardware reaches a cloud
sandbox through a device forwarder, attached
by the executor that runs the sandbox, with no listener anywhere.
Resources and reuse. CPU, memory, process count and disk are limited per
workload, within ceilings an operator sets per executor and per project. Disk
use is sampled and held to its limit (pkg/executor/internal/diskwatch). A
container or Kubernetes executor creates a new container or Pod for every
workload and removes it when the workload ends. An executor pool's runner takes
one run and is retired, and the idle runners a pool keeps ready have run
nothing yet. No sandbox is reused, because a reused sandbox would carry
whatever the previous tenant left in it. The one thing a pool can carry from
run to run is a runner volume, and only between runs of the same project
(runner volumes).
The agent's own privileges. An agent that installs firewalls needs
CAP_NET_ADMIN. cloop clears ambient and inheritable capabilities at startup
and grants that capability only to nft (pkg/caps), so no workload inherits
it.
Credential guards¶
The hub process serves every guard itself, and a sandbox reaches each one
through its firewall. The git proxy and the Kubernetes monitor have listeners
of their own, with TLS certified by the hub's own CA under tls: auto
(pkg/hubca, which can be rotated online). The model relay uses the git
proxy's listener by default, the egress proxy has its own, and the run
knowledge search uses the dashboard's. Each guard accepts a session token
minted for one run, checks the request against that run's policy, and only
then adds a credential that the sandbox never sees:
flowchart LR
subgraph sbxside["Sandbox: tokens"]
git["git<br/>cloop_git1_ password"]
kubectl["kubectl<br/>cloop_kube1_ token"]
harness["harness<br/>cloop_relay_ token"]
clients["HTTP clients<br/>cloop_egress1_ proxy password"]
searcher["knowledge search<br/>cloop_know1_ token"]
end
subgraph held["Hub: credentials"]
gp["Git proxy<br/>policy per ref, push scan"]
kg["Kubernetes monitor<br/>verb, resource, namespace"]
mr["Model relay<br/>model allowlist, budget"]
ep["Egress proxy<br/>host allowlist, quota"]
ks["Run knowledge search<br/>the project's sources only"]
end
git --> gp --> forgesvc["GitHub"]
kubectl --> kg --> apiserver["Kubernetes API"]
harness --> mr --> llm["Model API"]
clients --> ep --> internet["Internet"]
searcher --> ks --> index[("Knowledge index")]
style held fill:#4674d126,stroke:#4674d1
style sbxside fill:#cc339926,stroke:#c39
| Guard | Decides | Code | Detail |
|---|---|---|---|
| Git proxy | each pushed ref, against the branch allowlist, read-only access and the repository scope; refuses a push that contains a value the run was given | pkg/gitproxy, pkg/executor/gitproxycreds, pkg/forgehost |
git interception proxy, git proxy architecture |
| Kubernetes monitor | each API request, parsed into verb, group, resource, subresource, namespace and name before the cluster credential is added; shell-like subresources are refused for every verb | pkg/kubeguard, pkg/kubeshare |
Kubernetes access monitor |
| Egress proxy | each connection against its egress grant's hosts, addresses, ports and methods, and its byte quotas | pkg/egressbroker |
scoped network egress |
| Model relay | each model call, against the project's model allowlist and the daily budget of the person who started the run; usage is counted from the API's own figures | pkg/claudeproxy, pkg/ui/model_relay.go |
the model relay |
| Run knowledge search | only the sources bound to the run's project are searched; retrieved text is fenced and cited | pkg/knowledgerun, pkg/knowledge |
knowledge sources |
| CI relay | a federated pipeline's model calls, against the rule that admitted it | pkg/ciauth, pkg/claudeproxy |
running an agent in CI |
What the guards have in common:
- A session lasts no longer than its run. Git proxy, Kubernetes monitor and model relay sessions depend on the run's secret lease: they are extended with it and end with it. Egress and knowledge sessions are renewed while the run is alive and closed when it ends. If you revoke the grant behind a session, its next request is refused.
- Sessions survive a hub restart. They are recorded by hash in
proxy_sessions(pkg/sessionrecord). The member that adopts a run after a restart restores its sessions along with its lease, and derives the credential again from the grant, never from anything recorded. - They fail closed. A guard that is configured but not running refuses the traffic. It never falls back to giving the credential to the sandbox.
- They are audited and metered. Their audit actions include
gitproxy.push_denied,gitproxy.push_secret_refused,kubeguard.request_denied,egress.connect,model_relay.deniedandknowledge.run.search; see metrics for what they measure.
Where no guard runs, the credential itself is leased into the sandbox. It is
still scoped, minimised, revocable and scrubbed from output, but a grant whose
limits only a guard can enforce is not delivered as if those limits applied.
Without the git proxy, a GitHub branch allowlist turns a github_app grant
into a read-only token and refuses a github_pat, which can't be narrowed.
Without the Kubernetes monitor, a kubeconfig grant with namespace or verb
limits is refused unless an administrator records on the grant why it may go
out without them, which is audited as
secret.unguarded_ack.
Transport and the agent channel¶
- TLS settings come from one place (
pkg/tlsconf) for the dashboard,cloop serveand the monitors' listeners: TLS 1.2 or later, with ECDHE key exchange and AEAD ciphers only. - Devices pin the hub where they can. Whenever the hub serves TLS itself,
enrollment carries the hub's SPKI fingerprint. An agent or forwarder enrolled
with it checks the pin on top of the system certificate store, and several
pins can be staged for a rotation. A device enrolled with no pin, behind a
proxy that terminates TLS, verifies against the system store. Plaintext
ws://to a host other than loopback is refused unless explicitly allowed. - The agent protocol is versioned. The hub speaks versions 1 to 28 and
stamps each frame with the version the session negotiated. The hub won't
place a run on a device that is too old for what the run needs, such as a
revokeframe for revocable material. The run is refused at placement with an error that names the device and the upgrade it needs, so it doesn't fail later on. - What a device sends back is limited. A handle accepts only the bundle that its own dispatch asked for, up to that dispatch's cap, within a budget per executor and per hub process (256 MiB and 1 GiB by default), and nothing after its final status (② Hub ↔ remote agent).
- Members sign what they forward with an HMAC that covers the sender, the recipient, the time and a nonce. The member that owns the request authenticates the original caller again and doesn't rely on the forwarding member (the peer channel).
- The hub CA issues short-lived certificates for the git proxy's and the
Kubernetes monitor's listeners under
tls: auto. It is rotated online in three stages (staged, activated, retired), and there is an emergency path (rotating the hub CA).
Supply chain¶
- Releases are verified by who built them.
release.ymlsigns the release archives keylessly when a tag is pushed. The installer,cloop upgradeand an agent's upgrade check that signing identity with cosign (pkg/provenance); they don't rely on a checksum published next to the archive. If cosign is missing, they refuse to install (release provenance). - The edge channel has its own pin.
edge.ymlsigns builds ofmain, and they are verified against that identity only. Each build carries its first-parent sequence number onmain, and a device's root helper refuses a build older than the installed one, whatever the agent or the hub asks for (edge channel). - cloop's images are signed per digest when they are published, and the
hub verifies cloop's own images by digest before a sandbox runs one.
executors.image_provenancedecides whether an unverified image is flagged on the run or refused. - Each build records what it is.
pkg/versionholds the commit, the sequence number and the table of published releases with the agent protocol each one speaks. The Upgrade dialog and the auto-update policy (pkg/executor/autoupdate) read it before they offer a build to a device.
Audit and evidence¶
- One chain per database. The hub's
state.dband every project's have anaudit_eventstable whose rows are hash-chained. Each row's hash covers its own content and the previous row's hash, so editing or deleting a row breaks the chain from that point on (pkg/statedb).cloop hub audit verifychecks the chains, andpkg/auditmergereads them as one trail. - One list of action names. Every action name the hub can write is
registered in
pkg/auditaction, and the audit events reference is generated from that registry. A page that cites an action that doesn't exist fails the build. - Lost rows are recorded. If an append fails, the hub counts it and, once
the database accepts writes again, writes an
audit.gaprow into the chain. - Truncation can be detected. Every five minutes by default, the leader
writes each chain's head to a checkpoint outside the database
(
pkg/auditcheckpoint). Pruning seals what it removes (pkg/auditretention). - The trail can leave the machine as it is written. Once
audit.forwardnames a sink, the leader sends every row of every chain to syslog over TLS, to an HTTPS endpoint such as Splunk's, or to a file. Delivery is at least once, from a cursor per sink that a new leader resumes from. If a chain no longer lines up with a sink's cursor, that is recorded too, asaudit.forward.discontinuity(pkg/auditforward, forwarding the trail). - Health is checked.
/metricsexports the hub's metric families (pkg/hubmetrics).cloop hub doctorand the dashboard's health panel check the deployment's configuration, andcloop hub doctor --smokechecks its dispatch path end to end (pkg/hubdoctor). Front-end telemetry is off by default and is scrubbed before it is stored (pkg/telemetry).
Availability as a security property¶
- One control plane, with one leader. A second
cloop uion the same database joins the cluster instead of running as a separate hub, and one member at a time holds the control-plane lease (pkg/hublease,pkg/hubcluster). - Runs outlive the process that started them. When a member dies, another member adopts its runs, agent connections, leases and guard sessions (hub cluster). If a member dies after starting a workload but before recording it, the dispatch intent it wrote first lets the next member find the workload and stop it, so it doesn't run unsupervised.
- A workload can't take down the fleet one node at a time. Failover is
capped (
executors.failover.max_attempts), and a task that more than one executor died under is quarantined until someone resets it (the failover cap). - A full disk pauses runs instead of corrupting data. Runs pause when free
space falls below a minimum and record their outcome using space held in
reserve (
pkg/diskreserve). The write-ahead log is trimmed and truncated, and the janitor and the leader's retention sweep prune what grows (disk retention). - Abuse is limited at the front door: a token bucket per address, caps on WebSocket connections per address and in total, limits on request body size, and panic recovery around every handler.
Component map¶
This section lists every package under pkg/, grouped by layer, with what
each one is for.
Entry points¶
| Package | Role |
|---|---|
cmd |
the Cobra command tree behind main.go, grouped in cloop --help; most commands are thin wrappers over a package below |
cmd/cloop-forward |
the standalone device forwarder binary |
Serving the hub¶
| Package | Role |
|---|---|
pkg/ui |
the hub: route table, handlers, dashboard assets, WebSockets, run dispatch, the guards' listeners, leader duties |
pkg/apiserver |
cloop serve, the REST API without the dashboard |
pkg/apidoc |
the HTTP API reference and GET /api/openapi.json, rendered from the route tables |
pkg/apierror |
the structured error both HTTP surfaces answer with |
pkg/jsonbody |
reading a JSON request body; every handler uses it |
pkg/sameorigin |
whether a request came from one of the hub's own pages, and which forwarded headers to trust |
pkg/exposure |
which address the hub listens on; refuses to put a hub without sign-in on the network |
pkg/reqid |
request correlation IDs through handlers, the orchestrator and providers |
pkg/clijson |
framing JSON output so it can still be read from a stream that also carries diagnostics |
pkg/multiui |
the registry of projects the dashboard serves |
pkg/telemetry |
the model for browser diagnostic events |
pkg/stt |
speech-to-text for dictation |
Identity and access¶
| Package | Role |
|---|---|
pkg/oidcauth |
OIDC relying party: authorization code with PKCE, ID-token validation, session cookies, terminal sign-in, sudo |
pkg/sessionstore |
durable dashboard sessions; the only credential a session holds is stored sealed |
pkg/apitoken |
scoped, expiring, individually revocable API tokens |
pkg/statictoken |
the static admin token: identified without being stored, and retirable |
pkg/hubclient |
the terminal's side of cloop login, cloop remote and cloop whoami |
pkg/ciauth |
federating a CI pipeline's OIDC token into a hub session |
pkg/celmatch |
the strict CEL subset pipeline rules are written in |
pkg/scim |
the hub's implementation of SCIM 2.0 |
pkg/provision |
what the hub does when a user is provisioned, suspended or deleted |
pkg/offboard |
offboarding in one operation |
pkg/authz |
roles, permissions, scopes, bindings and the enforcement predicate |
pkg/rolestore |
role bindings written at runtime |
pkg/projectmember |
a project's members, people and teams, and the role each one holds |
pkg/hubteam |
the hub's teams: named groups of people, optionally following an IdP or SCIM group, that projects are shared with |
pkg/projectconfine |
which directories may be a hub project |
pkg/quota |
quotas and admission |
pkg/quotastore |
per-identity quotas and overrides in the hub database |
pkg/approvalnotify |
telling approvers that a request or a token is waiting for them |
pkg/claudecodeauth |
Claude Code sign-in per identity, driven from the browser |
pkg/copilotauth |
keeping each person's Copilot CLI separate from everyone else's |
State and persistence¶
| Package | Role |
|---|---|
pkg/statedb |
the SQLite schema and its stores: migrations, audit chains, proxy sessions, WAL bounds |
pkg/state |
loading and saving a project's state, with external edits merged |
pkg/pm |
the plan and task model |
pkg/taskqueue |
the record of every unit of work |
pkg/config |
.cloop/config.yaml, its per-instance overlays, defaults and bounds |
pkg/configdiff |
drift between the YAML file and its database mirror |
pkg/configvalidate |
schema and semantic validation of config and state |
pkg/atomicfile |
crash-safe writes |
pkg/artifact |
per-task output artifacts |
pkg/attachments |
files attached to a task, accessed only through the attachment store's directory |
pkg/eventlog |
tailing, replaying and verifying the audit log |
pkg/dbbackup |
hot backups and restores |
pkg/dbmaintain |
VACUUM and ANALYZE |
pkg/dbverify |
SQLite integrity checks |
pkg/janitor |
automatic retention of .cloop |
pkg/diskusage |
what .cloop costs on disk, by producer |
pkg/diskreserve |
reserved disk space that lets a run record its outcome when the disk is full |
pkg/boundedread |
size-capped reads of files a run wrote |
pkg/logtail |
reading the last lines of a log without reading the whole file |
pkg/migrate |
upgrading and repairing a .cloop directory |
pkg/archive |
the task archive |
pkg/snapshot |
full .cloop backup and restore |
Running the control plane¶
| Package | Role |
|---|---|
pkg/hublease |
the control-plane lease, which confines leader work to one member |
pkg/hubcluster |
membership, leadership, the event bus, ownership and the peer channel |
pkg/hubdoctor |
diagnosing a deployed control plane |
pkg/hubmetrics |
the hub's metrics registry |
pkg/version |
this build's identity and the published release table |
pkg/runprobe |
tells a process that isn't a hub whether a run is executing |
pkg/taskrecover |
repairing tasks a dead run left in progress |
pkg/pausereason |
why a run paused, in machine-readable form |
pkg/runbuild |
a long-lived run moving to a newer build at a task boundary |
pkg/outlive |
keeping a run going after the control plane that started it stops |
pkg/upgrade |
self-update from verified releases |
pkg/provenance |
verifying a release artifact's signer |
The run¶
| Package | Role |
|---|---|
pkg/orchestrator |
the task loop: pick a task, run the harness, read the outcome, save it, and evolve the plan |
pkg/provider |
the provider interface, retries and circuit breaker; anthropic, openai, ollama, claudecode, copilot, mock, cached, fallback and agentproc beneath it |
pkg/router |
which provider a task role uses |
pkg/watchdog |
cancelling an in-flight task on demand |
pkg/procgroup |
process groups a harness leaves behind |
pkg/checkpoint |
mid-task checkpoints |
pkg/reviewgate |
the review gate: a second model checks a task's changes before they leave the working copy |
pkg/donecheck |
a task counts as done only once its work is committed |
pkg/requirements |
project requirements and their checks |
pkg/reqproof |
red-green proofs of requirement tests |
pkg/worktree |
a worktree per parallel task |
pkg/mergequeue |
serialising merges of task branches |
pkg/mergeresolve |
resolving merge conflicts with a model |
pkg/feature |
parallel features in one project |
pkg/featureops |
creating, removing and publishing a feature |
pkg/knowledgerun |
a run's access to its project's knowledge |
pkg/provideraudit |
the per-project log of provider calls |
pkg/cost |
pricing token usage |
pkg/budget |
a project's daily budget |
pkg/globalbudget |
the cross-project daily budget |
pkg/ratelimit |
Anthropic rate-limit headers and Claude Code subscription usage |
pkg/secret |
the project's own encrypted secrets file |
pkg/env |
per-project environment variables |
pkg/hooks |
shell hooks before and after tasks and plans |
pkg/condition |
conditions that decide whether a task runs |
pkg/approvalgate |
human approval before critical tasks |
pkg/multiagent |
specialist sub-agents per task |
pkg/consensus |
multi-model voting on critical tasks |
pkg/git |
git helpers for task branches and commits |
pkg/github |
a small GitHub REST client |
pkg/gitremote |
reading a remote as the GitHub repository it names |
pkg/notify |
desktop and chat notifications |
pkg/webhook |
event webhooks |
pkg/alert |
threshold alerts on a plan |
pkg/metrics |
run metrics export |
pkg/tracing |
OpenTelemetry spans |
pkg/logger |
structured logging |
pkg/memory |
session learning carried between runs |
pkg/learning |
cross-session lessons distilled from a plan |
pkg/promptstats |
which prompts correlate with success |
pkg/promptopt |
prompt A/B testing |
pkg/replay |
the structured step log |
pkg/cache |
a disk cache of provider responses |
pkg/mcp |
cloop mcp: the plan as Model Context Protocol tools, and the knowledge search a run's agent is given |
Between tasks, the loop applies per-task context overrides (pkg/ctxedit) and
raises the priority of tasks whose deadlines are near (pkg/promote). It also
calls helpers that use a model: pkg/clarify, pkg/coach, pkg/diagnosis,
pkg/docs, pkg/eval, pkg/optimizer, pkg/review, pkg/risk and
pkg/verify.
Execution¶
| Package | Role |
|---|---|
pkg/executor |
the Executor interface, registry, binding, placement, host-execution policy, supervision and failover |
pkg/executor/localprocess |
the host driver |
pkg/executor/container |
the docker and podman driver |
pkg/executor/remote |
the hub's side of enrolled devices, and the agent protocol |
pkg/executor/kubernetes |
the Pod driver |
pkg/executor/agent |
the device agent |
pkg/executor/install |
installing the agent as a hardened service, with its upgrade helper |
pkg/executor/autoupdate |
which devices roll forward, and when |
pkg/executor/reconcile |
turning the executors section of config into registered executors |
pkg/executor/gitprovision |
setting up the source tree where the workload runs |
pkg/executor/gitwriteback |
turning what a harness changed into something the hub can merge |
pkg/executor/gitcreds |
leasing the credential a workspace fetch needs |
pkg/executor/gitproxycreds |
putting the git proxy between a sandbox and the forge |
pkg/executor/projectseed |
carrying a project into a sandbox that fetched its own source |
pkg/executor/resultframe |
carrying a seeded run's outcome out through its output stream |
pkg/executor/featurehub |
the hub's half of a feature on an isolating executor |
pkg/executorstore |
enrollment, agent credentials and handles in the database |
pkg/executorpool |
executor pools: their controllers' credentials, runner tokens, and which queued runs a pool may see and take |
pkg/kubecontroller |
cloop executor controller: the in-cluster controller that keeps an ExecutorPool's ephemeral runner Pods, scaled by the hub's demand feed, and the commands that install it |
deploy/helm |
the executor controller's Helm chart, compiled into the binary so a hub serves its own build's chart to helm install |
pkg/sandbox |
parsing and validating .cloop/sandbox.yaml |
pkg/hostsandbox |
keeping a person's runs on the hub's own host to their own files (Landlock, namespaces) |
pkg/imagepolicy |
which images may run, pinned to a digest |
pkg/isolationprobe |
what a sandbox's kernel says about itself, and the verdict |
pkg/isolationverify |
running the probe under each executor's runtime |
pkg/writeback |
landing an isolated executor's work in the project's repository |
pkg/netfilter |
compiling an egress authorisation into an nftables ruleset |
pkg/fwpolicy |
firewall containment between levels |
pkg/cloudmeta |
the table of cloud metadata services |
pkg/denylog |
what a sandbox's firewall refused, per run |
pkg/caps |
giving a Linux capability only to the program that needs it |
pkg/securewipe |
destroying credential files and reporting whether it worked |
pkg/deviceforward |
the forwarder protocol, enrollment and the device side |
pkg/forwarderstore |
forwarder enrollment and bindings in the database |
Secrets and credential guards¶
| Package | Role |
|---|---|
pkg/secretstore |
the secret broker's SQLite backend |
pkg/secretbroker |
secrets, grants, leases, envelope encryption and the keyring |
pkg/rootkey |
resolving the root key |
pkg/kms |
KEKs held by a KMS |
pkg/keyescrow |
the root wrapped under an operator passphrase |
pkg/security |
the HMAC key that lets the hub refuse a tampered agent or forwarder enrollment token from its format alone, before any lookup |
pkg/redact |
scrubbing known values and credential shapes from text |
pkg/sessionrecord |
durable records of the guards' sessions |
pkg/hubca |
the hub's CA and the short-lived certificates of its guards |
pkg/tlsconf |
server TLS parameters and SPKI pinning |
pkg/gitproxy |
the git smart-HTTP interception proxy |
pkg/forgehost |
which forge host a repository reference names |
pkg/kubeguard |
the Kubernetes API monitor |
pkg/kubeshare |
a cluster shared through a least-privilege ServiceAccount |
pkg/kubeexec |
running a kubeconfig's exec plugin on the developer's machine |
pkg/egressbroker |
the egress proxy and its leases |
pkg/claudeproxy |
the model relay's engine, for CI pipelines and runs |
Knowledge¶
| Package | Role |
|---|---|
pkg/knowledge |
sources, bindings, sync, the chunk index and retrieval; Confluence and Jira connectors and embeddings beneath it |
pkg/kb |
a project's own knowledge base file |
Audit and observability¶
| Package | Role |
|---|---|
pkg/auditaction |
the registry of audit action names |
pkg/auditcheckpoint |
audit chain heads written outside the database |
pkg/auditexport |
the trail in the formats a SIEM consumes |
pkg/auditforward |
sending the trail off the hub as it is written |
pkg/auditmerge |
reading the hub's and the projects' chains as one |
pkg/auditretention |
sealing and pruning the trail |
pkg/audit |
cloop audit: a security and compliance scan of a project |
pkg/doctor |
cloop doctor: checking a developer's checkout |
pkg/integrations |
health of configured integrations |
pkg/chaos |
fault injection, to test resilience claims |
The rest of the CLI¶
The remaining packages are planning and analysis tools, reports, terminal UIs
and local workflow tools. Each has a thin command in cmd, and most of them
call a model and print or apply the result. None of them is on the hub's
dispatch path. Where the dashboard offers one that asks a model (Split,
Revise, the blocker analysis, the plan assistant, suggestions), the model call
runs as a workload on the project's executor, under the caller's own account.
Only the two inline replays run in the hub, and they are gated like host
execution
(model calls the dashboard makes).
pkg/acceptance, pkg/adr, pkg/agent, pkg/aipair, pkg/analyze,
pkg/ask, pkg/autodeps, pkg/backlog, pkg/batchedit, pkg/bench,
pkg/blocker, pkg/brief, pkg/calibrate, pkg/changelog, pkg/chat,
pkg/cipipe, pkg/commitmsg, pkg/compact, pkg/compare,
pkg/complexity, pkg/contextgraph, pkg/critique, pkg/daemon,
pkg/decompose, pkg/depseditor, pkg/epic, pkg/explain,
pkg/feedback, pkg/filewatch, pkg/finetune, pkg/flow,
pkg/forecast, pkg/githubsync, pkg/ical, pkg/impact,
pkg/importer, pkg/insights, pkg/journal, pkg/kanban,
pkg/linter, pkg/microstandup, pkg/milestone,
pkg/naming, pkg/narrative, pkg/nlcli, pkg/notebook,
pkg/notifywizard, pkg/onboard, pkg/perfprofile, pkg/pivot,
pkg/plandiff, pkg/planedit, pkg/planio, pkg/plugin, pkg/pr,
pkg/prioritize, pkg/profile, pkg/query, pkg/recipe,
pkg/release, pkg/reorder, pkg/report, pkg/retro,
pkg/riskmatrix, pkg/roadmap, pkg/scaffold, pkg/scope,
pkg/search, pkg/selfimprove, pkg/session, pkg/shell,
pkg/simulate, pkg/skill, pkg/sprint, pkg/standup,
pkg/suggest, pkg/summarize, pkg/sync, pkg/taskadd,
pkg/taskreplay, pkg/taskstats, pkg/taskwatch, pkg/tdd,
pkg/team, pkg/teamassign, pkg/template, pkg/testgen,
pkg/testrun, pkg/timeline, pkg/timetravel, pkg/trace,
pkg/tui, pkg/viz, pkg/voice, pkg/whatif, pkg/wizard and
pkg/workspace.
How the architecture is kept true¶
Besides each package's own tests, the suites below check the claims on this
page and the pages it links to. tests/arch and tests/docs read the source
without running it, as does the call-graph check in tests/security. The rest
run the real code:
| Suite | What it checks |
|---|---|
tests/security |
the threat model as tests: the call graph from every route and duty to os/exec, strict mode, that secrets are not disclosed, lease and grant revocation, redaction and push scanning, image trust and provenance, workspace provisioning and write-back, sealing keys, sessions, sudo and offboarding; together with the package suites, the rows of the guarantee → test table |
tests/arch |
structural rules, read with go/ast: no orphan packages, no writer that replaces rows, a single RBAC predicate, every production dispatch goes through the harness preflight, no nil resolver for a starter's account, and every SQLite open uses the shared connection policy |
tests/docs |
the documentation against the code: executor kinds, secret kinds, roles, permissions, workspace kinds, placement constraints and quota resources are all documented; the HTTP API, audit events and credential patterns references are regenerated; cited tests and actions exist; diagrams are legible in both colour schemes; every package and suite is named on this page |
tests/hermetic |
that no test reads the developer's real home |
tests/e2e |
the real binary end to end: hub restarts that adopt runs, features, the review gate, the model relay, egress and firewall denials in containers, terminal sign-in, requirement gates and proofs, knowledge sources against fake Atlassian servers |
tests/flagship |
the main end-to-end scenario: hub bootstrap, a container executor, a repository-scoped lease, write-back |
tests/cluster |
several hub members on one database: failover, revocation after a member dies, adopted credentials, a Pod's workspace session, a KMS and a change of root |
tests/kube |
the Kubernetes driver on a real cluster, with the git proxy, the monitor, image resolution and cluster shares |
tests/kubecontroller |
an executor pool on a real cluster, with the controller its Helm chart installs: scaling from zero and back, runner volumes kept for their project and wiped for any other, refused runner tokens, the controller's leader and runners killed mid-run, and a revoked controller credential |
tests/upgrade |
a rolling upgrade from the latest release to main on one database and one cluster, and the rollback |
tests/forwarder |
device forwarders with real USB/IP and TAP devices |
tests/soak |
concurrent tenants: nothing leaks from one tenant to another, and no containers, credentials or goroutines are left behind |
tests/ciscripts |
the scripts CI runs: changelog coverage, the failure annotations, release notes, the security-fix age, the upgrade rehearsal |
tests/ciworkflow |
the published CI workflow against a real hub and a real Claude Code, with GitHub Actions played by a shell |
make requirements-check checks cloop against its own requirements in
.cloop/requirements.yaml: its security and correctness guarantees, written as
requirements together with the tests that check
them.
Reading on¶
| To understand | Read |
|---|---|
| how a workload is placed, provisioned, supervised and failed over | executors |
| what the hub trusts and which test proves each claim | security model |
| what an attacker at each boundary can still do | threat model |
| how pushes are limited without the sandbox holding a forge token | git interception proxy, then git proxy architecture |
| how a sandbox reads one namespace of a cluster and nothing else | Kubernetes access monitor |
| how several hub processes serve one control plane | hub cluster |
| how credentials are minted, granted, leased and revoked | secrets and egress |
| how to run, back up, rotate and upgrade all of this | operator runbook |