statedb

package
v0.0.4 Latest Latest
Warning

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

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

Documentation

Overview

autoupdate.go stores the fleet's auto-update policy: whether the control plane rolls its executors forward on its own, which release they converge on, and how many move at once (Task 20331).

A sibling of executorsandbox.go and projectlimits.go — an operator's standing policy about a set of devices, held in the control plane, never readable by the devices it governs. The difference is that this one is fleet-wide rather than per-executor; see the migration for why.

Package statedb provides a SQLite-backed persistent store for cloop project state. It replaces the JSON flat-file store with a normalized schema that allows concurrent-safe writes, real SQL queries for reporting, and historical task rows.

Package statedb — typed sentinel errors.

HTTP handlers, the orchestrator, and CLI commands need to distinguish a handful of well-known failure modes from a generic "something went wrong". The Go convention is to expose package-level sentinel errors and have callers use errors.Is to test against them. This file is the authoritative list for cloop's relational data layer.

HTTP mapping (used by pkg/ui and pkg/apiserver via HTTPStatus below):

ErrTaskNotFound    → 404 Not Found
ErrProjectNotFound → 404 Not Found
ErrStaleVersion    → 409 Conflict
ErrDBLocked        → 503 Service Unavailable
ErrSchemaMismatch  → 500 Internal Server Error

All these errors are safe targets for errors.Is. Internal helpers wrap driver errors with %w so the sentinel survives across package boundaries.

executoraudience.go stores who may run work on one executor.

See migrations/0046_executor_audience.sql for why this is a table rather than a set of role bindings, and pkg/authz/audience.go for the matching semantics. This file is storage only: it does not know who is asking, and deliberately does not decide anything.

executorlimits.go stores the per-executor resource ceiling.

The sibling of projectlimits.go, field for field and failure for failure. The two exist separately because they answer questions that do not reduce to each other: a project's cap travels with the project wherever it is placed, and an executor's cap holds whatever lands on it. See migrations/0045_executor_resource_limits.sql for why neither the fleet ceiling nor the project ceiling can express the second one.

executorsandbox.go stores the per-executor sandbox configuration an admin sets from the UI: whether payloads run on the executor's host or in a container on it, and which engine, runtime, image and network that container uses.

The sibling of projectlimits.go and executors.go's project_executors. All three record an operator's policy about something rather than a fact belonging to it, which is why all three live in the control plane and none is readable — or writable — from the thing they constrain.

Maintenance helpers for the SQLite state database (Task 20107).

VACUUM reclaims space from rows previously deleted (cloop deletes step rows on `cloop compact`, prunes archived tasks, etc.). ANALYZE refreshes the query planner's statistics so growing tables continue to use sensible indexes. Both operations run as autocommit statements and acquire d.mu so no other goroutine in this process can interleave with them; cross-process contention is bounded by busy_timeout=5000.

Persistence: every successful VACUUM/ANALYZE run is appended to the `maintenance_log` table by AppendMaintenanceLog so:

  • cloop db maintain --auto can compare current page_count against the last vacuum's page_count_after to decide whether to skip,
  • cloop doctor can surface "last maintenance: 3d ago, freed 4.2 MB",
  • operators have a forensic trail when investigating disk-usage spikes.

Schema migration framework for statedb.

Replaces the prior "execute the entire CREATE TABLE IF NOT EXISTS blob on every Open" approach with versioned, append-only .sql files embedded into the binary. Each migration runs inside its own transaction and records itself in schema_migrations on success; a crash mid-migration rolls back cleanly so the next Open re-attempts from the failed version.

Lifecycle:

Open() ──► applyPragmas() ──► Migrate() ──► return *DB

Adding a new migration:

  1. Create pkg/statedb/migrations/NNNN_<slug>.sql where NNNN is the next unused 4-digit version (e.g. 0002_add_pinned.sql).
  2. Write idempotent-friendly DDL. Single-statement migrations are safest because the modernc.org/sqlite driver does not natively support executing multiple statements with the binding parameter API; the migration runner therefore splits on `;` boundaries before exec.
  3. NEVER edit a shipped migration file. Roll forward with another file.

Existing databases (pre-framework) are detected at boot: if all the 0001 tables already exist but schema_migrations is empty, version 1 is recorded as the baseline rather than re-applied. This makes the rollout safe for databases written by older binaries.

projectlimits.go stores the per-project resource ceiling.

The companion to executors.go's project_executors: both record an operator's policy *about* a project rather than a fact belonging to it, which is why both live in the control plane and neither is readable from the repository the project's developers control.

Package statedb — Store interface for the relational data layer. The DB struct (SQLite) implements Store. A future PostgreSQL driver need only implement this interface to be used as a drop-in replacement.

Index

Constants

View Source
const (
	// ExecutorSessionRunning: work is in flight on the executor.
	ExecutorSessionRunning = "running"
	// ExecutorSessionRequeued: a supervisor claimed the session for failover
	// and the work has been handed to another executor.
	ExecutorSessionRequeued = "requeued"
	// ExecutorSessionFinished: the work completed on this executor.
	ExecutorSessionFinished = "finished"
	// ExecutorSessionFailed: the work ended in failure and was not requeued.
	ExecutorSessionFailed = "failed"
)

Executor session states as persisted in the state column.

View Source
const (
	ExecutorStatusOnline   = "online"
	ExecutorStatusOffline  = "offline"
	ExecutorStatusDegraded = "degraded"
	ExecutorStatusUnknown  = "unknown"
)

Executor health states as persisted in the status column.

View Source
const (
	RoleEffectAllow = "allow"
	RoleEffectDeny  = "deny"
)

Effect values for RoleBindingRow.Effect.

View Source
const AuditGenesisHash = genesisHash

AuditGenesisHash is the prev_hash of a chain that has never been pruned. Exported so the sealer can start its own verification from the same place the verifier does.

View Source
const EnvAllowSchemaDowngrade = "CLOOP_ALLOW_SCHEMA_DOWNGRADE"

EnvAllowSchemaDowngrade names the operator opt-out. Set it to 1/true/yes/on to open a database whose schema is ahead of this binary's.

An environment variable rather than a config key on purpose: the check runs inside statedb.Open, which is reached from CLI commands, the orchestrator and the hub alike, several of them before any config file has been read. A knob that cannot be set in every context where the guard fires is not an opt-out.

View Source
const HubScope = "hub"

HubScope is the fence name this binary writes. See the migration for why the column exists at all when there is only ever one value.

View Source
const LatestVersion = 1 << 30

LatestVersion is the sentinel MigrateTo accepts to mean "every embedded migration". Any real schema version is far below it.

View Source
const NoStep = -1

NoStep is the EventRow.Step value for events that are not bound to any step. Steps are 0-based, so 0 is a real step number — callers recording a non-step-bound event must set Step to NoStep explicitly.

View Source
const ProviderCallBodiesPrunedKey = "bodies_pruned_at"

ProviderCallBodiesPrunedKey is the key under which a stripped provider-call row records when its bodies went, inside the headers JSON object.

Exported because pkg/ui reads it: the detail endpoint surfaces it so the panel can say "bodies pruned" instead of showing an empty prompt, and the replay endpoint refuses a call whose prompt no longer exists rather than sending an empty one to a provider.

View Source
const TelemetryMaxLimit = 1000

TelemetryMaxLimit bounds one page. Chosen so that a page of worst-case rows still marshals to a response a browser will render rather than choke on.

View Source
const TelemetryMaxRows = 50000

TelemetryMaxRows is the hard ceiling on stored events.

Enforced on the write path rather than by the hourly retention janitor, because the writer here is an endpoint that accepts unauthenticated POSTs: a bound that only holds once an hour is not a bound against the traffic this table is actually exposed to. At the field budgets pkg/telemetry enforces, the worst case is a few hundred megabytes, and the realistic case — the trails of a handful of sessions — is a few megabytes.

Variables

View Source
var (
	// ErrTaskNotFound indicates the requested task ID does not exist in the
	// project plan. Returned by lookup helpers in pkg/state and pkg/statedb.
	ErrTaskNotFound = errors.New("statedb: task not found")

	// ErrProjectNotFound indicates the working directory has no cloop project
	// (no state.db, no migratable state.json). Returned by state.Load.
	ErrProjectNotFound = errors.New("statedb: project not found")

	// ErrStaleVersion indicates an optimistic-concurrency check failed: the
	// caller's plan version no longer matches the on-disk version. Callers
	// should re-load and re-apply.
	ErrStaleVersion = errors.New("statedb: stale plan version")

	// ErrDBLocked indicates SQLite returned a busy/locked error that the
	// busy_timeout did not absorb (extreme contention or a long-running
	// writer in another process). Callers may retry the operation.
	ErrDBLocked = errors.New("statedb: database busy")

	// ErrSchemaMismatch indicates the on-disk schema is not what this binary
	// expects (corruption, partial migration, or a future binary's database
	// opened by an older one). Manual intervention is usually required.
	ErrSchemaMismatch = errors.New("statedb: schema mismatch")

	// ErrSchemaTooNew indicates the database has been migrated past the
	// highest migration this binary embeds — an older binary opened a
	// database a newer one moved forward.
	//
	// Distinct from ErrSchemaMismatch because the remedy is different: this
	// is fixed by running a different binary, not by repairing the database.
	// Errors carrying it also satisfy errors.Is for ErrSchemaMismatch, so
	// callers that only handle the general case keep working. The detail —
	// both versions, and which build applied the newer schema — is in
	// *SchemaTooNewError, reachable with errors.As.
	ErrSchemaTooNew = errors.New("statedb: database schema is newer than this binary")

	// ErrExecutorNotFound indicates the requested execution backend is not
	// registered in the executors table. Callers must treat this as fatal
	// for the operation rather than falling back to host execution — see
	// pkg/executor's fail-closed resolution policy.
	ErrExecutorNotFound = errors.New("statedb: executor not found")

	// ErrBrokerSecretNotFound indicates no broker_secrets row has the
	// requested ID. Like ErrExecutorNotFound this is fatal for the
	// operation: a caller must never substitute a different credential for
	// one it could not find.
	ErrBrokerSecretNotFound = errors.New("statedb: broker secret not found")

	// ErrBrokerGrantNotFound indicates no broker_grants row has the
	// requested ID.
	ErrBrokerGrantNotFound = errors.New("statedb: broker grant not found")

	// ErrGrantRequestNotFound indicates no secret_grant_requests row has the
	// requested ID (Task 20271). Distinct from ErrBrokerGrantNotFound because
	// the two answer different questions: an unknown grant means the
	// authorisation does not exist, while an unknown request means nobody ever
	// asked — and a decide call that confused them would report "already
	// decided" for an id that was simply mistyped.
	ErrGrantRequestNotFound = errors.New("statedb: grant request not found")

	// ErrEgressGrantNotFound indicates no egress_grants row has the
	// requested ID. Like the broker sentinels this is fatal for the
	// operation: a caller must never fall back to a different egress policy
	// than the one it asked for.
	ErrEgressGrantNotFound = errors.New("statedb: egress grant not found")

	// ErrExecutorHealthNotFound indicates the executor has no health record:
	// the scheduler has never probed it. Callers must not read this as
	// "healthy" — an unobserved executor is unknown, not ready.
	ErrExecutorHealthNotFound = errors.New("statedb: executor health not found")

	// ErrExecutorSessionNotFound indicates no executor_sessions row has the
	// requested ID.
	ErrExecutorSessionNotFound = errors.New("statedb: executor session not found")

	// ErrExecutorSessionClaimLost indicates a conditional requeue matched no
	// row: another supervisor already claimed the session, or it is no longer
	// running. This is the exactly-once signal, and it is a distinct error
	// because "someone else is failing this over" must never be retried as if
	// it were a transient fault — doing so is how the same work ends up
	// running twice.
	ErrExecutorSessionClaimLost = errors.New("statedb: executor session claim lost")

	// ErrAPITokenNotFound indicates no api_tokens row has the requested ID.
	//
	// On the verification path this is an ordinary outcome, not a fault: it
	// is what a forged or already-purged token id looks like. Callers must
	// answer it exactly as they answer a bad secret — an unauthenticated
	// caller must not be able to use the difference to learn which token ids
	// exist.
	ErrAPITokenNotFound = errors.New("statedb: api token not found")

	// ErrSessionNotFound indicates no sessions row has the requested id.
	//
	// Like ErrAPITokenNotFound this is an ordinary outcome on the
	// authentication path, not a fault: it is what a forged cookie, a cookie
	// from before a restart that wiped the database, and a session an
	// administrator just terminated all look like. All three must produce the
	// same answer to the caller.
	ErrSessionNotFound = errors.New("statedb: session not found")

	// ErrRoleBindingNotFound indicates no role_bindings row has the requested
	// id. Unlike the two above this is a genuine operator error rather than an
	// expected outcome — binding ids are only ever obtained by listing the
	// table — so callers should surface it instead of absorbing it.
	ErrRoleBindingNotFound = errors.New("statedb: role binding not found")

	// ErrCIPipelineRuleNotFound indicates no ci_pipeline_rules row carries the
	// requested id (Task 20278).
	//
	// Surfaced rather than absorbed. A rule id is only ever obtained from a
	// listing, so not finding one means the operator's view is stale — most
	// likely because a second maintainer deleted the same rule concurrently —
	// and a PUT that silently recreated it would resurrect an allowlist entry
	// somebody had just decided to remove.
	ErrCIPipelineRuleNotFound = errors.New("statedb: ci pipeline rule not found")
)

Sentinel errors. Add to this list — do not rename or reuse.

View Source
var (
	// ErrEnrollmentNotFound: no enrollment token with that ID.
	ErrEnrollmentNotFound = errors.New("statedb: enrollment token not found")
	// ErrAgentCredentialNotFound: no agent credential with that ID.
	ErrAgentCredentialNotFound = errors.New("statedb: agent credential not found")
	// ErrEnrollmentSpent: the token was already redeemed or revoked. This is
	// the replay signal, and it is a distinct error because "someone reused
	// this token" is an operational event worth surfacing, not a generic
	// failure.
	ErrEnrollmentSpent = errors.New("statedb: enrollment token already redeemed or revoked")
)

Sentinel errors for enrollment persistence. Callers use errors.Is.

View Source
var (
	// ErrKEKNotFound: no KEK row with that ID.
	ErrKEKNotFound = errors.New("statedb: sealing key not found")
	// ErrKEKInUse: retirement was refused because sealed rows still
	// reference the key. The refusal is computed inside the same
	// transaction as the retirement, so a row written concurrently either
	// blocks the retirement or arrives after it — never in between.
	ErrKEKInUse = errors.New("statedb: sealing key still referenced by sealed rows")
)

Sentinels for the key registry.

View Source
var ErrAuditAnchorNotFound = errors.New("statedb: no audit anchor")

ErrAuditAnchorNotFound is returned by LatestAuditAnchor when the trail has never been pruned. Callers treat it as "the chain starts at genesis", which is the pre-Task-20218 behaviour and not an error condition.

View Source
var ErrExecutorHandleNotFound = errors.New("statedb: executor handle not found")

ErrExecutorHandleNotFound is returned when no row exists for a handle id.

View Source
var ErrHubLeaseNotFound = errors.New("statedb: hub lease not found")

ErrHubLeaseNotFound is returned when no row exists for a scope.

Functions

func AllowSchemaDowngradeFromEnv

func AllowSchemaDowngradeFromEnv() bool

AllowSchemaDowngradeFromEnv reports whether the operator opt-out is set.

Exported so `cloop hub doctor` can tell an operator the guard is disabled. A safety check that can be switched off silently gets switched off and left that way, so the off state is itself a finding.

func AuditAttachSession added in v0.0.2

func AuditAttachSession(d *DB, in AttachAuditInput)

AuditAttachSession records one interactive-session event.

Best-effort, like every other emitter in this package: a wedged journal must not stop an operator reaching a sandbox during an incident. That is a deliberate trade — an unrecorded session is worse than no session in theory, and much better than an unreachable sandbox at 3am in practice — and it is the same trade every other audit path here already makes.

func AuditCapPause added in v0.0.2

func AuditCapPause(d *DB, in CapPauseInput)

AuditCapPause records a run parked by a subscription cap.

func AuditCapResume added in v0.0.2

func AuditCapResume(d *DB, in CapResumeInput)

AuditCapResume records the hub restarting a cap-paused run with no human in the loop.

func AuditChainLink(ev AuditEvent) (want string, ok bool)

AuditChainLink recomputes one row's hash and reports whether it matches, so callers outside this package (the sealer, which must not archive a chain that is already broken) can verify without duplicating computeRowHash.

func AuditExecutorLifecycle

func AuditExecutorLifecycle(d *DB, in ExecutorAuditInput)

AuditExecutorLifecycle records an executor-fleet mutation. Best-effort, like every other emitter in this file: a wedged audit log must not stop an operator from cordoning a misbehaving node.

func AuditImagePolicyDenial

func AuditImagePolicyDenial(d *DB, in ImagePolicyDenialInput)

AuditImagePolicyDenial records a container image refused by the hub's trust policy. Best-effort, like every other emitter here.

func AuditSecretDecision

func AuditSecretDecision(d *DB, in SecretAuditInput)

AuditSecretDecision records a secret-broker mint, grant, lease, renew, revoke, or denial. Best-effort, like every other emitter in this file: an unavailable audit log must not stop an executor from receiving the credentials it was granted.

func AuditTaskDelete

func AuditTaskDelete(d *DB, taskID int, actor string)

AuditTaskDelete records a task removal. EntityID is the deleted task's id.

func AuditTaskDispatch added in v0.0.2

func AuditTaskDispatch(d *DB, in TaskDispatchInput)

AuditTaskDispatch records one task entering execution.

Best-effort, like every other emitter in this file: a wedged audit log must not stop a task from running. The chain verifier surfaces the resulting gap.

func AuditTaskStatus

func AuditTaskStatus(d *DB, taskID int, oldStatus, newStatus, actor string)

AuditTaskStatus records a manual status flip (UI/CLI initiated).

func AuditTaskUpsert

func AuditTaskUpsert(d *DB, t *pm.Task, actor string)

AuditTaskUpsert is the exported wrapper that callers outside this package (orchestrator, UI handlers) use to emit a task-mutation event with their own actor identity. Best-effort.

func AuditWorkspaceProvision

func AuditWorkspaceProvision(d *DB, in WorkspaceAuditInput)

AuditWorkspaceProvision records one workspace-provisioning phase.

Best-effort, like every other emitter in this file: an unavailable audit log must not stop a workload from getting its source tree.

func GrowthTables added in v0.0.2

func GrowthTables() []string

GrowthTables names the row tables retention covers, in the order a report should list them. Exported so hubdoctor can enumerate them without duplicating the list and drifting from it.

func HTTPStatus

func HTTPStatus(err error) int

HTTPStatus returns the HTTP status code that best represents err for an API response. Unknown errors map to 500. Use this from any HTTP handler to keep status-code logic out of individual call sites.

if err != nil {
    jsonErr(w, err.Error(), statedb.HTTPStatus(err))
    return
}

func LatestSchemaVersion

func LatestSchemaVersion() (int, error)

LatestSchemaVersion returns the highest migration version this *binary* carries, which is what a database will be at once Migrate has run.

It exists so a diagnostic can compare the two numbers. On its own, CurrentSchemaVersion answers "what has been applied" and cannot distinguish a fully-migrated database from one an older binary left behind — and the second is the interesting case, because a hub rolled back to a previous image runs against a schema from the future and fails on whichever column it does not know about.

func MarshalAuditPayload

func MarshalAuditPayload(v any) string

MarshalAuditPayload is a small helper around json.Marshal used by audit callers that want a non-empty payload but don't want to import encoding/json at every call site. Returns "" on marshal failure rather than aborting the caller's flow — the audit row still records the mutation type.

