statedb

package
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 43 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
ErrCorruptTaskColumn → 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.

firewall.go stores the two dashboard-set levels of the IP-layer egress firewall: one rule set per device, and one per project (Task 20363).

See migrations/0055_egress_firewall_rules.sql for the tables and pkg/fwpolicy for the rule that the project's set, and every virtual executor's firewall, fit inside the device's. This file only stores; the containment decision is made by the caller, inside UpdateFirewalls when the write depends on what it read.

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.
  4. To add a column a database may already have — one `cloop migrate` or a development build created outside this sequence — write `ALTER TABLE t ADD COLUMN IF NOT EXISTS c <declaration>`. SQLite has no such statement; applyOne runs it as a plain ADD COLUMN when the column is missing and otherwise checks that the existing column is the one declared (see addcolumn.go). A plain ADD COLUMN on such a database fails with "duplicate column name" and stops every start that opens it.

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.

Package statedb — rehydration of the JSON-encoded task columns.

plan_tasks stores six list fields as JSON text: depends_on, tags and annotations since the first schema, and links, on_success and on_failure since 0054 (Task 20361). Decoding them used to be a discarded error per column per row, which made a damaged column indistinguishable from an empty one: the field came back nil, and the load reported success.

For some of them that is a visibility loss. For depends_on it is a correctness bug, because an empty dependency list is not "no information" — it is a positive assertion that the task has no prerequisites, and pm.DepsReady reads it as permission to run. A task whose dependencies were dropped on load is admitted ahead of the work it depends on, defeating the gate silently (Task 20297). on_success and on_failure are the same kind of field: pm.ResolveBranch skips the branch not taken, so an empty list lets both branches of a branched plan run. That asymmetry is why the columns are decoded under different rules rather than one.

Index

Constants

View Source
const (
	// AuditChainIntact: every row verified and none records a loss.
	AuditChainIntact = "intact"
	// AuditChainGaps: every row verified, and some are audit.gap rows
	// recording events that could not be appended.
	AuditChainGaps = "gaps"
	// AuditChainBroken: a row failed verification.
	AuditChainBroken = "broken"
)

Verdicts a chain verification can reach, from AuditVerifyReport.Status.

View Source
const (
	// GapRecordedOnRecovery: ahead of the next append that succeeded.
	GapRecordedOnRecovery = "recovery"
	// GapRecordedByFlush: by the background reporter, on its own.
	GapRecordedByFlush = "flush"
	// GapRecordedAtShutdown: by a hub shutting down cleanly.
	GapRecordedAtShutdown = "shutdown"
	// GapRecordedAtExit: by a CLI command finishing.
	GapRecordedAtExit = "exit"
)

How a gap row came to be written, recorded in its payload.

View Source
const (
	// DispatchIntentDispatching: the executor has been, or is about to be,
	// asked to start the workload, and has not answered.
	DispatchIntentDispatching = "dispatching"
	// DispatchIntentStarted: the executor answered with a handle.
	DispatchIntentStarted = "started"
)

Dispatch intent states.

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"
	// ExecutorSessionFailoverExhausted: the executor went unreachable after
	// the session had already been re-dispatched as often as
	// executors.failover.max_attempts allows (Task 20391). The claim closed
	// it in this state instead of requeued, and nothing re-dispatches it.
	ExecutorSessionFailoverExhausted = "failover_exhausted"
)

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 (
	// ProjectClaimCreating: a creation holds the path while it makes the
	// directory and initialises it.
	ProjectClaimCreating = "creating"
	// ProjectClaimCreated: the project exists and is registered.
	ProjectClaimCreated = "created"
)

Project claim states.

View Source
const (
	ProxySessionGit    = "git"
	ProxySessionKube   = "kube"
	ProxySessionEgress = "egress"
)

Proxy session kinds.

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

Effect values for RoleBindingRow.Effect.

View Source
const (
	SessionKindBrowser = "browser"
	SessionKindCLI     = "cli"
)

The session kinds the kind column holds. pkg/oidcauth owns what they mean; these are the stored spellings, and the sweep below compares against one.

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 BusyTimeoutMillis = 5000

BusyTimeoutMillis is how long any connection under the policy waits on another connection's lock before returning SQLITE_BUSY.

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 JournalSizeLimitBytes int64 = 64 << 20

JournalSizeLimitBytes is the size a read-write connection under the policy trims the write-ahead log back to when it restarts it (Task 20392).

SQLite rewrites a WAL from its start once a checkpoint has copied every frame back into the database, but it never shrinks the file: the log stays at the high-water mark of the largest burst written between two checkpoints — a bulk delete, a VACUUM, which writes the whole database through it — for as long as the database is open. On 2026-10-06 the hub's own log was 340 MB beside a 409 MB database on a disk 98% full, in use, and never coming back. With journal_size_limit set, the connection whose write restarts the log truncates the file to this size when that write commits, or to the size of the write if that is larger. Nothing else changes: the limit is not a cap on how large the log may grow, and checkpoints run as before.

64 MiB is several times the log a busy hub needs between two automatic checkpoints (1000 pages, 4 MiB), so the trim costs a steady-state hub nothing, and small enough that a burst no longer leaves a meaningful share of a small disk behind.

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 LegacyKeyID = "legacy"

