store

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 24 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).

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.

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.

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.

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

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.

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

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

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 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 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 INSERT: this caller's seq allocation and head read must not interleave with another writer's, keeping seq order and chain order identical. The DB trigger takes the same lock, binding writers this package knows nothing about; the lock here is re-entrant and free.

Types

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

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

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

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

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

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

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) 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) 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) 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) 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) 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` is in the WHERE, not checked separately — a revoked, 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) 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) 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) 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.

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) 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) 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) 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) 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) InsertRunPAT added in v0.8.2

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

InsertRunPAT — see RunPATStore.

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) 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) 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) 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) 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) 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) 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) 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) 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) 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) (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) 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) 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) 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) 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.

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

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) 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) 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) 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) RunHasOpenRequest added in v0.8.0

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

RunHasOpenRequest — see RunPauser.

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) 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) 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) 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) 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) 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) 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, 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 — ON DELETE RESTRICT makes delete-and-recreate impossible for an assigned profile). One statement serves both write routes: POST always inserts (fresh id), PUT always updates (id from the path).

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.

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.

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.

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 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 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 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 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 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 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 — it gets no proxy.
	MarkRunRevived(ctx context.Context, id uuid.UUID, from types.LostReason, ended *EndedKept) (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 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 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 AND revoked 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)
	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)
	DeleteGovernanceProfile(ctx context.Context, id uuid.UUID) error
	ListGovernanceProfiles(ctx context.Context) ([]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)
	// 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 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