Every payload is passed through redaction on the way out (see audit_redact.go), so no emitter can write credential material into the chain even by accident. Doing it here rather than at each call site is the point: the audit table is append-only and hash-chained, so a leaked secret cannot be edited out afterwards without breaking the chain.

func MarshalHandleMeta

func MarshalHandleMeta(meta map[string]string) string

MarshalHandleMeta renders a driver's extras for the meta_json column. A nil or empty map becomes "{}" rather than "", so the column always holds valid JSON and a reader never has to special-case the empty string.

func ProviderCallBodiesPruned added in v0.0.2

func ProviderCallBodiesPruned(headersJSON string) (time.Time, bool)

ProviderCallBodiesPruned reports when a provider-call row's bodies were dropped by retention, reading the marker out of its headers JSON.

ok is false for a row that still has its bodies, which is every row on a hub where the strip has not run. Callers that render or replay a prompt should check this before treating an empty prompt as the truth.

func RoleBindingID added in v0.0.2

func RoleBindingID(effect, claim, value, project, executor string) string

RoleBindingID derives the deterministic identifier for a binding's identity tuple.

Identity is (effect, claim, value, project, executor) and deliberately excludes role, reason and provenance: re-granting the same subject a different role on the same scope should replace the binding, not leave two rows whose winner depends on resolution order. The reason and the actor of the most recent write are what the audit trail keeps.

Truncated to 48 bits. This is a content address for deduplication, not a security boundary — the values it covers are already visible to anyone who can read the table — and a 12-character id is one an operator can retype from a terminal during an incident.

func SandboxSettingsFor added in v0.0.3

func SandboxSettingsFor(d *DB, executorID string) (executor.SandboxSettings, error)

SandboxSettingsFor resolves an executor's settings for a dispatch, collapsing "no row" and "lookup failed" onto the same answer the caller can act on.

The two are not the same, and the difference is the reason this helper exists rather than every caller deciding for itself. A missing row is a fact: this executor was never configured, and it should run what it always ran. A failed lookup is not a fact about the executor at all, and answering it with the zero value would silently unconfigure a container-mode executor for the duration of a database fault — a downgrade from containment to host execution, arrived at by no one's decision, at the moment the hub is least able to notice.

So a failed lookup returns the error and the caller refuses the dispatch. The run fails loudly and is retried; it does not quietly run somewhere weaker than the admin asked for. That is the same trade the container driver makes when it refuses to construct without a runtime.

A nil DB means no control plane — the single-user, no-database path — and yields unset settings with no error, because there is no policy to fail to read.

func SealedSetNames

func SealedSetNames() []string

SealedSetNames returns the populations retirement checks, so a test can assert they match the sets registered for rotation.

func SetAuditEnabled

func SetAuditEnabled(on bool)

SetAuditEnabled toggles audit emission globally. Intended for tests and for callers running cloop against a database where the audit_events table is not desired (e.g. one-shot CLI commands that read state).

func SetAutoUpdatePolicy added in v0.0.3

func SetAutoUpdatePolicy(d *DB, p autoupdate.Policy, setBy string) error

SetAutoUpdatePolicy replaces the fleet's policy. setBy is the identity that set it, for the audit trail.

func SetBinaryVersion

func SetBinaryVersion(v string)

SetBinaryVersion records which build this process is, for stamping into schema_migrations when it applies a migration. Call it once during process initialisation; cmd does so with the ldflags-injected version.

It lives here rather than being read from pkg/cmd because pkg/statedb sits below the CLI in the import graph and must not depend on it.

func UnmarshalHandleMeta

func UnmarshalHandleMeta(s string) map[string]string

UnmarshalHandleMeta parses the meta_json column. Unparsable metadata is dropped rather than propagated: the extras are advisory, and refusing to rehydrate a workload because a hint field is corrupt would strand a running container to protect a label.

func UserErrorf added in v0.0.2

func UserErrorf(sentinel error, format string, args ...any) error

UserErrorf returns an error that prints as the formatted message and matches sentinel under errors.Is.

Use it wherever a sentinel would otherwise be wrapped in prose the user is meant to read:

return statedb.UserErrorf(statedb.ErrProjectNotFound,
	"no cloop project found (run 'cloop init' first)")

prints "no cloop project found (run 'cloop init' first)" and still satisfies errors.Is(err, statedb.ErrProjectNotFound).

format is rendered with fmt.Sprintf, so it takes ordinary verbs but not %w — this constructor already owns the wrapping. A nil sentinel degrades to a plain fmt.Errorf rather than producing an error that matches nothing, so a caller that passes one by mistake still gets a usable message.

Types

type APITokenRow

type APITokenRow struct {
	ID           string    `json:"id"`
	Name         string    `json:"name"`
	Hash         string    `json:"-"` // never serialized: see the migration comment
	Prefix       string    `json:"prefix"`
	Roles        []string  `json:"roles"`
	ProjectScope []string  `json:"project_scope,omitempty"`
	CreatedBy    string    `json:"created_by,omitempty"`
	CreatedAt    time.Time `json:"created_at"`
	ExpiresAt    time.Time `json:"expires_at,omitempty"`
	LastUsedAt   time.Time `json:"last_used_at,omitempty"`
	RevokedAt    time.Time `json:"revoked_at,omitempty"`

	// Kind labels a hub-issued token; "" is an ordinary PAT (migration 0023).
	Kind string `json:"kind,omitempty"`

	// OwnerJSON is the owning identity's claim bundle, stored opaquely. This
	// layer deliberately does not know its shape: pkg/apitoken owns the
	// encoding, and a second opinion here about what a claim set looks like is
	// how the two drift apart. Never serialized outward for the same reason
	// Hash is not — it carries a user's email and group memberships.
	OwnerJSON string `json:"-"`
}

APITokenRow is one persisted API token.

Roles and ProjectScope are stored as JSON arrays rather than normalized child tables: both are short, always read whole with the row, and are validated by pkg/apitoken on the way in and on the way out. A join here would buy nothing and would put the verification path — which runs on every authenticated request — behind three queries instead of one.

type AgentCredentialRow

type AgentCredentialRow struct {
	AgentID string
	Name    string
	// SecretHash is hex SHA-256 of the credential secret, never the secret.
	SecretHash   string
	WorkDirRoot  string
	Labels       map[string]string
	CreatedAt    time.Time
	LastSeen     time.Time
	RevokedAt    time.Time
	EnrollmentID string
}

AgentCredentialRow is one enrolled agent's long-lived credential.

type AttachAuditInput added in v0.0.2

type AttachAuditInput struct {
	// Event is the fully-qualified action: auditaction.ActionSandboxAttachOpen,
	// ActionSandboxAttachClose, or ActionSandboxAttachDenied.
	//
	// Passed in whole rather than assembled from an action suffix, because
	// these three are the complete set and a typo should be visible at the
	// call site rather than produce a plausible-looking new event type that
	// no retention rule or SIEM filter knows about. Typing it as an
	// auditaction.Action is what turns "should be visible" into "cannot
	// compile": there is no longer a bare string to get wrong.
	Event auditaction.Action
	// Actor is the acting identity — the OIDC subject label, the same
	// spelling auditAuthz writes, so filtering the trail by person returns a
	// whole session rather than half of it.
	Actor string
	// SessionID correlates the open with its close.
	SessionID string
	// ExecutorID and HandleID name the sandbox that was entered. Both,
	// because the executor alone does not identify which workload and the
	// handle alone does not survive as a lookup key once the run ends.
	ExecutorID string
	HandleID   string
	// TaskID is the work the sandbox was running, and the entity this row is
	// filed under: "show me everything that happened to task 41" is the query
	// an incident actually starts from.
	TaskID int
	// Command is the argv as the caller gave it, unmodified. Not normalised
	// and not shell-quoted — the point is to record what was asked for, and a
	// prettied-up rendering is a different string from the one that ran.
	Command string
	// Writable reports whether the session had an input channel at all.
	Writable bool
	// Detail is the refusal reason for a denial, or the close reason for a
	// close. Empty on a clean open.
	Detail string
}

AttachAuditInput carries one interactive-session lifecycle event.

type AuditAnchor

type AuditAnchor struct {
	ID        int64
	CreatedAt time.Time
	Actor     string

	// Cutoff is the --before instant the operator asked for. Kept distinct
	// from the ids it resolved to so an audit of the audit can tell a policy
	// decision from its effect.
	Cutoff time.Time

	PrunedFirstID   int64
	PrunedThroughID int64
	PrunedCount     int64

	// BoundaryHash is the row_hash of PrunedThroughID: what the first
	// surviving row's prev_hash must equal.
	BoundaryHash string

	// RetainedFromID is the first surviving id, or 0 when the prune emptied
	// the table.
	RetainedFromID int64

	ExportPath   string
	ExportFormat string
	ExportSHA256 string
	ExportBytes  int64

	PrevAnchorHash string
	AnchorHash     string
}

AuditAnchor records one deliberate truncation of the audit chain. See migration 0027 for what each field is load-bearing for.

type AuditEvent

type AuditEvent struct {
	ID         int64     // monotonic primary key, assigned on append
	Timestamp  time.Time // UTC
	Actor      string    // "system", "orchestrator", "ui", "cli", or user-supplied
	EventType  string    // e.g. "task.create", "plan.update", "config.set"
	EntityType string    // "task" | "plan" | "config" | "step" | "session"
	EntityID   string    // stringified id ("12" for task 12), empty for singletons
	Payload    string    // JSON blob describing the mutation
	PrevHash   string    // 64-hex SHA-256 of prior row, or 64 zeros for the first row
	RowHash    string    // 64-hex SHA-256 of this row
}

AuditEvent is one row in the audit_events table.

type AuditFilter

type AuditFilter struct {
	Actor      string    // exact-match
	EntityType string    // exact-match (e.g. "task")
	EntityID   string    // exact-match within EntityType
	EventType  string    // exact-match (e.g. "task.update")
	Since      time.Time // only events with timestamp >= Since
	Until      time.Time // only events with timestamp <= Until
	FromID     int64     // only events with id >= FromID
	ToID       int64     // only events with id <= ToID
	Search     string    // case-insensitive LIKE on payload
	Order      string    // "asc" (default) or "desc"
	Limit      int       // 0 = no limit (capped to 10 000 internally)
	Offset     int
}

AuditFilter narrows ListAuditEvents down to the rows the caller cares about. All fields are optional — zero values mean "no filter".

type AuditPruneCandidate

type AuditPruneCandidate struct {
	FirstID   int64
	ThroughID int64
	Count     int64
	// BoundaryHash is the row_hash at ThroughID, carried so the seal and the
	// delete can be checked against the same value.
	BoundaryHash string
	// Remaining is how many rows would survive.
	Remaining int64
}

AuditPruneCandidate describes what a prune with the given cutoff would remove, without removing anything. Both the CLI's --dry-run and the real path use it, so the number an operator is shown is the number that runs.

type AuditVerifyReport

type AuditVerifyReport struct {
	Total     int    // rows checked
	OK        bool   // true when every link verified
	BreakAtID int64  // first row whose hash did not match (0 if OK)
	Reason    string // human-readable description of the break

	// ExpectedHash and ActualHash carry the two hashes the break compared,
	// in full rather than the abbreviated form embedded in Reason. Reason is
	// for a human reading a terminal; these are for a UI that wants to show
	// them side by side, and for a report that gets pasted into a ticket
	// where a 12-character prefix is not enough to reason about. Both are
	// empty for an id-gap break, where no hash was compared, and for a
	// clean run.
	ExpectedHash string
	ActualHash   string

	// Anchored reports that a retention prune has truncated the chain and the
	// walk was validated against an anchor rather than against genesis. The
	// remaining fields describe that anchor, so a report can say "verified
	// from id N; everything before it is in <export>, sha256 <digest>" instead
	// of leaving an operator to wonder why the trail does not start at 1.
	Anchored     bool
	AnchorID     int64
	PrunedCount  int64
	ExportPath   string
	ExportSHA256 string

	// VerifiedFromID is the id of the first row the walk checked, or 0 when
	// the table is empty.
	VerifiedFromID int64
}

AuditVerifyReport summarises a chain-verification run.

type AutoUpdatePolicy added in v0.0.3

type AutoUpdatePolicy struct {
	Policy autoupdate.Policy `json:"policy"`
	SetAt  time.Time         `json:"set_at,omitempty"`
	SetBy  string            `json:"set_by,omitempty"`
}

AutoUpdatePolicy is the stored policy plus its provenance.

func AutoUpdatePolicyFor added in v0.0.3

func AutoUpdatePolicyFor(d *DB) (AutoUpdatePolicy, error)

AutoUpdatePolicyFor returns the fleet's policy.

A fleet that has never been configured yields a disabled zero policy and no error. "Nobody has set this" and "automatic upgrades are off" are the same operational state, and making the caller distinguish them would put an error path in front of the overwhelmingly common case.

type BrokerGrantRow

type BrokerGrantRow struct {
	ID              string
	SecretID        string
	Scope           string
	SubjectType     string
	SubjectValue    string
	ConstraintsJSON string
	ExpiresAt       string
	CreatedAt       string
	CreatedBy       string
	RevokedAt       string
	// Owner is denormalised from the secret this grant points at, so a grant
	// listing can be scoped without resolving each row's secret.
	Owner string
}

BrokerGrantRow is one row of broker_grants.

type BrokerSecretRow

type BrokerSecretRow struct {
	ID      string
	Kind    string
	Name    string
	Payload []byte // ciphertext under this row's DEK; never plaintext
	// KeyID names the KEK that WrappedDEK is sealed under, or "legacy" for
	// rows predating envelope encryption (Task 20181). WrappedDEK is the
	// row's data key, itself sealed. Neither is key material on its own.
	KeyID        string
	WrappedDEK   []byte
	MetadataJSON string
	CreatedAt    string
	CreatedBy    string
	// Owner is the identity that personally owns this secret, or "" for a
	// shared one. See migration 0039.
	Owner string
}

BrokerSecretRow is one row of broker_secrets. Times are RFC3339 strings, matching the rest of the schema.

type CIExchangeRow added in v0.0.2

type CIExchangeRow struct {
	ID         string    `json:"id"`
	At         time.Time `json:"at"`
	Accepted   bool      `json:"accepted"`
	Issuer     string    `json:"issuer,omitempty"`
	Subject    string    `json:"subject,omitempty"`
	Repository string    `json:"repository,omitempty"`
	Ref        string    `json:"ref,omitempty"`
	Workflow   string    `json:"workflow,omitempty"`
	Actor      string    `json:"actor,omitempty"`
	EventName  string    `json:"event_name,omitempty"`
	RunID      string    `json:"run_id,omitempty"`
	RuleID     string    `json:"rule_id,omitempty"`
	RuleName   string    `json:"rule_name,omitempty"`
	SessionID  string    `json:"session_id,omitempty"`

	// Reason is a stable code; Detail is the human-readable explanation. The
	// pair exists so a dashboard can group by cause while still showing the
	// sentence that names the actual problem.
	Reason string `json:"reason,omitempty"`
	Detail string `json:"detail,omitempty"`

	RemoteAddr string `json:"remote_addr,omitempty"`

	// Claims is the verified payload, or nil on a token that never verified.
	Claims map[string]any `json:"claims,omitempty"`
}

CIExchangeRow is one recorded federation attempt.

type CIPipelineRuleRow added in v0.0.2

type CIPipelineRuleRow struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Enabled bool   `json:"enabled"`

	Repository  string `json:"repository,omitempty"`
	Ref         string `json:"ref,omitempty"`
	Workflow    string `json:"workflow,omitempty"`
	Environment string `json:"environment,omitempty"`
	Actor       string `json:"actor,omitempty"`
	EventName   string `json:"event_name,omitempty"`
	Condition   string `json:"condition,omitempty"`

	Models          []string `json:"models,omitempty"`
	MaxRequests     int      `json:"max_requests,omitempty"`
	MaxOutputTokens int      `json:"max_output_tokens,omitempty"`
	TTLSeconds      int      `json:"ttl_seconds,omitempty"`

	Project       string    `json:"project,omitempty"`
	CreatedBy     string    `json:"created_by,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at,omitempty"`
	LastMatchedAt time.Time `json:"last_matched_at,omitempty"`
}

CIPipelineRuleRow is one persisted allowlist entry.

type CapPauseInput added in v0.0.2

type CapPauseInput struct {
	// ProjectPath is the working directory whose run was stopped.
	ProjectPath string
	// Windows names the tripped windows ("five_hour", "weekly", ...).
	Windows []string
	// Utilization and Cap are the global utilization and the configured
	// per-project ceiling that it reached, for the first tripped window.
	Utilization float64
	Cap         float64
	// ResumesAt is when every tripped window has rolled over. Zero when the
	// usage API did not report a reset, in which case the row says so rather
	// than inventing a time a resume could later be matched against.
	ResumesAt time.Time
	// Detail is the operator-facing phrase stored as the pause reason.
	Detail string
	Actor  string
}

CapPauseInput is one run stopped by a Claude Code subscription cap.

type CapResumeInput added in v0.0.2

type CapResumeInput struct {
	ProjectPath string
	// PausedDetail is the pause reason the run carried, so the row names the
	// wall it was waiting on.
	PausedDetail string
	// ResumesAt is the instant the run was waiting for, and ResumedAt is when
	// the hub actually acted. The gap between them is the sweep interval, and
	// having both is what distinguishes a late sweep from a wrong reset time.
	ResumesAt time.Time
	ResumedAt time.Time
	Actor     string
}

CapResumeInput is one cap-paused run restarted by the hub on its own.

type CostEntry

type CostEntry struct {
	Timestamp      time.Time
	TaskID         int
	TaskTitle      string
	Provider       string
	Model          string
	InputTokens    int
	OutputTokens   int
	ThinkingTokens int
	EstimatedUSD   float64

	// Identity is who this spend is attributed to — an oidcauth OwnerKey, or
	// the "local" sentinel for an unauthenticated run (Task 20264). Empty on
	// rows written before migration 0035, which means *unattributed* rather
	// than unowned. Self-reported by the orchestrator; see 0035 for why quota
	// enforcement deliberately does not read it.
	Identity string

	// RowID is the costs.id of this row. Set by the readers that select it,
	// zero elsewhere (AppendCost does not read it back).
	//
	// It exists for the hub's spend drain, which needs a watermark it can
	// resume from without double-booking. A timestamp cannot serve: two tasks
	// finishing in the same nanosecond collide, and a clock adjustment moves
	// one backwards — either would book a tenant twice or not at all. The
	// autoincrement id is monotonic in insertion order, which is the order the
	// drain actually walks.
	RowID int64
}

CostEntry records the cost of one task execution in the costs table.

type DB

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

DB is a thread-safe handle to the SQLite state database. Open one DB per workdir; Close it when done.

func Open

func Open(dbPath string) (*DB, error)

Open opens (or creates) the SQLite database at dbPath, applies tuning pragmas, and runs any pending schema migrations.

Concurrency model: every cloop process (Web UI, orchestrator, CLI commands) opens its own *DB handle pointing at the same .cloop/state.db file. WAL mode + busy_timeout coordinate them at the SQLite file level, so writes from one process do not return SQLITE_BUSY to readers in another.

Errors returned by this function may wrap the typed sentinels ErrDBLocked or ErrSchemaMismatch — callers should use errors.Is to distinguish them from generic open failures. A database migrated past this binary's schema is refused with ErrSchemaTooNew rather than opened; see schema_guard.go.

func OpenWithOptions

func OpenWithOptions(dbPath string, opts OpenOptions) (*DB, error)

OpenWithOptions is Open with the version-skew opt-out under the caller's control rather than the environment's.

func (*DB) AcquireHubLease