LegacyKeyID is the key id pre-envelope material reads as (secretbroker's LegacyKeyID, which migration 0019 stamps).

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.

View Source
const WALCheckpointBusyTimeout = 250 * time.Millisecond

WALCheckpointBusyTimeout is how long a TRUNCATE checkpoint waits for another connection before reporting busy — and so roughly the longest a writer can be held up behind one, which SQLite makes wait while the checkpoint holds the write lock. A quarter of a second outlasts the reads and writes cloop makes, and is a twentieth of the 5 s every writer waits before giving up.

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

	// ErrDeviceAuthorizationNotFound indicates no device_authorizations row
	// has the requested device code or user code (Task 20412). An ordinary
	// outcome: a mistyped user code, and a device code that expired and was
	// swept, look exactly like this, and must be answered alike.
	ErrDeviceAuthorizationNotFound = errors.New("statedb: device authorization 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")

	// ErrCorruptTaskColumn indicates a task row holds a JSON-encoded column
	// that could not be decoded (Task 20297).
	//
	// Only raised for columns where dropping the value would change what the
	// program does rather than what it displays: depends_on, whose empty value
	// pm.DepsReady reads as "no prerequisites, may run", and on_success and
	// on_failure, whose empty value lets both branches of a branched plan run
	// (Task 20361). Fatal to the load on purpose: the alternative is admitting
	// a task ahead of work it depends on, and the error names the task and
	// column so the row can be repaired. Columns that are safe to drop are
	// reported through the sink in taskcolumns.go instead and do not produce
	// this error.
	ErrCorruptTaskColumn = errors.New("statedb: corrupt task column")

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

	// ErrProjectMemberNotFound indicates no project_members row names the
	// requested identity on the requested project (Task 20366). An ordinary
	// answer — "that person is not on this project" — which the REST layer
	// reports as 404 and the CLI as a plain sentence.
	ErrProjectMemberNotFound = errors.New("statedb: project member not found")

	// ErrSecretLeaseNotFound indicates no secret_leases row names the lease
	// (Task 20382): it was released, swept, or recorded by a binary that
	// predates the table.
	ErrSecretLeaseNotFound = errors.New("statedb: secret lease not found")

	// ErrProxySessionNotFound indicates no proxy_sessions row names the
	// session (Task 20383): it was retired, recorded by a binary that predates
	// the table, or never recorded — a session nothing could restore.
	ErrProxySessionNotFound = errors.New("statedb: proxy session not found")

	// ErrCISessionNotFound indicates no ci_sessions row names the session
	// (Task 20390): it was retired, minted by a binary that predates the
	// table, or never recorded — a session nothing could restore.
	ErrCISessionNotFound = errors.New("statedb: CI session 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 (
	// ErrSCIMNotFound: no provisioned user, group or credential has that id.
	ErrSCIMNotFound = errors.New("statedb: scim resource not found")
	// ErrSCIMVersion: the resource changed since the caller read it.
	ErrSCIMVersion = errors.New("statedb: scim resource was changed concurrently")
)

Sentinels for the SCIM directory.

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 ErrDispatchIntentNotFound = errors.New("statedb: dispatch intent not found")

ErrDispatchIntentNotFound is returned when no row names a run.

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.

View Source
var ErrHubOwnerNotFound = errors.New("statedb: hub owner not found")

ErrHubOwnerNotFound is returned when no member owns (kind, key).

View Source
var ErrHubTLSNotFound = errors.New("statedb: no hub certificate by that id")

ErrHubTLSNotFound is returned when the hub has no row by that id: no CA minted yet, or no leaf issued for that monitor.

View Source
var ErrStaticTokenFingerprint = errors.New("statedb: a static token fingerprint is 64 lowercase hex characters")

ErrStaticTokenFingerprint is returned for a fingerprint that is not 64 lowercase hex characters.

View Source
var ErrVirtualExecutorExists = errors.New("statedb: an executor with this id already exists")

ErrVirtualExecutorExists reports a create for an ID already in use — by a virtual executor or by any other executor the control plane has recorded.

View Source
var ErrVirtualExecutorNotFound = errors.New("statedb: virtual executor not found")

ErrVirtualExecutorNotFound reports an unknown virtual executor ID.

Functions

func AdoptAuditFailureStatus added in v0.0.5

func AdoptAuditFailureStatus(dir string) (int64, error)

AdoptAuditFailureStatus takes over the losses that processes no longer running left in dir: each becomes owed by this process, to be recorded in its chain as an audit.gap row naming where it came from, and the file is removed. Returns how many events were adopted.

A file is claimed by renaming it before it is read, so two processes starting beside one dead one cannot both record its losses. Entries that cannot be adopted — a database that is gone, or cannot be confirmed as cloop's — are written back to a file of their own and said on stderr, so the record of the loss is not destroyed by the attempt to record it.

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

func AuditAdoptRequested(d *DB, in AdoptRequestInput)

AuditAdoptRequested records somebody asking a run to adopt the hub's build at its next task boundary (Task 20389).

func AuditAnchorLink(a AuditAnchor) (want string, ok bool)

AuditAnchorLink recomputes one anchor's hash and reports whether it matches, so a verifier outside this package can walk the anchor chain.

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

func AuditFailureStatusDir(workDir string) string

AuditFailureStatusDir is where a hub serving workDir keeps its status file, and where `cloop hub doctor` looks for it.

func AuditGapLostEvents added in v0.0.5

func AuditGapLostEvents(payload string) int64

AuditGapLostEvents reads how many events a gap row's payload records as lost, ignoring every other field: a reader that wants only the count must not lose it to a field of a shape it does not expect.

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

func AuditProjectFirewall(d *DB, in ProjectFirewallAuditInput)

AuditProjectFirewall records a project's firewall rule set changing, on the control plane's chain, where the rule set is stored. Best-effort like every emitter in this file.

func AuditProjectLifecycle added in v0.0.5

func AuditProjectLifecycle(d *DB, in ProjectAuditInput)

AuditProjectLifecycle records a project created, refused, removed or kept in the control plane's chain. Best-effort, like every helper here.

func AuditRunReexecuted added in v0.0.5

func AuditRunReexecuted(d *DB, in RunReexecInput)

AuditRunReexecuted records a run moving to a newer build. Written by the new image, on the project's chain: which build ran which of its tasks is a question about the project.

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

func AuditTaskQuarantine(d *DB, t *pm.Task, q pm.TaskQuarantine, actor string)

AuditTaskQuarantine records a task marked as a suspected node killer (Task 20391). It belongs in the project's chain: d must be the project's handle.

func AuditTaskQuarantineRelease added in v0.0.5

func AuditTaskQuarantineRelease(d *DB, taskID int, q pm.TaskQuarantine, actor, via string)

AuditTaskQuarantineRelease records an explicit reset releasing a suspected node killer. via names the reset; actor is who pressed it.

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

func CheckSchemaAhead(path string) error

CheckSchemaAhead reports, without writing, whether this build would refuse to open the database at path because it was migrated past this binary: the decision Open's guard makes (checkNotFromFuture), asked of a read-only connection. Nil means it would open it — not ahead, or ahead only by migrations recorded as additive — and a *SchemaTooNewError names what blocks it. Any other error means the question could not be asked: the file would not open read-only, or its schema_migrations would not read. CLOOP_ALLOW_SCHEMA_DOWNGRADE is not consulted; the caller weighs the opt-out.

`cloop hub doctor` asks this rather than comparing version numbers itself. It used to fail every database that was ahead, including the ones this guard opens without complaint (Task 20387).

func DSN added in v0.0.5

func DSN(path string, access Access) string

DSN returns the data source name a handle with the given access opens path with.

The name is a file: URI, so that the path is escaped rather than parsed: the driver cuts a bare path at its first '?', so a project directory with a question mark in its name would open some other file, and mode=ro only means something to SQLite inside a URI.

func FlushAuditGaps added in v0.0.5

func FlushAuditGaps(recorded string)

FlushAuditGaps records what every chain is owed now, rather than at the reporter's next tick, and prints whatever has not been said. recorded is one of the GapRecorded* values. A hub calls it as it shuts down.

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

func OpenConn(path string, access Access) (*sql.DB, error)

OpenConn opens path under the connection policy and checks that the first connection can be established, so a missing file or a bad path is reported here rather than by whichever query happens to run first.

The pool holds one connection. Within a process that serialises every statement on the handle, which is what all of cloop's callers want; the policy does not depend on it, because the driver applies the policy to every connection it opens, so a caller that needs more may raise the limit.

A ReadOnly open of a file that does not exist fails rather than creating it.

func PeekHubCluster added in v0.0.5

func PeekHubCluster(dbPath, kind string) (members []HubMemberRow, owners []HubOwnerRow, err error)

PeekHubCluster reads the members serving the control plane at dbPath and the ownership rows of one kind (every kind when kind is empty), through a read-only handle (Task 20374).

It is for a process that is not a member and must not become one by looking: a CLI deciding whether a hub is running a project. Open would migrate the database, and a hub's live control plane is the one database a newer CLI build must never migrate early — nor may an older one be refused for asking a question this narrow. Nothing here writes, so neither the schema version nor a lock held by the hub stands in the way, beyond the busy timeout every connection waits.

A control plane no clustered hub has served has no cluster tables, and reads as no members and no owners rather than as an error.

func PeekRunOwner added in v0.0.5

func PeekRunOwner(dbPath string) (*runbuild.Owner, error)

PeekRunOwner reads the run-owner record of the project database at dbPath through a read-only handle: it never migrates or creates the database, so a diagnostic may read any project's record, whatever build wrote it. Nil when no run has recorded one.

func ProjectMemberID added in v0.0.5

func ProjectMemberID(projectPath, identityKey string) string

ProjectMemberID derives a new membership's id from the one tuple that identifies it: the project and the person. Role is not part of it, so changing someone's role replaces their row rather than adding a second one whose winner nobody could predict.

96 bits, because the id is also the primary key that keeps the pair unique — the table predates this build and has no UNIQUE constraint on the pair. Lookups go by the pair, not the id (PutProjectMember), so a collision fails an insert rather than landing on another person's row.

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

func ReadHubMeta(conn *sql.DB, key string) (string, bool, error)

ReadHubMeta is HubMeta through a bare connection: one OpenConn opened, typically ReadOnly, so that reading a hub's record neither migrates its database nor, when the file is missing, creates one (Task 20410). A database without the metadata table holds no value.

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

func SetAuditFailureStatusDir(dir string)

SetAuditFailureStatusDir names the directory this process keeps its status file in. Empty turns it off.

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

func SettleAuditFailuresAtExit()

SettleAuditFailuresAtExit is FlushAuditGaps for a process about to exit, and says plainly what could not be recorded: a CLI command that failed an append may otherwise end before its reporter has printed anything.

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.

func WALPath added in v0.0.5

func WALPath(dbPath string) string

WALPath returns the path of the write-ahead log beside dbPath.

func WALSize added in v0.0.5

func WALSize(dbPath string) (int64, error)

WALSize reports the size of the write-ahead log beside dbPath. A log that does not exist is zero bytes, not an error: the last connection to close a WAL database deletes it.

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

type Access int

Access is what a handle opened under the connection policy may do.

const (
	// ReadWrite handles read and write. Their transactions take the write
	// lock at BEGIN (BEGIN IMMEDIATE): every transaction cloop opens reads
	// before it writes, and a deferred one fails with SQLITE_BUSY_SNAPSHOT —
	// no busy handler, no retry — whenever another handle commits between its
	// read and its write. In WAL mode an immediate transaction blocks no
	// reader.
	ReadWrite Access = iota
	// ReadOnly handles cannot write, enforced by SQLite rather than by the
	// caller's good behaviour: the file is opened mode=ro. A diagnostic, a
	// backup check or a liveness probe that looks at a hub's live database
	// can neither change it nor, when the file does not exist, create an
	// empty one in its place.
	ReadOnly
)

func (Access) String added in v0.0.5

func (a Access) String() string

String names the access mode for messages.

type AdoptRequestInput added in v0.0.5

type AdoptRequestInput struct {
	ProjectPath string
	RequestID   string
	PID         int
	RunBuild    runbuild.Build
	HubBuild    runbuild.Build
	Behind      int
	Actor       string
}

AdoptRequestInput is one request for a run to adopt the hub's build.

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

type AppTokenSlotRow struct {
	LeaseID    string
	GrantID    string
	Holder     string
	SecretID   string
	SecretName string
	// Scope is the broker's JSON account of the mint: installation, repository
	// ids, permissions asked for and granted.
	Scope string
	// Guarded: a git proxy session presents the token, named by SessionID.
	Guarded     bool
	SessionID   string
	EnvExported bool
	// FileName is the lease file an unguarded token is delivered in.
	FileName string
	// TokenExpiresAt is when the token the workload or session holds stops
	// working at GitHub.
	TokenExpiresAt time.Time
	UpdatedAt      time.Time
}

AppTokenSlotRow is one GitHub App installation token a lease holds: the scope it was minted at and when the token held now expires, never the token.

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

type AuditChainFailures struct {
	Path  string `json:"path"`
	Chain string `json:"chain"`
	// Unrecorded is lost and not yet in the chain as an audit.gap row.
	Unrecorded AuditLoss `json:"unrecorded"`
	// TotalEvents were lost since the process started; RecordedEvents of
	// them are in the chain now, in GapRows gap rows.
	TotalEvents      int64     `json:"total_events"`
	RecordedEvents   int64     `json:"recorded_events"`
	GapRows          int64     `json:"gap_rows"`
	LastGapID        int64     `json:"last_gap_id,omitempty"`
	LastGapAt        time.Time `json:"last_gap_at,omitempty"`
	GapWriteFailures int64     `json:"gap_write_failures,omitempty"`
	LastGapError     string    `json:"last_gap_error,omitempty"`
}

AuditChainFailures is one database's account of failed audit appends in this process.

func AuditFailures added in v0.0.5

func AuditFailures() []AuditChainFailures

AuditFailures reports every database this process failed an audit append to, ordered by path. Empty when it never has.

type AuditCheckpointClaim added in v0.0.5

type AuditCheckpointClaim struct {
	// Claimed: the caller writes Window now. Window is the window asked
	// for, or the one before it when a member that claimed that one died
	// before writing it — the window is then written late rather than not
	// at all, and the caller asks again for the current one.
	Claimed bool
	Window  int64
	// Prior is the marker as it was found.
	Prior AuditCheckpointWindow
	// HeldBy names another member writing a window right now, whose claim
	// goes stale at RetryAt; the caller should look again then.
	HeldBy  string
	RetryAt time.Time
	// FutureMarker reports a marker more than one window ahead of the
	// caller's clock, which was overwritten rather than obeyed.
	FutureMarker bool
}

AuditCheckpointClaim says what ClaimAuditCheckpointWindow decided.

type AuditCheckpointWindow added in v0.0.5

type AuditCheckpointWindow struct {
	Window int64 `json:"window"`
	// IntervalS is the interval, in seconds, the window was numbered under:
	// a window number means nothing without it.
	IntervalS int64     `json:"interval_s,omitempty"`
	Member    string    `json:"member"`
	ClaimedAt time.Time `json:"claimed_at"`
	Done      bool      `json:"done"`
}

AuditCheckpointWindow is the shared record of which checkpoint window was last claimed, by which hub member, and whether it was written.

It is what keeps a leader change from either skipping or doubling a window: a new leader resumes from it rather than from its own clock. It lives in the database the checkpoints exist to distrust, which is acceptable because all it can be used for is liveness — the worst a forged marker does is stop checkpoints being written, which `cloop hub doctor` reports as stale — and a marker claiming a window in the future is ignored rather than obeyed.

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

type AuditFailureStatus struct {
	PID  int    `json:"pid"`
	Host string `json:"host"`
	// Instance tells this process from a later one given the same PID, and
	// ProcStart is the kernel's start time for the PID (Linux), which tells it
	// from an unrelated process that reused the PID.
	Instance  string               `json:"instance,omitempty"`
	ProcStart string               `json:"proc_start,omitempty"`
	UpdatedAt time.Time            `json:"updated_at"`
	Chains    []AuditChainFailures `json:"chains"`

	// File is where it was read from; not stored.
	File string `json:"-"`
}

AuditFailureStatus is one process's status file.

func ReadAuditFailureStatus added in v0.0.5

func ReadAuditFailureStatus(dir string) ([]AuditFailureStatus, error)

ReadAuditFailureStatus reads every status file in dir. A directory that does not exist holds none. Only regular files are read: a symlink planted there is not followed.

func (AuditFailureStatus) ProcessRunning added in v0.0.5

func (s AuditFailureStatus) ProcessRunning() bool

ProcessRunning reports whether the process a status file names may still be running.

On the host that wrote it the process table decides: the PID must be alive, be this process only if the instance is this one's, and have the start time the file recorded. From another host only age can: a file nobody rewrote for auditStatusStale is abandoned, a fresher one is taken to be live, since adopting a live process's losses would record them twice.

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

type AuditGapCount struct {
	Name   string `json:"name"`
	Events int64  `json:"events"`
}

AuditGapCount is one line of a gap row's histogram.

type AuditGapPayload added in v0.0.5

type AuditGapPayload struct {
	LostEvents    int64           `json:"lost_events"`
	FailedAppends int64           `json:"failed_appends"`
	FirstFailedAt string          `json:"first_failed_at"`
	LastFailedAt  string          `json:"last_failed_at"`
	FirstError    string          `json:"first_error,omitempty"`
	LastError     string          `json:"last_error,omitempty"`
	Actions       []AuditGapCount `json:"actions,omitempty"`
	Reasons       []AuditGapCount `json:"reasons,omitempty"`
	Recorded      string          `json:"recorded"`
	Host          string          `json:"host,omitempty"`
	PIDs          []int           `json:"pids,omitempty"`
	AdoptedFrom   []int           `json:"adopted_from,omitempty"`
}

AuditGapPayload is what an audit.gap row's payload holds. VerifyAuditChain reads LostEvents back to say how many events a chain's gaps cover.

The histograms are lists of {name, events}, not maps keyed by action. Every payload passes through key-based redaction (MarshalAuditPayload), and an action such as `secret.lease` or `api_token.created` is exactly the kind of key it withholds — as a map key its count would be replaced with a marker, for the very actions an auditor most needs counted.

type AuditGapSummary added in v0.0.5

type AuditGapSummary struct {
	Gaps      int
	Events    int64
	LastID    int64
	LastAt    time.Time
	FirstAt   time.Time
	Truncated bool // more gap rows than were read; Events is a lower bound
}

AuditGapSummary is what a chain's audit.gap rows add up to, read without walking the chain.

type AuditHead added in v0.0.5

type AuditHead struct {
	// LastID, LastRowHash and LastTimestamp describe the newest row; all zero
	// when the table is empty.
	LastID        int64
	LastRowHash   string
	LastTimestamp time.Time
	// FirstID is the oldest surviving row's id, 0 when empty.
	FirstID int64
	// Rows is how many rows the table holds.
	Rows int64
	// Anchor is the newest prune anchor, nil when the chain was never pruned.
	Anchor *AuditAnchor
}

AuditHead is where a chain ends, and what anchors its beginning.

func ReadAuditHead added in v0.0.5

func ReadAuditHead(conn *sql.DB) (AuditHead, error)

ReadAuditHead reads the head of the audit chain in the database behind conn: a connection OpenConn opened, typically ReadOnly on a backup this package must not migrate (Task 20410). A database that predates the audit tables reads as an empty chain, and one that predates prune anchors as unpruned.

type AuditLoss added in v0.0.5

type AuditLoss struct {
	// Events is how many audit rows were lost; Appends how many append calls
	// failed (a batch is one call).
	Events  int64 `json:"events"`
	Appends int64 `json:"appends"`

	FirstAt    time.Time `json:"first_at"`
	LastAt     time.Time `json:"last_at"`
	FirstError string    `json:"first_error,omitempty"`
	LastError  string    `json:"last_error,omitempty"`

	// Actions counts the lost events by action name, Reasons by failure class
	// (the hubmetrics.AuditFail* values).
	Actions map[string]int64 `json:"actions,omitempty"`
	Reasons map[string]int64 `json:"reasons,omitempty"`

	// PIDs names the processes whose failures these are: this one, and any
	// whose losses it adopted. AdoptedFrom names the second kind: processes
	// that exited without recording what they lost.
	PIDs        []int `json:"pids,omitempty"`
	AdoptedFrom []int `json:"adopted_from,omitempty"`
}

AuditLoss describes audit events that could not be appended to one chain.

func (AuditLoss) Empty added in v0.0.5

func (l AuditLoss) Empty() bool

Empty reports whether nothing is described.

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

	// Gaps counts the audit.gap rows the walk passed (Task 20404): records,
	// written into the chain itself, of events that could not be appended.
	// GapEvents is how many lost events they cover, GapIDs the first few of
	// their ids and LastGapAt when the newest was written. A chain with gaps
	// is intact — every row present hashes correctly — but it is not
	// complete, and Status says so.
	Gaps      int
	GapEvents int64
	GapIDs    []int64
	LastGapAt time.Time
}

AuditVerifyReport summarises a chain-verification run.

func (AuditVerifyReport) Status added in v0.0.5

func (r AuditVerifyReport) Status() string

Status names the verdict: AuditChainIntact, AuditChainGaps or AuditChainBroken.

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
	// RevokedCause is why a revoked grant was revoked — "owner_offboarded",
	// "secret_deleted", "superseded", or "" — see migration 0062.
	RevokedCause string
	// SupersededBy names the grant that replaced a superseded one — see
	// migration 0063.
	SupersededBy string
	// SuspendedAt and SuspendedBy record a grant suspended because its owner
	// was deactivated at the identity provider (Task 20415, migration 0067).
	// Written by the SCIM suspension and its lift and by nothing else:
	// PutBrokerGrant sets them on insert and never on update, so a writer
	// holding a copy read before the suspension cannot clear it by saving.
	SuspendedAt string
	SuspendedBy string
	// UnguardedAckAt, UnguardedAckBy and UnguardedAckReason record an
	// administrator's acknowledgement that a kubeconfig grant may be
	// delivered without the Kubernetes access monitor (Task 20420, migration
	// 0070). Written by SetBrokerGrantUnguardedAck alone: PutBrokerGrant sets
	// them on insert and never on update.
	UnguardedAckAt     string
	UnguardedAckBy     string
	UnguardedAckReason 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 BrokerSecretTombstoneRow added in v0.0.5

type BrokerSecretTombstoneRow struct {
	SecretID  string
	Name      string
	Kind      string
	Owner     string
	DeletedAt string
	DeletedBy string
	// Cause is "deleted" or "offboarded"; see migration 0062.
	Cause  string
	Reason string
}

BrokerSecretTombstoneRow is one row of broker_secret_tombstones: what a deleted secret was, kept so a grant left pointing at it can still name it (migration 0062, Task 20400). Metadata only — nothing sealed survives a delete.

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

type CISessionFilter struct {
	RuleID string
	Holder string
	// OpenOnly drops closed sessions.
	OpenOnly bool
}

CISessionFilter narrows ListCISessions. Zero fields do not filter.

type CISessionRow added in v0.0.5

type CISessionRow struct {
	SessionID string
	// TokenSHA256 is the hex SHA-256 of the session token the pipeline
	// presents.
	TokenSHA256 string
	// RuleID is the allowlist rule that admitted the pipeline.
	RuleID string
	// Holder is the hub process serving the session.
	Holder string
	// Instance is the hub instance whose configuration governs the session:
	// the port of the process that minted or last restored it.
	Instance string
	// Provenance is the JSON account of the pipeline the rule admitted: the
	// rule's name, the project it binds, and the verified claims of the OIDC
	// token the session was minted for.
	Provenance string
	// Policy is the JSON account of what the session may do.
	Policy string
	// Counters is the JSON checkpoint of what the session has spent.
	Counters    string
	IssuedAt    time.Time
	ExpiresAt   time.Time
	LastUsedAt  time.Time
	UpdatedAt   time.Time
	ClosedAt    time.Time
	CloseReason string
}

