Documentation
¶
Overview ¶
autoupdate.go stores the fleet's auto-update policy: whether the control plane rolls its executors forward on its own, which release they converge on, and how many move at once (Task 20331).
A sibling of executorsandbox.go and projectlimits.go — an operator's standing policy about a set of devices, held in the control plane, never readable by the devices it governs. The difference is that this one is fleet-wide rather than per-executor; see the migration for why.
Package statedb provides a SQLite-backed persistent store for cloop project state. It replaces the JSON flat-file store with a normalized schema that allows concurrent-safe writes, real SQL queries for reporting, and historical task rows.
Package statedb — typed sentinel errors.
HTTP handlers, the orchestrator, and CLI commands need to distinguish a handful of well-known failure modes from a generic "something went wrong". The Go convention is to expose package-level sentinel errors and have callers use errors.Is to test against them. This file is the authoritative list for cloop's relational data layer.
HTTP mapping (used by pkg/ui and pkg/apiserver via HTTPStatus below):
ErrTaskNotFound → 404 Not Found ErrProjectNotFound → 404 Not Found ErrStaleVersion → 409 Conflict ErrDBLocked → 503 Service Unavailable ErrSchemaMismatch → 500 Internal Server Error
All these errors are safe targets for errors.Is. Internal helpers wrap driver errors with %w so the sentinel survives across package boundaries.
executoraudience.go stores who may run work on one executor.
See migrations/0046_executor_audience.sql for why this is a table rather than a set of role bindings, and pkg/authz/audience.go for the matching semantics. This file is storage only: it does not know who is asking, and deliberately does not decide anything.
executorlimits.go stores the per-executor resource ceiling.
The sibling of projectlimits.go, field for field and failure for failure. The two exist separately because they answer questions that do not reduce to each other: a project's cap travels with the project wherever it is placed, and an executor's cap holds whatever lands on it. See migrations/0045_executor_resource_limits.sql for why neither the fleet ceiling nor the project ceiling can express the second one.
executorsandbox.go stores the per-executor sandbox configuration an admin sets from the UI: whether payloads run on the executor's host or in a container on it, and which engine, runtime, image and network that container uses.
The sibling of projectlimits.go and executors.go's project_executors. All three record an operator's policy about something rather than a fact belonging to it, which is why all three live in the control plane and none is readable — or writable — from the thing they constrain.
Maintenance helpers for the SQLite state database (Task 20107).
VACUUM reclaims space from rows previously deleted (cloop deletes step rows on `cloop compact`, prunes archived tasks, etc.). ANALYZE refreshes the query planner's statistics so growing tables continue to use sensible indexes. Both operations run as autocommit statements and acquire d.mu so no other goroutine in this process can interleave with them; cross-process contention is bounded by busy_timeout=5000.
Persistence: every successful VACUUM/ANALYZE run is appended to the `maintenance_log` table by AppendMaintenanceLog so:
- cloop db maintain --auto can compare current page_count against the last vacuum's page_count_after to decide whether to skip,
- cloop doctor can surface "last maintenance: 3d ago, freed 4.2 MB",
- operators have a forensic trail when investigating disk-usage spikes.
Schema migration framework for statedb.
Replaces the prior "execute the entire CREATE TABLE IF NOT EXISTS blob on every Open" approach with versioned, append-only .sql files embedded into the binary. Each migration runs inside its own transaction and records itself in schema_migrations on success; a crash mid-migration rolls back cleanly so the next Open re-attempts from the failed version.
Lifecycle:
Open() ──► applyPragmas() ──► Migrate() ──► return *DB
Adding a new migration:
- Create pkg/statedb/migrations/NNNN_<slug>.sql where NNNN is the next unused 4-digit version (e.g. 0002_add_pinned.sql).
- 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.
- NEVER edit a shipped migration file. Roll forward with another file.
Existing databases (pre-framework) are detected at boot: if all the 0001 tables already exist but schema_migrations is empty, version 1 is recorded as the baseline rather than re-applied. This makes the rollout safe for databases written by older binaries.
projectlimits.go stores the per-project resource ceiling.
The companion to executors.go's project_executors: both record an operator's policy *about* a project rather than a fact belonging to it, which is why both live in the control plane and neither is readable from the repository the project's developers control.
Package statedb — Store interface for the relational data layer. The DB struct (SQLite) implements Store. A future PostgreSQL driver need only implement this interface to be used as a drop-in replacement.
Index ¶
- Constants
- Variables
- func AllowSchemaDowngradeFromEnv() bool
- func AuditAttachSession(d *DB, in AttachAuditInput)
- func AuditCapPause(d *DB, in CapPauseInput)
- func AuditCapResume(d *DB, in CapResumeInput)
- func AuditChainLink(ev AuditEvent) (want string, ok bool)
- func AuditExecutorLifecycle(d *DB, in ExecutorAuditInput)
- func AuditImagePolicyDenial(d *DB, in ImagePolicyDenialInput)
- func AuditSecretDecision(d *DB, in SecretAuditInput)
- func AuditTaskDelete(d *DB, taskID int, actor string)
- func AuditTaskDispatch(d *DB, in TaskDispatchInput)
- func AuditTaskStatus(d *DB, taskID int, oldStatus, newStatus, actor string)
- func AuditTaskUpsert(d *DB, t *pm.Task, actor string)
- func AuditWorkspaceProvision(d *DB, in WorkspaceAuditInput)
- func GrowthTables() []string
- func HTTPStatus(err error) int
- func LatestSchemaVersion() (int, error)
- func MarshalAuditPayload(v any) string
- func MarshalHandleMeta(meta map[string]string) string
- func ProviderCallBodiesPruned(headersJSON string) (time.Time, bool)
- func RoleBindingID(effect, claim, value, project, executor string) string
- func SandboxSettingsFor(d *DB, executorID string) (executor.SandboxSettings, error)
- func SealedSetNames() []string
- func SetAuditEnabled(on bool)
- func SetAutoUpdatePolicy(d *DB, p autoupdate.Policy, setBy string) error
- func SetBinaryVersion(v string)
- func UnmarshalHandleMeta(s string) map[string]string
- func UserErrorf(sentinel error, format string, args ...any) error
- type APITokenRow
- type AgentCredentialRow
- type AttachAuditInput
- type AuditAnchor
- type AuditEvent
- type AuditFilter
- type AuditPruneCandidate
- type AuditVerifyReport
- type AutoUpdatePolicy
- type BrokerGrantRow
- type BrokerSecretRow
- type CIExchangeRow
- type CIPipelineRuleRow
- type CapPauseInput
- type CapResumeInput
- type CostEntry
- type DB
- func (d *DB) AcquireHubLease(row HubLeaseRow, expect HubLeaseRow) (bool, error)
- func (d *DB) AddExecutorAudience(executorID, kind, value, addedBy string) error
- func (d *DB) AdvanceSpendCursor(projectPath string, from, to int64) (won bool, err error)
- func (d *DB) AllExecutorAudiences() (map[string][]ExecutorAudienceEntry, error)
- func (d *DB) Analyze() error
- func (d *DB) AppendAuditEvent(ev *AuditEvent) error
- func (d *DB) AppendAuditEvents(evs []*AuditEvent) error
- func (d *DB) AppendCost(entry CostEntry) error
- func (d *DB) AppendMaintenanceLog(e MaintenanceLogEntry) (int64, error)
- func (d *DB) AppendProviderCall(row ProviderCallRow) (int64, error)
- func (d *DB) AppendStep(row StepRow) error
- func (d *DB) AppendTelemetry(events []telemetry.Event) error
- func (d *DB) AsControlPlane() *DB
- func (d *DB) AsProject() *DB
- func (d *DB) AttributeRequestUseTasks() (int, error)
- func (d *DB) AuditDistinctActors() ([]string, error)
- func (d *DB) AuditDistinctEntityTypes() ([]string, error)
- func (d *DB) BindProjectExecutor(projectPath, executorID, boundBy string) error
- func (d *DB) BrokerMeta(key string) (string, bool, error)
- func (d *DB) ClaimExecutorSessionRequeue(id, claimToken, newClaimToken string, at time.Time) (ExecutorSessionRow, error)
- func (d *DB) ClearExecutorAudience(executorID string) error
- func (d *DB) ClearExecutorResourceLimit(executorID string) error
- func (d *DB) ClearExecutorSandbox(executorID string) error
- func (d *DB) ClearHubLease(scope string, expect HubLeaseRow, at time.Time) (bool, error)
- func (d *DB) ClearKill(taskID int) error
- func (d *DB) ClearProjectResourceLimit(projectPath string) error
- func (d *DB) Close() error
- func (d *DB) CloseExecutorSession(id, state string, at time.Time) error
- func (d *DB) CountBrokerSecretsByKey() (map[string]int, error)
- func (d *DB) CountEvents() (int, error)
- func (d *DB) CountRunningExecutorSessions(executorID string) (int, error)
- func (d *DB) CountSessionsByKey() (map[string]int, error)
- func (d *DB) CurrentSchemaVersion() (int, error)
- func (d *DB) DeleteBrokerSecret(id string) error
- func (d *DB) DeleteCIPipelineRule(id string) (bool, error)
- func (d *DB) DeleteExecutor(id string) error
- func (d *DB) DeleteExecutorHandle(handleID string) error
- func (d *DB) DeleteExecutorHealth(executorID string) error
- func (d *DB) DeleteExpiredSessions(absoluteCutoff, idleCutoff time.Time) ([]SessionRow, error)
- func (d *DB) DeleteHubMeta(key string) error
- func (d *DB) DeleteQuotaOverride(identity string) (bool, error)
- func (d *DB) DeleteRoleBinding(id string) (bool, error)
- func (d *DB) DeleteSecretLeaseDir(dir string) error
- func (d *DB) DeleteSession(id string) (bool, error)
- func (d *DB) DeleteSessionsBySubject(subject, keepID string) ([]SessionRow, error)
- func (d *DB) DeleteTask(id int) error
- func (d *DB) ExecutorAudience(executorID string) ([]ExecutorAudienceEntry, error)
- func (d *DB) ExecutorResourceCeiling(executorID string) (executor.ResourceCeiling, bool, error)
- func (d *DB) ExecutorSandboxRecord(executorID string) (ExecutorSandbox, bool, error)
- func (d *DB) ExecutorSandboxSettings(executorID string) (executor.SandboxSettings, bool, error)
- func (d *DB) ExpireGrantRequests(now time.Time) ([]GrantRequestRow, error)
- func (d *DB) GetAPIToken(id string) (APITokenRow, error)
- func (d *DB) GetAgentCredential(agentID string) (AgentCredentialRow, error)
- func (d *DB) GetBrokerGrant(id string) (BrokerGrantRow, error)
- func (d *DB) GetBrokerSecret(id string) (BrokerSecretRow, error)
- func (d *DB) GetCIPipelineRule(id string) (CIPipelineRuleRow, error)
- func (d *DB) GetConfigBlob() (string, error)
- func (d *DB) GetEgressGrant(id string) (EgressGrantRow, error)
- func (d *DB) GetEnrollmentToken(id string) (EnrollmentTokenRow, error)
- func (d *DB) GetExecutor(id string) (ExecutorRow, error)
- func (d *DB) GetExecutorHandle(handleID string) (ExecutorHandleRow, error)
- func (d *DB) GetExecutorHealth(executorID string) (ExecutorHealthRow, error)
- func (d *DB) GetExecutorSession(id string) (ExecutorSessionRow, error)
- func (d *DB) GetGrantRequest(id string) (GrantRequestRow, error)
- func (d *DB) GetHubLease(scope string) (HubLeaseRow, error)
- func (d *DB) GetRoleBinding(id string) (RoleBindingRow, error)
- func (d *DB) GetSession(id string) (SessionRow, error)
- func (d *DB) GrowthTableStats() ([]TableStat, error)
- func (d *DB) HasProjectState() (bool, error)
- func (d *DB) HubMeta(key string) (string, bool, error)
- func (d *DB) LastMaintenanceLog() (*MaintenanceLogEntry, error)
- func (d *DB) LatestAuditAnchor() (AuditAnchor, error)
- func (d *DB) ListAPITokens() ([]APITokenRow, error)
- func (d *DB) ListAgentCredentials() ([]AgentCredentialRow, error)
- func (d *DB) ListAuditAnchors() ([]AuditAnchor, error)
- func (d *DB) ListAuditEvents(f AuditFilter) (rows []AuditEvent, total int, err error)
- func (d *DB) ListBrokerGrants() ([]BrokerGrantRow, error)
- func (d *DB) ListBrokerSecrets() ([]BrokerSecretRow, error)
- func (d *DB) ListBrokerSecretsNotUnderKey(keyID string, limit int) ([]SealedRow, error)
- func (d *DB) ListCIExchanges(limit int) ([]CIExchangeRow, error)
- func (d *DB) ListCIPipelineRules() ([]CIPipelineRuleRow, error)
- func (d *DB) ListEgressGrants() ([]EgressGrantRow, error)
- func (d *DB) ListEnrollmentTokens() ([]EnrollmentTokenRow, error)
- func (d *DB) ListEvents(offset, limit int) (rows []EventRow, total int, err error)
- func (d *DB) ListExecutorHandles(executorID string) ([]ExecutorHandleRow, error)
- func (d *DB) ListExecutorHealth() ([]ExecutorHealthRow, error)
- func (d *DB) ListExecutorResourceLimits() ([]ExecutorResourceLimit, error)
- func (d *DB) ListExecutorSandboxes() ([]ExecutorSandbox, error)
- func (d *DB) ListExecutorSessions(executorID string, onlyRunning bool) ([]ExecutorSessionRow, error)
- func (d *DB) ListExecutors() ([]ExecutorRow, error)
- func (d *DB) ListGrantRequestUses(requestID string) ([]GrantRequestUseRow, error)
- func (d *DB) ListGrantRequests() ([]GrantRequestRow, error)
- func (d *DB) ListKEKs() ([]KEKRow, error)
- func (d *DB) ListProjectExecutorBindings() ([]ProjectExecutorBinding, error)
- func (d *DB) ListProjectResourceLimits() ([]ProjectResourceLimit, error)
- func (d *DB) ListProviderCalls(offset, limit, taskID int, provider string) (rows []ProviderCallRow, total int, err error)
- func (d *DB) ListQuotaCounters() ([]QuotaCounterRow, error)
- func (d *DB) ListQuotaOverrides() ([]QuotaOverrideRow, error)
- func (d *DB) ListRoleBindings() ([]RoleBindingRow, error)
- func (d *DB) ListRotations(limit int) ([]RotationRow, error)
- func (d *DB) ListSecretLeaseDirs() ([]SecretLeaseDirRow, error)
- func (d *DB) ListSessions() ([]SessionRow, error)
- func (d *DB) ListSessionsNotUnderKey(keyID string, limit int) ([]SealedRow, error)
- func (d *DB) ListTelemetrySessions(source string, limit int) ([]TelemetrySession, error)
- func (d *DB) LoadProviderCall(id int64) (*ProviderCallRow, error)
- func (d *DB) LoadSpendCursor(projectPath string) (SpendCursor, bool, error)
- func (d *DB) LoadState() (*State, error)
- func (d *DB) LoadStateLite() (*State, error)
- func (d *DB) LoadTask(id int) (*pm.Task, error)
- func (d *DB) LookupKill(taskID int) (KillRequest, bool, error)
- func (d *DB) MaxAuditID() (int64, error)
- func (d *DB) MaxCostRowID() (int64, error)
- func (d *DB) MonthlyCosts(year, month int) ([]CostEntry, error)
- func (d *DB) OffboardIdentity(w OffboardWrite) (OffboardApplied, error)
- func (d *DB) OpenExecutorSession(row ExecutorSessionRow) error
- func (d *DB) OpenSpendCursor(projectPath, identity string, startAt int64) error
- func (d *DB) PendingKills() ([]KillRequest, error)
- func (d *DB) PingContext(ctx context.Context) error
- func (d *DB) PlanAuditPrune(cutoff time.Time) (AuditPruneCandidate, error)
- func (d *DB) PrimaryKEK() (KEKRow, bool, error)
- func (d *DB) ProjectExecutor(projectPath string) (string, bool, error)
- func (d *DB) ProjectResourceCeiling(projectPath string) (executor.ResourceCeiling, bool, error)
- func (d *DB) PromoteKEK(id string, at time.Time) error
- func (d *DB) PruneAuditPrefix(a AuditAnchor) (AuditAnchor, error)
- func (d *DB) PruneCIExchanges(keep int) (int, error)
- func (d *DB) PruneCosts(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)
- func (d *DB) PruneEvents(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)
- func (d *DB) PruneProviderCalls(p ProviderCallRetention, now time.Time, dryRun bool) (ProviderCallPruneResult, error)
- func (d *DB) PruneQuotaCounters(bucket string) error
- func (d *DB) PruneSteps(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)
- func (d *DB) PruneTelemetry(cutoff time.Time) (int64, error)
- func (d *DB) PruneTelemetryRows(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)
- func (d *DB) PutAPIToken(row APITokenRow) error
- func (d *DB) PutAgentCredential(row AgentCredentialRow) error
- func (d *DB) PutBrokerGrant(row BrokerGrantRow) error
- func (d *DB) PutBrokerSecret(row BrokerSecretRow) error
- func (d *DB) PutCIExchange(row CIExchangeRow) error
- func (d *DB) PutCIPipelineRule(row CIPipelineRuleRow) error
- func (d *DB) PutEgressGrant(row EgressGrantRow) error
- func (d *DB) PutEnrollmentToken(row EnrollmentTokenRow) error
- func (d *DB) PutExecutorHandle(row ExecutorHandleRow) error
- func (d *DB) PutExecutorHealth(row ExecutorHealthRow) error
- func (d *DB) PutGrantRequest(row GrantRequestRow) error
- func (d *DB) PutGrantRequestUse(row GrantRequestUseRow) error
- func (d *DB) PutKEK(row KEKRow) error
- func (d *DB) PutQuotaCounter(row QuotaCounterRow) error
- func (d *DB) PutQuotaOverride(row QuotaOverrideRow) error
- func (d *DB) PutRoleBinding(row RoleBindingRow) (RoleBindingRow, error)
- func (d *DB) PutRotation(row RotationRow) error
- func (d *DB) PutSecretLeaseDir(row SecretLeaseDirRow) error
- func (d *DB) PutSession(row SessionRow) error
- func (d *DB) QueryTelemetry(f TelemetryFilter) ([]telemetry.Event, int, error)
- func (d *DB) ReadAuditRange(fromID, throughID int64, limit int) ([]AuditEvent, error)
- func (d *DB) ReadCosts() ([]CostEntry, error)
- func (d *DB) ReadCostsAfterID(afterID int64, limit int) ([]CostEntry, error)
- func (d *DB) ReadCostsSince(since time.Time) ([]CostEntry, error)
- func (d *DB) ReconstructTask(taskID int) (TaskStory, error)
- func (d *DB) RecordEvent(row EventRow) error
- func (d *DB) RedeemEnrollmentToken(id, agentID string, at time.Time) error
- func (d *DB) ReleaseHubLease(scope, instanceID string, at time.Time) (bool, error)
- func (d *DB) RemoveExecutorAudience(executorID, kind, value string) error
- func (d *DB) RenewHubLease(scope, instanceID string, at time.Time) (bool, error)
- func (d *DB) ReplaceBrokerSecretSealed(id string, expect, next SealedRow) (bool, error)
- func (d *DB) ReplaceQuotaGauges(rows []QuotaCounterRow) error
- func (d *DB) ReplaceSessionSealed(id string, expect, next SealedRow) (bool, error)
- func (d *DB) RequestKill(req KillRequest) error
- func (d *DB) RetireKEK(id string, at time.Time) error
- func (d *DB) RevokeAPIToken(id string, at time.Time) error
- func (d *DB) RevokeAgentCredential(agentID string, at time.Time) error
- func (d *DB) RevokeBrokerGrant(id string, at time.Time) error
- func (d *DB) RevokeEgressGrant(id string, at time.Time) error
- func (d *DB) RevokeEnrollmentToken(id string, at time.Time) error
- func (d *DB) Role() Role
- func (d *DB) SaveState(s *State) error
- func (d *DB) SchemaStamp() (SchemaStamp, error)
- func (d *DB) SessionsDueForRefresh(cutoff time.Time, limit int) ([]SessionRow, error)
- func (d *DB) SetBrokerMeta(key, value string) error
- func (d *DB) SetConfigBlob(yamlBlob string) error
- func (d *DB) SetExecutorResourceLimit(executorID string, c executor.ResourceCeiling, setBy string) error
- func (d *DB) SetExecutorSandbox(executorID string, s executor.SandboxSettings, setBy string) error
- func (d *DB) SetHubMeta(key, value string) error
- func (d *DB) SetProjectResourceLimit(projectPath string, c executor.ResourceCeiling, setBy string) error
- func (d *DB) SizeStats() (SizeStats, error)
- func (d *DB) SpendByIdentity(from, to time.Time) ([]IdentitySpend, error)
- func (d *DB) TaskRunID(taskID int) (string, bool, error)
- func (d *DB) TouchAPIToken(id string, at time.Time) error
- func (d *DB) TouchAgentCredential(agentID string, at time.Time) error
- func (d *DB) TouchCIPipelineRule(id string, at time.Time) error
- func (d *DB) TouchExecutorHeartbeat(id, status string, at time.Time) error
- func (d *DB) TouchSession(id string, t time.Time) error
- func (d *DB) UnbindProjectExecutor(projectPath string) error
- func (d *DB) UpdateSessionRefresh(id string, up SessionRefreshUpdate) error
- func (d *DB) UpsertExecutor(row ExecutorRow) error
- func (d *DB) UpsertTask(t *pm.Task) error
- func (d *DB) Vacuum() error
- func (d *DB) VacuumInto(outPath string) error
- func (d *DB) VerifyAuditChain() (AuditVerifyReport, error)
- func (d *DB) WALCheckpointTruncate() (string, error)
- type EgressGrantRow
- type EnrollmentTokenRow
- type EventRow
- type EventType
- type ExecutorAudienceEntry
- type ExecutorAuditInput
- type ExecutorHandleRow
- type ExecutorHealthRow
- type ExecutorInventory
- type ExecutorResourceLimit
- type ExecutorRow
- type ExecutorSandbox
- type ExecutorSessionRow
- type GrantRequestRow
- type GrantRequestUseRow
- type HubLeaseRow
- type IdentitySpend
- type ImagePolicyDenialInput
- type KEKRow
- type KillRequest
- type MaintenanceLogEntry
- type MigrateOptions
- type MigrationCompat
- type MigrationReport
- type OffboardApplied
- type OffboardWrite
- type OpenOptions
- type ProjectExecutorBinding
- type ProjectResourceLimit
- type ProviderCallPruneResult
- type ProviderCallRetention
- type ProviderCallRow
- type QuotaCounterRow
- type QuotaOverrideRow
- type Role
- type RoleBindingRow
- type RotationRow
- type RowPruneResult
- type RowRetention
- type SchemaStamp
- type SchemaTooNewError
- type SealedRow
- type SecretAuditInput
- type SecretLeaseDirRow
- type SessionClaims
- type SessionRefreshUpdate
- type SessionRow
- type SizeStats
- type SpendCursor
- type State
- type StepRow
- type Store
- type TableStat
- type TaskDispatchInput
- type TaskRunStory
- type TaskStory
- type TelemetryFilter
- type TelemetrySession
- type VersionDivergence
- type WorkspaceAuditInput
Constants ¶
const ( // ExecutorSessionRunning: work is in flight on the executor. ExecutorSessionRunning = "running" // ExecutorSessionRequeued: a supervisor claimed the session for failover // and the work has been handed to another executor. ExecutorSessionRequeued = "requeued" // ExecutorSessionFinished: the work completed on this executor. ExecutorSessionFinished = "finished" // ExecutorSessionFailed: the work ended in failure and was not requeued. ExecutorSessionFailed = "failed" )
Executor session states as persisted in the state column.
const ( ExecutorStatusOnline = "online" ExecutorStatusOffline = "offline" ExecutorStatusDegraded = "degraded" ExecutorStatusUnknown = "unknown" )
Executor health states as persisted in the status column.
const ( RoleEffectAllow = "allow" RoleEffectDeny = "deny" )
Effect values for RoleBindingRow.Effect.
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.
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.
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.
const LatestVersion = 1 << 30
LatestVersion is the sentinel MigrateTo accepts to mean "every embedded migration". Any real schema version is far below it.
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.
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.
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.
const TelemetryMaxRows = 50000
TelemetryMaxRows is the hard ceiling on stored events.
Enforced on the write path rather than by the hourly retention janitor, because the writer here is an endpoint that accepts unauthenticated POSTs: a bound that only holds once an hour is not a bound against the traffic this table is actually exposed to. At the field budgets pkg/telemetry enforces, the worst case is a few hundred megabytes, and the realistic case — the trails of a handful of sessions — is a few megabytes.
Variables ¶
var ( // ErrTaskNotFound indicates the requested task ID does not exist in the // project plan. Returned by lookup helpers in pkg/state and pkg/statedb. ErrTaskNotFound = errors.New("statedb: task not found") // ErrProjectNotFound indicates the working directory has no cloop project // (no state.db, no migratable state.json). Returned by state.Load. ErrProjectNotFound = errors.New("statedb: project not found") // ErrStaleVersion indicates an optimistic-concurrency check failed: the // caller's plan version no longer matches the on-disk version. Callers // should re-load and re-apply. ErrStaleVersion = errors.New("statedb: stale plan version") // ErrDBLocked indicates SQLite returned a busy/locked error that the // busy_timeout did not absorb (extreme contention or a long-running // writer in another process). Callers may retry the operation. ErrDBLocked = errors.New("statedb: database busy") // ErrSchemaMismatch indicates the on-disk schema is not what this binary // expects (corruption, partial migration, or a future binary's database // opened by an older one). Manual intervention is usually required. ErrSchemaMismatch = errors.New("statedb: schema mismatch") // ErrSchemaTooNew indicates the database has been migrated past the // highest migration this binary embeds — an older binary opened a // database a newer one moved forward. // // Distinct from ErrSchemaMismatch because the remedy is different: this // is fixed by running a different binary, not by repairing the database. // Errors carrying it also satisfy errors.Is for ErrSchemaMismatch, so // callers that only handle the general case keep working. The detail — // both versions, and which build applied the newer schema — is in // *SchemaTooNewError, reachable with errors.As. ErrSchemaTooNew = errors.New("statedb: database schema is newer than this binary") // ErrExecutorNotFound indicates the requested execution backend is not // registered in the executors table. Callers must treat this as fatal // for the operation rather than falling back to host execution — see // pkg/executor's fail-closed resolution policy. ErrExecutorNotFound = errors.New("statedb: executor not found") // ErrBrokerSecretNotFound indicates no broker_secrets row has the // requested ID. Like ErrExecutorNotFound this is fatal for the // operation: a caller must never substitute a different credential for // one it could not find. ErrBrokerSecretNotFound = errors.New("statedb: broker secret not found") // ErrBrokerGrantNotFound indicates no broker_grants row has the // requested ID. ErrBrokerGrantNotFound = errors.New("statedb: broker grant not found") // ErrGrantRequestNotFound indicates no secret_grant_requests row has the // requested ID (Task 20271). Distinct from ErrBrokerGrantNotFound because // the two answer different questions: an unknown grant means the // authorisation does not exist, while an unknown request means nobody ever // asked — and a decide call that confused them would report "already // decided" for an id that was simply mistyped. ErrGrantRequestNotFound = errors.New("statedb: grant request not found") // ErrEgressGrantNotFound indicates no egress_grants row has the // requested ID. Like the broker sentinels this is fatal for the // operation: a caller must never fall back to a different egress policy // than the one it asked for. ErrEgressGrantNotFound = errors.New("statedb: egress grant not found") // ErrExecutorHealthNotFound indicates the executor has no health record: // the scheduler has never probed it. Callers must not read this as // "healthy" — an unobserved executor is unknown, not ready. ErrExecutorHealthNotFound = errors.New("statedb: executor health not found") // ErrExecutorSessionNotFound indicates no executor_sessions row has the // requested ID. ErrExecutorSessionNotFound = errors.New("statedb: executor session not found") // ErrExecutorSessionClaimLost indicates a conditional requeue matched no // row: another supervisor already claimed the session, or it is no longer // running. This is the exactly-once signal, and it is a distinct error // because "someone else is failing this over" must never be retried as if // it were a transient fault — doing so is how the same work ends up // running twice. ErrExecutorSessionClaimLost = errors.New("statedb: executor session claim lost") // ErrAPITokenNotFound indicates no api_tokens row has the requested ID. // // On the verification path this is an ordinary outcome, not a fault: it // is what a forged or already-purged token id looks like. Callers must // answer it exactly as they answer a bad secret — an unauthenticated // caller must not be able to use the difference to learn which token ids // exist. ErrAPITokenNotFound = errors.New("statedb: api token not found") // ErrSessionNotFound indicates no sessions row has the requested id. // // Like ErrAPITokenNotFound this is an ordinary outcome on the // authentication path, not a fault: it is what a forged cookie, a cookie // from before a restart that wiped the database, and a session an // administrator just terminated all look like. All three must produce the // same answer to the caller. ErrSessionNotFound = errors.New("statedb: session not found") // ErrRoleBindingNotFound indicates no role_bindings row has the requested // id. Unlike the two above this is a genuine operator error rather than an // expected outcome — binding ids are only ever obtained by listing the // table — so callers should surface it instead of absorbing it. ErrRoleBindingNotFound = errors.New("statedb: role binding not found") // ErrCIPipelineRuleNotFound indicates no ci_pipeline_rules row carries the // requested id (Task 20278). // // Surfaced rather than absorbed. A rule id is only ever obtained from a // listing, so not finding one means the operator's view is stale — most // likely because a second maintainer deleted the same rule concurrently — // and a PUT that silently recreated it would resurrect an allowlist entry // somebody had just decided to remove. ErrCIPipelineRuleNotFound = errors.New("statedb: ci pipeline rule not found") )
Sentinel errors. Add to this list — do not rename or reuse.
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.
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.
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.
var ErrExecutorHandleNotFound = errors.New("statedb: executor handle not found")
ErrExecutorHandleNotFound is returned when no row exists for a handle id.
var ErrHubLeaseNotFound = errors.New("statedb: hub lease not found")
ErrHubLeaseNotFound is returned when no row exists for a scope.
Functions ¶
func AllowSchemaDowngradeFromEnv ¶
func AllowSchemaDowngradeFromEnv() bool
AllowSchemaDowngradeFromEnv reports whether the operator opt-out is set.
Exported so `cloop hub doctor` can tell an operator the guard is disabled. A safety check that can be switched off silently gets switched off and left that way, so the off state is itself a finding.
func AuditAttachSession ¶ added in v0.0.2
func AuditAttachSession(d *DB, in AttachAuditInput)
AuditAttachSession records one interactive-session event.
Best-effort, like every other emitter in this package: a wedged journal must not stop an operator reaching a sandbox during an incident. That is a deliberate trade — an unrecorded session is worse than no session in theory, and much better than an unreachable sandbox at 3am in practice — and it is the same trade every other audit path here already makes.
func AuditCapPause ¶ added in v0.0.2
func AuditCapPause(d *DB, in CapPauseInput)
AuditCapPause records a run parked by a subscription cap.
func AuditCapResume ¶ added in v0.0.2
func AuditCapResume(d *DB, in CapResumeInput)
AuditCapResume records the hub restarting a cap-paused run with no human in the loop.
func AuditChainLink ¶
func AuditChainLink(ev AuditEvent) (want string, ok bool)
AuditChainLink recomputes one row's hash and reports whether it matches, so callers outside this package (the sealer, which must not archive a chain that is already broken) can verify without duplicating computeRowHash.
func AuditExecutorLifecycle ¶
func AuditExecutorLifecycle(d *DB, in ExecutorAuditInput)
AuditExecutorLifecycle records an executor-fleet mutation. Best-effort, like every other emitter in this file: a wedged audit log must not stop an operator from cordoning a misbehaving node.
func AuditImagePolicyDenial ¶
func AuditImagePolicyDenial(d *DB, in ImagePolicyDenialInput)
AuditImagePolicyDenial records a container image refused by the hub's trust policy. Best-effort, like every other emitter here.
func AuditSecretDecision ¶
func AuditSecretDecision(d *DB, in SecretAuditInput)
AuditSecretDecision records a secret-broker mint, grant, lease, renew, revoke, or denial. Best-effort, like every other emitter in this file: an unavailable audit log must not stop an executor from receiving the credentials it was granted.
func AuditTaskDelete ¶
AuditTaskDelete records a task removal. EntityID is the deleted task's id.
func AuditTaskDispatch ¶ added in v0.0.2
func AuditTaskDispatch(d *DB, in TaskDispatchInput)
AuditTaskDispatch records one task entering execution.
Best-effort, like every other emitter in this file: a wedged audit log must not stop a task from running. The chain verifier surfaces the resulting gap.
func AuditTaskStatus ¶
AuditTaskStatus records a manual status flip (UI/CLI initiated).
func AuditTaskUpsert ¶
AuditTaskUpsert is the exported wrapper that callers outside this package (orchestrator, UI handlers) use to emit a task-mutation event with their own actor identity. Best-effort.
func AuditWorkspaceProvision ¶
func AuditWorkspaceProvision(d *DB, in WorkspaceAuditInput)
AuditWorkspaceProvision records one workspace-provisioning phase.
Best-effort, like every other emitter in this file: an unavailable audit log must not stop a workload from getting its source tree.
func GrowthTables ¶ added in v0.0.2
func GrowthTables() []string
GrowthTables names the row tables retention covers, in the order a report should list them. Exported so hubdoctor can enumerate them without duplicating the list and drifting from it.
func HTTPStatus ¶
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 ¶
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 ¶
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 ¶
MarshalHandleMeta renders a driver's extras for the meta_json column. A nil or empty map becomes "{}" rather than "", so the column always holds valid JSON and a reader never has to special-case the empty string.
func ProviderCallBodiesPruned ¶ added in v0.0.2
ProviderCallBodiesPruned reports when a provider-call row's bodies were dropped by retention, reading the marker out of its headers JSON.
ok is false for a row that still has its bodies, which is every row on a hub where the strip has not run. Callers that render or replay a prompt should check this before treating an empty prompt as the truth.
func RoleBindingID ¶ added in v0.0.2
RoleBindingID derives the deterministic identifier for a binding's identity tuple.
Identity is (effect, claim, value, project, executor) and deliberately excludes role, reason and provenance: re-granting the same subject a different role on the same scope should replace the binding, not leave two rows whose winner depends on resolution order. The reason and the actor of the most recent write are what the audit trail keeps.
Truncated to 48 bits. This is a content address for deduplication, not a security boundary — the values it covers are already visible to anyone who can read the table — and a 12-character id is one an operator can retype from a terminal during an incident.
func SandboxSettingsFor ¶ added in v0.0.3
func SandboxSettingsFor(d *DB, executorID string) (executor.SandboxSettings, error)
SandboxSettingsFor resolves an executor's settings for a dispatch, collapsing "no row" and "lookup failed" onto the same answer the caller can act on.
The two are not the same, and the difference is the reason this helper exists rather than every caller deciding for itself. A missing row is a fact: this executor was never configured, and it should run what it always ran. A failed lookup is not a fact about the executor at all, and answering it with the zero value would silently unconfigure a container-mode executor for the duration of a database fault — a downgrade from containment to host execution, arrived at by no one's decision, at the moment the hub is least able to notice.
So a failed lookup returns the error and the caller refuses the dispatch. The run fails loudly and is retried; it does not quietly run somewhere weaker than the admin asked for. That is the same trade the container driver makes when it refuses to construct without a runtime.
A nil DB means no control plane — the single-user, no-database path — and yields unset settings with no error, because there is no policy to fail to read.
func SealedSetNames ¶
func SealedSetNames() []string
SealedSetNames returns the populations retirement checks, so a test can assert they match the sets registered for rotation.
func SetAuditEnabled ¶
func SetAuditEnabled(on bool)
SetAuditEnabled toggles audit emission globally. Intended for tests and for callers running cloop against a database where the audit_events table is not desired (e.g. one-shot CLI commands that read state).
func SetAutoUpdatePolicy ¶ added in v0.0.3
func SetAutoUpdatePolicy(d *DB, p autoupdate.Policy, setBy string) error
SetAutoUpdatePolicy replaces the fleet's policy. setBy is the identity that set it, for the audit trail.
func SetBinaryVersion ¶
func SetBinaryVersion(v string)
SetBinaryVersion records which build this process is, for stamping into schema_migrations when it applies a migration. Call it once during process initialisation; cmd does so with the ldflags-injected version.
It lives here rather than being read from pkg/cmd because pkg/statedb sits below the CLI in the import graph and must not depend on it.
func UnmarshalHandleMeta ¶
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
UserErrorf returns an error that prints as the formatted message and matches sentinel under errors.Is.
Use it wherever a sentinel would otherwise be wrapped in prose the user is meant to read:
return statedb.UserErrorf(statedb.ErrProjectNotFound, "no cloop project found (run 'cloop init' first)")
prints "no cloop project found (run 'cloop init' first)" and still satisfies errors.Is(err, statedb.ErrProjectNotFound).
format is rendered with fmt.Sprintf, so it takes ordinary verbs but not %w — this constructor already owns the wrapping. A nil sentinel degrades to a plain fmt.Errorf rather than producing an error that matches nothing, so a caller that passes one by mistake still gets a usable message.
Types ¶
type APITokenRow ¶
type APITokenRow struct {
ID string `json:"id"`
Name string `json:"name"`
Hash string `json:"-"` // never serialized: see the migration comment
Prefix string `json:"prefix"`
Roles []string `json:"roles"`
ProjectScope []string `json:"project_scope,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
LastUsedAt time.Time `json:"last_used_at,omitempty"`
RevokedAt time.Time `json:"revoked_at,omitempty"`
// Kind labels a hub-issued token; "" is an ordinary PAT (migration 0023).
Kind string `json:"kind,omitempty"`
// OwnerJSON is the owning identity's claim bundle, stored opaquely. This
// layer deliberately does not know its shape: pkg/apitoken owns the
// encoding, and a second opinion here about what a claim set looks like is
// how the two drift apart. Never serialized outward for the same reason
// Hash is not — it carries a user's email and group memberships.
OwnerJSON string `json:"-"`
}
APITokenRow is one persisted API token.
Roles and ProjectScope are stored as JSON arrays rather than normalized child tables: both are short, always read whole with the row, and are validated by pkg/apitoken on the way in and on the way out. A join here would buy nothing and would put the verification path — which runs on every authenticated request — behind three queries instead of one.
type AgentCredentialRow ¶
type AgentCredentialRow struct {
AgentID string
Name string
// SecretHash is hex SHA-256 of the credential secret, never the secret.
SecretHash string
WorkDirRoot string
Labels map[string]string
CreatedAt time.Time
LastSeen time.Time
RevokedAt time.Time
EnrollmentID string
}
AgentCredentialRow is one enrolled agent's long-lived credential.
type AttachAuditInput ¶ added in v0.0.2
type AttachAuditInput struct {
// Event is the fully-qualified action: auditaction.ActionSandboxAttachOpen,
// ActionSandboxAttachClose, or ActionSandboxAttachDenied.
//
// Passed in whole rather than assembled from an action suffix, because
// these three are the complete set and a typo should be visible at the
// call site rather than produce a plausible-looking new event type that
// no retention rule or SIEM filter knows about. Typing it as an
// auditaction.Action is what turns "should be visible" into "cannot
// compile": there is no longer a bare string to get wrong.
Event auditaction.Action
// Actor is the acting identity — the OIDC subject label, the same
// spelling auditAuthz writes, so filtering the trail by person returns a
// whole session rather than half of it.
Actor string
// SessionID correlates the open with its close.
SessionID string
// ExecutorID and HandleID name the sandbox that was entered. Both,
// because the executor alone does not identify which workload and the
// handle alone does not survive as a lookup key once the run ends.
ExecutorID string
HandleID string
// TaskID is the work the sandbox was running, and the entity this row is
// filed under: "show me everything that happened to task 41" is the query
// an incident actually starts from.
TaskID int
// Command is the argv as the caller gave it, unmodified. Not normalised
// and not shell-quoted — the point is to record what was asked for, and a
// prettied-up rendering is a different string from the one that ran.
Command string
// Writable reports whether the session had an input channel at all.
Writable bool
// Detail is the refusal reason for a denial, or the close reason for a
// close. Empty on a clean open.
Detail string
}
AttachAuditInput carries one interactive-session lifecycle event.
type AuditAnchor ¶
type AuditAnchor struct {
ID int64
CreatedAt time.Time
Actor string
// Cutoff is the --before instant the operator asked for. Kept distinct
// from the ids it resolved to so an audit of the audit can tell a policy
// decision from its effect.
Cutoff time.Time
PrunedFirstID int64
PrunedThroughID int64
PrunedCount int64
// BoundaryHash is the row_hash of PrunedThroughID: what the first
// surviving row's prev_hash must equal.
BoundaryHash string
// RetainedFromID is the first surviving id, or 0 when the prune emptied
// the table.
RetainedFromID int64
ExportPath string
ExportFormat string
ExportSHA256 string
ExportBytes int64
PrevAnchorHash string
AnchorHash string
}
AuditAnchor records one deliberate truncation of the audit chain. See migration 0027 for what each field is load-bearing for.
type AuditEvent ¶
type AuditEvent struct {
ID int64 // monotonic primary key, assigned on append
Timestamp time.Time // UTC
Actor string // "system", "orchestrator", "ui", "cli", or user-supplied
EventType string // e.g. "task.create", "plan.update", "config.set"
EntityType string // "task" | "plan" | "config" | "step" | "session"
EntityID string // stringified id ("12" for task 12), empty for singletons
Payload string // JSON blob describing the mutation
PrevHash string // 64-hex SHA-256 of prior row, or 64 zeros for the first row
RowHash string // 64-hex SHA-256 of this row
}
AuditEvent is one row in the audit_events table.
type AuditFilter ¶
type AuditFilter struct {
Actor string // exact-match
EntityType string // exact-match (e.g. "task")
EntityID string // exact-match within EntityType
EventType string // exact-match (e.g. "task.update")
Since time.Time // only events with timestamp >= Since
Until time.Time // only events with timestamp <= Until
FromID int64 // only events with id >= FromID
ToID int64 // only events with id <= ToID
Search string // case-insensitive LIKE on payload
Order string // "asc" (default) or "desc"
Limit int // 0 = no limit (capped to 10 000 internally)
Offset int
}
AuditFilter narrows ListAuditEvents down to the rows the caller cares about. All fields are optional — zero values mean "no filter".
type AuditPruneCandidate ¶
type AuditPruneCandidate struct {
FirstID int64
ThroughID int64
Count int64
// BoundaryHash is the row_hash at ThroughID, carried so the seal and the
// delete can be checked against the same value.
BoundaryHash string
// Remaining is how many rows would survive.
Remaining int64
}
AuditPruneCandidate describes what a prune with the given cutoff would remove, without removing anything. Both the CLI's --dry-run and the real path use it, so the number an operator is shown is the number that runs.
type AuditVerifyReport ¶
type AuditVerifyReport struct {
Total int // rows checked
OK bool // true when every link verified
BreakAtID int64 // first row whose hash did not match (0 if OK)
Reason string // human-readable description of the break
// ExpectedHash and ActualHash carry the two hashes the break compared,
// in full rather than the abbreviated form embedded in Reason. Reason is
// for a human reading a terminal; these are for a UI that wants to show
// them side by side, and for a report that gets pasted into a ticket
// where a 12-character prefix is not enough to reason about. Both are
// empty for an id-gap break, where no hash was compared, and for a
// clean run.
ExpectedHash string
ActualHash string
// Anchored reports that a retention prune has truncated the chain and the
// walk was validated against an anchor rather than against genesis. The
// remaining fields describe that anchor, so a report can say "verified
// from id N; everything before it is in <export>, sha256 <digest>" instead
// of leaving an operator to wonder why the trail does not start at 1.
Anchored bool
AnchorID int64
PrunedCount int64
ExportPath string
ExportSHA256 string
// VerifiedFromID is the id of the first row the walk checked, or 0 when
// the table is empty.
VerifiedFromID int64
}
AuditVerifyReport summarises a chain-verification run.
type AutoUpdatePolicy ¶ added in v0.0.3
type AutoUpdatePolicy struct {
Policy autoupdate.Policy `json:"policy"`
SetAt time.Time `json:"set_at,omitempty"`
SetBy string `json:"set_by,omitempty"`
}
AutoUpdatePolicy is the stored policy plus its provenance.
func AutoUpdatePolicyFor ¶ added in v0.0.3
func AutoUpdatePolicyFor(d *DB) (AutoUpdatePolicy, error)
AutoUpdatePolicyFor returns the fleet's policy.
A fleet that has never been configured yields a disabled zero policy and no error. "Nobody has set this" and "automatic upgrades are off" are the same operational state, and making the caller distinguish them would put an error path in front of the overwhelmingly common case.
type BrokerGrantRow ¶
type BrokerGrantRow struct {
ID string
SecretID string
Scope string
SubjectType string
SubjectValue string
ConstraintsJSON string
ExpiresAt string
CreatedAt string
CreatedBy string
RevokedAt string
// Owner is denormalised from the secret this grant points at, so a grant
// listing can be scoped without resolving each row's secret.
Owner string
}
BrokerGrantRow is one row of broker_grants.
type BrokerSecretRow ¶
type BrokerSecretRow struct {
ID string
Kind string
Name string
Payload []byte // ciphertext under this row's DEK; never plaintext
// KeyID names the KEK that WrappedDEK is sealed under, or "legacy" for
// rows predating envelope encryption (Task 20181). WrappedDEK is the
// row's data key, itself sealed. Neither is key material on its own.
KeyID string
WrappedDEK []byte
MetadataJSON string
CreatedAt string
CreatedBy string
// Owner is the identity that personally owns this secret, or "" for a
// shared one. See migration 0039.
Owner string
}
BrokerSecretRow is one row of broker_secrets. Times are RFC3339 strings, matching the rest of the schema.
type CIExchangeRow ¶ added in v0.0.2
type CIExchangeRow struct {
ID string `json:"id"`
At time.Time `json:"at"`
Accepted bool `json:"accepted"`
Issuer string `json:"issuer,omitempty"`
Subject string `json:"subject,omitempty"`
Repository string `json:"repository,omitempty"`
Ref string `json:"ref,omitempty"`
Workflow string `json:"workflow,omitempty"`
Actor string `json:"actor,omitempty"`
EventName string `json:"event_name,omitempty"`
RunID string `json:"run_id,omitempty"`
RuleID string `json:"rule_id,omitempty"`
RuleName string `json:"rule_name,omitempty"`
SessionID string `json:"session_id,omitempty"`
// Reason is a stable code; Detail is the human-readable explanation. The
// pair exists so a dashboard can group by cause while still showing the
// sentence that names the actual problem.
Reason string `json:"reason,omitempty"`
Detail string `json:"detail,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
// Claims is the verified payload, or nil on a token that never verified.
Claims map[string]any `json:"claims,omitempty"`
}
CIExchangeRow is one recorded federation attempt.
type CIPipelineRuleRow ¶ added in v0.0.2
type CIPipelineRuleRow struct {
ID string `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Repository string `json:"repository,omitempty"`
Ref string `json:"ref,omitempty"`
Workflow string `json:"workflow,omitempty"`
Environment string `json:"environment,omitempty"`
Actor string `json:"actor,omitempty"`
EventName string `json:"event_name,omitempty"`
Condition string `json:"condition,omitempty"`
Models []string `json:"models,omitempty"`
MaxRequests int `json:"max_requests,omitempty"`
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
TTLSeconds int `json:"ttl_seconds,omitempty"`
Project string `json:"project,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
LastMatchedAt time.Time `json:"last_matched_at,omitempty"`
}
CIPipelineRuleRow is one persisted allowlist entry.
type CapPauseInput ¶ added in v0.0.2
type CapPauseInput struct {
// ProjectPath is the working directory whose run was stopped.
ProjectPath string
// Windows names the tripped windows ("five_hour", "weekly", ...).
Windows []string
// Utilization and Cap are the global utilization and the configured
// per-project ceiling that it reached, for the first tripped window.
Utilization float64
Cap float64
// ResumesAt is when every tripped window has rolled over. Zero when the
// usage API did not report a reset, in which case the row says so rather
// than inventing a time a resume could later be matched against.
ResumesAt time.Time
// Detail is the operator-facing phrase stored as the pause reason.
Detail string
Actor string
}
CapPauseInput is one run stopped by a Claude Code subscription cap.
type CapResumeInput ¶ added in v0.0.2
type CapResumeInput struct {
ProjectPath string
// PausedDetail is the pause reason the run carried, so the row names the
// wall it was waiting on.
PausedDetail string
// ResumesAt is the instant the run was waiting for, and ResumedAt is when
// the hub actually acted. The gap between them is the sweep interval, and
// having both is what distinguishes a late sweep from a wrong reset time.
ResumesAt time.Time
ResumedAt time.Time
Actor string
}
CapResumeInput is one cap-paused run restarted by the hub on its own.
type CostEntry ¶
type CostEntry struct {
Timestamp time.Time
TaskID int
TaskTitle string
Provider string
Model string
InputTokens int
OutputTokens int
ThinkingTokens int
EstimatedUSD float64
// Identity is who this spend is attributed to — an oidcauth OwnerKey, or
// the "local" sentinel for an unauthenticated run (Task 20264). Empty on
// rows written before migration 0035, which means *unattributed* rather
// than unowned. Self-reported by the orchestrator; see 0035 for why quota
// enforcement deliberately does not read it.
Identity string
// RowID is the costs.id of this row. Set by the readers that select it,
// zero elsewhere (AppendCost does not read it back).
//
// It exists for the hub's spend drain, which needs a watermark it can
// resume from without double-booking. A timestamp cannot serve: two tasks
// finishing in the same nanosecond collide, and a clock adjustment moves
// one backwards — either would book a tenant twice or not at all. The
// autoincrement id is monotonic in insertion order, which is the order the
// drain actually walks.
RowID int64
}
CostEntry records the cost of one task execution in the costs table.
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a thread-safe handle to the SQLite state database. Open one DB per workdir; Close it when done.
func Open ¶
Open opens (or creates) the SQLite database at dbPath, applies tuning pragmas, and runs any pending schema migrations.
Concurrency model: every cloop process (Web UI, orchestrator, CLI commands) opens its own *DB handle pointing at the same .cloop/state.db file. WAL mode + busy_timeout coordinate them at the SQLite file level, so writes from one process do not return SQLITE_BUSY to readers in another.
Errors returned by this function may wrap the typed sentinels ErrDBLocked or ErrSchemaMismatch — callers should use errors.Is to distinguish them from generic open failures. A database migrated past this binary's schema is refused with ErrSchemaTooNew rather than opened; see schema_guard.go.
func OpenWithOptions ¶
func OpenWithOptions(dbPath string, opts OpenOptions) (*DB, error)
OpenWithOptions is Open with the version-skew opt-out under the caller's control rather than the environment's.
func (*DB) AcquireHubLease ¶
func (d *DB) AcquireHubLease(row HubLeaseRow, expect HubLeaseRow) (bool, error)
AcquireHubLease takes the lease for row.Scope, conditioned on the current row still matching expect.
It returns true when the lease was taken and false when it was not, without an error in either case: losing a race is an ordinary outcome here, not a failure, and the caller needs to re-read the row to describe the winner anyway.
expect is the row the caller previously read, and is the entire concurrency story. A zero-valued expect means "I saw no row": the INSERT then succeeds only if none appeared in the meantime, because a conflict falls through to a DO UPDATE whose WHERE cannot match. A populated expect means "I saw this row and judged it takeable": the update applies only if it is still byte-for-byte that row, so a holder that renewed in the intervening microseconds keeps its lease and this caller is told it lost.
This is one statement on purpose. SQLite applies it atomically without an explicit transaction, which avoids the read-then-upgrade pattern that deadlocks two processes against each other under WAL — the exact failure the busy_timeout cannot absorb.
func (*DB) AddExecutorAudience ¶ added in v0.0.3
AddExecutorAudience admits a principal to an executor. Adding one that is already admitted refreshes its provenance rather than failing, so the UI's "add" button is idempotent.
Note what the first successful call does: it turns the gate on for that executor, because an executor with no rows is unrestricted. The caller is responsible for not locking the fleet out of itself — see pkg/ui/executoraudience.go, which refuses an add that would leave the calling admin unable to reach the executor they just restricted.
func (*DB) AdvanceSpendCursor ¶ added in v0.0.2
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 ¶
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 ¶
AppendCost inserts a cost entry into the costs table.
func (*DB) AppendMaintenanceLog ¶
func (d *DB) AppendMaintenanceLog(e MaintenanceLogEntry) (int64, error)
AppendMaintenanceLog inserts a maintenance_log row. Returns the new row id. StartedAt / CompletedAt default to time.Now().UTC() when zero so callers don't have to fill them in for ad-hoc records.
func (*DB) AppendProviderCall ¶
func (d *DB) AppendProviderCall(row ProviderCallRow) (int64, error)
AppendProviderCall inserts one provider-call row and returns the assigned id. Best-effort: errors are wrapped through classifyDriverErr so callers can distinguish ErrDBLocked from real corruption, but production code paths typically log-and-discard.
func (*DB) AppendStep ¶
AppendStep inserts a step row (idempotent on step number).
func (*DB) AppendTelemetry ¶ added in v0.0.2
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
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) AttributeRequestUseTasks ¶ added in v0.0.2
AttributeRequestUseTasks stamps the task a recorded lease was dispatched for onto the use rows that do not have one yet, and reports how many it filled in.
It exists because a lease is issued *before* the workload that will hold it is dispatched. At lease time the broker knows the executor and the project and genuinely does not know the task — there is no task yet — so the use row starts with task_id 0 and the answer arrives a moment later, when the driver writes an executor_handles row carrying both the task and the lease bindings that workload was started with.
Reconciling the two afterwards is therefore the only way to get the attribution at all, and it has to happen while the handle is still live: handle rows are deleted when the workload reaches a terminal state, so a reconciliation that only ran on demand would report nothing for exactly the runs that have finished — which are the ones an approver reviews. Callers run it on the same schedule they sweep expired requests, and again when listing.
Idempotent and monotonic: it only fills in zeros (the UPDATE's guard, and PutGrantRequestUse's) so a handle that has since been reused for other work cannot rewrite an attribution already made.
func (*DB) AuditDistinctActors ¶
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 ¶
AuditDistinctEntities returns the set of distinct (entity_type, entity_id) pairs as "type/id" strings (id may be empty for singletons). Sorted.
func (*DB) BindProjectExecutor ¶
BindProjectExecutor pins projectPath to executorID. The executor must already exist, so a typo cannot strand a project on a nonexistent backend.
func (*DB) BrokerMeta ¶
BrokerMeta reads a broker-scoped metadata value.
func (*DB) ClaimExecutorSessionRequeue ¶
func (d *DB) ClaimExecutorSessionRequeue(id, claimToken, newClaimToken string, at time.Time) (ExecutorSessionRow, error)
ClaimExecutorSessionRequeue atomically claims a running session for failover and rotates its claim token.
The exactly-once guarantee lives entirely in this statement's WHERE clause. SQLite serialises writers, so of N supervisors racing to fail over the same dead executor exactly one finds state = 'running' with the claim token it read and updates a row; every other supervisor matches nothing and gets ErrExecutorSessionClaimLost. Rotating claim_token in the same statement also makes a retry by the winner idempotent-safe: the old token can never match again.
A read-then-write implementation would let two supervisors both decide the session is running and both requeue the work — which here means two AI agents editing the same repository concurrently. That is the failure this function exists to make impossible, so the read and the write must not be separable.
The follow-up read on a zero-row update is only to tell the operator which of two situations they are in; it never grants a claim, so a row appearing or vanishing between the two statements cannot turn a loss into a win.
func (*DB) ClearExecutorAudience ¶ added in v0.0.3
ClearExecutorAudience removes every principal, making the executor unrestricted. It is what executor deletion calls, so a re-enrolled executor reusing an ID does not inherit a stale allowlist.
func (*DB) ClearExecutorResourceLimit ¶ added in v0.0.3
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
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 ¶
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 ¶
ClearKill removes the kill row for taskID. Idempotent — clearing a row that does not exist returns nil.
func (*DB) ClearProjectResourceLimit ¶ added in v0.0.2
ClearProjectResourceLimit removes the ceiling for projectPath. Clearing a project that was never capped is not an error.
func (*DB) CloseExecutorSession ¶
CloseExecutorSession moves a session to a terminal state. Returns ErrExecutorSessionNotFound when the ID is unknown, so a driver reporting completion for a session the control plane never opened surfaces loudly instead of being silently dropped.
func (*DB) CountBrokerSecretsByKey ¶
CountBrokerSecretsByKey groups secrets by the key they are sealed under.
func (*DB) CountEvents ¶
CountEvents returns the total number of journal rows. Cheap; used by the UI to decide whether to refresh the top page.
func (*DB) CountRunningExecutorSessions ¶
CountRunningExecutorSessions returns how many sessions are still in flight. An empty executorID counts across the whole fleet.
func (*DB) CountSessionsByKey ¶
CountSessionsByKey groups sessions holding a refresh token by sealing key.
Sessions with no refresh token are excluded: they hold nothing sealed, so counting them would make a rotation look permanently incomplete on a hub whose IdP issues no refresh tokens.
func (*DB) CurrentSchemaVersion ¶
CurrentSchemaVersion returns the highest migration version recorded in the database. Useful for diagnostic commands (cloop db verify, cloop migrate status). Acquires the DB mutex like other read helpers in this package.
func (*DB) DeleteBrokerSecret ¶
DeleteBrokerSecret removes a secret. Deleting one that does not exist is an error, so a CLI can tell the operator their reference was wrong instead of reporting a successful no-op.
func (*DB) DeleteCIPipelineRule ¶ added in v0.0.2
DeleteCIPipelineRule removes a rule. It reports whether a row was removed, so the caller can tell "deleted" from "already gone" without a prior read.
func (*DB) DeleteExecutor ¶
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 ¶
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 ¶
DeleteExecutorHealth forgets the health record for executorID. Deleting a row that was never written is not an error: de-enrolling an executor that was never probed must not fail.
func (*DB) DeleteExpiredSessions ¶
func (d *DB) DeleteExpiredSessions(absoluteCutoff, idleCutoff time.Time) ([]SessionRow, error)
DeleteExpiredSessions removes sessions past either clock and returns what it deleted.
absoluteCutoff is compared against expires_at (the ceiling set at login) and idleCutoff against last_seen. Both are absolute times computed by the caller from its configured policy, so this layer stays free of any opinion about how long a session should live.
func (*DB) DeleteHubMeta ¶ added in v0.0.2
DeleteHubMeta removes a hub-scoped metadata value. Deleting one that is not there succeeds: the caller asked for it to be gone, and it is.
func (*DB) DeleteQuotaOverride ¶
DeleteQuotaOverride removes one identity's override, reporting whether a row existed.
func (*DB) DeleteRoleBinding ¶ added in v0.0.2
DeleteRoleBinding removes a binding, reporting whether a row existed.
func (*DB) DeleteSecretLeaseDir ¶
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 ¶
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 ¶
DeleteTask removes a single task by id. Returns nil even if the row did not exist — the post-condition (task absent) is satisfied either way.
func (*DB) ExecutorAudience ¶ added in v0.0.3
func (d *DB) ExecutorAudience(executorID string) ([]ExecutorAudienceEntry, error)
ExecutorAudience returns the principals admitted to executorID, oldest first so the panel's order is stable as entries are added.
An empty slice with a nil error means the executor is unrestricted. An error means the question could not be answered, which callers must not read as "unrestricted" — see pkg/ui/executoraudience.go for the failure posture a gate requires, which is the opposite of the one a ceiling requires.
func (*DB) ExecutorResourceCeiling ¶ added in v0.0.3
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
ExecutorSandboxSettings returns the settings for executorID.
The boolean is false when the executor has no row, which the dispatch path reads as "this executor keeps its own default" — deliberately distinct from an error, which means the lookup failed. The distinction matters in one direction especially: a storage fault that silently became "unset" would downgrade a container-mode executor to host execution exactly when the hub is least healthy, which is the failure this whole feature exists to prevent. See SandboxSettingsFor for the caller that has to choose what to do about it.
func (*DB) ExpireGrantRequests ¶ added in v0.0.2
func (d *DB) ExpireGrantRequests(now time.Time) ([]GrantRequestRow, error)
ExpireGrantRequests moves every pending request whose deadline has passed to the expired state, and returns the rows it changed.
Returning the rows rather than a count is what lets the caller emit one audit event per expiry. A request that lapses is a decision — nobody made one, and access was refused as a result — and the trail should say so with the same weight as a deny, so the requester can see why nothing happened.
Requests with no deadline (expires_at = ”) are left alone: the broker always sets one, and a row from some future path that deliberately has none should not be swept by a rule it never opted into.
func (*DB) GetAPIToken ¶
func (d *DB) GetAPIToken(id string) (APITokenRow, error)
GetAPIToken returns one token by ID, or ErrAPITokenNotFound.
This is the verification path's only read, which is why the lookup is by primary key: an authenticated request must not cost a table scan, and a scan would additionally make request latency a function of how many tokens the hub has ever issued.
func (*DB) GetAgentCredential ¶
func (d *DB) GetAgentCredential(agentID string) (AgentCredentialRow, error)
GetAgentCredential returns one agent by ID, or ErrAgentCredentialNotFound.
func (*DB) GetBrokerGrant ¶
func (d *DB) GetBrokerGrant(id string) (BrokerGrantRow, error)
GetBrokerGrant returns one grant by ID.
func (*DB) GetBrokerSecret ¶
func (d *DB) GetBrokerSecret(id string) (BrokerSecretRow, error)
GetBrokerSecret returns one secret by ID.
func (*DB) GetCIPipelineRule ¶ added in v0.0.2
func (d *DB) GetCIPipelineRule(id string) (CIPipelineRuleRow, error)
GetCIPipelineRule returns one rule by ID.
func (*DB) GetConfigBlob ¶
GetConfigBlob returns the YAML-serialised project config previously stored via SetConfigBlob. Returns ("", nil) when no blob has been written yet (fresh project, or DB created before this column existed).
func (*DB) GetEgressGrant ¶
func (d *DB) GetEgressGrant(id string) (EgressGrantRow, error)
GetEgressGrant returns one grant by ID.
func (*DB) GetEnrollmentToken ¶
func (d *DB) GetEnrollmentToken(id string) (EnrollmentTokenRow, error)
GetEnrollmentToken returns one token by ID, or ErrEnrollmentNotFound.
func (*DB) GetExecutor ¶
func (d *DB) GetExecutor(id string) (ExecutorRow, error)
GetExecutor returns one executor by ID, or ErrExecutorNotFound.
func (*DB) GetExecutorHandle ¶
func (d *DB) GetExecutorHandle(handleID string) (ExecutorHandleRow, error)
GetExecutorHandle returns one row, or ErrExecutorHandleNotFound.
func (*DB) GetExecutorHealth ¶
func (d *DB) GetExecutorHealth(executorID string) (ExecutorHealthRow, error)
GetExecutorHealth returns one health row, or ErrExecutorHealthNotFound.
"Never probed" is deliberately an error rather than a zero-valued ready row: a caller that cannot tell the two apart would treat an unobserved executor as healthy, which is the failure mode this table exists to prevent.
func (*DB) GetExecutorSession ¶
func (d *DB) GetExecutorSession(id string) (ExecutorSessionRow, error)
GetExecutorSession returns one session by ID, or ErrExecutorSessionNotFound.
func (*DB) GetGrantRequest ¶ added in v0.0.2
func (d *DB) GetGrantRequest(id string) (GrantRequestRow, error)
GetGrantRequest returns one request by ID.
func (*DB) GetHubLease ¶
func (d *DB) GetHubLease(scope string) (HubLeaseRow, error)
GetHubLease returns the row for scope, or ErrHubLeaseNotFound.
func (*DB) GetRoleBinding ¶ added in v0.0.2
func (d *DB) GetRoleBinding(id string) (RoleBindingRow, error)
GetRoleBinding returns one binding by id, or ErrRoleBindingNotFound.
func (*DB) GetSession ¶
func (d *DB) GetSession(id string) (SessionRow, error)
GetSession returns one session by id, or an error wrapping ErrSessionNotFound.
This sits on the authentication path, so the lookup is by primary key: a signed-in request must not cost a scan, and a scan would make request latency a function of how many people are logged in.
func (*DB) GrowthTableStats ¶ added in v0.0.2
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 ¶
HasProjectState reports whether any project state has ever been written to this database — that is, whether the metadata table holds a row.
It distinguishes a database that merely *exists* from one that holds a project. Opening a state.db creates the file and every table in it, so file existence alone says nothing: several code paths open a project's database for reasons unrelated to its state (executor reconciliation does it on every CLI invocation), and each of them leaves behind a complete but empty database. Callers that need "is there a project here" must ask this.
func (*DB) HubMeta ¶ added in v0.0.2
HubMeta reads a hub-scoped metadata value, reporting whether it was present.
The bool matters and callers must read it: an absent record and a record holding the empty string mean different things to every caller this has, and the one that exists today reads "no probe has run" versus "a probe wrote something unparsable" — which get different messages and different remedies.
func (*DB) LastMaintenanceLog ¶
func (d *DB) LastMaintenanceLog() (*MaintenanceLogEntry, error)
LastMaintenanceLog returns the most recent maintenance_log row. Returns (nil, nil) when no maintenance has been recorded yet — callers should treat that as "schedule a maintenance run on first invocation".
func (*DB) LatestAuditAnchor ¶
func (d *DB) LatestAuditAnchor() (AuditAnchor, error)
LatestAuditAnchor returns the most recent anchor, or ErrAuditAnchorNotFound when the trail has never been pruned.
"Most recent" is by pruned_through_id rather than by id: the anchor that governs the live table is the one that cut furthest into it, and ordering by the boundary rather than by insertion order means an out-of-order write cannot make a stale anchor authoritative.
func (*DB) ListAPITokens ¶
func (d *DB) ListAPITokens() ([]APITokenRow, error)
ListAPITokens returns every token, newest first. Revoked and expired rows are included: an operator auditing access needs to see what was withdrawn, and the caller decides how to present it.
func (*DB) ListAgentCredentials ¶
func (d *DB) ListAgentCredentials() ([]AgentCredentialRow, error)
ListAgentCredentials returns every enrolled agent, oldest first.
func (*DB) ListAuditAnchors ¶
func (d *DB) ListAuditAnchors() ([]AuditAnchor, error)
ListAuditAnchors returns every anchor in ascending boundary order. Used by `cloop hub audit anchors` and by the verifier's report so an operator can see the full retention history, not just the newest cut.
func (*DB) ListAuditEvents ¶
func (d *DB) ListAuditEvents(f AuditFilter) (rows []AuditEvent, total int, err error)
ListAuditEvents returns rows matching the filter, plus the unfiltered total count for paging UIs.
func (*DB) ListBrokerGrants ¶
func (d *DB) ListBrokerGrants() ([]BrokerGrantRow, error)
ListBrokerGrants returns every grant, including revoked and expired ones.
Filtering is the broker's job, not the store's: an audit UI listing "who had access" needs the rows a policy filter would drop, and a store that silently hid them would make that question unanswerable.
func (*DB) ListBrokerSecrets ¶
func (d *DB) ListBrokerSecrets() ([]BrokerSecretRow, error)
ListBrokerSecrets returns every stored secret, name-ordered.
func (*DB) ListBrokerSecretsNotUnderKey ¶
ListBrokerSecretsNotUnderKey returns secrets still sealed under some other key. Ordered by ID so a stuck row appears in the same position each round, which makes an operator's "it keeps failing on the same one" observation true rather than coincidental.
func (*DB) ListCIExchanges ¶ added in v0.0.2
func (d *DB) ListCIExchanges(limit int) ([]CIExchangeRow, error)
ListCIExchanges returns the most recent exchanges, newest first.
func (*DB) ListCIPipelineRules ¶ added in v0.0.2
func (d *DB) ListCIPipelineRules() ([]CIPipelineRuleRow, error)
ListCIPipelineRules returns every rule in evaluation order: oldest first, then by ID.
The order is the matching order, and it is stable across restarts on purpose. "First match wins" is only a meaningful rule if two hubs reading the same table agree about which rule is first.
func (*DB) ListEgressGrants ¶
func (d *DB) ListEgressGrants() ([]EgressGrantRow, error)
ListEgressGrants returns every grant, including revoked and expired ones.
Filtering is the broker's job for the same reason it is in ListBrokerGrants: an audit reader asking "who could reach the Internet last quarter" needs exactly the rows a policy filter would drop.
func (*DB) ListEnrollmentTokens ¶
func (d *DB) ListEnrollmentTokens() ([]EnrollmentTokenRow, error)
ListEnrollmentTokens returns every token, newest first.
func (*DB) ListEvents ¶
ListEvents returns events in reverse-chronological order (latest first). limit caps the page size; offset skips past the most recent N. total is the total number of events in the journal so callers can drive infinite- scroll UIs.
func (*DB) ListExecutorHandles ¶
func (d *DB) ListExecutorHandles(executorID string) ([]ExecutorHandleRow, error)
ListExecutorHandles returns rows owned by executorID, oldest first. Passing "" returns every row, which is what a cross-driver sweep wants.
Oldest-first matters: an orphan sweep applies a grace period to started_at, and processing in dispatch order means the first row it examines is the one most likely to have aged past it.
func (*DB) ListExecutorHealth ¶
func (d *DB) ListExecutorHealth() ([]ExecutorHealthRow, error)
ListExecutorHealth returns every health row ordered by executor ID.
func (*DB) ListExecutorResourceLimits ¶ added in v0.0.3
func (d *DB) ListExecutorResourceLimits() ([]ExecutorResourceLimit, error)
ListExecutorResourceLimits returns every recorded executor ceiling, newest first, for the admin-facing panel.
func (*DB) ListExecutorSandboxes ¶ added in v0.0.3
func (d *DB) ListExecutorSandboxes() ([]ExecutorSandbox, error)
ListExecutorSandboxes returns every recorded configuration, newest first, so the fleet view can render each card's containment in one query rather than one per executor.
func (*DB) ListExecutorSessions ¶
func (d *DB) ListExecutorSessions(executorID string, onlyRunning bool) ([]ExecutorSessionRow, error)
ListExecutorSessions returns sessions ordered by start time then ID. An empty executorID lists every executor's sessions; onlyRunning restricts the result to work that is still in flight, which is what failover and capacity checks ask for.
func (*DB) ListExecutors ¶
func (d *DB) ListExecutors() ([]ExecutorRow, error)
ListExecutors returns every persisted executor ordered by ID.
func (*DB) ListGrantRequestUses ¶ added in v0.0.2
func (d *DB) ListGrantRequestUses(requestID string) ([]GrantRequestUseRow, error)
ListGrantRequestUses returns the leases recorded against one request, or — when requestID is empty — every recorded use.
func (*DB) ListGrantRequests ¶ added in v0.0.2
func (d *DB) ListGrantRequests() ([]GrantRequestRow, error)
ListGrantRequests returns every request, newest first.
Unfiltered, for the same reason ListBrokerGrants is: deciding which requests a caller may see is an authorisation question, and a store that pre-filtered would make "show me everything that was ever asked for" — the review question — unanswerable.
func (*DB) ListProjectExecutorBindings ¶
func (d *DB) ListProjectExecutorBindings() ([]ProjectExecutorBinding, error)
ListProjectExecutorBindings returns every project→executor binding, ordered by project path.
func (*DB) ListProjectResourceLimits ¶ added in v0.0.2
func (d *DB) ListProjectResourceLimits() ([]ProjectResourceLimit, error)
ListProjectResourceLimits returns every recorded ceiling, newest first, for the operator-facing panel.
func (*DB) ListProviderCalls ¶
func (d *DB) ListProviderCalls(offset, limit, taskID int, provider string) (rows []ProviderCallRow, total int, err error)
ListProviderCalls returns up to limit rows ordered most-recent-first, optionally filtered. The summary form omits the heavy prompt/response/headers columns so the list endpoint stays cheap even when the table grows large.
Filters:
- taskID > 0: only calls bound to that task
- provider != "": only calls from that provider
Returns total = count of all matching rows (regardless of limit/offset) so the UI can render pagination controls.
func (*DB) ListQuotaCounters ¶
func (d *DB) ListQuotaCounters() ([]QuotaCounterRow, error)
ListQuotaCounters returns every counter row.
func (*DB) ListQuotaOverrides ¶
func (d *DB) ListQuotaOverrides() ([]QuotaOverrideRow, error)
ListQuotaOverrides returns every override, ordered by identity so the admin panel renders the same way on every load.
func (*DB) ListRoleBindings ¶ added in v0.0.2
func (d *DB) ListRoleBindings() ([]RoleBindingRow, error)
ListRoleBindings returns every binding.
Denies sort first so that a human reading the output — and the resolver's own scan — meets the rows that take away authority before the rows that grant it. Within an effect the order is stable by claim, value and id so repeated listings do not shuffle.
func (*DB) ListRotations ¶
func (d *DB) ListRotations(limit int) ([]RotationRow, error)
ListRotations returns rotation records, newest first.
func (*DB) ListSecretLeaseDirs ¶
func (d *DB) ListSecretLeaseDirs() ([]SecretLeaseDirRow, error)
ListSecretLeaseDirs returns every recorded directory, oldest first.
At startup this is precisely the orphan set: a hub that shut down cleanly deleted its rows as it wiped, so anything still here outlived the process that created it.
func (*DB) ListSessions ¶
func (d *DB) ListSessions() ([]SessionRow, error)
ListSessions returns every session, most recently active first.
func (*DB) ListSessionsNotUnderKey ¶
ListSessionsNotUnderKey returns sessions whose sealed refresh token is under some other key.
func (*DB) ListTelemetrySessions ¶ added in v0.0.2
func (d *DB) ListTelemetrySessions(source string, limit int) ([]TelemetrySession, error)
ListTelemetrySessions rolls events up by session, most recently active first.
func (*DB) LoadProviderCall ¶
func (d *DB) LoadProviderCall(id int64) (*ProviderCallRow, error)
LoadProviderCall returns the full row (including prompt, response, headers) for a single audit-log id. Returns sql.ErrNoRows-wrapped error when missing.
func (*DB) LoadSpendCursor ¶ added in v0.0.2
func (d *DB) LoadSpendCursor(projectPath string) (SpendCursor, bool, error)
LoadSpendCursor returns the cursor for projectPath, or ok=false when the hub has never dispatched a run for it.
ok=false is not an error: it is the normal state of a project the hub only reads, and of every project on a hub that has just been upgraded. The caller charges nothing until a dispatch opens a cursor, which is the safe direction — the alternative would be charging an identity the hub had to guess.
func (*DB) LoadStateLite ¶
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 ¶
LoadTask returns a single task by ID. Returns ErrTaskNotFound if the task does not exist. Useful from HTTP handlers where the typical 404 path is "the requested task does not exist".
func (*DB) LookupKill ¶
func (d *DB) LookupKill(taskID int) (KillRequest, bool, error)
LookupKill returns the pending kill for taskID and whether a row exists. Used by the orchestrator after a worker exits to recover the operator's chosen target_status. Returns (KillRequest{}, false, nil) when no row.
func (*DB) MaxAuditID ¶
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
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 ¶
MonthlyCosts returns cost entries for the given UTC year/month.
func (*DB) OffboardIdentity ¶ added in v0.0.2
func (d *DB) OffboardIdentity(w OffboardWrite) (OffboardApplied, error)
OffboardIdentity severs the listed sessions and tokens and writes the deny binding, in one transaction, chaining the caller's audit events into the same commit.
Ordering inside the transaction is deliberate: the deny binding is written first. Within a transaction the order is invisible to other readers, but it is not invisible to a crash-and-retry: if the commit is lost and the operator re-runs, the surface that matters most for a *future* sign-in is the one already expressed in the row they will re-write identically (the binding id is content-addressed, so re-writing is idempotent).
func (*DB) OpenExecutorSession ¶
func (d *DB) OpenExecutorSession(row ExecutorSessionRow) error
OpenExecutorSession records work starting on an executor.
This is a plain INSERT, not an upsert: a session id identifies one attempt, and silently overwriting an existing row would erase the claim token a supervisor may already be holding for the in-flight attempt.
func (*DB) OpenSpendCursor ¶ added in v0.0.2
OpenSpendCursor records who is paying for the run about to start and where its billing begins, replacing any previous cursor for the project.
startAt is the project ledger's current MAX(id) — everything already there belongs to an earlier run and must not be charged to this one. Passing it in rather than reading it here keeps this package free of any assumption about which database the cost rows live in: the control plane holds the cursor, the project holds the ledger, and they are different files.
func (*DB) PendingKills ¶
func (d *DB) PendingKills() ([]KillRequest, error)
PendingKills returns all currently-pending abort requests, oldest first. The orchestrator's poller calls this every fast tick.
func (*DB) PingContext ¶
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 ¶
PrimaryKEK returns the current primary, if any.
Separate from ListKEKs because it sits on the seal path: every seal re-reads it to confirm this process has not fallen behind a rotation run elsewhere, so it has to be one indexed lookup and not a scan.
func (*DB) ProjectExecutor ¶
ProjectExecutor returns the executor ID bound to projectPath. The boolean is false when the project has no binding, which callers treat as "use the registry default" — distinct from an error, which means the lookup itself failed and must not be interpreted as "unbound".
func (*DB) ProjectResourceCeiling ¶ added in v0.0.2
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 ¶
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
PruneCIExchanges keeps the newest keep rows and deletes the rest.
The exchange log is a debugging surface, not the audit trail, so it is bounded by count rather than by age: an operator debugging a pipeline wants the last N attempts, and a hub nobody has federated against in a month should still be able to show the exchange that failed.
func (*DB) PruneCosts ¶ added in v0.0.2
func (d *DB) PruneCosts(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)
PruneCosts bounds the cost ledger.
The ledger is mirrored to .cloop/costs.jsonl, so a pruned row is not the last record of the spend — which is why bounding it at all is defensible. Two readers still argue for a generous ceiling. `cloop cost report` aggregates over the whole table, so pruning shortens the history it can show; and the hub's spend enforcement tracks its booking position in project_spend_cursor.last_cost_row_id, reading forward from it with ReadCostsAfterID. Deleting the oldest rows cannot strand that cursor unless the hub has fallen more than MaxRows behind, which is a broken hub rather than a busy one — but it is the reason this prune takes the oldest rows and never the newest.
func (*DB) PruneEvents ¶ added in v0.0.2
func (d *DB) PruneEvents(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)
PruneEvents bounds the unified event history.
func (*DB) PruneProviderCalls ¶ added in v0.0.2
func (d *DB) PruneProviderCalls(p ProviderCallRetention, now time.Time, dryRun bool) (ProviderCallPruneResult, error)
PruneProviderCalls strips aged bodies and then bounds the rows.
The two limits are applied in that order and both are honoured: a row that is old enough to lose its bodies and also falls outside the row ceiling is deleted, and the bytes are reported once — the strip empties the columns before the delete measures what the row still held.
func (*DB) PruneQuotaCounters ¶
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
PruneTelemetry deletes events older than cutoff and returns how many went. The operator-invoked companion to the automatic row ceiling: `cloop hub telemetry prune` for when a trail should go now rather than when it ages out.
The age half of the same policy now also runs unattended — see PruneTelemetryRows and the janitor's telemetry step (Task 20291). Both go through rowretention.go's id-bounded path rather than comparing the RFC3339Nano text column directly, because a string comparison on that column is only an ordering if every writer used identical UTC formatting, and a row whose timestamp will not parse must stop the prune rather than be swept into it.
func (*DB) PruneTelemetryRows ¶ added in v0.0.2
func (d *DB) PruneTelemetryRows(p RowRetention, now time.Time, dryRun bool) (RowPruneResult, error)
PruneTelemetryRows applies age and count retention to the browser telemetry trail.
trimTelemetryTx already caps the table at TelemetryMaxRows on the write path, because a public ingest endpoint must not be able to fill a disk between two janitor passes. This is the age half of the policy, which no write-time cap can express: a debugging trail that has gone quiet should still age out.
func (*DB) PutAPIToken ¶
func (d *DB) PutAPIToken(row APITokenRow) error
PutAPIToken inserts a token row. It is an insert, not an upsert: a token's secret, roles, and scope are fixed at mint time, and silently rewriting an existing row would turn "re-run the create call" into an undetectable privilege change on a credential already in circulation. Rotating means minting a new token and revoking the old one.
func (*DB) PutAgentCredential ¶
func (d *DB) PutAgentCredential(row AgentCredentialRow) error
PutAgentCredential inserts or updates an agent credential row.
func (*DB) PutBrokerGrant ¶
func (d *DB) PutBrokerGrant(row BrokerGrantRow) error
PutBrokerGrant inserts or replaces a grant.
func (*DB) PutBrokerSecret ¶
func (d *DB) PutBrokerSecret(row BrokerSecretRow) error
PutBrokerSecret inserts or replaces a secret.
The unique index on name is what makes a duplicate a storage error rather than a silently-shadowed second row, so a name collision surfaces here even if two processes race past the broker's own check.
func (*DB) PutCIExchange ¶ added in v0.0.2
func (d *DB) PutCIExchange(row CIExchangeRow) error
PutCIExchange records a federation attempt.
func (*DB) PutCIPipelineRule ¶ added in v0.0.2
func (d *DB) PutCIPipelineRule(row CIPipelineRuleRow) error
PutCIPipelineRule inserts or replaces a rule.
Unlike PutAPIToken this is an upsert, because a rule is a policy an operator edits rather than a credential in circulation: rewriting one is the intended operation, and the row carries no secret whose replacement could go unnoticed. What editing a rule must *also* do — revoke the sessions already minted under it — is the caller's job, because this layer does not know the live registry exists.
func (*DB) PutEgressGrant ¶
func (d *DB) PutEgressGrant(row EgressGrantRow) error
PutEgressGrant inserts or replaces an egress grant.
func (*DB) PutEnrollmentToken ¶
func (d *DB) PutEnrollmentToken(row EnrollmentTokenRow) error
PutEnrollmentToken inserts or updates a token row.
func (*DB) PutExecutorHandle ¶
func (d *DB) PutExecutorHandle(row ExecutorHandleRow) error
PutExecutorHandle inserts or replaces the row for row.HandleID.
Unlike OpenExecutorSession this *is* an upsert. A driver may re-record a handle it already owns — rehydration re-writes rows so updated_at reflects the adopting control plane, and a Start that retries against the same handle id must not fail on a duplicate key. There is no claim token here to clobber: the row carries identity, not ownership.
func (*DB) PutExecutorHealth ¶
func (d *DB) PutExecutorHealth(row ExecutorHealthRow) error
PutExecutorHealth inserts or updates the health row for row.ExecutorID.
The row is written wholesale rather than merged field-by-field: the prober computes the complete verdict (state, reason, failure count, transition time) in one place, and a partial update would let a stale field outlive the observation that produced it.
func (*DB) PutGrantRequest ¶ added in v0.0.2
func (d *DB) PutGrantRequest(row GrantRequestRow) error
PutGrantRequest inserts or replaces a request by ID.
func (*DB) PutGrantRequestUse ¶ added in v0.0.2
func (d *DB) PutGrantRequestUse(row GrantRequestUseRow) error
PutGrantRequestUse records that a lease redeemed an approved request's grant.
Upsert on (request_id, lease_id): a lease is leased once and then renewed every few minutes, and each renewal should move last_seen rather than add a row. first_seen is preserved by the conflict clause for the same reason RevokeBrokerGrant refuses to move a revocation timestamp — when a credential first came into use is a fact, and a renewal must not rewrite it.
func (*DB) PutKEK ¶
PutKEK inserts or replaces a KEK record.
The WHERE clause on the upsert makes retirement terminal. Without it a PutKEK against a retired id would restore its salt and state, un-doing a crypto-shred with an ordinary write — the one operation in this file that is supposed to be irreversible.
func (*DB) PutQuotaCounter ¶
func (d *DB) PutQuotaCounter(row QuotaCounterRow) error
PutQuotaCounter writes one counter's current value. A value at or below zero deletes the row instead of storing a zero: an identity holding nothing should not keep a row per resource forever, and a row that says 0 and a missing row must not be two different things the reader has to reconcile.
func (*DB) PutQuotaOverride ¶
func (d *DB) PutQuotaOverride(row QuotaOverrideRow) error
PutQuotaOverride inserts or replaces one identity's override.
func (*DB) PutRoleBinding ¶ added in v0.0.2
func (d *DB) PutRoleBinding(row RoleBindingRow) (RoleBindingRow, error)
PutRoleBinding inserts or replaces a binding, returning the row as stored.
The caller supplies already-normalized claim and value: normalization is pkg/authz's definition of when two bindings are the same one, and a second implementation here would silently disagree with it at exactly the moment an operator is trying to override a configured binding.
func (*DB) PutRotation ¶
func (d *DB) PutRotation(row RotationRow) error
PutRotation inserts or updates a rotation record.
func (*DB) PutSecretLeaseDir ¶
func (d *DB) PutSecretLeaseDir(row SecretLeaseDirRow) error
PutSecretLeaseDir records the intent to materialise a lease at row.Dir.
Call it before anything writes plaintext there. The ordering is the whole point of the table: a row without a directory is harmless and self-clearing, a directory without a row is an orphan nothing can attribute.
func (*DB) PutSession ¶
func (d *DB) PutSession(row SessionRow) error
PutSession inserts a session row.
An insert rather than an upsert: the id is a digest of 256 random bits, so a collision is not a case worth writing code for, while silently overwriting one would replace a live session's subject and roles with another's.
func (*DB) QueryTelemetry ¶ added in v0.0.2
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) ReadCostsAfterID ¶ added in v0.0.2
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 ¶
ReadCostsSince returns cost entries with timestamp >= since, ordered ascending.
func (*DB) ReconstructTask ¶ added in v0.0.2
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 ¶
RecordEvent appends one row to the events journal. Best-effort: the caller is expected to ignore the returned error (event-recording must never block the orchestrator). Idempotency is NOT guaranteed — duplicate calls record duplicate rows. The auto-increment id ordering is the source of truth for "what happened first" on a single host.
func (*DB) RedeemEnrollmentToken ¶
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 ¶
ReleaseHubLease gives up a lease held by instanceID, leaving the row in place with released_at set.
Scoped to instanceID so a late release from a hub that already lost the lease cannot free the lease its successor now holds — the same fencing discipline as RenewHubLease, applied to the path most likely to run last.
func (*DB) RemoveExecutorAudience ¶ added in v0.0.3
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 ¶
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 ¶
ReplaceBrokerSecretSealed swaps a secret's envelope under compare-and-swap.
The WHERE clause matches the old key ID *and the exact ciphertext*, not just the ID. Matching the key alone would be a lost-update bug with teeth: a rotation that read a row, and an operator who re-minted that credential while the rotation was mid-flight, would race — and the rotation would win, silently restoring the superseded credential under a fresh wrap, with every column looking correct afterwards. Comparing the bytes turns that race into a no-op the rotator counts as skipped.
func (*DB) ReplaceQuotaGauges ¶
func (d *DB) ReplaceQuotaGauges(rows []QuotaCounterRow) error
ReplaceQuotaGauges swaps every gauge row (bucket = ”) for rows in one transaction.
A transaction rather than delete-then-insert because the intermediate state — no gauge counters at all — is a state in which every tenant has unlimited headroom. On a hub reconciling at startup that window is short, but it is exactly the window an attacker watching for a restart would aim at, and making it atomic costs nothing.
func (*DB) ReplaceSessionSealed ¶
ReplaceSessionSealed swaps a session's sealed refresh token under compare-and-swap, with the same reasoning as above: a session that refreshed its token mid-rotation must not have the previous token restored.
func (*DB) RequestKill ¶
func (d *DB) RequestKill(req KillRequest) error
RequestKill upserts one abort request. Repeated calls for the same task ID overwrite target_status / requested_at — the latest request wins. Returns nil when the row was written.
func (*DB) RetireKEK ¶
RetireKEK marks a KEK retired and destroys its salt.
The reference check runs inside the retiring transaction, which is the difference between a guarantee and a race: checking first and retiring after would let a lease minted in between end up sealed under a key whose salt is already gone, and that material would be unrecoverable with no error anywhere to explain it.
Blanking the salt is the point of retirement. A state flag alone could be ignored by a future code path or flipped back by a hand-edited database; without the salt the key cannot be derived from the passphrase at all, so retirement is a property of the cryptography rather than of the control flow.
func (*DB) RevokeAPIToken ¶
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 ¶
RevokeAgentCredential marks an agent's credential revoked.
func (*DB) RevokeBrokerGrant ¶
RevokeBrokerGrant stamps a grant revoked.
The UPDATE is conditional on revoked_at being empty so a second revoke cannot move the timestamp forward. That matters for the audit trail: the moment access was withdrawn is a fact, and a retry must not rewrite it. Revoking an already-revoked grant is reported as success, because the caller's desired end state holds.
func (*DB) RevokeEgressGrant ¶
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 ¶
RevokeEnrollmentToken marks a token revoked, redeemed or not.
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 ¶
SessionsDueForRefresh returns sessions whose IdP check is older than cutoff and that still hold a sealed refresh token, oldest first and at most limit rows.
The limit bounds one pass: a hub where every session comes due at once must spread the token-endpoint calls over several ticks rather than opening a thousand connections to the IdP in one burst, which the IdP would rightly treat as an attack.
func (*DB) SetBrokerMeta ¶
SetBrokerMeta writes a broker-scoped metadata value.
func (*DB) SetConfigBlob ¶
SetConfigBlob persists the YAML-serialised project config into the metadata table. The write is wrapped in a transaction so a crash mid-write leaves either the previous value or the new one — never a partial row.
func (*DB) SetExecutorResourceLimit ¶ added in v0.0.3
func (d *DB) SetExecutorResourceLimit(executorID string, c executor.ResourceCeiling, setBy string) error
SetExecutorResourceLimit records a ceiling for executorID, replacing any previous one. setBy is the identity that set it, for the audit trail.
A zero ceiling is stored rather than rejected, for the reason its project counterpart is: "considered and capped at nothing" and "never considered" are the same to the dispatch path and different to the admin reading the panel. Callers that mean "remove the policy" call ClearExecutorResourceLimit.
func (*DB) SetExecutorSandbox ¶ added in v0.0.3
SetExecutorSandbox records sandbox settings for executorID, replacing any previous ones. setBy is the identity that set them, for the audit trail.
Settings that configure nothing are stored rather than rejected, for the reason SetProjectResourceLimit stores a zero ceiling: "no row" and "a row that asserts nothing" mean the same thing at dispatch and different things to the admin reading the panel, who wants to see that someone looked at this executor. Callers meaning "remove the policy" call ClearExecutorSandbox.
func (*DB) SetHubMeta ¶ added in v0.0.2
SetHubMeta writes a hub-scoped metadata value, replacing any previous one.
func (*DB) SetProjectResourceLimit ¶ added in v0.0.2
func (d *DB) SetProjectResourceLimit(projectPath string, c executor.ResourceCeiling, setBy string) error
SetProjectResourceLimit records a ceiling for projectPath, replacing any previous one. setBy is the identity that set it, for the audit trail.
A zero ceiling is stored rather than rejected: "capped at nothing on every resource" and "no row" mean the same thing to the dispatch path, but they mean different things to the operator reading the panel, who wants to see that someone considered this project and decided it needed no cap. Callers that mean "remove the policy" call ClearProjectResourceLimit.
func (*DB) SizeStats ¶
SizeStats reads PRAGMA page_count, page_size, and freelist_count.
Connection-scoped pragmas — modernc.org/sqlite returns one row per query. We hold d.mu so the three reads stay consistent against concurrent writers in this process; concurrent writers in *other* processes can still extend the file between the page_count and freelist_count reads, but the resulting values remain individually valid (worst case: a slightly stale Bytes total).
func (*DB) SpendByIdentity ¶ added in v0.0.2
func (d *DB) SpendByIdentity(from, to time.Time) ([]IdentitySpend, error)
SpendByIdentity aggregates costs per identity over [from, to).
A zero `from` or `to` leaves that side of the window open, so the caller can ask for "today", "since Monday" or "everything" through one query. Rows with no identity aggregate under the empty string: they are reported as unattributed rather than dropped, because spend that happened is spend that happened and a total that silently omits it would not reconcile against the project ledger.
func (*DB) TaskRunID ¶ added in v0.0.2
TaskRunID reports the execution a task's latest attempt was dispatched into, and whether one was recorded.
Exported for the reconstruction in `cloop audit-log --task`, which needs the run id to pull the lease rows that belong to the same execution.
func (*DB) TouchAPIToken ¶
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 ¶
TouchAgentCredential records a successful authentication.
func (*DB) TouchCIPipelineRule ¶ added in v0.0.2
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 ¶
TouchExecutorHeartbeat records liveness for an enrolled executor and updates its status. Returns ErrExecutorNotFound when the ID is unknown, so a stale agent cannot resurrect a de-enrolled record by heartbeating.
func (*DB) TouchSession ¶
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 ¶
UnbindProjectExecutor removes the binding for projectPath. Unbinding a project that was never bound is not an error.
func (*DB) UpdateSessionRefresh ¶
func (d *DB) UpdateSessionRefresh(id string, up SessionRefreshUpdate) error
UpdateSessionRefresh stores a rotated refresh token, stamps the check time, and — when the provider re-asserted them — replaces the row's claims. A nil sealed value clears the stored token, which is how a session that can no longer be revalidated stops being retried.
Clearing also blanks the key id. Leaving a stale one behind would make the row look, to the rotator and to `cloop hub key retire`, like material still sealed under a key that in fact protects nothing — which is exactly the kind of phantom reference that makes an operator conclude retirement is broken.
One statement, not two. The stamp records that the session was checked and the claims record what it was checked *as*; a row carrying the first without the second is a session that looks freshly re-authorized while still holding authority the IdP has withdrawn.
func (*DB) UpsertExecutor ¶
func (d *DB) UpsertExecutor(row ExecutorRow) error
UpsertExecutor inserts or updates an executor row keyed by ID. CreatedAt is preserved on update: re-enrolling an agent must not rewrite its enrollment date.
func (*DB) Vacuum ¶
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 ¶
VacuumInto produces a transactionally-consistent copy of the database at outPath using SQLite's `VACUUM INTO 'path'` statement. The output is a single self-contained file (no WAL/SHM sidecars) and is the recommended hot-backup primitive when the C-level Online Backup API is not available.
outPath must not already exist — VACUUM INTO refuses to overwrite. The caller is responsible for clearing any stale destination first.
The path is interpolated into a SQL string literal because VACUUM INTO does not accept bound parameters. Single quotes in the path are escaped by doubling, matching SQLite's quoting rules.
func (*DB) VerifyAuditChain ¶
func (d *DB) VerifyAuditChain() (AuditVerifyReport, error)
VerifyAuditChain walks audit_events in id order and recomputes each row's hash, comparing to the stored value. The first mismatch (or gap, or missing prev_hash linkage) stops the walk and is reported. Verification is read-only and holds the DB mutex only during the row scan.
A pruned chain (Task 20218) does not start at genesis, and the naive walk would report the first survivor as tampered. When an anchor exists the walk starts from it instead: the first row must be exactly the id the anchor pinned, and its prev_hash must be the row_hash of the last row the anchor removed. That is a stronger check than "tolerate a short chain" — it means a second, undeclared truncation just after the boundary is still caught, because the anchor says where the chain has to resume.
What this cannot do is prove the pruned prefix itself was honest; nothing inside the database can, since an attacker with write access could author both the truncation and the anchor. That is what the export digest is for, and VerifyAuditExports checks it.
func (*DB) WALCheckpointTruncate ¶
WALCheckpointTruncate flushes the WAL into the main database file and truncates the WAL to zero bytes. Used as the first step of a hot backup (Task 20115) so VACUUM INTO produces a snapshot that includes every committed transaction up to the moment of the backup, not just whatever had previously been folded into the main file.
PRAGMA wal_checkpoint returns three columns: busy (0 if checkpoint completed without contention), log_size (frames in the WAL before the checkpoint), and checkpointed (frames moved to the main DB). The string returned summarises those values for logging; a non-zero busy column is not an error per se — under heavy concurrent load the checkpoint can only flush as many frames as it could acquire — but it is reported so an operator can decide whether to retry.
type EgressGrantRow ¶
type EgressGrantRow struct {
ID string
Scope string
SubjectType string
SubjectValue string
PolicyJSON string
ExpiresAt string
CreatedAt string
CreatedBy string
RevokedAt string
}
EgressGrantRow is one row of egress_grants. Times are RFC3339 strings, matching the rest of the schema.
type EnrollmentTokenRow ¶
type EnrollmentTokenRow struct {
ID string
Name string
// SecretHash is hex SHA-256 of the token secret, never the secret.
SecretHash string
WorkDirRoot string
Labels map[string]string
CreatedAt time.Time
ExpiresAt time.Time
CreatedBy string
RedeemedAt time.Time
RedeemedAgentID string
RevokedAt time.Time
}
EnrollmentTokenRow is one minted enrollment token.
type EventRow ¶
type EventRow struct {
ID int64 // primary key, assigned on insert (zero on the way in)
Timestamp time.Time // when the event happened
Type EventType
TaskID int // 0 when not task-bound
TaskTitle string // empty when not task-bound
Step int // NoStep (-1) when not step-bound; 0 is a real step
Message string // short, human-readable summary
Details string // free-form JSON blob (may be empty)
}
EventRow is one row in the events table.
type EventType ¶
type EventType string
EventType is a string enum identifying the kind of journal entry. Readers must treat unknown values as benign — the table is intentionally extensible.
const ( EventSessionStarted EventType = "session_started" EventSessionPaused EventType = "session_paused" EventSessionFailed EventType = "session_failed" EventPlanComplete EventType = "plan_complete" EventTaskStarted EventType = "task_started" EventTaskDone EventType = "task_done" EventTaskFailed EventType = "task_failed" EventTaskSkipped EventType = "task_skipped" EventTaskHeal EventType = "task_heal" EventTaskKilled EventType = "task_killed" EventTaskAdded EventType = "task_added" EventTaskAddedExternal EventType = "task_added_external" EventTaskDeleted EventType = "task_deleted" EventTaskStatusChange EventType = "task_status_change" EventEvolveRoundStart EventType = "evolve_round_start" EventEvolveDiscovered EventType = "evolve_discovered" EventEvolveNoOp EventType = "evolve_no_op" // EventWriteBack records what an isolated executor returned of a task's // work: the branch, the commit, and whether it merged. // // Its own type rather than a field on task_done, because the two answer // different questions and are allowed to disagree. A task can succeed and // have its work fail to come back — the harness ran, the push did not — // and a journal that folded the second into the first would have no way to // show an operator the run whose transcript was right and whose code was // lost. Making exactly that case visible is why this subsystem exists. EventWriteBack EventType = "write_back" // EventTaskBackground records work an agent harness left running after it // reported the task complete (Task 20205), and how that resolved. // // Its own type rather than a detail on task_done because it explains the // two things a status alone cannot: why a task sat running long after its // agent had stopped talking, and why a task whose output ended in // TASK_DONE was nonetheless not accepted as done. EventTaskBackground EventType = "task_background" // EventTaskAborted records a run that never produced work: a provider // usage limit or quota, a rejected credential, a harness that refused to // start, or output with no diff and no artifact behind it (Task 20211). // // Its own type rather than task_failed because the two are different // claims and are acted on differently. A failure is a judgement about the // work; an abort is the absence of any work to judge, so the task returns // to pending instead of staying terminal. Folding them together is what // let fourteen tasks be recorded as done whose entire stored summary was // "You've hit your limit" — and let auto-evolve plan follow-up work on // the belief that they had shipped. EventTaskAborted EventType = "task_aborted" // EventResourceCeiling records that an operator's resource ceiling lowered // what a workload was given (Task 20301). // // It is on the project's own journal rather than only in the hub's log // because the person it concerns is the developer whose sandbox got less // memory than its .cloop/sandbox.yaml asked for. To them an unannounced // clamp is indistinguishable from a slow machine, and there is nothing in // their repository that would explain it — the ceiling lives on the hub, // deliberately out of their reach. One row naming the resource, both // numbers and which ceiling bound it is the difference between a mystery // and an address to complain to. EventResourceCeiling EventType = "resource_ceiling" // EventProjectSeed records that a run dispatched to an isolating executor // went without the project state the hub would normally send with it // (Task 20316). // // On the project's journal for the same reason as the ceiling above: the // consequence lands on the developer and nothing in their repository // explains it. A sandbox that receives no seed finds no `.cloop/` in the // tree it cloned, so `cloop run` exits with "no cloop project found" — // which reads as a broken project rather than as an executor too old to // have been sent one. One row naming the executor and the upgrade is the // difference between that and an hour spent debugging an intact project. EventProjectSeed EventType = "project_seed" // EventProjectResult records what a run on an isolating executor sent back // of the project — which tasks it finished, what it added, what the hub // kept of its own instead — or why nothing came back (Task 20339). // // It is project_seed's other half. The seed is how the plan reaches a // device that cannot read the hub's database; this is how the run's // outcome returns to the database the dashboard renders. Without a row // here, a remote run that did its work and a remote run whose results were // lost look the same afterwards: a transcript ending "All tasks complete" // beside a plan whose tasks are all still pending. EventProjectResult EventType = "project_result" // EventFeatureCreated and EventFeatureRemoved record, on a project's own // journal, a feature of it being created or removed (Task 20341). They are // the parent's rows because the feature's own journal begins with its // creation and ends — deleted along with its worktree — at its removal. EventFeatureCreated EventType = "feature_created" EventFeatureRemoved EventType = "feature_removed" // EventFeaturePR records a feature's pull request being opened or updated, // on the feature's journal and its parent's. EventFeaturePR EventType = "feature_pr" )
type ExecutorAudienceEntry ¶ added in v0.0.3
type ExecutorAudienceEntry struct {
ExecutorID string `json:"executor_id"`
Kind string `json:"kind"` // "user" | "group" | "role"
Value string `json:"value"` // as the admin typed it
AddedAt time.Time `json:"added_at,omitempty"`
AddedBy string `json:"added_by,omitempty"`
}
ExecutorAudienceEntry is one principal admitted to one executor, plus its provenance.
func (ExecutorAudienceEntry) Member ¶ added in v0.0.3
func (e ExecutorAudienceEntry) Member() authz.AudienceMember
Member renders the entry as the claim predicate pkg/authz matches on.
type ExecutorAuditInput ¶
type ExecutorAuditInput struct {
// Action is the lifecycle verb: "enroll", "revoke", "cordon",
// "uncordon", "drain", "bind", "unbind". It becomes the suffix of the
// event type, so the whole family is filterable as event_type LIKE
// 'executor.%' and each verb individually by exact match.
Action string
// ExecutorID is the fleet identifier the action targeted. Empty is
// legitimate for "unbind", which names a project rather than a device.
ExecutorID string
// Actor is the acting identity, normally the OIDC subject label.
Actor string
// Detail is optional context: the device name, the cordon reason, the
// project path a binding points at. Never credentials — the enrollment
// token in particular must not appear here, and the central redaction
// in MarshalAuditPayload enforces that even if a caller tries.
Detail map[string]any
}
ExecutorAuditInput carries one executor-fleet decision to the audit log (Task 20167).
Executor lifecycle was the gap in the trail: enrolling a device grants an arbitrary machine the right to run project workloads and to request brokered credentials, and revoking, cordoning, draining, or re-binding one changes where a project's code executes. Those are exactly the events an operator needs to reconstruct — "who attached this machine, and when" — and none of them were recorded before.
type ExecutorHandleRow ¶
type ExecutorHandleRow struct {
HandleID string
ExecutorID string
Driver string
// ExternalID is the name the runtime knows the workload by. Its shape is
// driver-specific and nothing outside the owning driver interprets it.
ExternalID string
ProjectPath string
TaskID int
PID int
Image string
// MetaJSON is a marshalled map[string]string of driver-specific extras.
// ” and '{}' both mean "none"; readers must tolerate both because the
// column default is '{}' and a caller may write ”.
MetaJSON string
// SecretsJSON is a marshalled array of the workload's secret-lease
// bindings — lease ids, variable names and paths, never values. This
// package stores it as opaque text; pkg/executorstore owns the type.
//
// Unlike MetaJSON, ” and '[]' are *not* the same. ” means nobody
// recorded what this workload held (a row from before 0031), and '[]'
// means it held nothing. Collapsing them would let a revocation read an
// unrecorded holder as a non-holder and report success; see 0031.
SecretsJSON string
StartedAt time.Time
// Deadline is when the workload's timeout expires; the zero time means
// unbounded. Absolute, so a hub that was down does not restart the clock.
Deadline time.Time
UpdatedAt time.Time
}
ExecutorHandleRow is the durable identity of one dispatched workload.
type ExecutorHealthRow ¶
type ExecutorHealthRow struct {
ExecutorID string
State string
Reason string
ConsecutiveFailures int
LastSeen time.Time
LastProbe time.Time
StateChangedAt time.Time
}
ExecutorHealthRow is the scheduler's probe-driven view of one executor. ExecutorID shares the identifier space with ExecutorRow.ID but is not a foreign key — see the file comment.
type ExecutorInventory ¶
type ExecutorInventory struct {
// AgentVersion is the cloop build the device reported at its last connect.
// Empty means it reported none; the literal "1" means the placeholder that
// pre-inventory agents sent. Both are "unknown", but only the second tells
// an operator the device is definitely old.
AgentVersion string `json:"agent_version,omitempty"`
// OS and Arch are the device's GOOS/GOARCH.
OS string `json:"os,omitempty"`
Arch string `json:"arch,omitempty"`
// CPUs and MemoryMB are logical cores and total memory. Zero means the
// agent could not detect it, which is never "too small".
CPUs int `json:"cpus,omitempty"`
MemoryMB int `json:"memory_mb,omitempty"`
// Harnesses are the agent CLIs on the device's PATH ("claude", "codex").
Harnesses []string `json:"harnesses,omitempty"`
// ContainerRuntimes are the container runtimes it can drive.
ContainerRuntimes []string `json:"container_runtimes,omitempty"`
// WorkDirRoot is the directory the agent confines every workload beneath.
WorkDirRoot string `json:"workdir_root,omitempty"`
}
ExecutorInventory is what an edge device reported about itself, parsed out of its capability advertisement into typed, individually-queryable fields.
It is a nested struct rather than eight more fields on ExecutorRow because these travel together: they are refreshed as a set on every connect, they are all empty for a non-enrolled backend, and grouping them keeps "is this row's device inventory known" a single question.
Capabilities on ExecutorRow still holds the full advertisement. This is not a replacement for it but a projection of the parts that have to be selectable — see migration 0030 for why both exist.
func (ExecutorInventory) Known ¶
func (i ExecutorInventory) Known() bool
Known reports whether anything was actually advertised. Used to decide between rendering an inventory and rendering nothing at all — a panel that showed "0 CPUs, no harnesses" for a device that never reported would be inventing a fault.
type ExecutorResourceLimit ¶ added in v0.0.3
type ExecutorResourceLimit struct {
ExecutorID string `json:"executor_id"`
Ceiling executor.ResourceCeiling `json:"ceiling"`
SetAt time.Time `json:"set_at,omitempty"`
SetBy string `json:"set_by,omitempty"`
}
ExecutorResourceLimit is one executor's ceiling plus its provenance.
type ExecutorRow ¶
type ExecutorRow struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
Endpoint string `json:"endpoint,omitempty"`
Status string `json:"status"`
Capabilities json.RawMessage `json:"capabilities,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
LastHeartbeat time.Time `json:"last_heartbeat,omitempty"`
CreatedAt time.Time `json:"created_at"`
EnrolledBy string `json:"enrolled_by,omitempty"`
// Inventory is the parsed, queryable subset of what a device advertised.
// Zero for backends that were configured rather than enrolled: a container
// driver has no build version of its own, it runs this one.
Inventory ExecutorInventory `json:"inventory,omitzero"`
}
ExecutorRow is one persisted execution backend.
type ExecutorSandbox ¶ added in v0.0.3
type ExecutorSandbox struct {
ExecutorID string `json:"executor_id"`
Settings executor.SandboxSettings `json:"settings"`
SetAt time.Time `json:"set_at,omitempty"`
SetBy string `json:"set_by,omitempty"`
}
ExecutorSandbox is one executor's sandbox configuration plus its provenance.
Settings is the part that travels to the device in a start frame; SetAt and SetBy never leave the control plane. See executor.SandboxSettings for why that split is in the types rather than in a convention.
type ExecutorSessionRow ¶
type ExecutorSessionRow struct {
ID string
ExecutorID string
HandleID string
ProjectPath string
TaskID int
// ClaimToken gates the requeue latch. It is a concurrency token, not a
// secret: it is stored and compared in plaintext because it authorises
// nothing beyond winning one race.
ClaimToken string
State string
Attempt int
StartedAt time.Time
UpdatedAt time.Time
EndedAt time.Time
RequeuedFrom string
// SpecJSON is the marshalled executor.Spec the session was dispatched
// with. Failover re-dispatches from it, so it must survive a
// control-plane restart: a session whose spec lived only in the memory
// of the process that started it could not be requeued by the process
// that replaces it, which is exactly when requeueing matters most.
SpecJSON string
}
ExecutorSessionRow is one unit of work in flight on an executor.
type GrantRequestRow ¶ added in v0.0.2
type GrantRequestRow struct {
ID string
RequestedBy string
SecretID string
SecretName string
Kind string
SubjectType string
SubjectValue string
Scope string
ConstraintsJSON string
TTLSeconds int64
Justification string
State string
CreatedAt string
ExpiresAt string
DecidedBy string
DecidedAt string
DecisionNote string
GrantID string
GrantExpiresAt string
}
GrantRequestRow is one row of secret_grant_requests. Times are RFC3339 strings and durations are whole seconds, matching the rest of the schema.
type GrantRequestUseRow ¶ added in v0.0.2
type GrantRequestUseRow struct {
RequestID string
LeaseID string
GrantID string
ExecutorID string
ProjectPath string
// TaskID is the unit of work that held the lease, or 0 when the dispatch
// was run-scoped rather than task-scoped. See the migration's note: 0 means
// "not attributed", never "no task ran".
TaskID int
FirstSeen string
LastSeen string
}
GrantRequestUseRow is one row of secret_grant_request_uses: one lease that redeemed the grant an approval produced.
type HubLeaseRow ¶
type HubLeaseRow struct {
Scope string
InstanceID string
Hostname string
PID int
// BootID disambiguates a pid across reboots; ” when the platform does
// not expose one. A liveness probe must not trust PID unless this matches
// the probing host's current boot id.
BootID string
Address string
Version string
AcquiredAt time.Time
HeartbeatAt time.Time
// ReleasedAt is the instant of a graceful release; the zero time means the
// lease is still held (or was abandoned without one).
ReleasedAt time.Time
}
HubLeaseRow is one fence row: who holds the control plane, and since when.
func (HubLeaseRow) Held ¶
func (r HubLeaseRow) Held() bool
Held reports whether the row represents a lease nobody has given up. It says nothing about whether the holder is alive — that is HeartbeatAt's job, and deciding it is pkg/hublease's.
type IdentitySpend ¶ added in v0.0.2
type IdentitySpend struct {
Identity string
InputTokens int
OutputTokens int
ThinkingTokens int
EstimatedUSD float64
Entries int
}
IdentitySpend is one identity's total spend over a window.
type ImagePolicyDenialInput ¶
type ImagePolicyDenialInput struct {
// ProjectPath identifies the project whose .cloop/sandbox.yaml was
// refused. It is the entity the row is filed under.
ProjectPath string
// Image is the reference as the project wrote it, unmodified — the
// normalized form would hide exactly the homograph or confusion attempt
// that is the reason to look.
Image string
// Rule is the imagepolicy rule that refused it ("registry-allowlist",
// "digest-required", …). Kept as the same string the API and the error
// text use, so filtering the trail and reading a bug report agree.
Rule string
// Reason is the human-readable observation.
Reason string
// Registry and Repository are the parsed components, empty when the
// reference did not parse. They are what makes "show me every denial for
// this registry" a query rather than a grep.
Registry string
Repository string
// Actor is the acting identity, normally the OIDC subject label.
Actor string
}
ImagePolicyDenialInput carries one refused container image to the audit log (Task 20177).
This is the one sandbox decision worth a permanent row. The others describe a project asking for less than it could have; this one describes a project asking to execute code from somewhere the operator does not trust, and the question it has to answer later is "was this a typo in one repo, or did the same unknown registry get named across six projects in an hour". Only a durable, queryable trail answers the second.
type KEKRow ¶
type KEKRow struct {
ID string
Salt string
State string
CheckValue []byte
CreatedAt string
CreatedBy string
RetiredAt string
}
KEKRow is one row of broker_keks.
type KillRequest ¶
type KillRequest struct {
TaskID int
TargetStatus string // empty when the operator did not pick a final status
RequestedAt time.Time
RequestedBy string // free-form (UI client ID, "ui", etc.) — informational only
// Attempt identifies the execution being aborted: the RFC3339Nano
// started_at the requester observed on the task. The orchestrator honours
// the row only while the task's started_at still matches, so a request
// that outlives its run cannot cancel a later attempt of the same task.
// Empty means "no attempt named" and is treated as stale (Task 20203).
Attempt string
}
KillRequest is one pending manual-abort request keyed by task ID.
type MaintenanceLogEntry ¶
type MaintenanceLogEntry struct {
ID int64
Operation string
StartedAt time.Time
CompletedAt time.Time
PageCountBefore int64
PageCountAfter int64
PageSize int64
BytesBefore int64
BytesAfter int64
Note string
}
MaintenanceLogEntry mirrors one row of the maintenance_log table.
ID is auto-assigned by SQLite on insert. Operation is one of "vacuum", "analyze", or "vacuum+analyze" (we record the combined run as a single row). Note carries free-form context — typically the auto-mode reasoning string.
func (MaintenanceLogEntry) BytesFreed ¶
func (e MaintenanceLogEntry) BytesFreed() int64
BytesFreed returns BytesBefore - BytesAfter (zero when the file did not shrink, e.g. because freelist was already empty).
type MigrateOptions ¶
type MigrateOptions struct {
// Target stops after the migration numbered Target. Zero means
// LatestVersion — every embedded migration.
Target int
// AllowSchemaDowngrade suppresses the refusal to open a database whose
// schema is ahead of this binary's. See schema_guard.go; the operator-
// facing spelling is the CLOOP_ALLOW_SCHEMA_DOWNGRADE environment
// variable, which Migrate and MigrateTo read on the caller's behalf.
AllowSchemaDowngrade bool
}
MigrateOptions configures MigrateWithOptions.
type MigrationCompat ¶ added in v0.0.2
type MigrationCompat string
MigrationCompat says whether a migration can be tolerated by a binary that predates it.
const ( // CompatUnknown is the zero value: nothing classified this migration, so // nothing may assume it is safe. Treated exactly like CompatBreaking. CompatUnknown MigrationCompat = "" // CompatAdditive means the migration only introduced objects that did not // exist before — new tables, new non-unique indexes, new views. An older // binary cannot read or write them because it does not know their names, // and their presence changes nothing about the statements it does issue. CompatAdditive MigrationCompat = "additive" // CompatBreaking means the migration changed something that was already // there, or did something this classifier does not understand. Either way // an older binary must not open the database. CompatBreaking MigrationCompat = "breaking" )
func (MigrationCompat) Tolerable ¶ added in v0.0.2
func (c MigrationCompat) Tolerable() bool
Tolerable reports whether a binary that does not embed this migration may still open a database that has it.
type MigrationReport ¶
type MigrationReport struct {
StartVersion int // version recorded before this run (0 = brand new DB)
EndVersion int // highest version applied (or already present)
Applied []int // versions newly applied during this run
BaselineApplied bool // true when an existing pre-framework DB was adopted at v1
// Divergent names a version this database recorded under a different
// migration file than this binary embeds. See checkNames.
Divergent []VersionDivergence
}
MigrationReport summarises the outcome of a Migrate call.
func Migrate ¶
func Migrate(db *sql.DB) (*MigrationReport, error)
Migrate brings db up to the latest embedded schema version. Safe to call repeatedly: a fully migrated database is a no-op. Each pending migration runs inside a transaction together with the schema_migrations row insert, so an interrupted run leaves the database at the previous version.
Errors are wrapped with ErrSchemaMismatch so callers can use errors.Is to distinguish migration failures from generic SQLite errors. A database ahead of this binary additionally carries ErrSchemaTooNew.
func MigrateTo ¶
func MigrateTo(db *sql.DB, target int) (*MigrationReport, error)
MigrateTo is Migrate, stopping after the migration numbered target. It exists so that a test can construct the schema some earlier release actually shipped and then let Open() migrate it forward, which is the only way to exercise a migration against the database it will really meet.
The alternative — migrate fully, then hand-undo one migration's DDL and delete its schema_migrations row — does not work, and the way it fails is worth recording. currentVersion is MAX(version), so forgetting migration N forgets everything after it too, and they all re-run. That is harmless while every later migration is CREATE ... IF NOT EXISTS, and stops being harmless the moment one of them is an ALTER TABLE ADD COLUMN, which SQLite has no idempotent spelling for. It then fails with "duplicate column name" inside whichever unrelated test did the rewinding.
Note that target bounds how far *forward* this call migrates and has no bearing on the version-skew refusal, which always compares the database against the highest migration the binary embeds.
func MigrateWithOptions ¶
func MigrateWithOptions(db *sql.DB, opts MigrateOptions) (*MigrationReport, error)
MigrateWithOptions is Migrate with the opt-out under the caller's control rather than the environment's. `cloop hub doctor` uses it to inspect a database the hub itself would refuse — diagnosing a skew is the one job that must still work when the skew is present.
type OffboardApplied ¶ added in v0.0.2
type OffboardApplied struct {
// Sessions, Tokens and Glasses are the ids that really changed state:
// sessions that existed and were deleted, tokens that were live and are now
// stamped revoked. Ids that were already gone or already revoked are
// omitted — they are not this operator's action.
Sessions []string
Tokens []string
Glasses []string
// DenyBindingIDs are the ids of the bindings written.
DenyBindingIDs []string
// At is the timestamp stamped on the revocations.
At time.Time
}
OffboardApplied reports what the transaction actually changed, as opposed to what it was asked to change.
func (OffboardApplied) Severed ¶ added in v0.0.2
func (a OffboardApplied) Severed() bool
Severed reports whether anything at all changed.
type OffboardWrite ¶ added in v0.0.2
type OffboardWrite struct {
// IdentityKey labels the person in the audit trail. Free-form; pkg/offboard
// passes the canonical owner key (lowercased email, or "sub:"+subject).
IdentityKey string
// SessionIDs are hashed session ids, as stored in sessions.id.
SessionIDs []string
// TokenIDs and GlassesIDs both address api_tokens.id. They are separate
// fields rather than one list because they are separate *surfaces* to an
// operator — "their PAT still works" and "their glasses link still works"
// are different incidents — and the audit trail records them separately.
TokenIDs []string
GlassesIDs []string
// DenyBindings are written in the same transaction. They are what keep the
// account out if the IdP later hands it a fresh session: revoking sessions
// alone is undone by the next successful sign-in.
//
// It is a list because one identity can be addressed by more than one
// claim. Denying only the email leaves a re-issued session that carries the
// same subject and no email claim — which several IdPs do once an account
// is disabled — matching nothing. pkg/offboard writes one per identifier it
// resolved.
DenyBindings []RoleBindingRow
// At stamps revoked_at. Zero means now.
At time.Time
// Audit builds the events to chain into this same commit. It is a callback
// rather than a slice because it must describe what the transaction
// actually changed, which is not known until the writes have run: rows
// disappear between planning and committing (a session expires, a token was
// already revoked), and an audit trail that reports the plan instead of the
// outcome is one that overstates containment.
//
// Returning no events is allowed. An error aborts the whole transaction —
// an unrecorded severing of authority is treated as a failed severing,
// because the operator can safely re-run but cannot recover the record.
Audit func(OffboardApplied) ([]*AuditEvent, error)
}
OffboardWrite is the resolved set of rows to sever, plus the deny binding to write. Every field is optional: an identity with no API tokens simply passes an empty TokenIDs.
type OpenOptions ¶
type OpenOptions struct {
// AllowSchemaDowngrade opens a database whose schema is ahead of this
// binary's instead of refusing it. Open sets this from
// CLOOP_ALLOW_SCHEMA_DOWNGRADE; diagnostics that must read a skewed
// database in order to report on it set it directly.
AllowSchemaDowngrade bool
}
OpenOptions configures OpenWithOptions.
type ProjectExecutorBinding ¶
type ProjectExecutorBinding struct {
ProjectPath string `json:"project_path"`
ExecutorID string `json:"executor_id"`
BoundAt time.Time `json:"bound_at"`
BoundBy string `json:"bound_by,omitempty"`
}
ProjectExecutorBinding pins one project directory to one executor.
type ProjectResourceLimit ¶ added in v0.0.2
type ProjectResourceLimit struct {
ProjectPath string `json:"project_path"`
Ceiling executor.ResourceCeiling `json:"ceiling"`
SetAt time.Time `json:"set_at,omitempty"`
SetBy string `json:"set_by,omitempty"`
}
ProjectResourceLimit is one project's ceiling plus its provenance.
type ProviderCallPruneResult ¶ added in v0.0.2
type ProviderCallPruneResult struct {
// Stripped is the number of rows whose bodies were cleared, and
// StrippedBytes the text those bodies held.
Stripped int64 `json:"stripped"`
StrippedBytes int64 `json:"stripped_bytes"`
RowPruneResult
}
ProviderCallPruneResult reports both halves of a provider_calls pass.
type ProviderCallRetention ¶ added in v0.0.2
type ProviderCallRetention struct {
// BodyMaxAge clears prompt, system_prompt and response on rows older than
// this, keeping the row itself. Zero or less keeps every body.
BodyMaxAge time.Duration
// Rows bounds the rows themselves, applied after the strip.
Rows RowRetention
}
ProviderCallRetention bounds provider_calls, which needs two limits rather than one because its rows are cheap and its bodies are not.
func (ProviderCallRetention) Active ¶ added in v0.0.2
func (p ProviderCallRetention) Active() bool
Active reports whether this policy would change anything.
type ProviderCallRow ¶
type ProviderCallRow struct {
ID int64
Timestamp time.Time
Provider string
Model string
TaskID int
TaskTitle string
RequestID string
Prompt string
SystemPrompt string
Response string
ErrorMessage string
Status string // "ok" | "error" | "timeout" | "context_canceled"
Headers string // JSON object, secrets already redacted
InputTokens int
OutputTokens int
ThinkingTokens int
LatencyMs int
}
ProviderCallRow is one row in the provider_calls table. Headers is a JSON-encoded string by the time it reaches this layer (the redaction step runs in pkg/provideraudit before the row is built).
type QuotaCounterRow ¶
type QuotaCounterRow struct {
Identity string
Resource string
Bucket string
Value float64
UpdatedAt time.Time
}
QuotaCounterRow is one row of quota_counters.
type QuotaOverrideRow ¶
type QuotaOverrideRow struct {
Identity string
LimitsJSON string
UpdatedAt time.Time
UpdatedBy string
}
QuotaOverrideRow is one row of quota_overrides. LimitsJSON is stored opaquely: the set of resources is pkg/quota's business, and a storage layer that validated it would be a second place to update when one is added.
type RoleBindingRow ¶ added in v0.0.2
type RoleBindingRow struct {
ID string
Effect string
Claim string
Value string
Role string
Project string
Executor string
Reason string
CreatedAt time.Time
CreatedBy string
}
RoleBindingRow is one row of role_bindings.
type RotationRow ¶
type RotationRow struct {
ID string
ToKeyID string
State string
StartedAt string
UpdatedAt string
FinishedAt string
StartedBy string
Total int
Rewrapped int
Skipped int
Failed int
LastError string
}
RotationRow is one row of key_rotations.
type RowPruneResult ¶ added in v0.0.2
type RowPruneResult struct {
// Deleted is the number of rows removed.
Deleted int64 `json:"deleted"`
// BytesReleased is the variable-width column content those rows held. It
// is returned to the database freelist, not to the filesystem — only the
// subsequent VACUUM shrinks the file — so callers must not add it to a
// "bytes reclaimed" total that also counts the VACUUM.
//
// Measured across the rows the prune set out to remove, so when the call
// also returns an error it describes the intent rather than the outcome.
// Read it together with the error, never instead of it.
BytesReleased int64 `json:"bytes_released"`
// Remaining is the row count after the prune (before it, in a dry run).
Remaining int64 `json:"remaining"`
}
RowPruneResult is what one table's prune did, or would have done in a dry run.
type RowRetention ¶ added in v0.0.2
type RowRetention struct {
// MaxRows is the hard ceiling: the newest MaxRows rows survive and the
// rest are deleted. Zero or less keeps every row.
MaxRows int
// MaxAge deletes rows older than this. Zero or less keeps every row
// regardless of age.
//
// An age bound alone does not make a table bounded — an unbounded write
// rate still produces an unbounded table within any fixed window — which is
// why hubdoctor reports boundedness off MaxRows and not off this.
MaxAge time.Duration
}
RowRetention bounds one row table. The zero value bounds nothing.
func (RowRetention) Active ¶ added in v0.0.2
func (r RowRetention) Active() bool
Active reports whether this policy would delete anything at all.
func (RowRetention) Bounded ¶ added in v0.0.2
func (r RowRetention) Bounded() bool
Bounded reports whether this policy puts a ceiling on the table's size.
type SchemaStamp ¶
type SchemaStamp struct {
Version int `json:"version"`
Name string `json:"name,omitempty"`
AppliedAt time.Time `json:"applied_at,omitempty"`
AppliedBy string `json:"applied_by,omitempty"`
// Compat is the applying build's reading of whether this migration is
// tolerable to older binaries. Empty for rows written before that was
// recorded, which reads as "not known to be safe".
Compat MigrationCompat `json:"compat,omitempty"`
}
SchemaStamp is one schema_migrations row: which version was applied, when, and by which build.
AppliedBy is "" for rows written before this provenance existed, and for rows written by a process that never called SetBinaryVersion. Absence is not an error — it just means the refusal message cannot name a build.
type SchemaTooNewError ¶
type SchemaTooNewError struct {
// DBVersion is the highest version recorded in schema_migrations.
DBVersion int
// BinaryVersion is the highest migration embedded in this binary.
BinaryVersion int
// Stamp describes the schema_migrations row for DBVersion, when one could
// be read. A zero Stamp means the row was unreadable, not that it is
// absent — either way the message simply omits the provenance.
Stamp SchemaStamp
// Blocking lists the migrations ahead of this binary that it cannot
// tolerate: the ones recorded as breaking, plus any it could not classify.
// Migrations recorded as additive are absent, because they did not
// contribute to the refusal.
Blocking []SchemaStamp
}
SchemaTooNewError is the detail behind ErrSchemaTooNew: the two versions that disagree, plus whatever the database records about the build that moved it forward.
It satisfies errors.Is for both ErrSchemaTooNew and ErrSchemaMismatch. The second is deliberate: every existing caller that already treats a schema mismatch as fatal (HTTPStatus maps it to 500, classifyDriverErr declines to re-wrap it) keeps working unchanged, while callers that care about this specific cause can ask for it by name.
func (*SchemaTooNewError) Error ¶
func (e *SchemaTooNewError) Error() string
func (*SchemaTooNewError) Is ¶
func (e *SchemaTooNewError) Is(target error) bool
Is makes both sentinels match. See the type comment for why ErrSchemaMismatch is included.
type SealedRow ¶
SealedRow is a row of sealed material as the rotator sees it: an ID, the associated data it is bound to, and the three envelope parts.
type SecretAuditInput ¶
type SecretAuditInput struct {
Actor string
// EventType is the registered action: auditaction.ActionSecretLease,
// ActionSecretGrant, ActionSecretRevoke, and the rest of the secret,
// lease, egress and github_app families.
//
// Typed rather than a bare string because the whole broker surface funnels
// through this one struct, which makes it the single place a made-up
// action could enter the trail. Requiring an auditaction.Action means the
// value came from the registry, and tests/arch/auditaction_test.go can see
// that it did.
EventType auditaction.Action
EntityID string // secret ID, or grant ID when no secret is involved
Timestamp time.Time
// Payload is the decision's metadata. Callers are responsible for
// redacting it; pkg/secretbroker.Redact runs over every event before it
// reaches here, and again inside the secretstore auditor.
Payload map[string]any
}
SecretAuditInput carries one secret-broker decision to the audit log (Task 20159). It exists so pkg/secretstore does not have to construct an AuditEvent — and, more to the point, so the entity_type is set in exactly one place and every broker row is filterable as entity_type='secret'.
type SecretLeaseDirRow ¶
type SecretLeaseDirRow struct {
// Dir is the absolute path of the lease directory and the primary key.
Dir string
// LeaseID is the lease whose material lives there.
LeaseID string
// ExecutorID is the executor the lease was issued to, ” when unbound.
ExecutorID string
// ProjectPath is the project the work belongs to, ” when not scoped.
ProjectPath string
// CreatedAt is when the intent was recorded — before the directory was
// created, not after.
CreatedAt time.Time
// ExpiresAt is the lease's expiry; the zero time means unbounded. The
// sweep does not need it to decide, but an operator reading the audit
// event for a swept directory needs to know whether it had already lapsed.
ExpiresAt time.Time
}
SecretLeaseDirRow is one credential directory this hub has committed to creating. It records where plaintext is, never what it is.
type SessionClaims ¶ added in v0.0.2
SessionClaims is a replacement set of group and role claims.
type SessionRefreshUpdate ¶ added in v0.0.2
type SessionRefreshUpdate struct {
KeyID string
WrappedDEK []byte
Sealed []byte
CheckedAt time.Time
Claims *SessionClaims
// ClaimsAsOf and ClaimsExpireAt update the claim-freshness clocks. A zero
// value leaves the stored column alone, which is what lets one update
// express "claims re-asserted" (both set), "claims repudiated"
// (ClaimsExpireAt only, backdated to the refusal) and "grant renewed but
// nothing learned" (neither) without a separate statement for each.
ClaimsAsOf time.Time
ClaimsExpireAt time.Time
}
SessionRefreshUpdate is the outcome of one IdP revalidation.
Claims is nil when the provider returned no id_token, meaning the stored claims are left untouched; a non-nil value replaces them, including with an empty set, which is how a user losing their last group is recorded.
type SessionRow ¶
type SessionRow struct {
ID string
Subject string
Issuer string
Email string
DisplayName string
OwnerKey string
Groups []string
Roles []string
IP string
UserAgent string
IssuedAt time.Time
LastSeen time.Time
ExpiresAt time.Time
RefreshSealed []byte
// RefreshKeyID and RefreshWrappedDEK are the envelope this row's sealed
// refresh token belongs to (Task 20181). "legacy" means the token is
// sealed directly under the passphrase-derived key, with no wrapped DEK.
RefreshKeyID string
RefreshWrappedDEK []byte
RefreshCheckedAt time.Time
// ClaimsAsOf and ClaimsExpireAt date the row's groups and roles, as opposed
// to RefreshCheckedAt which dates the grant (Task 20273). See
// migrations/0038_session_claim_freshness.sql for why they must be separate
// columns, and pkg/oidcauth/claimfresh.go for what reads them.
ClaimsAsOf time.Time
ClaimsExpireAt time.Time
}
SessionRow is one row of the sessions table.
RefreshSealed is ciphertext. Nothing in this package encrypts or decrypts it — the sealing happens in pkg/sessionstore, so the storage layer never holds a live credential and cannot leak one by logging a row.
type SizeStats ¶
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 ¶
FreelistBytes returns the bytes currently allocated to the freelist — roughly the upper bound on what VACUUM can reclaim.
type SpendCursor ¶ added in v0.0.2
type SpendCursor struct {
ProjectPath string
// Identity is who the hub resolved when it dispatched the current run.
// The only identity this project's spend is ever charged to.
Identity string
// LastCostRowID is the highest costs.id already charged.
LastCostRowID int64
UpdatedAt time.Time
}
SpendCursor is the hub's booking position for one project.
type State ¶
type State struct {
Goal string
WorkDir string
MaxSteps int
CurrentStep int
Status string
// PauseReason explains a "paused" Status. Stored as JSON in the
// schemaless metadata table rather than as a column, so adding it needed
// no migration — and an older binary reading this DB just ignores the key
// instead of being locked out by a schema it does not know.
PauseReason *pausereason.Reason
Steps []StepRow
// StepCount mirrors len(Steps) on full loads but is also populated by
// LoadStateLite — where Steps is nil — so callers that only need a
// count don't have to materialize every row's Output (Task 20125).
StepCount int
// LastStepTime is the timestamp of the newest step row. Populated by
// both LoadState (last element of Steps) and LoadStateLite (cheap
// lookup of the highest step number); used by health probes to detect
// stalled runs without scanning the full Steps slice.
LastStepTime time.Time
CreatedAt time.Time
UpdatedAt time.Time
Model string
Instructions string
AutoEvolve bool
EvolveStep int
Provider string
Effort string
PMMode bool
Plan *pm.Plan
Milestones []*milestone.Milestone
TotalInputTokens int
TotalOutputTokens int
DefaultMaxMinutes int
SkipClarify bool
InnovateMode bool
Parallel bool
MaxParallel int
WorktreeParallel bool
PlanOnly bool
RetryFailed bool
DryRun bool
}
State mirrors pkg/state.ProjectState but is owned by this package to avoid import cycles. pkg/state converts between the two representations.
type StepRow ¶
type StepRow struct {
Step int
Task string
Output string
ExitCode int
Duration string
Time time.Time
InputTokens int
OutputTokens int
}
StepRow represents one recorded step result.
type Store ¶
type Store interface {
// SaveState atomically persists the full project state (metadata, tasks,
// steps). Existing tasks are replaced; steps are upserted (never deleted).
SaveState(s *State) error
// LoadState reads back the complete project state. Returns a zero-value
// State (not an error) when the database exists but has no rows yet.
LoadState() (*State, error)
// UpsertTask inserts or replaces a single task row. Useful for
// status-only updates without a full SaveState round-trip.
UpsertTask(t *pm.Task) error
// LoadTask returns a single task by id. Returns ErrTaskNotFound when
// the row does not exist; callers should test with errors.Is.
LoadTask(id int) (*pm.Task, error)
// AppendStep inserts a step row. Idempotent on step number (upsert).
AppendStep(row StepRow) error
// AppendCost appends a single cost entry for the current project.
AppendCost(entry CostEntry) error
// ReadCosts returns all cost entries in chronological order.
ReadCosts() ([]CostEntry, error)
// ReadCostsSince returns cost entries with timestamp >= since.
ReadCostsSince(since time.Time) ([]CostEntry, error)
// MonthlyCosts returns cost entries for the given UTC year/month.
MonthlyCosts(year, month int) ([]CostEntry, error)
// PingContext verifies the underlying store is reachable by issuing a
// trivial query. Used by readiness probes; ctx bounds wait time on a
// busy connection.
PingContext(ctx context.Context) error
// Close releases the underlying connection.
Close() error
}
Store is the abstract data-access interface for cloop project state. All methods must be safe for concurrent use.
SQLite implementation: DB (this package). PostgreSQL: implement this interface with a pgx/pgxpool-backed struct.
type TableStat ¶ added in v0.0.2
type TableStat struct {
Name string `json:"name"`
Rows int64 `json:"rows"`
// Bytes is the variable-width column content across all rows. See
// growthTable.bytesExpr: an under-estimate of what the table costs on disk,
// deliberately.
Bytes int64 `json:"bytes"`
// Oldest is the insertion timestamp of the lowest-id row, or the zero time
// when the table is empty or that row's timestamp will not parse.
Oldest time.Time `json:"oldest,omitempty"`
}
TableStat is one row table's current size, for reporting.
type TaskDispatchInput ¶ added in v0.0.2
type TaskDispatchInput struct {
// TaskID and TaskTitle identify the unit of work. The title is recorded
// rather than joined later because a plan can be edited: an auditor reading
// this row six weeks on should see what the task said when it ran.
TaskID int
TaskTitle string
// ProjectPath is the project the task belongs to.
ProjectPath string
// RunID names the execution. It is the join key to the secret.lease rows
// the hub wrote for the same dispatch, and the reason leases attach to an
// execution rather than to a task id that may run many times.
RunID string
// ExecutorID, ExecutorKind and Isolation are where the work landed, as
// advertised at placement. Stored rather than derived by joining the
// executor registry later, for the reason migration 0032 gives: an executor
// re-enrolled under the same id would otherwise rewrite the history of
// every task it ever ran.
ExecutorID string
ExecutorKind string
Isolation string
// RequestedImage is the reference the sandbox spec asked for; PinnedImage
// is what actually ran, digest-pinned where the driver could resolve one.
// Both, because the gap between them is the question — a spec naming a tag
// and a run on a digest is reproducible, a run on the tag is not.
RequestedImage string
PinnedImage string
// SpecHash is the .cloop/sandbox.yaml content hash, empty when the project
// has no spec and ran on the executor's defaults. SetupHash identifies
// commands baked into a derived image.
SpecHash string
SetupHash string
// LeaseIDs are the secret leases handed to this run.
LeaseIDs []string
// Actor is who asked for the run.
Actor string
}
TaskDispatchInput carries the placement facts for one task execution (Task 20282).
It is filled by the orchestrator, which is the only layer that knows both a task id and the environment it was placed into: the hub chose the executor and started a whole `cloop run`, and by the time a task begins there is no placement decision left to read — the decision is why the process exists. What the hub leaves behind is .cloop/sandbox-run.json, and this struct is that record joined to the task it is about to run.
No field can carry a credential. LeaseIDs are the broker's opaque handles, not material; the digest and hashes are content addresses; Actor is an identity label. The token behind a lease never enters this package's scope.
type TaskRunStory ¶ added in v0.0.2
type TaskRunStory struct {
RunID string
// Dispatched and Finished are the boundary rows. Finished is nil for a run
// still in flight — or for one whose hub died so hard it never wrote a
// terminal row, which is a gap an auditor should see rather than have
// smoothed over with an invented outcome.
Dispatched *AuditEvent
Finished *AuditEvent
// ExecutorID, ExecutorKind and Isolation are where the work ran, read from
// the dispatch payload.
ExecutorID string
ExecutorKind string
Isolation string
// PinnedImage and SpecHash pin the environment.
PinnedImage string
SpecHash string
// LeaseIDs are the credentials the dispatch row claims were handed over.
LeaseIDs []string
// LeaseEvents are the broker's own rows for this run — the authoritative
// half. They are kept separate from LeaseIDs on purpose: the dispatch row
// is written by the orchestrator, which reads its lease ids from a file in
// a directory the workload can write to, whereas these rows are written by
// the hub and cannot be influenced from inside the sandbox. A reviewer
// checking a hostile workload should believe these.
LeaseEvents []AuditEvent
// Outcome, Reason and DurationMS come from the terminal row.
Outcome string
Reason string
DurationMS int64
// Actor is who the dispatch row attributes the run to.
Actor string
}
TaskRunStory is one execution of a task.
func (TaskRunStory) Duration ¶ added in v0.0.2
func (r TaskRunStory) Duration() string
Duration renders the execution's wall time, or "" when the trail does not record one.
func (TaskRunStory) RanOnHost ¶ added in v0.0.2
func (r TaskRunStory) RanOnHost() bool
RanOnHost reports whether this execution used the hub's own machine.
Mirrors orchestrator.RanOnHost, deliberately reimplemented over the audit payload rather than over a pm.Task: the point of a reconstruction is that it works without the task record.
type TaskStory ¶ added in v0.0.2
type TaskStory struct {
// TaskID is the task the story is about.
TaskID int
// Title is the task's title as the trail recorded it, from the most recent
// row that carried one. Empty when no row named it — which is itself a
// finding, not a formatting problem.
Title string
// Events are every row that belongs to this task's history, in trail order
// (ascending id, which is the order they were appended and therefore the
// order the hash chain fixes). Includes both the task-scoped rows and the
// run-correlated rows from other entities.
Events []AuditEvent
// Runs are the executions this task was dispatched into, oldest first. A
// task that ran five times has five entries, which is the distinction the
// whole run-id mechanism exists to preserve.
Runs []TaskRunStory
// Unattributed counts lifecycle rows that named no run — history from
// before run ids existed, or a dispatch whose provenance record was
// missing. It is reported rather than silently dropped: executions the
// trail cannot correlate are exactly what an auditor needs told.
Unattributed int
}
TaskStory is one task's reconstructed history.
type TelemetryFilter ¶ added in v0.0.2
type TelemetryFilter struct {
Source string
Kind string
Session string
// Search matches message, url, view and detail. Applied in SQL rather than
// in the caller because the table is bounded but large, and a reader
// filtering client-side would page through thousands of rows to find one.
Search string
Since time.Time
Limit int
Offset int
}
TelemetryFilter selects events for reading. The zero value means "the most recent page of everything", which is the query a reader opening the panel wants before they know what they are looking for.
type TelemetrySession ¶ added in v0.0.2
type TelemetrySession struct {
Session string `json:"session"`
Source string `json:"source"`
Events int `json:"events"`
Errors int `json:"errors"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
UserAgent string `json:"user_agent,omitempty"`
Actor string `json:"actor,omitempty"`
Release string `json:"release,omitempty"`
}
TelemetrySession is one page load, rolled up.
The index of the panel: a reader arrives knowing "a wearer reported a problem around ten past four" and needs to turn that into a session id before any per-event view is useful.
type VersionDivergence ¶ added in v0.0.3
VersionDivergence is one version whose recorded name is not the name this binary embeds for it.
type WorkspaceAuditInput ¶
type WorkspaceAuditInput struct {
// Phase is "start" or "end", matching executor.WorkspaceProvisionStart /
// WorkspaceProvisionEnd. It becomes the suffix of the event type, so the
// family is filterable as event_type LIKE 'workspace.%' and each phase
// individually by exact match. Anything else is dropped rather than
// written, so a typo cannot create a third, unqueryable event family.
Phase string
// ProjectPath is the project whose tree was being materialised. It is the
// entity the row is filed under, matching how image denials are filed.
ProjectPath string
// ExecutorID and ExecutorKind name the backend doing the fetch. Both are
// recorded because an ID is stable but says nothing about *where* the code
// landed, and "was this on a Pod or on someone's laptop" is the first
// question asked of a provisioning row.
ExecutorID string
ExecutorKind string
// HandleID correlates the two phases with each other and with the run.
HandleID string
// Kind, Repo, Ref and Depth are the workspace as dispatched — the clone
// URL exactly as the executor was told to fetch it, which is the only form
// that can be compared against what the operator believes is configured.
Kind string
Repo string
Ref string
Depth int
// GrantID and LeaseID name the authority used. They are the join key back
// into the secret-broker rows, which is what turns "this repo was fetched"
// into "this grant was exercised at this time".
GrantID string
LeaseID string
// DurationMS is set on the end phase only.
DurationMS int64
// Err is the failure, already redacted by the driver, or "" on success.
Err string
// Actor is the acting identity. Empty becomes "system": provisioning is
// decided by the dispatching control plane, and on the failover path there
// is no human to attribute it to.
Actor string
}
WorkspaceAuditInput carries one workspace-provisioning observation to the audit log (Task 20179).
Provisioning earns its own rows rather than being folded into the run's, because it is the moment a brokered credential is used against an external service on behalf of a project. "Which grant fetched which repository onto which executor, and did it work" is the question an operator has after an incident, and the run's own record cannot answer it: a run looks identical whether the tree arrived or not, which is the entire reason the workspace subsystem exists.
No field here can carry a credential. The Workspace the executor dispatched is structurally incapable of holding one — pkg/executor.Workspace has no field a token could go in, by design, because a Spec is persisted, logged and shipped across the remote-executor boundary — and GrantID/LeaseID are identifiers, not material. The token never enters this package's scope.
Source Files
¶
- apitokens.go
- audit.go
- audit_anchor.go
- audit_attach.go
- audit_emit.go
- audit_fingerprint.go
- audit_home.go
- audit_redact.go
- audit_run.go
- audit_story.go
- autoupdate.go
- broker.go
- cipipelines.go
- db.go
- egress.go
- errors.go
- events.go
- executor_agents.go
- executor_handles.go
- executor_scheduling.go
- executoraudience.go
- executorlimits.go
- executors.go
- executorsandbox.go
- grantrequests.go
- hub_instances.go
- hubmeta.go
- keys.go
- kill_requests.go
- maintenance.go
- migrate.go
- offboard.go
- projectlimits.go
- provider_calls.go
- quotas.go
- rolebindings.go
- rowretention.go
- schema_compat.go
- schema_guard.go
- secret_lease_dirs.go
- sessions.go
- spend_cursor.go
- store.go
- task_runs.go
- telemetry.go
- usererror.go