func (d *DB) AcquireHubLease(row HubLeaseRow, expect HubLeaseRow) (bool, error)

AcquireHubLease takes the lease for row.Scope, conditioned on the current row still matching expect.

It returns true when the lease was taken and false when it was not, without an error in either case: losing a race is an ordinary outcome here, not a failure, and the caller needs to re-read the row to describe the winner anyway.

expect is the row the caller previously read, and is the entire concurrency story. A zero-valued expect means "I saw no row": the INSERT then succeeds only if none appeared in the meantime, because a conflict falls through to a DO UPDATE whose WHERE cannot match. A populated expect means "I saw this row and judged it takeable": the update applies only if it is still byte-for-byte that row, so a holder that renewed in the intervening microseconds keeps its lease and this caller is told it lost.

This is one statement on purpose. SQLite applies it atomically without an explicit transaction, which avoids the read-then-upgrade pattern that deadlocks two processes against each other under WAL — the exact failure the busy_timeout cannot absorb.

func (*DB) AddExecutorAudience added in v0.0.3

func (d *DB) AddExecutorAudience(executorID, kind, value, addedBy string) error

AddExecutorAudience admits a principal to an executor. Adding one that is already admitted refreshes its provenance rather than failing, so the UI's "add" button is idempotent.

Note what the first successful call does: it turns the gate on for that executor, because an executor with no rows is unrestricted. The caller is responsible for not locking the fleet out of itself — see pkg/ui/executoraudience.go, which refuses an add that would leave the calling admin unable to reach the executor they just restricted.

func (*DB) AdvanceSpendCursor added in v0.0.2

func (d *DB) AdvanceSpendCursor(projectPath string, from, to int64) (won bool, err error)

AdvanceSpendCursor moves the booking position from `from` to `to`, and reports whether this caller was the one that moved it.

It is a compare-and-swap, not a plain update, and the caller must book the spend only when it returns won=true. That is what makes the claim to charge each row exactly once true rather than merely likely:

  • Across processes. Two hubs run from this repository (ports 8080 and 8888) and share one control-plane database. A plain monotonic update ("only move forward") does not help: both can read cursor=100, both read rows 101-110, and both charge them — the guard fires after the double-book, on the second write, too late to matter. With a CAS only one UPDATE matches, so only one of them books.

  • On failure. Booking first and advancing second means a failed advance re-presents the same rows on the next tick, and a control plane that stays unwritable (SQLITE_BUSY past the busy timeout, a read-only mount, a full disk) inflates the tenant's counter by that batch every thirty seconds until the budget is spent and the run is killed on arithmetic that never happened. Advancing first converts that unbounded failure into a bounded one: a process that dies between the CAS and the booking loses exactly one batch, once.

Under-billing a bounded amount once is a cost; over-billing repeatedly stops work that should be running. The order is chosen accordingly.

func (*DB) AllExecutorAudiences added in v0.0.3

func (d *DB) AllExecutorAudiences() (map[string][]ExecutorAudienceEntry, error)

AllExecutorAudiences returns every audience row keyed by executor ID.

One query rather than one per executor, because the fleet list renders an "available to" summary for every card and the per-executor form would make that N+1 round trips against a database the dispatch path also uses.

func (*DB) Analyze

func (d *DB) Analyze() error

Analyze runs ANALYZE to refresh the query planner's per-index statistics. Cheap on small databases; on large ones it scales roughly with total row count across all indexes.

func (*DB) AppendAuditEvent

func (d *DB) AppendAuditEvent(ev *AuditEvent) error

AppendAuditEvent appends one row to the audit_events table, computing the hash chain link from the previous row. Holds the DB mutex so the chain is consistent under concurrent writers in the same process. Cross-process safety is provided by SQLite WAL + busy_timeout — concurrent writers serialise at the file level.

Mutates ev in place: the assigned ID, computed hashes, and any auto-populated fields (Timestamp, Actor) are written back so callers can inspect them.

func (*DB) AppendAuditEvents

func (d *DB) AppendAuditEvents(evs []*AuditEvent) error

AppendAuditEvents chains and inserts a whole batch in a single transaction.

This is the bulk path, and the reason it exists is measured: emitting a 417-task plan through AppendAuditEvent cost 417 transactions and 417 tip reads per SaveState, which is most of what made the audit trail expensive enough to notice. Chaining in-process and writing once is the same chain with one commit.

Events are chained in slice order, so callers control causal order. Each element is mutated in place with its assigned ID and hashes. A nil or empty slice is a no-op; a nil element, or one with an empty event type, is an error and nothing is written.

func (*DB) AppendCost

func (d *DB) AppendCost(entry CostEntry) error

AppendCost inserts a cost entry into the costs table.

func (*DB) AppendMaintenanceLog

func (d *DB) AppendMaintenanceLog(e MaintenanceLogEntry) (int64, error)

AppendMaintenanceLog inserts a maintenance_log row. Returns the new row id. StartedAt / CompletedAt default to time.Now().UTC() when zero so callers don't have to fill them in for ad-hoc records.

func (*DB) AppendProviderCall

func (d *DB) AppendProviderCall(row ProviderCallRow) (int64, error)

AppendProviderCall inserts one provider-call row and returns the assigned id. Best-effort: errors are wrapped through classifyDriverErr so callers can distinguish ErrDBLocked from real corruption, but production code paths typically log-and-discard.

func (*DB) AppendStep

func (d *DB) AppendStep(row StepRow) error

AppendStep inserts a step row (idempotent on step number).

func (*DB) AppendTelemetry added in v0.0.2

func (d *DB) AppendTelemetry(events []telemetry.Event) error

AppendTelemetry stores a normalized batch and enforces the row ceiling.

Takes the whole batch in one transaction: a trail's value is its ordering, and a partially-written batch would show a reader a gap that never happened.

func (*DB) AsControlPlane added in v0.0.2

func (d *DB) AsControlPlane() *DB

AsControlPlane marks d as the hub's control-plane handle and returns it, so it can be chained onto an Open:

db, err := statedb.Open(state.DBPath(s.WorkDir))
... db.AsControlPlane()

Marking is idempotent and safe to repeat.

func (*DB) AsProject added in v0.0.2

func (d *DB) AsProject() *DB

AsProject marks d as one project's handle and returns it.

func (*DB) AttributeRequestUseTasks added in v0.0.2

func (d *DB) AttributeRequestUseTasks() (int, error)

AttributeRequestUseTasks stamps the task a recorded lease was dispatched for onto the use rows that do not have one yet, and reports how many it filled in.

It exists because a lease is issued *before* the workload that will hold it is dispatched. At lease time the broker knows the executor and the project and genuinely does not know the task — there is no task yet — so the use row starts with task_id 0 and the answer arrives a moment later, when the driver writes an executor_handles row carrying both the task and the lease bindings that workload was started with.

Reconciling the two afterwards is therefore the only way to get the attribution at all, and it has to happen while the handle is still live: handle rows are deleted when the workload reaches a terminal state, so a reconciliation that only ran on demand would report nothing for exactly the runs that have finished — which are the ones an approver reviews. Callers run it on the same schedule they sweep expired requests, and again when listing.

Idempotent and monotonic: it only fills in zeros (the UPDATE's guard, and PutGrantRequestUse's) so a handle that has since been reused for other work cannot rewrite an attribution already made.

func (*DB) AuditDistinctActors

func (d *DB) AuditDistinctActors() ([]string, error)

AuditDistinctActors returns the set of distinct actor values across the audit log, sorted ascending. Used by the Web UI's filter dropdown so the user sees only actors that actually exist.

func (*DB) AuditDistinctEntityTypes

func (d *DB) AuditDistinctEntityTypes() ([]string, error)

AuditDistinctEntities returns the set of distinct (entity_type, entity_id) pairs as "type/id" strings (id may be empty for singletons). Sorted.

func (*DB) BindProjectExecutor

func (d *DB) BindProjectExecutor(projectPath, executorID, boundBy string) error

BindProjectExecutor pins projectPath to executorID. The executor must already exist, so a typo cannot strand a project on a nonexistent backend.

func (*DB) BrokerMeta

func (d *DB) BrokerMeta(key string) (string, bool, error)

BrokerMeta reads a broker-scoped metadata value.

func (*DB) ClaimExecutorSessionRequeue

func (d *DB) ClaimExecutorSessionRequeue(id, claimToken, newClaimToken string, at time.Time) (ExecutorSessionRow, error)

ClaimExecutorSessionRequeue atomically claims a running session for failover and rotates its claim token.

The exactly-once guarantee lives entirely in this statement's WHERE clause. SQLite serialises writers, so of N supervisors racing to fail over the same dead executor exactly one finds state = 'running' with the claim token it read and updates a row; every other supervisor matches nothing and gets ErrExecutorSessionClaimLost. Rotating claim_token in the same statement also makes a retry by the winner idempotent-safe: the old token can never match again.

A read-then-write implementation would let two supervisors both decide the session is running and both requeue the work — which here means two AI agents editing the same repository concurrently. That is the failure this function exists to make impossible, so the read and the write must not be separable.

The follow-up read on a zero-row update is only to tell the operator which of two situations they are in; it never grants a claim, so a row appearing or vanishing between the two statements cannot turn a loss into a win.

func (*DB) ClearExecutorAudience added in v0.0.3

func (d *DB) ClearExecutorAudience(executorID string) error

ClearExecutorAudience removes every principal, making the executor unrestricted. It is what executor deletion calls, so a re-enrolled executor reusing an ID does not inherit a stale allowlist.

func (*DB) ClearExecutorResourceLimit added in v0.0.3

func (d *DB) ClearExecutorResourceLimit(executorID string) error

ClearExecutorResourceLimit removes the ceiling for executorID. Clearing an executor that was never capped is not an error.

func (*DB) ClearExecutorSandbox added in v0.0.3

func (d *DB) ClearExecutorSandbox(executorID string) error

ClearExecutorSandbox removes the configuration for executorID, returning it to the executor's own default. Clearing an executor that was never configured is not an error.

func (*DB) ClearHubLease

func (d *DB) ClearHubLease(scope string, expect HubLeaseRow, at time.Time) (bool, error)

ClearHubLease marks a lease released on behalf of an operator, conditioned on the row still matching expect.

This backs the `cloop hub lease clear` escape hatch. The compare-and-swap is what makes the command's staleness gate meaningful: the caller checks that the lease is stale and then clears it, and if the holder heartbeats in between, the observed heartbeat no longer matches and the clear is refused. Without it the gate would be advisory and the command could evict a hub that had just come back.

func (*DB) ClearKill

func (d *DB) ClearKill(taskID int) error

ClearKill removes the kill row for taskID. Idempotent — clearing a row that does not exist returns nil.

func (*DB) ClearProjectResourceLimit added in v0.0.2

func (d *DB) ClearProjectResourceLimit(projectPath string) error

ClearProjectResourceLimit removes the ceiling for projectPath. Clearing a project that was never capped is not an error.

func (*DB) Close

func (d *DB) Close() error

Close releases the database connection.

func (*DB) CloseExecutorSession

func (d *DB) CloseExecutorSession(id, state string, at time.Time) error

CloseExecutorSession moves a session to a terminal state. Returns ErrExecutorSessionNotFound when the ID is unknown, so a driver reporting completion for a session the control plane never opened surfaces loudly instead of being silently dropped.

func (*DB) CountBrokerSecretsByKey

func (d *DB) CountBrokerSecretsByKey() (map[string]int, error)

CountBrokerSecretsByKey groups secrets by the key they are sealed under.

func (*DB) CountEvents

func (d *DB) CountEvents() (int, error)

CountEvents returns the total number of journal rows. Cheap; used by the UI to decide whether to refresh the top page.

func (*DB) CountRunningExecutorSessions

func (d *DB) CountRunningExecutorSessions(executorID string) (int, error)

CountRunningExecutorSessions returns how many sessions are still in flight. An empty executorID counts across the whole fleet.

func (*DB) CountSessionsByKey

func (d *DB) CountSessionsByKey() (map[string]int, error)

CountSessionsByKey groups sessions holding a refresh token by sealing key.

Sessions with no refresh token are excluded: they hold nothing sealed, so counting them would make a rotation look permanently incomplete on a hub whose IdP issues no refresh tokens.

func (*DB) CurrentSchemaVersion

func (d *DB) CurrentSchemaVersion() (int, error)

CurrentSchemaVersion returns the highest migration version recorded in the database. Useful for diagnostic commands (cloop db verify, cloop migrate status). Acquires the DB mutex like other read helpers in this package.

func (*DB) DeleteBrokerSecret

func (d *DB) DeleteBrokerSecret(id string) error

DeleteBrokerSecret removes a secret. Deleting one that does not exist is an error, so a CLI can tell the operator their reference was wrong instead of reporting a successful no-op.

func (*DB) DeleteCIPipelineRule added in v0.0.2

func (d *DB) DeleteCIPipelineRule(id string) (bool, error)

DeleteCIPipelineRule removes a rule. It reports whether a row was removed, so the caller can tell "deleted" from "already gone" without a prior read.

func (*DB) DeleteExecutor

func (d *DB) DeleteExecutor(id string) error

DeleteExecutor removes an executor and every project binding that pointed at it. Both happen in one transaction so a project can never be left pinned to an executor that no longer exists — such a project would fail every Resolve until an operator noticed.

func (*DB) DeleteExecutorHandle

func (d *DB) DeleteExecutorHandle(handleID string) error

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

func (*DB) DeleteExecutorHealth

func (d *DB) DeleteExecutorHealth(executorID string) error

DeleteExecutorHealth forgets the health record for executorID. Deleting a row that was never written is not an error: de-enrolling an executor that was never probed must not fail.

func (*DB) DeleteExpiredSessions

func (d *DB) DeleteExpiredSessions(absoluteCutoff, idleCutoff time.Time) ([]SessionRow, error)

DeleteExpiredSessions removes sessions past either clock and returns what it deleted.

absoluteCutoff is compared against expires_at (the ceiling set at login) and idleCutoff against last_seen. Both are absolute times computed by the caller from its configured policy, so this layer stays free of any opinion about how long a session should live.

func (*DB) DeleteHubMeta added in v0.0.2

func (d *DB) DeleteHubMeta(key string) error

DeleteHubMeta removes a hub-scoped metadata value. Deleting one that is not there succeeds: the caller asked for it to be gone, and it is.

func (*DB) DeleteQuotaOverride

func (d *DB) DeleteQuotaOverride(identity string) (bool, error)

DeleteQuotaOverride removes one identity's override, reporting whether a row existed.

func (*DB) DeleteRoleBinding added in v0.0.2

func (d *DB) DeleteRoleBinding(id string) (bool, error)

DeleteRoleBinding removes a binding, reporting whether a row existed.

func (*DB) DeleteSecretLeaseDir

func (d *DB) DeleteSecretLeaseDir(dir string) error

DeleteSecretLeaseDir forgets one directory, after it has been wiped.

Deleting a row that is not there is not an error: a lease can be wiped twice — once when its workload exits, once by a revocation that raced it — and neither call should fail.

func (*DB) DeleteSession

func (d *DB) DeleteSession(id string) (bool, error)

DeleteSession removes one session and reports whether a row existed.

func (*DB) DeleteSessionsBySubject

func (d *DB) DeleteSessionsBySubject(subject, keepID string) ([]SessionRow, error)

DeleteSessionsBySubject removes every session belonging to subject except the one named by keepID (pass "" to remove all of them), returning the rows it deleted so the caller can audit each one.

Read-then-delete inside one transaction: an audit entry the operator cannot tie to a specific session is not much of an audit entry, and doing it in two statements outside a transaction would let a login between them produce a row that is deleted but never recorded.

func (*DB) DeleteTask

func (d *DB) DeleteTask(id int) error

DeleteTask removes a single task by id. Returns nil even if the row did not exist — the post-condition (task absent) is satisfied either way.

func (*DB) ExecutorAudience added in v0.0.3

func (d *DB) ExecutorAudience(executorID string) ([]ExecutorAudienceEntry, error)

ExecutorAudience returns the principals admitted to executorID, oldest first so the panel's order is stable as entries are added.

An empty slice with a nil error means the executor is unrestricted. An error means the question could not be answered, which callers must not read as "unrestricted" — see pkg/ui/executoraudience.go for the failure posture a gate requires, which is the opposite of the one a ceiling requires.

func (*DB) ExecutorResourceCeiling added in v0.0.3

func (d *DB) ExecutorResourceCeiling(executorID string) (executor.ResourceCeiling, bool, error)

ExecutorResourceCeiling returns the ceiling for executorID.

The boolean is false when the executor has no ceiling, which the dispatch path reads as "the fleet and project ceilings alone apply" — deliberately distinct from an error, which means the lookup failed and must not be read as "uncapped". A storage fault that silently became "no limit" would remove the policy exactly when the hub is least healthy.

func (*DB) ExecutorSandboxRecord added in v0.0.3

func (d *DB) ExecutorSandboxRecord(executorID string) (ExecutorSandbox, bool, error)

ExecutorSandboxRecord returns the settings for executorID together with their provenance, for the fleet view that shows who chose an executor's containment.

func (*DB) ExecutorSandboxSettings added in v0.0.3

func (d *DB) ExecutorSandboxSettings(executorID string) (executor.SandboxSettings, bool, error)

ExecutorSandboxSettings returns the settings for executorID.

The boolean is false when the executor has no row, which the dispatch path reads as "this executor keeps its own default" — deliberately distinct from an error, which means the lookup failed. The distinction matters in one direction especially: a storage fault that silently became "unset" would downgrade a container-mode executor to host execution exactly when the hub is least healthy, which is the failure this whole feature exists to prevent. See SandboxSettingsFor for the caller that has to choose what to do about it.

func (*DB) ExpireGrantRequests added in v0.0.2

func (d *DB) ExpireGrantRequests(now time.Time) ([]GrantRequestRow, error)

ExpireGrantRequests moves every pending request whose deadline has passed to the expired state, and returns the rows it changed.

Returning the rows rather than a count is what lets the caller emit one audit event per expiry. A request that lapses is a decision — nobody made one, and access was refused as a result — and the trail should say so with the same weight as a deny, so the requester can see why nothing happened.

Requests with no deadline (expires_at = ”) are left alone: the broker always sets one, and a row from some future path that deliberately has none should not be swept by a rule it never opted into.

func (*DB) GetAPIToken

func (d *DB) GetAPIToken(id string) (APITokenRow, error)

GetAPIToken returns one token by ID, or ErrAPITokenNotFound.

This is the verification path's only read, which is why the lookup is by primary key: an authenticated request must not cost a table scan, and a scan would additionally make request latency a function of how many tokens the hub has ever issued.

func (*DB) GetAgentCredential

func (d *DB) GetAgentCredential(agentID string) (AgentCredentialRow, error)

GetAgentCredential returns one agent by ID, or ErrAgentCredentialNotFound.

func (*DB) GetBrokerGrant

func (d *DB) GetBrokerGrant(id string) (BrokerGrantRow, error)

GetBrokerGrant returns one grant by ID.

func (*DB) GetBrokerSecret

func (d *DB) GetBrokerSecret(id string) (BrokerSecretRow, error)

GetBrokerSecret returns one secret by ID.

func (*DB) GetCIPipelineRule added in v0.0.2

func (d *DB) GetCIPipelineRule(id string) (CIPipelineRuleRow, error)

GetCIPipelineRule returns one rule by ID.

func (*DB) GetConfigBlob

func (d *DB) GetConfigBlob() (string, error)

GetConfigBlob returns the YAML-serialised project config previously stored via SetConfigBlob. Returns ("", nil) when no blob has been written yet (fresh project, or DB created before this column existed).

func (*DB) GetEgressGrant

func (d *DB) GetEgressGrant(id string) (EgressGrantRow, error)