CISessionRow is one minted CI relay session. It says who serves the session, which pipeline it was minted for, what it may do and how its token hashes — never a token, and never the credential the relay presents upstream.

func (CISessionRow) Open added in v0.0.5

func (r CISessionRow) Open() bool

Open reports whether the session has not been closed.

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

func (d *DB) AbandonKEKs(ids []string, at time.Time) ([]string, error)

AbandonKEKs retires the given keys although sealed rows still reference them, and returns the ids it retired (Task 20410).

RetireKEK refuses exactly this, and it is right to: retiring a referenced key destroys what the rows hold. AbandonKEKs exists for the one caller that has already decided that — `cloop db restore --accept-secret-loss`, restoring a backup whose keys the hub's CLOOP_SECRET_KEY cannot derive. Left live, those keys stop the hub starting at all (secretbroker.OpenKeyring refuses a registry with a live key it cannot derive, rather than fork it); retired, the hub mints a fresh primary and every row sealed under them reports that its key was retired and its material cannot be recovered — which is now the truth. The backup file itself still holds the salts.

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

func (d *DB) AdjustQuotaCounter(identity, resource, bucket string, delta, limit float64, enforce bool, at time.Time) (value float64, admitted bool, err error)

AdjustQuotaCounter adds delta to one counter in a single write transaction and returns the value afterwards (Task 20354).

It is the operation several hub processes sharing one control plane need and PutQuotaCounter cannot give them: PutQuotaCounter stores an absolute value each process computed from its own memory, so two processes admitting at once each write "one more than I saw" and one admission vanishes — and each process checks the limit against its own count, so N processes admit N times the cap.

With enforce set and delta positive, the addition is refused — admitted is false and the counter unchanged — when it would take the value above limit. Check and increment share the transaction, which statedb opens IMMEDIATE, so no other writer can land between them. The result never goes below zero: a double release must not mint headroom.

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

func (d *DB) AppendHubEvents(events []HubEventRow) (int64, error)

AppendHubEvents publishes events in one transaction and returns the sequence number of the last one. Batching is what keeps a chatty run's live output from costing one write transaction per line.

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

func (d *DB) AuditCheckpointWindowMarker() (AuditCheckpointWindow, bool, error)

AuditCheckpointWindowMarker reads the window marker, for status output.

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

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

AuditGapSummary totals the chain's audit.gap rows through the event_type index. It is the cheap question `cloop hub doctor` asks; VerifyAuditChain is the thorough one.

func (*DB) AuditHead added in v0.0.5

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

AuditHead reads the chain's newest row, its row count and its newest anchor.

Three plain reads rather than one transaction: every transaction this package opens takes the write lock at BEGIN (connpolicy.go), and a checkpoint must neither stall writers nor fail on a database that has stopped taking writes — that is exactly when its record matters. Holding d.mu orders them against this handle's own appends; another process appending in between moves the head forward, which describes a later moment rather than an inconsistent one.

func (*DB) AuditRowAt added in v0.0.5

func (d *DB) AuditRowAt(id int64) (AuditEvent, bool, error)

AuditRowAt returns the row with id, and whether there is one.

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

func (d *DB) CheckpointCISession(id, holder, counters string, lastUsed time.Time) (bool, error)

CheckpointCISession replaces an open session's counters and last use, provided holder holds it, and reports whether it did.

func (*DB) CheckpointProxySession added in v0.0.5

func (d *DB) CheckpointProxySession(kind, id, holder, counters string) (bool, error)

CheckpointProxySession replaces an open session's counters, provided holder holds it, and reports whether it did.

func (*DB) ClaimAuditCheckpointWindow added in v0.0.5

func (d *DB) ClaimAuditCheckpointWindow(w int64, interval time.Duration, member string, now time.Time, stale time.Duration) (AuditCheckpointClaim, error)

ClaimAuditCheckpointWindow decides, under one write transaction, whether member writes checkpoint window w of interval.

  • Nothing claimed yet, an older window, or a window numbered under another interval (the interval was changed): w is claimed.
  • w, or the window before it, claimed and not written: by member itself (a retry) or by a member whose claim is older than stale (it died between claiming and writing), that window is claimed — late, if it is the one before; by anybody else, not claimed, and RetryAt says when the claim lapses.
  • w already written, or the next window claimed (that member is ahead): not claimed.
  • Further ahead than that: the marker cannot be right, and is replaced.

func (*DB) ClaimExecutorHandleMeta added in v0.0.5

func (d *DB) ClaimExecutorHandleMeta(handleID, key, expect, value string) (bool, error)

ClaimExecutorHandleMeta sets the meta key of handleID's row to value, provided it holds expect now — absent counts as "" — and reports whether it did (Task 20419). It is a compare-and-swap on the whole meta document: the row is re-read and the write conditioned on what was read, so of two hub processes adopting one workload row only one records itself as its owner, whichever process each runs in. A row that is gone is not claimed.

func (*DB) ClaimExecutorSessionRequeue

