store

package
v0.8.7 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Governance profiles and their subject assignments (migration 0052).

This file is DUMB on purpose: it round-trips rows and owns exactly one piece of logic, ResolveGovernanceProfile's ORDER BY, so precedence is a property of the single indexed read, not a caller that could forget it. Every write is validated at the API boundary (internal/api/governance.go).

Governance changes (migration 0126): a governance write held for a second human. The store owns the parts that must be one transaction: proposing (expire a lapsed change at the same target, then insert) and deciding (lock the change row, require it pending and unexpired, run the target's apply, move the state, commit). It knows nothing about any target: the apply a caller hands DecideGovernanceChange is that target's own code, run on the decision transaction.

Package store provides typed CRUD over the Wardyn schema using pgx/v5. Callers supply contexts with deadlines. InsertAuditEvent stays a free function taking the pool explicitly since it predates a Store value in the audit.Recorder wiring.

Naming: Create* inserts and returns the hydrated row; Get* fetches by primary key (ErrNotFound when absent); List* returns a slice, empty never nil; Update*/Decide* are point mutations with explicit optimistic guards.

The Azure DevOps personal access tokens Wardyn creates for `minted_pat` runs (migration 0102, #1428): the record that makes a token revocable after a crash. A row holds no secret; the token value lives in daemon memory only.

The console's reads of ado_run_pats (#1428): every token a run held, and the newest token created for a person on a row. Like the table, neither carries a secret.

The Azure DevOps sign-in end counter (migration 0129): per person, how many times their sign-in was disconnected or erased and whether one is running now, so a run token created on any replica across one of those ends revokes itself. The row holds no secret.

Per-user API tokens. Kept out of store.go on purpose (lint size boundary), mirroring store_sshkeys.go's split.

SECURITY: the raw token never reaches SQL — every method here hashes it with hashToken before touching the table, so token_sha256 is the only form that exists at rest.

The reads the attention projection and GET /me/attention need beyond what GET /runs already scoped: every PENDING approval on a batch of runs, in one query, so paging runs costs one extra round trip regardless of how many are held; and the two scoped PENDING counts /me/attention answers, counted in the database rather than by listing every row.

The attach lease (migration 0129): the one writer slot of a run's shared terminal, in Postgres so every replica agrees who holds it. The database's clock decides when a lease lapses.

Console branding (migration 0091_branding). One row or none; the request is validated at the API boundary (internal/api/branding.go).

Capability grants and the per-kind enforcement switch (migration 0042). Kept out of store.go on purpose, mirroring store_sshkeys.go's split.

Registered portals and the delegated tokens they are handed. Like store_apitokens.go and store_devices.go, every method hashes the RAW credential with hashToken before it reaches SQL.

Hybrid enrolment and audit federation (migration 0066): the organisation-side device inventory (devices, device_enrolment_tokens) and the laptop side's durable forwarder cursor (org_federation). Kept out of store.go (already at a lint size boundary), mirroring store_apitokens.go/store_sshkeys.go's split.

SECURITY: hashToken (store_ephemeral.go) is reused for both credential tables — the raw device bearer and raw enrolment token never reach a row, only hex(sha256(raw)).

Short-lived control-plane handoff row (migration 0026): single-use WS attach tickets, durable and shared across every control plane — an in-process map can't survive a restart or be seen by a second control plane. Consume-once is a single DELETE ... RETURNING, atomic under concurrency and across processes.

The sign-in gate and the owner guard: how a suspended identity is refused at issuance and how a credential minted during a suspension is refused at its insert (migration 0113, 0127).

Launch presets (migration 0087_launch_presets). Round-trips rows; the request body is validated at the API boundary (internal/api/presets.go).

The store half of leaver deprovisioning: the SCIM projection on an identity row, the suspension and reactivation transactions, and the durable step ledger (migration 0127).

The identity row every successful sign-in leaves (migration 0113).

Per-person preferences (migration 0080_user_view_type).

A held push's complete path list (migration 0085_push_content_paths).

Console-managed role mappings (migration 0051): the store half of internal/auth/oidc's RoleMappingSource. Kept out of store.go on purpose, mirroring store_capabilities.go's split.

The run lease (long-holds design rev 4; migration 0073): the reads and the conditional writes the lease sweep needs. Kept out of store.go for the same size reason as store_watcher.go.

Lost-run surface (long-holds design rev 4, RL-9; migration 0083): token stamp, lapsed-token read, and lost claim. Split out of store.go for size, like store_run_lease.go.

The live exec tail (migration 0129): the masked bytes a run's dispatching replica has printed so far, readable from any replica. One final row per run is run_outputs' (0122); these chunks are what stands in for it until that row is written, and what a recovery falls back to when the substrate cannot be re-read.

The persisted run output (migration 0119): one masked final row per run, the pending row that records a capture is owed, and the erasure tombstones that keep an erased run's output erased. Kept out of store.go for the same size reason as store_watcher.go.

Pause and resume (long-holds design rev 4, §3, RL-7; migration 0097): the presence clock, the reads the pause sweep needs and its conditional writes. Kept out of store.go for the same size reason as store_run_lease.go.

A run's sealed proxy config (#1176, migration 0093): what a revive rebuilds the run's proxy from, so the proxy container never holds it at rest.

Tightened limits reach live runs (long-holds design rev 4, §2.2, RL-8; migration 0095): the read and the conditional write the re-clamp sweep needs. Kept out of store.go for the same size reason as store_run_lease.go.

Revive and restart with current limits (long-holds design rev 4, RL-10 and RL-11; migration 0084): the claim a revive makes before it replaces a run's proxy, and the read the admin version-window listing makes.

Landing-page GET /runs scoping: RunFilter, the filtered list read, and hidden-row counts. Attention projection lives elsewhere; this file only scopes, windows and orders the bare AgentRun rows.

Sandbox ref -> substrate routing rows (migration 0021): the Postgres RefStore, letting a control-plane restart rehydrate lifecycle routing in multi-substrate deployments.

SSH gateway key registry. Kept out of store.go on purpose (lint size boundary), mirroring store_sandbox_ref.go's split.

Per-user run-detail cockpit widget-layout persistence (migration 0037). Kept out of store.go/iface.go on purpose — see RunLayoutStore's doc below.

User types (migration 0071_user_types). Round-trips rows; every write is validated at the API boundary (internal/api/user_types.go).

The run-watcher lease (migration 0027): two writes that make "who is responsible for finishing this run" a fact in Postgres, not a goroutine in one process. Kept out of store.go (lint size); shares run columns with store.go's CreateRun/GetRun and pagination.go's ListRunsPage.

The workspaces table (tier 3 of the three-tier split): JSONB parameter helpers, the wsCols column list, scanWorkspace, and the CRUD and scoped-writer methods PG exposes over a workspace row. store_sources.go is the read/hydrate half of the pair (tiers 1-2 plus s.hydrated).

User drives and their subject grants (migration 0054), split out of store.go like governance.go. Kept DUMB: round-trips rows, owns only ResolveUserDrive's ORDER BY (precedence must live in the one indexed read, not a caller). Writes are validated at the API boundary (types.ValidateUserDrive, types.ValidateUserDriveGrant).

Index

Constants

View Source
const (
	AuditOriginDevice       = "device"
	AuditOriginOrganisation = "organisation"
)

The two AuditFilter.Origin values: rows a device forwarded, and rows the organisation wrote itself (FederatedDeviceID decides which is which).

View Source
const (
	RetentionNotOldest      = "audit_retention_not_oldest"
	RetentionNotClosed      = "audit_retention_not_closed"
	RetentionInsideWindow   = "audit_retention_inside_window"
	RetentionLiveRun        = "audit_retention_live_run"
	RetentionDigestMismatch = "audit_retention_digest_mismatch"
)

The refusal reasons audit_retention_drop (migration 0123) raises, one SQLSTATE each. They are also the wire reasons of POST /audit/retention/drop, registered in internal/authz.

View Source
const (
	SplitNotOwner       = "audit_split_not_migrator"
	SplitNotPartitioned = "audit_split_not_partitioned"
	SplitNoLegacy       = "audit_split_no_legacy_partition"
	SplitEmpty          = "audit_split_legacy_empty"
	SplitChainBroken    = "audit_split_chain_broken"
	SplitNameTaken      = "audit_split_name_taken"
)

The refusal reasons of SplitLegacyAudit. Each is a state the split must not run in; nothing was changed.

View Source
const (
	JobKindSuspend = "suspend"
	// JobStepCutoff is step 1 of a suspension: the cutoff, the deactivation and the epoch bump.
	JobStepCutoff = "cutoff"
)

Job kinds and the one step name every kind shares.

View Source
const (
	SearchExternalID = "externalId"
	SearchUserName   = "userName"
	SearchEmail      = "emails.value"
)

SearchIdentities attrs, as SCIM spells them.

View Source
const (
	JobKindPurge            = "purge"
	JobStepAuditDeprovision = "audit_deprovision"
)

JobKindPurge is the ledger kind of a purge; JobStepAuditDeprovision is the step it ends on, and the one MarkIdentityPurged records pending so a purge that dies right after the mark is still on the ledger.

View Source
const (
	PeopleDirectoryDefaultLimit = 50
	PeopleDirectoryMaxLimit     = 200
)

A Limit of zero is PeopleDirectoryDefaultLimit; one above PeopleDirectoryMaxLimit is clamped to it.

View Source
const (
	JobKindGroupRemove = "group_remove"

	GroupStepSessions = "sessions"
	GroupStepTokens   = "tokens"
	GroupStepAudit    = "audit"
)

The group removal ledger (migration 0127's group_remove kind): one set of rows per person and group, the target of every step being the group's id.

View Source
const ADOSignInEndsStaleAfter = 10 * time.Minute

ADOSignInEndsStaleAfter is how long an end that began and never finished (its replica died) still counts as running. Past it the count settles, so a person's mints stop revoking themselves; an end finishes in seconds.

View Source
const AuditTargetTruncatedMarker = "…[truncated]"

AuditTargetTruncatedMarker is appended to a capped target so the row says what happened to it. Without it a truncated path reads as a real one, and an operator matching `target = '/api/v1/policies/aaa…'` would believe it.

View Source
const FederatedClaimHashSQL = `` /* 334-byte string literal not displayed */

FederatedClaimHashSQL recomputes, from one stored federated row aliased e, the hash the device claimed for it: equals e.data->'device_origin'->>'row_hash' for every row IngestDeviceAudit accepts. Claimed actor/source_ip/prev_hash live in device_origin; the claimed object is stored data minus that key; a data-less claim is named by device_origin.data_null ("json" for JSON null, "sql" for absent).

View Source
const HoldsSandboxSQL = `(lost_at IS NULL OR (lost_reason = '` + string(types.LostOutage) + `' AND (ends_at IS NULL OR ends_at > now())))`

HoldsSandboxSQL is TRUE when the agent_runs row's agent is still running: the run is not kept, or it was kept after a control-plane outage and has not reached its end, so only its proxy was stopped (api stopLostSandbox). A run kept after its end or a reboot, or an outage-kept run past its end, has its agent stopped. It is the one rule for a live run under WARDYN_MAX_CONCURRENT_RUNS, in the fleet capacity view and under WARDYN_RUN_MAX_AGE.

View Source
const MaxAuditTargetLen = 512

MaxAuditTargetLen bounds the audit row's `target` column (plain TEXT, unbounded). An authenticated caller looping a huge path via authz.denied (unrate-limited, unlike auth.fail) could otherwise write megabytes per second into a table whose Postgres trigger forbids DELETE; 512 bytes holds every real target this tree writes with an order of magnitude to spare.

View Source
const PeopleDirectoryActiveSessionWindow = 24 * time.Hour

PeopleDirectoryActiveSessionWindow is how recent a sign-in must be to count as a live session. Sessions are stateless signed cookies the server keeps no row for, so the directory knows only when a person last signed in, not how many cookies are out: it counts one when that sign-in is inside this window and no revoke cutoff has passed it.

View Source
const ReauthHoldMax = 1800 * time.Second

ReauthHoldMax is the longest a credential re-auth hold keeps the agent's connection parked (WARDYN_CREDENTIAL_REAUTH_TIMEOUT's ceiling). A re-auth request counts toward a waiting pause only once it is older than this: until then the agent is still inside the connection that asked, and freezing it would cut a sign-in the person may be finishing.

View Source
const SearchDisplayName = "displayName"

SearchDisplayName is the Groups filter attribute that SearchExternalID shares the rest of with Users.

View Source
const SystemAuditActor = "wardynd"

SystemAuditActor is the actor an unattended retention drop (WARDYN_AUDIT_RETENTION_AUTODROP) records: audit_retention_drop treats exactly this name as the system and marks the chained event unattested.

Variables

View Source
var AuditChainPageSize int32 = 1000

AuditChainPageSize is how many rows one page of the verify sweep reads — bounds memory to a page, never the table. Large enough that the per-page round trip is noise against re-hashing a thousand rows, small enough to stay a few hundred kilobytes. A var only so the parity test can shrink it and cross page boundaries.

View Source
var ErrAlreadyDecided = types.ErrApprovalAlreadyDecided

ErrAlreadyDecided is returned when DecideApproval targets an approval that has already left PENDING. Fail closed: a second decision never overwrites the first.

View Source
var ErrAuditPartitionOpen = errors.New("store: audit partition is still open")

ErrAuditPartitionOpen is a partition that can still receive rows: it has no digest yet.

View Source
var ErrConflict = errors.New("store: conflict")

ErrConflict reports a fenced write losing its race: a source scan slot already claimed, or the requirements merge hitting the key cap.

View Source
var ErrDeviceRevoked = errors.New("store: device is revoked")

ErrDeviceRevoked is IngestDeviceAudit's answer for a device revoked after its request was authenticated. Distinct from ErrConflict so the caller answers it as revocation, never a broken chain.

View Source
var ErrDriveAllocated = fmt.Errorf(
	"%w: this user drive gained an allocation while the write was being prepared", ErrConflict)

ErrDriveAllocated is returned by UpsertUserDrive when the caller asked for the write to apply only while the drive has no allocations (refuseIfAllocated) and the row has some. It is the statement-level half of the API's re-home guard: since a plain read-then-write would let a grant created in between get silently re-homed, the precondition rides the writing statement under a FOR UPDATE on the drive row in the same transaction, making it race-free.

View Source
var ErrDriveHomeNamespaceConflict = fmt.Errorf(
	"%w: another host_path drive on this host_root derives home directory names by a different rule", ErrConflict)

ErrDriveHomeNamespaceConflict is returned by UpsertUserDrive when a host_path drive would share a host_root with another host_path drive that derives home directory NAMES by a different rule. Wraps ErrConflict (as do ErrDriveSlugConflict and ErrDriveAllocated below) so callers asking only "is this a 409?" keep working, while the remedy stays distinguishable from a plain UNIQUE(name) collision.

View Source
var ErrDriveSlugConflict = fmt.Errorf(
	"%w: another user drive's name folds to the same storage-object name", ErrConflict)

ErrDriveSlugConflict is returned by UpsertUserDrive when a drive's name folds to a storage-object slug another drive already holds — "Corp NAS" against an existing "corp nas", or "Corp NAS (eng)" against "corp-nas-eng". Distinct from UNIQUE(name): the name itself is free, but types.DriveSlug(name) — the fragment every minted object name is built from — collides, so the remedy is a name differing by more than case or punctuation.

View Source
var ErrDuplicatePending = types.ErrDuplicatePendingApproval

ErrDuplicatePending is returned by CreateApproval when a partial unique index rejects a second open PENDING approval for the same dedup key (a concurrent raise lost the race). Callers treat it as a dedup signal — re-read and return the existing PENDING row — not a hard failure.

View Source
var ErrFederatedOrgRun = errors.New("store: federated row names an organisation run")

ErrFederatedOrgRun refuses a batch where a row's run_id names one of this organisation's own runs: phase one federates audit rows, never run records, so a laptop's row can't legitimately belong to one.

View Source
var ErrFederatedRowInvalid = errors.New("store: federated row cannot be stored as claimed")

ErrFederatedRowInvalid refuses a batch holding a row this organisation can't store (FederatedRowProblem) or a value Postgres can't represent (SQLSTATE class 22); the caller answers 4xx so the forwarder stops rather than retrying a 5xx forever.

View Source
var ErrGovernanceChangeStale = errors.New("store: the governance change is stale")

ErrGovernanceChangeStale is what a decision's apply returns when the target is no longer what the proposal reviewed. DecideGovernanceChange moves the change to stale and commits that, and returns this error.

View Source
var ErrIdentityBindingMismatch = errors.New("store: identity bound to another principal")

ErrIdentityBindingMismatch is a sign-in's refusal when the identity it names is already bound to another principal: issuing its session under the new one would leave it outside the identity's deactivation and epoch checks. Nothing is written.

View Source
var ErrIdentityDeactivated = errors.New("store: identity deactivated")

ErrIdentityDeactivated is the refusal of anything that would give a deactivated, purged or stale-epoch identity a new session, credential or run.

View Source
var ErrIdentityPurged = errors.New("store: identity purged")

ErrIdentityPurged is a reactivation's refusal: a purged identity is a permanent tombstone.

View Source
var ErrIdentityRebound = errors.New("store: the identity was bound since the suspension was planned")

ErrIdentityRebound is a SuspendPlan made before a sign-in bound the identity row: its forms miss the newly bound principal, so the caller reads the row again and makes a new plan.

View Source
var ErrLoginLockNoCapacity = db.ErrAdvisoryLockNoCapacity

ErrLoginLockNoCapacity is the one LockLoginSupersede error a caller may proceed unlocked on (the pool can't spare a connection); any other error means the caller should refuse.

View Source
var ErrNotFound = errors.New("store: not found")

ErrNotFound is returned when a Get* call finds no row.

View Source
var ErrPushPathListCap = errors.New("store: this run has stored its limit of push path lists")

ErrPushPathListCap is RecordPushPathList's refusal: the run already has its limit of lists.

View Source
var ErrRunCapReached = errors.New("store: deployment run cap reached")

ErrRunCapReached is CreateRunUnderCap's refusal: the deployment already holds its cap of non-terminal runs.

View Source
var ErrRunOutputErased = errors.New("store: the run's output was erased")

ErrRunOutputErased is every writer's and reader's answer for a run whose output was erased: the tombstone is checked in the same transaction as the write or read, under the run's lock, so no replica can recreate or serve it.

View Source
var ErrScimGroupExists = errors.New("store: scim group exists")

ErrScimGroupExists is a create whose externalId already names a group.

GroupRemovalSteps are the steps StartGroupRemoval opens, in the order they run.

Functions

func CapAuditTarget added in v0.7.4

func CapAuditTarget(target string) string

CapAuditTarget bounds one audit target. A target already within the cap is returned BYTE-IDENTICAL: audit queries match this column exactly, so the cap must be invisible to every row that is not abusive.

func DeleteCapabilityGrantQ added in v0.8.6

func DeleteCapabilityGrantQ(ctx context.Context, q Querier, id uuid.UUID) error

DeleteCapabilityGrantQ is DeleteCapabilityGrant's statement on q.

func DeleteGovernanceAssignmentQ added in v0.8.6

func DeleteGovernanceAssignmentQ(ctx context.Context, q Querier, id uuid.UUID) error

DeleteGovernanceAssignmentQ is DeleteGovernanceAssignment's statement on q.

func DeleteGovernanceProfileQ added in v0.8.6

func DeleteGovernanceProfileQ(ctx context.Context, q Querier, id uuid.UUID) error

DeleteGovernanceProfileQ is DeleteGovernanceProfile's body on a caller's transaction (q must be a pgx.Tx: it takes the transaction-scoped graph lock) and answers the same errors.

func DeleteRoleMappingQ added in v0.8.6

func DeleteRoleMappingQ(ctx context.Context, q Querier, id uuid.UUID) error

DeleteRoleMappingQ is DeleteRoleMapping's statement on q.

func FederatedActor added in v0.8.0

func FederatedActor(deviceID uuid.UUID, claimed string) string

FederatedActor is the actor a federated row is stored under: the forwarding device's own actor (device:<id>), a slash, then the claimed principal, so an org-principal filter never matches a laptop's claim.

func FederatedDeviceID added in v0.8.0

func FederatedDeviceID(ev types.AuditEvent) *uuid.UUID

FederatedDeviceID reports which device forwarded ev, or nil for the organisation's own row. TRUST BOUNDARY: federated only when both marks agree — data.device_origin.device_id and the FederatedActor prefix naming the same device — so no writer controlling only one can make its own row read as a device's. federatedRowSQL is the same predicate in SQL.

func FederatedRowProblem added in v0.8.0

func FederatedRowProblem(r types.FederatedAuditEvent) string

FederatedRowProblem says why r cannot be stored so its claim re-checks from the stored row, or "" when it can. data must be a JSON object without a top-level device_origin key (this organisation's marker), JSON null, or absent; target must be one CapAuditTarget leaves unchanged; device_id is this organisation's to derive, never the device's to claim. Exported so the API refuses with a 400 before any database work.

func GetCapabilityEnforcementQ added in v0.8.6

func GetCapabilityEnforcementQ(ctx context.Context, q Querier) (map[string]bool, error)

GetCapabilityEnforcementQ is GetCapabilityEnforcement on q.

func GetCapabilityGrantByKeyQ added in v0.8.6

func GetCapabilityGrantByKeyQ(ctx context.Context, q Querier, subjectType, subject, capability, value string, forUpdate bool) (types.CapabilityGrant, error)

GetCapabilityGrantByKeyQ reads the grant at one natural key on q, ErrNotFound when none; forUpdate locks the row.

func GetCapabilityGrantQ added in v0.8.6

func GetCapabilityGrantQ(ctx context.Context, q Querier, id uuid.UUID, forUpdate bool) (types.CapabilityGrant, error)

GetCapabilityGrantQ reads one grant by id on q, ErrNotFound when none; forUpdate locks the row.

func GetGovernanceAssignmentByKeyQ added in v0.8.6

func GetGovernanceAssignmentByKeyQ(ctx context.Context, q Querier, subjectType, subject string, forUpdate bool) (types.GovernanceAssignment, error)

GetGovernanceAssignmentByKeyQ reads the assignment at one natural key on q, ErrNotFound when none.

func GetGovernanceAssignmentQ added in v0.8.6

func GetGovernanceAssignmentQ(ctx context.Context, q Querier, id uuid.UUID, forUpdate bool) (types.GovernanceAssignment, error)

GetGovernanceAssignmentQ reads one assignment by id on q, ErrNotFound when none; forUpdate locks the row for the transaction.

func GetGovernanceProfileQ added in v0.8.6

func GetGovernanceProfileQ(ctx context.Context, q Querier, id uuid.UUID, forUpdate bool) (types.GovernanceProfile, error)

GetGovernanceProfileQ reads one profile row (raw, as GetGovernanceProfile does) on q.

func GetLiveAPITokenQ added in v0.8.6

func GetLiveAPITokenQ(ctx context.Context, q Querier, id uuid.UUID) (types.APIToken, error)

GetLiveAPITokenQ re-reads one token by id on q (a caller's transaction), and only while it can still authenticate: revoked and expired rows are ErrNotFound, as in GetAPITokenByRaw. A decision that must still hold the authority of the token it was made with reads it again through this, not through the context the request was authenticated with.

func GetUserTypeQ added in v0.8.6

func GetUserTypeQ(ctx context.Context, q Querier, id string, forUpdate bool) (types.UserType, error)

GetUserTypeQ is GetUserType on q; forUpdate locks the row for the transaction.

func InsertAuditEvent

func InsertAuditEvent(ctx context.Context, pool *pgxpool.Pool, ev *types.AuditEvent) error

InsertAuditEvent appends a single audit event. Implements audit.Recorder. The Postgres trigger blocks UPDATE/DELETE; this function only ever INSERTs.

ev is taken by POINTER so the hash chain can be handed back: on success ev.PrevHash/ev.RowHash carry the values Postgres computed, and ev.RowHash IS the chain head at that instant, which fanoutRecorder emits to the audit sinks so an external SIEM ends up holding a head hash Wardyn cannot later disown.

Runs in a transaction because pg_advisory_xact_lock must be held across the append: this caller's seq allocation and head read must not interleave with another writer's, keeping seq order and chain order identical. audit_append takes the same lock itself (the only way a row enters the table since 0111); the lock here is re-entrant and free, and keeps the bounded wait below in front of it.

func InsertAuditEventTx added in v0.8.6

func InsertAuditEventTx(ctx context.Context, tx pgx.Tx, ev *types.AuditEvent) error

InsertAuditEventTx is InsertAuditEvent's body on a caller-supplied transaction: the same target cap, pinned lock-wait bound and advisory lock on the chain, so a row written this way links exactly as every other row does.

func ListCapabilityGrantsQ added in v0.8.6

func ListCapabilityGrantsQ(ctx context.Context, q Querier) ([]types.CapabilityGrant, error)

ListCapabilityGrantsQ is ListCapabilityGrants on q.

func ListCapabilityRestrictionsQ added in v0.8.6

func ListCapabilityRestrictionsQ(ctx context.Context, q Querier) (map[string]map[string]bool, error)

ListCapabilityRestrictionsQ is ListCapabilityRestrictions on q.

func ListGovernanceProfilesQ added in v0.8.6

func ListGovernanceProfilesQ(ctx context.Context, q Querier) ([]types.GovernanceProfile, error)

ListGovernanceProfilesQ reads every profile by name on q.

func ListRoleMappingsQ added in v0.8.6

func ListRoleMappingsQ(ctx context.Context, q Querier) ([]types.RoleMapping, error)

ListRoleMappingsQ is ListRoleMappings on q.

func ListUserTypesQ added in v0.8.6

func ListUserTypesQ(ctx context.Context, q Querier) ([]types.UserType, error)

ListUserTypesQ is ListUserTypes on q.

func LockGovernanceAssignmentKey added in v0.8.6

func LockGovernanceAssignmentKey(ctx context.Context, q Querier, subjectType, subject string) error

LockGovernanceAssignmentKey takes the transaction-scoped lock keyed to one assignment's natural key, so a write to that key and the approval of a held change to it serialize. q must be a pgx.Tx.

func LockGovernanceGraph added in v0.8.6

func LockGovernanceGraph(ctx context.Context, q Querier) error

LockGovernanceGraph takes the transaction-scoped lock every write to the governance profile graph holds, so one write at a time sees and changes it. q must be a pgx.Tx.

func LockGovernanceTarget added in v0.8.6

func LockGovernanceTarget(ctx context.Context, q Querier, kind string, parts ...string) error

LockGovernanceTarget takes the transaction-scoped lock for one target of a governance change: the direct write to it and the approval of a held change to it serialize on it. parts are the target's natural key. q must be a pgx.Tx.

func PutCapabilityEnforcementQ added in v0.8.6

func PutCapabilityEnforcementQ(ctx context.Context, q Querier, enabled map[string]bool) (map[string]bool, error)

PutCapabilityEnforcementQ is PutCapabilityEnforcement's statements on q. A held governance change applies through it inside the decision transaction.

func SetCapabilityRestrictionQ added in v0.8.6

func SetCapabilityRestrictionQ(ctx context.Context, q Querier, capability, value string, restricted bool, by string) error

SetCapabilityRestrictionQ is SetCapabilityRestriction's statement on q.

func UpdateUserTypeQ added in v0.8.6

func UpdateUserTypeQ(ctx context.Context, q Querier, t types.UserType) (types.UserType, error)

UpdateUserTypeQ is UpdateUserType's statement on q. A held governance change applies through it inside the decision transaction.

func UpsertCapabilityGrantQ added in v0.8.6

func UpsertCapabilityGrantQ(ctx context.Context, q Querier, g types.CapabilityGrant) (types.CapabilityGrant, error)

UpsertCapabilityGrantQ is UpsertCapabilityGrant's statement on q. A held governance change applies through it inside the decision transaction.

func UpsertGovernanceAssignmentQ added in v0.8.6

func UpsertGovernanceAssignmentQ(ctx context.Context, q Querier, a types.GovernanceAssignment) (types.GovernanceAssignment, error)

UpsertGovernanceAssignmentQ is UpsertGovernanceAssignment's statement on q.

func UpsertRoleMappingQ added in v0.8.6

func UpsertRoleMappingQ(ctx context.Context, q Querier, m types.RoleMapping) (types.RoleMapping, error)

UpsertRoleMappingQ is UpsertRoleMapping's statement on q (the doc above is its contract). A held governance change applies through it inside the decision transaction.

func WithIdentityGuard added in v0.8.6

func WithIdentityGuard(ctx context.Context, g IdentityGuard) context.Context

WithIdentityGuard makes the insert writers that honour it (CreateRun, CreateRunUnderCap, CreateAPIToken, AddSSHKey) read the owner's identity rows FOR SHARE in their own transaction. Suspension takes the same rows FOR UPDATE, so a write either commits before the suspension's first step, where the sweep after it finds it, or reads the deactivation and is refused.

func WriteGovernanceProfileQ added in v0.8.6

func WriteGovernanceProfileQ(ctx context.Context, q Querier, id uuid.UUID, build GovernanceProfileBuild) (types.GovernanceProfile, error)

WriteGovernanceProfileQ is WriteGovernanceProfile's body on a caller's transaction: q must be a pgx.Tx, because the graph lock it takes is transaction-scoped. A held governance change applies through it inside the decision transaction.

Types

type ADOSignInEndState added in v0.8.6

type ADOSignInEndState struct {
	Gen     int64
	Running bool // an end began and has not finished, within ADOSignInEndsStaleAfter
	Reason  string
}

ADOSignInEndState is one person's count.

type ADOSignInEndStore added in v0.8.6

type ADOSignInEndStore interface {
	// BeginADOSignInEnd marks the start of an end of owner's sign-in.
	BeginADOSignInEnd(ctx context.Context, owner, reason string) error
	// FinishADOSignInEnd marks its finish.
	FinishADOSignInEnd(ctx context.Context, owner string) error
	// ADOSignInEnds is owner's count now; a person with no row has gen 0.
	ADOSignInEnds(ctx context.Context, owner string) (ADOSignInEndState, error)
}

ADOSignInEndStore is the sign-in end counter. Optional like RunOutputStore: the api layer type-asserts, and a store without it counts in the process.

type APITokensByPrincipalPager added in v0.8.0

type APITokensByPrincipalPager interface {
	ListAPITokensByPrincipalPage(ctx context.Context, principal string, p Page) ([]types.APIToken, error)
}

APITokensByPrincipalPager is the scoped analogue of Pager for GET /me/tokens, same safe-fallback posture as GrantsByRunPager (ListAPITokensByPrincipal is already WHERE principal=$1).

type AWSSSOSpentTokenStore added in v0.8.0

type AWSSSOSpentTokenStore interface {
	// MarkAWSSSOTokenSpent upserts one spent-token row, idempotent: re-marking
	// an already-spent fingerprint is a no-op, so row age stays "since first
	// spent" for the prune below.
	MarkAWSSSOTokenSpent(ctx context.Context, fingerprint, owner string, markedAt time.Time) error
	// AWSSSOTokenSpent reports whether fingerprint is already known dead; its
	// one caller memoizes the answer, so a miss costs at most one query per
	// fingerprint per process lifetime.
	AWSSSOTokenSpent(ctx context.Context, fingerprint string) (bool, error)
	// PruneAWSSSOSpentTokens deletes rows marked before cutoff, reporting the
	// count removed. Called from the reaper's existing per-tick advisory lock.
	PruneAWSSSOSpentTokens(ctx context.Context, cutoff time.Time) (int, error)
}

AWSSSOSpentTokenStore persists the AWS SSO refresh-token "spent" mark (internal/api/awssso_refresh.go's ssoRefreshSpent map) so it survives a daemon restart, through Store/PG rather than the secret/blob store since the mark is written precisely because that blob store failed.

type ActiveRunsAtPathReader added in v0.7.0

type ActiveRunsAtPathReader interface {
	ActiveRunsAtWorkspacePath(ctx context.Context, workspacePath string) ([]types.AgentRun, error)
}

ActiveRunsAtPathReader answers which OTHER non-terminal runs already operate on one host workspace path, checked on every run create. Its absence falls back to the unbounded ListRuns + an in-Go filter, SAFE (same answer, merely slower) since the warning is advisory and never blocks a launch.

type ActiveRunsByCreatorReader added in v0.7.5

type ActiveRunsByCreatorReader interface {
	ActiveRunsByCreator(ctx context.Context, createdBy, task, agent string) ([]types.AgentRun, error)
}

ActiveRunsByCreatorReader answers which of a person's runs of one task+agent are still non-terminal, so a new sign-in's supersede (supersedeCallerLoginRuns) can end them. Its absence has no fallback; PG implements it in production. task and agent, not "provider": a run row carries no provider column (it lives in the harness.login.start audit datum).

type ApprovalNotifyReader added in v0.8.6

type ApprovalNotifyReader interface {
	// ApprovalEscalations reads, in one query, the escalation state of every approval in ids that has
	// outbox rows. Approvals with none are absent from the map.
	ApprovalEscalations(ctx context.Context, ids []uuid.UUID, now time.Time) (map[uuid.UUID]types.ApprovalEscalation, error)
	// ApprovalNotifyChannelStats returns one row per channel that has outbox rows.
	ApprovalNotifyChannelStats(ctx context.Context, now time.Time) ([]types.ApprovalNotifyChannelStat, error)
}

ApprovalNotifyReader is the read side of the approval notification outbox that the console needs: where each pending approval sits on its escalation schedule, and each channel's delivery history. A capability interface like ApprovalsForRunsPager: an absent implementation leaves the projection and the status endpoint empty, never an error, since both only describe notifications.

type ApprovalsByRunCreatorPager added in v0.5.0

type ApprovalsByRunCreatorPager interface {
	ListApprovalsPageByRunCreator(ctx context.Context, createdBy string, stateFilter types.ApprovalState, p Page) ([]types.ApprovalRequest, error)
}

ApprovalsByRunCreatorPager is the ownership-scoped analogue of Pager.ListApprovalsPage: a member's GET /approvals (no ?run_id=) is scoped to approvals on runs THEY created, via a JOIN on agent_runs (approvals carry no created_by of their own). TRUST BOUNDARY: same fail-closed contract as RunsByCreatorPager — an absent implementation must never fall back to the unscoped list.

type ApprovalsByRunPager added in v0.7.0

type ApprovalsByRunPager interface {
	ListApprovalsPageByRun(ctx context.Context, runID uuid.UUID, stateFilter types.ApprovalState, p Page) ([]types.ApprovalRequest, error)
}

ApprovalsByRunPager is Pager.ListApprovalsPage narrowed to ONE run, the ?run_id= shape of GET /api/v1/approvals. Without it, a run-scoped poll falls back to fetching every approval the deployment has ever written and filtering in Go, a cost growing with deployment age since decided rows are never deleted.

Fail-safe like Pager (not fail-closed like ApprovalsByRunCreatorPager): ownership scoping happens before this is reached (getRunAuthorized), so the fallback is a performance question, not a privilege one.

type ApprovalsForRunsPager added in v0.8.0

type ApprovalsForRunsPager interface {
	// ListPendingApprovalsForRuns returns every state=PENDING approval whose
	// run_id is in runIDs — no other state, since only PENDING can be held or
	// contribute to Pending's count.
	ListPendingApprovalsForRuns(ctx context.Context, runIDs []uuid.UUID) ([]types.ApprovalRequest, error)
	// CountPendingApprovals returns how many approvals in the whole deployment
	// are state=PENDING — GET /me/attention's admin-view pending_approvals,
	// counted in the database rather than by listing every row.
	CountPendingApprovals(ctx context.Context) (int, error)
	// CountPendingApprovalsByRunCreator returns how many PENDING approvals are
	// raised on runs createdBy owns — GET /me/attention's user-view
	// pending_approvals, counted rather than listed.
	CountPendingApprovalsByRunCreator(ctx context.Context, createdBy string) (int, error)
}

ApprovalsForRunsPager is the attention projection's read surface — kept out of Store like every capability interface in this package, since widening Store would silently route a test double's unoverridden method to the unscoped behaviour. SECURITY: an absent implementation fails CLOSED at the api-layer call site (attention projection refuses rather than silently omitting `attention` from every run).

type AttachLease added in v0.8.6

type AttachLease struct {
	RunID     uuid.UUID
	HolderID  uuid.UUID
	Replica   string // the replica serving the holder's socket
	Principal string
	Source    string // "web" or "ssh"
	Since     time.Time
	Epoch     int64 // moves at each takeover of the row
	ExpiresAt time.Time
	Live      bool // read-only: ExpiresAt was ahead of the database's clock
}

AttachLease is one row of run_attach_leases. HolderID is the lease's identity: whatever writes into the terminal must still be the holder.

type AttachLeaseStore added in v0.8.6

type AttachLeaseStore interface {
	// AcquireAttachLease takes runID's lease for l.HolderID when it is free, has lapsed, or is
	// held by from (uuid.Nil: nobody in particular), for ttl. It reports whether it did.
	AcquireAttachLease(ctx context.Context, l AttachLease, from uuid.UUID, ttl time.Duration) (bool, error)
	// RenewAttachLease extends holderID's lease by ttl, reporting whether holderID still held it.
	RenewAttachLease(ctx context.Context, runID, holderID uuid.UUID, ttl time.Duration) (bool, error)
	// HoldsAttachLease reports whether holderID holds runID's lease right now, unlapsed.
	HoldsAttachLease(ctx context.Context, runID, holderID uuid.UUID) (bool, error)
	// ReleaseAttachLease deletes the lease if holderID holds it, reporting whether it did.
	ReleaseAttachLease(ctx context.Context, runID, holderID uuid.UUID) (bool, error)
	// ReserveAttachLease is a take-over: it replaces the live lease held by from (uuid.Nil:
	// whoever holds it) with a reservation for taker, which only taker's own client may take, for
	// ttl, so no queued client of someone else gets the slot the take-over freed. It returns the
	// lease it replaced and the reservation's holder id; found is false when no live lease was
	// held by from.
	ReserveAttachLease(ctx context.Context, runID, from uuid.UUID, taker, replica string, ttl time.Duration) (prev AttachLease, reservation uuid.UUID, found bool, err error)
	// GetAttachLease returns runID's lease, lapsed or not (Live says which).
	GetAttachLease(ctx context.Context, runID uuid.UUID) (AttachLease, bool, error)
}

AttachLeaseStore is the attach lease record. Optional like RunOutputStore: the api layer type-asserts, and a store without it keeps the writer slot in the process.

type AttachTicket added in v0.5.0

type AttachTicket struct {
	RunID     uuid.UUID
	ActorType types.ActorType
	Principal string
	Role      string
	// Via is the delegated lane the ticket was minted on (#1142), nil
	// otherwise; the ticket lane replays it onto the rows it writes.
	Via *types.DelegationVia
	// AuthorizedAt is when the minting request was admitted, and Email the
	// verified email a session revoke may name ("" on the admin-token and local
	// lanes). Redemption checks both against the revoke cutoff (#1474). A zero
	// AuthorizedAt is a row written before the column existed: never exempt.
	AuthorizedAt time.Time
	Email        string
}

AttachTicket is what one redeemed single-use WS attach ticket carries: the run it's bound to, the minting principal (audit attribution), and that principal's role at mint time. The ?ticket= WS lane bypasses humanOrAdminAuth entirely, so this stamped role is the only signal available to re-check owner-or-admin at consume time.

type AuditChainStatus added in v0.6.0

type AuditChainStatus struct {
	// OK is true when every chained row re-hashed to its stored row_hash and
	// linked to its predecessor.
	OK bool `json:"ok"`
	// Checked is how many chained rows the sweep walked.
	Checked int64 `json:"checked"`
	// Legacy is how many rows carry NO hash and sit BELOW the first chained
	// row (pre-migration-0047) — expected, not a failure. A hashless row
	// ABOVE that prefix is a break instead (auditChainWalk.step).
	Legacy int64 `json:"legacy"`
	// FirstSeq/HeadSeq bound the chained range (both 0 when Checked is 0).
	FirstSeq int64 `json:"first_seq"`
	HeadSeq  int64 `json:"head_seq"`
	// HeadHash is the row_hash of the newest chained row: the value to compare
	// against what a SIEM recorded off the sink stream.
	HeadHash string `json:"head_hash,omitempty"`
	// AnchorSeq is the last seq of the newest attested retention drop the sweep started from: the first
	// retained row must chain to that drop's recorded tail. 0 when no partition was ever dropped.
	AnchorSeq int64 `json:"anchor_seq,omitempty"`
	// BrokenSeq/Reason are set only when OK is false.
	BrokenSeq int64  `json:"broken_seq,omitempty"`
	Reason    string `json:"reason,omitempty"`
}

AuditChainStatus is one verification sweep's verdict (migration 0047). OK is the only field an alert should key on; BrokenSeq/Reason name the FIRST row that failed, not every row after it, since one edited row makes every later link mismatch too and listing them all buries the edit.

type AuditChainVerifier added in v0.6.0

type AuditChainVerifier interface {
	VerifyAuditChain(ctx context.Context) (AuditChainStatus, error)
}

AuditChainVerifier is the OPTIONAL store capability behind GET /api/v1/audit/chain/verify: a test fake or non-Postgres store has no chain, and the handler answers 501 rather than reporting a chain it never wrote as clean.

Operator-invoked, never run at boot (O(whole audit log)). Not a substitute for comparing HeadHash against an off-box copy: an actor who can rewrite one row can usually rewrite and re-chain the tail, which then verifies clean. See docs/OPERATIONS.md.

type AuditFilter added in v0.4.4

type AuditFilter struct {
	Since        time.Time       // events at or after this instant
	Until        time.Time       // events strictly before this instant
	Action       string          // exact action, e.g. "run.kill"
	ActionPrefix string          // action family, e.g. "credential." or "egress."
	Actor        string          // exact principal, e.g. "alice@corp.example" (human evidence)
	ActorType    types.ActorType // human / agent / system
	Outcome      string          // success / failure / warn
	Origin       string          // AuditOriginDevice or AuditOriginOrganisation
	// ActorAlt is a second stored actor that also matches Actor: the "subject:<id>"
	// a row holds for the person Actor names under WARDYN_AUDIT_SEAL=full. Rows
	// written before it was turned on keep the name, so both must match.
	ActorAlt string
	// DataContains is a JSON object the row's data must contain (Postgres
	// `data @> $n::jsonb`): every key present with an equal value, objects
	// recursively. A string, not a map, so the filter stays comparable (IsZero).
	DataContains string
}

AuditFilter narrows the audit feed to answer the questions a bigger window never can — "what happened between 14:00 and 16:00", "every secret.write this quarter", "every failure" — where the answer is 40 events inside 200k. The zero value matches everything, so an unfiltered read keeps taking the plain QueryRecentAuditEventsPage / QueryAuditEventsPage path unchanged.

It renders TWO ways from one declaration — SQL (where) for the Pager path and Go (Matches) for the fetch-all fallback — because a filter applied on only one of them returns silently UNFILTERED events on the other, which in a governance product is a quiet wrong answer. Keep the two in step; they are deliberately adjacent.

Actor IS filterable: for a HUMAN event it is the operator/member principal (e.g. "alice@corp.example"), so ?actor= answers "everything developer X did" — the per-principal evidence a vendor/compliance question needs and that no wider window yields. (An earlier note dismissed actor as filterable because for an AGENT event it is the per-run SPIFFE ID and would merely restate ?run_id= — but that reasoning only ever considered agent events; a human's approvals, kills, and policy writes all carry their principal here, and that is exactly what the finding asks for.) The agent-side "everything under X's runs" view still needs the agent_runs.created_by join, which does not fit this table-local filter.

func (AuditFilter) IsZero added in v0.4.4

func (f AuditFilter) IsZero() bool

IsZero reports whether the filter narrows nothing.

func (AuditFilter) Keep added in v0.4.4

func (f AuditFilter) Keep(events []types.AuditEvent) []types.AuditEvent

Keep returns the events matching f, in order. A zero filter returns events unchanged (no copy).

func (AuditFilter) Matches added in v0.4.4

func (f AuditFilter) Matches(ev types.AuditEvent) bool

Matches reports whether ev satisfies every set predicate.

type AuditPartitionExporter added in v0.8.6

type AuditPartitionExporter interface {
	// ExportAuditPartition reads one closed partition in seq order from ONE snapshot. header is called
	// once, before any row, with the manifest of that snapshot; row once per row. ErrNotFound when name
	// is not a partition of the audit log, ErrAuditPartitionOpen when it can still receive rows; in
	// both cases neither callback ran. An error from either callback stops the read and is returned.
	ExportAuditPartition(ctx context.Context, name string, header func(PartitionManifest) error, row func(AuditPartitionRow) error) error
}

AuditPartitionExporter is the OPTIONAL store capability behind GET /audit/export?partition=. A test fake or non-Postgres store has no partitions.

type AuditPartitionRow added in v0.8.6

type AuditPartitionRow struct {
	Seq        int64
	RecordedUS int64
	TimeUS     int64
	ID         uuid.UUID
	RunID      *uuid.UUID
	ActorType  string
	Actor      string
	Action     string
	Target     string
	Outcome    string
	SourceIP   string
	// Data is the jsonb's own text form, the exact bytes audit_row_hash embeds; nil for SQL NULL.
	Data *string
	// PrevHash and RowHash are nil for a row from before the chain began.
	PrevHash *string
	RowHash  *string
	// FoldHash is what the digest folds: RowHash, or for a hashless row the hash it would have had.
	FoldHash string
}

AuditPartitionRow is one stored row of a partition, exactly as the table holds it: the columns audit_row_hash covers, the hashes, and the position.

type AuditRetention added in v0.8.6

type AuditRetention interface {
	AuditRetentionStatus(ctx context.Context) (AuditRetentionStatus, error)
	// DropAuditPartition calls audit_retention_drop. A refusal is *AuditRetentionRefused; a name that is
	// not a partition of the audit log is ErrNotFound.
	DropAuditPartition(ctx context.Context, partition, digest, actor string) (AuditRetentionDrop, error)
	// AutodropAuditPartition drops the oldest partition if it is eligible, computing its digest in the
	// database, as the system actor. ok is false (and nothing was dropped) when it is not.
	AutodropAuditPartition(ctx context.Context) (drop AuditRetentionDrop, ok bool, err error)
	// SetAuditRetentionPolicy calls audit_retention_set_policy.
	SetAuditRetentionPolicy(ctx context.Context, days int) (AuditRetentionPolicyChange, error)
	// EnsureAuditPartitions calls audit_ensure_partitions and returns how many months it created.
	EnsureAuditPartitions(ctx context.Context, months int) (int, error)
	// AuditPartitionsAhead is how many months past the current one already have a partition.
	AuditPartitionsAhead(ctx context.Context) (int, error)
}

AuditRetention is the OPTIONAL store capability behind the retention endpoints, the daily partition ensure and the leader sweeper's autodrop: a test fake or non-Postgres store has no partitions.

type AuditRetentionDrop added in v0.8.6

type AuditRetentionDrop = types.AuditRetentionDrop

The retention wire types live in internal/types, shared with pkg/client; these are their store-side names.

type AuditRetentionPartition added in v0.8.6

type AuditRetentionPartition = types.AuditRetentionPartition

The retention wire types live in internal/types, shared with pkg/client; these are their store-side names.

type AuditRetentionPolicy added in v0.8.6

type AuditRetentionPolicy = types.AuditRetentionPolicy

The retention wire types live in internal/types, shared with pkg/client; these are their store-side names.

type AuditRetentionPolicyChange added in v0.8.6

type AuditRetentionPolicyChange = types.AuditRetentionPolicyChange

The retention wire types live in internal/types, shared with pkg/client; these are their store-side names.

type AuditRetentionRefused added in v0.8.6

type AuditRetentionRefused struct {
	Reason    string
	Partition string
}

AuditRetentionRefused is a drop the database refused. Reason is one of the Retention* constants.

func (*AuditRetentionRefused) Error added in v0.8.6

func (e *AuditRetentionRefused) Error() string

type AuditRetentionStatus added in v0.8.6

type AuditRetentionStatus = types.AuditRetentionStatus

The retention wire types live in internal/types, shared with pkg/client; these are their store-side names.

type AuditSplitRange added in v0.8.6

type AuditSplitRange struct {
	Name  string
	Rows  int64
	SeqLo int64
	SeqHi int64
	// RecordedLo and RecordedHi are the smallest and largest recorded_at the range's rows now carry.
	RecordedLo time.Time
	RecordedHi time.Time
	// Digest is the range's partition digest (audit_partition_digest), equal to the fold over its source rows.
	Digest string
}

AuditSplitRange is one seq-contiguous range the legacy partition was split into.

type AuditSplitRefused added in v0.8.6

type AuditSplitRefused struct {
	Reason string
	Detail string
}

AuditSplitRefused is a legacy split that was not attempted, or was undone, for a named reason.

func (*AuditSplitRefused) Error added in v0.8.6

func (e *AuditSplitRefused) Error() string

type AuditSplitResult added in v0.8.6

type AuditSplitResult struct {
	Rows    int64
	Checked int64
	Legacy  int64
	Ranges  []AuditSplitRange
}

AuditSplitResult is what one split did. Checked and Legacy are the chain verifier's counts, which were the same before the split and after it: the split removed no row from the chain.

func SplitLegacyAudit added in v0.8.6

func SplitLegacyAudit(ctx context.Context, tx pgx.Tx) (AuditSplitResult, error)

SplitLegacyAudit splits the legacy partition, which holds all of the audit history from before 0.8.6 under one recorded_at, into seq-contiguous ranges that ordinary retention can drop one at a time. It is the one-shot offline tool behind `wardynd -audit-split-legacy`.

It runs on tx, a read-write transaction on the migrator's connection that the caller commits only when this returns nil. The caller has already made sure no writer is running; this takes db.AuditPartitionLockKey and then db.AuditChainLockKey for the transaction (the order the retention drop takes them), so a writer that slipped in is held out, not interleaved.

A range is not a month of the rows' own time. The boundaries are where the running maximum of "time" first crosses a month, walking the rows in seq order, and each row's new recorded_at is that running maximum (never past the cutover). A spool replay writes an old "time" late, so splitting by "time" would scatter a month through the chain and leave an interior link behind when its range was dropped; the running maximum cannot go backwards, so every range is a contiguous prefix of chain order, and its largest recorded_at is the largest "time" it holds. row_hash does not cover recorded_at, so no hash changes.

Every range is copied to a new table, attached, and proved against its source (the same fold audit_partition_digest computes, and a row-for-row comparison), the legacy table is dropped, the ranges are written to the expected manifest and a 'split' anchor each, and the chain is verified again. The verification must see exactly the rows it saw before.

type BrandingStore added in v0.8.0

type BrandingStore interface {
	// GetBranding returns the record, or ErrNotFound for an unbranded console.
	GetBranding(ctx context.Context) (types.Branding, error)
	// PutBranding replaces the record. keepLogo leaves the stored logo as it
	// is and ignores b.Logo/b.LogoType.
	PutBranding(ctx context.Context, b types.Branding, keepLogo bool) (types.Branding, error)
	// DeleteBranding removes the record; false when there was none.
	DeleteBranding(ctx context.Context) (bool, error)
	// empty logo removes it. false when there is no record to attach it to.
	SetBrandingLogo(ctx context.Context, logo []byte, logoType string, fromFile bool, updatedBy string) (bool, error)
}

BrandingStore is the OPTIONAL store capability behind console branding, optional for DeviceStore's reason: a fake without it answers the public read as unbranded and refuses a write, never a silent no-op.

type CapabilityGrantsForPager added in v0.8.0

type CapabilityGrantsForPager interface {
	ListCapabilityGrantsForPage(ctx context.Context, users, groups []string, userType string, p Page) ([]types.CapabilityGrant, error)
}

CapabilityGrantsForPager is the scoped analogue of Pager for GET /me/capabilities, same safe-fallback posture as GrantsByRunPager; exists for the uniform list-route contract (?limit=&offset=+X-Wardyn-Truncated), not because grant lists routinely truncate.

type DelegateStore added in v0.8.0

type DelegateStore interface {
	CreateDelegate(ctx context.Context, d types.Delegate, raw string) (types.Delegate, error)
	GetDelegateByRaw(ctx context.Context, raw string) (types.Delegate, error)
	ListDelegates(ctx context.Context) ([]types.Delegate, error)
	RevokeDelegate(ctx context.Context, id uuid.UUID, now time.Time) (types.Delegate, error)
	MintDelegatedToken(ctx context.Context, t types.DelegatedToken, raw string, now time.Time) (types.DelegatedToken, error)
	GetDelegatedTokenByRaw(ctx context.Context, raw string, now time.Time) (types.DelegatedToken, error)
	GetDelegatedTokenByID(ctx context.Context, id uuid.UUID, now time.Time) (types.DelegatedToken, error)
}

DelegateStore is optional, type-asserted like DeviceStore: without it the exchange and delegated lane fail closed instead of silently no-op.

Authenticating reads filter revoked/expired rows in their WHERE, so revoked, expired, unknown, and mismatched credentials all answer ErrNotFound — the boundary gives no oracle signal.

type DeprovisionFailure added in v0.8.6

type DeprovisionFailure struct {
	IdentityID uuid.UUID
	Kind       string
	Step       string
	LastError  string
}

DeprovisionFailure is a ledger row of a suspension or purge that is still pending and has failed at least once: the step, and the error its last attempt ended on. The console's SCIM card lists them.

type DeprovisionJob added in v0.8.6

type DeprovisionJob struct {
	IdentityID uuid.UUID
	Kind       string
	Step       string
	Target     string
	Done       bool
	Attempts   int
	LastError  string
	Detail     map[string]int
}

DeprovisionJob is one row of the step ledger.

type DeviceIngestResult added in v0.8.0

type DeviceIngestResult struct {
	Accepted int
	Reset    bool
}

DeviceIngestResult reports what one IngestDeviceAudit call did. Accepted is how many rows were newly appended: 0 on any refusal (whole batch is one transaction) and also on an idempotent retry. Reset is true when accepted rows began with a genesis (empty-PrevHash) row while the device already had a recorded chain — a "device chain reset", accepted rather than refused.

type DeviceStore added in v0.8.0

type DeviceStore interface {
	MintEnrolmentToken(ctx context.Context, token string, t types.DeviceEnrolmentToken) (types.DeviceEnrolmentToken, error)
	ConsumeEnrolmentToken(ctx context.Context, token string, now time.Time) (types.DeviceEnrolmentToken, bool, error)
	ListEnrolmentTokens(ctx context.Context, now time.Time) ([]types.DeviceEnrolmentToken, error)
	RevokeEnrolmentToken(ctx context.Context, id uuid.UUID, now time.Time) (types.DeviceEnrolmentToken, error)
	CreateDevice(ctx context.Context, d types.Device, raw string) (types.Device, error)
	GetDeviceByRaw(ctx context.Context, raw string) (types.Device, error)
	TouchDevice(ctx context.Context, id uuid.UUID, now time.Time) error
	ListDevices(ctx context.Context) ([]types.Device, error)
	RevokeDevice(ctx context.Context, id uuid.UUID, now time.Time) (types.Device, error)
	IngestDeviceAudit(ctx context.Context, deviceID uuid.UUID, peer string, rows []types.FederatedAuditEvent) (DeviceIngestResult, error)
	ListAuditEventsAfterSeq(ctx context.Context, seq int64, limit int) ([]types.FederatedAuditEvent, error)
	GetFederationCursor(ctx context.Context) (seq int64, rowHash string, err error)
	SetFederationCursor(ctx context.Context, seq int64, rowHash string) error
}

DeviceStore is the optional store capability behind hybrid enrolment and audit federation (docs/design/0.8/PLAN.md, epic #78). Optional like Pager and AuditChainVerifier: a test fake or future non-Postgres store implements neither, so mounting routes must type-assert and answer 501/401 rather than silently no-op. SECURITY (fail-closed): nothing here has a default no-op on Store, so a skipped assertion is a compile error and a failed one has an explicit absent branch.

GetDeviceByRaw's WHERE carries `revoked_at IS NULL` so a revoked credential is not an oracle.

type EndedKept added in v0.8.0

type EndedKept struct {
	LostAt, KeptAfter, Now time.Time
}

EndedKept is the condition a write on a run kept by its OWN end (LostEnded) lands under during its files grace: the row must still carry the exact ended mark decided on (LostAt), after KeptAfter (Now less the ended-run grace). A grace that runs out between check and write refuses the write; nothing here moves the mark, so no write renews the grace. A revive also needs the run's end to be after Now.

type ErrGovernanceChangeNotPending added in v0.8.6

type ErrGovernanceChangeNotPending struct {
	State  string
	Lapsed bool
}

ErrGovernanceChangeNotPending is a decision's refusal when the change is no longer pending. State is what it is now; Lapsed is true when this very decision found it past expires_at and moved it to expired.

func (*ErrGovernanceChangeNotPending) Error added in v0.8.6

type ErrGovernanceChangePending added in v0.8.6

type ErrGovernanceChangePending struct{ ID uuid.UUID }

ErrGovernanceChangePending is a proposal's refusal when a live change already holds the target: ID is that change.

func (*ErrGovernanceChangePending) Error added in v0.8.6

type ErrProfileHasChildren added in v0.8.6

type ErrProfileHasChildren struct{ Names []string }

ErrProfileHasChildren is DeleteGovernanceProfile's refusal when other profiles still compose on the row: Names are those children, sorted.

func (*ErrProfileHasChildren) Error added in v0.8.6

func (e *ErrProfileHasChildren) Error() string

type ExpiredGovernanceChange added in v0.8.6

type ExpiredGovernanceChange struct {
	ID        uuid.UUID
	TargetKey string
}

ExpiredGovernanceChange is a lapsed change a proposal moved to expired: its id and its own target key, which differs from the proposal's when it was a create of the same name.

type GovernanceDecideFunc added in v0.8.6

type GovernanceDecideFunc func(q Querier, ch types.GovernanceChange) error

GovernanceDecideFunc runs inside the decision transaction with the change as it was locked. A nil error commits the decision; ErrGovernanceChangeStale commits the change as stale; any other error rolls everything back, the change still pending.

type GovernanceDecision added in v0.8.6

type GovernanceDecision struct {
	// To is types.GovernanceChangeApplied or types.GovernanceChangeRejected.
	To      string
	By      string
	ByEmail string
	Reason  string
}

GovernanceDecision is what a decision records on the change row.

type GovernanceProfileBuild added in v0.8.6

type GovernanceProfileBuild func(all []types.GovernanceProfile) (types.GovernanceProfile, error)

GovernanceProfileBuild produces the row a graph write stores, given every profile as the write transaction sees it under the graph lock. A refusal it returns aborts the write unchanged.

type GrantsByRunPager added in v0.8.0

type GrantsByRunPager interface {
	ListGrantsByRunPage(ctx context.Context, runID uuid.UUID, p Page) ([]types.CredentialGrant, error)
}

GrantsByRunPager is the scoped analogue of Pager for GET /runs/{id}/grants: ListGrantsByRun is already WHERE run_id=$1, so an absent implementation is safe (not fail-closed) — the fallback windows the same scoped rows in Go.

type IdentityGuard added in v0.8.6

type IdentityGuard struct {
	Principal string
	Epoch     int64
}

IdentityGuard names the owner a write acts for and the authority epoch its caller was admitted under. Epoch < 0 means the caller carries no epoch (an API token, a request-free caller): only a deactivation refuses.

type IdentityUpdate added in v0.8.6

type IdentityUpdate struct {
	ExternalID string
	UserName   string
	Emails     []string
	// Reactivate clears the deactivation and the purge schedule on the identity and on every row
	// bound to the same principal. A purged identity is ErrIdentityPurged.
	Reactivate bool
	// PeoplePrincipals are the people rows whose deactivation a reactivation clears.
	PeoplePrincipals []string
}

IdentityUpdate is one SCIM write to an identity row. A projection update never touches a binding column; ExternalID is set only when the caller has already checked it against the binding.

type JobKey added in v0.8.6

type JobKey struct{ Step, Target string }

JobKey names a ledger row within one identity and kind.

type LeaverStore added in v0.8.6

type LeaverStore interface {
	GetIdentity(ctx context.Context, id uuid.UUID) (PrincipalIdentity, error)
	SearchIdentities(ctx context.Context, issuer, attr, value string, limit int) ([]PrincipalIdentity, error)
	IdentitiesByAlias(ctx context.Context, issuer, valueLower string) ([]PrincipalIdentity, error)
	IdentityAliasValues(ctx context.Context, id uuid.UUID) ([]string, error)
	CreateScimIdentity(ctx context.Context, issuer, tenantID, objectID string, u IdentityUpdate, now time.Time) (PrincipalIdentity, bool, error)
	ApplyIdentityUpdate(ctx context.Context, id uuid.UUID, u IdentityUpdate, now time.Time) (PrincipalIdentity, error)
	SuspendIdentity(ctx context.Context, p SuspendPlan) (SuspendResult, error)
	PrincipalsByEmail(ctx context.Context, emails []string) ([]string, error)
	ListNonTerminalRunsBy(ctx context.Context, createdBy string) ([]types.AgentRun, error)
	EnsureDeprovisionJobs(ctx context.Context, identityID uuid.UUID, kind string, keys []JobKey) error
	ListDeprovisionJobs(ctx context.Context, identityID uuid.UUID, kind string) ([]DeprovisionJob, error)
	FinishDeprovisionJob(ctx context.Context, identityID uuid.UUID, kind string, k JobKey, detail map[string]int) error
	FailDeprovisionJob(ctx context.Context, identityID uuid.UUID, kind string, k JobKey, cause error) error
	ReopenDeprovisionJob(ctx context.Context, identityID uuid.UUID, kind string, k JobKey) error
	MarkIdentityPurged(ctx context.Context, id uuid.UUID, requireDue bool) (bool, error)
	PurgeDueIdentities(ctx context.Context, limit int) ([]uuid.UUID, error)
	PendingLeavers(ctx context.Context, idleFor time.Duration, limit int) ([]PendingLeaver, error)
	DeleteUserSubjectRows(ctx context.Context, id uuid.UUID, principals, emails []string) (grants, assignments, kept int64, err error)
	HeldEmails(ctx context.Context, id uuid.UUID, principals, emails []string) ([]string, error)
	ListDeactivatedIdentities(ctx context.Context, limit int) ([]PrincipalIdentity, error)
	ListDeprovisionFailures(ctx context.Context, limit int) ([]DeprovisionFailure, error)
}

LeaverStore is optional, like PrincipalIdentityStore: callers type-assert it.

type LoginIdentity added in v0.8.6

type LoginIdentity struct {
	Principal string
	Issuer    string
	TenantID  string
	ObjectID  string
	Email     string
}

LoginIdentity is what an approved sign-in tells the store.

type LoginLocker added in v0.8.0

type LoginLocker interface {
	LockLoginSupersede(ctx context.Context, actor string) (release func(), err error)
}

LoginLocker serializes one person's sign-in launches, and the credential capture that follows one, against every other replica — the piece the deterministic tie-break in api.supersedeOlderLoginRuns couldn't supply. Its absence falls back UNLOCKED (as serialized as 0.7.8 was); PG implements it in production.

Keyed by ACTOR, not credential scope: under the `shared` roster every sign-in resolves to the same empty scope owner, so a scope-keyed lock would serialize the whole deployment without serializing what it needs to.

type MemRunPATs added in v0.8.2

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

MemRunPATs is RunPATStore in memory, for the tests of every lane that creates or revokes a token and has no database.

func NewMemRunPATs added in v0.8.2

func NewMemRunPATs() *MemRunPATs

NewMemRunPATs returns an empty MemRunPATs.

func (*MemRunPATs) InsertRunPAT added in v0.8.2

func (m *MemRunPATs) InsertRunPAT(_ context.Context, p RunPAT) error

func (*MemRunPATs) LastRunPAT added in v0.8.2

func (m *MemRunPATs) LastRunPAT(_ context.Context, owner, providerRowID string) (RunPAT, bool, error)

func (*MemRunPATs) ListRunPATs added in v0.8.2

func (m *MemRunPATs) ListRunPATs(_ context.Context, runID uuid.UUID) ([]RunPAT, error)

func (*MemRunPATs) ListUnrevokedRunPATs added in v0.8.2

func (m *MemRunPATs) ListUnrevokedRunPATs(_ context.Context, f RunPATFilter) ([]RunPAT, error)

func (*MemRunPATs) MarkRunPATRevoked added in v0.8.2

func (m *MemRunPATs) MarkRunPATRevoked(_ context.Context, runID, authorizationID uuid.UUID, reason, lastError string) (bool, error)

func (*MemRunPATs) NoteRunPATRevokeFailed added in v0.8.2

func (m *MemRunPATs) NoteRunPATRevokeFailed(_ context.Context, runID, authorizationID uuid.UUID, lastError string) error

type PG

type PG struct {
	Pool *pgxpool.Pool

	// Now is THE APP CLOCK, letting a test run one that disagrees with the
	// database's. Nil means time.Now. Every stamp this store writes belongs on
	// the DATABASE's clock; the app clock only measures an ELAPSED TIME (a
	// difference of two readings, carrying no skew) that a statement then
	// subtracts from the database's own now(). See db.AppClockAgeSQL.
	Now func() time.Time
}

PG is the Postgres-backed Store: its methods (defined in store.go) hold the query bodies directly, so there is exactly one implementation of each query.

func NewPG

func NewPG(pool *pgxpool.Pool) PG

NewPG returns a PG Store over pool.

func (PG) ADOSignInEnds added in v0.8.6

func (s PG) ADOSignInEnds(ctx context.Context, owner string) (ADOSignInEndState, error)

ADOSignInEnds — see ADOSignInEndStore.

func (PG) AWSSSOTokenSpent added in v0.8.0

func (s PG) AWSSSOTokenSpent(ctx context.Context, fingerprint string) (bool, error)

AWSSSOTokenSpent reports whether fingerprint has a persisted spent-token row.

func (PG) AcquireAttachLease added in v0.8.6

func (s PG) AcquireAttachLease(ctx context.Context, l AttachLease, from uuid.UUID, ttl time.Duration) (bool, error)

AcquireAttachLease — see AttachLeaseStore.

func (PG) ActiveRunsAtWorkspacePath added in v0.7.0

func (s PG) ActiveRunsAtWorkspacePath(ctx context.Context, workspacePath string) ([]types.AgentRun, error)

ActiveRunsAtWorkspacePath returns the non-terminal runs bound to one host workspace path. Uses the POSITIVE state list, same reasoning as ActiveRunsByCreator: a forgotten new state merely under-warns rather than mis-treating it as active.

ponytail: no new index. workspace_path is selective and the state filter is a cheap check over matching rows; a composite (workspace_path, state) index is the upgrade if a deployment ever has enough runs on ONE path.

func (PG) ActiveRunsByCreator added in v0.7.5

func (s PG) ActiveRunsByCreator(ctx context.Context, createdBy, task, agent string) ([]types.AgentRun, error)

ActiveRunsByCreator returns createdBy's non-terminal runs of one task+agent. Uses the POSITIVE state list (types.NonTerminalRunStates): a state added to the enum and forgotten here merely misses a supersede, whereas `NOT IN (terminal)` would hand a new terminal state to the kill cascade.

ponytail: no new index. agent_runs_created_by_idx already indexes the selective column and one person owns few runs; upgrade to a composite if a deployment ever has enough history to notice.

func (PG) AddSSHKey added in v0.5.0

func (s PG) AddSSHKey(ctx context.Context, k types.SSHPublicKey) (types.SSHPublicKey, error)

AddSSHKey inserts a new registered key. Returns ErrConflict when the fingerprint (PK) already exists — a 23505 here means that exact key material is already registered.

func (PG) AddScimGroupMembers added in v0.8.6

func (s PG) AddScimGroupMembers(ctx context.Context, id uuid.UUID, identities []uuid.UUID, now time.Time) error

AddScimGroupMembers records each identity as a member, leaving one already recorded as it is and skipping an id that names no identity row.

func (PG) AddWorkspaceEgressDecision added in v0.5.0

func (s PG) AddWorkspaceEgressDecision(ctx context.Context, id uuid.UUID, host string, allow bool, maxApprovedEgress int) (types.Workspace, error)

AddWorkspaceEgressDecision records one `always`-scoped egress decision for host: on allow it is added to approved_egress (capped, deduped) and removed from denied_egress; on deny the mirror. Returns ErrConflict, not a bare "not found", when id exists but the cap guard refused the write.

func (PG) AppendRunOutputChunk added in v0.8.6

func (s PG) AppendRunOutputChunk(ctx context.Context, runID uuid.UUID, replica string, b []byte, keep int) (bool, error)

AppendRunOutputChunk — see RunOutputChunkStore.

func (PG) ApplyIdentityUpdate added in v0.8.6

func (s PG) ApplyIdentityUpdate(ctx context.Context, id uuid.UUID, u IdentityUpdate, now time.Time) (PrincipalIdentity, error)

ApplyIdentityUpdate applies u to the identity in one transaction: the projection, then the reactivation when asked. A purged identity refuses a reactivation and nothing else changes.

func (PG) ApprovalEscalations added in v0.8.6

func (s PG) ApprovalEscalations(ctx context.Context, ids []uuid.UUID, now time.Time) (map[uuid.UUID]types.ApprovalEscalation, error)

ApprovalEscalations — see ApprovalNotifyReader.

func (PG) ApprovalNotifyChannelStats added in v0.8.6

func (s PG) ApprovalNotifyChannelStats(ctx context.Context, now time.Time) ([]types.ApprovalNotifyChannelStat, error)

ApprovalNotifyChannelStats — see ApprovalNotifyReader.

func (PG) AuditHeadSeq added in v0.8.0

func (s PG) AuditHeadSeq(ctx context.Context) (int64, error)

AuditHeadSeq is this deployment's newest audit_events seq, 0 on an empty table: the head the forwarder measures lag against, read every tick whether or not a push succeeds.

func (PG) AuditKeyDestroyedAt added in v0.8.6

func (s PG) AuditKeyDestroyedAt(ctx context.Context, owner string) (time.Time, bool, error)

AuditKeyDestroyedAt is when the latest generation of owner's audit-seal key was destroyed. ok is false while none has been.

func (PG) AuditPartitionsAhead added in v0.8.6

func (s PG) AuditPartitionsAhead(ctx context.Context) (int, error)

func (PG) AuditPrincipalOf added in v0.8.6

func (s PG) AuditPrincipalOf(ctx context.Context, id string) (string, bool, error)

AuditPrincipalOf is the principal a subject id names. ok is false for an id that is not a uuid or that no bound identity row carries.

func (PG) AuditRetentionStatus added in v0.8.6

func (s PG) AuditRetentionStatus(ctx context.Context) (AuditRetentionStatus, error)

func (PG) AuditSubjectFor added in v0.8.6

func (s PG) AuditSubjectFor(ctx context.Context, principal string) (string, bool, error)

AuditSubjectFor is principal's subject id. ok is false when the person has no identity row.

func (PG) AutodropAuditPartition added in v0.8.6

func (s PG) AutodropAuditPartition(ctx context.Context) (AuditRetentionDrop, bool, error)

func (PG) BeginADOSignInEnd added in v0.8.6

func (s PG) BeginADOSignInEnd(ctx context.Context, owner, reason string) error

BeginADOSignInEnd — see ADOSignInEndStore.

func (PG) BlankRunTasks added in v0.8.6

func (s PG) BlankRunTasks(ctx context.Context, createdBy string) (int, error)

func (PG) ClaimSourceActiveRun added in v0.5.0

func (s PG) ClaimSourceActiveRun(ctx context.Context, id, runID uuid.UUID) error

ClaimSourceActiveRun fences a source's in-flight scan run, mirroring ClaimWorkspaceActiveRun. Returns ErrConflict when another run holds the slot.

func (PG) ClaimStaleRunWatchers added in v0.5.0

func (s PG) ClaimStaleRunWatchers(ctx context.Context, owner string, staleAfter time.Duration) ([]types.AgentRun, error)

ClaimStaleRunWatchers atomically takes the watcher lease, for owner, on every non-terminal run with a sandbox whose lease has been silent longer than staleAfter, returning the runs claimed for re-adoption.

The conditional UPDATE ... RETURNING is the mutual exclusion: replicas sweeping at once re-evaluate WHERE against the row version they blocked on, so the loser sees the winner's fresh heartbeat and gets no row — no advisory lock needed (one per in-flight run via db.TryAdvisoryLock would exhaust the pool, which holds a connection for the whole lock).

Two predicates carry the sweep's safety: nonTerminalRunStates matches agent_runs_watcher_sweep_idx's predicate verbatim (complement of types.RunState.IsTerminal, so a finished run is never re-adopted); and a non-empty sandbox_ref limits it to runs with something to watch — a row exists through the whole pre-dispatch window (grant writes, then a multi-minute build) and would otherwise look abandoned, so cleanup there stays boot-only.

A KEPT run (lost_at set, migration 0073) is also never claimed: its agent is stopped on purpose, and a watcher would read that as exit and finalize the run, tearing down files it's kept for. updated_at is deliberately not touched: it's the idle reaper's activity signal, and a lease write isn't activity.

func (PG) ClaimWorkspaceActiveRun

func (s PG) ClaimWorkspaceActiveRun(ctx context.Context, id, runID uuid.UUID, expected *uuid.UUID) (types.Workspace, bool, error)

ClaimWorkspaceActiveRun compare-and-sets active_run_id from expected (possibly nil) to runID, the serial-import-step gate: of two concurrent launches racing the same slot, the loser gets applied=false and must NOT launch.

func (PG) ClearPauseSettle added in v0.8.6

func (s PG) ClearPauseSettle(ctx context.Context, id uuid.UUID, due time.Time) error

ClearPauseSettle — see PauseSettler.

func (PG) ClearRunContainmentError added in v0.8.0

func (s PG) ClearRunContainmentError(ctx context.Context, id uuid.UUID) (bool, error)

ClearRunContainmentError — see RunLeaser.

func (PG) ClearRunPaused added in v0.8.0

func (s PG) ClearRunPaused(ctx context.Context, id uuid.UUID) (bool, error)

ClearRunPaused — see RunPauser.

func (PG) ClearSourceActiveRun added in v0.5.0

func (s PG) ClearSourceActiveRun(ctx context.Context, id, runID uuid.UUID) error

ClearSourceActiveRun releases the fence (idempotent; only the named run's claim is cleared, so a stale clear can't stomp a newer claim).

func (PG) ClearWorkspaceActiveRun

func (s PG) ClearWorkspaceActiveRun(ctx context.Context, id, runID uuid.UUID) (bool, error)

ClearWorkspaceActiveRun clears active_run_id ONLY while it still points at runID, so a terminal run's cleanup can never clobber a step that was concurrently launched and now owns the pointer.

func (PG) ConsumeAttachTicket added in v0.5.0

func (s PG) ConsumeAttachTicket(ctx context.Context, token string, now time.Time) (AttachTicket, bool, error)

ConsumeAttachTicket redeems token exactly once, returning the ticket it stood for. The DELETE ... RETURNING is the single-use guarantee: two racing redemptions can only have one return a row.

The run-id binding is checked by the CALLER, not here, so a redemption against the wrong run still BURNS the ticket (a leaked ticket probed against a guessed run must not survive the probe). A miss and an expired row are both (ok=false), indistinguishable to the caller.

func (PG) ConsumeEnrolmentToken added in v0.8.0

func (s PG) ConsumeEnrolmentToken(ctx context.Context, token string, now time.Time) (types.DeviceEnrolmentToken, bool, error)

ConsumeEnrolmentToken redeems token exactly once: a single-row conditional UPDATE...RETURNING, the same atomic consume-once shape as ConsumeAttachTicket, adapted to an UPDATE since the row is worth keeping after redemption for CreateDevice. TRUST BOUNDARY: an unknown, already-consumed, and expired token all answer (zero, false, nil) alike — no oracle, matching ConsumeAttachTicket.

func (PG) CountActiveRunsBy added in v0.7.0

func (s PG) CountActiveRunsBy(ctx context.Context, createdBy string) (int, error)

CountActiveRunsBy counts createdBy's runs that have not ended yet — the governance quota's one read (GovernanceLimits.MaxConcurrentRuns).

The state predicate is a POSITIVE list (types.NonTerminalRunStates), not `NOT IN (terminal)`: a state added to the enum and forgotten here merely undercounts, whereas the negated form would treat a new terminal state as active and wedge a capped member with nothing to stop it. Pinned against RunState.IsTerminal by a test in internal/types.

ponytail: no new index — agent_runs_created_by_idx already covers the selective column and the state filter is cheap over one member's rows.

func (PG) CountApprovalsForRun added in v0.7.2

func (s PG) CountApprovalsForRun(ctx context.Context, runID uuid.UUID) (int, error)

CountApprovalsForRun returns how many approvals a run has raised, in ANY state — the per-run DoS bound behind handleInternalRequestApproval (maxApprovalsPerRun), since a sandbox picks the hosts it asks about and could otherwise raise rows without limit. Counted in the database rather than List + filter in Go, which would read every approval row in the deployment.

func (PG) CountHiddenRuns added in v0.8.0

func (s PG) CountHiddenRuns(ctx context.Context, f RunFilter) (olderHidden, killedHidden int, err error)

CountHiddenRuns — see RunsFilteredPager.

func (PG) CountNonTerminalRuns added in v0.8.6

func (s PG) CountNonTerminalRuns(ctx context.Context) (int, error)

CountNonTerminalRuns is the number of non-terminal run rows that hold a sandbox (HoldsSandboxSQL), the quantity CreateRunUnderCap holds under the cap. It takes no lock: a pre-flight read for a refusal that must come before an identity is minted, never the authority.

func (PG) CountPendingApprovals added in v0.8.0

func (s PG) CountPendingApprovals(ctx context.Context) (int, error)

CountPendingApprovals — see ApprovalsForRunsPager. Backed by the partial approvals_state_idx: an index-only count, not a table scan.

func (PG) CountPendingApprovalsByRunCreator added in v0.8.0

func (s PG) CountPendingApprovalsByRunCreator(ctx context.Context, createdBy string) (int, error)

CountPendingApprovalsByRunCreator — see ApprovalsForRunsPager. Same semi-join ListApprovalsPageByRunCreator's own query uses (approvals has no created_by column of its own), counted instead of listed.

func (PG) CountPushPathLists added in v0.8.0

func (s PG) CountPushPathLists(ctx context.Context, runID uuid.UUID) (int, error)

func (PG) CreateAPIToken added in v0.6.0

func (s PG) CreateAPIToken(ctx context.Context, t types.APIToken, raw string) (types.APIToken, error)

CreateAPIToken inserts one token row. raw is the PLAINTEXT credential; only its hash is stored, and the caller must return the plaintext to its creator exactly once (unrecoverable afterwards). t.Token is ignored — passing the secret twice would be the one way to accidentally persist it.

A unique_violation (23505) on token_sha256 is ErrConflict: a raw collision in a 256-bit space means the caller reused a token value rather than minting a fresh one.

SECURITY: groups_truncated binds as SQL NULL when nil. NULL means "minted before anything recorded this", and consumers read that as TRUNCATED, not false. A plain bool or DEFAULT FALSE would assert "complete" for every legacy row — fail OPEN, the exact thing the marker exists to prevent.

func (PG) CreateApproval

func (s PG) CreateApproval(ctx context.Context, a types.ApprovalRequest) (types.ApprovalRequest, error)

CreateApproval inserts a new approval request. With approval notifications configured it also inserts the approval's outbox rows in the same transaction, so no crash can leave an approval nobody was told about; without them the INSERT is the only write.

func (PG) CreateDelegate added in v0.8.0

func (s PG) CreateDelegate(ctx context.Context, d types.Delegate, raw string) (types.Delegate, error)

CreateDelegate registers one portal. raw is the PLAINTEXT credential; only its hash is stored. Returns ErrConflict on a credential_sha256 collision (a reused credential in a 256-bit space).

func (PG) CreateDevice added in v0.8.0

func (s PG) CreateDevice(ctx context.Context, d types.Device, raw string) (types.Device, error)

CreateDevice inserts one device row, following CreateAPIToken's shape: raw is the plaintext device credential minted at enrolment. SECURITY: only its hash is stored, and d.CredentialSHA256 is ignored, so the caller can never accidentally persist the secret twice; raw is returned to the device exactly once, unrecoverable afterwards. ErrConflict on a credential_sha256 collision, same 256-bit space as MintEnrolmentToken.

func (PG) CreateGrant

func (s PG) CreateGrant(ctx context.Context, g types.CredentialGrant) (types.CredentialGrant, error)

CreateGrant inserts a credential grant (eligibility record) and returns it.

func (PG) CreatePerson added in v0.8.0

func (s PG) CreatePerson(ctx context.Context, p types.Person) (types.Person, bool, error)

func (PG) CreatePolicy

func (s PG) CreatePolicy(ctx context.Context, p types.RunPolicy) (types.RunPolicy, error)

CreatePolicy inserts a policy and returns the persisted row. Returns ErrConflict when the name's UNIQUE constraint (run_policies.name) rejects a duplicate — the caller maps that to 409, never the raw driver error.

func (PG) CreateRun

func (s PG) CreateRun(ctx context.Context, r types.AgentRun) (types.AgentRun, error)

CreateRun inserts a new run and returns the persisted row.

func (PG) CreateRunUnderCap added in v0.8.6

func (s PG) CreateRunUnderCap(ctx context.Context, r types.AgentRun, limit int) (types.AgentRun, error)

CreateRunUnderCap is CreateRun with a deployment-wide ceiling on non-terminal runs (WARDYN_MAX_CONCURRENT_RUNS). The count and the insert share one transaction-scoped advisory lock, so two replicas racing at the cap admit exactly the cap. limit <= 0 is unlimited and takes no lock. Rows are counted, not kept in a counter, so a crashed path cannot drift it.

func (PG) CreateScimGroup added in v0.8.6

func (s PG) CreateScimGroup(ctx context.Context, externalID, displayName string, now time.Time) (ScimGroup, error)

CreateScimGroup stores a group. An externalId any group already holds, in any case, is ErrScimGroupExists.

func (PG) CreateScimIdentity added in v0.8.6

func (s PG) CreateScimIdentity(ctx context.Context, issuer, tenantID, objectID string, u IdentityUpdate, now time.Time) (PrincipalIdentity, bool, error)

CreateScimIdentity records a SCIM user as an identity row with no principal, keyed by the object id, or finds the row already holding it (created=false). The first sign-in binds the principal.

func (PG) CreateUserType added in v0.8.0

func (s PG) CreateUserType(ctx context.Context, t types.UserType) (types.UserType, error)

CreateUserType inserts a custom type (built_in is never set here). ErrConflict when the id or name is taken, or the id is still named by an orphaned subject row.

func (PG) CreateWorkspace

func (s PG) CreateWorkspace(ctx context.Context, ws types.Workspace) (types.Workspace, error)

CreateWorkspace inserts an onboarded workspace and returns the persisted row.

func (PG) DecideApproval

func (s PG) DecideApproval(ctx context.Context, id uuid.UUID, decision types.ApprovalDecision) (types.ApprovalRequest, error)

DecideApproval transitions an approval from PENDING to decision.State. Returns ErrAlreadyDecided if the approval is not PENDING (fail-closed), via a single UPDATE with WHERE state='PENDING' to prevent TOCTOU races. SET is kept separate from RETURNING: updating only RETURNING would echo an un-updated row back as a green result over a silent no-op.

func (PG) DecideGovernanceChange added in v0.8.6

func (s PG) DecideGovernanceChange(ctx context.Context, id uuid.UUID, d GovernanceDecision, fn GovernanceDecideFunc) (types.GovernanceChange, error)

DecideGovernanceChange decides one change in one transaction:

  1. lock the change row;
  2. require it pending and unexpired. A lapsed pending row is moved to expired here and that is committed, and the caller gets *ErrGovernanceChangeNotPending with Lapsed set;
  3. run fn on the transaction (the target's checks and apply);
  4. move the row to d.To, stamping the decider and the time;
  5. commit.

fn returning ErrGovernanceChangeStale moves the row to stale instead and commits that. Any other error from fn, or from the state move, rolls the whole transaction back: a target written by fn is then unwritten and the change is still pending. ErrNotFound when no such change.

func (PG) DeleteBaseImage added in v0.5.0

func (s PG) DeleteBaseImage(ctx context.Context, id uuid.UUID, detach bool) error

DeleteBaseImage removes a catalog row; detach=true drops every workspace reference first (falling back to the derived recommended build — NULL is the marker, so "detach" is honest, not destructive). detach=false mirrors DeleteSource's residual race (STORE-2): a concurrent commit between the NOT EXISTS check and delete is instead caught by the base_image_id FK (0031:61), surfacing as 23503 — mapped to ErrConflict below.

func (PG) DeleteBranding added in v0.8.0

func (s PG) DeleteBranding(ctx context.Context) (bool, error)

DeleteBranding implements BrandingStore.

func (PG) DeleteCapabilityGrant added in v0.6.0

func (s PG) DeleteCapabilityGrant(ctx context.Context, id uuid.UUID) error

DeleteCapabilityGrant removes one grant by id. Returns ErrNotFound when no row matched — this is an admin-only surface, so there is no principal to scope the delete to and no existence oracle to worry about.

func (PG) DeleteGovernanceAssignment added in v0.7.0

func (s PG) DeleteGovernanceAssignment(ctx context.Context, id uuid.UUID) error

DeleteGovernanceAssignment removes one assignment by id, ErrNotFound when no row matched. Unassigning is the SUPPORTED way to widen someone back to the deployment ceiling.

func (PG) DeleteGovernanceProfile added in v0.7.0

func (s PG) DeleteGovernanceProfile(ctx context.Context, id uuid.UUID) error

DeleteGovernanceProfile removes one profile by id. ErrNotFound when no row matched.

ErrConflict when the row is still ASSIGNED: governance_assignments' ON DELETE RESTRICT refuses (23503) rather than cascading, since cascading would silently widen every assigned member back to the deployment ceiling. Translated into a sentinel so the route answers a caller-fixable 409 instead of a 500. *ErrProfileHasChildren when other profiles still compose on it (named under the graph lock, so the answer matches what the delete saw).

func (PG) DeleteLaunchPreset added in v0.8.0

func (s PG) DeleteLaunchPreset(ctx context.Context, name string) (types.LaunchPreset, error)

DeleteLaunchPreset removes a preset by name, returning the removed row, or ErrNotFound. Runs launched from it keep their preset/preset_version stamp.

func (PG) DeletePolicy

func (s PG) DeletePolicy(ctx context.Context, id uuid.UUID) error

DeletePolicy removes a policy by id. Returns ErrNotFound when no row matched. agent_runs.policy_id has no foreign key, so referencing runs keep a dangling policy_id; their authorization envelope survives via the run.policy.resolve audit event dispatch already recorded.

func (PG) DeleteRef added in v0.3.1

func (s PG) DeleteRef(ctx context.Context, ref string) error

DeleteRef removes the row for ref. Idempotent: deleting a missing ref is nil.

func (PG) DeleteRoleMapping added in v0.7.0

func (s PG) DeleteRoleMapping(ctx context.Context, id uuid.UUID) error

DeleteRoleMapping removes one row by id. Returns ErrNotFound when no row matched — admin-only surface, so there is no principal to scope the delete to and no existence oracle to worry about, exactly DeleteCapabilityGrant.

func (PG) DeleteRunOutputsOlderThan added in v0.8.6

func (s PG) DeleteRunOutputsOlderThan(ctx context.Context, age time.Duration) (int, error)

DeleteRunOutputsOlderThan — see RunOutputStore.

func (PG) DeleteRunProxyConfig added in v0.8.0

func (s PG) DeleteRunProxyConfig(ctx context.Context, id uuid.UUID) error

DeleteRunProxyConfig — see RunProxyConfigs.

func (PG) DeleteSSHKey added in v0.5.0

func (s PG) DeleteSSHKey(ctx context.Context, fingerprint, principal string) error

DeleteSSHKey removes fingerprint, scoped to principal so a human can only delete their OWN key. Returns ErrNotFound whether the fingerprint doesn't exist or belongs to someone else — indistinguishable on purpose, no existence leak across principals.

func (PG) DeleteSSHKeys added in v0.8.0

func (s PG) DeleteSSHKeys(ctx context.Context, principal string) (int, error)

DeleteSSHKeys removes a principal's registered keys, or every key when principal is empty. The count comes from the DELETE, not a prior snapshot.

func (PG) DeleteScimGroup added in v0.8.6

func (s PG) DeleteScimGroup(ctx context.Context, id uuid.UUID) error

DeleteScimGroup removes the group row and, by cascade, its members. Its ledger rows stay.

func (PG) DeleteSource added in v0.5.0

func (s PG) DeleteSource(ctx context.Context, id uuid.UUID, detach bool) error

DeleteSource removes a library source. detach=true strips every workspace attachment referencing it (?force=1) UNLESS that would leave a workspace with zero attachments (workspacesOrphanedBySource) — '[]' must stay unreachable (STORE-1). The check and the detach are two statements, so this only narrows, never closes (STORE-2), the race against a concurrent attach; same residual race as the detach=false path below.

func (PG) DeleteUserDrive added in v0.7.0

func (s PG) DeleteUserDrive(ctx context.Context, id uuid.UUID) error

DeleteUserDrive removes one drive by id. ErrNotFound if no row matched; ErrConflict if it's still allocated (user_drive_grants.drive_id is ON DELETE RESTRICT — Postgres refuses rather than cascading away grants whose directories may still hold someone's work).

func (PG) DeleteUserDriveGrant added in v0.7.0

func (s PG) DeleteUserDriveGrant(ctx context.Context, id uuid.UUID) (types.UserDriveGrant, error)

DeleteUserDriveGrant removes one grant by id and returns it (ErrNotFound if no match) — removing only the binding, never the volume/claim/directory. RETURNING lets the caller audit-log what was removed without a prior scan.

func (PG) DeleteUserSubjectRows added in v0.8.6

func (s PG) DeleteUserSubjectRows(ctx context.Context, id uuid.UUID, principals, emails []string) (grants, assignments, kept int64, err error)

DeleteUserSubjectRows deletes the user-subject capability grants and governance assignments of the purged identity id, matched case-insensitively, and says how many of each. A direct delete, never the GOV4 apply path. Rows keyed by one of principals go: the subject is a purged identity that can never authenticate again. Rows keyed by one of emails go only while no other principal holds that address, because an address can be recycled and the new holder's deny rows are theirs: another identity that is neither deactivated nor purged, or an API token or people row under a principal that is not in principals (or the address itself, a form of the leaver). Rows kept for that reason are counted in kept.

func (PG) DeleteUserType added in v0.8.0

func (s PG) DeleteUserType(ctx context.Context, id string) error

DeleteUserType removes a custom type that nothing names. ErrNotFound when no row has the id; ErrConflict when it is built in, still referenced, or held by a foreign key (role_mappings.user_type is ON DELETE RESTRICT). The reference check rides the DELETE itself, so a row written between the caller's check and this statement still refuses.

func (PG) DeleteWorkspace

func (s PG) DeleteWorkspace(ctx context.Context, id uuid.UUID) error

DeleteWorkspace removes a workspace by id. Returns ErrNotFound when no row matched.

func (PG) DropAuditPartition added in v0.8.6

func (s PG) DropAuditPartition(ctx context.Context, partition, digest, actor string) (AuditRetentionDrop, error)

func (PG) DryRunGovernance added in v0.8.6

func (s PG) DryRunGovernance(ctx context.Context, fn func(q Querier) error) error

DryRunGovernance runs fn on a transaction that is always rolled back, and returns fn's error. It is how a proposal learns whether a write would be accepted (and what it would produce) by running the very code that applies it, with nothing left behind.

func (PG) DuePauseSettles added in v0.8.6

func (s PG) DuePauseSettles(ctx context.Context) ([]PauseSettle, error)

DuePauseSettles — see PauseSettler.

func (PG) EnsureAuditPartitions added in v0.8.6

func (s PG) EnsureAuditPartitions(ctx context.Context, months int) (int, error)

func (PG) EnsureDeprovisionJobs added in v0.8.6

func (s PG) EnsureDeprovisionJobs(ctx context.Context, identityID uuid.UUID, kind string, keys []JobKey) error

EnsureDeprovisionJobs adds each key as a pending ledger row, leaving rows that exist as they are.

func (PG) EraseGovernanceChangePersonalFields added in v0.8.6

func (s PG) EraseGovernanceChangePersonalFields(ctx context.Context, names []string) (int, error)

EraseGovernanceChangePersonalFields clears the proposer and decider (their emails too, and the reason a decider typed) of every change whose recorded principal is one of names or whose recorded email matches one of them (case folded), and returns how many rows it touched. A pending change whose proposer is erased leaves pending in the same statement (to expired), so a change with no recorded proposer can never be approved. Idempotent: a row already cleared matches nothing.

func (PG) EraseRunOutputs added in v0.8.6

func (s PG) EraseRunOutputs(ctx context.Context, runIDs []uuid.UUID) error

EraseRunOutputs — see RunOutputStore. The locks are taken in key order, so two overlapping erasures cannot each hold what the other waits for.

func (PG) ExportAuditPartition added in v0.8.6

func (s PG) ExportAuditPartition(ctx context.Context, name string, header func(PartitionManifest) error, row func(AuditPartitionRow) error) error

func (PG) FailDeprovisionJob added in v0.8.6

func (s PG) FailDeprovisionJob(ctx context.Context, identityID uuid.UUID, kind string, k JobKey, cause error) error

FailDeprovisionJob records a failed attempt and leaves the step pending.

func (PG) FederationRevoked added in v0.8.0

func (s PG) FederationRevoked(ctx context.Context) (bool, error)

FederationRevoked reports whether the organisation revoked this laptop's device credential (MarkFederationRevoked), false when no row exists.

func (PG) FinishADOSignInEnd added in v0.8.6

func (s PG) FinishADOSignInEnd(ctx context.Context, owner string) error

FinishADOSignInEnd — see ADOSignInEndStore.

func (PG) FinishDeprovisionJob added in v0.8.6

func (s PG) FinishDeprovisionJob(ctx context.Context, identityID uuid.UUID, kind string, k JobKey, detail map[string]int) error

FinishDeprovisionJob marks a step done, storing the counts it produced.

func (PG) GetAPITokenByRaw added in v0.6.0

func (s PG) GetAPITokenByRaw(ctx context.Context, raw string) (types.APIToken, error)

GetAPITokenByRaw is the auth-time lookup: given a bearer string, resolve the LIVE token it stands for. Deliberately UNSCOPED by principal — this call is what authenticates the caller.

SECURITY: `revoked_at IS NULL` and the not-expired test (apiTokenLive) are in the WHERE, not checked separately — a revoked, expired, unknown, or non-matching token all fail IDENTICALLY with ErrNotFound, so the boundary is never an oracle for "this token once existed".

func (PG) GetApproval

func (s PG) GetApproval(ctx context.Context, id uuid.UUID) (types.ApprovalRequest, error)

GetApproval returns the approval for id, or ErrNotFound.

func (PG) GetAttachLease added in v0.8.6

func (s PG) GetAttachLease(ctx context.Context, runID uuid.UUID) (AttachLease, bool, error)

GetAttachLease — see AttachLeaseStore.

func (PG) GetBaseImage added in v0.5.0

func (s PG) GetBaseImage(ctx context.Context, id uuid.UUID) (types.BaseImageEntry, error)

GetBaseImage returns the catalog row for id, or ErrNotFound. Not on the Store interface (no handler reads one image by id); kept as a PG method because store_hydrate_pg_test.go round-trips through it.

func (PG) GetBaseImagesByIDs added in v0.5.0

func (s PG) GetBaseImagesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]types.BaseImageEntry, error)

GetBaseImagesByIDs is hydrateAll's own batched read, not a Store-interface method — its only caller is inside this package. Returns the base images for ids in ONE query, keyed by id, mirroring GetSourcesByIDs. Missing ids are simply absent (hydrateWorkspace leaves BaseImage nil for them).

func (PG) GetBranding added in v0.8.0

func (s PG) GetBranding(ctx context.Context) (types.Branding, error)

GetBranding implements BrandingStore.

func (PG) GetCapabilityEnforcement added in v0.6.0

func (s PG) GetCapabilityEnforcement(ctx context.Context) (map[string]bool, error)

GetCapabilityEnforcement returns the per-kind switch map. An ABSENT row means NOT ENFORCED, so the returned map is sparse by design and a missing key reads as false — that default is the whole zero-config back-compat story (see the migration). Never nil.

func (PG) GetDelegateByRaw added in v0.8.0

func (s PG) GetDelegateByRaw(ctx context.Context, raw string) (types.Delegate, error)

GetDelegateByRaw resolves the LIVE portal a credential stands for.

func (PG) GetDelegatedTokenByID added in v0.8.5

func (s PG) GetDelegatedTokenByID(ctx context.Context, id uuid.UUID, now time.Time) (types.DelegatedToken, error)

GetDelegatedTokenByID is GetDelegatedTokenByRaw by the grant's id: live while unexpired at now AND its portal still registered. A UI session opened through a portal re-asks it, so the session ends with the grant (#1475). Expired rows are swept on any mint, so ErrNotFound means ended.

func (PG) GetDelegatedTokenByRaw added in v0.8.0

func (s PG) GetDelegatedTokenByRaw(ctx context.Context, raw string, now time.Time) (types.DelegatedToken, error)

GetDelegatedTokenByRaw is the delegated lane's auth lookup: the token must be unexpired at now AND its portal still registered — the join is what makes revoking a portal kill all its tokens on their next request.

func (PG) GetDeviceByRaw added in v0.8.0

func (s PG) GetDeviceByRaw(ctx context.Context, raw string) (types.Device, error)

GetDeviceByRaw is the device auth-time lookup: given the bearer a device presented, resolve the live row it stands for. TRUST BOUNDARY: `revoked_at IS NULL` is in the WHERE (mirrors GetAPITokenByRaw), so a revoked/unknown/mismatched credential all fail identically with ErrNotFound.

func (PG) GetFederationCursor added in v0.8.0

func (s PG) GetFederationCursor(ctx context.Context) (int64, string, error)

GetFederationCursor returns how far this deployment's forwarder has pushed audit_events upward, and the row_hash the organisation acknowledged at that seq — 0 and "" when nothing forwarded yet (matching GetSiteConfig's zero-value-on-no-row contract).

func (PG) GetGovernanceChange added in v0.8.6

func (s PG) GetGovernanceChange(ctx context.Context, id uuid.UUID) (types.GovernanceChange, error)

GetGovernanceChange returns one change, ErrNotFound when none; a pending row past its expiry reads as expired.

func (PG) GetGovernanceProfile added in v0.7.0

func (s PG) GetGovernanceProfile(ctx context.Context, id uuid.UUID) (types.GovernanceProfile, error)

GetGovernanceProfile returns one profile by id, or ErrNotFound. The RAW row: a composed profile carries no authority here (see GetGovernanceProfileChain).

func (PG) GetGovernanceProfileChain added in v0.8.6

func (s PG) GetGovernanceProfileChain(ctx context.Context, id uuid.UUID) ([]types.GovernanceProfile, error)

GetGovernanceProfileChain reads one profile and its ancestors in ONE statement, leaf first, bounded at depth 3 (the profile, its base, that base's base). ErrNotFound when the leaf is gone. It returns whatever the bound reached, cycle or overflow included: the caller refuses a chain whose last row still names a base or whose ids repeat, and never composes a partial one.

func (PG) GetIdentity added in v0.8.6

func (s PG) GetIdentity(ctx context.Context, id uuid.UUID) (PrincipalIdentity, error)

GetIdentity is the row stored under id, or ErrNotFound.

func (PG) GetIdentityByObject added in v0.8.6

func (s PG) GetIdentityByObject(ctx context.Context, issuer, tenantID, objectID string) (PrincipalIdentity, error)

func (PG) GetLaunchPreset added in v0.8.0

func (s PG) GetLaunchPreset(ctx context.Context, name string) (types.LaunchPreset, error)

GetLaunchPreset returns one preset by name, or ErrNotFound.

func (PG) GetPerson added in v0.8.0

func (s PG) GetPerson(ctx context.Context, principal string) (types.Person, error)

func (PG) GetPolicy

func (s PG) GetPolicy(ctx context.Context, id uuid.UUID) (types.RunPolicy, error)

GetPolicy returns the policy for id, or ErrNotFound.

func (PG) GetPrincipalPref added in v0.8.0

func (s PG) GetPrincipalPref(ctx context.Context, principal, key string) (json.RawMessage, error)

func (PG) GetPushPathList added in v0.8.0

func (s PG) GetPushPathList(ctx context.Context, approvalID uuid.UUID) (types.PushPathList, error)

func (PG) GetRef added in v0.3.1

func (s PG) GetRef(ctx context.Context, ref string) (string, bool, error)

GetRef returns the substrate name for ref. A missing row is (found=false, nil error) — pre-migration and unknown refs are expected, not errors.

func (PG) GetRun

func (s PG) GetRun(ctx context.Context, id uuid.UUID) (types.AgentRun, error)

GetRun returns the run for id, or ErrNotFound.

func (PG) GetRunLayout added in v0.5.0

func (s PG) GetRunLayout(ctx context.Context, principal, preset string) (types.RunLayout, error)

GetRunLayout reads the one (principal, preset) row scoped to principal — a layout is never looked up by anything but its owner.

func (PG) GetRunOutput added in v0.8.6

func (s PG) GetRunOutput(ctx context.Context, runID uuid.UUID) (RunOutput, bool, error)

GetRunOutput — see RunOutputStore.

func (PG) GetRunProxyConfig added in v0.8.0

func (s PG) GetRunProxyConfig(ctx context.Context, id uuid.UUID) ([]byte, error)

GetRunProxyConfig — see RunProxyConfigs.

func (PG) GetSSHKeyByFingerprint added in v0.5.0

func (s PG) GetSSHKeyByFingerprint(ctx context.Context, fingerprint string) (types.SSHPublicKey, error)

GetSSHKeyByFingerprint is the gateway's pre-auth lookup: resolves which principal (if any) registered the offered fingerprint, and with which role, the admin-override signal. Deliberately UNSCOPED by principal — the caller has not authenticated yet; this call is what authenticates them. Returns ErrNotFound when unregistered.

func (PG) GetScimGroup added in v0.8.6

func (s PG) GetScimGroup(ctx context.Context, id uuid.UUID) (ScimGroup, error)

GetScimGroup is the group stored under id, or ErrNotFound.

func (PG) GetSiteConfig

func (s PG) GetSiteConfig(ctx context.Context) (types.SiteConfig, error)

GetSiteConfig returns the operator-wide site config, or a ZERO-VALUE SiteConfig (not an error) when no row has been written yet — first boot has no config, and "unconfigured" is a valid, common state rather than a failure the caller must special-case.

func (PG) GetSource added in v0.5.0

func (s PG) GetSource(ctx context.Context, id uuid.UUID) (types.Source, error)

GetSource returns the source for id, or ErrNotFound.

func (PG) GetSourcesByIDs added in v0.5.0

func (s PG) GetSourcesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]types.Source, error)

GetSourcesByIDs returns the sources for ids in ONE query, keyed by id — the hydrate pass's bulk read. Missing ids are simply absent (a dangling attachment contributes nothing; fold and mount-gate each handle it).

func (PG) GetUserDrive added in v0.7.0

func (s PG) GetUserDrive(ctx context.Context, id uuid.UUID) (types.UserDrive, error)

GetUserDrive returns one drive by id, or ErrNotFound.

func (PG) GetUserType added in v0.8.0

func (s PG) GetUserType(ctx context.Context, id string) (types.UserType, error)

GetUserType returns one type by id, or ErrNotFound.

func (PG) GetWorkspace

func (s PG) GetWorkspace(ctx context.Context, id uuid.UUID) (types.Workspace, error)

GetWorkspace returns the workspace for id, or ErrNotFound.

func (PG) GroupRemovalFailures added in v0.8.6

func (s PG) GroupRemovalFailures(ctx context.Context, limit int) ([]DeprovisionFailure, error)

GroupRemovalFailures is ListDeprovisionFailures for the pendingGroupRemovalWhere rows with a recorded error: one row per identity and step, with the error of the target touched last.

func (PG) GroupRemovalIdentities added in v0.8.6

func (s PG) GroupRemovalIdentities(ctx context.Context, groupID uuid.UUID) ([]uuid.UUID, error)

GroupRemovalIdentities is every identity a group's deletion must remove as a mover: the recorded members, and anyone whose removal from this group is still pending.

func (PG) HasGroupTierAssignments added in v0.7.0

func (s PG) HasGroupTierAssignments(ctx context.Context) (bool, error)

HasGroupTierAssignments reports whether ANY group-tier assignment exists: the gate on the stale-snapshot refusal (see hasGroupTierRows).

func (PG) HasGroupTierDriveGrants added in v0.7.0

func (s PG) HasGroupTierDriveGrants(ctx context.Context) (bool, error)

HasGroupTierDriveGrants reports whether ANY group-tier grant exists: the gate on the stale-snapshot refusal (see hasGroupTierRows).

func (PG) HasRunAuditEvent added in v0.8.0

func (s PG) HasRunAuditEvent(ctx context.Context, runID uuid.UUID, f AuditFilter) (bool, error)

HasRunAuditEvent reports whether runID has an audit row matching f.

func (PG) HeartbeatRunWatcher added in v0.5.0

func (s PG) HeartbeatRunWatcher(ctx context.Context, id uuid.UUID, owner string) error

HeartbeatRunWatcher refreshes the lease on id for owner: the "still watching" write every watcher goroutine repeats while alive; its silence lets another replica's ClaimStaleRunWatchers take over.

Unconditional on current owner, since a watcher can't abort its blocking Runner.Wait anyway and two watchers are harmless (the terminal transition is a CAS). Doesn't bump updated_at either, like the claim: a heartbeat there would make every run look forever-active and disable idle auto-stop. A missing row isn't an error; the lease is advisory.

func (PG) HeldEmails added in v0.8.6

func (s PG) HeldEmails(ctx context.Context, id uuid.UUID, principals, emails []string) ([]string, error)

HeldEmails is the emails another principal holds, by DeleteUserSubjectRows' rule: another identity than id that is neither deactivated nor purged, or a live API token (unrevoked, unexpired) or people row (not deactivated) under a principal that is neither one of principals nor the address itself and whose identity is neither deactivated nor purged. A previous holder that is gone holds nothing. Matched case-insensitively; the answer is lower-cased.

func (PG) HoldsAttachLease added in v0.8.6

func (s PG) HoldsAttachLease(ctx context.Context, runID, holderID uuid.UUID) (bool, error)

HoldsAttachLease — see AttachLeaseStore.

func (PG) IdentitiesByAlias added in v0.8.6

func (s PG) IdentitiesByAlias(ctx context.Context, issuer, valueLower string) ([]PrincipalIdentity, error)

IdentitiesByAlias is the rows under issuer that have no object id and were seen under valueLower as an email or a userName: the removal-only link a SCIM user with no object-id match falls to.

func (PG) IdentitiesByPrincipal added in v0.8.6

func (s PG) IdentitiesByPrincipal(ctx context.Context, principal string) ([]PrincipalIdentity, error)

func (PG) IdentityAliasValues added in v0.8.6

func (s PG) IdentityAliasValues(ctx context.Context, id uuid.UUID) ([]string, error)

IdentityAliasValues is every alias value (email and userName) the identity has been seen under, sorted.

func (PG) IdentityBlocked added in v0.8.6

func (s PG) IdentityBlocked(ctx context.Context, principal string, epoch int64) (bool, error)

IdentityBlocked reports whether principal's identity rows refuse a credential admitted under epoch (epoch < 0: only a deactivation refuses). It is the read the cookie, API-token and SSH-key lanes make at authentication, as one statement.

func (PG) IdentityRefused added in v0.8.6

func (s PG) IdentityRefused(ctx context.Context, issuer, tenantID, objectID, principal string) (bool, error)

IdentityRefused is the admission half of the sign-in gate, read-only: whether the identity a sign-in names is deactivated or purged. An Entra sign-in is named by (issuer, tenant, object id), and every sign-in by its resolved principal; either match refuses.

func (PG) IngestDeviceAudit added in v0.8.0

func (s PG) IngestDeviceAudit(ctx context.Context, deviceID uuid.UUID, peer string, rows []types.FederatedAuditEvent) (DeviceIngestResult, error)

IngestDeviceAudit appends one device's forwarded batch to this organisation's own audit_events, as the organisation's own chained rows ("one chain per writer"). A federated row keeps the device's claimed actor_type, action, target and outcome; what marks it forwarded is the device's provenance folded into `data` (mergeDeviceOrigin), not a new audit action, so no filter/SIEM rule keyed on `action` has to learn about it. TRUST BOUNDARY: three fields are never the device's to choose — actor is FederatedActor (claim kept in data.device_origin.actor); source_ip is peer (claim kept in data.device_origin.source_ip); and a run_id naming one of this organisation's own runs refuses the batch (ErrFederatedOrgRun).

Order of work, chosen so a device can make the org's own audit writers wait for at most its inserts:

  1. Every row must be storable (FederatedRowProblem) and representable by Postgres, or ErrFederatedRowInvalid.
  2. Every claimed RowHash is recomputed in SQL before any transaction or lock, in ONE statement (verifyClaimedHashes), against its own claimed PrevHash. A refused or replayed batch never touches the chain lock.
  3. The device row is locked FOR UPDATE, serializing this device's concurrent pushes before either reaches the chain lock (deadlock-free: no transaction takes the chain lock then a device row). A revoked device answers ErrDeviceRevoked.
  4. Idempotency rides the cursor and the hash, not audit_events.id: rows at or before LastSeq that are the rows held here (heldPrefix) are skipped — the forwarder's cursor can lag what was committed. A reused seq is not a re-send.
  5. The first new row's claimed PrevHash is either empty (genesis, accepted — a chain reset if one was already recorded) or equal to LastRowHash with seq past LastSeq; every later row's claimed PrevHash equals the preceding row's claimed RowHash. Otherwise ErrConflict.
  6. Only then the chain lock (db.AuditChainLockKey), under the lock timeout, and the inserts: field for field as claimed except actor, source_ip and merged data. Cursor advances in the same transaction.

Any refusal refuses the entire batch, never a verified prefix: it's the retry unit, and a partial accept would leave the cursor mid-batch with no way to tell the caller which rows to resend.

func (PG) InsertPendingRunOutput added in v0.8.6

func (s PG) InsertPendingRunOutput(ctx context.Context, runID uuid.UUID) error

InsertPendingRunOutput — see RunOutputStore.

func (PG) InsertRunPAT added in v0.8.2

func (s PG) InsertRunPAT(ctx context.Context, p RunPAT) error

InsertRunPAT — see RunPATStore.

func (PG) IssueLoginIdentity added in v0.8.6

func (s PG) IssueLoginIdentity(ctx context.Context, in LoginIdentity, now time.Time) (epoch int64, err error)

IssueLoginIdentity is the issuance half of the sign-in gate: in one transaction it upserts and binds the identity row (a failure refuses the sign-in), re-checks deactivation on every row bound to the principal and reads the authority epoch the session cookie will carry. The rows stay locked until the commit, so a suspension either precedes it, and the sign-in is refused with ErrIdentityDeactivated, or follows it, and bumps the epoch past the one returned. A sign-in whose identity is bound to another principal is ErrIdentityBindingMismatch: its session would carry neither that identity's deactivation nor its epoch. A refused sign-in writes nothing.

func (PG) LastRunPAT added in v0.8.2

func (s PG) LastRunPAT(ctx context.Context, owner, providerRowID string) (RunPAT, bool, error)

LastRunPAT — see RunPATReader.

func (PG) LatestAuditEventByAction

func (s PG) LatestAuditEventByAction(ctx context.Context, action string) (types.AuditEvent, error)

LatestAuditEventByAction returns the most recent audit event whose action equals the given action, or ErrNotFound when none exists. Used by /healthz to find the latest kernel.sensor.ping driving the eBPF health state.

Most recent by `time`, picked from a seq-ordered window: the audit spool replays at-least-once while keeping each event's original ev.Time, so a beat spooled through a DB blip could carry the highest seq but an hours-old time and falsely mark the sensor degraded. The window orders by seq to keep using the purpose-built (action, seq DESC) index; ordering by time directly would sort the whole action's history on every probe. Twenty is a window, not a proof — a longer replay backlog can push the fresh row out, bounding the staleness to one heartbeat interval against /healthz's own TTL.

func (PG) ListAPITokens added in v0.6.0

func (s PG) ListAPITokens(ctx context.Context) ([]types.APIToken, error)

ListAPITokens returns every token in the deployment, newest first (admin GET /tokens); revoked rows included, same reason as the self-service list.

func (PG) ListAPITokensByPrincipal added in v0.6.0

func (s PG) ListAPITokensByPrincipal(ctx context.Context, principal string) ([]types.APIToken, error)

ListAPITokensByPrincipal returns principal's own tokens, newest first (self-service GET /me/tokens). Revoked rows are INCLUDED so a human can confirm a retired token is retired; the row carries no usable credential either way.

func (PG) ListAPITokensByPrincipalPage added in v0.8.0

func (s PG) ListAPITokensByPrincipalPage(ctx context.Context, principal string, p Page) ([]types.APIToken, error)

ListAPITokensByPrincipalPage is ListAPITokensByPrincipal bounded by p.

func (PG) ListApprovals

func (s PG) ListApprovals(ctx context.Context, stateFilter types.ApprovalState) ([]types.ApprovalRequest, error)

ListApprovals returns approvals filtered by state. Pass empty string to list all.

func (PG) ListApprovalsPage added in v0.3.1

func (s PG) ListApprovalsPage(ctx context.Context, stateFilter types.ApprovalState, p Page) ([]types.ApprovalRequest, error)

ListApprovalsPage returns approvals filtered by state (empty = all) in reverse request order, bounded by p. The all-state feed rides approvals_requested_at_idx; a single-state filter rides approvals_state_requested_at_idx, serving both WHERE and ORDER BY without a sort.

func (PG) ListApprovalsPageByRun added in v0.7.0

func (s PG) ListApprovalsPageByRun(ctx context.Context, runID uuid.UUID, stateFilter types.ApprovalState, p Page) ([]types.ApprovalRequest, error)

ListApprovalsPageByRun is ListApprovalsPage narrowed to one run, same state filter and ordering.

ponytail: no new index. approvals_run_idx already makes this an index scan, and one run's approvals are few enough that sorting by requested_at is free — upgrade to a composite (run_id, requested_at DESC) if that changes.

func (PG) ListApprovalsPageByRunCreator added in v0.5.0

func (s PG) ListApprovalsPageByRunCreator(ctx context.Context, createdBy string, stateFilter types.ApprovalState, p Page) ([]types.ApprovalRequest, error)

ListApprovalsPageByRunCreator is ListApprovalsPage narrowed to approvals on runs createdBy owns, via a JOIN on agent_runs (approvals has no created_by of its own). Same state filter and ordering as ListApprovalsPage.

func (PG) ListAuditEventsAfterSeq added in v0.8.0

func (s PG) ListAuditEventsAfterSeq(ctx context.Context, seq int64, limit int) ([]types.FederatedAuditEvent, error)

ListAuditEventsAfterSeq returns this deployment's own audit_events strictly after seq, oldest first — the forwarder's read of its local chain to build the next batch IngestDeviceAudit expects. limit<=0 defaults to 1000, matching QueryAuditEvents/QueryRecentAuditEvents.

func (PG) ListBaseImages added in v0.5.0

func (s PG) ListBaseImages(ctx context.Context) ([]types.BaseImageEntry, error)

ListBaseImages returns the whole catalog, newest first.

func (PG) ListCapabilityGrants added in v0.6.0

func (s PG) ListCapabilityGrants(ctx context.Context) ([]types.CapabilityGrant, error)

ListCapabilityGrants returns every grant, oldest first — the admin Permissions screen's whole table in one read.

func (PG) ListCapabilityGrantsFor added in v0.6.0

func (s PG) ListCapabilityGrantsFor(ctx context.Context, users, groups []string, userType string) ([]types.CapabilityGrant, error)

ListCapabilityGrantsFor returns every grant that could apply to one caller: `all` rows, `user` rows naming any of users (the caller's lowercased sub AND email), `group` rows naming any of groups, and `user_type` rows naming the caller's one type. An empty userType matches no row, since the write boundary refuses an empty subject.

Deliberately NOT filtered by capability: the resolver needs one kind and GET /me/capabilities needs all four, and one round trip serving both beats two indexes and two queries. Per-kind/per-value matching happens in Go.

ponytail: no cache. This runs per request, on the (subject_type, subject) index; a process-local cache is the HA blocker OPERATIONS already names for other state, and a stale permission cache is a security bug, not a slow page. Add one only behind a shared invalidation channel.

func (PG) ListCapabilityGrantsForPage added in v0.8.0

func (s PG) ListCapabilityGrantsForPage(ctx context.Context, users, groups []string, userType string, p Page) ([]types.CapabilityGrant, error)

ListCapabilityGrantsForPage is ListCapabilityGrantsFor bounded by p: same four subject arms, so a page never drops a `user_type` grant the unpaged read returns.

func (PG) ListCapabilityRestrictions added in v0.8.0

func (s PG) ListCapabilityRestrictions(ctx context.Context) (map[string]map[string]bool, error)

ListCapabilityRestrictions returns every restricted value, kind -> set. The table holds one row per restricted admin-configured resource, so one read per resolution answers every value it asks about. Never nil.

func (PG) ListDeactivatedIdentities added in v0.8.6

func (s PG) ListDeactivatedIdentities(ctx context.Context, limit int) ([]PrincipalIdentity, error)

ListDeactivatedIdentities is the identities deactivated and not purged, most recently deactivated first, at most limit.

func (PG) ListDelegates added in v0.8.0

func (s PG) ListDelegates(ctx context.Context) ([]types.Delegate, error)

ListDelegates returns every portal, newest first, including revoked — admins need to see that a retired portal is retired.

func (PG) ListDeprovisionFailures added in v0.8.6

func (s PG) ListDeprovisionFailures(ctx context.Context, limit int) ([]DeprovisionFailure, error)

ListDeprovisionFailures is the suspend and purge ledger rows that are pending with a recorded error for an identity still deactivated or purged (the sweeper's scope: a reactivated person's leftover rows are not unfinished work), oldest identity first, at most limit. One row per identity, kind and step: a step that fails for several targets is listed once, with the error of the one touched last.

func (PG) ListDeprovisionJobs added in v0.8.6

func (s PG) ListDeprovisionJobs(ctx context.Context, identityID uuid.UUID, kind string) ([]DeprovisionJob, error)

ListDeprovisionJobs is the ledger of one identity and kind, oldest first.

func (PG) ListDevices added in v0.8.0

func (s PG) ListDevices(ctx context.Context) ([]types.Device, error)

ListDevices returns every enrolled device, newest first, revoked rows included (an admin needs to see a retired device is retired). Phase one has no console surface for this (CLI/API only).

func (PG) ListEnrolmentTokens added in v0.8.0

func (s PG) ListEnrolmentTokens(ctx context.Context, now time.Time) ([]types.DeviceEnrolmentToken, error)

ListEnrolmentTokens returns every token still redeemable at now (unconsumed, unexpired), newest first — the ones an admin may still cancel; a redeemed or expired token is never listed.

func (PG) ListGovernanceAssignments added in v0.7.0

func (s PG) ListGovernanceAssignments(ctx context.Context) ([]types.GovernanceAssignment, error)

ListGovernanceAssignments returns every assignment in the order the resolver itself would rank them (tier, then priority, then subject), so the console's table reads top-down as the precedence rule.

func (PG) ListGovernanceChanges added in v0.8.6

func (s PG) ListGovernanceChanges(ctx context.Context, state string) ([]types.GovernanceChange, error)

ListGovernanceChanges returns changes newest first. state narrows to one state ("" is every state); a pending row past its expiry reads as expired.

func (PG) ListGovernanceProfiles added in v0.7.0

func (s PG) ListGovernanceProfiles(ctx context.Context) ([]types.GovernanceProfile, error)

ListGovernanceProfiles returns every profile by name (the handle an admin looks for), unlike the creation order other List* calls use. Deployment- wide config, so there's no per-caller scoping to filter on.

func (PG) ListGrantsByRun

func (s PG) ListGrantsByRun(ctx context.Context, runID uuid.UUID) ([]types.CredentialGrant, error)

ListGrantsByRun returns all grants for a run.

func (PG) ListGrantsByRunPage added in v0.8.0

func (s PG) ListGrantsByRunPage(ctx context.Context, runID uuid.UUID, p Page) ([]types.CredentialGrant, error)

ListGrantsByRunPage is ListGrantsByRun bounded by p.

func (PG) ListGroupDenyGrants added in v0.7.0

func (s PG) ListGroupDenyGrants(ctx context.Context, capability string) ([]types.CapabilityGrant, error)

ListGroupDenyGrants returns the GROUP-subject DENY rows of one capability kind — the only rows internal/api's unresolvable-group-deny check can match.

It exists because that check runs on the FAIL-CLOSED path every caller with an unanswerable group snapshot takes (every pre-0.7 API token, every request): answering it with ListCapabilityGrants would let one such token force an unbounded full-table read per checked value — an authorization question whose cost scales with table size is an availability surface, not just a slow page. The predicate is what internal/api applied in Go afterwards, moved into SQL; value-overlap matching stays in Go (TestCapUnresolvableGroupDenyMatchesFullScan pins the two together). The (subject_type, subject) index already serves it.

ponytail: no EXISTS fast path in front of it. It would be a second round trip to save a query that already returns nothing on the deployments the fast path would help.

func (PG) ListLapsedTokenRuns added in v0.8.0

func (s PG) ListLapsedTokenRuns(ctx context.Context, life time.Duration) ([]types.AgentRun, error)

ListLapsedTokenRuns — see RunLoser. Both sides use the database's clock, so replica clock skew cannot make a live run look lapsed.

func (PG) ListLaunchPresets added in v0.8.0

func (s PG) ListLaunchPresets(ctx context.Context) ([]types.LaunchPreset, error)

ListLaunchPresets returns every preset by name.

func (PG) ListLeasedRuns added in v0.8.0

func (s PG) ListLeasedRuns(ctx context.Context) ([]types.AgentRun, error)

ListLeasedRuns reads the runs the lease sweep acts on. RUNNING only: a run still starting reaches its end on the first sweep after it is up, and a terminal run has nothing left to end.

func (PG) ListNonTerminalRunsBy added in v0.8.6

func (s PG) ListNonTerminalRunsBy(ctx context.Context, createdBy string) ([]types.AgentRun, error)

ListNonTerminalRunsBy is createdBy's runs that have not ended.

func (PG) ListPauseCandidates added in v0.8.0

func (s PG) ListPauseCandidates(ctx context.Context) ([]PauseCandidate, time.Time, error)

ListPauseCandidates — see RunPauser.

func (PG) ListPendingApprovalsForRuns added in v0.8.0

func (s PG) ListPendingApprovalsForRuns(ctx context.Context, runIDs []uuid.UUID) ([]types.ApprovalRequest, error)

ListPendingApprovalsForRuns — see ApprovalsForRunsPager. Backed by approvals_run_idx and the partial approvals_state_idx: an index scan over run_id = ANY($1) intersected with the partial index, not a table scan.

func (PG) ListPeople added in v0.8.0

func (s PG) ListPeople(ctx context.Context) ([]types.Person, error)

func (PG) ListPeopleDirectory added in v0.8.6

func (s PG) ListPeopleDirectory(ctx context.Context, f PeopleDirectoryFilter) (PeopleDirectoryPage, error)

func (PG) ListPolicies

func (s PG) ListPolicies(ctx context.Context) ([]types.RunPolicy, error)

ListPolicies returns all policies in reverse creation order. The slice is empty (never nil) when no policies exist.

func (PG) ListPoliciesPage added in v0.3.1

func (s PG) ListPoliciesPage(ctx context.Context, p Page) ([]types.RunPolicy, error)

ListPoliciesPage returns policies in reverse creation order, bounded by p (run_policies_created_at_idx covers the ORDER BY).

func (PG) ListProfiledLiveRuns added in v0.8.0

func (s PG) ListProfiledLiveRuns(ctx context.Context) ([]types.AgentRun, error)

ListProfiledLiveRuns — see RunLimitsReclamper.

func (PG) ListRoleMappings added in v0.7.0

func (s PG) ListRoleMappings(ctx context.Context) ([]types.RoleMapping, error)

ListRoleMappings returns every row, oldest first — both the console's People screen and internal/auth/oidc's login-time merge (mergeRoleMaps) read the whole table in one call; there is no per-caller scoping to filter on, this is deployment-wide config.

func (PG) ListRunPATs added in v0.8.2

func (s PG) ListRunPATs(ctx context.Context, runID uuid.UUID) ([]RunPAT, error)

ListRunPATs — see RunPATReader. The primary key's run_id prefix serves it.

func (PG) ListRunProxyReleases added in v0.8.0

func (s PG) ListRunProxyReleases(ctx context.Context) ([]RunProxyRelease, error)

ListRunProxyReleases — see RunReviver.

func (PG) ListRuns

func (s PG) ListRuns(ctx context.Context) ([]types.AgentRun, error)

ListRuns returns all runs in reverse creation order (unbounded).

func (PG) ListRunsFiltered added in v0.8.0

func (s PG) ListRunsFiltered(ctx context.Context, f RunFilter, p Page) ([]types.AgentRun, error)

ListRunsFiltered is ListRunsPage narrowed and reordered per f. Owner- scoped reads ride agent_runs_created_by_idx (bitmap scan + in-memory sort); an org-wide read is a parallel seq scan with top-N heapsort — both linear in the caller's history, which the spec accepted (no ended_at index backs this CASE-based order/window).

func (PG) ListRunsPage added in v0.3.1

func (s PG) ListRunsPage(ctx context.Context, p Page) ([]types.AgentRun, error)

ListRunsPage returns runs in reverse creation order, bounded by p. agent_runs_created_at_idx makes the ORDER BY + LIMIT an index scan.

func (PG) ListRunsPageByCreator added in v0.5.0

func (s PG) ListRunsPageByCreator(ctx context.Context, createdBy string, p Page) ([]types.AgentRun, error)

ListRunsPageByCreator is ListRunsPage narrowed to one creator, same order. ponytail: no dedicated (created_by, created_at) index yet — agent_runs is small enough per-operator that the existing created_at index plus a filter scan is fine; add one if a member's run list ever gets slow.

func (PG) ListSSHKeysByPrincipal added in v0.5.0

func (s PG) ListSSHKeysByPrincipal(ctx context.Context, principal string) ([]types.SSHPublicKey, error)

ListSSHKeysByPrincipal returns principal's own registered keys, newest first — the self-service GET /me/ssh-keys list.

func (PG) ListSSHKeysByPrincipalPage added in v0.8.0

func (s PG) ListSSHKeysByPrincipalPage(ctx context.Context, principal string, p Page) ([]types.SSHPublicKey, error)

ListSSHKeysByPrincipalPage is ListSSHKeysByPrincipal bounded by p.

func (PG) ListSources added in v0.5.0

func (s PG) ListSources(ctx context.Context) ([]types.Source, error)

ListSources returns the whole library, newest first.

func (PG) ListStalePendingRunOutputs added in v0.8.6

func (s PG) ListStalePendingRunOutputs(ctx context.Context, age time.Duration, limit int) ([]uuid.UUID, error)

ListStalePendingRunOutputs — see RunOutputStore.

func (PG) ListUnrevokedRunPATs added in v0.8.2

func (s PG) ListUnrevokedRunPATs(ctx context.Context, f RunPATFilter) ([]RunPAT, error)

ListUnrevokedRunPATs — see RunPATStore.

func (PG) ListUserDriveGrants added in v0.7.0

func (s PG) ListUserDriveGrants(ctx context.Context) ([]types.UserDriveGrant, error)

ListUserDriveGrants returns every grant in the order the resolver itself would rank them (tier, then priority, then subject) so the console's table reads top-down as the precedence rule, not insertion order.

func (PG) ListUserDriveGrantsPage added in v0.7.0

func (s PG) ListUserDriveGrantsPage(ctx context.Context, p Page) ([]types.UserDriveGrant, error)

ListUserDriveGrantsPage is ListUserDriveGrants bounded to one window. The LIMIT changes the plan, not just the size: this ORDER BY has no serving index, so unbounded it's a Seq Scan + full sort (50k rows, PG17: 149.7ms disk sort); LIMIT turns it into a top-N heapsort (37.7ms, in memory). The Seq Scan itself needs an index (a migration) to remove.

func (PG) ListUserDrives added in v0.7.0

func (s PG) ListUserDrives(ctx context.Context) ([]types.UserDriveListItem, error)

ListUserDrives returns every drive by name (the handle an admin looks for) with its grant count, in one read — the console's whole table. The count rides along so the delete affordance is honest (a drive with grants answers 409, visible before clicking); a grouped subquery, not a JOIN, keeps the drive row single.

func (PG) ListUserTypes added in v0.8.0

func (s PG) ListUserTypes(ctx context.Context) ([]types.UserType, error)

ListUserTypes returns every type: the built-in first, then by priority (highest first) and name.

func (PG) ListWorkspaces

func (s PG) ListWorkspaces(ctx context.Context) ([]types.Workspace, error)

ListWorkspaces returns all workspaces in reverse creation order. The slice is empty (never nil) when no workspaces exist.

func (PG) ListWorkspacesPage added in v0.3.1

func (s PG) ListWorkspacesPage(ctx context.Context, p Page) ([]types.Workspace, error)

ListWorkspacesPage returns workspaces in reverse creation order, bounded by p (workspaces_created_at_idx covers the ORDER BY).

func (PG) ListWorkspacesPageForOwner added in v0.6.0

func (s PG) ListWorkspacesPageForOwner(ctx context.Context, owner string, p Page) ([]types.Workspace, error)

ListWorkspacesPageForOwner is ListWorkspacesPage narrowed to what one member may see: their own owned rows plus every operator-owned row (”, every pre-0.6 workspace). workspaces_owned_by_idx covers the IN.

func (PG) LockLoginSupersede added in v0.8.0

func (s PG) LockLoginSupersede(ctx context.Context, actor string) (func(), error)

LockLoginSupersede holds db.LoginSupersedeLockClass keyed to actor for up to db.LoginSupersedeLockWait. Session-scoped, not transaction-scoped: it guards several independent statements, including a later read-modify-write on the capture path. Borrows one connection from this pool for the hold's duration; see ErrLoginLockNoCapacity for the one error a caller may proceed on.

func (PG) Locker added in v0.8.6

func (s PG) Locker() db.Locker

Locker is the cross-replica locker over this store's database (db.LockerFor): every PG over one pool shares it. The api package takes its ordered locks through it when the store offers one, and falls back to an in-process locker for a store with no database (nil here, for a PG with no pool).

func (PG) MarkAWSSSOTokenSpent added in v0.8.0

func (s PG) MarkAWSSSOTokenSpent(ctx context.Context, fingerprint, owner string, markedAt time.Time) error

MarkAWSSSOTokenSpent upserts idempotently: ON CONFLICT DO NOTHING keeps row age "since first spent" even on a second failed persist or invalid_grant.

func (PG) MarkFederationRevoked added in v0.8.0

func (s PG) MarkFederationRevoked(ctx context.Context) error

MarkFederationRevoked durably records that the organisation revoked this laptop, so a restart comes back still refusing new runs.

func (PG) MarkIdentityPurged added in v0.8.6

func (s PG) MarkIdentityPurged(ctx context.Context, id uuid.UUID, requireDue bool) (bool, error)

MarkIdentityPurged turns the identity into a permanent tombstone (purged_at set) and records the purge's last step as pending, in one transaction that holds the row FOR UPDATE. It reports whether this call marked it: false when the row was already purged and, with requireDue, when it is not (still) deactivated with a purge_after that has passed. The re-check under the lock is what lets a reactivation that committed first win; once the mark commits a reactivation is ErrIdentityPurged.

func (PG) MarkPersonSignedIn added in v0.8.0

func (s PG) MarkPersonSignedIn(ctx context.Context, principal string, now time.Time) error

func (PG) MarkRunEnded added in v0.8.0

func (s PG) MarkRunEnded(ctx context.Context, id uuid.UUID, now time.Time) (bool, error)

MarkRunEnded — see RunLeaser.

func (PG) MarkRunEndingSoon added in v0.8.0

func (s PG) MarkRunEndingSoon(ctx context.Context, id uuid.UUID, endsAt time.Time, thresholdSec int) (bool, error)

MarkRunEndingSoon — see RunLeaser. Keyed on the end itself, so moving the end re-arms every warning without anyone having to reset a counter.

func (PG) MarkRunLost added in v0.8.0

func (s PG) MarkRunLost(ctx context.Context, id uuid.UUID, reason types.LostReason, now time.Time, tokenLife time.Duration) (bool, error)

MarkRunLost — see RunLoser.

func (PG) MarkRunOutputIncomplete added in v0.8.6

func (s PG) MarkRunOutputIncomplete(ctx context.Context, runID uuid.UUID) (bool, error)

MarkRunOutputIncomplete — see RunOutputStore.

func (PG) MarkRunPATRevoked added in v0.8.2

func (s PG) MarkRunPATRevoked(ctx context.Context, runID, authorizationID uuid.UUID, reason, lastError string) (bool, error)

MarkRunPATRevoked — see RunPATStore.

func (PG) MarkRunPaused added in v0.8.0

func (s PG) MarkRunPaused(ctx context.Context, id uuid.UUID, reason types.PauseReason, activeAt *time.Time) (bool, error)

MarkRunPaused — see RunPauser.

func (PG) MarkRunRevived added in v0.8.0

func (s PG) MarkRunRevived(ctx context.Context, id uuid.UUID, from types.LostReason, ended *EndedKept, limit int, startsAgent bool) (bool, error)

MarkRunRevived — see RunReviver.

func (PG) MergeWorkspaceRequirements added in v0.5.0

func (s PG) MergeWorkspaceRequirements(ctx context.Context, id uuid.UUID, add map[string]types.WorkspaceRequirement) (types.Workspace, error)

MergeWorkspaceRequirements ADDS rows to a workspace's requirements overlay atomically (`jsonb ||`), so a concurrent edit can never be clobbered by read-modify-write. The WHERE clause enforces the same key-count cap the PUT endpoint does, evaluated on the POST-merge total so a multi-key merge can't overshoot it in one jump; at the cap it returns ErrConflict.

func (PG) MintAttachTicket added in v0.5.0

func (s PG) MintAttachTicket(ctx context.Context, token string, t AttachTicket, now, expiresAt time.Time) error

MintAttachTicket records one outstanding ticket, expiring at expiresAt, and sweeps already-expired rows in the same statement. The sweep compares against the caller's clock (now), NOT now(), so a DB clock running ahead can't silently delete live tickets.

ponytail: the sweep rides the mint instead of a background worker — the table holds only unredeemed tickets inside a 30s TTL, i.e. a handful of rows. Add a sweeper (or an expires_at index) only if mint volume ever makes that false.

func (PG) MintDelegatedToken added in v0.8.0

func (s PG) MintDelegatedToken(ctx context.Context, t types.DelegatedToken, raw string, now time.Time) (types.DelegatedToken, error)

MintDelegatedToken stores one token (raw is plaintext; only its hash persists), stamps the portal's last use, and sweeps expired tokens. created_at is written on the DB clock, back-dated by t.CreatedAt's age like CreateAPIToken, since it's compared against a session cutoff that Postgres stamps.

ponytail: the sweep rides the mint, like MintAttachTicket — rows live only ten minutes. Add a real sweeper if mint volume ever breaks that.

func (PG) MintEnrolmentToken added in v0.8.0

func (s PG) MintEnrolmentToken(ctx context.Context, token string, t types.DeviceEnrolmentToken) (types.DeviceEnrolmentToken, error)

MintEnrolmentToken inserts one single-use enrolment token. ErrConflict on a unique_violation (23505) against token_sha256 — a raw collision in a 256-bit random space, same as CreateAPIToken's.

func (PG) NotePauseSettle added in v0.8.6

func (s PG) NotePauseSettle(ctx context.Context, id uuid.UUID, after time.Duration) (time.Time, error)

NotePauseSettle — see PauseSettler.

func (PG) NoteRunPATRevokeFailed added in v0.8.2

func (s PG) NoteRunPATRevokeFailed(ctx context.Context, runID, authorizationID uuid.UUID, lastError string) error

NoteRunPATRevokeFailed — see RunPATStore.

func (PG) PendingGroupRemovals added in v0.8.6

func (s PG) PendingGroupRemovals(ctx context.Context, idleFor time.Duration, limit int) ([]PendingGroupRemoval, error)

PendingGroupRemovals is the pendingGroupRemovalWhere removals that no step of the same removal has touched for idleFor (a request still working on it is not one the sweeper may race), at most limit.

func (PG) PendingLeavers added in v0.8.6

func (s PG) PendingLeavers(ctx context.Context, idleFor time.Duration, limit int) ([]PendingLeaver, error)

PendingLeavers is the identities with a suspend or purge ledger row still pending that no step has touched for idleFor (a request still working on it is not one the sweeper may race), at most limit. An identity that is neither deactivated nor purged has a stale ledger and is not listed.

func (PG) Ping added in v0.6.0

func (s PG) Ping(ctx context.Context) error

Ping proves the pool can actually reach Postgres (a live query round-trip, not just a constructed pool).

func (PG) PrincipalAliases added in v0.8.6

func (s PG) PrincipalAliases(ctx context.Context, principal string) ([]string, error)

func (PG) PrincipalForName added in v0.8.6

func (s PG) PrincipalForName(ctx context.Context, name string) (string, error)

func (PG) PrincipalsByEmail added in v0.8.6

func (s PG) PrincipalsByEmail(ctx context.Context, emails []string) ([]string, error)

PrincipalsByEmail is the distinct subs the emails resolve to through api_tokens(principal, email) and people, matched case-insensitively: how a suspension reaches someone no identity row binds.

func (PG) ProposeGovernanceChange added in v0.8.6

ProposeGovernanceChange stores ch as pending, expiring ttl from now on the database's clock. In the same transaction it first moves any lapsed pending change for the same target to expired (returned, for the audit rows), so a live one is the only thing the one-pending-per-target index can refuse: that is *ErrGovernanceChangePending naming it. A create also holds the name it creates (claimCreateName).

func (PG) PruneAWSSSOSpentTokens added in v0.8.0

func (s PG) PruneAWSSSOSpentTokens(ctx context.Context, cutoff time.Time) (int, error)

PruneAWSSSOSpentTokens deletes spent-token rows older than cutoff (aws_sso_spent_tokens_marked_at_idx backs the WHERE clause).

func (PG) PurgeDueIdentities added in v0.8.6

func (s PG) PurgeDueIdentities(ctx context.Context, limit int) ([]uuid.UUID, error)

PurgeDueIdentities is the identities deactivated and not yet purged whose purge_after has passed, oldest first, at most limit. It is only a candidate read: MarkIdentityPurged re-checks under the lock.

func (PG) PurgeTerminalRunProxyConfigs added in v0.8.0

func (s PG) PurgeTerminalRunProxyConfigs(ctx context.Context) (int64, error)

PurgeTerminalRunProxyConfigs — see RunProxyConfigs. The terminal test is types.RunState.IsTerminal itself, not a copy of its set in SQL.

func (PG) PutBranding added in v0.8.0

func (s PG) PutBranding(ctx context.Context, b types.Branding, keepLogo bool) (types.Branding, error)

PutBranding implements BrandingStore.

func (PG) PutCapabilityEnforcement added in v0.6.0

func (s PG) PutCapabilityEnforcement(ctx context.Context, enabled map[string]bool) (map[string]bool, error)

PutCapabilityEnforcement replaces the WHOLE switch map: any capability not named in enabled loses its row (absent == not enforced). One statement, not a transaction: the DELETE and INSERT run against the same CTE snapshot over disjoint capability sets, so the pair is already atomic.

func (PG) PutLaunchPreset added in v0.8.0

func (s PG) PutLaunchPreset(ctx context.Context, p types.LaunchPreset) (types.LaunchPreset, PresetWrite, error)

PutLaunchPreset creates p at version 1, or replaces the named row and moves its version by one. A write identical to the stored row changes nothing, not even the version, so re-applying an unchanged document is a no-op. The comparison and the bump are one statement, so two concurrent writers cannot both land on the same version.

func (PG) PutPrincipalPref added in v0.8.0

func (s PG) PutPrincipalPref(ctx context.Context, principal, key string, value json.RawMessage) error

func (PG) PutRef added in v0.3.1

func (s PG) PutRef(ctx context.Context, ref, substrateName string) error

PutRef upserts the ref -> substrate-name row (ref is the primary key).

func (PG) PutRunLayout added in v0.5.0

func (s PG) PutRunLayout(ctx context.Context, principal, preset string, layout []types.RunLayoutWidget) (types.RunLayout, error)

PutRunLayout upserts principal's layout for preset. ON CONFLICT DO UPDATE makes this one idempotent write with no read-then-decide round trip and no lost-update race between two tabs saving the same preset back to back.

func (PG) PutRunProxyConfig added in v0.8.0

func (s PG) PutRunProxyConfig(ctx context.Context, id uuid.UUID, sealed []byte) error

PutRunProxyConfig — see RunProxyConfigs.

func (PG) PutSiteConfig

func (s PG) PutSiteConfig(ctx context.Context, cfg types.SiteConfig) (types.SiteConfig, error)

PutSiteConfig upserts the single operator-wide site config row. The `singleton` primary key (CHECKed true) makes a second row impossible at the schema level. A write replaces every key types.SiteConfig declares (no partial merge; the API validates the full document first) but keeps any key it doesn't, which only a newer wardynd could have written (declaredJSONKeys).

func (PG) QueryAuditEvents

func (s PG) QueryAuditEvents(ctx context.Context, runID uuid.UUID, limit int) ([]types.AuditEvent, error)

QueryAuditEvents returns audit events for a run in time order. limit <= 0 means no explicit limit (returns up to 1000).

func (PG) QueryAuditEventsFilteredPage added in v0.4.4

func (s PG) QueryAuditEventsFilteredPage(ctx context.Context, runID *uuid.UUID, f AuditFilter, p Page) ([]types.AuditEvent, error)

QueryAuditEventsFilteredPage returns audit events narrowed by f, bounded by p. A non-nil runID scopes to one run and keeps that trail's chronological seq ASC order (the per-run contract in docs/sdk.md); nil is the newest-first global feed. Only a request that actually sets a predicate takes this path — an unfiltered read stays on the two plain queries above, whose plans are unchanged.

No new index: 0001 covers (time) and 0017 (action, seq DESC); a bounded LIMIT query index-scans the pkey backward and filters, which is what the caps are for. DataContains adds a per-row `data @>` test with no GIN index behind it; the planner walks the pkey or the (time) index and filters, and the LIMIT plus a Since window bound it (about 0.1 s over 2M rows in the #1449 review).

func (PG) QueryAuditEventsPage added in v0.3.1

func (s PG) QueryAuditEventsPage(ctx context.Context, runID uuid.UUID, p Page) ([]types.AuditEvent, error)

QueryAuditEventsPage returns a run's audit events in seq (chronological) order, bounded by p. audit_events_run_seq_idx makes WHERE run_id + ORDER BY seq an indexed range scan with no sort; OFFSET pages forward without flipping to DESC, so the per-run trail stays ASC (docs/sdk.md's exit-code contract) and a caller pages to the newest events with ?offset=.

func (PG) QueryRecentAuditEvents

func (s PG) QueryRecentAuditEvents(ctx context.Context, limit int) ([]types.AuditEvent, error)

QueryRecentAuditEvents returns the most-recent audit events across ALL runs, newest first — the global SIEM-style feed the Audit view renders, unlike the chronological per-run QueryAuditEvents.

func (PG) QueryRecentAuditEventsPage added in v0.3.1

func (s PG) QueryRecentAuditEventsPage(ctx context.Context, p Page) ([]types.AuditEvent, error)

QueryRecentAuditEventsPage returns the newest-first global audit feed, bounded by p. seq is the audit_events PRIMARY KEY, so ORDER BY seq DESC + LIMIT is an index-scan-backward with no added index.

func (PG) ReadRunOutputChunks added in v0.8.6

func (s PG) ReadRunOutputChunks(ctx context.Context, runID uuid.UUID, limit int) (RunOutputChunks, error)

ReadRunOutputChunks — see RunOutputChunkStore.

func (PG) ReclampRunLimits added in v0.8.0

func (s PG) ReclampRunLimits(ctx context.Context, from types.AgentRun, toLimits types.RunLimits, toEnd *time.Time, toWait int, endTightenedAt *time.Time) (bool, error)

ReclampRunLimits — see RunLimitsReclamper. The compare on the limits read is what keeps a PATCH decided against the old gate from landing after it, and this write from landing over a PATCH's.

func (PG) RecordPushPathList added in v0.8.0

func (s PG) RecordPushPathList(ctx context.Context, approvalID, runID uuid.UUID, l types.PushPathList, limit int) (bool, error)

func (PG) RefreshAPITokenIdentity added in v0.8.0

func (s PG) RefreshAPITokenIdentity(ctx context.Context, principal, role, userType string, groups []string, truncated bool) error

RefreshAPITokenIdentity re-stamps role, user type, and the group snapshot (with its completeness bit) on EVERY token principal holds — the api-token twin of RefreshSSHKeyRoles; OnLogin fires both so one login bounds both frozen credentials.

SECURITY: a token's identity is frozen at mint with nothing else to refresh it — without this, demoting an admin leaves their wdn_ tokens authenticating AS AN ADMIN until explicitly revoked, and a token keeps authorizing against stale group memberships until a fresh mint.

truncated is bound EXACTLY as passed, never defaulted or inferred: it must come from the login's own session-completeness signal, the same bit a fresh mint stamps. A NULL groups_truncated already reads as TRUNCATED downstream (fail closed); silently defaulting this to false would assert "these are all their groups" for a snapshot that isn't — the wrong direction for an authorization decision.

identity_stamped_at moves with the role in the SAME statement, so a login that stamps a token's role stamps its age too, and a failed statement leaves that principal's tokens stale rather than half-updated. WARDYN_ROLE_STAMP_TTL reads it; `revoked_at IS NULL` stays in the WHERE, so a login never revives a revoked token (an expired one keeps its own expiry, which this never writes).

Still bounded-stale, not live: the ceiling is the owner's next login (see docs/SSH.md §Bounds for the key lane's equivalent). No error when the principal holds no tokens — a zero-row UPDATE is the ordinary case.

SECURITY: a token an admin minted FOR this principal (minted_by set) is revoked instead of re-stamped when the login's role differs from its stamp — its role was derived before the person's own groups were known and the minter saw its plaintext, so re-stamping upward would hand the minter's tier to whoever holds that credential. SET reads the row's OLD role, so the comparison is against the stamp.

func (PG) RefreshRunOutputClaim added in v0.8.6

func (s PG) RefreshRunOutputClaim(ctx context.Context, runID uuid.UUID) error

RefreshRunOutputClaim — see RunOutputStore.

func (PG) RefreshSSHKeyRoles added in v0.6.0

func (s PG) RefreshSSHKeyRoles(ctx context.Context, principal, role string, checkedAt time.Time) error

RefreshSSHKeyRoles re-stamps role and role_checked_at for every key owned by principal, from the OIDC OnLogin hook, with that login's freshly-derived role. This bounds the admin-override stamp to at most WARDYN_SSH_ROLE_TTL stale: it overwrites role too, not only the timestamp, so a demoted or promoted human's keys take effect on their very next login, with no delete-then-re-register needed. No registered keys is a normal, silent no-op.

A CAPPED key (registered in the user view) keeps its user role — the login refreshes only its timestamp, never promotes it.

func (PG) ReleaseAttachLease added in v0.8.6

func (s PG) ReleaseAttachLease(ctx context.Context, runID, holderID uuid.UUID) (bool, error)

ReleaseAttachLease — see AttachLeaseStore.

func (PG) RenameScimGroup added in v0.8.6

func (s PG) RenameScimGroup(ctx context.Context, id uuid.UUID, displayName string) error

RenameScimGroup changes a group's display name; its externalId never changes.

func (PG) RenewAttachLease added in v0.8.6

func (s PG) RenewAttachLease(ctx context.Context, runID, holderID uuid.UUID, ttl time.Duration) (bool, error)

RenewAttachLease — see AttachLeaseStore.

func (PG) ReopenDeprovisionJob added in v0.8.6

func (s PG) ReopenDeprovisionJob(ctx context.Context, identityID uuid.UUID, kind string, k JobKey) error

ReopenDeprovisionJob makes a finished step pending again: a run the ledger counted done that is found live once more.

func (PG) ReserveAttachLease added in v0.8.6

func (s PG) ReserveAttachLease(ctx context.Context, runID, from uuid.UUID, taker, replica string, ttl time.Duration) (AttachLease, uuid.UUID, bool, error)

ReserveAttachLease — see AttachLeaseStore.

func (PG) ResetFederation added in v0.8.0

func (s PG) ResetFederation(ctx context.Context) error

ResetFederation is a (re-)enrolment: a new device identity starts an empty chain at the organisation, resetting the cursor to 0 and clearing the revoked mark. The only writer that clears it.

func (PG) ResolveGovernanceProfile added in v0.7.0

func (s PG) ResolveGovernanceProfile(ctx context.Context, userSubjects, groups []string, userType string) (*types.GovernanceProfile, types.CapabilitySubjectType, error)

ResolveGovernanceProfile returns THE ONE profile that applies to a caller, or ErrNotFound when no assignment matches (which the caller reads as "fall through to the deployment ceiling", the absent-row back-compat rule).

The whole precedence rule is the ORDER BY, deliberately: one indexed read on the UNIQUE(subject_type, subject) btree, so there's no second implementation in Go to skip, mis-order, or forget. subjectMatch and subjectPrecedence are the same fragments ResolveUserDrive splices, so the ceiling and the drive can't disagree about "most specific".

SECURITY: users/groups are normalized from nil to empty for the same reason ListCapabilityGrantsFor does: a nil Go slice binds as SQL NULL and `x = ANY(NULL)` is NULL, not false. Fails closed either way, but a predicate whose behavior hinges on a driver detail has no place at an authorization boundary.

userType is the caller's one type id; "" matches no row.

func (PG) ResolveReauthApproval added in v0.7.6

func (s PG) ResolveReauthApproval(ctx context.Context, id uuid.UUID, decision types.ApprovalDecision, ev types.AuditEvent) (types.ApprovalRequest, error)

ResolveReauthApproval moves a credential_reauth approval to APPROVED and writes its credential.reauth.resolve audit row in ONE transaction: the state change unblocks a HELD credential request, and the audit row is the only durable record that a human acted behind it. Committed separately, a crash between them would leave a run resuming with nothing in the trail saying who resolved it (invariant I7). If either write fails the approval stays PENDING and the idempotent reconcile-on-read tries again.

It is NOT part of the Store interface (see iface.go on transactional surfaces): internal/api type-asserts for it.

The CAS is DecideApproval's own: WHERE state='PENDING', so a concurrent resolver loses cleanly with ErrAlreadyDecided and writes no second audit row.

func (PG) ResolveUserDrive added in v0.7.0

func (s PG) ResolveUserDrive(ctx context.Context, userSubjects, groups []string, userType string) (
	*types.UserDrive, *types.UserDriveGrant, types.CapabilitySubjectType, error)

ResolveUserDrive returns THE ONE drive that applies to a caller, the winning grant, and its tier — or ErrNotFound. The whole precedence rule lives in the ORDER BY (subjectMatch/subjectPrecedence, shared with ResolveGovernanceProfile since both rank the same subject vocabulary): one indexed read on UNIQUE(subject_type, subject), so there's no second Go implementation to mis-order.

Disabled grants stay in the query; the winner's own `enabled` decides PAUSED vs MOUNTED. Excluding them in the WHERE would let a paused row fall through to a wider `all`-tier drive no admin chose for this member — silently, since the only visible signal would be a mount appearing rather than one stopping (DESIGN §2.2).

userSubjects/groups are normalized from nil to empty: a nil Go slice binds as SQL NULL, and `x = ANY(NULL)` is NULL rather than false — same reasoning as ResolveGovernanceProfile.

userType is the caller's one type id; "" matches no row.

func (PG) ResumeGroupRemoval added in v0.8.6

func (s PG) ResumeGroupRemoval(ctx context.Context, groupID, identityID uuid.UUID) (bool, error)

ResumeGroupRemoval says whether the sweeper may go on with a removal it selected earlier: true only when the pair's ledger still has a pending step and the person is not recorded a member again. It never opens a removal or deletes a membership, so a candidate the identity provider added back since it was selected is dropped, not removed again. The identity row is locked FOR UPDATE first, in a statement of its own: a membership insert's foreign-key check holds that row, so the lock waits for an add in flight to commit or roll back. The membership is then read by a second statement, whose snapshot is taken after the lock and so sees that add; an add that starts later waits for this transaction, and is ordered after the answer.

func (PG) RevokeAPIToken added in v0.6.0

func (s PG) RevokeAPIToken(ctx context.Context, id uuid.UUID, principal string, now time.Time) (types.APIToken, error)

RevokeAPIToken marks id revoked. principal scopes the UPDATE when non-empty (self-service: a human may only revoke their OWN token). SECURITY: someone else's id is ErrNotFound, not a distinguishable 403 — no existence leak across principals. An EMPTY principal is the ADMIN path and revokes anyone's.

Already-revoked is ErrNotFound too (`revoked_at IS NULL` in the WHERE): revoke is idempotent, so a second call must not emit a second token.revoke audit row for an act that didn't happen.

func (PG) RevokeDelegate added in v0.8.0

func (s PG) RevokeDelegate(ctx context.Context, id uuid.UUID, now time.Time) (types.Delegate, error)

RevokeDelegate marks id revoked. Unknown or already-revoked is ErrNotFound, so a second revoke writes no second audit row. Outstanding delegated tokens need no write of their own: GetDelegatedTokenByRaw joins on this column.

func (PG) RevokeDevice added in v0.8.0

func (s PG) RevokeDevice(ctx context.Context, id uuid.UUID, now time.Time) (types.Device, error)

RevokeDevice marks id revoked. Already-revoked is ErrNotFound too (`revoked_at IS NULL` in the WHERE): idempotent, so a second call emits no second revoke audit row.

func (PG) RevokeEnrolmentToken added in v0.8.0

func (s PG) RevokeEnrolmentToken(ctx context.Context, id uuid.UUID, now time.Time) (types.DeviceEnrolmentToken, error)

RevokeEnrolmentToken cancels a redeemable token by setting consumed_at (the column ConsumeEnrolmentToken's WHERE requires NULL), so a revoke can't race a redemption. Already-redeemed/revoked/expired/unknown is ErrNotFound.

func (PG) RunCapacity added in v0.8.6

func (s PG) RunCapacity(ctx context.Context, o RunCapacityOpts) (RunCapacity, error)

RunCapacity reads every non-terminal row in one query (the positive types.NonTerminalRunStates list, never NOT IN terminal, so a new terminal state cannot be counted as active) and folds it per the state table in the 0.8.6 fleet design: STARTING, RUNNING and WAITING_FOR_CONFIRMATION rows with lost_at NULL hold capacity, paused ones included; kept rows (lost_at set) count as Kept; PENDING rows appear only in States. A kept row whose agent still runs (HoldsSandboxSQL: kept after an outage, before its end) also holds its agent's reservation, without the proxy that was stopped.

func (PG) RunHasOpenRequest added in v0.8.0

func (s PG) RunHasOpenRequest(ctx context.Context, id uuid.UUID) (bool, error)

RunHasOpenRequest — see RunPauser.

func (PG) RunIDsCreatedBy added in v0.8.6

func (s PG) RunIDsCreatedBy(ctx context.Context, createdBy string) ([]uuid.UUID, error)

func (PG) RunWatcherFresh added in v0.5.0

func (s PG) RunWatcherFresh(ctx context.Context, id uuid.UUID, staleAfter time.Duration) (bool, error)

RunWatcherFresh reports whether id's watcher lease is younger than staleAfter — mirrors ClaimStaleRunWatchers' own staleness predicate (a live watcher owns it). A missing row → (false, nil): nothing is watching it, so the reaper may proceed.

func (PG) SaveChunkedRunOutput added in v0.8.6

func (s PG) SaveChunkedRunOutput(ctx context.Context, runID uuid.UUID, maskScope string, limit int) (bool, error)

SaveChunkedRunOutput — see RunOutputChunkStore.

func (PG) SaveFinalRunOutput added in v0.8.6

func (s PG) SaveFinalRunOutput(ctx context.Context, o RunOutput) error

SaveFinalRunOutput — see RunOutputStore.

func (PG) SaveGapRunOutput added in v0.8.6

func (s PG) SaveGapRunOutput(ctx context.Context, runID uuid.UUID) (bool, error)

SaveGapRunOutput — see RunOutputStore.

func (PG) ScimGroupMembers added in v0.8.6

func (s PG) ScimGroupMembers(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error)

ScimGroupMembers is the identity ids recorded as members of the group, oldest first.

func (PG) SearchIdentities added in v0.8.6

func (s PG) SearchIdentities(ctx context.Context, issuer, attr, value string, limit int) ([]PrincipalIdentity, error)

SearchIdentities is the identity rows a SCIM filter `attr eq value` selects under issuer, at most limit. externalId matches the SCIM projection or the bound object id; userName and emails.value match the projection, the login email and every alias the identity has been seen under.

func (PG) SearchScimGroups added in v0.8.6

func (s PG) SearchScimGroups(ctx context.Context, attr, value string, limit int) ([]ScimGroup, error)

SearchScimGroups is the groups a SCIM filter `attr eq value` selects, at most limit. Both attributes match case-insensitively.

func (PG) SetAuditRetentionPolicy added in v0.8.6

func (s PG) SetAuditRetentionPolicy(ctx context.Context, days int) (AuditRetentionPolicyChange, error)
func (s PG) SetBrandingLogo(ctx context.Context, logo []byte, logoType string, fromFile bool, updatedBy string) (bool, error)

SetBrandingLogo implements BrandingStore.

func (PG) SetCapabilityRestriction added in v0.8.0

func (s PG) SetCapabilityRestriction(ctx context.Context, capability, value string, restricted bool, by string) error

SetCapabilityRestriction restricts one value (inserts its row, keeping the first writer's provenance) or lifts the restriction (deletes it).

func (PG) SetFederationCursor added in v0.8.0

func (s PG) SetFederationCursor(ctx context.Context, seq int64, rowHash string) error

SetFederationCursor durably advances the forwarder's cursor (seq and the local row's row_hash, so a later tick can tell it from a table-reset row at the same seq), upserting the singleton row. Called after a batch is accepted upstream, so a crash between accept and this write only costs an idempotent retry (see IngestDeviceAudit), never a gap.

func (PG) SetRunAgentExecID added in v0.3.1

func (s PG) SetRunAgentExecID(ctx context.Context, id uuid.UUID, execID string) error

SetRunAgentExecID scoped-writes ONLY agent_exec_id, once the driver execs the agent. The crash reconciler reads it to observe agent liveness across a restart.

func (PG) SetRunContainmentError added in v0.8.0

func (s PG) SetRunContainmentError(ctx context.Context, id uuid.UUID, msg string, now time.Time) error

SetRunContainmentError — see RunLeaser.

func (PG) SetRunDiskMiB added in v0.8.0

func (s PG) SetRunDiskMiB(ctx context.Context, id uuid.UUID, mib int) error

SetRunDiskMiB scoped-writes ONLY the resolved ephemeral-disk-cap column. Called once applyEphemeralDisk resolves the effective disk_mib, since that needs dispatch-time inputs unavailable at CreateRun.

func (PG) SetRunEndAndWait added in v0.8.0

func (s PG) SetRunEndAndWait(ctx context.Context, id uuid.UUID, fromLimits types.RunLimits, fromEnd *time.Time, fromWait int, toEnd *time.Time, toWait int, ended *EndedKept) (bool, error)

SetRunEndAndWait — see RunLeaser. The compare on the values the caller read is what makes the caller's clamp and gate decision the one that lands: a concurrent change, the lease sweep ending the run, or a re-clamp tightening the limits the gate was read from turns this into a no-op.

func (PG) SetRunFailureHint added in v0.6.0

func (s PG) SetRunFailureHint(ctx context.Context, id uuid.UUID, hint string) error

SetRunFailureHint scoped-writes ONLY failure_hint — the one-line operator reason a run FAILED before its agent started, known only at the failure site (failAndRevoke). Best-effort at the call site; ErrNotFound when no row matched.

func (PG) SetRunImage added in v0.2.0

func (s PG) SetRunImage(ctx context.Context, id uuid.UUID, image string) error

SetRunImage scoped-writes ONLY the resolved-image provenance column, once the image resolves after the row is inserted.

func (PG) SetRunProxyRelease added in v0.8.0

func (s PG) SetRunProxyRelease(ctx context.Context, id uuid.UUID, release string) error

SetRunProxyRelease — see RunReviver.

func (PG) SetRunSizing added in v0.8.6

func (s PG) SetRunSizing(ctx context.Context, id uuid.UUID, z RunSizing) error

SetRunSizing scoped-writes ONLY the dispatch-time sizing columns. It does not bump updated_at: the record is not activity, and the idle clock reads updated_at.

func (PG) SetRunStatusDetail added in v0.7.6

func (s PG) SetRunStatusDetail(ctx context.Context, id uuid.UUID, detail string) error

SetRunStatusDetail scoped-writes ONLY status_detail — what the substrate says a STARTING run is waiting on, or the control plane's own `image: Building` line on a PENDING run. Fed by runner.SandboxSpec.OnWaiting, once per CHANGE of reason. Deliberately does NOT bump updated_at: that column is also what the idle reaper and the killed-run tail-upload grace measure by, and a diagnostic line a kubelet triggers must never buy a run more life or hold a terminal run's upload door open.

func (PG) SetRunTitle added in v0.8.0

func (s PG) SetRunTitle(ctx context.Context, id uuid.UUID, title string) error

SetRunTitle — see RunTitler.

func (PG) SetSandboxRef

func (s PG) SetSandboxRef(ctx context.Context, id uuid.UUID, ref string) error

SetSandboxRef records the runner reference (container ID / pod name). A non-empty ref also records this release as the one that started the run's proxy (proxy_release, migration 0084).

func (PG) SetSourceScanResult added in v0.5.0

func (s PG) SetSourceScanResult(ctx context.Context, id uuid.UUID, profile []byte, status types.WorkspaceStatus, runID uuid.UUID, seed map[string]types.WorkspaceRequirement) (types.Source, error)

SetSourceScanResult persists a scan outcome FENCED on the claiming run: only the run holding active_run_id may write, so a stale upload from a superseded run can never clobber a fresher result. `seed` rebuilds the scan_seeded subset of requirements (provenance-aware, see sourceScanRebuild); NULL (failed scan) leaves the contract untouched, while a non-nil empty seed still rebuilds — see sourceRequirementsParam.

func (PG) SetSourceScanResultUnfenced added in v0.5.0

func (s PG) SetSourceScanResultUnfenced(ctx context.Context, id uuid.UUID, profile []byte, status types.WorkspaceStatus, seed map[string]types.WorkspaceRequirement) (types.Source, error)

SetSourceScanResultUnfenced persists a SYNCHRONOUS (inline local_dir) scan, which never claimed a run slot to fence against. Same provenance-aware rebuild semantics as the fenced writer (sourceScanRebuild).

func (PG) SetWorkspaceApprovedEgress

func (s PG) SetWorkspaceApprovedEgress(ctx context.Context, id uuid.UUID, domains []string) (types.Workspace, error)

SetWorkspaceApprovedEgress replaces ONLY the approved-egress column (plus updated_at and egress_edited_at), scoped so it can never clobber a concurrently-persisted async scan. egress_edited_at always bumps, even for an identical list, since ReconcileWorkspaceEgressDecisions treats it as "the operator confirmed this at this moment".

func (PG) SetWorkspaceBuiltImage

func (s PG) SetWorkspaceBuiltImage(ctx context.Context, id uuid.UUID, imageRef, builtHash string) (types.Workspace, error)

SetWorkspaceBuiltImage scoped-writes ONLY the image cache columns (build-once/reuse-many), same anti-clobber discipline as the other scoped writers.

func (PG) SetWorkspaceDeniedEgress added in v0.5.0

func (s PG) SetWorkspaceDeniedEgress(ctx context.Context, id uuid.UUID, domains []string) (types.Workspace, error)

SetWorkspaceDeniedEgress is denied_egress's mirror of SetWorkspaceApprovedEgress: same anti-clobber scoping, same full-list replace, same egress_edited_at stamp (removing a permanent DENY is an operator override the boot heal must not undo).

func (PG) SetWorkspaceImportState

func (s PG) SetWorkspaceImportState(ctx context.Context, id uuid.UUID,
	status types.WorkspaceStatus, activeRunID *uuid.UUID, expectedActive *uuid.UUID) (types.Workspace, bool, error)

SetWorkspaceImportState is the scoped writer the scan orchestrator uses to advance status + the in-flight run pointer. Fenced on expectedActive (the active_run_id observed in the read the decision came from; nil means "no in-flight run"), so a caller deciding from a STALE read cannot land it. applied=false means the slot moved under the caller, which must re-read rather than retry blindly.

func (PG) SetWorkspaceLLMCred added in v0.4.0

func (s PG) SetWorkspaceLLMCred(ctx context.Context, id uuid.UUID, cred *types.WorkspaceLLMCred) (types.Workspace, error)

SetWorkspaceLLMCred replaces ONLY the model/harness cred binding column (plus updated_at), scoped so it can never clobber a concurrently-persisted async scan. Pass nil to clear the binding.

func (PG) SetWorkspaceOwner added in v0.6.0

func (s PG) SetWorkspaceOwner(ctx context.Context, id uuid.UUID, owner string) (types.Workspace, error)

SetWorkspaceOwner replaces ONLY the owned_by column (plus updated_at) — the offboarding reassign (decision O6). Deliberately the ONLY writer of the column after CreateWorkspace: UpdateWorkspace omits owned_by, so an ordinary edit can never move ownership.

func (PG) SetWorkspaceRecordResult

func (s PG) SetWorkspaceRecordResult(ctx context.Context, id uuid.UUID,
	taskKey string, result json.RawMessage, onlyIfStatus string) (types.Workspace, bool, error)

SetWorkspaceRecordResult atomically upserts ONE task's entry in the Record Mode record_results map (jsonb merge, so concurrent writers of DIFFERENT tasks never lose each other's entries). When onlyIfStatus is non-empty the write is a compare-and-set against the task's stored status. Returns applied=false (no error) on a guard miss.

func (PG) SetWorkspaceRequirements added in v0.5.0

func (s PG) SetWorkspaceRequirements(ctx context.Context, id uuid.UUID, reqs map[string]types.WorkspaceRequirement) (types.Workspace, error)

SetWorkspaceRequirements replaces ONLY the requirements-contract column (plus updated_at), scoped like SetWorkspaceApprovedEgress. Pass the FULL desired map — this replaces rather than merges.

func (PG) SpendApprovalOnce added in v0.7.10

func (s PG) SpendApprovalOnce(ctx context.Context, id uuid.UUID, jti string) (bool, error)

SpendApprovalOnce spends a `once` Azure DevOps capability approval: it writes jti into the row's minted_jti if, and only if, nothing has spent it yet. spent=false means another request already did (or the row is not an approved, once-scoped escalation) and the caller must not forward. The empty-minted_jti condition is the whole exactly-once argument: two concurrent re-resolves both reach this UPDATE, Postgres serialises them on the row, and the second finds the column already written.

Not part of the Store interface (see ResolveReauthApproval): the api package type-asserts for it.

func (PG) StampRunActive added in v0.8.0

func (s PG) StampRunActive(ctx context.Context, id uuid.UUID) (bool, error)

StampRunActive — see RunPauser.

func (PG) StampRunTokenRenewed added in v0.8.0

func (s PG) StampRunTokenRenewed(ctx context.Context, id uuid.UUID) (bool, error)

StampRunTokenRenewed — see RunLoser.

func (PG) StartGroupRemoval added in v0.8.6

func (s PG) StartGroupRemoval(ctx context.Context, groupID, identityID uuid.UUID) error

StartGroupRemoval opens the ledger of one person leaving one group, in one transaction with the removal of the membership row. The three steps join as pending when the person was recorded a member (a new removal, so an earlier finished one is cleared) or when no removal of this pair was ever recorded (a member whose add never reached Wardyn is still removed). A pair whose last removal finished and who has not been added since is a repeat and changes nothing, so an identity provider that repeats the request costs nothing, and a removal still pending is resumed.

func (PG) StopKeptRunIf added in v0.8.0

func (s PG) StopKeptRunIf(ctx context.Context, id uuid.UUID, to types.RunState, lostAt *time.Time, lostReason types.LostReason, endsAt *time.Time) (bool, error)

StopKeptRunIf — see RunLeaser.

func (PG) SubjectKeyDestroyedSince added in v0.8.6

func (s PG) SubjectKeyDestroyedSince(ctx context.Context, owner, purpose string, since time.Time) (bool, error)

func (PG) SuspendIdentity added in v0.8.6

func (s PG) SuspendIdentity(ctx context.Context, p SuspendPlan) (SuspendResult, error)

SuspendIdentity is step 1 of a suspension, one transaction. It takes every row it will change FOR UPDATE, in id order (so a sign-in holding one of them finishes first or is refused), writes the session cutoff for every sub and email form, deactivates the identity rows and bumps their authority epoch, stamps the people rows, records the step done and the plan's pending steps. A suspension of an active identity first clears the ledger of any earlier one.

func (PG) TouchAPIToken added in v0.6.0

func (s PG) TouchAPIToken(ctx context.Context, id uuid.UUID, now time.Time) error

TouchAPIToken records that id was just used. BEST EFFORT: the auth branch ignores the error, since failing to record a touch must never fail an otherwise-valid request.

ponytail: one UPDATE per authenticated request; add a throttle only if a hot token ever makes that write volume a problem.

func (PG) TouchDevice added in v0.8.0

func (s PG) TouchDevice(ctx context.Context, id uuid.UUID, now time.Time) error

TouchDevice records that id was just used. Best effort, like TouchAPIToken: the request path must never fail an otherwise-valid request over a lost keepalive write.

func (PG) TouchRun

func (s PG) TouchRun(ctx context.Context, id uuid.UUID) error

TouchRun bumps a run's updated_at to now() without changing any other field — the activity keepalive interactive-attach calls so the idle reaper doesn't stop a run a human is attached to. The TERMINAL-run guard lives HERE, in one WHERE clause, rather than at each of the four callers: updated_at is also the clock the killed-run tail-upload grace is measured from, so an authenticated caller touching a killed run's row on a cadence could hold that upload door open indefinitely. State predicate is the positive list, for the reason given on CountActiveRunsBy.

func (PG) UpdateBaseImageName added in v0.5.0

func (s PG) UpdateBaseImageName(ctx context.Context, id uuid.UUID, name string) (types.BaseImageEntry, error)

UpdateBaseImageName renames a catalog base-image row — the explicit, scoped rename path, mirroring UpdateSourceConfig above. handleCreateBaseImage is the only caller, on an identity hit where the REQUEST carried an explicit name (never via UpsertBaseImage's own conflict clause — see its doc).

func (PG) UpdatePolicy

func (s PG) UpdatePolicy(ctx context.Context, id uuid.UUID, name string, spec types.RunPolicySpec) (types.RunPolicy, error)

UpdatePolicy replaces a policy's name and spec and bumps updated_at. Returns ErrNotFound when no policy has the given id, and ErrConflict when the rename collides with run_policies.name's UNIQUE constraint — same mapping as CreatePolicy, since a rename onto a taken name is the same caller-fixable mistake as an insert under one. The caller validates the spec before calling.

func (PG) UpdateRunStateIf

func (s PG) UpdateRunStateIf(ctx context.Context, id uuid.UUID, fromState, toState types.RunState) (bool, error)

UpdateRunStateIf conditionally transitions a run from fromState to toState in a single UPDATE ... WHERE id=$ AND state=$from, returning whether it applied. TOCTOU-safe: a concurrent kill/stop that already moved the run to a terminal state is never clobbered. False with a nil error means someone else won the transition (or the run doesn't exist); the caller does nothing.

func (PG) UpdateRunStateIfCreatedBefore added in v0.8.6

func (s PG) UpdateRunStateIfCreatedBefore(ctx context.Context, id uuid.UUID, fromState, toState types.RunState, createdNotAfter time.Time) (bool, error)

UpdateRunStateIfCreatedBefore transitions a run only when still in fromState, created_at is at or before createdNotAfter, and its agent still runs (HoldsSandboxSQL: not kept, or kept after an outage before its end; a run kept with its agent stopped is left to its files grace). The max-age stop's own predicate on the run's age: it carries neither the idleness guard nor the open-request guard of UpdateRunStateIfIdle, because an absolute age cap exists to end a run that is not going to finish by itself.

func (PG) UpdateRunStateIfIdle

func (s PG) UpdateRunStateIfIdle(ctx context.Context, id uuid.UUID, fromState, toState types.RunState, notAfter time.Time) (bool, error)

UpdateRunStateIfIdle is UpdateRunStateIf plus an idleness guard: it transitions a run only when still in fromState, updated_at has not advanced past notAfter (the caller's snapshot), it has no lost/end mark, and it has no open hold-aware request within its wait. Closes the reaper's idleness TOCTOU — an active `wardyn run attach` TouchRun can bump updated_at between snapshot and UPDATE, so guarding on state alone could stop a now-active run; notAfter makes a touched-since-snapshot run no-op instead. The hold-aware clause enforces "idle stop never fires while a request is open within its wait" inside the same atomic CAS, since a request raised in that window is visible only to a check inside the statement.

func (PG) UpdateSourceConfig added in v0.5.0

func (s PG) UpdateSourceConfig(ctx context.Context, id uuid.UUID, name string, reqs map[string]types.WorkspaceRequirement) (types.Source, error)

UpdateSourceConfig replaces a source's operator-editable fields (name, requirements) — scoped, never touching scan-owned columns.

func (PG) UpdateUserType added in v0.8.0

func (s PG) UpdateUserType(ctx context.Context, t types.UserType) (types.UserType, error)

UpdateUserType rewrites a type's name, description and priority; id and built_in never change. ErrNotFound when missing, ErrConflict on name clash.

func (PG) UpdateUserTypeMetadata added in v0.8.6

func (s PG) UpdateUserTypeMetadata(ctx context.Context, t types.UserType) (types.UserType, error)

UpdateUserTypeMetadata rewrites a type's name and description and never its priority, so an edit that was judged metadata-only cannot overwrite a priority change approved after it was read. ErrNotFound when missing, ErrConflict on name clash.

func (PG) UpdateWorkspace

func (s PG) UpdateWorkspace(ctx context.Context, id uuid.UUID, ws types.Workspace, stampEgressEdit bool) (types.Workspace, error)

UpdateWorkspace replaces a workspace's editable composition via a FULL-column write of every scan-owned field (profile, image_ref, built_profile_hash, status, etc.), WITH ONE DELIBERATE EXCEPTION: denied_egress is NOT in this SET clause (see Workspace.DeniedEgress) — a permanent deny survives a composition edit, unlike ApprovedEgress. Callers must round-trip the fetched row. Returns ErrNotFound for an unknown id.

egress_edited_at is passed through unless stampEgressEdit marks this write as the operator action the column records: unconditional stamping would suppress the boot heal for a decision nobody undid, while plain omission let this writer clear approved_egress without moving the stamp, so ReconcileWorkspaceEgressDecisions re-widened the list on restart. The stamp is now() from the database, not wardynd's clock, since it's compared against approvals.decided_at and clock skew there fails OPEN.

func (PG) UpsertBaseImage added in v0.5.0

func (s PG) UpsertBaseImage(ctx context.Context, b types.BaseImageEntry) (types.BaseImageEntry, error)

UpsertBaseImage inserts a catalog image or returns the row with the same identity (kind, image, steps) — the identity index is the dedupe rule. The CHECK constraint refuses 'recommended' structurally: that build is derived per-workspace and has no catalog identity.

An identity hit does NOT touch name, deliberately: this upsert is also the PASSTHROUGH path a workspace/run resolves its base-image spec through, which always derives an auto-placeholder name — applying EXCLUDED.name unconditionally would silently rename an operator's custom-named row back to that placeholder. See UpdateBaseImageName for the rename path.

func (PG) UpsertCapabilityGrant added in v0.6.0

func (s PG) UpsertCapabilityGrant(ctx context.Context, g types.CapabilityGrant) (types.CapabilityGrant, error)

UpsertCapabilityGrant writes one grant, keyed on the natural (subject_type, subject, capability, value) UNIQUE: re-granting the same triple FLIPS the effect in place rather than leaving two contradictory rows (which would resolve as a permanent deny). The returned row carries the EXISTING id on a conflict, not g.ID, so a re-submit gets the id the DELETE route needs.

func (PG) UpsertGovernanceAssignment added in v0.7.0

func (s PG) UpsertGovernanceAssignment(ctx context.Context, a types.GovernanceAssignment) (types.GovernanceAssignment, error)

UpsertGovernanceAssignment binds one subject to one profile, keyed on the natural UNIQUE (subject_type, subject): re-assigning a subject REPOINTS its single row rather than leaving a second one behind (two rows for one subject would make "which profile does Bob get" a tie-break instead of the admin's last write).

The returned row carries the EXISTING id on conflict, not a.ID, so the caller gets the id the DELETE route needs.

Returns ErrNotFound when profile_id names no profile (FK 23503 → 404).

func (PG) UpsertGovernanceProfile added in v0.7.0

func (s PG) UpsertGovernanceProfile(ctx context.Context, p types.GovernanceProfile) (types.GovernanceProfile, error)

UpsertGovernanceProfile writes one profile exactly as given. It is WriteGovernanceProfile with nothing to decide, so it takes the same graph lock and a write can never skip it.

func (PG) UpsertLoginIdentity added in v0.8.6

func (s PG) UpsertLoginIdentity(ctx context.Context, in LoginIdentity, now time.Time) (PrincipalIdentity, error)

func (PG) UpsertRoleMapping added in v0.7.0

func (s PG) UpsertRoleMapping(ctx context.Context, m types.RoleMapping) (types.RoleMapping, error)

UpsertRoleMapping writes one row, keyed on the natural UNIQUE (value): re-adding an already-mapped value FLIPS its role in place rather than leaving a second row behind — the same "re-granting the same key updates it" contract UpsertCapabilityGrant follows, for the same reason (two rows for one value cannot happen; the UNIQUE index already forbids it, this just makes the natural retry path a clean update instead of a 409).

The returned row carries the row's real id, which on a conflict is the EXISTING one, not m.ID — the caller needs the id the DELETE route will be given, and an admin re-submitting the same value must not be handed an id that names no row.

A user_type naming no user_types row fails the foreign key; that is ErrNotFound, so the caller can tell a type deleted between its check and this write from a store fault.

The conflict path updates role and user_type only, deliberately NOT created_by — creation provenance (who ADDED this mapping) stays with the original creator across a later role flip by a different admin, the same way created_at is untouched on conflict (no SET at all, so Postgres leaves it).

The conflict path also forces migrated_from_member to false, UNCONDITIONALLY — on a flip to admin/security_admin too, not only a real type pick (never EXCLUDED.migrated_from_member, which the caller never sets to true anyway — see the type's own doc comment). Decision, not an oversight: the marker means "a person still needs to look at this row", and an admin choosing ANYTHING for it — a type or a tier — is that look. The alternative (clear only on a type pick) would let the marker survive a flip to admin and ride along if the row is later flipped BACK to user with no type named — a "migrated" chip on a row an admin, in the interim, explicitly decided was an admin mapping, which is a worse answer than "the row was touched, the flag's job is done" for a marker the design itself frames as a one-time prompt, never a permanent history bit. TestPG_RoleMappings_UpsertClearsMigratedFromMemberOnAdminFlip pins this call.

func (PG) UpsertSource added in v0.5.0

func (s PG) UpsertSource(ctx context.Context, src types.Source) (types.Source, error)

UpsertSource inserts a library source or returns the existing row with the same identity (kind, locator, ref) — ONE statement, no read-then-write race: the UNIQUE constraint IS the dedupe rule. Identity fields must arrive CANONICALIZED (the api layer owns that) — the store stores what it is given.

func (PG) UpsertUserDrive added in v0.7.0

func (s PG) UpsertUserDrive(ctx context.Context, d types.UserDrive, refuseIfAllocated bool) (types.UserDrive, error)

UpsertUserDrive writes one drive keyed on ID: INSERT if absent, else UPDATE in place — renaming is allowed while grants still point at it, since ON DELETE RESTRICT rules out delete-and-recreate for an allocated drive. A rename moves the PVC's name; existing members bind an object that no longer exists until they re-mount (docs/OPERATIONS.md "User drives on Kubernetes"). Returns ErrConflict on UNIQUE(name).

An identity-affecting PUT on an already-allocated drive is refused 409 (ErrDriveAllocated) unless ?confirm=rehome; the precondition is re-asserted in this statement, under the row lock UpsertUserDrive takes, so a grant created between the guard's read and this write can't be silently re-homed.

SECURITY: two host_path shares over the same root must derive home names the same way (namespace is the host_root, not the drive — see shareBackend). Different home_templates (`sub` vs `email_local`) could otherwise fold two principals onto one directory name, breaking migration 0059's one-home-per-principal invariant on a backend its index can't reach; same-template drives on one root stay legal. Enforced in the statement itself, not a prior read — not a hard constraint under READ COMMITTED, but the case that actually happens (one admin registering a second view) can't race itself.

created_by/created_at are untouched on update: provenance stays with whoever registered the drive.

func (PG) UpsertUserDriveGrant added in v0.7.0

func (s PG) UpsertUserDriveGrant(ctx context.Context, g types.UserDriveGrant, homeOverrideStated bool) (types.UserDriveGrant, error)

UpsertUserDriveGrant allocates one drive to one subject, keyed on UNIQUE(subject_type, subject): re-allocating REPOINTS the existing row rather than leaving a second one, so a subject's allocation is always the admin's last write. On conflict the returned id is the EXISTING row's, not g.ID. Returns ErrNotFound when drive_id names no drive (FK, 23503).

SECURITY: refuses (ErrConflict) if another subject already holds the same home_override in the same object-name NAMESPACE (host_root, not drive — see shareBackend) — one home_override must name one person's directory, or two people end up sharing a minted volume with every bind-time check passing. Guard checks same-drive OR two host_path drives on the same host_root (compared as the stored string; driveHostRootNesting handles the symlink case on the drive-write path). Enforced in the statement itself, one round trip: migration 0059's partial unique index is the race-free floor for the same-drive case; the cross-drive case has none, so here the statement is the whole guard — sufficient for the case that happens, one admin typing one name twice. Residual: a DERIVED (non-override) home can still collide across same-root shares with different templates; not refusable here since a group/all grant doesn't know its principals until sign-in.

enabled is written verbatim, so a zero-value grant is disabled by default — fail-closed at the API boundary, where the omission is still visible.

SECURITY: also guards a silent re-home. Clearing home_override moves that person to the derived name next run, stranding their old directory unnamed. homeOverrideStated is the API's tri-state for "the client said nothing": false refuses the update on a row with a stated override (no row returned, read as ErrConflict); stating it IS the confirmation, no ?confirm= to invent. The zero value is safe — a caller that forgets the argument gets a refusal, not a silent clear.

func (PG) UserTypeReferences added in v0.8.0

func (s PG) UserTypeReferences(ctx context.Context, id string) (int, error)

UserTypeReferences counts the capability-grant, governance-assignment and drive-grant rows that name the type as their subject.

func (PG) UserTypeTokenStamps added in v0.8.0

func (s PG) UserTypeTokenStamps(ctx context.Context, id string) (int, error)

UserTypeTokenStamps counts the unrevoked API tokens stamped with the type.

func (PG) VerifyAuditChain added in v0.6.0

func (s PG) VerifyAuditChain(ctx context.Context) (AuditChainStatus, error)

VerifyAuditChain walks the audit_events hash chain oldest-first, re-hashing every row with migration 0047's audit_row_hash — the SAME function the insert trigger uses, so there's no second implementation to drift.

The sweep is paged as a correctness property, not just a cost one: an unbounded `ORDER BY seq` can plan as Seq Scan -> Sort, buffering the whole table before returning row one — the tamper-investigation endpoint would get slowest over time. Keyset paging (`seq > $1 ORDER BY seq LIMIT n`) is an index scan with a bound instead, so memory is one page and an early break genuinely stops the work.

Paging does not verify less: since migration 0056 the trigger allocates seq while holding the chain lock and releases it at commit, so seq order IS commit order, and a row can never appear below a cursor the walk already passed. Walk state is carried across pages by one auditChainWalk, so a chain longer than a page is one continuous chain.

Everything runs in ONE repeatable-read snapshot, so the chain, the newest attested retention drop, the high-water mark and the partition manifest are the same instant: an append between two reads cannot read as a removed tail. The walk starts from the newest `drop` anchor that dropped rows (row_count above 0: an empty partition leaves nothing to chain to; the first retained row must chain to its recorded tail) and still inspects every retained row. The chain alone cannot see a removed NEWEST tail, so after it the newest row must also match audit_partition_meta's high-water mark, and every partition the expected manifest names must be present unless an anchor accounts for it.

func (PG) WithIdentityShared added in v0.8.6

func (s PG) WithIdentityShared(ctx context.Context, g IdentityGuard, fn func() error) error

WithIdentityShared runs fn while g's identity rows are held FOR SHARE and unchanged: the write a sign-in's captured credential makes lands before a suspension takes the rows, or is refused with ErrIdentityDeactivated. fn may write through another connection (the secret store).

The rows are held on a connection dialled for this call, outside the pool: fn borrows from the pool, and a hold taken from it would wait on itself once the pool is down to that one connection. The dials are bounded like the lock connections: at most db.LockPoolMaxConns at once per pool, process-wide, and a call that finds them all in use waits db.LockPoolAcquireWait and is then refused with db.ErrLockNoCapacity.

func (PG) WorkspacesAttaching added in v0.5.0

func (s PG) WorkspacesAttaching(ctx context.Context, id uuid.UUID) ([]string, error)

WorkspacesAttaching returns the names of workspaces whose attachments reference source id — the loud half of delete-in-use (one GIN probe on workspaces_attachments_gin). DELETE refuses with these names rather than silently orphaning a workspace to a source that no longer exists.

func (PG) WorkspacesUsingBaseImage added in v0.5.0

func (s PG) WorkspacesUsingBaseImage(ctx context.Context, id uuid.UUID) ([]string, error)

WorkspacesUsingBaseImage is the catalog's delete-in-use check.

func (PG) WriteGovernanceProfile added in v0.8.6

func (s PG) WriteGovernanceProfile(ctx context.Context, id uuid.UUID, build GovernanceProfileBuild) (types.GovernanceProfile, error)

WriteGovernanceProfile writes one profile keyed on its PRIMARY KEY: an unminted id INSERTs, an existing one UPDATEs in place (name included, so a profile can be renamed while assignments still point at it, since ON DELETE RESTRICT makes delete-and-recreate impossible for an assigned profile). One statement serves POST (fresh id) and PUT (id from the path).

The whole write is one transaction under a graph-wide advisory lock: build sees every profile as that transaction reads them, decides the row (merging what a PUT left absent, refusing a cycle, a depth overflow or an overlay its base does not permit) and the row is stored before the lock is released, so two concurrent writes cannot together make a cycle or a depth-4 chain.

contact follows GovernanceProfile.ContactSet: written (or cleared, when empty) only when it is true, otherwise the stored value stays. A composed row (Overlay set) binds '{}' for ceiling and limits and sets them outright, since it stores no raw authority; its composition columns are always written from the row as given.

Returns ErrConflict when UNIQUE(name) rejects the write. created_by and created_at are NOT touched on update: creation provenance stays with whoever authored the profile.

type Page added in v0.3.1

type Page struct {
	Limit  int
	Offset int
}

Page bounds a List query to Limit rows after Offset, ordered by the query's own ORDER BY. Limit<=0 means unbounded, used by internal callers needing the whole table; public read handlers always pass an explicit Limit (capped by api.parseListPage).

type Pager added in v0.3.1

type Pager interface {
	ListRunsPage(ctx context.Context, p Page) ([]types.AgentRun, error)
	ListPoliciesPage(ctx context.Context, p Page) ([]types.RunPolicy, error)
	ListWorkspacesPage(ctx context.Context, p Page) ([]types.Workspace, error)
	ListApprovalsPage(ctx context.Context, stateFilter types.ApprovalState, p Page) ([]types.ApprovalRequest, error)
	QueryAuditEventsPage(ctx context.Context, runID uuid.UUID, p Page) ([]types.AuditEvent, error)
	QueryRecentAuditEventsPage(ctx context.Context, p Page) ([]types.AuditEvent, error)
	// QueryAuditEventsFilteredPage serves a narrowed audit read (time range,
	// action, outcome, actor type); see AuditFilter in auditfilter.go.
	QueryAuditEventsFilteredPage(ctx context.Context, runID *uuid.UUID, f AuditFilter, p Page) ([]types.AuditEvent, error)
	// ListUserDriveGrantsPage serves the drives console's allocation table;
	// its ORDER BY has no index, so unbounded it sorts every allocation on
	// every load of the admin screen.
	ListUserDriveGrantsPage(ctx context.Context, p Page) ([]types.UserDriveGrant, error)
}

Pager is the paginated read surface, deliberately excluded from Store: widening Store would silently route a test double's embedded-but-not- overridden methods to it. Handlers type-assert for Pager, falling back to unbounded List* + in-Go windowing when a store doesn't implement it; PG always does.

type PartitionDigest added in v0.8.6

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

PartitionDigest is the streaming fold of a closed partition, in seq order:

d_0 = sha256(Header)            d_i = sha256(d_{i-1} || row_hash_i)

every value lowercase hex text. It holds one 64-byte state however many rows are added, so a partition larger than Postgres's 1 GB field limit digests in constant memory.

func NewPartitionDigest added in v0.8.6

func NewPartitionDigest(m PartitionManifest) *PartitionDigest

NewPartitionDigest starts the fold at the manifest's header.

func (*PartitionDigest) Add added in v0.8.6

func (p *PartitionDigest) Add(rowHash string)

Add folds one row's hash (the stored row_hash; a row from before the chain began folds the hash audit_row_hash(NULL, ...) would have given it) into the digest.

func (*PartitionDigest) Sum added in v0.8.6

func (p *PartitionDigest) Sum() string

Sum is the digest so far, as 64 hex characters.

type PartitionManifest added in v0.8.6

type PartitionManifest struct {
	Partition    string `json:"partition"`
	Rows         int64  `json:"row_count"`
	SeqLo        int64  `json:"seq_lo"`
	SeqHi        int64  `json:"seq_hi"`
	RecordedLoUS int64  `json:"recorded_lo_us"`
	RecordedHiUS int64  `json:"recorded_hi_us"`
}

PartitionManifest is what a partition's digest binds besides its rows: its name, the range of seq and recorded_at it actually holds (recorded_at as integer microseconds since the epoch, the unit audit_row_hash uses) and its row count. An empty partition has Rows 0 and every range zero.

Header is the canonical text the digest starts from. It is the migration 0119 definition, which audit_partition_digest(p_partition) implements in the database: the two are pinned together by a test.

func (PartitionManifest) Header added in v0.8.6

func (m PartitionManifest) Header() string

Header is the JSON array of strings the fold starts from, in jsonb's own text form (a comma and a space between elements), so a manifest the database and Go each render is byte-identical. A range of an empty partition renders as an empty string.

type PauseCandidate added in v0.8.0

type PauseCandidate struct {
	Run types.AgentRun
	// OpenRequest: the run has a PENDING request inside its wait (openHoldSQL).
	OpenRequest bool
	// WaitingRequest: one of those counts toward a waiting pause
	// (waitingHoldSQL).
	WaitingRequest bool
}

PauseCandidate is one live run as the pause sweep sees it.

type PauseSettle added in v0.8.6

type PauseSettle struct {
	RunID uuid.UUID
	DueAt time.Time
}

PauseSettle is one run a pause compensation may have left with its agent container not matching its pause mark (migration 0132).

type PauseSettler added in v0.8.6

type PauseSettler interface {
	// NotePauseSettle records run id as owing a settle from after on (the
	// database's clock), moving a due_at already recorded only forward, and
	// returns the due_at it holds.
	NotePauseSettle(ctx context.Context, id uuid.UUID, after time.Duration) (time.Time, error)
	// DuePauseSettles lists the runs whose settle is due.
	DuePauseSettles(ctx context.Context) ([]PauseSettle, error)
	// ClearPauseSettle deletes run id's record only while it still reads due,
	// so a later note survives.
	ClearPauseSettle(ctx context.Context, id uuid.UUID, due time.Time) error
}

PauseSettler is the durable record of the runs whose container may not match their pause mark. Optional like RunPauser; production is always PG.

type PendingGroupRemoval added in v0.8.6

type PendingGroupRemoval struct {
	GroupID    uuid.UUID
	IdentityID uuid.UUID
}

PendingGroupRemoval is one person's removal from one group with ledger rows still pending.

type PendingLeaver added in v0.8.6

type PendingLeaver struct {
	ID     uuid.UUID
	Purged bool
}

PendingLeaver is an identity with ledger rows still pending: a suspension its identity provider stopped retrying, or a purge begun and not finished.

type PeopleDirectoryFilter added in v0.8.6

type PeopleDirectoryFilter struct {
	// Query keeps people whose principal starts with it (case-sensitive) or whose email starts
	// with it (case-insensitive). Empty keeps everyone.
	Query string
	// State is "active" (not deactivated), "deactivated", or "" for both.
	State string
	// After is the last principal of the previous page; empty starts at the top.
	After string
	Limit int
}

PeopleDirectoryFilter narrows and pages ListPeopleDirectory.

type PeopleDirectoryPage added in v0.8.6

type PeopleDirectoryPage struct {
	People []PersonListing
	Next   string
}

PeopleDirectoryPage is one page; Next is the principal to pass as After, or "" on the last page.

type PeopleDirectoryStore added in v0.8.6

type PeopleDirectoryStore interface {
	// ListPeopleDirectory returns one page of the directory, ordered by principal.
	ListPeopleDirectory(ctx context.Context, f PeopleDirectoryFilter) (PeopleDirectoryPage, error)
}

PeopleDirectoryStore lists the people a deployment knows: every identity that has signed in (principal_identities, migration 0113) and every person an admin set up beforehand (people, migration 0090). Optional, like PersonStore.

type PersonErasureStore added in v0.8.6

type PersonErasureStore interface {
	// RunIDsCreatedBy lists every run created_by createdBy.
	RunIDsCreatedBy(ctx context.Context, createdBy string) ([]uuid.UUID, error)
	// BlankRunTasks empties the task text of every run created_by createdBy and
	// returns how many it changed. It never touches updated_at: the idle reaper
	// reads it.
	BlankRunTasks(ctx context.Context, createdBy string) (int, error)
	// PrincipalForName maps a name an event carries (a principal, an
	// entra:<tenant>:<object> form, an email the person signed in with) to the
	// person's principal. A name that resolves to none, or to more than one, is
	// returned as it is.
	PrincipalForName(ctx context.Context, name string) (string, error)
	// PrincipalAliases lists the names the identity directory knows principal
	// by: the emails it signed in with (lowercased) and its entra:<tenant>:<object>
	// form. A row sealed under a name the directory did not yet know is under
	// that name's own key, so an erasure destroys these too.
	PrincipalAliases(ctx context.Context, principal string) ([]string, error)
	// EraseGovernanceChangePersonalFields clears the proposer and decider (and the decider's reason) of
	// every governance change recorded under one of names (a principal, or an email matched
	// case-folded) and moves a pending change whose proposer is one of them to expired. It returns the
	// rows it touched.
	EraseGovernanceChangePersonalFields(ctx context.Context, names []string) (int, error)
	// SubjectKeyDestroyedSince reports whether any generation of owner's key
	// for purpose has a destroyed_at after since.
	SubjectKeyDestroyedSince(ctx context.Context, owner, purpose string, since time.Time) (bool, error)
}

PersonErasureStore is what erasing a person reads and writes beyond the stores that own a scope: the person's runs, and the identity directory that maps the names an audit row carries onto one principal. Optional like PrincipalIdentityStore: callers type-assert and refuse a scope the store cannot serve rather than report it erased.

type PersonListing added in v0.8.6

type PersonListing struct {
	Principal string
	Email     string
	// Entra is true for a person keyed by Entra tenant and object id.
	Entra      bool
	PreCreated bool
	// FirstSignInAt and LastSignInAt are nil for a person who has never signed in.
	FirstSignInAt *time.Time
	LastSignInAt  *time.Time
	DeactivatedAt *time.Time
	// ActiveSessions is 0 or 1, see PeopleDirectoryActiveSessionWindow.
	ActiveSessions int
	APITokens      int
	SSHKeys        int
	Credentials    int
	ActiveRuns     int
	// Groups, GroupsTruncated and GroupsVerified are the person's last verified login groups
	// (key_domain_login_groups, stamped at every SSO sign-in). GroupsVerified is false when no
	// snapshot exists; GroupsTruncated is the login's own bit that its groups claim was cut short.
	Groups          []string
	GroupsTruncated bool
	GroupsVerified  bool
}

PersonListing is one directory row.

type PersonStore added in v0.8.0

type PersonStore interface {
	// CreatePerson inserts p, or returns the row already stored under
	// p.Principal with created=false. A different principal holding the same
	// email (case-insensitively) is ErrConflict.
	CreatePerson(ctx context.Context, p types.Person) (out types.Person, created bool, err error)
	// GetPerson is the row stored under exactly principal, or ErrNotFound.
	GetPerson(ctx context.Context, principal string) (types.Person, error)
	// ListPeople returns every row, oldest first.
	ListPeople(ctx context.Context) ([]types.Person, error)
	// MarkPersonSignedIn stamps first_signed_in_at on principal's row the first
	// time they sign in. No row is not an error: most people were never
	// pre-created.
	MarkPersonSignedIn(ctx context.Context, principal string, now time.Time) error
}

PersonStore holds the identities an admin created or confirmed before their first sign-in (migration 0090, #1157; the Entra object-id key, 0099, #1195). Optional, like DeviceStore: the API answers 501 on a store without it.

type PresetWrite added in v0.8.0

type PresetWrite string

PresetWrite is what PutLaunchPreset did: created the row, replaced it (and moved its version), or found it already identical and wrote nothing.

const (
	PresetCreated   PresetWrite = "created"
	PresetUpdated   PresetWrite = "updated"
	PresetUnchanged PresetWrite = "unchanged"
)

type PrincipalIdentity added in v0.8.6

type PrincipalIdentity struct {
	ID         uuid.UUID
	Principal  string
	Issuer     string
	TenantID   string
	ObjectID   string
	EmailLower string
	// ScimExternalID and ScimUserName are the SCIM projection, not binding columns.
	ScimExternalID string
	ScimUserName   string
	DeactivatedAt  *time.Time
	AuthorityEpoch int64
	PurgeAfter     *time.Time
	PurgedAt       *time.Time
	CreatedAt      time.Time
	LastLoginAt    *time.Time
}

PrincipalIdentity is one human identity Wardyn has seen sign in. An Entra identity is (Issuer, TenantID, ObjectID); any other issuer has an empty TenantID and ObjectID and is keyed by (Issuer, Principal). Principal is "" on a row written before the person's first sign-in.

type PrincipalIdentityStore added in v0.8.6

type PrincipalIdentityStore interface {
	// UpsertLoginIdentity records an approved sign-in: it creates the identity row, or finds the
	// one for the same (issuer, tenant, object id) on Entra and (issuer, principal) elsewhere, binds
	// its principal if it has none, stamps the sign-in time and keeps the email as an alias. A row's
	// principal, issuer, tenant and object id are never changed once bound; a sign-in that would
	// change one writes nothing: ErrIdentityBindingMismatch when its row is bound to another
	// principal, ErrConflict when it names no row at all.
	UpsertLoginIdentity(ctx context.Context, in LoginIdentity, now time.Time) (PrincipalIdentity, error)
	// GetIdentityByObject is the row for exactly (issuer, tenantID, objectID), or ErrNotFound. An
	// empty objectID is always ErrNotFound.
	GetIdentityByObject(ctx context.Context, issuer, tenantID, objectID string) (PrincipalIdentity, error)
	// IdentitiesByPrincipal is every row bound to principal, oldest first (one per issuer it has
	// signed in on); empty when it has none.
	IdentitiesByPrincipal(ctx context.Context, principal string) ([]PrincipalIdentity, error)
}

PrincipalIdentityStore is optional, like PersonStore: it is not part of Store, so callers type-assert and treat its absence as "nothing recorded".

type PrincipalPrefStore added in v0.8.0

type PrincipalPrefStore interface {
	// GetPrincipalPref returns principal's value for key, or ErrNotFound.
	GetPrincipalPref(ctx context.Context, principal, key string) (json.RawMessage, error)
	// PutPrincipalPref upserts principal's value for key.
	PutPrincipalPref(ctx context.Context, principal, key string, value json.RawMessage) error
}

PrincipalPrefStore persists per-person preferences. Like RunLayoutStore it is not part of Store, so the api test doubles need not grow it; the api type-asserts and treats its absence as "nothing remembered".

type PushPathListStore added in v0.8.0

type PushPathListStore interface {
	// RecordPushPathList stores approvalID's list once, for runID. A second
	// call for the same approval stores nothing and reports false; a list
	// past the run's limit returns ErrPushPathListCap. Count and insert are
	// one step, so concurrent calls for a run serialize.
	RecordPushPathList(ctx context.Context, approvalID, runID uuid.UUID, l types.PushPathList, limit int) (bool, error)
	// GetPushPathList returns approvalID's list, or ErrNotFound.
	GetPushPathList(ctx context.Context, approvalID uuid.UUID) (types.PushPathList, error)
	// CountPushPathLists counts the lists stored for runID's approvals.
	CountPushPathLists(ctx context.Context, runID uuid.UUID) (int, error)
}

PushPathListStore keeps each push_content approval's types.PushPathList. Like PrincipalPrefStore it is not part of Store, so the api test doubles need not grow it; the api type-asserts it.

type Querier added in v0.8.6

type Querier interface {
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

Querier is the statement surface a store write needs: satisfied by *pgxpool.Pool (one statement, its own implicit transaction) and by pgx.Tx (a statement inside a caller's transaction). A write written against Querier runs the same SQL either way, which is what lets a held governance change apply inside the decision transaction.

A write that relies on a transaction-scoped lock (an advisory xact lock) must be handed a pgx.Tx: on a pool the lock is released as soon as its own statement ends.

type Recorder

type Recorder struct {
	Pool *pgxpool.Pool
}

Recorder wraps a pool and implements audit.Recorder via InsertAuditEvent.

func (Recorder) Record

func (rec Recorder) Record(ctx context.Context, ev types.AuditEvent) error

Record appends ev to the append-only audit_events table. The chain hash InsertAuditEvent fills in is dropped here since ev is taken by value; a caller that wants the head hash calls InsertAuditEvent directly with &ev.

type RunAuditMatcher added in v0.8.0

type RunAuditMatcher interface {
	HasRunAuditEvent(ctx context.Context, runID uuid.UUID, f AuditFilter) (bool, error)
}

RunAuditMatcher answers "does this run have ANY audit row matching f?" as an EXISTS, so the answer does not depend on how many other rows the run has or where the match sits in its trail — which a bounded, oldest-first QueryAuditEvents window cannot promise. A capability interface for the reason Pager gives.

type RunCapacity added in v0.8.6

type RunCapacity struct {
	States             map[string]int               `json:"states"`
	Paused             int                          `json:"paused"`
	Kept               int                          `json:"kept"`
	Totals             RunCapacityRunner            `json:"totals"`
	AgeBuckets         []RunCapacityAge             `json:"age_buckets"`
	ByRunner           map[string]RunCapacityRunner `json:"by_runner"`
	ByOwner            []RunCapacityOwner           `json:"by_owner"`
	ByOwnerTruncated   bool                         `json:"by_owner_truncated"`
	Unschedulable      []RunCapacityUnschedulable   `json:"unschedulable"`
	UnschedulableTotal int                          `json:"unschedulable_total"`
	// OldestActiveSeconds is the age since created_at of the oldest holding run (0 when none).
	OldestActiveSeconds int64 `json:"oldest_active_seconds"`
}

RunCapacity is the fleet's configured reservations, computed from the non-terminal rows.

type RunCapacityAge added in v0.8.6

type RunCapacityAge struct {
	Bucket string `json:"bucket"`
	Count  int    `json:"count"`
}

RunCapacityAge is one age bucket (since created_at) of holding runs.

type RunCapacityOpts added in v0.8.6

type RunCapacityOpts struct {
	Now                 time.Time
	CurrentKind         string
	UnschedulableReason func(detail string) string
}

RunCapacityOpts parameterises RunCapacity. CurrentKind is the deployment's runner kind; Totals carries only that kind's entry; other kinds appear only under ByRunner. UnschedulableReason maps a status_detail to its capacity-blocker reason, or "" when it is not one.

type RunCapacityOwner added in v0.8.6

type RunCapacityOwner struct {
	Owner   string                     `json:"owner"`
	Holding int                        `json:"holding"`
	Runners map[string]RunCapacitySums `json:"by_runner"`
}

RunCapacityOwner is one owner's holding runs, summed per runner kind (never across kinds). Ordering uses the deployment's current kind.

type RunCapacityRunner added in v0.8.6

type RunCapacityRunner struct {
	Basis string `json:"basis"`
	RunCapacitySums
}

RunCapacityRunner is one runner kind's sums. Basis says what they mean: Kubernetes sums requests, Docker has no reservation and reports caps.

type RunCapacitySums added in v0.8.6

type RunCapacitySums struct {
	Holding               int   `json:"holding"`
	Unknown               int   `json:"unknown"`
	AgentCPURequestMillis int64 `json:"agent_cpu_request_millis"`
	AgentCPULimitMillis   int64 `json:"agent_cpu_limit_millis"`
	AgentMemoryRequestMiB int64 `json:"agent_memory_request_mib"`
	AgentMemoryLimitMiB   int64 `json:"agent_memory_limit_mib"`
	ProxyCPUMillis        int64 `json:"proxy_cpu_millis"`
	ProxyMemoryMiB        int64 `json:"proxy_memory_mib"`
	// ProxyCPUUncapped counts recorded rows whose proxy had no CPU cap (Docker).
	ProxyCPUUncapped int   `json:"proxy_cpu_uncapped"`
	HeldCPUMillis    int64 `json:"held_cpu_millis"`
	HeldMemoryMiB    int64 `json:"held_memory_mib"`
}

RunCapacitySums is one set of configured-reservation sums. Holding counts the rows that hold capacity; Unknown counts the holding rows whose reservation was never recorded (runner_kind NULL), which add to no sum. A run's held figure is its agent request plus its proxy.

type RunCapacityUnschedulable added in v0.8.6

type RunCapacityUnschedulable struct {
	ID                    uuid.UUID `json:"id"`
	Owner                 string    `json:"owner"`
	WaitedSeconds         int64     `json:"waited_seconds"`
	Reason                string    `json:"reason"`
	RunnerKind            *string   `json:"runner_kind"`
	AgentCPURequestMillis *int64    `json:"agent_cpu_request_millis"`
	AgentMemoryRequestMiB *int64    `json:"agent_memory_request_mib"`
	ProxyCPUMillis        *int64    `json:"proxy_cpu_millis"`
	ProxyMemoryMiB        *int64    `json:"proxy_memory_mib"`
}

RunCapacityUnschedulable is one STARTING run waiting for room, with its recorded reservation (nil where unrecorded).

type RunFilter added in v0.8.0

type RunFilter struct {
	// Owner exact-matches created_by; empty = every creator. The api layer
	// resolves ?owner=me|all before calling down.
	Owner string
	// Statuses is a subset of "active"/"ended"/"failed"/"killed", OR'd
	// together; empty = no filter. "needs" is rejected at the api layer.
	Statuses []string
	// EndedWithin bounds ended rows' age; <= 0 = unbounded. Live rows are
	// never windowed by this.
	EndedWithin time.Duration
	// IncludeKilled disables killedVisibleFor's default hide.
	IncludeKilled bool
	// Workspace exact-matches COALESCE(NULLIF(repo,”), workspace_path).
	Workspace string
	// Query ILIKE-searches title/task/repo/created_by. Raw — the store
	// escapes '%', '_' and '\' itself.
	Query string
}

RunFilter narrows ListRunsFiltered/CountHiddenRuns to the landing page's opt-in contract. Zero value matches every row (killed rows still subject to killedVisibleFor).

type RunLayoutStore added in v0.5.0

type RunLayoutStore interface {
	// GetRunLayout returns ErrNotFound when nothing has been saved yet; the
	// api layer treats that as the default shape, not an error.
	GetRunLayout(ctx context.Context, principal, preset string) (types.RunLayout, error)
	// PutRunLayout upserts principal's layout for preset and returns the
	// stored row, including the server-assigned updated_at.
	PutRunLayout(ctx context.Context, principal, preset string, layout []types.RunLayoutWidget) (types.RunLayout, error)
}

RunLayoutStore is the run-cockpit widget-layout persistence surface. Deliberately NOT part of the Store interface: ~30 test doubles embed store.Store, and widening it would break every one for a feature they have nothing to do with. The api layer type-asserts s.cfg.Store to RunLayoutStore and degrades when absent (GET returns the default shape, PUT 501s); production is always PG, which has it.

type RunLeaser added in v0.8.0

type RunLeaser interface {
	// ListLeasedRuns returns every RUNNING run that has an end or is kept.
	ListLeasedRuns(ctx context.Context) ([]types.AgentRun, error)
	// MarkRunEnded marks run id kept-and-ended at now, only while still
	// RUNNING, not already kept, and its end is at or before now; clears a
	// pause. The conditional UPDATE is the cross-replica mutual exclusion:
	// exactly one caller sees true and runs the end.
	MarkRunEnded(ctx context.Context, id uuid.UUID, now time.Time) (bool, error)
	// MarkRunEndingSoon records that the thresholdSec-before-endsAt warning
	// went out, reporting whether this call recorded it: false when that
	// warning (or a closer one) already went for this end, or the end changed.
	MarkRunEndingSoon(ctx context.Context, id uuid.UUID, endsAt time.Time, thresholdSec int) (bool, error)
	// SetRunEndAndWait moves run id's end/wait from (fromEnd, fromWait) to
	// (toEnd, toWait), only while the run still has those values, limits
	// fromLimits, and isn't terminal. A run lost to a reboot/outage may still
	// move its end — extending it is how it becomes revivable again. A run
	// kept by its OWN end (LostEnded) moves only with ended set, exactly as
	// EndedKept describes. false means the run changed since the read (end,
	// wait, or a re-clamped profile); nil ends mean "no end". Moving the end
	// clears end_tightened_at.
	SetRunEndAndWait(ctx context.Context, id uuid.UUID, fromLimits types.RunLimits, fromEnd *time.Time, fromWait int, toEnd *time.Time, toWait int, ended *EndedKept) (bool, error)
	// StopKeptRunIf makes a kept run (RUNNING with a lost/end mark) terminal,
	// only while the row still carries the EXACT kept mark read (lostAt,
	// lostReason, endsAt), checked atomically with the state guard in one
	// UPDATE. Closes the same TOCTOU class UpdateRunStateIfIdle closes for the
	// idle reaper: a stale lease-sweep row must never win the destructive
	// RUNNING->terminal transition and tear down a run it no longer describes.
	// false means the mark or state changed; the caller's re-assert next sweep
	// pass makes dropping that safe.
	StopKeptRunIf(ctx context.Context, id uuid.UUID, to types.RunState, lostAt *time.Time, lostReason types.LostReason, endsAt *time.Time) (bool, error)
	// SetRunContainmentError records that a kept run's stop (proxy or agent)
	// failed with msg, only while RUNNING and kept. Message refreshes on every
	// call; containment_error_at keeps the first failure's time.
	SetRunContainmentError(ctx context.Context, id uuid.UUID, msg string, now time.Time) error
	// ClearRunContainmentError clears it once the stop is confirmed; true only
	// for the call that cleared a recorded error, so resolution is audited
	// once across replicas.
	ClearRunContainmentError(ctx context.Context, id uuid.UUID) (bool, error)
}

RunLeaser is the lease surface. Optional like RunWatcherLeaser and for the same reason: the ~30 test doubles that embed store.Store would route these calls to a nil interface. The api layer type-asserts and runs no lease sweep when a store lacks it; production is always PG.

type RunLimitsReclamper added in v0.8.0

type RunLimitsReclamper interface {
	// ListProfiledLiveRuns returns every non-terminal run that captured a
	// governance profile at create and is not kept by its own end (LostEnded).
	// A run lost to a reboot or a control-plane outage is still profiled-live:
	// tightening its limits must still land, the same way extending its end
	// still does (F1, long-holds design rev 4 §2.3).
	ListProfiledLiveRuns(ctx context.Context) ([]types.AgentRun, error)
	// ReclampRunLimits writes run id's tightened limits, end and wait, but only
	// while the run still has the limits, end and wait in from (the run as the
	// caller read it), is not terminal and is not kept by its own end
	// (LostEnded). A non-nil endTightenedAt is stamped as end_tightened_at.
	// false means the run changed since it was read; the next sweep reads it
	// again.
	ReclampRunLimits(ctx context.Context, from types.AgentRun, toLimits types.RunLimits, toEnd *time.Time, toWait int, endTightenedAt *time.Time) (bool, error)
}

RunLimitsReclamper is the re-clamp surface. Optional like RunLeaser and for the same reason; the api layer runs no re-clamp when a store lacks it.

type RunLoser added in v0.8.0

type RunLoser interface {
	// StampRunTokenRenewed records id's token renewal at the database's clock.
	// false means the run is already kept (lost/ended), so the renew must be
	// refused — a lost run's identity must never carry forward.
	StampRunTokenRenewed(ctx context.Context, id uuid.UUID) (bool, error)
	// ListLapsedTokenRuns returns every RUNNING, not-kept run whose token was
	// last renewed more than life ago, by the database's clock.
	ListLapsedTokenRuns(ctx context.Context, life time.Duration) ([]types.AgentRun, error)
	// MarkRunLost marks id kept and lost for reason at now, only while RUNNING
	// and not already kept; with tokenLife>0 it re-requires the token still
	// lapsed by that much, so a race with a renew resolves to the renew.
	// Exactly one caller sees true. It clears a pause only for reboot — the
	// one reason whose container is truly gone — since an outage's agent
	// keeps running (frozen or not) and clearing here would make resume skip
	// a thaw a still-frozen agent still needs.
	MarkRunLost(ctx context.Context, id uuid.UUID, reason types.LostReason, now time.Time, tokenLife time.Duration) (bool, error)
}

RunLoser is the lost-run surface, kept optional because test doubles that embed store.Store would otherwise route these calls to a nil interface; the api layer type-asserts, and production is always PG.

type RunOutput added in v0.8.6

type RunOutput struct {
	RunID      uuid.UUID
	Output     []byte
	Truncated  bool
	Incomplete bool
	CaptureGap bool
	Source     string // "stdout" or "pane_snapshot"
	MaskScope  string // "run", "globals_only" or ""
	CapturedAt *time.Time
	ClaimedAt  time.Time
}

RunOutput is one row of run_outputs. CapturedAt is nil while the row is pending (a capture is owed); MaskScope is "" when the row carries none.

type RunOutputChunkStore added in v0.8.6

type RunOutputChunkStore interface {
	// AppendRunOutputChunk adds b, which the caller's masker has already passed, as runID's next
	// chunk, then deletes the oldest chunks until what is left is the fewest that still hold
	// keep bytes. It reports false, and writes nothing, once the run's final row exists: a
	// writer that was slower than the finisher never recreates what the final row replaced.
	AppendRunOutputChunk(ctx context.Context, runID uuid.UUID, replica string, b []byte, keep int) (bool, error)
	// ReadRunOutputChunks returns the last limit bytes of runID's chunks.
	ReadRunOutputChunks(ctx context.Context, runID uuid.UUID, limit int) (RunOutputChunks, error)
	// SaveChunkedRunOutput writes runID's row from its chunks, as an incomplete capture gap (what
	// the replica that wrote them had not flushed is lost), and deletes them, in one transaction.
	// It reports false, writing nothing, when the run has no chunks or already has a final row.
	SaveChunkedRunOutput(ctx context.Context, runID uuid.UUID, maskScope string, limit int) (bool, error)
}

RunOutputChunkStore is the live-chunk record. Optional like RunOutputStore: the api layer type-asserts, and a store without it keeps the tail in the writing process only. Every method checks the run's erasure tombstone in its own transaction and answers ErrRunOutputErased.

type RunOutputChunks added in v0.8.6

type RunOutputChunks struct {
	Bytes     []byte
	Truncated bool
	Found     bool
}

RunOutputChunks is a run's live tail as stored: the chunks' bytes in order, ending at the newest. Found is false when no chunk is held. Truncated says bytes older than Bytes were printed: chunks were trimmed, or the read cut the front.

type RunOutputStore added in v0.8.6

type RunOutputStore interface {
	// InsertPendingRunOutput records that a capture is owed for runID. A row
	// that already exists is left as it is.
	InsertPendingRunOutput(ctx context.Context, runID uuid.UUID) error
	// RefreshRunOutputClaim stamps a pending row's claimed_at: the final write
	// is being retried, so the sweeper must not resolve it yet.
	RefreshRunOutputClaim(ctx context.Context, runID uuid.UUID) error
	// SaveFinalRunOutput writes the run's final row (output, flags and
	// mask_scope as given), replacing a pending or capture-gap row. The
	// database stamps captured_at.
	SaveFinalRunOutput(ctx context.Context, o RunOutput) error
	// SaveGapRunOutput writes a capture-gap row with no bytes, only over a
	// pending row or none: a final row already committed is never overwritten.
	// It reports whether it wrote.
	SaveGapRunOutput(ctx context.Context, runID uuid.UUID) (bool, error)
	// MarkRunOutputIncomplete sets incomplete on runID's final row, reporting
	// whether a final row was there to mark.
	MarkRunOutputIncomplete(ctx context.Context, runID uuid.UUID) (bool, error)
	// GetRunOutput returns runID's row, pending or final. found is false when
	// there is none.
	GetRunOutput(ctx context.Context, runID uuid.UUID) (o RunOutput, found bool, err error)
	// EraseRunOutputs writes a tombstone for each run and deletes its rows, in
	// one transaction: all of it or none of it.
	EraseRunOutputs(ctx context.Context, runIDs []uuid.UUID) error
	// DeleteRunOutputsOlderThan deletes final rows captured longer ago than age
	// (the database's clock), returning how many went.
	DeleteRunOutputsOlderThan(ctx context.Context, age time.Duration) (int, error)
	// ListStalePendingRunOutputs returns the runs whose pending row was claimed
	// longer ago than age and which are terminal, oldest first, at most limit.
	ListStalePendingRunOutputs(ctx context.Context, age time.Duration, limit int) ([]uuid.UUID, error)
}

RunOutputStore is the run-output record. Optional like RunWatcherLeaser: the ~30 test doubles that embed Store would route these to a nil interface, so the api layer type-asserts and keeps output in memory only when a store lacks it. Production is always PG. Every method but the sweeper's two checks the run's erasure tombstone in its own transaction and answers ErrRunOutputErased.

type RunPAT added in v0.8.2

type RunPAT struct {
	RunID           uuid.UUID
	AuthorizationID uuid.UUID
	Owner           string // whose token: the run's creator
	ProviderRowID   string // the provider row the run was dispatched on
	Org             string // the Azure DevOps organisation the token is scoped to
	Scope           string // the space-joined scope string it was created with
	ValidTo         time.Time
	CreatedAt       time.Time  // stamped by the store on insert; ignored on the way in
	RevokedAt       *time.Time // nil while Wardyn believes the token is live
	RevokeReason    string
	LastError       string // why the last revoke attempt failed; "" otherwise
}

RunPAT is one row of ado_run_pats.

type RunPATFilter added in v0.8.2

type RunPATFilter struct {
	RunID         uuid.UUID
	Owner         string
	ProviderRowID string
}

RunPATFilter narrows ListUnrevokedRunPATs; a zero field matches everything.

type RunPATReader added in v0.8.2

type RunPATReader interface {
	// ListRunPATs returns every token recorded for runID, revoked or not,
	// oldest first. Empty, never nil, when there is none.
	ListRunPATs(ctx context.Context, runID uuid.UUID) ([]RunPAT, error)
	// LastRunPAT returns the newest token recorded for owner on
	// providerRowID; found=false when there is none.
	LastRunPAT(ctx context.Context, owner, providerRowID string) (p RunPAT, found bool, err error)
}

RunPATReader reads the run-token record for display. Optional like RunPATStore: a store without it has no tokens to show.

type RunPATStore added in v0.8.2

type RunPATStore interface {
	// InsertRunPAT records a token BEFORE its first use. ErrConflict when that
	// (run, authorization id) is already recorded. CreatedAt is the store's.
	InsertRunPAT(ctx context.Context, p RunPAT) error
	// MarkRunPATRevoked closes a token's record: it was revoked, or its valid_to
	// has passed. reason is required (one of api's adoPATRevoke* values). It
	// reports whether it closed the row: false when the row is unknown or
	// already closed, so two revokers race harmlessly. lastError, if non-empty,
	// keeps why the last revoke attempt failed.
	MarkRunPATRevoked(ctx context.Context, runID, authorizationID uuid.UUID, reason, lastError string) (bool, error)
	// NoteRunPATRevokeFailed records why a revoke attempt failed and leaves the
	// row OPEN, so the sweep retries and the setup check can list a token Wardyn
	// could not revoke while its valid_to is ahead. A closed or unknown row is
	// left as it is.
	NoteRunPATRevokeFailed(ctx context.Context, runID, authorizationID uuid.UUID, lastError string) error
	// ListUnrevokedRunPATs returns the tokens still recorded live that match f,
	// oldest first.
	ListUnrevokedRunPATs(ctx context.Context, f RunPATFilter) ([]RunPAT, error)
}

RunPATStore is the run-token record. Optional like RunPauser: it is not part of Store because the test doubles that embed Store would route these to a nil interface; the api layer type-asserts. Production is always PG; MemRunPATs is the same contract without a database. Unlike RunPauser, a caller that finds no RunPATStore must REFUSE TO MINT: a token is never created unrecorded.

type RunPauser added in v0.8.0

type RunPauser interface {
	// ListPauseCandidates returns every RUNNING, not-kept run with a sandbox,
	// paused or not, and the store's own clock read with them.
	ListPauseCandidates(ctx context.Context) ([]PauseCandidate, time.Time, error)
	// StampRunActive moves run id's presence clock to now, and reports whether
	// the run is paused. A terminal or missing run is not stamped (false, nil).
	StampRunActive(ctx context.Context, id uuid.UUID) (paused bool, err error)
	// MarkRunPaused marks run id paused for reason, but only while it is still
	// RUNNING, not kept, not paused, its presence clock still reads activeAt
	// (the snapshot the caller judged idle; nil is "never stamped"), and — for
	// a waiting pause — it still has a request the waiting pause counts, or —
	// for an idle pause — it has no open request. The conditional UPDATE is
	// what makes a keystroke, or a request closing or opening, between the
	// caller's read and this write win: false means leave the run running.
	MarkRunPaused(ctx context.Context, id uuid.UUID, reason types.PauseReason, activeAt *time.Time) (bool, error)
	// ClearRunPaused clears run id's pause, and reports whether it was paused.
	ClearRunPaused(ctx context.Context, id uuid.UUID) (bool, error)
	// RunHasOpenRequest reports whether run id has a PENDING request inside
	// its wait.
	RunHasOpenRequest(ctx context.Context, id uuid.UUID) (bool, error)
}

RunPauser is the pause surface. Optional like RunLeaser and for the same reason; the api layer runs no pause sweep, and stamps no presence, when a store lacks it. Production is always PG.

type RunProxyConfigs added in v0.8.0

type RunProxyConfigs interface {
	// PutRunProxyConfig writes run id's sealed config, replacing any.
	PutRunProxyConfig(ctx context.Context, id uuid.UUID, sealed []byte) error
	// GetRunProxyConfig reads it; ErrNotFound when there is none.
	GetRunProxyConfig(ctx context.Context, id uuid.UUID) ([]byte, error)
	// DeleteRunProxyConfig removes it. Idempotent.
	DeleteRunProxyConfig(ctx context.Context, id uuid.UUID) error
	// PurgeTerminalRunProxyConfigs removes every row whose run is terminal,
	// the backstop for a delete that failed, and returns how many it removed.
	PurgeTerminalRunProxyConfigs(ctx context.Context) (int64, error)
}

RunProxyConfigs stores each run's sealed proxy config. The caller seals and opens it; the store never sees it in the clear. Optional for RunLeaser's reason: the api layer type-asserts, and production is always PG.

type RunProxyRelease added in v0.8.0

type RunProxyRelease struct {
	RunID      uuid.UUID        `json:"run_id"`
	CreatedBy  string           `json:"created_by"`
	State      types.RunState   `json:"state"`
	LostReason types.LostReason `json:"lost_reason,omitempty"`
	Release    string           `json:"proxy_release"`
}

RunProxyRelease is one row of the version-window listing.

type RunReviver added in v0.8.0

type RunReviver interface {
	// MarkRunRevived claims run id for a new proxy: while it is RUNNING and still as the revive read
	// it, live (from "") or lost to an outage or a reboot (from that reason), it clears the lost
	// mark (and any unresolved containment error — the old proxy it was about is replaced), stamps
	// the token as just renewed (the revive mints a fresh one, and the lapsed-token sweep must not
	// read the old stamp), and refreshes the watcher lease (a stopped agent starts only after the
	// new proxy, and the watcher sweep must not probe it before then).
	//
	// A run kept by its own end (from LostEnded) is claimed only with ended set: its exact mark,
	// its files grace still live, and its end after ended.Now. It also clears a pause ONLY when
	// from is a reboot: that revive restarts the agent container (docker start), so a stale pause
	// mark would 409 the run's files/resources reads over an agent that's actually running. A live
	// restart (from "") or an outage revive never touch the agent's own process, frozen or not, so
	// their pause mark, if any, must survive for the thaw an eventual resume still needs to
	// perform (a lease end already cleared the pause of a run kept by its own end).
	//
	// false means the run went terminal, ended, was lost or revived since, or its grace or end ran
	// out (an outage-kept run's end too, as revive admission requires) — it gets no proxy.
	//
	// startsAgent is a revive that starts the run's stopped agent (a reboot, an end, or an
	// outage whose agent was stopped). With limit > 0 its claim takes a slot under the
	// deployment cap like a create does: under CreateRunUnderCap's lock,
	// ErrRunCapReached when every OTHER run that holds a sandbox already reaches limit,
	// the run left as it was. The row's own count is not trusted for this: an outage-kept
	// run whose end was moved later reads as holding a sandbox though its agent is
	// stopped. A revive that starts no agent (a live restart, an outage revive whose agent
	// still runs) adds nothing and is never refused.
	MarkRunRevived(ctx context.Context, id uuid.UUID, from types.LostReason, ended *EndedKept, limit int, startsAgent bool) (bool, error)
	// SetRunProxyRelease records release as the one that started run id's
	// proxy, once a revive's new proxy runs.
	SetRunProxyRelease(ctx context.Context, id uuid.UUID, release string) error
	// ListRunProxyReleases returns every non-terminal run that has a sandbox,
	// with the release that started its proxy ("" before migration 0084).
	ListRunProxyReleases(ctx context.Context) ([]RunProxyRelease, error)
}

RunReviver is the revive surface. Optional for RunLeaser's reason: the test doubles that embed store.Store would route these calls to a nil interface. The api layer type-asserts; production is always PG.

type RunSizing added in v0.8.6

type RunSizing struct {
	RunnerKind            string
	AgentCPURequestMillis int64
	AgentCPULimitMillis   int64
	AgentMemoryRequestMiB int64
	AgentMemoryLimitMiB   int64
	ProxyCPUMillis        *int64
	ProxyMemoryMiB        int64
}

RunSizing is the configured reservation recorded at dispatch (migration 0117). A nil ProxyCPUMillis means the proxy had no CPU cap.

type RunTitler added in v0.8.0

type RunTitler interface {
	// SetRunTitle overwrites run id's title unconditionally — a rename is
	// allowed in any run state (design.md §3.4), unlike the lease's end/wait,
	// so there is no state guard and no compare-and-swap here.
	SetRunTitle(ctx context.Context, id uuid.UUID, title string) error
}

RunTitler is the run-rename surface (#1197 L2, design.md §3.4). Optional like RunLeaser and for the same reason: the ~30 hand-written test doubles that implement Store directly would otherwise need a mechanical update for a route most of them never exercise. The api layer type-asserts; production is always PG.

type RunWatcherLeaser added in v0.5.0

type RunWatcherLeaser interface {
	ClaimStaleRunWatchers(ctx context.Context, owner string, staleAfter time.Duration) ([]types.AgentRun, error)
	HeartbeatRunWatcher(ctx context.Context, id uuid.UUID, owner string) error
	// RunWatcherFresh reports whether id's lease heartbeat is younger than
	// staleAfter (a live process owns it). The undispatched-run reaper checks
	// this so it never false-fails a RUNNING run whose sandbox_ref write was
	// lost but whose watcher still holds the lease. Missing row = not fresh (reap it).
	RunWatcherFresh(ctx context.Context, id uuid.UUID, staleAfter time.Duration) (bool, error)
}

RunWatcherLeaser is deliberately not part of Store (like Pager): ~30 test doubles embed store.Store overriding only a few methods, so widening Store would route lease writes to the embedded nil interface inside the watcher goroutine, whose recover() would swallow the panic and kill it silently. The api layer type-asserts and skips leasing when a store lacks this; PG always has it.

type RunsByCreatorPager added in v0.5.0

type RunsByCreatorPager interface {
	ListRunsPageByCreator(ctx context.Context, createdBy string, p Page) ([]types.AgentRun, error)
}

RunsByCreatorPager is the ownership-scoped analogue of Pager.ListRunsPage: a member's GET /runs is scoped to created_by = the caller. TRUST BOUNDARY: unlike Pager, an absent implementation must fail closed (500), not fall back to the unscoped list — an unscoped list IS the vulnerability here.

type RunsFilteredPager added in v0.8.0

type RunsFilteredPager interface {
	ListRunsFiltered(ctx context.Context, f RunFilter, p Page) ([]types.AgentRun, error)
	// CountHiddenRuns answers the X-Wardyn-Hidden-* headers: rows baseWhere
	// matches but ended_within excludes (olderHidden), plus rows
	// killedVisibleFor additionally excludes (killedHidden) — one query.
	CountHiddenRuns(ctx context.Context, f RunFilter) (olderHidden, killedHidden int, err error)
}

RunsFilteredPager is the landing-page scoping surface for GET /runs. Kept out of Pager/Store: widening either would silently route a test double's embedded method to the wrong (unscoped) behaviour. An absent implementation fails CLOSED at the api-layer call site, same as RunsByCreatorPager — a silent unscoped fallback would leak every run to a caller the filter was meant to narrow.

type SSHKeysByPrincipalPager added in v0.8.0

type SSHKeysByPrincipalPager interface {
	ListSSHKeysByPrincipalPage(ctx context.Context, principal string, p Page) ([]types.SSHPublicKey, error)
}

SSHKeysByPrincipalPager is the scoped analogue of Pager for GET /me/ssh-keys, same safe-fallback posture as GrantsByRunPager (ListSSHKeysByPrincipal is already WHERE principal=$1).

type ScimGroup added in v0.8.6

type ScimGroup struct {
	ID          uuid.UUID
	ExternalID  string
	DisplayName string
	CreatedAt   time.Time
}

ScimGroup is a group the identity provider reported. ExternalID is the provider's id for it.

type ScimGroupStore added in v0.8.6

type ScimGroupStore interface {
	CreateScimGroup(ctx context.Context, externalID, displayName string, now time.Time) (ScimGroup, error)
	GetScimGroup(ctx context.Context, id uuid.UUID) (ScimGroup, error)
	SearchScimGroups(ctx context.Context, attr, value string, limit int) ([]ScimGroup, error)
	RenameScimGroup(ctx context.Context, id uuid.UUID, displayName string) error
	ScimGroupMembers(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error)
	// AddScimGroupMembers records identities as members, skipping any id that names no identity.
	AddScimGroupMembers(ctx context.Context, id uuid.UUID, identities []uuid.UUID, now time.Time) error
	StartGroupRemoval(ctx context.Context, groupID, identityID uuid.UUID) error
	ResumeGroupRemoval(ctx context.Context, groupID, identityID uuid.UUID) (bool, error)
	GroupRemovalIdentities(ctx context.Context, groupID uuid.UUID) ([]uuid.UUID, error)
	DeleteScimGroup(ctx context.Context, id uuid.UUID) error
	PendingGroupRemovals(ctx context.Context, idleFor time.Duration, limit int) ([]PendingGroupRemoval, error)
	GroupRemovalFailures(ctx context.Context, limit int) ([]DeprovisionFailure, error)
}

ScimGroupStore is optional, like LeaverStore: callers type-assert it.

type Store

type Store interface {
	// AgentRun.
	CreateRun(ctx context.Context, r types.AgentRun) (types.AgentRun, error)
	GetRun(ctx context.Context, id uuid.UUID) (types.AgentRun, error)
	ListRuns(ctx context.Context) ([]types.AgentRun, error)
	// CountActiveRunsBy counts one creator's non-terminal runs — the governance quota's read
	// (GovernanceLimits.MaxConcurrentRuns). Called only when an assigned profile sets a cap.
	CountActiveRunsBy(ctx context.Context, createdBy string) (int, error)
	UpdateRunStateIf(ctx context.Context, id uuid.UUID, fromState, toState types.RunState) (bool, error)
	UpdateRunStateIfIdle(ctx context.Context, id uuid.UUID, fromState, toState types.RunState, notAfter time.Time) (bool, error)
	SetSandboxRef(ctx context.Context, id uuid.UUID, ref string) error
	SetRunImage(ctx context.Context, id uuid.UUID, image string) error
	SetRunDiskMiB(ctx context.Context, id uuid.UUID, mib int) error
	SetRunAgentExecID(ctx context.Context, id uuid.UUID, execID string) error
	TouchRun(ctx context.Context, id uuid.UUID) error

	// RunPolicy.
	CreatePolicy(ctx context.Context, p types.RunPolicy) (types.RunPolicy, error)
	GetPolicy(ctx context.Context, id uuid.UUID) (types.RunPolicy, error)
	ListPolicies(ctx context.Context) ([]types.RunPolicy, error)
	UpdatePolicy(ctx context.Context, id uuid.UUID, name string, spec types.RunPolicySpec) (types.RunPolicy, error)
	DeletePolicy(ctx context.Context, id uuid.UUID) error

	// Workspace.
	CreateWorkspace(ctx context.Context, ws types.Workspace) (types.Workspace, error)
	GetWorkspace(ctx context.Context, id uuid.UUID) (types.Workspace, error)
	ListWorkspaces(ctx context.Context) ([]types.Workspace, error)
	// UpdateWorkspace writes the full column set, CARRYING ws.EgressEditedAt rather than stamping
	// it, or ReconcileWorkspaceEgressDecisions could re-widen a list the operator just cleared.
	// stampEgressEdit lets the ONE caller that clears the list say so, stamped by the DATABASE like
	// every other writer of that column.
	UpdateWorkspace(ctx context.Context, id uuid.UUID, ws types.Workspace, stampEgressEdit bool) (types.Workspace, error)
	// SetWorkspaceApprovedEgress replaces the approved-egress list and stamps EgressEditedAt: the
	// documented undo for an `always` decision, stopping the boot heal from re-adding a removed host.
	SetWorkspaceApprovedEgress(ctx context.Context, id uuid.UUID, domains []string) (types.Workspace, error)
	// AddWorkspaceEgressDecision records one `always`-scoped decision: on allow, host moves into
	// approved_egress (capped, deduped) and out of denied_egress; on deny the mirror. ErrConflict
	// (not ErrNotFound) when the cap refuses the write. Does NOT stamp EgressEditedAt — a decision
	// isn't an operator override of itself.
	AddWorkspaceEgressDecision(ctx context.Context, id uuid.UUID, host string, allow bool, maxApprovedEgress int) (types.Workspace, error)
	// SetWorkspaceDeniedEgress mirrors SetWorkspaceApprovedEgress for denied-egress: pass the FULL
	// desired list, replacing rather than merging.
	SetWorkspaceDeniedEgress(ctx context.Context, id uuid.UUID, domains []string) (types.Workspace, error)
	SetWorkspaceLLMCred(ctx context.Context, id uuid.UUID, cred *types.WorkspaceLLMCred) (types.Workspace, error)
	// SetWorkspaceOwner replaces ONLY owned_by (offboarding: reassign a departed member's workspace
	// to the operator via owner ""). Scoped and separate from UpdateWorkspace so no ordinary edit
	// can move ownership or race a concurrent async scan.
	SetWorkspaceOwner(ctx context.Context, id uuid.UUID, owner string) (types.Workspace, error)
	SetWorkspaceRequirements(ctx context.Context, id uuid.UUID, reqs map[string]types.WorkspaceRequirement) (types.Workspace, error)
	SetWorkspaceRecordResult(ctx context.Context, id uuid.UUID, taskKey string, result json.RawMessage, onlyIfStatus string) (types.Workspace, bool, error)
	ClaimWorkspaceActiveRun(ctx context.Context, id, runID uuid.UUID, expected *uuid.UUID) (types.Workspace, bool, error)
	ClearWorkspaceActiveRun(ctx context.Context, id, runID uuid.UUID) (bool, error)
	SetWorkspaceBuiltImage(ctx context.Context, id uuid.UUID, imageRef, builtHash string) (types.Workspace, error)
	// SetWorkspaceImportState advances the scan pipeline. FENCED: applies only while the import-step
	// slot still holds expectedActive; applied=false means the caller must re-read, not retry blindly.
	SetWorkspaceImportState(ctx context.Context, id uuid.UUID, status types.WorkspaceStatus, activeRunID *uuid.UUID, expectedActive *uuid.UUID) (types.Workspace, bool, error)
	// MergeWorkspaceRequirements ADDS overlay rows atomically (jsonb ||), safe against concurrent
	// edits the full-replace SetWorkspaceRequirements would race. ErrConflict at the key cap.
	MergeWorkspaceRequirements(ctx context.Context, id uuid.UUID, add map[string]types.WorkspaceRequirement) (types.Workspace, error)
	DeleteWorkspace(ctx context.Context, id uuid.UUID) error

	// Source library (tier 1) — a repo/dir configured once, attached to many workspaces. Upsert
	// dedupes on (kind, locator, ref).
	UpsertSource(ctx context.Context, src types.Source) (types.Source, error)
	GetSource(ctx context.Context, id uuid.UUID) (types.Source, error)
	GetSourcesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]types.Source, error)
	ListSources(ctx context.Context) ([]types.Source, error)
	UpdateSourceConfig(ctx context.Context, id uuid.UUID, name string, reqs map[string]types.WorkspaceRequirement) (types.Source, error)
	WorkspacesAttaching(ctx context.Context, id uuid.UUID) ([]string, error)
	DeleteSource(ctx context.Context, id uuid.UUID, detach bool) error
	ClaimSourceActiveRun(ctx context.Context, id, runID uuid.UUID) error
	ClearSourceActiveRun(ctx context.Context, id, runID uuid.UUID) error
	SetSourceScanResult(ctx context.Context, id uuid.UUID, profile []byte, status types.WorkspaceStatus, runID uuid.UUID, seed map[string]types.WorkspaceRequirement) (types.Source, error)
	SetSourceScanResultUnfenced(ctx context.Context, id uuid.UUID, profile []byte, status types.WorkspaceStatus, seed map[string]types.WorkspaceRequirement) (types.Source, error)

	// Base-image catalog (tier 2). Upsert dedupes on (kind, image, steps); "recommended" is
	// structurally excluded (CHECK) — it is a per-workspace derived build, never a catalog row.
	UpsertBaseImage(ctx context.Context, b types.BaseImageEntry) (types.BaseImageEntry, error)
	// UpdateBaseImageName renames a catalog row — the operator-editable counterpart to
	// UpdateSourceConfig above, deliberately NOT folded into UpsertBaseImage's identity-hit dedupe.
	UpdateBaseImageName(ctx context.Context, id uuid.UUID, name string) (types.BaseImageEntry, error)
	ListBaseImages(ctx context.Context) ([]types.BaseImageEntry, error)
	WorkspacesUsingBaseImage(ctx context.Context, id uuid.UUID) ([]string, error)
	DeleteBaseImage(ctx context.Context, id uuid.UUID, detach bool) error

	// CredentialGrant.
	CreateGrant(ctx context.Context, g types.CredentialGrant) (types.CredentialGrant, error)
	ListGrantsByRun(ctx context.Context, runID uuid.UUID) ([]types.CredentialGrant, error)

	// ApprovalRequest.
	CreateApproval(ctx context.Context, a types.ApprovalRequest) (types.ApprovalRequest, error)
	GetApproval(ctx context.Context, id uuid.UUID) (types.ApprovalRequest, error)
	ListApprovals(ctx context.Context, stateFilter types.ApprovalState) ([]types.ApprovalRequest, error)
	// DecideApproval transitions state from PENDING to decision.State; returns
	// ErrAlreadyDecided if the approval is not PENDING. See types.ApprovalDecision.
	DecideApproval(ctx context.Context, id uuid.UUID, decision types.ApprovalDecision) (types.ApprovalRequest, error)

	// AuditEvent.
	QueryAuditEvents(ctx context.Context, runID uuid.UUID, limit int) ([]types.AuditEvent, error)
	QueryRecentAuditEvents(ctx context.Context, limit int) ([]types.AuditEvent, error)
	LatestAuditEventByAction(ctx context.Context, action string) (types.AuditEvent, error)

	// SiteConfig.
	GetSiteConfig(ctx context.Context) (types.SiteConfig, error)
	PutSiteConfig(ctx context.Context, cfg types.SiteConfig) (types.SiteConfig, error)

	// Sandbox ref -> substrate routing (the orchestrator's RefStore seam; see
	// store_sandbox_ref.go). GetRef reports a missing row as found=false, nil.
	PutRef(ctx context.Context, ref, substrateName string) error
	GetRef(ctx context.Context, ref string) (substrateName string, found bool, err error)
	DeleteRef(ctx context.Context, ref string) error

	// Short-lived cross-process handoff row (see store_ephemeral.go): single-use WS attach tickets.
	// Consume-once, and the consumer is a single DELETE ... RETURNING, so two racing redemptions —
	// on one control plane or two — can only have one win.
	MintAttachTicket(ctx context.Context, token string, t AttachTicket, now, expiresAt time.Time) error
	ConsumeAttachTicket(ctx context.Context, token string, now time.Time) (AttachTicket, bool, error)

	// SSH gateway key registry (migration 0033, self-service via /api/v1/me/ssh-keys). AddSSHKey
	// returns ErrConflict on a taken fingerprint (the PK; one key maps to one owner). DeleteSSHKey is
	// scoped to principal (someone else's key is ErrNotFound, no existence leak). GetSSHKeyByFingerprint
	// is the gateway's unscoped auth-time lookup. RefreshSSHKeyRoles re-stamps role+role_checked_at
	// (migration 0046) from the OIDC OnLogin hook, bounding the admin-override stamp's staleness.
	AddSSHKey(ctx context.Context, k types.SSHPublicKey) (types.SSHPublicKey, error)
	ListSSHKeysByPrincipal(ctx context.Context, principal string) ([]types.SSHPublicKey, error)
	GetSSHKeyByFingerprint(ctx context.Context, fingerprint string) (types.SSHPublicKey, error)
	DeleteSSHKey(ctx context.Context, fingerprint, principal string) error
	// DeleteSSHKeys removes one principal's keys, or all keys when principal is empty.
	DeleteSSHKeys(ctx context.Context, principal string) (int, error)
	RefreshSSHKeyRoles(ctx context.Context, principal, role string, checkedAt time.Time) error
	// RefreshAPITokenIdentity re-stamps role, user type and the group snapshot (plus its completeness
	// bit) on every unrevoked api_tokens row a principal holds, from the same OnLogin hook as
	// RefreshSSHKeyRoles — neither credential had a way to learn about a demotion or group change.
	RefreshAPITokenIdentity(ctx context.Context, principal, role, userType string, groups []string, truncated bool) error

	// Per-user API tokens (migration 0045). SECURITY: GetAPITokenByRaw is the
	// third auth branch on every authenticated route.
	//
	// CreateAPIToken/GetAPITokenByRaw hash the plaintext token internally — the raw value never
	// reaches SQL. GetAPITokenByRaw returns ErrNotFound for unknown, mismatched, revoked AND expired
	// tokens alike, so it's not an existence oracle. RevokeAPIToken is principal-scoped when non-empty
	// (self-service; someone else's id is ErrNotFound) and revokes ANY token when empty (admin).
	// TouchAPIToken is best effort — its error must never fail a request.
	CreateAPIToken(ctx context.Context, t types.APIToken, raw string) (types.APIToken, error)
	GetAPITokenByRaw(ctx context.Context, raw string) (types.APIToken, error)
	TouchAPIToken(ctx context.Context, id uuid.UUID, now time.Time) error
	ListAPITokensByPrincipal(ctx context.Context, principal string) ([]types.APIToken, error)
	ListAPITokens(ctx context.Context) ([]types.APIToken, error)
	RevokeAPIToken(ctx context.Context, id uuid.UUID, principal string, now time.Time) (types.APIToken, error)

	// Capability grants and the per-kind enforcement switch (migration 0042).
	// SECURITY: the resolver runs on the request path; the fail mode is a
	// fail-open authz gate hiding in an untyped test double.
	UpsertCapabilityGrant(ctx context.Context, g types.CapabilityGrant) (types.CapabilityGrant, error)
	DeleteCapabilityGrant(ctx context.Context, id uuid.UUID) error
	ListCapabilityGrants(ctx context.Context) ([]types.CapabilityGrant, error)
	// ListGroupDenyGrants returns the group-subject DENY rows of one kind, the only rows the
	// unresolvable-group-deny refusal can match — separate from ListCapabilityGrants so that
	// request-path check doesn't cost scale with table size.
	ListGroupDenyGrants(ctx context.Context, capability string) ([]types.CapabilityGrant, error)
	// ListCapabilityGrantsFor returns the grants that could apply to one caller: `all` rows plus
	// `user` rows naming any of users (sub AND email) plus `group` rows naming any of groups plus
	// `user_type` rows naming userType. Not filtered by capability — see the implementation.
	ListCapabilityGrantsFor(ctx context.Context, users, groups []string, userType string) ([]types.CapabilityGrant, error)
	// GetCapabilityEnforcement returns the sparse per-kind switch map; an absent key means NOT
	// enforced, the zero-config back-compat default.
	GetCapabilityEnforcement(ctx context.Context) (map[string]bool, error)
	// PutCapabilityEnforcement replaces the WHOLE map (a capability the caller omits loses its row)
	// and returns the stored result.
	PutCapabilityEnforcement(ctx context.Context, enabled map[string]bool) (map[string]bool, error)
	// ListCapabilityRestrictions returns the restricted values ("Available to: Only...", migration
	// 0081) as kind -> set of values; an absent value is not restricted. Never nil.
	ListCapabilityRestrictions(ctx context.Context) (map[string]map[string]bool, error)
	// SetCapabilityRestriction turns one value's restriction on or off (idempotent either way).
	SetCapabilityRestriction(ctx context.Context, capability, value string, restricted bool, by string) error

	// Console-managed role mappings (migration 0051): the store half of internal/auth/oidc's
	// RoleMappingSource, read once per login and merged with the chart's WARDYN_OIDC_ROLE_MAP.
	// SECURITY: on the OIDC login path; the fail mode is silently falling back
	// to env-only, WIDENING who derives admin under WARDYN_OIDC_DEFAULT_ROLE=admin.
	//
	// UpsertRoleMapping keys on the natural UNIQUE (value): re-adding an already-mapped value flips
	// its role in place, returning the EXISTING row's id on a conflict (never the candidate's).
	UpsertRoleMapping(ctx context.Context, m types.RoleMapping) (types.RoleMapping, error)
	DeleteRoleMapping(ctx context.Context, id uuid.UUID) error
	// ListRoleMappings returns every row, oldest first — the console's People
	// screen and the OIDC login-time merge's whole data need.
	ListRoleMappings(ctx context.Context) ([]types.RoleMapping, error)

	// User types (migration 0071_user_types). CreateUserType and
	// UpdateUserType return ErrConflict on a taken id or name; DeleteUserType
	// returns ErrConflict while the type is built in or still named by a
	// subject row (UserTypeReferences counts those).
	ListUserTypes(ctx context.Context) ([]types.UserType, error)
	GetUserType(ctx context.Context, id string) (types.UserType, error)
	CreateUserType(ctx context.Context, t types.UserType) (types.UserType, error)
	UpdateUserType(ctx context.Context, t types.UserType) (types.UserType, error)
	// UpdateUserTypeMetadata is UpdateUserType without the priority column.
	UpdateUserTypeMetadata(ctx context.Context, t types.UserType) (types.UserType, error)
	UserTypeReferences(ctx context.Context, id string) (int, error)
	// UserTypeTokenStamps counts the unrevoked API tokens stamped with the type.
	UserTypeTokenStamps(ctx context.Context, id string) (int, error)
	DeleteUserType(ctx context.Context, id string) error

	// Launch presets (migration 0087, store_launch_presets.go). PutLaunchPreset
	// is an upsert by name that moves the version only when the row changes.
	ListLaunchPresets(ctx context.Context) ([]types.LaunchPreset, error)
	GetLaunchPreset(ctx context.Context, name string) (types.LaunchPreset, error)
	PutLaunchPreset(ctx context.Context, p types.LaunchPreset) (types.LaunchPreset, PresetWrite, error)
	DeleteLaunchPreset(ctx context.Context, name string) (types.LaunchPreset, error)

	// Governance profiles and their subject assignments (migration 0052): the assignable ceiling
	// that replaces Config.DefaultPolicy for a principal an admin has named.
	// SECURITY: on the run-creation request path; the fail mode is falling
	// back to the deployment-wide ceiling, past the floor an admin set.
	//
	// UpsertGovernanceProfile keys on the PRIMARY KEY (a fresh id inserts, an existing one updates
	// in place, rename included) and returns ErrConflict when UNIQUE(name) rejects the write.
	// DeleteGovernanceProfile returns ErrConflict when the profile is still ASSIGNED — the ON DELETE
	// RESTRICT, so deleting a profile can never silently widen its members.
	UpsertGovernanceProfile(ctx context.Context, p types.GovernanceProfile) (types.GovernanceProfile, error)
	// WriteGovernanceProfile is the graph-aware write: one transaction under the profile-graph
	// lock, where build decides the stored row from every profile as that transaction reads them
	// (migration 0125: a profile may be composed on another). DeleteGovernanceProfile also
	// returns *ErrProfileHasChildren when another profile composes on the row.
	WriteGovernanceProfile(ctx context.Context, id uuid.UUID, build GovernanceProfileBuild) (types.GovernanceProfile, error)
	DeleteGovernanceProfile(ctx context.Context, id uuid.UUID) error
	// ListGovernanceProfiles and GetGovernanceProfileChain return RAW rows: a composed row
	// carries no authority of its own. Only internal/api/governance_compose.go calls them for
	// authority (a source guard fails any other caller); GetGovernanceProfileChain is one
	// statement, leaf first, bounded at depth 3, and ErrNotFound when the leaf is gone.
	ListGovernanceProfiles(ctx context.Context) ([]types.GovernanceProfile, error)
	GetGovernanceProfileChain(ctx context.Context, id uuid.UUID) ([]types.GovernanceProfile, error)
	// UpsertGovernanceAssignment keys on the natural UNIQUE (subject_type,
	// subject): re-assigning a subject REPOINTS its one row, returning the
	// EXISTING row's id on a conflict. ErrNotFound when profile_id names no
	// profile (the FK rejects it).
	UpsertGovernanceAssignment(ctx context.Context, a types.GovernanceAssignment) (types.GovernanceAssignment, error)
	DeleteGovernanceAssignment(ctx context.Context, id uuid.UUID) error
	ListGovernanceAssignments(ctx context.Context) ([]types.GovernanceAssignment, error)
	// Governance changes (migration 0126): a governance write held for a second human. Part of
	// this interface, not an optional seam, because a store without them would let a covered write
	// through unreviewed. See governance_changes.go for each one's contract.
	ProposeGovernanceChange(ctx context.Context, ch types.GovernanceChange, ttl time.Duration) (types.GovernanceChange, []ExpiredGovernanceChange, error)
	ListGovernanceChanges(ctx context.Context, state string) ([]types.GovernanceChange, error)
	GetGovernanceChange(ctx context.Context, id uuid.UUID) (types.GovernanceChange, error)
	DecideGovernanceChange(ctx context.Context, id uuid.UUID, d GovernanceDecision, fn GovernanceDecideFunc) (types.GovernanceChange, error)
	// DryRunGovernance runs fn on a transaction that is always rolled back.
	DryRunGovernance(ctx context.Context, fn func(q Querier) error) error
	// ResolveGovernanceProfile returns THE ONE profile that applies to a caller — user > group >
	// user_type > all, sub over email within the user tier, then priority DESC and name ASC — as a
	// single indexed read whose ORDER BY is the whole precedence rule. ErrNotFound means "no
	// assignment matched", read as the deployment ceiling. userSubjects must arrive in the caller's
	// own precedence order since the query matches by POSITION.
	//
	// Also returns WHICH TIER matched, load-bearing not informational: on a
	// stale/truncated group snapshot the caller must tell a user-tier winner
	// (serve it) from an all-tier one (refuse), indistinguishable from the
	// profile alone. HasGroupTierAssignments below is that separate read.
	ResolveGovernanceProfile(ctx context.Context, userSubjects, groups []string, userType string) (*types.GovernanceProfile, types.CapabilitySubjectType, error)
	// HasGroupTierAssignments gates the stale/truncated group-snapshot refusal:
	// with no group-tier row there is nothing an unknown group could have
	// matched, so a nil or truncated snapshot must fall through rather than
	// refuse (see the implementation).
	HasGroupTierAssignments(ctx context.Context) (bool, error)

	// User drives and their subject grants (migration 0054): admin-registered per-user storage a
	// member may mount into a run, and the rows allocating one to a user, group, or everyone.
	// SECURITY: same reason as the governance resolver above.
	//
	// UpsertUserDrive keys on the PRIMARY KEY (fresh id inserts, existing updates in place, rename
	// included) and returns ErrConflict when UNIQUE(name) rejects the write. DeleteUserDrive returns
	// ErrConflict while the drive is still ALLOCATED (ON DELETE RESTRICT).
	//
	// refuseIfAllocated is a PRECONDITION: the write applies only while the
	// drive has no allocations (ErrDriveAllocated otherwise) — the API's
	// re-home guard re-asserts, inside the writing statement, the read
	// decision that permitted the write.
	UpsertUserDrive(ctx context.Context, d types.UserDrive, refuseIfAllocated bool) (types.UserDrive, error)
	GetUserDrive(ctx context.Context, id uuid.UUID) (types.UserDrive, error)
	DeleteUserDrive(ctx context.Context, id uuid.UUID) error
	// ListUserDrives returns every drive by name WITH its grant count — what makes the console's
	// delete affordance honest, since a drive with grants answers 409.
	ListUserDrives(ctx context.Context) ([]types.UserDriveListItem, error)
	// UpsertUserDriveGrant keys on the natural UNIQUE (subject_type, subject): re-allocating a
	// subject REPOINTS its one row, returning the EXISTING row's id on a conflict. ErrNotFound when
	// drive_id names no drive (FK). ErrConflict when ANOTHER subject already holds this drive with
	// the same home_override — a directory name is one person's, and on a managed drive an override
	// is the last way left to point two people at one object Wardyn creates.
	//
	// homeOverrideStated is the REQUEST's tri-state, not the row's: false means the caller never
	// mentioned home_override, and a repoint that never mentioned it must not CLEAR one (see the
	// guard in PG's statement) — that same ErrConflict, distinguished from the uniqueness one purely
	// by this argument (a stated override disables the re-home guard; an unstated one is empty).
	UpsertUserDriveGrant(ctx context.Context, g types.UserDriveGrant, homeOverrideStated bool) (types.UserDriveGrant, error)
	// DeleteUserDriveGrant RETURNS the row it removed (ErrNotFound when none
	// matched): the delete's own audit row has to name the subject that was
	// de-allocated, and by then it is gone.
	DeleteUserDriveGrant(ctx context.Context, id uuid.UUID) (types.UserDriveGrant, error)
	ListUserDriveGrants(ctx context.Context) ([]types.UserDriveGrant, error)
	// ResolveUserDrive mirrors ResolveGovernanceProfile's precedence and
	// stale-snapshot reasoning. DISABLED grants stay IN the query: one that
	// wins its tier comes back Enabled false, rendered PAUSED rather than
	// falling through (a pause must never widen a member onto an unchosen
	// drive). Returns the winning GRANT beside the drive since every override
	// lives on the binding row. ErrNotFound means "no drive".
	ResolveUserDrive(ctx context.Context, userSubjects, groups []string, userType string) (
		*types.UserDrive, *types.UserDriveGrant, types.CapabilitySubjectType, error)
	// HasGroupTierDriveGrants gates the stale/truncated group-snapshot refusal:
	// with no group-tier row there is nothing an unknown group could have
	// matched, so a nil or truncated snapshot must fall through rather than
	// refuse (see the implementation).
	HasGroupTierDriveGrants(ctx context.Context) (bool, error)

	// Ping proves the store is actually reachable, not just constructed — the
	// /readyz readiness probe's one call. A live TCP connect with no working
	// query would otherwise read as healthy forever.
	Ping(ctx context.Context) error
}

Store is the abstract persistence seam the control plane talks to. The default Postgres implementation is PG (query bodies in store.go); a future pure-Go SQLite backend satisfies this same interface unchanged.

Out of scope on purpose: transactional surfaces (broker mint FOR UPDATE, identity revocation) need a real transaction and stay on the pool directly.

SECURITY: several method groups below are deliberately part of this interface, not a type-asserted optional seam, because they run on a request-path authz decision — a test double that doesn't implement them would be a compile error instead of a silent fail-open degrade. Each such group is marked SECURITY below with its specific failure mode.

type SuspendPlan added in v0.8.6

type SuspendPlan struct {
	IdentityID uuid.UUID
	// Principal is the principal IdentityID's row bound when the plan was made ("" for none). A row that
	// binds another under the suspension's lock is ErrIdentityRebound, and nothing is written.
	Principal string
	// Principals are the principals whose identity rows are deactivated with IdentityID's and whose
	// people rows get deactivated_at.
	Principals []string
	// CutoffSubs are every sub and email form a session cutoff is written for.
	CutoffSubs []string
	// PurgeAfter schedules the automatic purge that long after the deactivation. Zero schedules
	// none; an earlier schedule is kept.
	PurgeAfter time.Duration
	// PendingJobs are added as pending ledger rows in the same transaction, so a crash after the
	// commit still leaves the rest of the suspension for the sweeper. Existing rows are kept.
	PendingJobs []JobKey
}

SuspendPlan is what step 1 of a suspension writes, all in one transaction.

type SuspendResult added in v0.8.6

type SuspendResult struct {
	WasActive bool
	Epoch     int64
}

SuspendResult is step 1's outcome. WasActive says the identity was active before it, so this was a new suspension and not the repair of an old one.

type WorkspacesByOwnerPager added in v0.6.0

type WorkspacesByOwnerPager interface {
	ListWorkspacesPageForOwner(ctx context.Context, owner string, p Page) ([]types.Workspace, error)
}

WorkspacesByOwnerPager is the ownership-scoped analogue of Pager.ListWorkspacesPage: a member's GET /workspaces sees their own owned rows plus the operator-owned ones (owned_by = ”), never another member's. Unlike RunsByCreatorPager, an absent implementation is not fail-closed: the api-layer fallback applies the same owned_by filter in Go before windowing, so scoping still holds.

Jump to

Keyboard shortcuts

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