GetEgressGrant returns one grant by ID.

func (*DB) GetEnrollmentToken

func (d *DB) GetEnrollmentToken(id string) (EnrollmentTokenRow, error)

GetEnrollmentToken returns one token by ID, or ErrEnrollmentNotFound.

func (*DB) GetExecutor

func (d *DB) GetExecutor(id string) (ExecutorRow, error)

GetExecutor returns one executor by ID, or ErrExecutorNotFound.

func (*DB) GetExecutorHandle

func (d *DB) GetExecutorHandle(handleID string) (ExecutorHandleRow, error)

GetExecutorHandle returns one row, or ErrExecutorHandleNotFound.

func (*DB) GetExecutorHealth

func (d *DB) GetExecutorHealth(executorID string) (ExecutorHealthRow, error)

GetExecutorHealth returns one health row, or ErrExecutorHealthNotFound.

"Never probed" is deliberately an error rather than a zero-valued ready row: a caller that cannot tell the two apart would treat an unobserved executor as healthy, which is the failure mode this table exists to prevent.

func (*DB) GetExecutorSession

func (d *DB) GetExecutorSession(id string) (ExecutorSessionRow, error)

GetExecutorSession returns one session by ID, or ErrExecutorSessionNotFound.

func (*DB) GetGrantRequest added in v0.0.2

func (d *DB) GetGrantRequest(id string) (GrantRequestRow, error)

GetGrantRequest returns one request by ID.

func (*DB) GetHubLease

func (d *DB) GetHubLease(scope string) (HubLeaseRow, error)

GetHubLease returns the row for scope, or ErrHubLeaseNotFound.

func (*DB) GetRoleBinding added in v0.0.2

func (d *DB) GetRoleBinding(id string) (RoleBindingRow, error)

GetRoleBinding returns one binding by id, or ErrRoleBindingNotFound.

func (*DB) GetSession

func (d *DB) GetSession(id string) (SessionRow, error)

GetSession returns one session by id, or an error wrapping ErrSessionNotFound.

This sits on the authentication path, so the lookup is by primary key: a signed-in request must not cost a scan, and a scan would make request latency a function of how many people are logged in.

func (*DB) GrowthTableStats added in v0.0.2

func (d *DB) GrowthTableStats() ([]TableStat, error)

GrowthTableStats measures every table retention covers.

One full scan per table: `cloop hub doctor` is an on-demand command and the whole point of the finding is to be accurate about a table nobody has looked at. Not for a request path.

func (*DB) HasProjectState

func (d *DB) HasProjectState() (bool, error)

HasProjectState reports whether any project state has ever been written to this database — that is, whether the metadata table holds a row.

It distinguishes a database that merely *exists* from one that holds a project. Opening a state.db creates the file and every table in it, so file existence alone says nothing: several code paths open a project's database for reasons unrelated to its state (executor reconciliation does it on every CLI invocation), and each of them leaves behind a complete but empty database. Callers that need "is there a project here" must ask this.

func (*DB) HubMeta added in v0.0.2

func (d *DB) HubMeta(key string) (string, bool, error)

HubMeta reads a hub-scoped metadata value, reporting whether it was present.

The bool matters and callers must read it: an absent record and a record holding the empty string mean different things to every caller this has, and the one that exists today reads "no probe has run" versus "a probe wrote something unparsable" — which get different messages and different remedies.

func (*DB) LastMaintenanceLog

func (d *DB) LastMaintenanceLog() (*MaintenanceLogEntry, error)

LastMaintenanceLog returns the most recent maintenance_log row. Returns (nil, nil) when no maintenance has been recorded yet — callers should treat that as "schedule a maintenance run on first invocation".

func (*DB) LatestAuditAnchor

func (d *DB) LatestAuditAnchor() (AuditAnchor, error)

LatestAuditAnchor returns the most recent anchor, or ErrAuditAnchorNotFound when the trail has never been pruned.

"Most recent" is by pruned_through_id rather than by id: the anchor that governs the live table is the one that cut furthest into it, and ordering by the boundary rather than by insertion order means an out-of-order write cannot make a stale anchor authoritative.

func (*DB) ListAPITokens

func (d *DB) ListAPITokens() ([]APITokenRow, error)

ListAPITokens returns every token, newest first. Revoked and expired rows are included: an operator auditing access needs to see what was withdrawn, and the caller decides how to present it.

func (*DB) ListAgentCredentials

func (d *DB) ListAgentCredentials() ([]AgentCredentialRow, error)

ListAgentCredentials returns every enrolled agent, oldest first.

func (*DB) ListAuditAnchors

func (d *DB) ListAuditAnchors() ([]AuditAnchor, error)

ListAuditAnchors returns every anchor in ascending boundary order. Used by `cloop hub audit anchors` and by the verifier's report so an operator can see the full retention history, not just the newest cut.

func (*DB) ListAuditEvents

func (d *DB) ListAuditEvents(f AuditFilter) (rows []AuditEvent, total int, err error)

ListAuditEvents returns rows matching the filter, plus the unfiltered total count for paging UIs.

func (*DB) ListBrokerGrants

func (d *DB) ListBrokerGrants() ([]BrokerGrantRow, error)

ListBrokerGrants returns every grant, including revoked and expired ones.

Filtering is the broker's job, not the store's: an audit UI listing "who had access" needs the rows a policy filter would drop, and a store that silently hid them would make that question unanswerable.

func (*DB) ListBrokerSecrets

func (d *DB) ListBrokerSecrets() ([]BrokerSecretRow, error)

ListBrokerSecrets returns every stored secret, name-ordered.

func (*DB) ListBrokerSecretsNotUnderKey

func (d *DB) ListBrokerSecretsNotUnderKey(keyID string, limit int) ([]SealedRow, error)

ListBrokerSecretsNotUnderKey returns secrets still sealed under some other key. Ordered by ID so a stuck row appears in the same position each round, which makes an operator's "it keeps failing on the same one" observation true rather than coincidental.

func (*DB) ListCIExchanges added in v0.0.2

func (d *DB) ListCIExchanges(limit int) ([]CIExchangeRow, error)

ListCIExchanges returns the most recent exchanges, newest first.

func (*DB) ListCIPipelineRules added in v0.0.2

func (d *DB) ListCIPipelineRules() ([]CIPipelineRuleRow, error)

ListCIPipelineRules returns every rule in evaluation order: oldest first, then by ID.

The order is the matching order, and it is stable across restarts on purpose. "First match wins" is only a meaningful rule if two hubs reading the same table agree about which rule is first.

func (*DB) ListEgressGrants

func (d *DB) ListEgressGrants() ([]EgressGrantRow, error)

ListEgressGrants returns every grant, including revoked and expired ones.

Filtering is the broker's job for the same reason it is in ListBrokerGrants: an audit reader asking "who could reach the Internet last quarter" needs exactly the rows a policy filter would drop.

func (*DB) ListEnrollmentTokens

func (d *DB) ListEnrollmentTokens() ([]EnrollmentTokenRow, error)

ListEnrollmentTokens returns every token, newest first.

func (*DB) ListEvents

func (d *DB) ListEvents(offset, limit int) (rows []EventRow, total int, err error)

ListEvents returns events in reverse-chronological order (latest first). limit caps the page size; offset skips past the most recent N. total is the total number of events in the journal so callers can drive infinite- scroll UIs.

func (*DB) ListExecutorHandles

func (d *DB) ListExecutorHandles(executorID string) ([]ExecutorHandleRow, error)

ListExecutorHandles returns rows owned by executorID, oldest first. Passing "" returns every row, which is what a cross-driver sweep wants.

Oldest-first matters: an orphan sweep applies a grace period to started_at, and processing in dispatch order means the first row it examines is the one most likely to have aged past it.

func (*DB) ListExecutorHealth

func (d *DB) ListExecutorHealth() ([]ExecutorHealthRow, error)

ListExecutorHealth returns every health row ordered by executor ID.

func (*DB) ListExecutorResourceLimits added in v0.0.3

func (d *DB) ListExecutorResourceLimits() ([]ExecutorResourceLimit, error)

ListExecutorResourceLimits returns every recorded executor ceiling, newest first, for the admin-facing panel.

func (*DB) ListExecutorSandboxes added in v0.0.3

func (d *DB) ListExecutorSandboxes() ([]ExecutorSandbox, error)

ListExecutorSandboxes returns every recorded configuration, newest first, so the fleet view can render each card's containment in one query rather than one per executor.

func (*DB) ListExecutorSessions

func (d *DB) ListExecutorSessions(executorID string, onlyRunning bool) ([]ExecutorSessionRow, error)

ListExecutorSessions returns sessions ordered by start time then ID. An empty executorID lists every executor's sessions; onlyRunning restricts the result to work that is still in flight, which is what failover and capacity checks ask for.

func (*DB) ListExecutors

func (d *DB) ListExecutors() ([]ExecutorRow, error)

ListExecutors returns every persisted executor ordered by ID.

func (*DB) ListGrantRequestUses added in v0.0.2

func (d *DB) ListGrantRequestUses(requestID string) ([]GrantRequestUseRow, error)

ListGrantRequestUses returns the leases recorded against one request, or — when requestID is empty — every recorded use.

func (*DB) ListGrantRequests added in v0.0.2

func (d *DB) ListGrantRequests() ([]GrantRequestRow, error)

ListGrantRequests returns every request, newest first.

Unfiltered, for the same reason ListBrokerGrants is: deciding which requests a caller may see is an authorisation question, and a store that pre-filtered would make "show me everything that was ever asked for" — the review question — unanswerable.

func (*DB) ListKEKs

func (d *DB) ListKEKs() ([]KEKRow, error)

ListKEKs returns every KEK, newest first.

func (*DB) ListProjectExecutorBindings

func (d *DB) ListProjectExecutorBindings() ([]ProjectExecutorBinding, error)

ListProjectExecutorBindings returns every project→executor binding, ordered by project path.

func (*DB) ListProjectResourceLimits added in v0.0.2

func (d *DB) ListProjectResourceLimits() ([]ProjectResourceLimit, error)

ListProjectResourceLimits returns every recorded ceiling, newest first, for the operator-facing panel.

func (*DB) ListProviderCalls

func (d *DB) ListProviderCalls(offset, limit, taskID int, provider string) (rows []ProviderCallRow, total int, err error)

ListProviderCalls returns up to limit rows ordered most-recent-first, optionally filtered. The summary form omits the heavy prompt/response/headers columns so the list endpoint stays cheap even when the table grows large.

Filters:

  • taskID > 0: only calls bound to that task
  • provider != "": only calls from that provider

Returns total = count of all matching rows (regardless of limit/offset) so the UI can render pagination controls.

func (*DB) ListQuotaCounters

func (d *DB) ListQuotaCounters() ([]QuotaCounterRow, error)

ListQuotaCounters returns every counter row.

func (*DB) ListQuotaOverrides

func (d *DB) ListQuotaOverrides() ([]QuotaOverrideRow, error)

ListQuotaOverrides returns every override, ordered by identity so the admin panel renders the same way on every load.

func (*DB) ListRoleBindings added in v0.0.2

func (d *DB) ListRoleBindings() ([]RoleBindingRow, error)

ListRoleBindings returns every binding.

Denies sort first so that a human reading the output — and the resolver's own scan — meets the rows that take away authority before the rows that grant it. Within an effect the order is stable by claim, value and id so repeated listings do not shuffle.

func (*DB) ListRotations

func (d *DB) ListRotations(limit int) ([]RotationRow, error)

ListRotations returns rotation records, newest first.

func (*DB) ListSecretLeaseDirs

func (d *DB) ListSecretLeaseDirs() ([]SecretLeaseDirRow, error)

ListSecretLeaseDirs returns every recorded directory, oldest first.

At startup this is precisely the orphan set: a hub that shut down cleanly deleted its rows as it wiped, so anything still here outlived the process that created it.

func (*DB) ListSessions

func (d *DB) ListSessions() ([]SessionRow, error)

ListSessions returns every session, most recently active first.

func (*DB) ListSessionsNotUnderKey

func (d *DB) ListSessionsNotUnderKey(keyID string, limit int) ([]SealedRow, error)

ListSessionsNotUnderKey returns sessions whose sealed refresh token is under some other key.

func (*DB) ListTelemetrySessions added in v0.0.2

func (d *DB) ListTelemetrySessions(source string, limit int) ([]TelemetrySession, error)

ListTelemetrySessions rolls events up by session, most recently active first.

func (*DB) LoadProviderCall

func (d *DB) LoadProviderCall(id int64) (*ProviderCallRow, error)

LoadProviderCall returns the full row (including prompt, response, headers) for a single audit-log id. Returns sql.ErrNoRows-wrapped error when missing.

func (*DB) LoadSpendCursor added in v0.0.2

func (d *DB) LoadSpendCursor(projectPath string) (SpendCursor, bool, error)

LoadSpendCursor returns the cursor for projectPath, or ok=false when the hub has never dispatched a run for it.

ok=false is not an error: it is the normal state of a project the hub only reads, and of every project on a hub that has just been upgraded. The caller charges nothing until a dispatch opens a cursor, which is the safe direction — the alternative would be charging an identity the hub had to guess.

func (*DB) LoadState

func (d *DB) LoadState() (*State, error)

func (*DB) LoadStateLite

func (d *DB) LoadStateLite() (*State, error)

LoadStateLite is like LoadState but skips loading the per-step rows (which on long-running projects can be megabytes of Output strings). The returned State has Steps == nil; StepCount and LastStepTime are filled from cheap aggregate SQL so handlers that only render counts or check last-activity for health don't pay for decoding every Output (Task 20125).

func (*DB) LoadTask

func (d *DB) LoadTask(id int) (*pm.Task, error)

LoadTask returns a single task by ID. Returns ErrTaskNotFound if the task does not exist. Useful from HTTP handlers where the typical 404 path is "the requested task does not exist".

func (*DB) LookupKill

func (d *DB) LookupKill(taskID int) (KillRequest, bool, error)

LookupKill returns the pending kill for taskID and whether a row exists. Used by the orchestrator after a worker exits to recover the operator's chosen target_status. Returns (KillRequest{}, false, nil) when no row.

func (*DB) MaxAuditID

func (d *DB) MaxAuditID() (int64, error)

MaxAuditID returns the largest id currently in the table, or 0 if empty. Used by the `events tail --follow` poller to discover new rows.

func (*DB) MaxCostRowID added in v0.0.2

func (d *DB) MaxCostRowID() (int64, error)

MaxCostRowID returns the highest costs.id present, or 0 when the table is empty. It seeds a drain watermark at a point that means "everything already here is somebody else's business", so a hub adopting an existing project does not retroactively bill a tenant for spend that predates it.

func (*DB) MonthlyCosts

func (d *DB) MonthlyCosts(year, month int) ([]CostEntry, error)

MonthlyCosts returns cost entries for the given UTC year/month.

func (*DB) OffboardIdentity added in v0.0.2

func (d *DB) OffboardIdentity(w OffboardWrite) (OffboardApplied, error)

OffboardIdentity severs the listed sessions and tokens and writes the deny binding, in one transaction, chaining the caller's audit events into the same commit.

Ordering inside the transaction is deliberate: the deny binding is written first. Within a transaction the order is invisible to other readers, but it is not invisible to a crash-and-retry: if the commit is lost and the operator re-runs, the surface that matters most for a *future* sign-in is the one already expressed in the row they will re-write identically (the binding id is content-addressed, so re-writing is idempotent).

func (*DB) OpenExecutorSession

func (d *DB) OpenExecutorSession(row ExecutorSessionRow) error

OpenExecutorSession records work starting on an executor.

This is a plain INSERT, not an upsert: a session id identifies one attempt, and silently overwriting an existing row would erase the claim token a supervisor may already be holding for the in-flight attempt.

func (*DB) OpenSpendCursor added in v0.0.2

func (d *DB) OpenSpendCursor(projectPath, identity string, startAt int64) error

OpenSpendCursor records who is paying for the run about to start and where its billing begins, replacing any previous cursor for the project.

startAt is the project ledger's current MAX(id) — everything already there belongs to an earlier run and must not be charged to this one. Passing it in rather than reading it here keeps this package free of any assumption about which database the cost rows live in: the control plane holds the cursor, the project holds the ledger, and they are different files.

func (*DB) PendingKills

func (d *DB) PendingKills() ([]KillRequest, error)

PendingKills returns all currently-pending abort requests, oldest first. The orchestrator's poller calls this every fast tick.

func (*DB) PingContext

func (d *DB) PingContext(ctx context.Context) error

PingContext runs a `SELECT 1` against the underlying connection, honouring ctx for cancellation/deadline. Used by readiness probes (e.g. /readyz) to verify the SQLite store is reachable.

Intentionally does NOT acquire d.mu: *sql.DB is concurrency-safe, and the mutex is reserved for transactional read-modify-write ops in this package. Holding it here would block readiness on whichever long-running write happens to own the lock — exactly what a probe must NOT do. The ctx timeout bounds wait time on the single underlying connection (we set MaxOpenConns(1)) when it is busy.

func (*DB) PlanAuditPrune

func (d *DB) PlanAuditPrune(cutoff time.Time) (AuditPruneCandidate, error)

PlanAuditPrune resolves a time cutoff to a contiguous id prefix.

Rows strictly older than cutoff are eligible. The boundary is the largest such id, and because ids are assigned monotonically under d.mu while timestamps come from the same clock, "older than cutoff" and "id <= boundary" describe the same set — but we count the set explicitly rather than assuming it, so a clock that went backwards produces a visibly wrong count instead of a silently over-broad delete.

func (*DB) PrimaryKEK

func (d *DB) PrimaryKEK() (KEKRow, bool, error)

PrimaryKEK returns the current primary, if any.

Separate from ListKEKs because it sits on the seal path: every seal re-reads it to confirm this process has not fallen behind a rotation run elsewhere, so it has to be one indexed lookup and not a scan.

func (*DB) ProjectExecutor

func (d *DB) ProjectExecutor(projectPath string) (string, bool, error)

ProjectExecutor returns the executor ID bound to projectPath. The boolean is false when the project has no binding, which callers treat as "use the registry default" — distinct from an error, which means the lookup itself failed and must not be interpreted as "unbound".

func (*DB) ProjectResourceCeiling added in v0.0.2

func (d *DB) ProjectResourceCeiling(projectPath string) (executor.ResourceCeiling, bool, error)

ProjectResourceCeiling returns the ceiling for projectPath.

The boolean is false when the project has no ceiling, which the dispatch path reads as "the fleet ceiling alone applies" — deliberately distinct from an error, which means the lookup failed and must not be read as "uncapped". A storage fault that silently became "no limit" would remove the policy exactly when the hub is least healthy.

func (*DB) PromoteKEK

func (d *DB) PromoteKEK(id string, at time.Time) error

PromoteKEK makes id the sole primary.

Demotion and promotion happen in one transaction because the partial unique index permits only one primary: doing them in two statements outside a transaction would leave a window with none (a hub starting in that window mints a third key) or, if ordered the other way, fail on the index. The index is what makes this correct under concurrency rather than merely usually correct.

func (*DB) PruneAuditPrefix

func (d *DB) PruneAuditPrefix(a AuditAnchor) (AuditAnchor, error)

PruneAuditPrefix deletes audit_events with id <= throughID and records an anchor describing the cut.

The caller must already have sealed the prefix; this function will not delete rows without an export digest, because an anchor with no evidence behind it is indistinguishable from a cover-up and the whole point of the anchor is to be checkable. Pass a.ExportSHA256 and a.ExportPath from the completed seal.

Delete and anchor happen in one transaction: a crash between them would otherwise leave either a truncated chain nothing explains, or an anchor claiming a truncation that did not happen. Both verify as tampering, and both would be this function's fault rather than an attacker's.