func (d *DB) ClaimExecutorSessionRequeue(id, claimToken, newClaimToken string, maxAttempts int, 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 failover cap is decided in the same statement (Task 20391). A session whose attempt is at most maxAttempts — re-dispatched fewer times than the cap — becomes requeued; one past it becomes failover_exhausted, and the returned row's State says which. Deciding it here rather than in the caller is what makes every supervisor and every hub member agree: whoever wins the claim also fixes the session's fate, and a loser has nothing left to decide differently. maxAttempts below zero is read as zero — no re-dispatch — because no reading of a bad cap may be "no cap".

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

func (d *DB) ClaimHubOwner(row, expect HubOwnerRow) (bool, error)

ClaimHubOwner takes (row.Kind, row.Key) for row.InstanceID, conditioned on the current row still matching expect — the compare-and-swap AcquireHubLease documents. A zero expect means "I saw no owner"; a populated one means "I saw this owner and judged it gone". It reports whether the claim was taken.

func (*DB) ClaimProjectPath added in v0.0.5

func (d *DB) ClaimProjectPath(claim ProjectClaim, stale func(ProjectClaim) bool) (held *ProjectClaim, err error)

ClaimProjectPath records that claim.Path is being created.

It refuses when a live claim is the same path, inside it, or above it, and returns that claim; held is nil when the path was claimed. stale decides which existing claims no longer hold their path — a creation that died with its hub, a project removed from the registry by hand — and those are deleted in the same transaction. A nil stale treats every claim as live.

claim.Path must be canonical (projectconfine.Canonical).

func (*DB) ClearAdoptRequest added in v0.0.5

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

ClearAdoptRequest removes the stored request if it is still the one with id, and reports whether it did. Compare-and-clear, inside one write transaction: the run clears the request it acted on, and must not clear a second one filed meanwhile.

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

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

ClearExecutorFirewall removes executorID's rule set. Clearing an absent one is not an error.

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

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

ClearProjectFirewall removes the project's rule set.

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

func (d *DB) ClearTaskQuarantine(taskID int) (bool, error)

ClearTaskQuarantine is the explicit reset's half: it removes a task's mark and the losses behind it, so the task starts again with no evidence against it. It reports whether there was a mark to remove.

The losses go with the mark on purpose. A reset is a person deciding the task deserves another chance; keeping its old losses would quarantine it again on the first node lost afterwards, which is not another chance. The audit trail and the event journal keep the history.

func (*DB) Close

func (d *DB) Close() error

Close releases the database connection.

func (*DB) CloseCISession added in v0.0.5

func (d *DB) CloseCISession(id, holder string, at time.Time, reason, counters string) (bool, error)

CloseCISession marks an open session closed with reason, provided holder holds it, and reports whether it did. counters, when non-empty, replaces the checkpoint with the session's final count. A session closed once stays closed: nothing restores it, and a second close changes nothing.

func (*DB) CloseCISessionAny added in v0.0.5

func (d *DB) CloseCISessionAny(id string, at time.Time, reason string) (bool, error)

CloseCISessionAny marks an open session closed whoever holds it, and reports whether it did: an operator's revocation, which must land whether or not the process holding the record is the one asked, or alive. The holder, if live, stops serving it at its next checkpoint.

func (*DB) CloseCISessionsForRule added in v0.0.5

func (d *DB) CloseCISessionsForRule(ruleID string, at time.Time, reason string) ([]CISessionRow, error)

CloseCISessionsForRule closes every open session ruleID minted, whoever holds it, and returns them as they were before the close. It is how an operator's edit, disable or deletion of a rule reaches a session no process is serving at that moment, which would otherwise be restored later under a rule that no longer stands.

func (*DB) CloseExecutorSession

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

CloseExecutorSession moves a running 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.

A session that is no longer running is left as it is and the call succeeds: its end is already recorded. That matters for the one writer that gets there first on purpose — a failover's claim, which records requeued or failover_exhausted (Task 20391). The workload's own watcher notices the end much later, often only when the dead node's stream finally closes, and before this condition its close overwrote the claim's verdict with "failed": the exhausted session vanished from the record, and with it what stops a re-dispatch.

func (*DB) CloseProxySession added in v0.0.5

func (d *DB) CloseProxySession(kind, id, holder string, at time.Time, reason, counters string) (bool, error)

CloseProxySession marks an open session closed with reason, provided holder holds it, and reports whether it did. counters, when non-empty, replaces the checkpoint with the session's final count. A session closed once stays closed: nothing restores it, and a second close changes nothing.

func (*DB) CompleteAuditCheckpointWindow added in v0.0.5

func (d *DB) CompleteAuditCheckpointWindow(w int64, member string) (bool, error)

CompleteAuditCheckpointWindow marks window w written, if member still holds it. Reports whether it did: a member whose claim was taken over in the meantime does not overwrite the new holder's marker.

func (*DB) CompleteProjectClaim added in v0.0.5

func (d *DB) CompleteProjectClaim(path string, hubMade bool, root string) error

CompleteProjectClaim marks a claim created, and records whether this hub made the directory and under which projects root. It fails when there is no claim on path to complete.

func (*DB) CountBrokerSecretsByKey

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

CountBrokerSecretsByKey groups secrets by the key they are sealed under.

func (*DB) CountDeviceAuthorizations added in v0.0.5

func (d *DB) CountDeviceAuthorizations(since time.Time) (int, error)

CountDeviceAuthorizations counts the rows with created_at after since — the code-issuing rate a hub with several members shares.

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

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

CountHubTLSByKey groups the rows by the key sealing their private keys.

func (*DB) CountLiveSessions added in v0.0.5

func (d *DB) CountLiveSessions(c SessionCutoffs) (int, error)

CountLiveSessions counts the sessions DeleteExpiredSessions would keep under the same cutoffs. A count rather than a listing, so nothing sealed leaves the row.

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

func (d *DB) CreateVirtualExecutor(v VirtualExecutor) error

CreateVirtualExecutor records a new virtual executor and the executors-table row that makes it bindable, in one transaction.

The ID must be free across *every* executor the control plane knows: a virtual executor that shared an ID with a device would inherit that device's bindings, ceiling and access list, and a binding meant for one would run on the other.

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

func (d *DB) DeleteAppTokenSlot(leaseID, grantID, holder string) (bool, error)

DeleteAppTokenSlot forgets one slot, provided holder holds it.

func (*DB) DeleteAppTokenSlots added in v0.0.5

func (d *DB) DeleteAppTokenSlots(leaseID, holder string) (int, error)

DeleteAppTokenSlots forgets the slots of leaseID that holder holds, and returns how many went.

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

func (d *DB) DeleteBrokerSecretTombstoned(id string, tomb BrokerSecretTombstoneRow) error

DeleteBrokerSecretTombstoned removes a secret and records its tombstone in the same transaction, so there is no moment at which the row is gone and nothing remembers what it was. A tombstone already present for the id — a secret deleted, re-minted under the same id by a restore, and deleted again — is replaced by the newer one.

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

func (d *DB) DeleteCISession(id, holder string) (bool, error)

DeleteCISession forgets a session, provided holder holds it, and reports whether a row went. It is how the janitor retires the record of a session that closed or lapsed.

func (*DB) DeleteDeviceAuthorizationsBefore added in v0.0.5

func (d *DB) DeleteDeviceAuthorizationsBefore(cutoff time.Time) ([]DeviceAuthorizationRow, error)

DeleteDeviceAuthorizationsBefore removes the rows whose expires_at is at or before cutoff and returns them, so the caller can act on an approval nobody collected. Read and deleted in one transaction, for the reason DeleteExpiredSessions gives.

func (*DB) DeleteDispatchIntent added in v0.0.5

func (d *DB) DeleteDispatchIntent(runID string) error

DeleteDispatchIntent forgets one intent. A missing row is not an error: the run's record and its end both retire it, and either may come second.

func (*DB) DeleteDispatchIntentForHandle added in v0.0.5

func (d *DB) DeleteDispatchIntentForHandle(executorID, handleID string) (bool, error)

DeleteDispatchIntentForHandle forgets the intent naming handleID on executorID, and reports whether one went: what recording the run does, which knows the workload by its handle rather than by the run id.

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(c SessionCutoffs) ([]SessionRow, error)

DeleteExpiredSessions removes sessions past one of c's cutoffs and returns what it deleted.

The absolute cutoff is compared against expires_at (the ceiling set at login) and the idle cutoffs against last_seen, each kind against its own: a CLI session idles out on a clock no longer than a browser's (Task 20412). All 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) DeleteLegalHold added in v0.0.5

func (d *DB) DeleteLegalHold(identity string, audit *AuditEvent) (bool, error)

DeleteLegalHold releases a hold, reporting whether one was placed.

func (*DB) DeleteOrphanedAppTokenSlots added in v0.0.5

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

DeleteOrphanedAppTokenSlots forgets every slot whose lease has no record any more — released, retired or swept — and returns how many went. The janitor's sweep: a slot outlives its lease only when the process holding both stopped between the two deletes.

func (*DB) DeleteProjectMember added in v0.0.5

func (d *DB) DeleteProjectMember(projectPath, identityKey string, audit MemberAudit) (*ProjectMemberRow, error)

DeleteProjectMember removes one membership and returns the row it removed, or nil when there was none — in which case nothing is written and audit is not called, because nothing changed.

func (*DB) DeleteProjectMembersOf added in v0.0.5

func (d *DB) DeleteProjectMembersOf(projectPath string, audit MemberAudit) ([]ProjectMemberRow, error)

DeleteProjectMembersOf removes every membership on one project and returns the rows removed. Called when the project leaves the hub: memberships are keyed by path, and a roster outliving its project would admit those people to whatever is registered at that path next. audit is called once per removed row.

func (*DB) DeleteProxySession added in v0.0.5

func (d *DB) DeleteProxySession(kind, id, holder string) (bool, error)

DeleteProxySession forgets a session, provided holder holds it, and reports whether a row went. It is how the janitor retires the record of a session that closed or lapsed.

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

func (d *DB) DeleteSCIMGroup(id string, audit []*AuditEvent) (bool, error)

DeleteSCIMGroup removes a provisioned group and its memberships.

func (*DB) DeleteSCIMUser added in v0.0.5

func (d *DB) DeleteSCIMUser(id string, audit []*AuditEvent) (bool, error)

DeleteSCIMUser removes a provisioned user and its group memberships, with its audit rows, after the offboarding that deleting one runs.

func (*DB) DeleteSecretLease added in v0.0.5

func (d *DB) DeleteSecretLease(leaseID, holder string) (bool, error)

DeleteSecretLease forgets a lease, provided holder still holds it, and reports whether a row went. A missing row is not an error: a lease can be released twice — once when its run ends, once by a revocation that raced it — and neither call should fail.

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

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

DeleteVirtualExecutor removes a virtual executor and everything keyed by its ID: its executors-table row, the projects bound to it, and its sandbox, ceiling and access-list rows. One transaction, because a half-deleted virtual executor whose access list survived would, if the ID were ever reused, hand the new one an audience nobody chose for it.

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

func (d *DB) ExecutorFirewall(executorID string) (FirewallRecord, bool, error)

ExecutorFirewall returns the device-level rule set stored for executorID.

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

func (d *DB) ExecutorSessionChain(id string) ([]ExecutorSessionRow, error)

ExecutorSessionChain returns the failover chain that ends at id, oldest first: the session the run was first dispatched as, then each replacement a failover opened (requeued_from), down to id itself (Task 20391). It is how a failover names every node a run has lost so far.

The walk is bounded (maxSessionChain) and refuses to revisit a session, so a cycle written into the table by hand ends the walk rather than the process. A predecessor that is gone — pruned by retention — ends it too: the chain is then what is left of it.

func (*DB) ExecutorSessionForHandle added in v0.0.5

func (d *DB) ExecutorSessionForHandle(executorID, handleID string) (ExecutorSessionRow, bool, error)

ExecutorSessionForHandle returns the session recorded for the workload handleID on executorID, the most recent one if several name it, and whether there is one (Task 20396).

It is how the hub learns, from a run it follows — which it knows by its executor and handle, not by its session — whether the failover claim has taken that run off its executor. Read from the table rather than told by the supervisor because the supervisor that claims a session is often in another process: another hub member, or this hub before a restart.

The executor_id half of idx_executor_sessions_executor_state narrows the scan to one executor's sessions.

func (*DB) ExecutorSessionSuccessor added in v0.0.5

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

ExecutorSessionSuccessor returns the session a failover opened to replace id, if there is one.

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

func (d *DB) ExtendProxySession(kind, id, holder string, expiresAt time.Time) (bool, error)

ExtendProxySession moves an open session's deadline, provided holder holds it, and reports whether it did.

func (*DB) ExtendSecretLease added in v0.0.5

func (d *DB) ExtendSecretLease(leaseID, holder string, expiresAt time.Time) (bool, error)

ExtendSecretLease moves a lease's deadline, provided holder still holds it, and reports whether it did. A lease taken over by another process is not extended by the one that lost it.

func (*DB) FindSCIMGroups added in v0.0.5

func (d *DB) FindSCIMGroups(byDisplayName bool, value string) ([]SCIMGroupRow, error)

FindSCIMGroups returns the groups whose display name is name, compared lowercased, or whose external id is name, exactly.

func (*DB) FindSCIMUsers added in v0.0.5

func (d *DB) FindSCIMUsers(column, value string) ([]SCIMUserRow, error)

FindSCIMUsers returns the users whose column equals value: user_name_key or external_id_key compare lowercased values, external_id and email as given.

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

func (d *DB) GetBrokerSecretTombstone(secretID string) (BrokerSecretTombstoneRow, error)

GetBrokerSecretTombstone returns the tombstone of a deleted secret, or a wrapped ErrBrokerSecretNotFound when nothing was recorded for the id — a secret deleted before tombstones existed, or one that never existed.

func (*DB) GetCIPipelineRule added in v0.0.2

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

GetCIPipelineRule returns one rule by ID.

func (*DB) GetCISession added in v0.0.5

func (d *DB) GetCISession(id string) (CISessionRow, error)

GetCISession returns one session, or a wrapped ErrCISessionNotFound.

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

func (d *DB) GetDeviceAuthorization(deviceHash string) (DeviceAuthorizationRow, error)

GetDeviceAuthorization returns the row for a device code digest, or an error wrapping ErrDeviceAuthorizationNotFound.

func (*DB) GetDeviceAuthorizationByUserCode added in v0.0.5

func (d *DB) GetDeviceAuthorizationByUserCode(userCodeHash string) (DeviceAuthorizationRow, error)

GetDeviceAuthorizationByUserCode returns the newest row for a user code digest. Newest, because the index is not unique: a code drawn again after its first holder expired would otherwise resolve to the dead one.

func (*DB) GetDispatchIntent added in v0.0.5

func (d *DB) GetDispatchIntent(runID string) (DispatchIntentRow, error)

GetDispatchIntent returns one intent, or a wrapped ErrDispatchIntentNotFound.

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

func (d *DB) GetHubOwner(kind, key string) (HubOwnerRow, error)

GetHubOwner returns the owner row for (kind, key), or ErrHubOwnerNotFound.

func (*DB) GetHubTLS added in v0.0.5

func (d *DB) GetHubTLS(id string) (HubTLSRow, error)

GetHubTLS returns the row by id, or ErrHubTLSNotFound.

func (*DB) GetProjectMember added in v0.0.5

func (d *DB) GetProjectMember(projectPath, identityKey string) (ProjectMemberRow, error)

GetProjectMember returns one membership, or an error wrapping ErrProjectMemberNotFound.

func (*DB) GetProxySession added in v0.0.5

func (d *DB) GetProxySession(kind, id string) (ProxySessionRow, error)

GetProxySession returns one session, or a wrapped ErrProxySessionNotFound.

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

func (d *DB) GetSCIMCredential(id string) (SCIMCredentialRow, error)

GetSCIMCredential returns one credential by id.

func (*DB) GetSCIMGroup added in v0.0.5

func (d *DB) GetSCIMGroup(id string) (SCIMGroupRow, error)

GetSCIMGroup returns one provisioned group with its members.

func (*DB) GetSCIMUser added in v0.0.5

func (d *DB) GetSCIMUser(id string) (SCIMUserRow, error)

GetSCIMUser returns one provisioned user, or an error wrapping ErrSCIMNotFound.

func (*DB) GetSecretLease added in v0.0.5

func (d *DB) GetSecretLease(leaseID string) (SecretLeaseRow, error)

GetSecretLease returns one lease, or a wrapped ErrSecretLeaseNotFound.

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

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

HeartbeatHubMember advances a member's heartbeat.

false means the row is gone or marked left: a peer or an operator decided this process is no longer a member, and it must stop acting as one rather than quietly re-inserting itself.

func (*DB) History added in v0.0.5

func (d *DB) History(q HistoryQuery) (HistoryPage, error)

History reads one page of the journal.

Every read is a seek and a LIMIT query per table, on the julianday index for a page down and on the primary key for a read of what is new, so its cost follows the page size rather than the journal's length. An Offset read also walks the index entries it skips.

func (*DB) HistoryEvent added in v0.0.5

func (d *DB) HistoryEvent(id int64) (*EventRow, error)

HistoryEvent returns event id whole, or nil when there is no such event.

func (*DB) HistoryStep added in v0.0.5

func (d *DB) HistoryStep(n int64) (*StepRow, error)

HistoryStep returns step n whole, or nil when there is no such step.

func (*DB) HistoryTop added in v0.0.5

func (d *DB) HistoryTop() (HistoryCursor, error)

HistoryTop returns the newest step and event written.

func (*DB) HubEventBounds added in v0.0.5

func (d *DB) HubEventBounds() (lowest, highest, issued int64, err error)

HubEventBounds returns the lowest and highest seq still on the bus, and the highest seq ever issued. A reader starts at the high-water mark so it does not replay history, and compares its cursor with the lowest seq to learn that it fell so far behind that pruning took events it never read.

The high-water mark comes from sqlite_sequence, not MAX(seq): after the leader prunes every row the table is empty, and a reader that restarted then must still begin past the numbers already handed out.

func (*DB) HubEventsAfter added in v0.0.5

func (d *DB) HubEventsAfter(after int64, limit int) ([]HubEventRow, error)

HubEventsAfter returns up to limit events with seq > after, in order.

Reading by seq is only lossless because SQLite has one writer at a time: a row with a higher seq can never become visible before one with a lower seq, so a reader that has seen seq N has seen everything at or below it. A database with concurrent writers would need a different cursor.

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

func (d *DB) InsertCISession(row CISessionRow) error

InsertCISession records a session that was just minted. A row with the same id is an error, never overwritten: an id is 96 random bits, and a second session under one would be a session whose record describes another.

func (*DB) InsertHubTLS added in v0.0.5

func (d *DB) InsertHubTLS(row HubTLSRow) (inserted bool, err error)

InsertHubTLS writes a row that does not exist yet, and reports whether it was written. An existing row is left exactly as it is and inserted is false: the CA is minted once, and of two hub processes minting it at the same moment the one that loses must adopt the winner's rather than replace it — a replaced CA is one every sandbox already given the old one refuses.

func (*DB) InsertSCIMCredential added in v0.0.5

func (d *DB) InsertSCIMCredential(c SCIMCredentialRow, audit *AuditEvent) error

InsertSCIMCredential stores a new credential, refusing a name a live one already has, in one transaction with its audit row.

func (*DB) JoinHubMember added in v0.0.5

func (d *DB) JoinHubMember(row HubMemberRow) error

JoinHubMember records row as a serving member, replacing any previous row for the same instance id. Instance ids are minted per process, so a replace only ever happens to a process re-joining after it left.

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

func (d *DB) LeaseRevocations(leaseID string) ([]LeaseRevocationRow, error)

LeaseRevocations returns every revocation recorded for leaseID, the whole-lease one first.

func (*DB) LeaveHubMember added in v0.0.5

func (d *DB) LeaveHubMember(instanceID string, at time.Time) error

LeaveHubMember marks a member as having left gracefully. Idempotent.

func (*DB) LegalHoldFor added in v0.0.5

func (d *DB) LegalHoldFor(identities []string) (LegalHoldRow, bool, error)

LegalHoldFor returns the hold placed on any of identities, if there is one.

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

func (d *DB) ListAppTokenSlots(leaseID string) ([]AppTokenSlotRow, error)

ListAppTokenSlots returns the slots leaseID holds, or every slot when leaseID is empty.

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

func (d *DB) ListCISessions(f CISessionFilter) ([]CISessionRow, error)

ListCISessions returns the sessions matching f, oldest first.

func (*DB) ListDispatchIntents added in v0.0.5

func (d *DB) ListDispatchIntents() ([]DispatchIntentRow, error)

ListDispatchIntents returns every outstanding intent, oldest 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) ListExecutorFirewalls added in v0.0.5

func (d *DB) ListExecutorFirewalls() ([]FirewallRecord, error)

ListExecutorFirewalls returns every device-level rule set, newest first.

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

func (d *DB) ListHubMembers() ([]HubMemberRow, error)

ListHubMembers returns every member row, serving or not, oldest first.

func (*DB) ListHubOwners added in v0.0.5

func (d *DB) ListHubOwners(kind string) ([]HubOwnerRow, error)

ListHubOwners returns every row of one kind, or of every kind when kind is empty, ordered by kind then key.

func (*DB) ListHubTLS added in v0.0.5

func (d *DB) ListHubTLS() ([]HubTLSRow, error)

ListHubTLS returns every row, by id.

func (*DB) ListHubTLSNotUnderKey added in v0.0.5

func (d *DB) ListHubTLSNotUnderKey(keyID string, limit int) ([]HubTLSRow, error)

ListHubTLSNotUnderKey returns up to limit rows whose private key is sealed under some other key, by id.

func (*DB) ListKEKs

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

ListKEKs returns every KEK, newest first.

func (*DB) ListLeaseRevocations added in v0.0.5

func (d *DB) ListLeaseRevocations(pendingOnly bool) ([]LeaseRevocationRow, error)

ListLeaseRevocations returns every recorded revocation, or with pendingOnly those not yet enforced, oldest first.

func (*DB) ListLegalHolds added in v0.0.5

func (d *DB) ListLegalHolds() ([]LegalHoldRow, error)

ListLegalHolds returns every hold.

func (*DB) ListProjectClaims added in v0.0.5

func (d *DB) ListProjectClaims() ([]ProjectClaim, error)

ListProjectClaims returns every claim, ordered by path.

func (*DB) ListProjectExecutorBindings

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

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

func (*DB) ListProjectFirewalls added in v0.0.5

func (d *DB) ListProjectFirewalls() ([]FirewallRecord, error)

ListProjectFirewalls returns every project rule set, newest first.

func (*DB) ListProjectMembers added in v0.0.5

func (d *DB) ListProjectMembers() ([]ProjectMemberRow, error)

ListProjectMembers returns every membership on the hub, ordered by project then identity. The whole table, because the authorization path needs all of it: pkg/projectmember caches one snapshot and answers both "who is on this project" and "what does this person hold" from it. Rows number one per person per shared project, so the table stays small.

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

func (d *DB) ListProxySessions(f ProxySessionFilter) ([]ProxySessionRow, error)

ListProxySessions returns the sessions matching f, oldest first.

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

func (d *DB) ListRetiredStaticTokens() ([]RetiredStaticTokenRow, error)

ListRetiredStaticTokens returns every retirement, newest first. The whole table, because a hub refuses every fingerprint in it and the table holds one row per token ever retired.

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

func (d *DB) ListSCIMCredentials() ([]SCIMCredentialRow, error)

ListSCIMCredentials returns every credential, revoked ones included, newest first.

func (*DB) ListSCIMGroups added in v0.0.5

func (d *DB) ListSCIMGroups(withMembers bool) ([]SCIMGroupRow, error)

ListSCIMGroups returns every provisioned group, oldest first, with members when withMembers is set.

func (*DB) ListSCIMUsers added in v0.0.5

func (d *DB) ListSCIMUsers() ([]SCIMUserRow, error)