Returns the anchor as written, with ID, RetainedFromID and the hashes filled in.

func (*DB) PruneCIExchanges added in v0.0.2

func (d *DB) PruneCIExchanges(keep int) (int, error)

PruneCIExchanges keeps the newest keep rows and deletes the rest.

The exchange log is a debugging surface, not the audit trail, so it is bounded by count rather than by age: an operator debugging a pipeline wants the last N attempts, and a hub nobody has federated against in a month should still be able to show the exchange that failed.

func (*DB) PruneCosts added in v0.0.2

func (d *DB) PruneCosts(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)

PruneCosts bounds the cost ledger.

The ledger is mirrored to .cloop/costs.jsonl, so a pruned row is not the last record of the spend — which is why bounding it at all is defensible. Two readers still argue for a generous ceiling. `cloop cost report` aggregates over the whole table, so pruning shortens the history it can show; and the hub's spend enforcement tracks its booking position in project_spend_cursor.last_cost_row_id, reading forward from it with ReadCostsAfterID. Deleting the oldest rows cannot strand that cursor unless the hub has fallen more than MaxRows behind, which is a broken hub rather than a busy one — but it is the reason this prune takes the oldest rows and never the newest.

func (*DB) PruneEvents added in v0.0.2

func (d *DB) PruneEvents(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)

PruneEvents bounds the unified event history.

func (*DB) PruneProviderCalls added in v0.0.2

func (d *DB) PruneProviderCalls(p ProviderCallRetention, now time.Time, dryRun bool) (ProviderCallPruneResult, error)

PruneProviderCalls strips aged bodies and then bounds the rows.

The two limits are applied in that order and both are honoured: a row that is old enough to lose its bodies and also falls outside the row ceiling is deleted, and the bytes are reported once — the strip empties the columns before the delete measures what the row still held.

func (*DB) PruneQuotaCounters

func (d *DB) PruneQuotaCounters(bucket string) error

PruneQuotaCounters deletes daily counters older than bucket. Gauge rows (bucket = ”) are never pruned: they are live state, not history, and ” sorts before every date so an unguarded comparison would delete all of them.

func (*DB) PruneSteps added in v0.0.2

func (d *DB) PruneSteps(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)

PruneSteps bounds the steps table.

Callers must not run this while an orchestrator is executing against the same project. steps is the one table here that is not purely append-only: a run holds the whole step history in memory (state.Load reads every row) and saveStateLocked upserts all of it on each save, so rows deleted underneath a live run come straight back on its next write. The janitor skips this step for a running project for that reason.

func (*DB) PruneTelemetry added in v0.0.2

func (d *DB) PruneTelemetry(cutoff time.Time) (int64, error)

PruneTelemetry deletes events older than cutoff and returns how many went. The operator-invoked companion to the automatic row ceiling: `cloop hub telemetry prune` for when a trail should go now rather than when it ages out.

The age half of the same policy now also runs unattended — see PruneTelemetryRows and the janitor's telemetry step (Task 20291). Both go through rowretention.go's id-bounded path rather than comparing the RFC3339Nano text column directly, because a string comparison on that column is only an ordering if every writer used identical UTC formatting, and a row whose timestamp will not parse must stop the prune rather than be swept into it.

func (*DB) PruneTelemetryRows added in v0.0.2

func (d *DB) PruneTelemetryRows(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)

PruneTelemetryRows applies age and count retention to the browser telemetry trail.

trimTelemetryTx already caps the table at TelemetryMaxRows on the write path, because a public ingest endpoint must not be able to fill a disk between two janitor passes. This is the age half of the policy, which no write-time cap can express: a debugging trail that has gone quiet should still age out.

func (*DB) PutAPIToken

func (d *DB) PutAPIToken(row APITokenRow) error

PutAPIToken inserts a token row. It is an insert, not an upsert: a token's secret, roles, and scope are fixed at mint time, and silently rewriting an existing row would turn "re-run the create call" into an undetectable privilege change on a credential already in circulation. Rotating means minting a new token and revoking the old one.

func (*DB) PutAgentCredential

func (d *DB) PutAgentCredential(row AgentCredentialRow) error

PutAgentCredential inserts or updates an agent credential row.

func (*DB) PutBrokerGrant

func (d *DB) PutBrokerGrant(row BrokerGrantRow) error

PutBrokerGrant inserts or replaces a grant.

func (*DB) PutBrokerSecret

func (d *DB) PutBrokerSecret(row BrokerSecretRow) error

PutBrokerSecret inserts or replaces a secret.

The unique index on name is what makes a duplicate a storage error rather than a silently-shadowed second row, so a name collision surfaces here even if two processes race past the broker's own check.

func (*DB) PutCIExchange added in v0.0.2

func (d *DB) PutCIExchange(row CIExchangeRow) error

PutCIExchange records a federation attempt.

func (*DB) PutCIPipelineRule added in v0.0.2

func (d *DB) PutCIPipelineRule(row CIPipelineRuleRow) error

PutCIPipelineRule inserts or replaces a rule.

Unlike PutAPIToken this is an upsert, because a rule is a policy an operator edits rather than a credential in circulation: rewriting one is the intended operation, and the row carries no secret whose replacement could go unnoticed. What editing a rule must *also* do — revoke the sessions already minted under it — is the caller's job, because this layer does not know the live registry exists.

func (*DB) PutEgressGrant

func (d *DB) PutEgressGrant(row EgressGrantRow) error

PutEgressGrant inserts or replaces an egress grant.

func (*DB) PutEnrollmentToken

func (d *DB) PutEnrollmentToken(row EnrollmentTokenRow) error

PutEnrollmentToken inserts or updates a token row.

func (*DB) PutExecutorHandle

func (d *DB) PutExecutorHandle(row ExecutorHandleRow) error

PutExecutorHandle inserts or replaces the row for row.HandleID.

Unlike OpenExecutorSession this *is* an upsert. A driver may re-record a handle it already owns — rehydration re-writes rows so updated_at reflects the adopting control plane, and a Start that retries against the same handle id must not fail on a duplicate key. There is no claim token here to clobber: the row carries identity, not ownership.

func (*DB) PutExecutorHealth

func (d *DB) PutExecutorHealth(row ExecutorHealthRow) error

PutExecutorHealth inserts or updates the health row for row.ExecutorID.

The row is written wholesale rather than merged field-by-field: the prober computes the complete verdict (state, reason, failure count, transition time) in one place, and a partial update would let a stale field outlive the observation that produced it.

func (*DB) PutGrantRequest added in v0.0.2

func (d *DB) PutGrantRequest(row GrantRequestRow) error

PutGrantRequest inserts or replaces a request by ID.

func (*DB) PutGrantRequestUse added in v0.0.2

func (d *DB) PutGrantRequestUse(row GrantRequestUseRow) error

PutGrantRequestUse records that a lease redeemed an approved request's grant.

Upsert on (request_id, lease_id): a lease is leased once and then renewed every few minutes, and each renewal should move last_seen rather than add a row. first_seen is preserved by the conflict clause for the same reason RevokeBrokerGrant refuses to move a revocation timestamp — when a credential first came into use is a fact, and a renewal must not rewrite it.

func (*DB) PutKEK

func (d *DB) PutKEK(row KEKRow) error

PutKEK inserts or replaces a KEK record.

The WHERE clause on the upsert makes retirement terminal. Without it a PutKEK against a retired id would restore its salt and state, un-doing a crypto-shred with an ordinary write — the one operation in this file that is supposed to be irreversible.

func (*DB) PutQuotaCounter

func (d *DB) PutQuotaCounter(row QuotaCounterRow) error

PutQuotaCounter writes one counter's current value. A value at or below zero deletes the row instead of storing a zero: an identity holding nothing should not keep a row per resource forever, and a row that says 0 and a missing row must not be two different things the reader has to reconcile.

func (*DB) PutQuotaOverride

func (d *DB) PutQuotaOverride(row QuotaOverrideRow) error

PutQuotaOverride inserts or replaces one identity's override.

func (*DB) PutRoleBinding added in v0.0.2

func (d *DB) PutRoleBinding(row RoleBindingRow) (RoleBindingRow, error)

PutRoleBinding inserts or replaces a binding, returning the row as stored.

The caller supplies already-normalized claim and value: normalization is pkg/authz's definition of when two bindings are the same one, and a second implementation here would silently disagree with it at exactly the moment an operator is trying to override a configured binding.

func (*DB) PutRotation

func (d *DB) PutRotation(row RotationRow) error

PutRotation inserts or updates a rotation record.

func (*DB) PutSecretLeaseDir

func (d *DB) PutSecretLeaseDir(row SecretLeaseDirRow) error

PutSecretLeaseDir records the intent to materialise a lease at row.Dir.

Call it before anything writes plaintext there. The ordering is the whole point of the table: a row without a directory is harmless and self-clearing, a directory without a row is an orphan nothing can attribute.

func (*DB) PutSession

func (d *DB) PutSession(row SessionRow) error

PutSession inserts a session row.

An insert rather than an upsert: the id is a digest of 256 random bits, so a collision is not a case worth writing code for, while silently overwriting one would replace a live session's subject and roles with another's.

func (*DB) QueryTelemetry added in v0.0.2

func (d *DB) QueryTelemetry(f TelemetryFilter) ([]telemetry.Event, int, error)

QueryTelemetry returns one page of events, newest first, and the total number of rows matching the filter so a caller can render "showing 200 of 4,812".

func (*DB) ReadAuditRange

func (d *DB) ReadAuditRange(fromID, throughID int64, limit int) ([]AuditEvent, error)

ReadAuditRange returns up to limit rows with fromID <= id <= throughID, in ascending id order.

It exists next to ListAuditEvents rather than reusing it because ListAuditEvents runs an unfiltered COUNT(*) on every call to populate its paging total. That is right for a UI page and wrong for a cursor: sealing a 1.09M-row prefix in batches would run a full-table count per batch. Paging by id rather than by OFFSET matters for the same reason — OFFSET re-walks the skipped rows every time.

func (*DB) ReadCosts

func (d *DB) ReadCosts() ([]CostEntry, error)

ReadCosts returns all cost entries ordered by timestamp ascending.

func (*DB) ReadCostsAfterID added in v0.0.2

func (d *DB) ReadCostsAfterID(afterID int64, limit int) ([]CostEntry, error)

ReadCostsAfterID returns cost entries with id > afterID, oldest first.

This is the hub's spend drain (Task 20264): it books everything it has not booked before and remembers the highest id it saw. Ordering by id rather than timestamp is what makes the watermark sound — see CostEntry.RowID.

limit bounds one drain so a project whose ledger grew while the hub was down cannot pull an unbounded result set into memory; the caller advances its watermark and comes back for the rest.

func (*DB) ReadCostsSince

func (d *DB) ReadCostsSince(since time.Time) ([]CostEntry, error)

ReadCostsSince returns cost entries with timestamp >= since, ordered ascending.

func (*DB) ReconstructTask added in v0.0.2

func (d *DB) ReconstructTask(taskID int) (TaskStory, error)

ReconstructTask assembles one task's whole story from audit_events alone.

Returns a story with no events — rather than an error — for a task id the trail has never heard of. "No rows" is a legitimate and important answer: it is what an auditor sees for work that predates the trail, and turning it into an error would make the absence of evidence indistinguishable from a failure to look.

func (*DB) RecordEvent

func (d *DB) RecordEvent(row EventRow) error

RecordEvent appends one row to the events journal. Best-effort: the caller is expected to ignore the returned error (event-recording must never block the orchestrator). Idempotency is NOT guaranteed — duplicate calls record duplicate rows. The auto-increment id ordering is the source of truth for "what happened first" on a single host.

func (*DB) RedeemEnrollmentToken

func (d *DB) RedeemEnrollmentToken(id, agentID string, at time.Time) error

RedeemEnrollmentToken atomically claims a token for agentID.

The single-use guarantee lives entirely in this statement's WHERE clause. SQLite serialises writers, so of two concurrent redemptions exactly one finds redeemed_at = ” and updates a row; the other matches nothing and gets ErrEnrollmentSpent. A read-then-write implementation would let both succeed and hand two devices the same identity, which is the precise attack that single-use tokens exist to prevent.

func (*DB) ReleaseHubLease

func (d *DB) ReleaseHubLease(scope, instanceID string, at time.Time) (bool, error)

ReleaseHubLease gives up a lease held by instanceID, leaving the row in place with released_at set.

Scoped to instanceID so a late release from a hub that already lost the lease cannot free the lease its successor now holds — the same fencing discipline as RenewHubLease, applied to the path most likely to run last.

func (*DB) RemoveExecutorAudience added in v0.0.3

func (d *DB) RemoveExecutorAudience(executorID, kind, value string) error

RemoveExecutorAudience withdraws a principal. Removing one that was never admitted is not an error.

Removing the *last* principal makes the executor unrestricted again, which is a widening of access rather than a narrowing. That is the correct reading of "the list is now empty" and it is why the REST layer audits a removal that empties the list distinctly from one that does not.

func (*DB) RenewHubLease

func (d *DB) RenewHubLease(scope, instanceID string, at time.Time) (bool, error)

RenewHubLease advances the heartbeat for a lease this instance holds.

Returning false is the fencing signal, and callers must treat it as such: it means the row is no longer ours — taken over while we were paused, cleared by an operator, or released by us already — and the process must stop acting as the control plane rather than retry. It is deliberately not an error, because it is not one: it is an answer.

func (*DB) ReplaceBrokerSecretSealed

func (d *DB) ReplaceBrokerSecretSealed(id string, expect, next SealedRow) (bool, error)

ReplaceBrokerSecretSealed swaps a secret's envelope under compare-and-swap.

The WHERE clause matches the old key ID *and the exact ciphertext*, not just the ID. Matching the key alone would be a lost-update bug with teeth: a rotation that read a row, and an operator who re-minted that credential while the rotation was mid-flight, would race — and the rotation would win, silently restoring the superseded credential under a fresh wrap, with every column looking correct afterwards. Comparing the bytes turns that race into a no-op the rotator counts as skipped.

func (*DB) ReplaceQuotaGauges

func (d *DB) ReplaceQuotaGauges(rows []QuotaCounterRow) error

ReplaceQuotaGauges swaps every gauge row (bucket = ”) for rows in one transaction.

A transaction rather than delete-then-insert because the intermediate state — no gauge counters at all — is a state in which every tenant has unlimited headroom. On a hub reconciling at startup that window is short, but it is exactly the window an attacker watching for a restart would aim at, and making it atomic costs nothing.

func (*DB) ReplaceSessionSealed

func (d *DB) ReplaceSessionSealed(id string, expect, next SealedRow) (bool, error)

ReplaceSessionSealed swaps a session's sealed refresh token under compare-and-swap, with the same reasoning as above: a session that refreshed its token mid-rotation must not have the previous token restored.

func (*DB) RequestKill

func (d *DB) RequestKill(req KillRequest) error

RequestKill upserts one abort request. Repeated calls for the same task ID overwrite target_status / requested_at — the latest request wins. Returns nil when the row was written.

func (*DB) RetireKEK

func (d *DB) RetireKEK(id string, at time.Time) error

RetireKEK marks a KEK retired and destroys its salt.

The reference check runs inside the retiring transaction, which is the difference between a guarantee and a race: checking first and retiring after would let a lease minted in between end up sealed under a key whose salt is already gone, and that material would be unrecoverable with no error anywhere to explain it.

Blanking the salt is the point of retirement. A state flag alone could be ignored by a future code path or flipped back by a hand-edited database; without the salt the key cannot be derived from the passphrase at all, so retirement is a property of the cryptography rather than of the control flow.

func (*DB) RevokeAPIToken

func (d *DB) RevokeAPIToken(id string, at time.Time) error

RevokeAPIToken stamps a token as revoked. Revoking an already-revoked token keeps the original timestamp: the first withdrawal is the one that matters, and overwriting it would let a later call quietly move the date an auditor reads as "when did this stop working".

Returns ErrAPITokenNotFound when no such token exists, so a caller cannot mistake a typo for a successful revocation.

func (*DB) RevokeAgentCredential

func (d *DB) RevokeAgentCredential(agentID string, at time.Time) error

RevokeAgentCredential marks an agent's credential revoked.

func (*DB) RevokeBrokerGrant

func (d *DB) RevokeBrokerGrant(id string, at time.Time) error

RevokeBrokerGrant stamps a grant revoked.

The UPDATE is conditional on revoked_at being empty so a second revoke cannot move the timestamp forward. That matters for the audit trail: the moment access was withdrawn is a fact, and a retry must not rewrite it. Revoking an already-revoked grant is reported as success, because the caller's desired end state holds.

func (*DB) RevokeEgressGrant

func (d *DB) RevokeEgressGrant(id string, at time.Time) error

RevokeEgressGrant stamps a grant revoked.

Conditional on revoked_at being empty, matching RevokeBrokerGrant: the moment access was withdrawn is a fact, and a retry must not move it. Revoking an already-revoked grant reports success, because the caller's desired end state holds.

func (*DB) RevokeEnrollmentToken

func (d *DB) RevokeEnrollmentToken(id string, at time.Time) error

RevokeEnrollmentToken marks a token revoked, redeemed or not.

func (*DB) Role added in v0.0.2

func (d *DB) Role() Role

Role reports how this handle has been classified.

func (*DB) SaveState

func (d *DB) SaveState(s *State) error

func (*DB) SchemaStamp

func (d *DB) SchemaStamp() (SchemaStamp, error)

SchemaStamp returns provenance for the highest version recorded in this database: which migration last moved the schema, when, and by which build.

Returns a zero SchemaStamp and no error when the database has no recorded migrations at all — a brand-new file, which is not a fault.

func (*DB) SessionsDueForRefresh

func (d *DB) SessionsDueForRefresh(cutoff time.Time, limit int) ([]SessionRow, error)

SessionsDueForRefresh returns sessions whose IdP check is older than cutoff and that still hold a sealed refresh token, oldest first and at most limit rows.

The limit bounds one pass: a hub where every session comes due at once must spread the token-endpoint calls over several ticks rather than opening a thousand connections to the IdP in one burst, which the IdP would rightly treat as an attack.

func (*DB) SetBrokerMeta

func (d *DB) SetBrokerMeta(key, value string) error

SetBrokerMeta writes a broker-scoped metadata value.

func (*DB) SetConfigBlob

func (d *DB) SetConfigBlob(yamlBlob string) error

SetConfigBlob persists the YAML-serialised project config into the metadata table. The write is wrapped in a transaction so a crash mid-write leaves either the previous value or the new one — never a partial row.

func (*DB) SetExecutorResourceLimit added in v0.0.3

func (d *DB) SetExecutorResourceLimit(executorID string, c executor.ResourceCeiling, setBy string) error

SetExecutorResourceLimit records a ceiling for executorID, replacing any previous one. setBy is the identity that set it, for the audit trail.

A zero ceiling is stored rather than rejected, for the reason its project counterpart is: "considered and capped at nothing" and "never considered" are the same to the dispatch path and different to the admin reading the panel. Callers that mean "remove the policy" call ClearExecutorResourceLimit.

func (*DB) SetExecutorSandbox added in v0.0.3

func (d *DB) SetExecutorSandbox(executorID string, s executor.SandboxSettings, setBy string) error

SetExecutorSandbox records sandbox settings for executorID, replacing any previous ones. setBy is the identity that set them, for the audit trail.

Settings that configure nothing are stored rather than rejected, for the reason SetProjectResourceLimit stores a zero ceiling: "no row" and "a row that asserts nothing" mean the same thing at dispatch and different things to the admin reading the panel, who wants to see that someone looked at this executor. Callers meaning "remove the policy" call ClearExecutorSandbox.

func (*DB) SetHubMeta added in v0.0.2

func (d *DB) SetHubMeta(key, value string) error

SetHubMeta writes a hub-scoped metadata value, replacing any previous one.

func (*DB) SetProjectResourceLimit added in v0.0.2

func (d *DB) SetProjectResourceLimit(projectPath string, c executor.ResourceCeiling, setBy string) error

SetProjectResourceLimit records a ceiling for projectPath, replacing any previous one. setBy is the identity that set it, for the audit trail.

A zero ceiling is stored rather than rejected: "capped at nothing on every resource" and "no row" mean the same thing to the dispatch path, but they mean different things to the operator reading the panel, who wants to see that someone considered this project and decided it needed no cap. Callers that mean "remove the policy" call ClearProjectResourceLimit.

func (*DB) SizeStats

func (d *DB) SizeStats() (SizeStats, error)

SizeStats reads PRAGMA page_count, page_size, and freelist_count.

Connection-scoped pragmas — modernc.org/sqlite returns one row per query. We hold d.mu so the three reads stay consistent against concurrent writers in this process; concurrent writers in *other* processes can still extend the file between the page_count and freelist_count reads, but the resulting values remain individually valid (worst case: a slightly stale Bytes total).

func (*DB) SpendByIdentity added in v0.0.2

func (d *DB) SpendByIdentity(from, to time.Time) ([]IdentitySpend, error)

SpendByIdentity aggregates costs per identity over [from, to).

A zero `from` or `to` leaves that side of the window open, so the caller can ask for "today", "since Monday" or "everything" through one query. Rows with no identity aggregate under the empty string: they are reported as unattributed rather than dropped, because spend that happened is spend that happened and a total that silently omits it would not reconcile against the project ledger.

func (*DB) TaskRunID added in v0.0.2

func (d *DB) TaskRunID(taskID int) (string, bool, error)

TaskRunID reports the execution a task's latest attempt was dispatched into, and whether one was recorded.

Exported for the reconstruction in `cloop audit-log --task`, which needs the run id to pull the lease rows that belong to the same execution.

func (*DB) TouchAPIToken

func (d *DB) TouchAPIToken(id string, at time.Time) error

TouchAPIToken records that a token was used. Called off the verification path and coalesced by pkg/apitoken, so a failure here is logged by the caller and never surfaces as an authentication error — a lost timestamp is not a lost authorization decision.

func (*DB) TouchAgentCredential

func (d *DB) TouchAgentCredential(agentID string, at time.Time) error

TouchAgentCredential records a successful authentication.

func (*DB) TouchCIPipelineRule added in v0.0.2

func (d *DB) TouchCIPipelineRule(id string, at time.Time) error

TouchCIPipelineRule stamps a rule as having admitted a pipeline.

Failures are the caller's to ignore: last_matched_at is advisory, and a federation that succeeded must not be reported as failed because a bookkeeping write lost a race with a delete.

func (*DB) TouchExecutorHeartbeat

func (d *DB) TouchExecutorHeartbeat(id, status string, at time.Time) error

TouchExecutorHeartbeat records liveness for an enrolled executor and updates its status. Returns ErrExecutorNotFound when the ID is unknown, so a stale agent cannot resurrect a de-enrolled record by heartbeating.

func (*DB) TouchSession

func (d *DB) TouchSession(id string, t time.Time) error

TouchSession records that a session was used at t.

Called off the authentication path and throttled by the caller; a failure is advisory. The write is conditional on moving the timestamp forward so two concurrent requests cannot walk last_seen backwards — the ordering the caller's in-memory cache enforces within one process, enforced again here for the cross-process case.

func (*DB) UnbindProjectExecutor

func (d *DB) UnbindProjectExecutor(projectPath string) error

UnbindProjectExecutor removes the binding for projectPath. Unbinding a project that was never bound is not an error.

func (*DB) UpdateSessionRefresh

func (d *DB) UpdateSessionRefresh(id string, up SessionRefreshUpdate) error

UpdateSessionRefresh stores a rotated refresh token, stamps the check time, and — when the provider re-asserted them — replaces the row's claims. A nil sealed value clears the stored token, which is how a session that can no longer be revalidated stops being retried.

Clearing also blanks the key id. Leaving a stale one behind would make the row look, to the rotator and to `cloop hub key retire`, like material still sealed under a key that in fact protects nothing — which is exactly the kind of phantom reference that makes an operator conclude retirement is broken.

One statement, not two. The stamp records that the session was checked and the claims record what it was checked *as*; a row carrying the first without the second is a session that looks freshly re-authorized while still holding authority the IdP has withdrawn.

func (*DB) UpsertExecutor

func (d *DB) UpsertExecutor(row ExecutorRow) error

UpsertExecutor inserts or updates an executor row keyed by ID. CreatedAt is preserved on update: re-enrolling an agent must not rewrite its enrollment date.

func (*DB) UpsertTask

func (d *DB) UpsertTask(t *pm.Task) error

func (*DB) Vacuum

func (d *DB) Vacuum() error

Vacuum runs a full VACUUM on the database. Cannot be inside a transaction; the autocommit code path in modernc.org/sqlite handles this correctly. WAL mode is preserved across the VACUUM (SQLite re-applies the journal_mode pragma internally).

func (*DB) VacuumInto

func (d *DB) VacuumInto(outPath string) error

VacuumInto produces a transactionally-consistent copy of the database at outPath using SQLite's `VACUUM INTO 'path'` statement. The output is a single self-contained file (no WAL/SHM sidecars) and is the recommended hot-backup primitive when the C-level Online Backup API is not available.

outPath must not already exist — VACUUM INTO refuses to overwrite. The caller is responsible for clearing any stale destination first.

The path is interpolated into a SQL string literal because VACUUM INTO does not accept bound parameters. Single quotes in the path are escaped by doubling, matching SQLite's quoting rules.

func (*DB) VerifyAuditChain

func (d *DB) VerifyAuditChain() (AuditVerifyReport, error)

VerifyAuditChain walks audit_events in id order and recomputes each row's hash, comparing to the stored value. The first mismatch (or gap, or missing prev_hash linkage) stops the walk and is reported. Verification is read-only and holds the DB mutex only during the row scan.

A pruned chain (Task 20218) does not start at genesis, and the naive walk would report the first survivor as tampered. When an anchor exists the walk starts from it instead: the first row must be exactly the id the anchor pinned, and its prev_hash must be the row_hash of the last row the anchor removed. That is a stronger check than "tolerate a short chain" — it means a second, undeclared truncation just after the boundary is still caught, because the anchor says where the chain has to resume.

What this cannot do is prove the pruned prefix itself was honest; nothing inside the database can, since an attacker with write access could author both the truncation and the anchor. That is what the export digest is for, and VerifyAuditExports checks it.

func (*DB) WALCheckpointTruncate

func (d *DB) WALCheckpointTruncate() (string, error)

WALCheckpointTruncate flushes the WAL into the main database file and truncates the WAL to zero bytes. Used as the first step of a hot backup (Task 20115) so VACUUM INTO produces a snapshot that includes every committed transaction up to the moment of the backup, not just whatever had previously been folded into the main file.

PRAGMA wal_checkpoint returns three columns: busy (0 if checkpoint completed without contention), log_size (frames in the WAL before the checkpoint), and checkpointed (frames moved to the main DB). The string returned summarises those values for logging; a non-zero busy column is not an error per se — under heavy concurrent load the checkpoint can only flush as many frames as it could acquire — but it is reported so an operator can decide whether to retry.

type EgressGrantRow

type EgressGrantRow struct {
	ID           string
	Scope        string
	SubjectType  string
	SubjectValue string
	PolicyJSON   string
	ExpiresAt    string
	CreatedAt    string
	CreatedBy    string
	RevokedAt    string
}

EgressGrantRow is one row of egress_grants. Times are RFC3339 strings, matching the rest of the schema.

type EnrollmentTokenRow

type EnrollmentTokenRow struct {
	ID   string
	Name string
	// SecretHash is hex SHA-256 of the token secret, never the secret.
	SecretHash      string
	WorkDirRoot     string
	Labels          map[string]string
	CreatedAt       time.Time
	ExpiresAt       time.Time
	CreatedBy       string
	RedeemedAt      time.Time
	RedeemedAgentID string
	RevokedAt       time.Time
}

EnrollmentTokenRow is one minted enrollment token.

type EventRow

type EventRow struct {
	ID        int64     // primary key, assigned on insert (zero on the way in)
	Timestamp time.Time // when the event happened
	Type      EventType
	TaskID    int    // 0 when not task-bound
	TaskTitle string // empty when not task-bound
	Step      int    // NoStep (-1) when not step-bound; 0 is a real step
	Message   string // short, human-readable summary
	Details   string // free-form JSON blob (may be empty)
}

EventRow is one row in the events table.

type EventType

type EventType string

EventType is a string enum identifying the kind of journal entry. Readers must treat unknown values as benign — the table is intentionally extensible.

const (
	EventSessionStarted    EventType = "session_started"
	EventSessionPaused     EventType = "session_paused"
	EventSessionFailed     EventType = "session_failed"
	EventPlanComplete      EventType = "plan_complete"
	EventTaskStarted       EventType = "task_started"
	EventTaskDone          EventType = "task_done"
	EventTaskFailed        EventType = "task_failed"
	EventTaskSkipped       EventType = "task_skipped"
	EventTaskHeal          EventType = "task_heal"
	EventTaskKilled        EventType = "task_killed"
	EventTaskAdded         EventType = "task_added"
	EventTaskAddedExternal EventType = "task_added_external"
	EventTaskDeleted       EventType = "task_deleted"
	EventTaskStatusChange  EventType = "task_status_change"
	EventEvolveRoundStart  EventType = "evolve_round_start"
	EventEvolveDiscovered  EventType = "evolve_discovered"
	EventEvolveNoOp        EventType = "evolve_no_op"
	// EventWriteBack records what an isolated executor returned of a task's
	// work: the branch, the commit, and whether it merged.
	//
	// Its own type rather than a field on task_done, because the two answer
	// different questions and are allowed to disagree. A task can succeed and
	// have its work fail to come back — the harness ran, the push did not —
	// and a journal that folded the second into the first would have no way to
	// show an operator the run whose transcript was right and whose code was
	// lost. Making exactly that case visible is why this subsystem exists.
	EventWriteBack EventType = "write_back"

	// EventTaskBackground records work an agent harness left running after it
	// reported the task complete (Task 20205), and how that resolved.
	//
	// Its own type rather than a detail on task_done because it explains the
	// two things a status alone cannot: why a task sat running long after its
	// agent had stopped talking, and why a task whose output ended in
	// TASK_DONE was nonetheless not accepted as done.
	EventTaskBackground EventType = "task_background"

	// EventTaskAborted records a run that never produced work: a provider
	// usage limit or quota, a rejected credential, a harness that refused to
	// start, or output with no diff and no artifact behind it (Task 20211).
	//
	// Its own type rather than task_failed because the two are different
	// claims and are acted on differently. A failure is a judgement about the
	// work; an abort is the absence of any work to judge, so the task returns
	// to pending instead of staying terminal. Folding them together is what
	// let fourteen tasks be recorded as done whose entire stored summary was
	// "You've hit your limit" — and let auto-evolve plan follow-up work on
	// the belief that they had shipped.
	EventTaskAborted EventType = "task_aborted"

	// EventResourceCeiling records that an operator's resource ceiling lowered
	// what a workload was given (Task 20301).
	//
	// It is on the project's own journal rather than only in the hub's log
	// because the person it concerns is the developer whose sandbox got less
	// memory than its .cloop/sandbox.yaml asked for. To them an unannounced
	// clamp is indistinguishable from a slow machine, and there is nothing in
	// their repository that would explain it — the ceiling lives on the hub,
	// deliberately out of their reach. One row naming the resource, both
	// numbers and which ceiling bound it is the difference between a mystery
	// and an address to complain to.
	EventResourceCeiling EventType = "resource_ceiling"

	// EventProjectSeed records that a run dispatched to an isolating executor
	// went without the project state the hub would normally send with it
	// (Task 20316).
	//
	// On the project's journal for the same reason as the ceiling above: the
	// consequence lands on the developer and nothing in their repository
	// explains it. A sandbox that receives no seed finds no `.cloop/` in the
	// tree it cloned, so `cloop run` exits with "no cloop project found" —
	// which reads as a broken project rather than as an executor too old to
	// have been sent one. One row naming the executor and the upgrade is the
	// difference between that and an hour spent debugging an intact project.
	EventProjectSeed EventType = "project_seed"

	// EventProjectResult records what a run on an isolating executor sent back
	// of the project — which tasks it finished, what it added, what the hub
	// kept of its own instead — or why nothing came back (Task 20339).
	//
	// It is project_seed's other half. The seed is how the plan reaches a
	// device that cannot read the hub's database; this is how the run's
	// outcome returns to the database the dashboard renders. Without a row
	// here, a remote run that did its work and a remote run whose results were
	// lost look the same afterwards: a transcript ending "All tasks complete"
	// beside a plan whose tasks are all still pending.
	EventProjectResult EventType = "project_result"

	// EventFeatureCreated and EventFeatureRemoved record, on a project's own
	// journal, a feature of it being created or removed (Task 20341). They are
	// the parent's rows because the feature's own journal begins with its
	// creation and ends — deleted along with its worktree — at its removal.
	EventFeatureCreated EventType = "feature_created"
	EventFeatureRemoved EventType = "feature_removed"

	// EventFeaturePR records a feature's pull request being opened or updated,
	// on the feature's journal and its parent's.
	EventFeaturePR EventType = "feature_pr"
)

type ExecutorAudienceEntry added in v0.0.3

type ExecutorAudienceEntry struct {
	ExecutorID string    `json:"executor_id"`
	Kind       string    `json:"kind"`  // "user" | "group" | "role"
	Value      string    `json:"value"` // as the admin typed it
	AddedAt    time.Time `json:"added_at,omitempty"`
	AddedBy    string    `json:"added_by,omitempty"`
}

ExecutorAudienceEntry is one principal admitted to one executor, plus its provenance.

func (ExecutorAudienceEntry) Member added in v0.0.3

Member renders the entry as the claim predicate pkg/authz matches on.

type ExecutorAuditInput

type ExecutorAuditInput struct {
	// Action is the lifecycle verb: "enroll", "revoke", "cordon",
	// "uncordon", "drain", "bind", "unbind". It becomes the suffix of the
	// event type, so the whole family is filterable as event_type LIKE
	// 'executor.%' and each verb individually by exact match.
	Action string

	// ExecutorID is the fleet identifier the action targeted. Empty is
	// legitimate for "unbind", which names a project rather than a device.
	ExecutorID string

	// Actor is the acting identity, normally the OIDC subject label.
	Actor string

	// Detail is optional context: the device name, the cordon reason, the
	// project path a binding points at. Never credentials — the enrollment
	// token in particular must not appear here, and the central redaction
	// in MarshalAuditPayload enforces that even if a caller tries.
	Detail map[string]any
}

ExecutorAuditInput carries one executor-fleet decision to the audit log (Task 20167).

Executor lifecycle was the gap in the trail: enrolling a device grants an arbitrary machine the right to run project workloads and to request brokered credentials, and revoking, cordoning, draining, or re-binding one changes where a project's code executes. Those are exactly the events an operator needs to reconstruct — "who attached this machine, and when" — and none of them were recorded before.

type ExecutorHandleRow

type ExecutorHandleRow struct {
	HandleID   string
	ExecutorID string
	Driver     string
	// ExternalID is the name the runtime knows the workload by. Its shape is
	// driver-specific and nothing outside the owning driver interprets it.
	ExternalID  string
	ProjectPath string
	TaskID      int
	PID         int
	Image       string
	// MetaJSON is a marshalled map[string]string of driver-specific extras.
	// ” and '{}' both mean "none"; readers must tolerate both because the
	// column default is '{}' and a caller may write ”.
	MetaJSON string
	// SecretsJSON is a marshalled array of the workload's secret-lease
	// bindings — lease ids, variable names and paths, never values. This
	// package stores it as opaque text; pkg/executorstore owns the type.
	//
	// Unlike MetaJSON, ” and '[]' are *not* the same. ” means nobody
	// recorded what this workload held (a row from before 0031), and '[]'
	// means it held nothing. Collapsing them would let a revocation read an
	// unrecorded holder as a non-holder and report success; see 0031.
	SecretsJSON string
	StartedAt   time.Time
	// Deadline is when the workload's timeout expires; the zero time means
	// unbounded. Absolute, so a hub that was down does not restart the clock.
	Deadline  time.Time
	UpdatedAt time.Time
}

ExecutorHandleRow is the durable identity of one dispatched workload.

type ExecutorHealthRow

type ExecutorHealthRow struct {
	ExecutorID          string
	State               string
	Reason              string
	ConsecutiveFailures int
	LastSeen            time.Time
	LastProbe           time.Time
	StateChangedAt      time.Time
}

ExecutorHealthRow is the scheduler's probe-driven view of one executor. ExecutorID shares the identifier space with ExecutorRow.ID but is not a foreign key — see the file comment.

type ExecutorInventory

type ExecutorInventory struct {
	// AgentVersion is the cloop build the device reported at its last connect.
	// Empty means it reported none; the literal "1" means the placeholder that
	// pre-inventory agents sent. Both are "unknown", but only the second tells
	// an operator the device is definitely old.
	AgentVersion string `json:"agent_version,omitempty"`
	// OS and Arch are the device's GOOS/GOARCH.
	OS   string `json:"os,omitempty"`
	Arch string `json:"arch,omitempty"`
	// CPUs and MemoryMB are logical cores and total memory. Zero means the
	// agent could not detect it, which is never "too small".
	CPUs     int `json:"cpus,omitempty"`
	MemoryMB int `json:"memory_mb,omitempty"`
	// Harnesses are the agent CLIs on the device's PATH ("claude", "codex").
	Harnesses []string `json:"harnesses,omitempty"`
	// ContainerRuntimes are the container runtimes it can drive.
	ContainerRuntimes []string `json:"container_runtimes,omitempty"`
	// WorkDirRoot is the directory the agent confines every workload beneath.
	WorkDirRoot string `json:"workdir_root,omitempty"`
}

ExecutorInventory is what an edge device reported about itself, parsed out of its capability advertisement into typed, individually-queryable fields.

It is a nested struct rather than eight more fields on ExecutorRow because these travel together: they are refreshed as a set on every connect, they are all empty for a non-enrolled backend, and grouping them keeps "is this row's device inventory known" a single question.

Capabilities on ExecutorRow still holds the full advertisement. This is not a replacement for it but a projection of the parts that have to be selectable — see migration 0030 for why both exist.

func (ExecutorInventory) Known

func (i ExecutorInventory) Known() bool

Known reports whether anything was actually advertised. Used to decide between rendering an inventory and rendering nothing at all — a panel that showed "0 CPUs, no harnesses" for a device that never reported would be inventing a fault.

type ExecutorResourceLimit added in v0.0.3

type ExecutorResourceLimit struct {
	ExecutorID string                   `json:"executor_id"`
	Ceiling    executor.ResourceCeiling `json:"ceiling"`
	SetAt      time.Time                `json:"set_at,omitempty"`
	SetBy      string                   `json:"set_by,omitempty"`
}

ExecutorResourceLimit is one executor's ceiling plus its provenance.

type ExecutorRow

type ExecutorRow struct {
	ID            string            `json:"id"`
	Name          string            `json:"name"`
	Kind          string            `json:"kind"`
	Endpoint      string            `json:"endpoint,omitempty"`
	Status        string            `json:"status"`
	Capabilities  json.RawMessage   `json:"capabilities,omitempty"`
	Labels        map[string]string `json:"labels,omitempty"`
	LastHeartbeat time.Time         `json:"last_heartbeat,omitempty"`
	CreatedAt     time.Time         `json:"created_at"`
	EnrolledBy    string            `json:"enrolled_by,omitempty"`
	// Inventory is the parsed, queryable subset of what a device advertised.
	// Zero for backends that were configured rather than enrolled: a container
	// driver has no build version of its own, it runs this one.
	Inventory ExecutorInventory `json:"inventory,omitzero"`
}