ListSCIMUsers returns every provisioned user, oldest 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) ListSecretLeases added in v0.0.5

func (d *DB) ListSecretLeases() ([]SecretLeaseRow, error)

ListSecretLeases returns every recorded lease, oldest first.

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

func (d *DB) ListStaticTokenUse() ([]StaticTokenUseRow, error)

ListStaticTokenUse returns every row, the most recently held first.

func (*DB) ListTaskQuarantines added in v0.0.5

func (d *DB) ListTaskQuarantines() (map[int]*pm.TaskQuarantine, error)

ListTaskQuarantines returns every marked task's mark, keyed by task id.

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

func (d *DB) ListVirtualExecutors() ([]VirtualExecutor, error)

ListVirtualExecutors returns every virtual executor, ordered by parent and then name.

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

func (d *DB) LoadSCIMDirectory() (SCIMDirectory, error)

LoadSCIMDirectory reads the whole directory at one version.

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

func (d *DB) MarkDispatchStarted(runID, handleID string, egressIDs []string) (bool, error)

MarkDispatchStarted completes an intent with the workload's handle and the egress sessions bound to it, and reports whether the intent was there.

func (*DB) MarkHubSeen added in v0.0.5

func (d *DB) MarkHubSeen(kind, key string, expires, now time.Time) (bool, error)

MarkHubSeen records (kind, key) as seen until expires and reports whether it was fresh — not already recorded with an expiry still ahead of now. Check and record share one write transaction, so of two members presenting the same value at once exactly one is told it was fresh.

func (*DB) MarkLeaseRevocationEnforced added in v0.0.5

func (d *DB) MarkLeaseRevocationEnforced(leaseID, grantID, by, detail string, at time.Time) (bool, error)

MarkLeaseRevocationEnforced records that a pending revocation's material was taken back from every holder, and reports whether it was pending.

func (*DB) MarkSCIMUserOffboarded added in v0.0.5

func (d *DB) MarkSCIMUserOffboarded(id string, at time.Time, audit []*AuditEvent) (bool, error)

MarkSCIMUserOffboarded records that the full offboarding ran for a user and clears its suspension record: what an offboarding destroyed is never restored, so a later reactivation must find nothing to lift. It reports false when the user is gone, active again, or already marked.

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

func (d *DB) NarrowCISession(id, holder, instance, policy string, expiresAt time.Time) (bool, error)

NarrowCISession rewrites an open session's policy and deadline — what a restore held it to — and the instance now serving it, provided holder holds it, and reports whether it did.

func (*DB) NoteDispatchIntent added in v0.0.5

func (d *DB) NoteDispatchIntent(runID, detail string) error

NoteDispatchIntent records why an intent is still outstanding.

func (*DB) NoteLeaseRevocation added in v0.0.5

func (d *DB) NoteLeaseRevocation(leaseID, grantID, detail string) error

NoteLeaseRevocation records why a pending revocation is still pending. A revocation already enforced is left alone.

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

func (d *DB) OpenCISessionIDsHeldBy(holder string) ([]string, error)

OpenCISessionIDsHeldBy returns the ids of the open sessions holder holds: what a hub process checks the sessions it serves against on every checkpoint tick, without reading the records themselves.

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

func (d *DB) Path() string

Path returns the database file this handle was opened over, as the caller named it. A subscriber to the secret broker's revocations uses it to tell which control plane a revocation belongs to (Task 20403).

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

func (d *DB) PendingSCIMOffboards(cutoff time.Time) ([]SCIMUserRow, error)

PendingSCIMOffboards returns the users inactive since before cutoff whose full offboarding has not run.

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

func (d *DB) ProjectClaimAt(path string) (ProjectClaim, bool, error)

ProjectClaimAt returns the claim on exactly path.

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

func (d *DB) ProjectFirewall(projectPath string) (FirewallRecord, bool, error)

ProjectFirewall returns the rule set stored for the project at projectPath.

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

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

PruneHubEvents deletes events published before cutoff.

func (*DB) PruneHubMembers added in v0.0.5

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

PruneHubMembers deletes the rows of members that stopped heartbeating before cutoff, whether they left gracefully or not. The row of a member that is gone serves forensics for a while and then only clutters `cloop hub cluster status`, so the leader trims it.

func (*DB) PruneHubSeen added in v0.0.5

func (d *DB) PruneHubSeen(now time.Time) (int64, error)

PruneHubSeen deletes rows whose expiry has passed.

func (*DB) PruneLeaseRevocations added in v0.0.5

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

PruneLeaseRevocations deletes the revocations enforced before cutoff, and reports how many went. A pending revocation is never pruned: it is the one thing that tells a member reaching its workload to take the material back.

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

func (d *DB) PutAppTokenSlot(row AppTokenSlotRow) error

PutAppTokenSlot records a slot, replacing any row for the same lease and grant.

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

func (d *DB) PutDeviceAuthorization(row DeviceAuthorizationRow) error

PutDeviceAuthorization inserts a row. An insert rather than an upsert, as for sessions: the device code is 256 random bits, and silently replacing a row would hand one terminal's approval to another.

func (*DB) PutDispatchIntent added in v0.0.5

func (d *DB) PutDispatchIntent(row DispatchIntentRow) error

PutDispatchIntent records a workload about to be started. A run id is minted once per dispatch, so a conflict is the same dispatch writing again, and its row is brought up to date.

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

func (d *DB) PutHubOwner(row HubOwnerRow) error

PutHubOwner records row unconditionally: the latest writer wins.

For facts rather than claims. An agent's socket is wherever the agent last connected — the member it left may not have noticed yet, and must not be able to keep the row by virtue of having written it first.

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

func (d *DB) PutLeaseRevocation(row LeaseRevocationRow) error

PutLeaseRevocation records a revocation. Revoking a lease (or grant) that is already revoked re-opens it: the reason and actor are the latest ones, a kill is never weakened to a scrub, the first revocation's time is kept, and the enforcement is cleared — the new request is enforced again, which for a revocation already enforced finds nothing left and settles at once.

func (*DB) PutLegalHold added in v0.0.5

func (d *DB) PutLegalHold(h LegalHoldRow, audit *AuditEvent) error

PutLegalHold places a hold, or restates one already placed — its reason and who placed it are replaced, its first placement time kept.

func (*DB) PutProjectMember added in v0.0.5

func (d *DB) PutProjectMember(row ProjectMemberRow, audit MemberAudit) (ProjectMemberRow, *ProjectMemberRow, error)

PutProjectMember inserts or replaces one membership and returns the row as stored along with the one it replaced (nil for a new member).

A membership is found by its pair — project and identity — never by its id alone, so a row written under another id scheme (a stranded build's) is still the one replaced, and a new pair whose derived id is taken fails on the primary key instead of overwriting somebody else's row.

The caller passes an already-normalised path and identity key; pkg/projectmember is the one definition of when two memberships are the same, and a second one here would disagree with it exactly when somebody re-grants a role.

func (*DB) PutProxySession added in v0.0.5

func (d *DB) PutProxySession(row ProxySessionRow) error

PutProxySession records a session, replacing any row with the same kind and id.

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

func (d *DB) PutSecretLease(row SecretLeaseRow) error

PutSecretLease records a lease, replacing any row with the same id.

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

func (d *DB) PutTaskQuarantine(taskID int, q pm.TaskQuarantine) error

PutTaskQuarantine marks a task. Marking one that is already marked updates the mark in place: the newest evidence names more nodes, never fewer.

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

func (d *DB) RecordTaskNodeLoss(taskID int, loss pm.NodeLoss) (bool, error)

RecordTaskNodeLoss records that loss.ExecutorID went unreachable while task taskID was running. Recording the same session twice for one task is a no-op: the claim that produces a loss is exactly-once, and a retry of its handler must not count one lost node twice.

It reports whether a row was added.

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

func (d *DB) ReleaseHubOwner(kind, key, instanceID string) (bool, error)

ReleaseHubOwner deletes (kind, key) if instanceID still owns it. A member releasing something another member has since taken over releases nothing.

func (*DB) ReleaseHubOwnerIfClaim added in v0.0.5

func (d *DB) ReleaseHubOwnerIfClaim(expect HubOwnerRow) (bool, error)

ReleaseHubOwnerIfClaim deletes (kind, key) if it is still exactly the claim the caller read — owner and claim instant. It is how a member removes an orphan it judged dead without racing a member that adopted it meanwhile.

func (*DB) ReleaseProjectClaim added in v0.0.5

func (d *DB) ReleaseProjectClaim(path string) error

ReleaseProjectClaim drops the claim on path, if there is one: a creation that failed, or a project removed from the hub.

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

func (d *DB) ReplaceHubTLS(next HubTLSRow, expectSerial string) (bool, error)

ReplaceHubTLS swaps the row by id for next if, and only if, its serial is still expectSerial, and reports whether it did. False is an ordinary outcome: another hub process replaced it first, and the caller re-reads the winner.

Updated in place, never deleted and re-inserted, for the reason SaveState is (tests/arch/rowreplace_test.go): a binary that knows fewer columns must not reset the ones it does not know.

func (*DB) ReplaceHubTLSSealed added in v0.0.5

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

ReplaceHubTLSSealed swaps a row's key envelope under compare-and-swap on the key id and the exact ciphertext, for the reason ReplaceBrokerSecretSealed gives: a leaf renewed while a rotation was reading it must not have the previous key restored under a fresh wrap.

func (*DB) ReplaceQuotaGauges

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

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

A transaction 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.

The swap updates the gauges that stay in place and deletes only the ones that go. It used to delete every gauge row and insert the set again, which reset any column this binary does not know on every reconciliation — the row replacement Task 20388 removed from every writer.

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

func (d *DB) ReportStaticTokenUse(reports []StaticTokenUseReport) error

ReportStaticTokenUse merges reports into static_token_use in one transaction.

Merged rather than overwritten, because several hub processes may hold one token and each flushes only what it saw: the latest use and the latest refusal win whichever process reported them, the refusal count adds up, and a report that saw no use leaves the recorded one alone. Compared as times in Go rather than as text in SQL, since RFC 3339 with trimmed fractions does not sort as a string.

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

func (d *DB) RetireStaticToken(row RetiredStaticTokenRow, ev *AuditEvent) (stored RetiredStaticTokenRow, written bool, err error)

RetireStaticToken records a retirement and, in the same commit, the audit event saying so, so the two cannot disagree: a retirement with no record is an authority change nobody can review, and a record with no retirement claims a containment that did not happen.

Retiring a fingerprint that is already retired writes nothing and returns the stored row with written=false. The first retirement is the one an auditor reads, and re-stamping it would move the date a caller is told the token stopped working. ev may be nil, for a caller that records elsewhere.

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

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

RevokeBrokerGrantWithCause is RevokeBrokerGrant recording why (migration 0062). The cause is written only by the revocation that stamps the grant: a grant already revoked keeps the cause it was revoked with, as it keeps the time.

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

func (d *DB) RevokeSCIMCredential(id string, at time.Time, by string, audit *AuditEvent) (bool, error)

RevokeSCIMCredential revokes a credential, both its secrets at once. A credential already revoked keeps the time it was revoked at and reports false.

func (*DB) RewriteSecretLeaseRecord added in v0.0.5

func (d *DB) RewriteSecretLeaseRecord(leaseID, holder, record string) (bool, error)

RewriteSecretLeaseRecord replaces a lease's record document, provided holder still holds it, and reports whether it did (Task 20403): a grant withdrawn from a live lease, or one standing on its successor, is written into the record so the process that takes the lease over inherits it.

func (*DB) Role added in v0.0.2

func (d *DB) Role() Role

Role reports how this handle has been classified.

func (*DB) RotateSCIMCredential added in v0.0.5

func (d *DB) RotateSCIMCredential(id, secretHash, previousHash string, previousExpires, at time.Time, by string,
	audit *AuditEvent) error

RotateSCIMCredential replaces a live credential's secret. The secret it replaces stays valid until previousExpires — zero ends it at once.

func (*DB) SCIMDirectoryVersion added in v0.0.5

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

SCIMDirectoryVersion reads the directory's version: one row, for a cache deciding whether to reload.

func (*DB) SCIMGroupsOf added in v0.0.5

func (d *DB) SCIMGroupsOf(userID string) ([]string, error)

SCIMGroupsOf returns the ids of the groups a user belongs to.

func (*DB) SaveAdoptRequest added in v0.0.5

func (d *DB) SaveAdoptRequest(r *runbuild.Request) error

SaveAdoptRequest files a request to adopt a newer build, replacing any earlier one.

func (*DB) SaveCommitPolicy added in v0.0.5

func (d *DB) SaveCommitPolicy(p *pm.CommitPolicy) error

SaveCommitPolicy stores the project's "done means committed" setting and nothing else (Task 20370), for the same reason as SaveReviewGate: the dashboard and the CLI change it while a run may be saving its tasks.

func (*DB) SaveFollowBuilds added in v0.0.5

func (d *DB) SaveFollowBuilds(on bool) error

SaveFollowBuilds stores the project's "follow new builds" option.

func (*DB) SaveReviewGate added in v0.0.5

func (d *DB) SaveReviewGate(g *pm.ReviewGate) error

SaveReviewGate stores the project's review gate settings and nothing else (Task 20357). The dashboard and `cloop review gate` change them while a run may be saving its tasks; a full SaveState from their copy would write stale tasks back over the run's.

func (*DB) SaveRunOwner added in v0.0.5

func (d *DB) SaveRunOwner(o *runbuild.Owner) error

SaveRunOwner stores the run-owner record. Nil clears it.

func (*DB) SaveRunStatus added in v0.0.5

func (d *DB) SaveRunStatus(status string, reason *pausereason.Reason, updatedAt time.Time) error

SaveRunStatus stores the run's status and pause reason and nothing else (Task 20362).

It exists for the one write that has to land after a full SaveState has just failed: an orchestrator that could not record a task's outcome stops, and says why it stopped. A full save rewrites every task row and merges the plan first — which is the very write that was refused when, say, a damaged row makes the merge give up — while three metadata keys in one transaction are the smallest change that tells the dashboard the run is no longer running. It leaves the plan exactly as the last successful save left it, so the outcome that was lost stays lost rather than half-written.

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

func (d *DB) SetBrokerGrantUnguardedAck(id, at, by, reason string) error

SetBrokerGrantUnguardedAck records — or, with every argument empty, clears — the unguarded acknowledgement on grant id (migration 0070). A grant that does not exist is a wrapped ErrBrokerGrantNotFound.

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

func (d *DB) SetExecutorFirewall(executorID string, r executor.FirewallRules, setBy string) error

SetExecutorFirewall stores executorID's rule set, replacing any previous one. An empty rule set is stored, not refused: it means "reach nothing", the strongest thing a firewall can say. ClearExecutorFirewall removes the level.

It does not check containment or constrain anything underneath; callers whose write depends on what they read use UpdateFirewalls.

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

func (d *DB) SetExecutorSessionRunningTasks(id string, tasks []int) (bool, error)

SetExecutorSessionRunningTasks records which tasks a running session's workload is working on (Task 20391). Only a session still running is written: once a claim has taken it, what it was running is history the failover already read, and a late write from a stream that had not caught up must not change it.

It reports whether a row was written.

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

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

SetHubMetaIfAbsent writes value only when key has no value yet, and returns whichever value the key holds afterwards. Two processes minting the same secret at once both call it and both come away with the first one written — which a read-then-SetHubMeta cannot promise, since the second write would silently replace a value the first process is already using.

func (*DB) SetProjectFirewall added in v0.0.5

func (d *DB) SetProjectFirewall(projectPath string, r executor.FirewallRules, setBy string) error

SetProjectFirewall stores the project's rule set, 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) SetQueryOnly added in v0.0.5

func (d *DB) SetQueryOnly(on bool) error

SetQueryOnly makes every write through this handle fail, as a read-only database file fails it — for root too, unlike a file mode — or allows writes again. It binds this handle's one connection and no other. For a reader that must not write, and for a test that needs a write to fail the way a read-only database fails it (Task 20419).

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

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

SupersedeBrokerGrant stamps a grant revoked because successor replaced it (migration 0063): revoked_cause 'superseded' and superseded_by naming the successor. As with any revocation, a grant already revoked keeps the time and cause — and successor — it was revoked with.

func (*DB) TakeAppTokenSlots added in v0.0.5

func (d *DB) TakeAppTokenSlots(leaseID, from, holder string) (int, error)

TakeAppTokenSlots moves every slot of leaseID still held by from to holder, and returns how many moved.

func (*DB) TakeCISession added in v0.0.5

func (d *DB) TakeCISession(id, from, holder, instance string) (bool, error)

TakeCISession moves an open session to holder, of instance, provided from still holds it, and reports whether it did. The condition is what lets two processes race to restore the same session and only one of them serve it.

func (*DB) TakeProxySession added in v0.0.5

func (d *DB) TakeProxySession(kind, id, from, holder string) (bool, error)

TakeProxySession moves an open session to holder, provided from still holds it, and reports whether it did. The condition is what lets two processes race to restore the same session and only one of them serve it.

func (*DB) TakeSecretLease added in v0.0.5

func (d *DB) TakeSecretLease(leaseID, from, holder string) (bool, error)

TakeSecretLease moves a lease to holder, provided it is still held by from, and reports whether it did. The condition is what lets two processes race to adopt the same run and only one of them hold its lease.

func (*DB) TaskNodeLosses added in v0.0.5

func (d *DB) TaskNodeLosses(taskID int) ([]pm.NodeLoss, error)

TaskNodeLosses returns the losses recorded against a task since its last explicit reset, oldest first.

func (*DB) TaskQuarantine added in v0.0.5

func (d *DB) TaskQuarantine(taskID int) (*pm.TaskQuarantine, error)

TaskQuarantine returns a task's mark, or nil when it carries none.

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

func (d *DB) TaskStatuses() (map[int]pm.TaskStatus, error)

TaskStatuses returns every task's status by ID, without loading the tasks. The janitor uses it to tell which verdict sidecars a stale-task recovery could still need (Task 20365).

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

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

TouchSCIMCredential records a use, moving last_used_at forward only.

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

func (d *DB) UpdateAppTokenSlot(row AppTokenSlotRow) (bool, error)

UpdateAppTokenSlot rewrites a slot's mutable fields — guarded, session, whether the token was exported, the held token's expiry, and its scope when row.Scope is set (a restore narrows it to its grant) — provided row.Holder holds it, and reports whether it did.

func (*DB) UpdateDeviceAuthorization added in v0.0.5

func (d *DB) UpdateDeviceAuthorization(deviceHash string, change func(*DeviceAuthorizationRow) (bool, error)) (DeviceAuthorizationRow, error)

UpdateDeviceAuthorization reads the row for deviceHash, hands it to change, and writes it back when change reports that it changed it — all under the database's write lock, so two hub processes deciding the same code (an approval racing a denial, two polls racing the exchange) are serialised and the second one sees the first one's result. It returns the row as stored afterwards.

change must not touch the database: it runs inside the transaction.

func (*DB) UpdateFirewalls added in v0.0.5

func (d *DB) UpdateFirewalls(fn func(*FirewallTx) error) error

UpdateFirewalls runs fn inside one write transaction, committing when it returns nil and rolling back otherwise.

func (*DB) UpdateHubOwnerMeta added in v0.0.5

func (d *DB) UpdateHubOwnerMeta(kind, key, instanceID, meta string, at time.Time) (bool, error)

UpdateHubOwnerMeta rewrites the meta of a row instanceID still owns. false means it no longer does.

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

func (d *DB) UpdateVirtualExecutor(id, name string, spec executor.VirtualSpec, by string) error

UpdateVirtualExecutor replaces a virtual executor's name and configuration. The parent is fixed at creation: moving a sub-executor to another machine would carry its bindings and access list to hardware nobody approved them for.

It does not check the configuration against the device's firewall; the dashboard's save goes through UpdateFirewalls for that (Task 20363).

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

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

VirtualExecutor reads one virtual executor.

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.

It waits WALCheckpointBusyTimeout rather than the handle's 5 s (Task 20392): a TRUNCATE checkpoint holds the write lock while it waits, so a backup taken beside a live hub would otherwise hold every one of that hub's writers up for as long as a reader kept it waiting. See wal.go.

func (*DB) WriteSCIMGroup added in v0.0.5

func (d *DB) WriteSCIMGroup(w SCIMGroupWrite) (SCIMGroupApplied, error)

WriteSCIMGroup applies one change to a provisioned group in one transaction: its uniqueness check on the display name, its membership, its audit rows and the directory version bump.

func (*DB) WriteSCIMUser added in v0.0.5

func (d *DB) WriteSCIMUser(w SCIMUserWrite) (SCIMUserApplied, error)

WriteSCIMUser applies one change to a provisioned user, in one transaction with its uniqueness checks, its suspension or lift, its audit rows, and the directory version bump other hub members' caches read.

type DeviceAuthorizationRow added in v0.0.5

type DeviceAuthorizationRow struct {
	DeviceHash     string
	UserCodeHash   string
	CredentialHash string
	Status         string

	ClientIP        string
	ClientUserAgent string
	ClientHostname  string

	CreatedAt       time.Time
	ExpiresAt       time.Time
	IntervalSeconds int
	LastPolledAt    time.Time

	Approver        string
	ApproverSession string
	Subject         string
	DecidedAt       time.Time
	Reason          string
}

DeviceAuthorizationRow is one row of device_authorizations.

type DispatchIntentRow added in v0.0.5

type DispatchIntentRow struct {
	RunID       string
	InstanceID  string
	ExecutorID  string
	Driver      string
	ProjectPath string
	HandleID    string
	LeaseIDs    []string
	EgressIDs   []string
	State       string
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Detail      string
}

DispatchIntentRow is one workload a hub process is starting.

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"

	// EventTaskReview records what a project's review gate decided about a
	// task's changes before they were published, and which of the agent's
	// held pushes went out (Task 20357).
	//
	// Its own type because it explains the case a status alone cannot: a task
	// whose agent said TASK_DONE and which nonetheless failed, because a
	// different model read its diff and would not let it leave the machine.
	EventTaskReview EventType = "task_review"

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

	// EventTaskInterrupted records a task whose execution was cut short
	// because its run stopped — the Stop button, a SIGINT or SIGTERM, a budget
	// stop from the hub, a --timeout expiring — rather than because of
	// anything the task did (Task 20348).
	//
	// Its own type for the same reason as task_aborted: the task goes back to
	// pending, and a row that read as task_failed would be a false statement
	// about work that was simply not allowed to finish. Filing interruptions as
	// failures is what left stopped tasks terminal, and the tasks depending on
	// them skipped, when the project was resumed.
	EventTaskInterrupted EventType = "task_interrupted"

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

	// EventRunReexecuted records a run replacing its own image with a newer
	// build at a task boundary (Task 20389), written by the new image: which
	// build it left, which it runs now, and why. The process, its pid and
	// its live log carry on, so without this row nothing would show where
	// one build's tasks end and the next one's begin.
	EventRunReexecuted EventType = "run_reexecuted"
	// EventRunAdoption records an adoption that did not happen, or not yet:
	// a refused build and the reason, a failed exec, a request waiting for
	// the task in flight. The run carries on on the build it has.
	EventRunAdoption EventType = "run_adoption"

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

	// EventFirewall records which stored firewall levels shaped a run — a
	// device's rule set, a project's — and what came of them, or why they
	// refused it (Task 20363).
	//
	// On the project's journal for the reason the ceiling is: a fetch that
	// times out inside a sandbox reads as a broken network, and the rules that
	// dropped it live on the hub where the developer cannot see them.
	EventFirewall EventType = "firewall"

	// EventCredentialRefresh records that a run's GitHub App token could not
	// be kept alive past GitHub's hour on the executor running it, or that its
	// renewal was refused (Task 20375).
	//
	// On the project's journal for the reason the firewall row is: the
	// symptom — git failing to authenticate an hour into a long run — lands on
	// the developer, and the cause — an agent too old to receive the new
	// token, a grant revoked, an installation suspended — is on the hub where
	// they cannot see it.
	EventCredentialRefresh EventType = "credential_refresh"

	// EventCredentialRefused records that a run went without a credential the
	// project was granted, because the secret behind the grant no longer
	// exists (Task 20400) — most often a colleague's personal credential,
	// destroyed when they were offboarded.
	//
	// On the project's journal for the reason the refresh row is: the symptom
	// — git refusing to authenticate, a cluster answering 401 — lands on the
	// people running the project, and the cause is a deletion on the hub they
	// may not even be able to see, since a grant over a personal secret is
	// visible to its owner and to admins alone. Written on the first run after
	// the loss on each hub process, not on every run.
	EventCredentialRefused EventType = "credential_refused"

	// EventEgress records what became of a run's access to the hub's egress
	// proxy (Task 20378): the session it was issued and the route to it, why
	// a project holding an egress grant got none, and when a session ended —
	// the run over, stopped, or its grant revoked with tunnels still open.
	//
	// On the project's journal for the reason the firewall row is: a sandbox
	// whose every request through the proxy fails looks like a broken
	// network, and the reasons — no proxy on the hub, a route the executor
	// cannot take, a revocation — are on the hub, out of the developer's
	// sight. It never carries the session's credential: the proxy URL is not
	// written here, only its address.
	EventEgress EventType = "egress"

	// EventFailover records what executor failover did to the project's run
	// and tasks (Task 20391): a lost node's tasks returned to pending for a
	// retry, a task marked as a suspected node killer, a run that used up
	// executors.failover.max_attempts and was not re-dispatched, and a
	// quarantine an explicit reset released.
	//
	// On the project's journal because the cause — nodes going unreachable,
	// the cap — is the hub's, and the effect, a task that failed or will not
	// run, is what the developer sees.
	EventFailover EventType = "failover"

	// EventDiskLimit records a run stopped because its workspace grew past
	// its disk limit — .cloop/sandbox.yaml resources.disk or an operator's
	// disk ceiling — or a start refused because the workspace was already
	// over it (Task 20405). The message names both sizes and how to raise the
	// limit; the details carry them as numbers.
	//
	// On the project's journal because the cause is a number the developer
	// may never have seen — a ceiling set on the hub — and the effect, a run
	// that stopped mid-task, is what they see.
	EventDiskLimit EventType = "disk_limit"
)

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
	// RunningTasks are the tasks the workload last announced it was working
	// on (Task 20391), kept so a failover can say which tasks a lost node
	// was running even when the hub's own copy of the project cannot: a run
	// on a device works on a copy. Written by SetExecutorSessionRunningTasks
	// while the session runs; empty when nothing was observed.
	RunningTasks []int
}

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

type FirewallRecord added in v0.0.5

type FirewallRecord struct {
	// Subject is the executor ID or the project path the rules are keyed by.
	Subject     string                 `json:"subject"`
	Rules       executor.FirewallRules `json:"rules"`
	Fingerprint string                 `json:"fingerprint"`
	SetAt       time.Time              `json:"set_at,omitempty"`
	SetBy       string                 `json:"set_by,omitempty"`
}

FirewallRecord is one stored rule set plus its provenance.

type FirewallTx added in v0.0.5

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

FirewallTx is one serialized read-modify-write over everything the firewall's levels are stored in: the two rule tables, virtual executor definitions and project bindings.

It exists for the saves whose correctness depends on what they read. A project's rule set is checked against its device's, and a device tightening rewrites the rule sets saved underneath it; done as separate statements, an admin narrowing a device and a maintainer saving a project at the same moment could each check against the other's old value and both commit, leaving a project outside its device until the next dispatch refused it. Inside one IMMEDIATE transaction the two serialize — across hub processes too, since the write lock is the database's.

func (*FirewallTx) ClearExecutorFirewall added in v0.0.5

func (t *FirewallTx) ClearExecutorFirewall(executorID string) error

ClearExecutorFirewall removes a device's rule set inside the transaction.

func (*FirewallTx) ClearProjectFirewall added in v0.0.5

func (t *FirewallTx) ClearProjectFirewall(projectPath string) error

ClearProjectFirewall removes a project's rule set inside the transaction.

func (*FirewallTx) CreateVirtualExecutor added in v0.0.5

func (t *FirewallTx) CreateVirtualExecutor(v VirtualExecutor) error

CreateVirtualExecutor records a new virtual executor inside the transaction, so a save checked against its device's rule set commits only if that rule set is still the one it was checked against.

func (*FirewallTx) ExecutorFirewall added in v0.0.5

func (t *FirewallTx) ExecutorFirewall(executorID string) (FirewallRecord, bool, error)

ExecutorFirewall reads a device's rule set inside the transaction.

func (*FirewallTx) ListProjectFirewalls added in v0.0.5

func (t *FirewallTx) ListProjectFirewalls() ([]FirewallRecord, error)

ListProjectFirewalls reads every project rule set inside the transaction.

func (*FirewallTx) ProjectExecutor added in v0.0.5

func (t *FirewallTx) ProjectExecutor(projectPath string) (string, bool, error)

ProjectExecutor reads a project's executor binding inside the transaction.