ExecutorRow is one persisted execution backend.

type ExecutorSandbox added in v0.0.3

type ExecutorSandbox struct {
	ExecutorID string                   `json:"executor_id"`
	Settings   executor.SandboxSettings `json:"settings"`
	SetAt      time.Time                `json:"set_at,omitempty"`
	SetBy      string                   `json:"set_by,omitempty"`
}

ExecutorSandbox is one executor's sandbox configuration plus its provenance.

Settings is the part that travels to the device in a start frame; SetAt and SetBy never leave the control plane. See executor.SandboxSettings for why that split is in the types rather than in a convention.

type ExecutorSessionRow

type ExecutorSessionRow struct {
	ID          string
	ExecutorID  string
	HandleID    string
	ProjectPath string
	TaskID      int
	// ClaimToken gates the requeue latch. It is a concurrency token, not a
	// secret: it is stored and compared in plaintext because it authorises
	// nothing beyond winning one race.
	ClaimToken   string
	State        string
	Attempt      int
	StartedAt    time.Time
	UpdatedAt    time.Time
	EndedAt      time.Time
	RequeuedFrom string
	// SpecJSON is the marshalled executor.Spec the session was dispatched
	// with. Failover re-dispatches from it, so it must survive a
	// control-plane restart: a session whose spec lived only in the memory
	// of the process that started it could not be requeued by the process
	// that replaces it, which is exactly when requeueing matters most.
	SpecJSON string
}

ExecutorSessionRow is one unit of work in flight on an executor.

type GrantRequestRow added in v0.0.2

type GrantRequestRow struct {
	ID              string
	RequestedBy     string
	SecretID        string
	SecretName      string
	Kind            string
	SubjectType     string
	SubjectValue    string
	Scope           string
	ConstraintsJSON string
	TTLSeconds      int64
	Justification   string
	State           string
	CreatedAt       string
	ExpiresAt       string
	DecidedBy       string
	DecidedAt       string
	DecisionNote    string
	GrantID         string
	GrantExpiresAt  string
}

GrantRequestRow is one row of secret_grant_requests. Times are RFC3339 strings and durations are whole seconds, matching the rest of the schema.

type GrantRequestUseRow added in v0.0.2

type GrantRequestUseRow struct {
	RequestID   string
	LeaseID     string
	GrantID     string
	ExecutorID  string
	ProjectPath string
	// TaskID is the unit of work that held the lease, or 0 when the dispatch
	// was run-scoped rather than task-scoped. See the migration's note: 0 means
	// "not attributed", never "no task ran".
	TaskID    int
	FirstSeen string
	LastSeen  string
}

GrantRequestUseRow is one row of secret_grant_request_uses: one lease that redeemed the grant an approval produced.

type HubLeaseRow

type HubLeaseRow struct {
	Scope      string
	InstanceID string
	Hostname   string
	PID        int
	// BootID disambiguates a pid across reboots; ” when the platform does
	// not expose one. A liveness probe must not trust PID unless this matches
	// the probing host's current boot id.
	BootID  string
	Address string
	Version string

	AcquiredAt  time.Time
	HeartbeatAt time.Time
	// ReleasedAt is the instant of a graceful release; the zero time means the
	// lease is still held (or was abandoned without one).
	ReleasedAt time.Time
}

HubLeaseRow is one fence row: who holds the control plane, and since when.

func (HubLeaseRow) Held

func (r HubLeaseRow) Held() bool

Held reports whether the row represents a lease nobody has given up. It says nothing about whether the holder is alive — that is HeartbeatAt's job, and deciding it is pkg/hublease's.

type IdentitySpend added in v0.0.2

type IdentitySpend struct {
	Identity       string
	InputTokens    int
	OutputTokens   int
	ThinkingTokens int
	EstimatedUSD   float64
	Entries        int
}

IdentitySpend is one identity's total spend over a window.

type ImagePolicyDenialInput

type ImagePolicyDenialInput struct {
	// ProjectPath identifies the project whose .cloop/sandbox.yaml was
	// refused. It is the entity the row is filed under.
	ProjectPath string
	// Image is the reference as the project wrote it, unmodified — the
	// normalized form would hide exactly the homograph or confusion attempt
	// that is the reason to look.
	Image string
	// Rule is the imagepolicy rule that refused it ("registry-allowlist",
	// "digest-required", …). Kept as the same string the API and the error
	// text use, so filtering the trail and reading a bug report agree.
	Rule string
	// Reason is the human-readable observation.
	Reason string
	// Registry and Repository are the parsed components, empty when the
	// reference did not parse. They are what makes "show me every denial for
	// this registry" a query rather than a grep.
	Registry   string
	Repository string
	// Actor is the acting identity, normally the OIDC subject label.
	Actor string
}

ImagePolicyDenialInput carries one refused container image to the audit log (Task 20177).

This is the one sandbox decision worth a permanent row. The others describe a project asking for less than it could have; this one describes a project asking to execute code from somewhere the operator does not trust, and the question it has to answer later is "was this a typo in one repo, or did the same unknown registry get named across six projects in an hour". Only a durable, queryable trail answers the second.

type KEKRow

type KEKRow struct {
	ID         string
	Salt       string
	State      string
	CheckValue []byte
	CreatedAt  string
	CreatedBy  string
	RetiredAt  string
}

KEKRow is one row of broker_keks.

type KillRequest

type KillRequest struct {
	TaskID       int
	TargetStatus string // empty when the operator did not pick a final status
	RequestedAt  time.Time
	RequestedBy  string // free-form (UI client ID, "ui", etc.) — informational only
	// Attempt identifies the execution being aborted: the RFC3339Nano
	// started_at the requester observed on the task. The orchestrator honours
	// the row only while the task's started_at still matches, so a request
	// that outlives its run cannot cancel a later attempt of the same task.
	// Empty means "no attempt named" and is treated as stale (Task 20203).
	Attempt string
}

KillRequest is one pending manual-abort request keyed by task ID.

type MaintenanceLogEntry

type MaintenanceLogEntry struct {
	ID              int64
	Operation       string
	StartedAt       time.Time
	CompletedAt     time.Time
	PageCountBefore int64
	PageCountAfter  int64
	PageSize        int64
	BytesBefore     int64
	BytesAfter      int64
	Note            string
}

MaintenanceLogEntry mirrors one row of the maintenance_log table.

ID is auto-assigned by SQLite on insert. Operation is one of "vacuum", "analyze", or "vacuum+analyze" (we record the combined run as a single row). Note carries free-form context — typically the auto-mode reasoning string.

func (MaintenanceLogEntry) BytesFreed

func (e MaintenanceLogEntry) BytesFreed() int64

BytesFreed returns BytesBefore - BytesAfter (zero when the file did not shrink, e.g. because freelist was already empty).

type MigrateOptions

type MigrateOptions struct {
	// Target stops after the migration numbered Target. Zero means
	// LatestVersion — every embedded migration.
	Target int

	// AllowSchemaDowngrade suppresses the refusal to open a database whose
	// schema is ahead of this binary's. See schema_guard.go; the operator-
	// facing spelling is the CLOOP_ALLOW_SCHEMA_DOWNGRADE environment
	// variable, which Migrate and MigrateTo read on the caller's behalf.
	AllowSchemaDowngrade bool
}

MigrateOptions configures MigrateWithOptions.

type MigrationCompat added in v0.0.2

type MigrationCompat string

MigrationCompat says whether a migration can be tolerated by a binary that predates it.

const (
	// CompatUnknown is the zero value: nothing classified this migration, so
	// nothing may assume it is safe. Treated exactly like CompatBreaking.
	CompatUnknown MigrationCompat = ""

	// CompatAdditive means the migration only introduced objects that did not
	// exist before — new tables, new non-unique indexes, new views. An older
	// binary cannot read or write them because it does not know their names,
	// and their presence changes nothing about the statements it does issue.
	CompatAdditive MigrationCompat = "additive"

	// CompatBreaking means the migration changed something that was already
	// there, or did something this classifier does not understand. Either way
	// an older binary must not open the database.
	CompatBreaking MigrationCompat = "breaking"
)

func (MigrationCompat) Tolerable added in v0.0.2

func (c MigrationCompat) Tolerable() bool

Tolerable reports whether a binary that does not embed this migration may still open a database that has it.

type MigrationReport

type MigrationReport struct {
	StartVersion    int   // version recorded before this run (0 = brand new DB)
	EndVersion      int   // highest version applied (or already present)
	Applied         []int // versions newly applied during this run
	BaselineApplied bool  // true when an existing pre-framework DB was adopted at v1

	// Divergent names a version this database recorded under a different
	// migration file than this binary embeds. See checkNames.
	Divergent []VersionDivergence
}

MigrationReport summarises the outcome of a Migrate call.

func Migrate

func Migrate(db *sql.DB) (*MigrationReport, error)

Migrate brings db up to the latest embedded schema version. Safe to call repeatedly: a fully migrated database is a no-op. Each pending migration runs inside a transaction together with the schema_migrations row insert, so an interrupted run leaves the database at the previous version.

Errors are wrapped with ErrSchemaMismatch so callers can use errors.Is to distinguish migration failures from generic SQLite errors. A database ahead of this binary additionally carries ErrSchemaTooNew.

func MigrateTo

func MigrateTo(db *sql.DB, target int) (*MigrationReport, error)

MigrateTo is Migrate, stopping after the migration numbered target. It exists so that a test can construct the schema some earlier release actually shipped and then let Open() migrate it forward, which is the only way to exercise a migration against the database it will really meet.

The alternative — migrate fully, then hand-undo one migration's DDL and delete its schema_migrations row — does not work, and the way it fails is worth recording. currentVersion is MAX(version), so forgetting migration N forgets everything after it too, and they all re-run. That is harmless while every later migration is CREATE ... IF NOT EXISTS, and stops being harmless the moment one of them is an ALTER TABLE ADD COLUMN, which SQLite has no idempotent spelling for. It then fails with "duplicate column name" inside whichever unrelated test did the rewinding.

Note that target bounds how far *forward* this call migrates and has no bearing on the version-skew refusal, which always compares the database against the highest migration the binary embeds.

func MigrateWithOptions

func MigrateWithOptions(db *sql.DB, opts MigrateOptions) (*MigrationReport, error)

MigrateWithOptions is Migrate with the opt-out under the caller's control rather than the environment's. `cloop hub doctor` uses it to inspect a database the hub itself would refuse — diagnosing a skew is the one job that must still work when the skew is present.

type OffboardApplied added in v0.0.2

type OffboardApplied struct {
	// Sessions, Tokens and Glasses are the ids that really changed state:
	// sessions that existed and were deleted, tokens that were live and are now
	// stamped revoked. Ids that were already gone or already revoked are
	// omitted — they are not this operator's action.
	Sessions []string
	Tokens   []string
	Glasses  []string

	// DenyBindingIDs are the ids of the bindings written.
	DenyBindingIDs []string

	// At is the timestamp stamped on the revocations.
	At time.Time
}

OffboardApplied reports what the transaction actually changed, as opposed to what it was asked to change.

func (OffboardApplied) Severed added in v0.0.2

func (a OffboardApplied) Severed() bool

Severed reports whether anything at all changed.

type OffboardWrite added in v0.0.2

type OffboardWrite struct {
	// IdentityKey labels the person in the audit trail. Free-form; pkg/offboard
	// passes the canonical owner key (lowercased email, or "sub:"+subject).
	IdentityKey string

	// SessionIDs are hashed session ids, as stored in sessions.id.
	SessionIDs []string

	// TokenIDs and GlassesIDs both address api_tokens.id. They are separate
	// fields rather than one list because they are separate *surfaces* to an
	// operator — "their PAT still works" and "their glasses link still works"
	// are different incidents — and the audit trail records them separately.
	TokenIDs   []string
	GlassesIDs []string

	// DenyBindings are written in the same transaction. They are what keep the
	// account out if the IdP later hands it a fresh session: revoking sessions
	// alone is undone by the next successful sign-in.
	//
	// It is a list because one identity can be addressed by more than one
	// claim. Denying only the email leaves a re-issued session that carries the
	// same subject and no email claim — which several IdPs do once an account
	// is disabled — matching nothing. pkg/offboard writes one per identifier it
	// resolved.
	DenyBindings []RoleBindingRow

	// At stamps revoked_at. Zero means now.
	At time.Time

	// Audit builds the events to chain into this same commit. It is a callback
	// rather than a slice because it must describe what the transaction
	// actually changed, which is not known until the writes have run: rows
	// disappear between planning and committing (a session expires, a token was
	// already revoked), and an audit trail that reports the plan instead of the
	// outcome is one that overstates containment.
	//
	// Returning no events is allowed. An error aborts the whole transaction —
	// an unrecorded severing of authority is treated as a failed severing,
	// because the operator can safely re-run but cannot recover the record.
	Audit func(OffboardApplied) ([]*AuditEvent, error)
}

OffboardWrite is the resolved set of rows to sever, plus the deny binding to write. Every field is optional: an identity with no API tokens simply passes an empty TokenIDs.

type OpenOptions

type OpenOptions struct {
	// AllowSchemaDowngrade opens a database whose schema is ahead of this
	// binary's instead of refusing it. Open sets this from
	// CLOOP_ALLOW_SCHEMA_DOWNGRADE; diagnostics that must read a skewed
	// database in order to report on it set it directly.
	AllowSchemaDowngrade bool
}

OpenOptions configures OpenWithOptions.

type ProjectExecutorBinding

type ProjectExecutorBinding struct {
	ProjectPath string    `json:"project_path"`
	ExecutorID  string    `json:"executor_id"`
	BoundAt     time.Time `json:"bound_at"`
	BoundBy     string    `json:"bound_by,omitempty"`
}

ProjectExecutorBinding pins one project directory to one executor.

type ProjectResourceLimit added in v0.0.2

type ProjectResourceLimit struct {
	ProjectPath string                   `json:"project_path"`
	Ceiling     executor.ResourceCeiling `json:"ceiling"`
	SetAt       time.Time                `json:"set_at,omitempty"`
	SetBy       string                   `json:"set_by,omitempty"`
}

ProjectResourceLimit is one project's ceiling plus its provenance.

type ProviderCallPruneResult added in v0.0.2

type ProviderCallPruneResult struct {
	// Stripped is the number of rows whose bodies were cleared, and
	// StrippedBytes the text those bodies held.
	Stripped      int64 `json:"stripped"`
	StrippedBytes int64 `json:"stripped_bytes"`

	RowPruneResult
}

ProviderCallPruneResult reports both halves of a provider_calls pass.

type ProviderCallRetention added in v0.0.2

type ProviderCallRetention struct {
	// BodyMaxAge clears prompt, system_prompt and response on rows older than
	// this, keeping the row itself. Zero or less keeps every body.
	BodyMaxAge time.Duration

	// Rows bounds the rows themselves, applied after the strip.
	Rows RowRetention
}

ProviderCallRetention bounds provider_calls, which needs two limits rather than one because its rows are cheap and its bodies are not.

func (ProviderCallRetention) Active added in v0.0.2

func (p ProviderCallRetention) Active() bool

Active reports whether this policy would change anything.

type ProviderCallRow

type ProviderCallRow struct {
	ID             int64
	Timestamp      time.Time
	Provider       string
	Model          string
	TaskID         int
	TaskTitle      string
	RequestID      string
	Prompt         string
	SystemPrompt   string
	Response       string
	ErrorMessage   string
	Status         string // "ok" | "error" | "timeout" | "context_canceled"
	Headers        string // JSON object, secrets already redacted
	InputTokens    int
	OutputTokens   int
	ThinkingTokens int
	LatencyMs      int
}

ProviderCallRow is one row in the provider_calls table. Headers is a JSON-encoded string by the time it reaches this layer (the redaction step runs in pkg/provideraudit before the row is built).

type QuotaCounterRow

type QuotaCounterRow struct {
	Identity  string
	Resource  string
	Bucket    string
	Value     float64
	UpdatedAt time.Time
}

QuotaCounterRow is one row of quota_counters.

type QuotaOverrideRow

type QuotaOverrideRow struct {
	Identity   string
	LimitsJSON string
	UpdatedAt  time.Time
	UpdatedBy  string
}

QuotaOverrideRow is one row of quota_overrides. LimitsJSON is stored opaquely: the set of resources is pkg/quota's business, and a storage layer that validated it would be a second place to update when one is added.

type Role added in v0.0.2

type Role int32

Role is what a *DB handle is being used as.

const (
	// RoleUnknown is an unclassified handle. It asserts nothing.
	RoleUnknown Role = iota
	// RoleControlPlane is the hub's own database, holding fleet-wide facts.
	RoleControlPlane
	// RoleProject is one project's database, holding that plan's life.
	RoleProject
)

func (Role) String added in v0.0.2

func (r Role) String() string

String renders the role as it appears in an assertion message.

type RoleBindingRow added in v0.0.2

type RoleBindingRow struct {
	ID        string
	Effect    string
	Claim     string
	Value     string
	Role      string
	Project   string
	Executor  string
	Reason    string
	CreatedAt time.Time
	CreatedBy string
}

RoleBindingRow is one row of role_bindings.

type RotationRow

type RotationRow struct {
	ID         string
	ToKeyID    string
	State      string
	StartedAt  string
	UpdatedAt  string
	FinishedAt string
	StartedBy  string
	Total      int
	Rewrapped  int
	Skipped    int
	Failed     int
	LastError  string
}

RotationRow is one row of key_rotations.

type RowPruneResult added in v0.0.2

type RowPruneResult struct {
	// Deleted is the number of rows removed.
	Deleted int64 `json:"deleted"`

	// BytesReleased is the variable-width column content those rows held. It
	// is returned to the database freelist, not to the filesystem — only the
	// subsequent VACUUM shrinks the file — so callers must not add it to a
	// "bytes reclaimed" total that also counts the VACUUM.
	//
	// Measured across the rows the prune set out to remove, so when the call
	// also returns an error it describes the intent rather than the outcome.
	// Read it together with the error, never instead of it.
	BytesReleased int64 `json:"bytes_released"`

	// Remaining is the row count after the prune (before it, in a dry run).
	Remaining int64 `json:"remaining"`
}

RowPruneResult is what one table's prune did, or would have done in a dry run.

type RowRetention added in v0.0.2

type RowRetention struct {
	// MaxRows is the hard ceiling: the newest MaxRows rows survive and the
	// rest are deleted. Zero or less keeps every row.
	MaxRows int

	// MaxAge deletes rows older than this. Zero or less keeps every row
	// regardless of age.
	//
	// An age bound alone does not make a table bounded — an unbounded write
	// rate still produces an unbounded table within any fixed window — which is
	// why hubdoctor reports boundedness off MaxRows and not off this.
	MaxAge time.Duration
}

RowRetention bounds one row table. The zero value bounds nothing.

func (RowRetention) Active added in v0.0.2

func (r RowRetention) Active() bool

Active reports whether this policy would delete anything at all.

func (RowRetention) Bounded added in v0.0.2

func (r RowRetention) Bounded() bool

Bounded reports whether this policy puts a ceiling on the table's size.

type SchemaStamp

type SchemaStamp struct {
	Version   int       `json:"version"`
	Name      string    `json:"name,omitempty"`
	AppliedAt time.Time `json:"applied_at,omitempty"`
	AppliedBy string    `json:"applied_by,omitempty"`
	// Compat is the applying build's reading of whether this migration is
	// tolerable to older binaries. Empty for rows written before that was
	// recorded, which reads as "not known to be safe".
	Compat MigrationCompat `json:"compat,omitempty"`
}