func (*FirewallTx) ProjectFirewall added in v0.0.5

func (t *FirewallTx) ProjectFirewall(projectPath string) (FirewallRecord, bool, error)

ProjectFirewall reads a project's rule set inside the transaction.

func (*FirewallTx) ProjectsBoundTo added in v0.0.5

func (t *FirewallTx) ProjectsBoundTo(ids ...string) ([]string, error)

ProjectsBoundTo returns the paths of the projects pinned to any of ids, in path order.

func (*FirewallTx) SetExecutorFirewall added in v0.0.5

func (t *FirewallTx) SetExecutorFirewall(executorID string, r executor.FirewallRules, setBy string) error

SetExecutorFirewall writes a device's rule set inside the transaction.

func (*FirewallTx) SetProjectFirewall added in v0.0.5

func (t *FirewallTx) SetProjectFirewall(projectPath string, r executor.FirewallRules, setBy string) error

SetProjectFirewall writes a project's rule set inside the transaction.

func (*FirewallTx) SetVirtualExecutorSpec added in v0.0.5

func (t *FirewallTx) SetVirtualExecutorSpec(id string, spec executor.VirtualSpec, by string) error

SetVirtualExecutorSpec replaces one virtual executor's configuration inside the transaction, keeping its name.

func (*FirewallTx) UpdateVirtualExecutor added in v0.0.5

func (t *FirewallTx) UpdateVirtualExecutor(id, name string, spec executor.VirtualSpec, by string) error

UpdateVirtualExecutor replaces a virtual executor's name and configuration inside the transaction.

func (*FirewallTx) VirtualExecutor added in v0.0.5

func (t *FirewallTx) VirtualExecutor(id string) (VirtualExecutor, bool, error)

VirtualExecutor reads one virtual executor inside the transaction.

func (*FirewallTx) VirtualExecutorsOf added in v0.0.5

func (t *FirewallTx) VirtualExecutorsOf(parentID string) ([]VirtualExecutor, error)

VirtualExecutorsOf reads every virtual executor of one device inside the transaction.

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

type HistoryCursor struct {
	Step  int64
	Event int64
}

HistoryCursor is a position in the journal's write order: the newest step number and event id a reader has seen. Step is -1 when it has seen no step (step numbers start at 0) and Event is 0 when it has seen no event.

type HistoryPage added in v0.0.5

type HistoryPage struct {
	// Rows in feed order, newest first.
	Rows []HistoryRow
	// Top is the newest step and event written when the page was read — what
	// a later After read starts from. On a Before page it is not set.
	Top HistoryCursor
	// Bottom is the place of the page's last row, where the next page down
	// starts; nil when the page is empty and was not itself a Before read.
	Bottom *HistoryPlace
	// More reports rows below Bottom.
	More bool
	// Gap is set on an After read that found more than Limit new rows: the
	// page is the newest page instead, and those rows were not all returned.
	Gap bool
}

HistoryPage is what History returns.

type HistoryPlace added in v0.0.5

type HistoryPlace struct {
	Day   float64
	Event bool
	Key   int64
}

HistoryPlace is a row's place in the feed's time order: the julian day it happened on (0 for a time that cannot be read), whether it is an event, and its step number or event id.

type HistoryQuery added in v0.0.5

type HistoryQuery struct {
	// Before pages down: the Limit rows listed after this place.
	Before *HistoryPlace
	// After returns every row written since this cursor, in feed order, when
	// there are at most Limit of them; otherwise the newest page with
	// HistoryPage.Gap set.
	After *HistoryCursor
	// Offset skips that many rows from the top before the page starts, for
	// callers that page by position. Only without Before and After; it reads
	// one index entry per skipped row, which positions avoid.
	Offset int
	// Limit is the most rows returned; it must be positive.
	Limit int
	// MaxValue, when positive, is the longest step output or event details
	// returned whole; longer ones are cut (see HistoryRow.Cut).
	MaxValue int
}

HistoryQuery selects one page of the feed.

type HistoryRow added in v0.0.5

type HistoryRow struct {
	Step  *StepRow
	Event *EventRow
	// Place is where the row lists in the feed.
	Place HistoryPlace
	// Cut is the full length in bytes of the step's Output or the event's
	// Details when the value here was shortened to HistoryQuery.MaxValue, and
	// 0 when it is whole. A shortened Output is its first MaxValue bytes; a
	// shortened Details is empty, since half a JSON document is not one.
	Cut int
}

HistoryRow is one row of the feed: a step or an event, never both.

type HubEventRow added in v0.0.5

type HubEventRow struct {
	Seq       int64
	Origin    string
	Target    string
	Topic     string
	Key       string
	Payload   string
	CreatedAt time.Time
}

HubEventRow is one event on the cluster bus.

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

type HubMemberRow struct {
	InstanceID string
	Hostname   string
	PID        int
	// BootID disambiguates a pid across reboots, exactly as on HubLeaseRow.
	BootID       string
	Address      string
	AdvertiseURL string
	Version      string
	// Meta is a JSON object of further endpoints; "{}" when there are none.
	Meta        string
	StartedAt   time.Time
	HeartbeatAt time.Time
	// LeftAt is a graceful leave; the zero time while the member serves.
	LeftAt time.Time
}

HubMemberRow is one hub process serving this control plane.

type HubOwnerRow added in v0.0.5

type HubOwnerRow struct {
	Kind       string
	Key        string
	InstanceID string
	Meta       string
	ClaimedAt  time.Time
	UpdatedAt  time.Time
}

HubOwnerRow records which member holds (Kind, Key).

type HubTLSRow added in v0.0.5

type HubTLSRow struct {
	// ID is "ca" or "leaf:<monitor>".
	ID string
	// Kind is "ca" or "leaf".
	Kind string
	// Serial is the certificate's serial in lowercase hex.
	Serial string
	// Issuer is, for a leaf, the serial of the CA that signed it.
	Issuer string
	// CertPEM is the certificate. Public.
	CertPEM string
	// KeyID, WrappedDEK and KeySealed are the private key's envelope.
	KeyID      string
	WrappedDEK []byte
	KeySealed  []byte
	// SANs are a leaf's names and addresses.
	SANs []string
	// NotBefore and NotAfter bound the certificate's validity.
	NotBefore time.Time
	NotAfter  time.Time
	// IssuedAt and IssuedBy say when and by which hub process.
	IssuedAt time.Time
	IssuedBy string
}

HubTLSRow is one row of hub_tls.

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

type LeaseRevocationRow struct {
	LeaseID string
	// GrantID is the grant the revocation names, "" for the whole lease.
	GrantID    string
	Action     string
	Reason     string
	Actor      string
	ExecutorID string
	ProjectID  string
	// Holder is the hub member that held the lease when it was revoked.
	Holder    string
	RevokedAt time.Time
	// EnforcedAt is when the material was found taken back from every
	// holder, zero while the revocation is pending.
	EnforcedAt time.Time
	EnforcedBy string
	Detail     string
}

LeaseRevocationRow is one revocation of a lease, or of one grant of it.

func (LeaseRevocationRow) Pending added in v0.0.5

func (r LeaseRevocationRow) Pending() bool

Pending reports whether the revocation has not been enforced yet.

type LegalHoldRow added in v0.0.5

type LegalHoldRow struct {
	Identity string
	Reason   string
	PlacedAt time.Time
	PlacedBy string
}

LegalHoldRow is one identity under a legal hold.

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

type MemberAudit func(prev, next *ProjectMemberRow) ([]*AuditEvent, error)

MemberAudit builds the audit events committed together with a membership change. prev is the row as it stood before the change and next the row after it; either is nil when there was none, so a grant has prev == nil and a removal next == nil. Returning an error aborts the change: an authorization change with no record is treated as one that did not happen.

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, and
	// columns appended to tables no shipped build rewrites. 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"

	// CompatAdditiveColumns is CompatAdditive for a migration that also
	// appends a column to one of rewrittenTables. A build from Task 20388 on
	// writes those tables row by row, in the columns it knows, so the new
	// column survives it and it tolerates the migration. A build before that
	// replaces their rows on every write and would erase the column; it has
	// never heard of this verdict, reads it as unknown, and refuses the
	// database instead.
	CompatAdditiveColumns MigrationCompat = "additive-columns"

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

Every build from Task 20254 to Task 20387 answered this with c == CompatAdditive, which is what makes CompatAdditiveColumns a refusal for them.

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

	// Members are the project memberships actually dropped (Task 20366), as
	// rows rather than ids: the report names the projects the person was
	// removed from, which an id cannot be read back to.
	Members []ProjectMemberRow

	// 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

	// MemberKeys are the identity keys whose project memberships are dropped
	// (Task 20366): every spelling of the person, because a grant recorded
	// under "sub:<subject>" survives a sweep by email, and a surviving
	// membership is live access to a colleague's project. Severed in this
	// transaction for the reason the transaction exists — "their sessions are
	// gone but they are still on three projects" is exactly the
	// partially-offboarded state nobody would discover.
	MemberKeys []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 ProjectAuditInput added in v0.0.5

type ProjectAuditInput struct {
	Action auditaction.Action
	// Path is the project's directory, the entity the row is filed under.
	Path   string
	Actor  string
	Detail map[string]any
}

ProjectAuditInput carries one project.create or project.delete row (Task 20418). Detail is the action's payload beside the path, keyed as the registry lists it (pkg/auditaction).

type ProjectClaim added in v0.0.5

type ProjectClaim struct {
	Path      string
	State     string
	Owner     string
	HubMade   bool
	Root      string
	ClaimedBy string
	ClaimedAt time.Time
	UpdatedAt time.Time
}

ProjectClaim is one row of project_claims.

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

type ProjectFirewallAuditInput struct {
	// Action is "set", "clear" or "constrain".
	Action      string
	ProjectPath string
	// ExecutorID is the executor the project resolved to when the rules were
	// written, so a reviewer can see which device's rules they were checked
	// against.
	ExecutorID string
	Actor      string
	Detail     map[string]any
}

ProjectFirewallAuditInput carries one change to a project's firewall rule set (Task 20363).

type ProjectMemberRow added in v0.0.5

type ProjectMemberRow struct {
	ID          string
	ProjectPath string
	IdentityKey string
	Role        string
	Reason      string
	GrantedAt   time.Time
	GrantedBy   string
}

ProjectMemberRow is one row of project_members.

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

type ProxySessionFilter struct {
	Kind string
	// LeaseIDs keeps the sessions fed by any of these leases.
	LeaseIDs []string
	// SessionIDs keeps the sessions with any of these ids.
	SessionIDs []string
	RunID      string
	Holder     string
	// OpenOnly drops closed sessions.
	OpenOnly bool
}

ProxySessionFilter narrows ListProxySessions. Zero fields do not filter.

type ProxySessionRow added in v0.0.5

type ProxySessionRow struct {
	Kind      string
	SessionID string
	// TokenSHA256 is the hex SHA-256 of the bearer token the sandbox presents.
	TokenSHA256 string
	LeaseID     string
	GrantID     string
	// Holder is the hub process serving the session.
	Holder     string
	RunID      string
	ProjectID  string
	ExecutorID string
	TaskID     string
	Actor      string
	// Scope is the registry's JSON account of what the session enforces.
	Scope string
	// Counters is the registry's JSON checkpoint of what the session has
	// moved, for a session whose quota outlives its process (egress).
	Counters    string
	IssuedAt    time.Time
	ExpiresAt   time.Time
	UpdatedAt   time.Time
	ClosedAt    time.Time
	CloseReason string
}

ProxySessionRow is one minted session. It says who serves the session, what it enforces and how its token hashes — never a token or the credential the session presents upstream.

func (ProxySessionRow) Open added in v0.0.5

func (r ProxySessionRow) Open() bool

Open reports whether the session has not been closed.

type QuarantinedTask added in v0.0.5

type QuarantinedTask struct {
	TaskID int
	Title  string
	Status string
	Mark   pm.TaskQuarantine
}

QuarantinedTask is one marked task as PeekTaskQuarantines reads it.

func PeekTaskQuarantines added in v0.0.5

func PeekTaskQuarantines(dbPath string) ([]QuarantinedTask, error)

PeekTaskQuarantines lists the marked tasks in the project database at dbPath without migrating or writing it: a read-only connection, for code that surveys many projects from outside them, such as `cloop executor list --inventory`. A database that predates the table has none.

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

type RetiredStaticTokenRow struct {
	Fingerprint string
	RetiredAt   time.Time
	RetiredBy   string
	Reason      string
}

RetiredStaticTokenRow is one row of retired_static_tokens.

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

type RunReexecInput struct {
	ProjectPath string
	RunID       string
	PID         int
	From, To    runbuild.Build
	Reason      string
	Reexecs     int
}

RunReexecInput is one run that replaced its image with a newer build at a task boundary (Task 20389).

type SCIMConflict added in v0.0.5

type SCIMConflict struct {
	// Column is the uniqueness rule broken: user_name_key, email,
	// external_id, external_id_key or display_name_key.
	Column string
	// Label is the caller's name for the rule, for the message.
	Label string
	// Value is the contested value, and OtherID the resource holding it.
	Value   string
	OtherID string
}

SCIMConflict reports that a write would give a provisioned resource a value another one already holds.

func (*SCIMConflict) Error added in v0.0.5

func (c *SCIMConflict) Error() string

type SCIMCredentialRow added in v0.0.5

type SCIMCredentialRow struct {
	ID                string
	Name              string
	SecretHash        string
	PreviousHash      string
	PreviousExpiresAt time.Time
	CreatedAt         time.Time
	CreatedBy         string
	RotatedAt         time.Time
	RotatedBy         string
	RevokedAt         time.Time
	RevokedBy         string
	LastUsedAt        time.Time
}

SCIMCredentialRow is one SCIM bearer credential. Only HMACs of its secret are stored.

func (SCIMCredentialRow) Revoked added in v0.0.5

func (c SCIMCredentialRow) Revoked() bool

Revoked reports whether the credential was revoked.

type SCIMDirectory added in v0.0.5

type SCIMDirectory struct {
	Version int64
	Users   []SCIMDirectoryUser
	// Groups maps a group id to its display name and external id.
	Groups map[string]SCIMDirectoryGroup
}

SCIMDirectory is what the authorization path needs of the directory: every user's identifiers and state, and the groups each belongs to.

type SCIMDirectoryGroup added in v0.0.5

type SCIMDirectoryGroup struct {
	DisplayName string
	ExternalID  string
}

SCIMDirectoryGroup is one group's names.

type SCIMDirectoryUser added in v0.0.5

type SCIMDirectoryUser struct {
	ID         string
	UserName   string
	ExternalID string
	Email      string
	Active     bool
	GroupIDs   []string
}

SCIMDirectoryUser is one user, without its resource.

type SCIMGroupApplied added in v0.0.5

type SCIMGroupApplied struct {
	Row SCIMGroupRow
	// Added and Removed are the membership changes; Unknown the member ids
	// asked for that name no provisioned user, which were not stored.
	Added   []string
	Removed []string
	Unknown []string
	At      time.Time
}

SCIMGroupApplied reports what a group write changed.

type SCIMGroupRow added in v0.0.5

type SCIMGroupRow struct {
	ID           string
	DisplayName  string
	ExternalID   string
	ResourceJSON string
	Version      int64
	CreatedAt    time.Time
	CreatedBy    string
	UpdatedAt    time.Time
	UpdatedBy    string
	// Members are the member user ids, sorted.
	Members []string
}

SCIMGroupRow is one provisioned group.

type SCIMGroupWrite added in v0.0.5

type SCIMGroupWrite struct {
	Row           SCIMGroupRow
	Create        bool
	ExpectVersion int64
	// SetMembers replaces the membership with Row.Members. Without it the
	// membership is left as it is.
	SetMembers bool
	At         time.Time
	Audit      func(SCIMGroupApplied) ([]*AuditEvent, error)
}

SCIMGroupWrite is one change to a provisioned group.

type SCIMSuspendWrite added in v0.0.5

type SCIMSuspendWrite struct {
	// Denies are written when no binding with their id exists; a binding
	// already in force — an administrator's — is left as it is.
	Denies []RoleBindingRow
	// SessionIDs are hashed session ids to delete.
	SessionIDs []string
	// TokenIDs and GlassesIDs are api_tokens rows to revoke.
	TokenIDs   []string
	GlassesIDs []string
	// GrantIDs are grants to suspend, when they stand and are not suspended.
	GrantIDs []string
	// Record is what the user's suspension record will say about the
	// identity; the ids written are filled in here.
	Record SCIMSuspensionRecord
}

SCIMSuspendWrite is what deactivating a user severs in the hub's own database: resolved by pkg/provision, written here in one transaction.

type SCIMSuspensionRecord added in v0.0.5

type SCIMSuspensionRecord struct {
	At time.Time `json:"at"`
	By string    `json:"by"`
	// Reason is the reason written on the deny bindings. The lift removes a
	// binding only while it still carries it: one an administrator wrote, or
	// re-wrote, for the same identity is theirs and stays.
	Reason string `json:"reason"`
	// Denies are the deny bindings the suspension created — not ones it
	// found already in force.
	Denies []string `json:"denies,omitempty"`
	// Grants are the grants it suspended.
	Grants []string `json:"grants,omitempty"`
	// Identity, Subjects and Emails are the hub identity it resolved, for the
	// offboarding that may follow.
	Identity string   `json:"identity,omitempty"`
	Subjects []string `json:"subjects,omitempty"`
	Emails   []string `json:"emails,omitempty"`
}

SCIMSuspensionRecord is what a suspension wrote and its lift may take back.

type SCIMUnique added in v0.0.5

type SCIMUnique struct {
	Column string
	Value  string
	Label  string
}

SCIMUnique is one uniqueness rule a user write must satisfy: no other user may hold Value in Column.

type SCIMUserApplied added in v0.0.5

type SCIMUserApplied struct {
	Row SCIMUserRow
	// Suspended: the denies written, sessions deleted, tokens, glasses
	// links and grants severed.
	DeniesWritten   []string
	DeniesInForce   []string
	SessionsDeleted []string
	TokensRevoked   []string
	GlassesRevoked  []string
	GrantsSuspended []string
	// Lifted: the denies removed and grants restored, and the ones left
	// because they were no longer the suspension's.
	DeniesLifted  []string
	DeniesKept    []string
	GrantsResumed []string
	At            time.Time
}

SCIMUserApplied reports what a user write changed.

type SCIMUserRow added in v0.0.5

type SCIMUserRow struct {
	ID          string
	UserName    string
	ExternalID  string
	DisplayName string
	// Email is the account's address, lowercased (scim.UserFields.Email).
	Email  string
	Active bool
	// ResourceJSON is the resource as provisioned, less id, meta, schemas and
	// any password.
	ResourceJSON string
	Version      int64
	CreatedAt    time.Time
	CreatedBy    string
	UpdatedAt    time.Time
	UpdatedBy    string
	// DeactivatedAt is when the user last went inactive; zero while active.
	DeactivatedAt time.Time
	// Suspension is what the suspension in force wrote (SCIMSuspensionRecord
	// as JSON), or "" when none is.
	Suspension string
	// OffboardedAt is when the full offboarding ran for this user.
	OffboardedAt time.Time
}

SCIMUserRow is one provisioned user.

func (SCIMUserRow) SuspensionRecord added in v0.0.5

func (r SCIMUserRow) SuspensionRecord() (SCIMSuspensionRecord, bool)

SuspensionRecord decodes the row's suspension, if one is in force.

type SCIMUserWrite added in v0.0.5

type SCIMUserWrite struct {
	// Row is the user as it is to be stored. Row.Version is ignored: the
	// stored version becomes ExpectVersion+1, or 1 on create.
	Row SCIMUserRow
	// Create inserts; otherwise the row is updated, and only while its stored
	// version is ExpectVersion.
	Create        bool
	ExpectVersion int64
	// Unique are the rules no other user may break.
	Unique []SCIMUnique
	// Suspend, when set, is written with the change; Lift likewise.
	Suspend *SCIMSuspendWrite
	Lift    bool
	// At stamps the change. Zero means now.
	At time.Time
	// Audit builds the audit rows from what was applied, chained into the
	// same commit. An error aborts the write.
	Audit func(SCIMUserApplied) ([]*AuditEvent, error)
}

SCIMUserWrite is one change to a provisioned user, with everything that follows from it.

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

type SchemaState struct {
	// Version is the highest migration schema_migrations records; zero when
	// the table is missing or empty. Open migrates it to LatestSchemaVersion.
	Version int
	// PreFramework reports a database Open would adopt as its baseline instead
	// of building from scratch: cloop's metadata table, and no migration
	// recorded — the rule detectBaseline applies.
	PreFramework bool
	// HoldsProject reports a project in the database: a metadata row, the
	// test HasProjectState applies. Opening a database creates the file and
	// every table in it, so the file existing says nothing.
	HoldsProject bool
}

SchemaState is what a database file is, in statedb's terms.

func InspectSchema added in v0.0.5

func InspectSchema(path string) (SchemaState, error)

InspectSchema reports what the database at path is, through a read-only handle: asking neither migrates it nor, when it does not exist, creates it.

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

func (e *SchemaTooNewError) Blockers() string

Blockers names the migrations ahead of this binary that it cannot tolerate, for a caller that reports the refusal in its own words.

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

type SealedFacts struct {
	// KEKs is the sealing-key registry, retired keys included.
	KEKs []KEKRow
	// Rows counts sealed rows by set ("secrets", "sessions", "hub_tls"),
	// then by the key id each is sealed under.
	Rows map[string]map[string]int
	// LegacySalt is the pre-envelope cipher's salt, hex; "" when the
	// database has none.
	LegacySalt string
	// LegacySample is one ciphertext sealed the pre-envelope way, so a
	// passphrase can be checked against the legacy key, which has no check
	// value; nil when no row is sealed that way.
	LegacySample []byte
}

SealedFacts is what a database's sealed material depends on.

func ReadSealedFacts added in v0.0.5

func ReadSealedFacts(conn *sql.DB, legacySaltKey string) (SealedFacts, error)

ReadSealedFacts reads the facts through conn. legacySaltKey names the legacy salt's row in broker_meta (secretbroker.LegacySaltMetaKey).

func (SealedFacts) ByKey added in v0.0.5

func (f SealedFacts) ByKey() map[string]int

ByKey totals Rows per key id, across sets.

func (SealedFacts) Sets added in v0.0.5

func (f SealedFacts) Sets() []string

Sets lists the sets with sealed rows, sorted.

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
	// InstanceID is the hub process that materialised it, on a control plane
	// several of them serve (Task 20354); ” for one written before that. A
	// sweep must not wipe a directory a live peer's run is reading from.
	InstanceID string
}

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

type SecretLeaseRow added in v0.0.5

type SecretLeaseRow struct {
	LeaseID string
	// Holder is the hub process keeping the lease alive.
	Holder     string
	ExecutorID string
	ProjectID  string
	RunID      string
	// Record is the broker's JSON account of the lease: requester, actor,
	// kinds, grant ids.
	Record    string
	IssuedAt  time.Time
	ExpiresAt time.Time
	UpdatedAt time.Time
}

SecretLeaseRow is one live lease. It says who holds the lease and who it was issued to, never what it carries.

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

type SessionCutoffs struct {
	Absolute time.Time
	Idle     time.Time
	CLIIdle  time.Time
}

SessionCutoffs are the absolute times a session is compared against to decide whether it has ended (Task 20412). Absolute applies to expires_at for every kind; the idle cutoffs to last_seen, one per kind, because a CLI session idles out on its own clock. The caller computes them from its policy, so this layer stays free of any opinion about how long a session should live.

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

	// Kind is what the session authenticates — SessionKindBrowser by cookie,
	// SessionKindCLI by bearer — and Client the host name a terminal reported
	// (Task 20412, migrations/0066_device_login.sql). An empty Kind is written
	// as a browser session, which is what every row before the column was.
	Kind   string
	Client string

	// Username and OID are the preferred_username and oid claims the session
	// signed in with (Task 20415, migrations/0067_scim_provisioning.sql): what
	// a SCIM directory matching on either is compared against.
	Username string
	OID      string
}

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
	// ReviewGate is the project's review-gate configuration (Task 20357),
	// stored as JSON under the review_gate meta key. Nil when never set.
	ReviewGate *pm.ReviewGate
	// CommitPolicy is the project's "done means committed" setting (Task
	// 20370), stored as JSON under the commit_policy meta key. Nil when never
	// set.
	CommitPolicy *pm.CommitPolicy
	// FollowBuilds, RunOwner and AdoptRequest are the project's side of a
	// run adopting newer builds (Task 20389): the "follow new builds" option
	// (follow_builds), the record the run keeps of its own process and build
	// (run_owner) and a pending one-shot request to adopt (run_adopt_request).
	// Each is written only through its own setter — SaveFollowBuilds,
	// SaveRunOwner, SaveAdoptRequest — and never by SaveState, so neither a
	// run's save nor a dashboard's can put back a stale copy.
	FollowBuilds bool
	RunOwner     *runbuild.Owner
	AdoptRequest *runbuild.Request
}

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

type StaticTokenUseReport added in v0.0.5

type StaticTokenUseReport struct {
	Fingerprint string
	// At is when the report was made.
	At time.Time
	// Held says the reporting process holds this token — it is its configured
	// static token — and stamps held_at, held_by and sso.
	Held   bool
	HeldBy string
	SSO    bool
	// UsedAt and UsedIP are the latest request admitted with the token since
	// the previous flush; zero when none was.
	UsedAt time.Time
	UsedIP string
	// Refused counts the requests refused because the token is retired since
	// the previous flush, the latest at RefusedAt from RefusedIP.
	Refused   int64
	RefusedAt time.Time
	RefusedIP string
}

StaticTokenUseReport is what one hub process flushes about one fingerprint: what it saw since its previous flush.

type StaticTokenUseRow added in v0.0.5

type StaticTokenUseRow struct {
	Fingerprint   string
	FirstSeenAt   time.Time
	HeldAt        time.Time
	HeldBy        string
	SSO           bool
	LastUsedAt    time.Time
	LastUsedIP    string
	RefusedCount  int64
	LastRefusedAt time.Time
	LastRefusedIP string
}

StaticTokenUseRow is one row of static_token_use.

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

type VirtualExecutor struct {
	ID       string               `json:"id"`
	ParentID string               `json:"parent_id"`
	Name     string               `json:"name"`
	Spec     executor.VirtualSpec `json:"spec"`
	// CreatedAt/By and UpdatedAt/By are provenance for the panel; the audit
	// trail carries the full history.
	CreatedAt time.Time `json:"created_at"`
	CreatedBy string    `json:"created_by,omitempty"`
	UpdatedAt time.Time `json:"updated_at"`
	UpdatedBy string    `json:"updated_by,omitempty"`
}

VirtualExecutor is one stored virtual executor.

type WALCheckpoint added in v0.0.5

type WALCheckpoint struct {
	// Busy reports that the checkpoint could not finish within its busy
	// timeout: another connection held the write lock, or a reader's snapshot
	// still needed frames in the log. Not an error. What could be copied back
	// was, and the log keeps its size until an attempt finds it free.
	Busy bool `json:"busy"`
	// LogFrames and CheckpointedFrames are SQLite's counts after the attempt:
	// the frames in the log, and how many of them are in the database file.
	// Both are zero once the log is truncated; on a busy attempt they say how
	// far it got. -1 means the database is not in WAL mode at all.
	LogFrames          int `json:"log_frames"`
	CheckpointedFrames int `json:"checkpointed_frames"`
	// BytesBefore and BytesAfter are the log file's size either side. A writer
	// may start the log again the moment the checkpoint finishes, so
	// BytesAfter can be a few pages rather than zero.
	BytesBefore int64 `json:"bytes_before"`
	BytesAfter  int64 `json:"bytes_after"`
	// Duration is how long the TRUNCATE checkpoint ran: an upper bound on how
	// long it held the write lock. The PASSIVE one before it, which takes no
	// such lock, is not counted.
	Duration time.Duration `json:"duration"`
}

WALCheckpoint is the outcome of one TRUNCATE checkpoint.

func CheckpointWAL added in v0.0.5

func CheckpointWAL(dbPath string, busy time.Duration) (WALCheckpoint, error)

CheckpointWAL runs PRAGMA wal_checkpoint(TRUNCATE) on the database at dbPath, through a connection of its own whose busy timeout is busy, or WALCheckpointBusyTimeout when busy is zero or less.

It never creates a database: one that does not exist is an error wrapping os.ErrNotExist. Nothing about the schema is read, so it migrates nothing and works on a database of any version.

A busy checkpoint is a result, not an error; see WALCheckpoint.Busy.

func (WALCheckpoint) BytesFreed added in v0.0.5

func (c WALCheckpoint) BytesFreed() int64

BytesFreed is how much the log shrank by.

func (WALCheckpoint) Truncated added in v0.0.5

func (c WALCheckpoint) Truncated() bool

Truncated reports whether the checkpoint reset the log.

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