SchemaStamp is one schema_migrations row: which version was applied, when, and by which build.

AppliedBy is "" for rows written before this provenance existed, and for rows written by a process that never called SetBinaryVersion. Absence is not an error — it just means the refusal message cannot name a build.

type SchemaTooNewError

type SchemaTooNewError struct {
	// DBVersion is the highest version recorded in schema_migrations.
	DBVersion int
	// BinaryVersion is the highest migration embedded in this binary.
	BinaryVersion int
	// Stamp describes the schema_migrations row for DBVersion, when one could
	// be read. A zero Stamp means the row was unreadable, not that it is
	// absent — either way the message simply omits the provenance.
	Stamp SchemaStamp
	// Blocking lists the migrations ahead of this binary that it cannot
	// tolerate: the ones recorded as breaking, plus any it could not classify.
	// Migrations recorded as additive are absent, because they did not
	// contribute to the refusal.
	Blocking []SchemaStamp
}

SchemaTooNewError is the detail behind ErrSchemaTooNew: the two versions that disagree, plus whatever the database records about the build that moved it forward.

It satisfies errors.Is for both ErrSchemaTooNew and ErrSchemaMismatch. The second is deliberate: every existing caller that already treats a schema mismatch as fatal (HTTPStatus maps it to 500, classifyDriverErr declines to re-wrap it) keeps working unchanged, while callers that care about this specific cause can ask for it by name.

func (*SchemaTooNewError) Error

func (e *SchemaTooNewError) Error() string

func (*SchemaTooNewError) Is

func (e *SchemaTooNewError) Is(target error) bool

Is makes both sentinels match. See the type comment for why ErrSchemaMismatch is included.

type SealedRow

type SealedRow struct {
	ID         string
	KeyID      string
	WrappedDEK []byte
	Ciphertext []byte
}

SealedRow is a row of sealed material as the rotator sees it: an ID, the associated data it is bound to, and the three envelope parts.

type SecretAuditInput

type SecretAuditInput struct {
	Actor string
	// EventType is the registered action: auditaction.ActionSecretLease,
	// ActionSecretGrant, ActionSecretRevoke, and the rest of the secret,
	// lease, egress and github_app families.
	//
	// Typed rather than a bare string because the whole broker surface funnels
	// through this one struct, which makes it the single place a made-up
	// action could enter the trail. Requiring an auditaction.Action means the
	// value came from the registry, and tests/arch/auditaction_test.go can see
	// that it did.
	EventType auditaction.Action
	EntityID  string // secret ID, or grant ID when no secret is involved
	Timestamp time.Time
	// Payload is the decision's metadata. Callers are responsible for
	// redacting it; pkg/secretbroker.Redact runs over every event before it
	// reaches here, and again inside the secretstore auditor.
	Payload map[string]any
}

SecretAuditInput carries one secret-broker decision to the audit log (Task 20159). It exists so pkg/secretstore does not have to construct an AuditEvent — and, more to the point, so the entity_type is set in exactly one place and every broker row is filterable as entity_type='secret'.

type SecretLeaseDirRow

type SecretLeaseDirRow struct {
	// Dir is the absolute path of the lease directory and the primary key.
	Dir string
	// LeaseID is the lease whose material lives there.
	LeaseID string
	// ExecutorID is the executor the lease was issued to, ” when unbound.
	ExecutorID string
	// ProjectPath is the project the work belongs to, ” when not scoped.
	ProjectPath string
	// CreatedAt is when the intent was recorded — before the directory was
	// created, not after.
	CreatedAt time.Time
	// ExpiresAt is the lease's expiry; the zero time means unbounded. The
	// sweep does not need it to decide, but an operator reading the audit
	// event for a swept directory needs to know whether it had already lapsed.
	ExpiresAt time.Time
}

SecretLeaseDirRow is one credential directory this hub has committed to creating. It records where plaintext is, never what it is.

type SessionClaims added in v0.0.2

type SessionClaims struct {
	Groups []string
	Roles  []string
}

SessionClaims is a replacement set of group and role claims.

type SessionRefreshUpdate added in v0.0.2

type SessionRefreshUpdate struct {
	KeyID      string
	WrappedDEK []byte
	Sealed     []byte
	CheckedAt  time.Time
	Claims     *SessionClaims

	// ClaimsAsOf and ClaimsExpireAt update the claim-freshness clocks. A zero
	// value leaves the stored column alone, which is what lets one update
	// express "claims re-asserted" (both set), "claims repudiated"
	// (ClaimsExpireAt only, backdated to the refusal) and "grant renewed but
	// nothing learned" (neither) without a separate statement for each.
	ClaimsAsOf     time.Time
	ClaimsExpireAt time.Time
}

SessionRefreshUpdate is the outcome of one IdP revalidation.

Claims is nil when the provider returned no id_token, meaning the stored claims are left untouched; a non-nil value replaces them, including with an empty set, which is how a user losing their last group is recorded.

type SessionRow

type SessionRow struct {
	ID            string
	Subject       string
	Issuer        string
	Email         string
	DisplayName   string
	OwnerKey      string
	Groups        []string
	Roles         []string
	IP            string
	UserAgent     string
	IssuedAt      time.Time
	LastSeen      time.Time
	ExpiresAt     time.Time
	RefreshSealed []byte
	// RefreshKeyID and RefreshWrappedDEK are the envelope this row's sealed
	// refresh token belongs to (Task 20181). "legacy" means the token is
	// sealed directly under the passphrase-derived key, with no wrapped DEK.
	RefreshKeyID      string
	RefreshWrappedDEK []byte
	RefreshCheckedAt  time.Time

	// ClaimsAsOf and ClaimsExpireAt date the row's groups and roles, as opposed
	// to RefreshCheckedAt which dates the grant (Task 20273). See
	// migrations/0038_session_claim_freshness.sql for why they must be separate
	// columns, and pkg/oidcauth/claimfresh.go for what reads them.
	ClaimsAsOf     time.Time
	ClaimsExpireAt time.Time
}

SessionRow is one row of the sessions table.

RefreshSealed is ciphertext. Nothing in this package encrypts or decrypts it — the sealing happens in pkg/sessionstore, so the storage layer never holds a live credential and cannot leak one by logging a row.

type SizeStats

type SizeStats struct {
	PageCount     int64
	PageSize      int64
	FreelistPages int64
	Bytes         int64
}

SizeStats captures the physical size of the SQLite database at a point in time. Bytes is derived as PageCount * PageSize for convenience; FreelistPages counts free pages that VACUUM would reclaim.

func (SizeStats) FreelistBytes

func (s SizeStats) FreelistBytes() int64

FreelistBytes returns the bytes currently allocated to the freelist — roughly the upper bound on what VACUUM can reclaim.

type SpendCursor added in v0.0.2

type SpendCursor struct {
	ProjectPath string
	// Identity is who the hub resolved when it dispatched the current run.
	// The only identity this project's spend is ever charged to.
	Identity string
	// LastCostRowID is the highest costs.id already charged.
	LastCostRowID int64
	UpdatedAt     time.Time
}

SpendCursor is the hub's booking position for one project.

type State

type State struct {
	Goal        string
	WorkDir     string
	MaxSteps    int
	CurrentStep int
	Status      string
	// PauseReason explains a "paused" Status. Stored as JSON in the
	// schemaless metadata table rather than as a column, so adding it needed
	// no migration — and an older binary reading this DB just ignores the key
	// instead of being locked out by a schema it does not know.
	PauseReason *pausereason.Reason
	Steps       []StepRow
	// StepCount mirrors len(Steps) on full loads but is also populated by
	// LoadStateLite — where Steps is nil — so callers that only need a
	// count don't have to materialize every row's Output (Task 20125).
	StepCount int
	// LastStepTime is the timestamp of the newest step row. Populated by
	// both LoadState (last element of Steps) and LoadStateLite (cheap
	// lookup of the highest step number); used by health probes to detect
	// stalled runs without scanning the full Steps slice.
	LastStepTime      time.Time
	CreatedAt         time.Time
	UpdatedAt         time.Time
	Model             string
	Instructions      string
	AutoEvolve        bool
	EvolveStep        int
	Provider          string
	Effort            string
	PMMode            bool
	Plan              *pm.Plan
	Milestones        []*milestone.Milestone
	TotalInputTokens  int
	TotalOutputTokens int
	DefaultMaxMinutes int
	SkipClarify       bool
	InnovateMode      bool
	Parallel          bool
	MaxParallel       int
	WorktreeParallel  bool
	PlanOnly          bool
	RetryFailed       bool
	DryRun            bool
}

State mirrors pkg/state.ProjectState but is owned by this package to avoid import cycles. pkg/state converts between the two representations.

type StepRow

type StepRow struct {
	Step         int
	Task         string
	Output       string
	ExitCode     int
	Duration     string
	Time         time.Time
	InputTokens  int
	OutputTokens int
}

StepRow represents one recorded step result.

type Store

type Store interface {

	// SaveState atomically persists the full project state (metadata, tasks,
	// steps). Existing tasks are replaced; steps are upserted (never deleted).
	SaveState(s *State) error

	// LoadState reads back the complete project state. Returns a zero-value
	// State (not an error) when the database exists but has no rows yet.
	LoadState() (*State, error)

	// UpsertTask inserts or replaces a single task row. Useful for
	// status-only updates without a full SaveState round-trip.
	UpsertTask(t *pm.Task) error

	// LoadTask returns a single task by id. Returns ErrTaskNotFound when
	// the row does not exist; callers should test with errors.Is.
	LoadTask(id int) (*pm.Task, error)

	// AppendStep inserts a step row. Idempotent on step number (upsert).
	AppendStep(row StepRow) error

	// AppendCost appends a single cost entry for the current project.
	AppendCost(entry CostEntry) error

	// ReadCosts returns all cost entries in chronological order.
	ReadCosts() ([]CostEntry, error)

	// ReadCostsSince returns cost entries with timestamp >= since.
	ReadCostsSince(since time.Time) ([]CostEntry, error)

	// MonthlyCosts returns cost entries for the given UTC year/month.
	MonthlyCosts(year, month int) ([]CostEntry, error)

	// PingContext verifies the underlying store is reachable by issuing a
	// trivial query. Used by readiness probes; ctx bounds wait time on a
	// busy connection.
	PingContext(ctx context.Context) error

	// Close releases the underlying connection.
	Close() error
}

Store is the abstract data-access interface for cloop project state. All methods must be safe for concurrent use.

SQLite implementation: DB (this package). PostgreSQL: implement this interface with a pgx/pgxpool-backed struct.

type TableStat added in v0.0.2

type TableStat struct {
	Name string `json:"name"`
	Rows int64  `json:"rows"`
	// Bytes is the variable-width column content across all rows. See
	// growthTable.bytesExpr: an under-estimate of what the table costs on disk,
	// deliberately.
	Bytes int64 `json:"bytes"`
	// Oldest is the insertion timestamp of the lowest-id row, or the zero time
	// when the table is empty or that row's timestamp will not parse.
	Oldest time.Time `json:"oldest,omitempty"`
}

TableStat is one row table's current size, for reporting.

type TaskDispatchInput added in v0.0.2

type TaskDispatchInput struct {
	// TaskID and TaskTitle identify the unit of work. The title is recorded
	// rather than joined later because a plan can be edited: an auditor reading
	// this row six weeks on should see what the task said when it ran.
	TaskID    int
	TaskTitle string

	// ProjectPath is the project the task belongs to.
	ProjectPath string

	// RunID names the execution. It is the join key to the secret.lease rows
	// the hub wrote for the same dispatch, and the reason leases attach to an
	// execution rather than to a task id that may run many times.
	RunID string

	// ExecutorID, ExecutorKind and Isolation are where the work landed, as
	// advertised at placement. Stored rather than derived by joining the
	// executor registry later, for the reason migration 0032 gives: an executor
	// re-enrolled under the same id would otherwise rewrite the history of
	// every task it ever ran.
	ExecutorID   string
	ExecutorKind string
	Isolation    string

	// RequestedImage is the reference the sandbox spec asked for; PinnedImage
	// is what actually ran, digest-pinned where the driver could resolve one.
	// Both, because the gap between them is the question — a spec naming a tag
	// and a run on a digest is reproducible, a run on the tag is not.
	RequestedImage string
	PinnedImage    string

	// SpecHash is the .cloop/sandbox.yaml content hash, empty when the project
	// has no spec and ran on the executor's defaults. SetupHash identifies
	// commands baked into a derived image.
	SpecHash  string
	SetupHash string

	// LeaseIDs are the secret leases handed to this run.
	LeaseIDs []string

	// Actor is who asked for the run.
	Actor string
}

TaskDispatchInput carries the placement facts for one task execution (Task 20282).

It is filled by the orchestrator, which is the only layer that knows both a task id and the environment it was placed into: the hub chose the executor and started a whole `cloop run`, and by the time a task begins there is no placement decision left to read — the decision is why the process exists. What the hub leaves behind is .cloop/sandbox-run.json, and this struct is that record joined to the task it is about to run.

No field can carry a credential. LeaseIDs are the broker's opaque handles, not material; the digest and hashes are content addresses; Actor is an identity label. The token behind a lease never enters this package's scope.

type TaskRunStory added in v0.0.2

type TaskRunStory struct {
	RunID string

	// Dispatched and Finished are the boundary rows. Finished is nil for a run
	// still in flight — or for one whose hub died so hard it never wrote a
	// terminal row, which is a gap an auditor should see rather than have
	// smoothed over with an invented outcome.
	Dispatched *AuditEvent
	Finished   *AuditEvent

	// ExecutorID, ExecutorKind and Isolation are where the work ran, read from
	// the dispatch payload.
	ExecutorID   string
	ExecutorKind string
	Isolation    string

	// PinnedImage and SpecHash pin the environment.
	PinnedImage string
	SpecHash    string

	// LeaseIDs are the credentials the dispatch row claims were handed over.
	LeaseIDs []string

	// LeaseEvents are the broker's own rows for this run — the authoritative
	// half. They are kept separate from LeaseIDs on purpose: the dispatch row
	// is written by the orchestrator, which reads its lease ids from a file in
	// a directory the workload can write to, whereas these rows are written by
	// the hub and cannot be influenced from inside the sandbox. A reviewer
	// checking a hostile workload should believe these.
	LeaseEvents []AuditEvent

	// Outcome, Reason and DurationMS come from the terminal row.
	Outcome    string
	Reason     string
	DurationMS int64

	// Actor is who the dispatch row attributes the run to.
	Actor string
}

TaskRunStory is one execution of a task.

func (TaskRunStory) Duration added in v0.0.2

func (r TaskRunStory) Duration() string

Duration renders the execution's wall time, or "" when the trail does not record one.

func (TaskRunStory) RanOnHost added in v0.0.2

func (r TaskRunStory) RanOnHost() bool

RanOnHost reports whether this execution used the hub's own machine.

Mirrors orchestrator.RanOnHost, deliberately reimplemented over the audit payload rather than over a pm.Task: the point of a reconstruction is that it works without the task record.

type TaskStory added in v0.0.2

type TaskStory struct {
	// TaskID is the task the story is about.
	TaskID int

	// Title is the task's title as the trail recorded it, from the most recent
	// row that carried one. Empty when no row named it — which is itself a
	// finding, not a formatting problem.
	Title string

	// Events are every row that belongs to this task's history, in trail order
	// (ascending id, which is the order they were appended and therefore the
	// order the hash chain fixes). Includes both the task-scoped rows and the
	// run-correlated rows from other entities.
	Events []AuditEvent

	// Runs are the executions this task was dispatched into, oldest first. A
	// task that ran five times has five entries, which is the distinction the
	// whole run-id mechanism exists to preserve.
	Runs []TaskRunStory

	// Unattributed counts lifecycle rows that named no run — history from
	// before run ids existed, or a dispatch whose provenance record was
	// missing. It is reported rather than silently dropped: executions the
	// trail cannot correlate are exactly what an auditor needs told.
	Unattributed int
}

TaskStory is one task's reconstructed history.

type TelemetryFilter added in v0.0.2

type TelemetryFilter struct {
	Source  string
	Kind    string
	Session string
	// Search matches message, url, view and detail. Applied in SQL rather than
	// in the caller because the table is bounded but large, and a reader
	// filtering client-side would page through thousands of rows to find one.
	Search string
	Since  time.Time
	Limit  int
	Offset int
}

TelemetryFilter selects events for reading. The zero value means "the most recent page of everything", which is the query a reader opening the panel wants before they know what they are looking for.

type TelemetrySession added in v0.0.2

type TelemetrySession struct {
	Session   string    `json:"session"`
	Source    string    `json:"source"`
	Events    int       `json:"events"`
	Errors    int       `json:"errors"`
	FirstSeen time.Time `json:"first_seen"`
	LastSeen  time.Time `json:"last_seen"`
	UserAgent string    `json:"user_agent,omitempty"`
	Actor     string    `json:"actor,omitempty"`
	Release   string    `json:"release,omitempty"`
}

TelemetrySession is one page load, rolled up.

The index of the panel: a reader arrives knowing "a wearer reported a problem around ten past four" and needs to turn that into a session id before any per-event view is useful.

type VersionDivergence added in v0.0.3

type VersionDivergence struct {
	Version  int
	Recorded string
	Embedded string
}

VersionDivergence is one version whose recorded name is not the name this binary embeds for it.

type WorkspaceAuditInput

type WorkspaceAuditInput struct {
	// Phase is "start" or "end", matching executor.WorkspaceProvisionStart /
	// WorkspaceProvisionEnd. It becomes the suffix of the event type, so the
	// family is filterable as event_type LIKE 'workspace.%' and each phase
	// individually by exact match. Anything else is dropped rather than
	// written, so a typo cannot create a third, unqueryable event family.
	Phase string

	// ProjectPath is the project whose tree was being materialised. It is the
	// entity the row is filed under, matching how image denials are filed.
	ProjectPath string

	// ExecutorID and ExecutorKind name the backend doing the fetch. Both are
	// recorded because an ID is stable but says nothing about *where* the code
	// landed, and "was this on a Pod or on someone's laptop" is the first
	// question asked of a provisioning row.
	ExecutorID   string
	ExecutorKind string

	// HandleID correlates the two phases with each other and with the run.
	HandleID string

	// Kind, Repo, Ref and Depth are the workspace as dispatched — the clone
	// URL exactly as the executor was told to fetch it, which is the only form
	// that can be compared against what the operator believes is configured.
	Kind  string
	Repo  string
	Ref   string
	Depth int

	// GrantID and LeaseID name the authority used. They are the join key back
	// into the secret-broker rows, which is what turns "this repo was fetched"
	// into "this grant was exercised at this time".
	GrantID string
	LeaseID string

	// DurationMS is set on the end phase only.
	DurationMS int64

	// Err is the failure, already redacted by the driver, or "" on success.
	Err string

	// Actor is the acting identity. Empty becomes "system": provisioning is
	// decided by the dispatching control plane, and on the failover path there
	// is no human to attribute it to.
	Actor string
}

WorkspaceAuditInput carries one workspace-provisioning observation to the audit log (Task 20179).

Provisioning earns 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 on behalf of a project. "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: a run looks identical whether the tree arrived or not, which is the entire reason the workspace subsystem exists.

No field here can carry a credential. The Workspace the executor dispatched is structurally incapable of holding one — pkg/executor.Workspace has no field a token could go in, by design, because a Spec is persisted, logged and shipped across the remote-executor boundary — and GrantID/LeaseID are identifiers, not material. The token never enters this package's scope.

Jump to

Keyboard shortcuts

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