types

package
v0.8.6 Latest Latest
Warning

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

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

Documentation

Overview

Governance profiles (migration 0052): the wire + store types for an ASSIGNABLE ceiling and the row that binds one to a subject.

Named `governance*` throughout since "profile" already means two other things in this tree (Recording-Mode synthesis, workspace scan profile), and grep ambiguity here is a containment-bug risk.

The rule for what a GROUP SUBJECT is, kept here since every write boundary already reaches this package.

A group subject matches by exact equality against the printable-ASCII login-time snapshot, guarded BEFORE the Unicode fold; anything outside that set can never match anyone — a dead deny the console still renders active, or a phantom group tier.

Lives here rather than internal/auth/oidc (which states the same rule for the match surface) so pkg/client and cmd/wardyn don't need the OIDC/JOSE stack; TestCanonicalGroupSubjectHasOneAnswer pins the two copies together.

Package types defines Wardyn's core domain vocabulary — AgentRun, RunPolicy, CredentialGrant, ApprovalRequest, and the audit event shape — shared by the control plane, runners, sidecars, and the CRD layer.

User drives (migration 0054): the wire + store types for an admin-registered per-user storage allocation and the row that grants one to a subject.

Named `drive` throughout: a DRIVE is what an admin registers and allocates, MOUNT is what a run does with one. The member's run request carries only `{enabled, read_only}` and NEVER a path — the server derives the home name.

Two kinds fall out of the backend rather than being stored beside it: MANAGED (Wardyn allocates the object) and SHARE (the platform already has the tree and Wardyn binds one per-user subdirectory of it).

Everything here is a CLOSED enum validated at the write boundary, pinned by migration 0054's three CHECKs against internal/db's TestClosedEnumChecksMatchConstants.

Home-segment rules: what a per-person home name may look like per substrate (a Docker volume-name component; a Kubernetes DNS-1123 subdomain), and how each is worded to users.

Index

Constants

View Source
const (
	BrandNamePrefix = "prefix"
	BrandNameSuffix = "suffix"
)

Branding name formats: "<Company> Wardyn" and "Wardyn for <Company>".

View Source
const (
	PATAccessRead  = "read"
	PATAccessWrite = "write"

	PATForgeGeneric         = "generic"
	PATForgeGitLab          = "gitlab"
	PATForgeBitbucketServer = "bitbucket_server"
	PATForgeGitea           = "gitea"
)

Values of a git_pat scope's access and forge axes.

View Source
const (
	GovernanceChangePending  = "pending"
	GovernanceChangeApplied  = "applied"
	GovernanceChangeRejected = "rejected"
	GovernanceChangeExpired  = "expired"
	GovernanceChangeStale    = "stale"
)

Governance change states (migration 0126's CHECK).

View Source
const (
	GovernanceTargetProfile    = "governance_profile"
	GovernanceTargetAssignment = "governance_assignment"

	GovernanceTargetCapabilityGrant        = "capability_grant"
	GovernanceTargetCapabilityEnforcement  = "capability_enforcement"
	GovernanceTargetCapabilityAvailability = "capability_availability"
	GovernanceTargetUserTypePriority       = "user_type_priority"
	GovernanceTargetRoleMapping            = "role_mapping"
	GovernanceTargetKeyDomainAssignment    = "key_domain_assignment"
)

The target kinds a governance change can hold (governance_changes.target_kind has no CHECK: the closed set is the table in internal/api, and a later lane adds a kind with no DDL).

View Source
const (
	// HoldWindowEgress is how long an egress_domain wait_for_review request
	// holds the sandbox from when it was raised (proxy's defaultHoldTimeout).
	// This under-reports a policy authored above 30s (first_use_hold_seconds,
	// up to maxHoldTimeout) — ported verbatim from the TS rule it replaces;
	// fixing the under-report is a separate proxy change (the proxy would
	// need to stamp its own resolved hold_seconds into the raise scope),
	// not this one.
	HoldWindowEgress = 30 * time.Second
	// HoldWindowADOCapability is how long an Azure DevOps capability
	// escalation (a tool_call with grant_id set) holds the sandbox (proxy's
	// maxCapabilityHoldTimeout).
	HoldWindowADOCapability = 240 * time.Second
	// HoldWindowPush is the proxy's absolute ceiling on a push_content hold
	// (maxHoldTimeout) — the conservative bound isHeld used when no client
	// surface reads a run's own resolved push_rules.hold_seconds.
	HoldWindowPush = 600 * time.Second
)

The three hold windows approval.Hold ports from ui/src/app/lib/types/ approvals.ts's now-retired isHeld — see that function's own history (#725/ F1, #509, #181) for why each window is what it is. Exported here, the one package both internal/approval and internal/egress/proxy already import, so the proxy's own defaultHoldTimeout/maxHoldTimeout/maxCapabilityHoldTimeout point at these instead of carrying a second copy of the same numbers (#1197).

View Source
const (
	// MintConflictPending — the approval is still open; approval_id is set and
	// the caller may poll.
	MintConflictPending = "pending"
	// MintConflictDenied — a human denied the approval; denied/reason are set.
	MintConflictDenied = "denied"
	// MintConflictScopeMismatch — the requested scope widens the grant.
	MintConflictScopeMismatch = "scope_mismatch"
	// MintConflictAlreadyMinted — the grant's single-use slot is already spent.
	MintConflictAlreadyMinted = "already_minted"
)

Mint 409-conflict "code" values: the machine-readable discriminator between the four distinct fail-closed conditions that all share HTTP 409 on POST /wardyn/v1/credentials/mint. THIS IS THE ONE HOME: internal/api's mint handler and cmd/wardyn-git-helper's client both read these, and internal/api's tests assert the LITERAL strings (a wire contract).

View Source
const (
	AzureRouteAnthropic = "anthropic"
	AzureRouteOpenAIV1  = "openai_v1"
)

The two inference routes an azure_foundry row serves. One row serves one route, and a route names the one Entra audience its sign-in is captured for.

View Source
const (
	PushPathListMaxPaths = 10_000
	PushPathListMaxBytes = 1 << 20
)

The bounds of a PushPathList: at most PushPathListMaxPaths paths and PushPathListMaxBytes bytes of path text, whichever cuts the list first.

View Source
const (
	PushActsAsGitHubApp = "github_app"
	PushActsAsGitPAT    = "git_pat"
	PushActsAsADOEntra  = "ado_entra"
)

The PushContentScope.ActsAsKind values: which lane's credential a held push authenticates with, so the console can word the card.

View Source
const (
	// The four AI kinds (anthropic_api_key, anthropic_subscription, bedrock,
	// openai_api_key) are GONE as of 0.8: model access is a model provider, and
	// migration 0100 converted and deleted every stored row of them.
	IntegrationKindGitHubApp = "github_app"
	IntegrationKindGitHost   = "git_host"
)

Integration kinds. An integration is a BASE COMPONENT extended by kind: a connection (secrets + egress), never an installer. The closed set below is every kind with bespoke behavior; any other kind is a GENERIC open slug carrying its own secrets/egress/config. UI grouping DERIVES from kind; there is no stored category.

View Source
const (
	// DeliveryProxyHeader: the proxy presents the secret in an HTTP header;
	// the sandbox never holds it. The default, never-resident lane.
	DeliveryProxyHeader = "proxy_header"
	// DeliveryResidentFile/DeliveryResidentEnv: materialized as a file, or
	// exported as an env var, inside the sandbox — disclosed exceptions.
	DeliveryResidentFile = "resident_file"
	DeliveryResidentEnv  = "resident_env"
)

Integration delivery modes: how one secret reaches the run.

View Source
const (
	OverrideOff      = "off"      // this workspace refuses the requirement outright
	OverrideOptional = "optional" // re-lane: off by default, a run may enable it
	OverrideRequired = "required" // re-lane: rides every run
)

AttachmentOverride values: a workspace's per-attachment stance on one requirement key its source declares.

View Source
const (
	ADOPATMaxHoursDefault = 8   // minted_pat: PATMaxHours 0 reads as this
	ADOPATMaxHoursLimit   = 168 // minted_pat: the longest a token may live, in hours
	ADOPATMaxDaysDefault  = 30  // own_pat: PATMaxDays 0 reads as this
	ADOPATMaxDaysLimit    = 90  // own_pat: the longest a pasted token may live, in days
)

Bounds and defaults for the personal-access-token lifetimes on ADOEntraConfig.

View Source
const ADOEntraAccessTokenSecret = "azure-devops-entra-access-token"

ADOEntraAccessTokenSecret is a SENTINEL secret name (NOT a stored secret) that resolves to an Azure DevOps access token minted from the RUN OWNER's own captured Entra sign-in (internal/api's resolveADOInjection). Nothing is stored under this name: the credential in the store is the person's refresh token, held in the reserved `wardyn-harness-ado-<row>-oauth` blob, and the access token exists only for the moment it is handed to the run's proxy sidecar.

SECURITY: THE TOKEN DOES NOT BOUND THE RUN, and no reader of this name may assume it does — measured against a real tenant, Entra issues an Azure DevOps token carrying EVERY scope the person consented to regardless of what subset is requested, so what holds a run to its granted capabilities is Wardyn's own capability check in front of the resource (the proxy), not the credential. The granted scope string the authority reports is recorded on the audit row because a token is opaque and that string is the only honest evidence of what the credential can do.

Deliberately NOT in sinkReservedSecret, for AWSSSOAccessTokenSecret's reason: being resolved at that sink, host-pinned, is the whole point.

View Source
const AWSSSOAccessTokenSecret = "aws-sso-access-token"

AWSSSOAccessTokenSecret is a SENTINEL secret name (NOT a stored secret) that works like the two OAuth sentinels above at the injection sink (host-pinned, forced header, masked), resolving to the run's OWN captured AWS IAM Identity Center access token — the credential an `aws sso login` captured into wardynd's secret store as a whole blob, not as a value under this name.

This is what makes portal.sso.<region> GetRoleCredentials (`authtype:none`) work: the proxy carries the session as the x-amz-sso_bearer_token HEADER and the sandbox never holds the token. The resolver, internal/api's resolveAWSSSOInjection, also re-derives the credential's scope from the roster and refuses any drift from the snapshot the grant was authored with.

Deliberately NOT in sinkReservedSecret: that guard refuses names an api_key grant must never resolve, but this name's entire purpose is to be resolved through that sink, host-pinned, exactly as bedrock-api-key is (internal/api/secrets.go explains why that one is excluded too). Nothing is stored under it — a secrets-API Put of this name is a value the sentinel arm never reads.

View Source
const IntegrationCredentialToken = "token"

IntegrationCredentialToken is the secret ROLE a generic header-delivered credential uses; closed kinds keep their own role names ("api_key", "pat", "ssh_key").

View Source
const ManagedOAuthSecret = "anthropic-managed-oauth"

ManagedOAuthSecret is a SENTINEL secret name (NOT a stored secret) that works exactly like SubscriptionOAuthSecret at the injection sink (host-pinned to api.anthropic.com, forced Authorization: Bearer, value masked), but resolves to the Wardyn-MANAGED subscription token: a long-lived `claude setup-token` OAuth token the operator captured via the container-login flow and Wardyn persisted (internal/api/harnesscred.go). This is what lets a distroless COMPOSE/containerized deployment, with no host ~/.claude to read, credential a subscription run proxy-side without ever making the token resident in the sandbox. Distinct from SubscriptionOAuthSecret only in SOURCE (managed store vs resident host ~/.claude), so the audit trail names which one credentialed a run.

View Source
const ModelProviderSecretPrefix = "wardyn-provider-"

ModelProviderSecretPrefix starts every per-person credential name: wardyn-provider-<uid>-{key,oauth,sso,entra}.

View Source
const PushActsAsOperator = "operator"

PushActsAsOperator is the ActsAsLabel of a git_pat whose secret is the operator's shared one rather than the run owner's own: no person owns it.

View Source
const PushContentMaxPaths = 10

PushContentMaxPaths caps the review-matched paths a PushContentScope names. PathsTotal carries the exact count; the complete list rides beside the scope as a PushPathList, stored per approval, never in the scope (the dedup key) or a refusal body.

View Source
const RecordingMaxParts = 2048

RecordingMaxParts bounds how many parts one run's session recording may be stored as (RL-12: wardyn-rec uploads a long run's cast every 24 h, or as soon as half the 64 MiB per-upload cap is waiting). It exists for the reason that cap does: a hostile sandbox must not grow its stored recording without bound, and the Recordings list reads every part of a run.

No existing limit bounds the honest part count: a governance profile may let a run have no end, and the deployment's ephemeral disk ceiling is optional. So this is a named ceiling, sized so an honest run does not reach it: 2048 parts is more than five and a half years of 24 h parts, or 64 GiB of terminal output in 32 MiB parts, and bounds one run's store at 2048 uploads of at most 64 MiB (128 GiB) and a replay or list render at 2048 reads. The control plane refuses a higher part (413), and the proxy never forwards one.

View Source
const SubscriptionOAuthSecret = "anthropic-subscription-oauth"

SubscriptionOAuthSecret is a SENTINEL secret name (NOT a stored secret): an api_key grant carrying it resolves at inject time to the operator's LIVE Anthropic subscription OAuth token (resident ~/.claude), never a stored value — so subscription runs are credentialed proxy-side like api-key runs while the sandbox holds only the inert sentinel. Also the durable "this profile uses subscription LLM auth" marker on a recorded profile, since the ~/.claude mount that would otherwise signal it is never synthesized. Shared here so api + recordmode + UI agree on the name.

View Source
const UserTypeStandard = "standard"

UserTypeStandard is the id of the built-in user type the migration seeds: editable, never deletable, and the sign-in tie floor.

Variables

View Source
var (
	// ErrApprovalAlreadyDecided: a decision was attempted on an approval that already left PENDING.
	// Fail closed — never let a second decision overwrite the first.
	ErrApprovalAlreadyDecided = errors.New("approval already decided")
	// ErrDuplicatePendingApproval: a partial unique index rejected a second open PENDING approval for
	// the same dedup key — a dedup signal (re-read the winner), not a hard failure.
	ErrDuplicatePendingApproval = errors.New("duplicate pending approval")
)

These sentinels live here (shared by internal/store and internal/approval) so the FSM can errors.Is instead of matching message text, which silently breaks on rewording.

ApprovalKinds is the closed set, checked by test against the constants above — closedEnumChecks once missed approvals.kind entirely, which would have let a DB-rejected constant pass every gate green.

ApprovalNotifyStates is the closed set, compared with the approval_notifications.state CHECK by internal/db's closedEnumChecks.

ApprovalStates is the closed set, in lifecycle order, so closedEnumChecks compares the DB CHECK against the Go set rather than a hand-typed test list.

ClosedADOTokenModes is the closed token-mode set — see ClosedGitLanes.

ClosedCredentialSources is the closed source set — the only values a write may name.

ClosedGitLanes is the closed lane set — see ClosedGitProviderKinds.

ClosedGitProviderKinds is the only kinds a write may name.

ClosedIntegrationKinds is the closed kind set — the kinds with bespoke behavior in code, and as of 0.5 the ONLY kinds a write may name (validateIntegrationWrite).

ClosedModelProviderKinds is the only kind set a write may name.

View Source
var ConfinementClassNames = map[ConfinementClass]string{
	CC1: "Fence",
	CC2: "Wall",
	CC3: "Vault",
}

ConfinementClassNames maps each wire code to its UI display label (mirrors cc-meta.ts CC_META).

DriveBackends is the closed set in the order the admin surface shows them.

DriveObjectSchemes is the closed set, in the order a row can only ever move through (a legacy row stays slug forever; a new row is always id).

DriveReclaims is the closed set in admin-surface order.

HomeTemplates is the closed set in admin-surface order.

View Source
var LegacyGitLanes = []GitLane{GitLaneApp, GitLanePAT, GitLaneSSH}

LegacyGitLanes are the lanes an empty GitProvider.Lanes list admits: not "every lane in ClosedGitLanes" but the three from when this rule was written, frozen forever so growing the closed set never silently opts a stored row into a new lane. It never applies to an Azure DevOps row: an empty list there is refused at the write boundary (the shared pat and ssh lanes it would expand to are retired), and migration 0103 rewrote every stored one.

View Source
var NonTerminalRunStates = []RunState{RunPending, RunStarting, RunRunning, RunWaiting}

NonTerminalRunStates is IsTerminal's complement, for the active-run quota query. POSITIVE, never `NOT IN (terminal)`: a forgotten member only undercounts here, but a negation would wedge capped accounts at their limit forever. Pinned against IsTerminal by test.

Functions

func ASCIIOnlySubject added in v0.7.0

func ASCIIOnlySubject(s string) bool

ASCIIOnlySubject reports whether s has no rune above ASCII — mirrors oidc.ASCIIOnly so internal/types needn't import the OIDC stack.

func CanonicalGroupSubject added in v0.7.0

func CanonicalGroupSubject(s string) (string, bool)

CanonicalGroupSubject canonicalizes an operator-authored group name into the exact string a session snapshot carries for it, or ok=false when no snapshot could ever carry it (empty or non-printable-ASCII).

SECURITY: guards the RAW value before folding — order is the property. strings.ToLower's Unicode case mapping (U+212A folds to 'k') would otherwise let a crafted look-alike fold onto the real ASCII group and inherit its grants.

func CanonicalUserSubject added in v0.7.0

func CanonicalUserSubject(s string) string

CanonicalUserSubject canonicalizes an operator-authored USER subject (OIDC `sub` or email) into the stored form.

SECURITY: same guard-before-fold order as CanonicalGroupSubject, to stop a Unicode look-alike (e.g. U+212A -> 'k') folding onto a real human's subject.

Unlike the group rule it does NOT refuse non-ASCII, deliberately: a `sub` is IdP-issued identity, not an operator label, so refusing it would refuse a real person — kept unfolded and unrefused.

Known gap: internal/api's capabilitySubjects folds with a bare ToLower, so a non-ASCII subject stored here can end up a dead, not misdirected, row there; fixing it belongs on the session surface.

func ClosedADOTokenModeList added in v0.7.10

func ClosedADOTokenModeList() []string

ClosedADOTokenModeList is ClosedADOTokenModes in a stable order.

func ClosedCredentialSourceList added in v0.7.2

func ClosedCredentialSourceList() []string

ClosedCredentialSourceList is ClosedCredentialSources in a stable order, for a rejected write's "want one of: …".

func ClosedGitLaneList added in v0.7.2

func ClosedGitLaneList() []string

ClosedGitLaneList is ClosedGitLanes in a stable order.

func ClosedGitProviderKindList added in v0.7.2

func ClosedGitProviderKindList() []string

ClosedGitProviderKindList is ClosedGitProviderKinds in a stable order.

func ClosedIntegrationKindList added in v0.5.0

func ClosedIntegrationKindList() []string

ClosedIntegrationKindList is ClosedIntegrationKinds sorted, for the "want one of: …" half of a rejected write's error.

func ClosedModelProviderKindList added in v0.8.0

func ClosedModelProviderKindList() []string

ClosedModelProviderKindList is ClosedModelProviderKinds in stable order, for a rejected write's error.

func DenyPathSegments added in v0.8.0

func DenyPathSegments(pattern string) ([]string, error)

DenyPathSegments is the one reading of a push_rules.deny_paths entry, shared by write-time validation and the broker's matcher so the two cannot disagree. A leading "/" is dropped (patterns anchor at the root either way); a trailing "/" means everything beneath (the .gitignore/CODEOWNERS spelling), so "infra/" reads as "infra/**". An empty, "." or ".." segment is refused — git never stores such a path, so it would silently match nothing and read as enforcement that isn't there. Invalid UTF-8 and leading/trailing whitespace are refused too (a near-certain typo).

func DriveHomeName added in v0.7.0

func DriveHomeName(d UserDrive, subject, override string) (string, error)

DriveHomeName derives the per-user home segment for one principal.

PRECEDENCE: (1) override (a user-tier grant's home_override) WINS; (2) hash — `d-` + the first 20 hex of sha256(drive id + "\n" + subject), the drive id keeping one member's two drives from colliding; (3) sub / email_local — the CLAIM, lowercased, email_local truncated at the first "@".

EVERY non-hash path is checked against driveHomeSegmentRe; an unusable claim is an ERROR, NEVER A GUESS. The remedy is a home_override.

`subject` is the identity the home is derived FROM and the caller selects it — never the GRANT's subject, since a group grant's subject would give an entire group one home.

func DriveHomeStricterRuleClause added in v0.7.0

func DriveHomeStricterRuleClause(b DriveBackend) string

DriveHomeStricterRuleClause is the extra sentence a backend's home rule needs beyond the frozen DOCKER-rule refusal, said to a MEMBER (or "" when that already covers it) — a server-composed SUFFIX appended after the frozen sentence, never a reworded canon.

func DriveObjectID added in v0.8.0

func DriveObjectID(id uuid.UUID) string

DriveObjectID is the drive's own id, dashless — the fixed-width fragment DriveObjectSchemeID puts where DriveSlug would otherwise go: always 32 lowercase hex characters, unlike DriveSlug whose length depends on caller input.

func DriveObjectName added in v0.7.0

func DriveObjectName(d UserDrive, home string) string

DriveObjectName is what the runner asks the substrate for, given a home already derived by DriveHomeName:

  • docker_volume -> wardyn-drive-<drive-slug>-<home>
  • k8s_pvc[_static] -> wardyn-drive-<drive-slug>-<home>
  • host_path -> <host_root>/<home>, cleaned

EVERY name Wardyn MINTS carries the DRIVE's slug: a home_override is a literal an admin typed on the GRANT, and the grant gets re-pointed between drives, so without the slug a re-point would leave two drives naming one volume. A SHARE keeps its own shape: <host_root> already scopes it.

THE DRIVE HALF IS d.ObjectScheme, beside DriveSlug rather than replacing it (migration 0061's unique index still keys on the slug). DriveObjectSchemeID puts DriveObjectID(d.ID) there instead — 32 hex characters, always, so <home> starts at a FIXED offset no crafted name can shift (issue #163).

func DriveObjectNamedByWardyn added in v0.7.0

func DriveObjectNamedByWardyn(b DriveBackend) bool

DriveObjectNamedByWardyn reports whether WARDYN MINTS this backend's storage object name rather than joining a path somebody else already created.

IT IS NOT THE MANAGED/SHARE SPLIT: DriveKind answers "does Wardyn CREATE the object?"; this answers "does Wardyn NAME it?". k8s_pvc_static is a SHARE by the first question and Wardyn-named by the second. Only host_path is on the other side. The SAME expression DriveObjectName branches on.

func DriveSlug added in v0.7.0

func DriveSlug(name string) string

DriveSlug folds a drive's human name into the DNS-1123 fragment a PVC name carries. ValidateUserDrive refuses a name that folds to nothing.

EXPORTED BECAUSE THE FOLD IS NOT INJECTIVE: "Corp NAS" and "corp nas" fold to one slug, so UNIQUE(name) alone would let two different-looking drive names mint the SAME object. Migration 0061 makes the slug column UNIQUE for every backend Wardyn names; this function decides what that column holds.

func DriveSubjectHash added in v0.7.0

func DriveSubjectHash(subject string) string

DriveSubjectHash fingerprints the principal a drive object was allocated to, in the one shape a substrate LABEL may carry it: the first 20 hex of sha256(subject), folded exactly as DriveHomeName folds it.

NON-PII IS THE WHOLE REQUIREMENT: a label is echoed verbatim by `docker volume inspect`/`kubectl describe` to anybody who can reach the daemon. The drive id is NOT in this digest, unlike DriveHomeName's — the label lives beside `wardyn.drive`, which already carries the id.

An EMPTY subject returns "" rather than the digest of "": the caller then writes no label, avoiding a fingerprint every identity-less caller would share.

func FoldWorkspaceContract added in v0.5.0

func FoldWorkspaceContract(
	attachments []WorkspaceAttachment,
	sources map[uuid.UUID]Source,
	overlay map[string]WorkspaceRequirement,
) map[string]WorkspaceRequirement

FoldWorkspaceContract computes a workspace's effective requirements (the map applyWorkspaceRequirements consumes unchanged) from its attachments, the attached sources' own contracts, and the workspace's overlay rows. Precedence, in order:

  1. Each attachment contributes its source's contract; a dangling SourceID contributes nothing.
  2. An override of "off" drops that source's contribution for that key.
  3. An override lane (optional|required) replaces the source's lane.
  4. SECURITY: a write:<path> key whose path isn't the source's own Locator is dropped, else a shared source could claim write on a path it doesn't own and widen a sibling mount in every attaching workspace.
  5. Collisions merge: Level = strongest, Provenance = weakest — fail-closed since the run-create trust boundary auto-mints only operator_set secrets (ponytail: overlay is the one-row override if too strict).
  6. An overlay row replaces the merged value outright; it can't remove a key ("off" is for that).

Pure: no store, no clock. With no attachments (or all-ephemeral), the result is exactly the overlay.

func ManagedBackendRejectsTemplate added in v0.7.0

func ManagedBackendRejectsTemplate(backend DriveBackend, tmpl HomeTemplate) bool

ManagedBackendRejectsTemplate is THE managed-backend home-template rule: a managed object's name ENDS in the home segment and is printed by `docker volume ls`/`kubectl get pvc` with no inspect/describe needed, so only `hash` — unique and revealing nothing — may name one. A share backend keeps every template.

Exported because the rule had TWO enforcement points that drifted apart: the write boundary widened from `email_local` to every non-hash template, but the resolver did not, so a legacy `sub` row was refused at authoring and still mounted. One predicate, so a third site cannot diverge again.

func PATRepoCovers added in v0.8.6

func PATRepoCovers(entry, key string) bool

PATRepoCovers reports whether the repos entry key entry covers key, both from PATRepoKey. An entry covers itself, and one trailing "/*" covers exactly one further path segment: "group/*" covers "group/app" and not "group/sub/app". A wildcard entry is covered only by the same wildcard.

func PATRepoKey added in v0.8.6

func PATRepoKey(forge, path string) (key string, ok bool)

PATRepoKey is the one repository-identity function for a git_pat scope. It returns the key a repos entry (or an extracted request path) compares by, and ok=false for a malformed one: empty, an empty segment, ".." anywhere, a "." segment, "?", "#", a backslash, a control character, an empty or repeated trailing "/*", or an unknown forge. An empty forge means generic.

generic returns the path exactly: no case-folding, no ".git" stripping, because git-http-backend serves different repositories for Team/App.git and team/app.git, and for team/foo and team/foo.git. A forge may canonicalise only where the proxy then forwards the canonical form, so that the compared string and the forwarded string are one value. No forge does yet, so every forge keys by the exact path.

func PATRepoSet added in v0.8.6

func PATRepoSet(entries ...string) *[]string

PATRepoSet returns a Repos value holding exactly the given entries (an empty call is the explicit empty list, "none").

func PushPathsDigest added in v0.8.0

func PushPathsDigest(paths []string) string

PushPathsDigest is PushContentScope.PathsDigest over paths.

func ShareBackendRejectsTemplate added in v0.7.0

func ShareBackendRejectsTemplate(backend DriveBackend, tmpl HomeTemplate) bool

ShareBackendRejectsTemplate is the MIRROR rule: a SHARE's directories are named by whoever owns the share, and Wardyn never mkdir's on one, so a `hash` home names a directory that will never exist. The empty template is excluded on both sides: ValidateUserDrive defaults it before this is asked.

func SplitRequirementKey added in v0.5.0

func SplitRequirementKey(key string) (typ, rest string, ok bool)

SplitRequirementKey splits a "<type>:<key>" requirement key on the first colon only (a write:<path> key may itself contain colons) and reports whether the type token is known.

func ValidToolEffect added in v0.7.0

func ValidToolEffect(e ToolEffect) bool

ValidToolEffect reports whether e is one of the three effects. A closed enum, checked at every ingest point: an unrecognised effect must be a 400, never a silently-ignored rule that reads as enforcement.

func ValidateUserDrive added in v0.7.0

func ValidateUserDrive(d *UserDrive, runnerTarget string) error

ValidateUserDrive normalizes d in place and validates it: trim, length, no control characters. runnerTarget must match d.Backend.RunnerTarget(), refused here rather than left to fail at run time; host_root is not checked against the deployment's env ceiling (see UserDriveHostRootCheck), since this package cannot read the environment.

func ValidateUserDriveGrant added in v0.7.0

func ValidateUserDriveGrant(g *UserDriveGrant) error

ValidateUserDriveGrant normalizes g in place and validates it: trim, length, no control characters, and for a GROUP subject the shared CanonicalGroupSubject the login-time snapshot is built with, so the subject vocabulary matches capabilitySubjects exactly.

Types

type ADOEntraConfig added in v0.7.10

type ADOEntraConfig struct {
	// TenantID is the Entra tenant every sign-in for this row goes to (GUID); a well-known alias ("common", "organizations") is refused.
	TenantID string `json:"tenant_id"`
	ClientID string `json:"client_id"` // app registration the sign-in names, as a GUID
	// CapabilityCeiling is the widest capability set a run on this row may
	// ever hold; required and non-empty when the block is present.
	CapabilityCeiling []adoscope.Capability `json:"capability_ceiling,omitempty"`
	DefaultProfile    []adoscope.Capability `json:"default_profile,omitempty"` // what a run gets by default; empty reads as adoscope.ProfileDefault
	TokenMode         ADOTokenMode          `json:"token_mode,omitempty"`      // empty reads as ADOTokenModeBearer
	// PATMaxHours is the longest a minted_pat run's token lives (1-168). 0
	// reads as 8; read via PATHours, never directly.
	PATMaxHours int `json:"pat_max_hours,omitempty"`
	// PATMaxDays is the furthest expiry an own_pat token may carry (1-90). 0
	// reads as 30; read via PATDays, never directly. A Server row has no entra
	// block, so it cannot set this and always reads the default, 30.
	PATMaxDays int `json:"pat_max_days,omitempty"`
	// RESTAPI: brokered REST calls, or only git traffic. Default true; read
	// via RESTAPIEnabled, never directly.
	RESTAPI *bool `json:"rest_api,omitempty"`
}

ADOEntraConfig is the Entra lane's configuration on one Azure DevOps row: tenant, app, widest ceiling, default profile, and token presentation. The ceiling is not a default: CapabilityCeiling is the most a run could ever be granted; DefaultProfile is what it gets by default.

func (*ADOEntraConfig) PATDays added in v0.8.2

func (c *ADOEntraConfig) PATDays() int

PATDays is PATMaxDays with the unset default applied.

func (*ADOEntraConfig) PATHours added in v0.8.2

func (c *ADOEntraConfig) PATHours() int

PATHours is PATMaxHours with the unset default applied.

func (*ADOEntraConfig) Profile added in v0.7.10

func (c *ADOEntraConfig) Profile() []adoscope.Capability

Profile is the capabilities a run gets when it asks for nothing: DefaultProfile, or adoscope.ProfileDefault when that is empty.

func (*ADOEntraConfig) RESTAPIEnabled added in v0.7.10

func (c *ADOEntraConfig) RESTAPIEnabled() bool

RESTAPIEnabled: the rest_api default (unset or nil receiver) is enabled.

type ADOTokenMode added in v0.7.10

type ADOTokenMode string

ADOTokenMode is how a run presents itself to Azure DevOps on an Entra-lane row: the Entra access token itself, a personal access token Wardyn creates for the run, or one the person pasted. A named field, so a refused mode says why rather than an opaque "unknown field" 400.

const (
	ADOTokenModeBearer ADOTokenMode = "bearer" // sends the Entra access token itself; the zero value
	// ADOTokenModeMintedPAT creates a short-lived personal access token per
	// run, in the person's name, through the sign-in app's own grant. Named
	// and closed, but the write boundary still refuses it: it is not yet
	// available.
	ADOTokenModeMintedPAT ADOTokenMode = "minted_pat"
	// ADOTokenModeOwnPAT is a personal access token the person pasted in
	// themselves. It needs no tenant or client: nothing signs in. SO EVERY
	// PICKER OF THE ONE SIGN-IN ROW MUST SKIP IT — the console-login capture
	// (adoEntraRow in cmd/wardynd) and the dispatch row lookup take any
	// enabled per_user entra-lane row today, and a row with no tenant or
	// client would fail their validation on every login and could shadow the
	// row that does sign in. Skipping own_pat there is L1's change (#1428).
	ADOTokenModeOwnPAT ADOTokenMode = "own_pat"
)

func (ADOTokenMode) Valid added in v0.7.10

func (m ADOTokenMode) Valid() bool

Valid reports whether m is an accepted mode.

type APIToken added in v0.6.0

type APIToken struct {
	ID              uuid.UUID  `json:"id"`
	Principal       string     `json:"principal"`
	Email           string     `json:"email,omitempty"`
	Role            string     `json:"role"`
	UserType        string     `json:"user_type"`
	Groups          []string   `json:"groups,omitempty"`
	GroupsTruncated *bool      `json:"groups_truncated,omitempty"`
	Name            string     `json:"name"`
	CreatedAt       time.Time  `json:"created_at"`
	LastUsedAt      *time.Time `json:"last_used_at,omitempty"`
	RevokedAt       *time.Time `json:"revoked_at,omitempty"`
	ExpiresAt       *time.Time `json:"expires_at,omitempty"` // nil = never expires; an expired token authenticates nothing
	MintedBy        string     `json:"minted_by,omitempty"`  // admin who minted this for its owner; empty when the owner minted it
	Token           string     `json:"token,omitempty"`      // plaintext, create response ONLY
	// IdentityStampedAt is when Role and Groups were last stamped: at mint, then at each sign-in of
	// the owner. Nil reads as stale under WARDYN_ROLE_STAMP_TTL. Never on the wire.
	IdentityStampedAt *time.Time `json:"-"`
}

APIToken is a per-user, long-lived bearer credential a human mints for scripts/CI, replacing shared use of the deployment-wide admin token; grants/RBAC/ownership bind to the owning human. Email/Role/Groups are a snapshot of the creating session (a bearer token carries no ID token to re-derive them from) — same bounded-stale ceiling as SSHPublicKey.Role: a demotion doesn't reach an outstanding token; revoke it. Groups distinguishes nil ("snapshot unavailable") from empty ("IdP sent no usable groups"); UserType change on the People page revokes tokens rather than re-stamping them. Token carries the plaintext credential, populated only on the create response — never stored, listed or logged. GroupsTruncated is THREE-VALUED: true = cut off by the cookie byte cap at mint; false = complete; nil = unknown (pre-dates this bit) and reads as TRUNCATED (fail closed) — nil-as-false would let a holder silently shed a group-assigned governance profile.

type ActorType

type ActorType string

ActorType distinguishes who performed an action in the audit stream.

const (
	ActorHuman  ActorType = "human"
	ActorAgent  ActorType = "agent"
	ActorSystem ActorType = "system"
)

type AgentProvider added in v0.7.2

type AgentProvider struct {
	// ID is the --agent value this row governs: a harness-catalog id OR a
	// WARDYN_AGENT_IMAGES key. The image-map half is deliberate: harnessByID
	// documents an image-map-only custom agent as supported, so a block that
	// refuses agents without a row must let the admin write one for their own.
	ID string `json:"id"`
	// Disabled turns the row off without deleting it — negative-sense, so the
	// zero value is ENABLED. Its runs are refused and the console renders it
	// unavailable, never hidden.
	Disabled bool `json:"disabled,omitempty"`
	// DefaultProvider names the model provider a new run of this agent uses
	// unless the person chooses another; must be enabled for this agent. If
	// later disabled, the default becomes a sentinel whose runs are refused,
	// never silently moved to another provider.
	DefaultProvider string `json:"default_provider,omitempty"`
}

AgentProvider is one agent row: which agent, whether it is offered, and the model provider its runs use unless the person chooses another. How a run reaches its model, and whose credential that is, live on the model provider (ModelProvider); every credential is each person's own.

type AgentProviders added in v0.7.2

type AgentProviders struct {
	// Agents are the per-agent rows; an agent with NO row is not offered at
	// all once the block exists.
	Agents []AgentProvider `json:"agents,omitempty"`
}

AgentProviders is the org's AGENT-ENABLEMENT POLICY: which coding agents this deployment offers and the model provider each uses by default. A sub-object of SiteConfig, shaped like WorkspaceProviders (see that type for the no-DDL / MDM-deliverable doctrine).

It exists because availability was an image map with no auth semantics: a customer's admin captured one AWS SSO session that silently backed every member's runs, with no way for a member to see whose credential was dying.

THE ZERO VALUE IS LEGACY OPEN MODE: with no block, every predicate over it answers exactly what it answered before the block existed (see the api package's agentProvidersConfigured / agentProviderFor).

VALIDATION LIVES IN internal/api (validateAgentProviders): a row's id is checked against the harness catalog / boot image map, and its default against the model providers — server state this package can't see. This file is types and closed enums only.

func (*AgentProviders) Empty added in v0.7.2

func (p *AgentProviders) Empty() bool

Empty reports whether p carries no policy — the shape a PUT uses to CLEAR the block. The write boundary normalizes this back to nil so a later GET doesn't render "agent_providers":{} forever.

type AgentRun

type AgentRun struct {
	ID        uuid.UUID `json:"id"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	CreatedBy string    `json:"created_by"` // human principal (token `sub`)
	Agent     string    `json:"agent"`      // e.g. "claude-code", "codex-cli"
	Repo      string    `json:"repo"`       // e.g. "org/name"
	Task      string    `json:"task"`       // human task description
	// Title groups runs in the console's list; empty for legacy/system runs, which fall back to Task.
	Title string `json:"title,omitempty"`
	// Description is optional free-text context; display-only, never interpreted.
	Description      string           `json:"description,omitempty"`
	PolicyID         *uuid.UUID       `json:"policy_id,omitempty"`
	ConfinementClass ConfinementClass `json:"confinement_class"`
	State            RunState         `json:"state"`
	SPIFFEID         string           `json:"spiffe_id"`     // spiffe://<trust-domain>/agent-run/<id>
	RunnerTarget     string           `json:"runner_target"` // "docker"
	SandboxRef       string           `json:"sandbox_ref,omitempty"`
	Image            string           `json:"image,omitempty"` // resolved sandbox image, kept for provenance; empty for legacy rows
	// Interactive marks a human-driven run: sandbox comes up RUNNING, no task exec'd, no completion watcher.
	Interactive bool `json:"interactive"`
	// WorkspacePath is the run's primary host directory, denormalized so the control plane can warn (never block) a second run on it.
	WorkspacePath string `json:"workspace_path,omitempty"`
	// WorkspaceID marks a governed scan run: the driver runs wardyn-scan instead of the agent, trusted (not sandbox input) to persist the profile.
	WorkspaceID *uuid.UUID `json:"workspace_id,omitempty"`
	// WorkspaceIDs is the onboarded workspaces this run's policy referenced at create; grants nothing, only tells an `always` egress decision what to persist to.
	WorkspaceIDs []uuid.UUID `json:"workspace_ids,omitempty"`
	SourceID     *uuid.UUID  `json:"source_id,omitempty"` // trusted run→library-source linkage for a per-source scan run; never set by user runs
	// AutoStopAfterSec is the run's idle auto-stop cap, frozen at create so the reaper works with no policy_id to JOIN. 0 = never; <0 = explicit never.
	AutoStopAfterSec int    `json:"auto_stop_after_sec,omitempty"`
	AgentExecID      string `json:"agent_exec_id,omitempty"` // docker exec id, so the crash reconciler can check agent liveness across a wardynd restart
	FailureHint      string `json:"failure_hint,omitempty"`  // operator-facing reason a run FAILED before the agent started; display-only
	// StatusDetail is what the substrate says this run awaits while STARTING; blanked on leaving STARTING except a TERMINAL failure reason (kept for postmortem).
	StatusDetail string `json:"status_detail,omitempty"`
	StatusReason string `json:"status_reason,omitempty"` // reason token parsed out of StatusDetail, derived at read, never stored
	// AutonomyLevel freezes the level resolved at create, not the profile/posture that produced it (that's on the audit row).
	AutonomyLevel AutonomyLevel `json:"autonomy_level,omitempty"`
	EndsAt        *time.Time    `json:"ends_at"`                   // run's lease end, captured once at create from RunLimits, never re-resolved
	WaitBudgetSec int           `json:"wait_budget_sec,omitempty"` // how long an approval this run raises stays open; 0 = deployment default alone applies
	// RunLimits are the owner's profile limits as they stood at create; later clamps use these captured bounds, never the profile's current ones.
	RunLimits           RunLimits  `json:"run_limits"`
	GovernanceProfileID *uuid.UUID `json:"governance_profile_id,omitempty"`
	// LostAt/LostReason mark a run that lost its sandbox but is kept (no proxy/network, files stay); stays RUNNING until a kill or the ended-run grace.
	LostAt     *time.Time `json:"lost_at,omitempty"`
	LostReason LostReason `json:"lost_reason,omitempty"`
	// ContainmentError/At: latest/first stop-confirm failure for a kept run; the lease sweep retries and clears both once it lands.
	ContainmentError   string     `json:"containment_error,omitempty"`
	ContainmentErrorAt *time.Time `json:"containment_error_at,omitempty"`
	// EndedAt stamps the transition into a terminal RunState; a lease-ended run stays RUNNING until the ended-run grace, so its end time is LostAt, not EndedAt.
	EndedAt        *time.Time `json:"ended_at,omitempty"`
	EndTightenedAt *time.Time `json:"end_tightened_at,omitempty"` // profile re-clamp shortened the end
	// PausedAt/PausedReason mark a run frozen with nobody present; keeps state/memory/files/proxy until activity or resume. Not contained — kill still works.
	PausedAt     *time.Time  `json:"paused_at,omitempty"`
	PausedReason PauseReason `json:"paused_reason,omitempty"`
	ActiveAt     *time.Time  `json:"active_at,omitempty"` // presence clock: last human input/egress decision/proxied bytes; keepalives never move it; nil reads as CreatedAt
	// ModelProviderID freezes the provider id at create; the kind is deliberately NOT frozen (can change later) and lives only on the run.create audit snapshot.
	ModelProviderID string `json:"model_provider_id,omitempty"`
	UserType        string `json:"user_type,omitempty"` // creator's resolved user type, frozen at create
	Preset          string `json:"preset,omitempty"`    // launch preset this run was expanded from; empty for an explicit-spec run
	PresetVersion   int    `json:"preset_version,omitempty"`
	OperatorOwned   bool   `json:"operator_owned,omitempty"` // mirrors identity.Claims.OperatorOwned so dispatch/revive re-mint read what create decided; the console reads it for the no-personal-owner entry carve-out
	// CreatedVia is the registered portal this run was launched through on the person's behalf; nil when self-launched. The owner is still the person.
	CreatedVia *uuid.UUID `json:"created_via,omitempty"`
	// HasRecording/RecordingBytes/RecordingDurationSec are derived, never stored, projected only with ?include=recording_meta; HasRecording is the only "no recording" signal.
	HasRecording         bool          `json:"has_recording"`
	RecordingBytes       int64         `json:"recording_bytes,omitempty"`
	RecordingDurationSec float64       `json:"recording_duration_sec,omitempty"`
	DiskMiB              int           `json:"disk_mib,omitempty"`  // effective ephemeral disk cap MiB; 0 = no cap resolved
	Attention            *RunAttention `json:"attention,omitempty"` // what this live run awaits and who can clear it; derived, only on view-opted-in read paths
}

AgentRun is one governed execution of a coding agent on behalf of a human, with its own identity (SPIFFE ID), credential grants, and audit trail.

type ApprovalDecision added in v0.5.0

type ApprovalDecision struct {
	State     ApprovalState
	DecidedBy string
	Reason    string
	Scope     ApprovalScope // meaningful only for an egress_domain approval; others leave the zero value
	ExpiresAt *time.Time
}

ApprovalDecision is what a human (or the sweeper, for a stale-PENDING expiry) is deciding, threaded as one value instead of growing DecideApproval's signature. ExpireStale leaves Scope at "" (never ScopeRun) — an expiry is a sweep nobody decided.

type ApprovalEscalation added in v0.8.6

type ApprovalEscalation struct {
	Tier   int16
	NextAt *time.Time
}

ApprovalEscalation is one pending approval's place on its notification schedule: the highest tier whose due time has passed, and the earliest due time still ahead (nil when no later tier exists).

type ApprovalKind

type ApprovalKind string

ApprovalKind enumerates what a human is being asked to approve.

const (
	ApprovalCredential   ApprovalKind = "credential"
	ApprovalEgressDomain ApprovalKind = "egress_domain"
	ApprovalToolCall     ApprovalKind = "tool_call"
	// ApprovalCredentialReauth: the run's model credential lapsed mid-run, so the proxy holds the
	// next exchange while the owner signs in again. Not a decision — the row moves to APPROVED only
	// via ResolveReauth, never approval.decide.
	ApprovalCredentialReauth ApprovalKind = "credential_reauth"
	// ApprovalPushContent: a brokered git push touched a require_review_paths path, held for an
	// admin decision. Admin-only — a member deciding their own push is the exfiltration it stops.
	ApprovalPushContent ApprovalKind = "push_content"
)

type ApprovalNotifyChannelStat added in v0.8.6

type ApprovalNotifyChannelStat struct {
	Channel        string
	LastSentAt     *time.Time
	LastError      string
	LastErrorAt    *time.Time
	FailedLastHour int
}

ApprovalNotifyChannelStat is one channel's delivery history read from the outbox: when a send last succeeded, the class and time of the last failed attempt, and how many rows went dead in the last hour. LastError is a failure class such as "http_status:503", never text a server or URL wrote.

type ApprovalNotifyState added in v0.8.6

type ApprovalNotifyState string

ApprovalNotifyState is the lifecycle of one approval_notifications outbox row.

const (
	NotifyPending   ApprovalNotifyState = "pending"
	NotifySending   ApprovalNotifyState = "sending"
	NotifySent      ApprovalNotifyState = "sent"
	NotifyCancelled ApprovalNotifyState = "cancelled"
	NotifyDead      ApprovalNotifyState = "dead"
)

type ApprovalRequest

type ApprovalRequest struct {
	ID             uuid.UUID       `json:"id"`
	RunID          uuid.UUID       `json:"run_id"`
	GrantID        *uuid.UUID      `json:"grant_id,omitempty"`
	Kind           ApprovalKind    `json:"kind"`
	RequestedScope json.RawMessage `json:"requested_scope"`
	State          ApprovalState   `json:"state"`
	RequestedAt    time.Time       `json:"requested_at"`
	DecidedAt      *time.Time      `json:"decided_at,omitempty"`
	DecidedBy      string          `json:"decided_by,omitempty"`
	MintedJTI      string          `json:"minted_jti,omitempty"`
	Reason         string          `json:"reason,omitempty"`
	// DecisionScope is how far the decision reaches (see ApprovalScope); empty on a PENDING row. Named
	// decision_scope to avoid clashing with RequestedScope (the unrelated host JSON).
	DecisionScope ApprovalScope `json:"decision_scope,omitempty"`
	// DecisionExpiresAt is set only when DecisionScope is until — a different clock than the EXPIRED-state sweeper, which ages out stale PENDING requests.
	DecisionExpiresAt *time.Time `json:"decision_expires_at,omitempty"`
	// ExpiresAt is when a PENDING request stops waiting: min(requested_at+WaitBudgetSec, EndsAt); computed on read, never stored.
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	Held      bool       `json:"held,omitempty"` // Held/HeldUntil project approval.Hold(this, now) at response time, never stored; both zero on a DECIDED row
	HeldUntil *time.Time `json:"held_until,omitempty"`
	// EscalationTier is the highest notification tier whose due time has passed, and SLADueAt is the
	// next tier's due time. Both are projected from approval_notifications at response time on PENDING
	// rows only, never stored; tier 0 (the first notice) and "no later tier" leave them zero.
	EscalationTier int16      `json:"escalation_tier,omitempty"`
	SLADueAt       *time.Time `json:"sla_due_at,omitempty"`
}

ApprovalRequest is a blocking human-in-the-loop gate. RequestedScope is EXACTLY what the approver saw; the broker writes MintedJTI back in the same transaction as the mint.

type ApprovalScope added in v0.5.0

type ApprovalScope string

ApprovalScope is how far a human's approve/deny decision reaches — orthogonal to FirstUseMode, which decides whether to escalate at all. egress_domain carries any of the four; tool_call carries none; credential carries only ScopeRun, re-mintable for the rest of the run.

const (
	// ScopeOnce releases exactly one connection — one CONNECT tunnel for HTTPS, one request for HTTP.
	ScopeOnce ApprovalScope = "once"
	// ScopeRun holds for the rest of the run — the legacy default meaning.
	ScopeRun ApprovalScope = "run"
	// ScopeUntil holds until DecisionExpiresAt or run end, whichever comes first.
	ScopeUntil ApprovalScope = "until"
	// ScopeAlways also persists the host onto the run's workspace so future runs inherit it. Operator-only.
	ScopeAlways ApprovalScope = "always"
)

func (ApprovalScope) Normalize added in v0.5.0

func (s ApprovalScope) Normalize() ApprovalScope

Normalize resolves a stored/wire value for every runtime read: empty means legacy run-scoped => ScopeRun; an unknown non-empty value can only come from a newer control plane, and treating it as ScopeRun would be fail-open across versions, so it floors to ScopeOnce. Caveat: on a DENY, once is actually WIDER (it re-raises), so this isn't uniformly fail-closed, just re-denied with extra queue noise.

func (ApprovalScope) Valid added in v0.5.0

func (s ApprovalScope) Valid() bool

Valid reports whether s is empty (legacy run-scoped) or a known scope — rejects garbage at the API write boundary; runtime reads still fail closed via Normalize.

type ApprovalState

type ApprovalState string

ApprovalState is the approval lifecycle.

const (
	ApprovalPending  ApprovalState = "PENDING"
	ApprovalApproved ApprovalState = "APPROVED"
	ApprovalDenied   ApprovalState = "DENIED"
	ApprovalExpired  ApprovalState = "EXPIRED"
	// ApprovalCancelled: the run reached a terminal state while still PENDING; answering it would
	// mint a credential for a gone sandbox. Every reader treats it as a refusal.
	ApprovalCancelled ApprovalState = "CANCELLED"
)

type ArtifactOverride deprecated

type ArtifactOverride struct {
	BaseURL        string `json:"base_url"`
	TokenSecretRef string `json:"token_secret_ref,omitempty"`
}

ArtifactOverride is one ecosystem's corporate artifact-registry redirect: the base URL to emit into that ecosystem's config (.npmrc/pip.conf/cargo config/settings.xml/GOPROXY/nuget.config) plus an optional secret ref for a token injected proxy-side (the sandbox never holds the value).

Deprecated: superseded by EgressRedirect. See SiteConfig.ArtifactOverrides for why the type is kept.

type AttentionBy added in v0.8.0

type AttentionBy string

AttentionBy names who the run is waiting on, in the viewer's own console view (User or Admin; see RunAttention for how the two differ).

const (
	// AttentionYou: the viewer themselves can act (decide the approval, sign
	// in, revive the run).
	AttentionYou AttentionBy = "you"
	// AttentionOwner: only the run's owner can act — reauth and lost-run
	// revive are never delegable — and the viewer isn't the owner; reachable
	// only in Admin view, since User view forces owner=me.
	AttentionOwner AttentionBy = "owner"
	// AttentionAdmin: only a security operator can act; the viewer is a
	// member who cannot decide this run's held approval.
	AttentionAdmin AttentionBy = "admin"
)

type AttentionKind added in v0.8.0

type AttentionKind string

AttentionKind names what a live run is waiting on. Absent (RunAttention nil) means the run needs nobody — terminal, lease-ended, or nothing PENDING held.

const (
	// AttentionApproval: a held approval (any kind but credential_reauth) is
	// parking the sandbox.
	AttentionApproval AttentionKind = "approval"
	// AttentionReauth: the run's model credential lapsed mid-run; the proxy
	// holds the next credential exchange for its owner to sign in.
	AttentionReauth AttentionKind = "reauth"
	// AttentionADOConsent: same as AttentionReauth, specifically an Azure
	// DevOps sign-in or consent request (TallyReauthADOSignIn/Consent).
	AttentionADOConsent AttentionKind = "ado_consent"
	// AttentionLost: the run lost its sandbox for a reason other than lease
	// end (reboot, outage) and needs a revive.
	AttentionLost AttentionKind = "lost"
)

type AuditEvent

type AuditEvent struct {
	ID        uuid.UUID       `json:"id"`
	Time      time.Time       `json:"time"`
	RunID     *uuid.UUID      `json:"run_id,omitempty"`
	ActorType ActorType       `json:"actor_type"`
	Actor     string          `json:"actor"`  // human sub, agent SPIFFE ID, or component name
	Action    string          `json:"action"` // dotted verb, e.g. "credential.mint", "egress.deny", "kernel.process.exec"
	Target    string          `json:"target,omitempty"`
	Outcome   string          `json:"outcome"` // "success" | "failure" | "denied"
	SourceIP  string          `json:"source_ip,omitempty"`
	Data      json.RawMessage `json:"data,omitempty"`

	// DeviceID names the enrolled device that forwarded this row; derived on read, never taken from a caller.
	DeviceID *uuid.UUID `json:"device_id,omitempty"`

	// PrevHash/RowHash form the tamper-evidence chain: RowHash = SHA-256(PrevHash || canonical serialization
	// of the fields above), computed BY POSTGRES in a BEFORE INSERT trigger, never by the caller; sinks use
	// the head hash to detect truncation. Verified via GET /api/v1/audit/chain/verify.
	PrevHash string `json:"prev_hash,omitempty"`
	RowHash  string `json:"row_hash,omitempty"`
}

AuditEvent is one append-only audit record. Every credential mint/revoke, approval decision, policy change, egress decision, and lifecycle change emits one, carrying the delegation chain (human sub + agent run). credential.*/identity.*/approval.*/policy.*/egress.*/run.*/recording.* are control-plane and agent self-report events; kernel.* is the eBPF/Tetragon ground-truth stream, emitted ONLY by the host-scoped sensor via POST /api/v1/internal/groundtruth, which FORCES actor_type=system+actor="wardyn-tetragon-ingest" and rejects any action without the "kernel." prefix. kernel.* Data carries stream/subtype/cgroup_id/container_id, a kind-specific argv/dst/path, loader (the documented mmap execve-hook bypass surface, flagged not blocked), and correlation ("mapped"|"unmapped" — unmapped => run_id NULL, never silently dropped). Outcome stays within "success"|"failure"|"denied": kernel events use failure for an escape/coverage-gap signal and never denied — a detection stream, it does not block. See internal/groundtruth.

type AuditRetentionDrop added in v0.8.6

type AuditRetentionDrop struct {
	Partition string `json:"partition"`
	Rows      int64  `json:"rows"`
	SeqLo     int64  `json:"seq_lo"`
	SeqHi     int64  `json:"seq_hi"`
	Digest    string `json:"digest"`
	// EventSeq is the seq of the chained audit.retention.partition_dropped event.
	EventSeq int64 `json:"event_seq"`
}

AuditRetentionDrop is a completed drop.

type AuditRetentionPartition added in v0.8.6

type AuditRetentionPartition struct {
	Name string `json:"name"`
	// Lo is absent for the legacy partition (it starts at MINVALUE).
	Lo    *time.Time `json:"lo,omitempty"`
	Hi    *time.Time `json:"hi,omitempty"`
	Rows  int64      `json:"rows"`
	State string     `json:"state"` // closed | open | future
	// Eligible is true when audit_retention_drop would take this partition now; otherwise Refusal is the
	// reason it would refuse with.
	Eligible bool   `json:"eligible"`
	Refusal  string `json:"refusal,omitempty"`
}

AuditRetentionPartition is one partition of the audit log as audit_retention_partitions reports it.

type AuditRetentionPolicy added in v0.8.6

type AuditRetentionPolicy struct {
	// Days is the stored policy; EffectiveDays is what the drop function applies now (the pending value
	// once its date has passed).
	Days          int `json:"days"`
	EffectiveDays int `json:"effective_days"`
	// PendingDays and PendingEffectiveAt are set together, for a decrease that has not taken effect.
	PendingDays        *int       `json:"pending_days,omitempty"`
	PendingEffectiveAt *time.Time `json:"pending_effective_at,omitempty"`
}

AuditRetentionPolicy is the persisted retention policy (audit_partition_meta). A decrease, including 0 -> finite, is pending until its date; days 0 is forever.

type AuditRetentionPolicyChange added in v0.8.6

type AuditRetentionPolicyChange struct {
	Outcome string `json:"outcome"`
	AuditRetentionPolicy
}

AuditRetentionPolicyChange is audit_retention_set_policy's answer. Outcome is applied, pending, cancelled or unchanged.

type AuditRetentionStatus added in v0.8.6

type AuditRetentionStatus struct {
	Policy AuditRetentionPolicy `json:"policy"`
	// Cutover is when the log was converted to partitions; everything before it is in the legacy partition.
	Cutover    time.Time                 `json:"cutover"`
	Partitions []AuditRetentionPartition `json:"partitions"`
	// MonthsAhead is how many months past the current one already have a partition.
	MonthsAhead int `json:"months_ahead"`
}

AuditRetentionStatus is what GET /audit/retention reports.

type AutonomyEgressPosture added in v0.8.0

type AutonomyEgressPosture string

AutonomyEgressPosture is one of the three egress states an AutonomyRubric caps against (internal/composer/autonomy.go's posture arithmetic, #97).

const (
	AutonomyEgressOpen     AutonomyEgressPosture = "open"
	AutonomyEgressReviewed AutonomyEgressPosture = "reviewed"
	AutonomyEgressSealed   AutonomyEgressPosture = "sealed"
)

type AutonomyLevel added in v0.8.0

type AutonomyLevel string

AutonomyLevel is one rung on the autonomy ladder a governance profile's AutonomyRubric caps against, L0 (most supervised) through L3 (least). The codes stay internal — the console renders plain labels, like ConfinementClass's CC1/CC2/CC3 (0.8 #77):

  • L0 "attended": interactive only, supervised seeding.
  • L1 "gated": adds non-interactive runs, but tool approvals derive to hold.
  • L2 "unattended": adds auto-approval and seeded auto tools.
  • L3: adds task_mode=exec, the door around every other gate — the top rung.
const (
	AutonomyL0 AutonomyLevel = "L0"
	AutonomyL1 AutonomyLevel = "L1"
	AutonomyL2 AutonomyLevel = "L2"
	AutonomyL3 AutonomyLevel = "L3"
)

func (AutonomyLevel) Rank added in v0.8.0

func (l AutonomyLevel) Rank() int

Rank orders AutonomyLevel weakest (most supervised) -> strongest (least), mirroring ConfinementClass.Rank(): resolving a rubric folds several applicable caps to their MINIMUM level (#97), and Rank is what "minimum" compares on. An unrecognised value ranks below L0 so it never wins a min() against a real level.

func (AutonomyLevel) Valid added in v0.8.0

func (l AutonomyLevel) Valid() bool

Valid reports whether l is one of the four defined rungs. Unlike ConfinementClass (whose every Rank() caller already tolerates rank 0), AutonomyRubric needs this gate: an author-facing field, so a typo must 400 rather than silently rank as "below L0".

type AutonomyPosture added in v0.8.0

type AutonomyPosture struct {
	Egress      AutonomyEgressPosture  `json:"egress"`
	Secrets     AutonomySecretsPosture `json:"secrets"`
	Confinement ConfinementClass       `json:"confinement"`
}

AutonomyPosture is a run's three-axis shape — egress reach, secret power, enforced confinement class — the input resolveRunAutonomy (#97) folds against a profile's AutonomyRubric. Computed, never stored on its own; it travels inside AutonomyResolution.

type AutonomyResolution added in v0.8.0

type AutonomyResolution struct {
	Level   AutonomyLevel   `json:"level"`
	Posture AutonomyPosture `json:"posture"`
	BoundBy []string        `json:"bound_by,omitempty"`
}

AutonomyResolution is what resolveRunAutonomy (#97) decides for one run: the level, the posture that produced it, and every rubric field that bound the result — provenance for the create audit row, the frozen AgentRun.AutonomyLevel, and the preflight response.

BoundBy is a LIST, deliberately: the fold is a min() over three axes, so rows routinely TIE at the resolved level, and naming just one cause would leave an admin editing a row that can't move the level while other causes stay hidden — every tied cause is named, in the fixed field order internal/composer/autonomy.go folds in.

Empty when nothing capped the level (no profile, no rubric, or an unset posture).

type AutonomyRubric added in v0.8.0

type AutonomyRubric struct {
	// EgressOpen caps the level when egress is OPEN: allow-all, or any allowlisted host beyond baseline.
	EgressOpen AutonomyLevel `json:"egress_open,omitempty"`
	// EgressReviewed caps the level when egress is REVIEWED: first-use approval raises approvals, nothing wide open.
	EgressReviewed AutonomyLevel `json:"egress_reviewed,omitempty"`
	// EgressSealed caps the level when egress is SEALED: neither of the above.
	EgressSealed AutonomyLevel `json:"egress_sealed,omitempty"`
	// SecretsPowerful caps the level when the run holds a POWERFUL secret: a write-capable grant, an
	// api_key to a non-baseline host, or a git_pat/ssh_key/env_secret grant.
	SecretsPowerful AutonomyLevel `json:"secrets_powerful,omitempty"`
	// SecretsBaseline caps the level when the run holds any grant, none of them powerful.
	SecretsBaseline AutonomyLevel `json:"secrets_baseline,omitempty"`
	// SecretsNone caps the level when the run holds no grant at all.
	SecretsNone AutonomyLevel `json:"secrets_none,omitempty"`
	// ConfinementCC1/CC2/CC3 cap the level by the run's ENFORCED confinement
	// class (an empty enforced class reads as CC1 — see AutonomyPosture).
	ConfinementCC1 AutonomyLevel `json:"confinement_cc1,omitempty"`
	ConfinementCC2 AutonomyLevel `json:"confinement_cc2,omitempty"`
	ConfinementCC3 AutonomyLevel `json:"confinement_cc3,omitempty"`
}

AutonomyRubric maps a run's posture to a permitted AutonomyLevel: three egress postures, three secret postures, three confinement classes, each unset (caps nothing) or one of the four levels. A run resolves to the MINIMUM over every field whose posture applies (internal/composer/autonomy.go, #97); an all-unset rubric caps nothing, like a nil rubric.

A closed struct with `omitempty` on every field, not a map — same doctrine as GovernanceLimits: small, complete, validated by the Go type (Validate), no DB CHECK needed.

func (AutonomyRubric) Validate added in v0.8.0

func (a AutonomyRubric) Validate() error

Validate reports the first field carrying an undefined AutonomyLevel, naming it — governanceLimitsRefusal prefixes the field name onto its 400 so an admin knows which of the nine to fix. An empty field is always valid: unset caps nothing.

type AutonomySecretsPosture added in v0.8.0

type AutonomySecretsPosture string

AutonomySecretsPosture is one of the three secret-power states an AutonomyRubric caps against.

const (
	AutonomySecretsPowerful AutonomySecretsPosture = "powerful"
	AutonomySecretsBaseline AutonomySecretsPosture = "baseline"
	AutonomySecretsNone     AutonomySecretsPosture = "none"
)

type AzureSettings added in v0.8.6

type AzureSettings struct {
	Endpoint string `json:"endpoint,omitempty"`
	Route    string `json:"route,omitempty"`
}

AzureSettings is an azure_foundry row's data-plane endpoint and the one route it serves (AzureRoute*).

type BaseImageEntry added in v0.5.0

type BaseImageEntry struct {
	ID    uuid.UUID `json:"id"`
	Kind  string    `json:"kind"` // "registry" | "custom" | "byo"
	Name  string    `json:"name"`
	Image string    `json:"image"` // the ref: the image itself (registry/byo) or the FROM (custom)
	// Steps are Dockerfile lines on a "custom" Image; not applied at build
	// time (host-side build RCE — see WorkspaceBaseImage.Steps), stored only
	// so the row round-trips.
	Steps     []string  `json:"steps,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

BaseImageEntry is one tier-2 catalog row: a shared, reusable image an operator saved. Kind is registry|custom|byo, never "recommended" (a derived per-workspace build with no catalog identity, enforced by a store CHECK).

type BedrockSettings added in v0.8.0

type BedrockSettings struct {
	Region       string `json:"region,omitempty"`
	BaseURL      string `json:"base_url,omitempty"`
	SSOStartURL  string `json:"sso_start_url,omitempty"`
	SSOAccountID string `json:"sso_account_id,omitempty"`
	SSORoleName  string `json:"sso_role_name,omitempty"`
}

BedrockSettings is a Bedrock kind's region, optional data-plane address, and (bedrock_sso only) the org-level sign-in setup. The start URL and pin are ADMIN-OWNED: a sign-in proposes, the provider disposes.

type Branding added in v0.8.0

type Branding struct {
	OrgName         string
	NameFormat      string
	Primary         string
	PrimaryText     string
	DarkPrimary     string
	DarkPrimaryText string
	SupportURL      string
	LogoType        string
	LogoFromFile    bool // the logo is the site config's branding.logo_path file (0105_branding_logo_from_file)
	UpdatedAt       time.Time
	UpdatedBy       string
}

Branding is the org-wide console branding record (migration 0091_branding). Only the primary colour, its text colour, the logo and the name are brandable: danger/warning/success/info and the admin-view cue are fixed Wardyn tokens a brand can never recolour. Empty DarkPrimary means the dark pair is derived (DeriveDarkPrimary in internal/api).

type CapabilityEffect added in v0.6.0

type CapabilityEffect string

CapabilityEffect is a grant's direction. Closed and complete; DENY BEATS ALLOW at resolution time, with no user-vs-group precedence — "Bob's user allow overrode the group deny" is a breach report, not a feature.

const (
	CapabilityAllow CapabilityEffect = "allow"
	CapabilityDeny  CapabilityEffect = "deny"
)

func (CapabilityEffect) Valid added in v0.6.0

func (e CapabilityEffect) Valid() bool

Valid reports whether e is allow or deny.

type CapabilityGrant added in v0.6.0

type CapabilityGrant struct {
	ID          uuid.UUID             `json:"id"`
	SubjectType CapabilitySubjectType `json:"subject_type"`
	Subject     string                `json:"subject"`
	Capability  string                `json:"capability"`
	Value       string                `json:"value"`
	Effect      CapabilityEffect      `json:"effect"`
	CreatedAt   time.Time             `json:"created_at"`
	CreatedBy   string                `json:"created_by,omitempty"`
}

CapabilityGrant is one row of the permissioning grant list: "subject S may (or may not) use capability C at value V".

Capability is a PLAIN STRING, not an enum, deliberately: the closed kind set lives in one Go slice (internal/api's capabilityKinds) and is checked at the write boundary, so a new kind needs no schema change. An unknown kind is simply inert.

Value's meaning is per kind: host or "*.suffix" for egress_host, else an exact secret name/workspace uuid/image ref; "*" is always the wildcard.

type CapabilitySubjectType added in v0.6.0

type CapabilitySubjectType string

CapabilitySubjectType names WHO a capability grant is written against. Closed and complete — its DB CHECK is pinned against these constants by internal/db's TestClosedEnumChecksMatchConstants.

const (
	// CapabilitySubjectUser is one human, named by lowercased OIDC "sub" or
	// email — either matches, so an admin can record whichever identity they
	// actually know.
	CapabilitySubjectUser CapabilitySubjectType = "user"
	// CapabilitySubjectGroup is one entry of the login-time union of the ID
	// token's roles+groups claims (oidc.Session.Groups); Entra App Roles are
	// grantable through this with no extra config.
	CapabilitySubjectGroup CapabilitySubjectType = "group"
	// CapabilitySubjectAll is every signed-in human, the baseline when an IdP
	// emits no usable groups claim. Subject is "" for this type.
	CapabilitySubjectAll CapabilitySubjectType = "all"
	// CapabilitySubjectUserType is everyone of one user type (UserType.ID); a
	// person holds exactly one, stamped at sign-in. It sits between group and
	// all for governance ceilings/drives; for grants it's one more subject,
	// so a DENY on a type is a wall no user or group allow lifts.
	CapabilitySubjectUserType CapabilitySubjectType = "user_type"
)

func (CapabilitySubjectType) Valid added in v0.6.0

func (t CapabilitySubjectType) Valid() bool

Valid rejects a garbage subject type at the API write boundary, mirroring ApprovalScope.Valid.

type CeilingOverlay added in v0.8.6

type CeilingOverlay struct {
	AllowedDomains          *[]string              `json:"allowed_domains,omitempty"`
	DeniedDomains           *[]string              `json:"denied_domains,omitempty"`
	AllowAllEgress          *bool                  `json:"allow_all_egress,omitempty"`
	FirstUseApproval        *FirstUseMode          `json:"first_use_approval,omitempty"`
	FirstUseHoldSeconds     *int                   `json:"first_use_hold_seconds,omitempty"`
	MaxHolds                *int                   `json:"max_holds,omitempty"`
	AllowedMethods          *[]string              `json:"allowed_methods,omitempty"`
	MinConfinementClass     *ConfinementClass      `json:"min_confinement_class,omitempty"`
	EligibleGrants          *[]GrantSpec           `json:"eligible_grants,omitempty"`
	AutoStopAfterSec        *int                   `json:"auto_stop_after_sec,omitempty"`
	WorkspaceMounts         *[]WorkspaceMount      `json:"workspace_mounts,omitempty"`
	WorkspaceRepos          *[]WorkspaceRepo       `json:"workspace_repos,omitempty"`
	LLMInspection           *LLMInspectionSpec     `json:"llm_inspection,omitempty"`
	UIApps                  *[]UIApp               `json:"ui_apps,omitempty"`
	Resources               *ResourcesOverlay      `json:"resources,omitempty"`
	ToolRules               *[]ToolRule            `json:"tool_rules,omitempty"`
	GitPushAnyBranch        *bool                  `json:"git_push_any_branch,omitempty"`
	PushRules               *PushRulesOverlay      `json:"push_rules,omitempty"`
	AzureDevOpsCapabilities *[]adoscope.Capability `json:"azure_devops_capabilities,omitempty"`
}

CeilingOverlay is the part of a RunPolicySpec a composed governance profile narrows, with PRESENCE typed: every field is a pointer, so "the overlay says nothing about this" (nil, inherit the base) and "the overlay says this is empty" (a pointer to an empty list, a value) can never be confused. That distinction is load-bearing: allowed_domains [] narrows to no domains, while allowed_methods [] would read as "all methods" in the proxy and is refused.

It mirrors RunPolicySpec field for field (composer's coverage guard fails on a field of either that the other lacks). Decoded strictly: an unknown key is an error, never a silently ignored narrowing.

func DecodeCeilingOverlay added in v0.8.6

func DecodeCeilingOverlay(raw []byte) (CeilingOverlay, error)

DecodeCeilingOverlay strictly decodes a stored or submitted overlay.

type ConfinementClass

type ConfinementClass string

ConfinementClass declares how strongly a sandbox substrate confines an agent; policy may refuse high-trust credential scopes to weak classes. See threatmodel/.

const (
	// CC1: hardened shared-kernel runc (userns, seccomp, AppArmor, cap-drop).
	CC1 ConfinementClass = "CC1"
	// CC2: gVisor userspace kernel (the default — needs a native Docker engine with runsc registered).
	CC2 ConfinementClass = "CC2"
	// CC3: Kata microVM (requires /dev/kvm).
	CC3 ConfinementClass = "CC3"
)

func (ConfinementClass) Rank

func (c ConfinementClass) Rank() int

Rank orders classes weakest→strongest; unrecognised values rank 0 so a minimum-class gate fails closed. Matching is exact — normalise untrusted input first.

type CredentialGrant

type CredentialGrant struct {
	ID        uuid.UUID `json:"id"`
	RunID     uuid.UUID `json:"run_id"`
	CreatedAt time.Time `json:"created_at"`
	Spec      GrantSpec `json:"spec"`
}

CredentialGrant records what a run is ELIGIBLE for — not issuance: minting happens only via the broker, and for RequiresApproval grants only inside the transaction that verifies an APPROVED ApprovalRequest for this run+scope.

type CredentialSource added in v0.7.2

type CredentialSource string

CredentialSource says WHOSE credential a git provider's lanes use.

const (
	// CredentialSourceShared: one credential, captured by an admin, backs
	// every run. The zero value, so an unset field keeps legacy behaviour.
	CredentialSourceShared CredentialSource = "shared"
	// CredentialSourcePerUser: one credential per principal, captured or
	// stored under their own namespace.
	CredentialSourcePerUser CredentialSource = "per_user"
)

func (CredentialSource) Valid added in v0.7.2

func (s CredentialSource) Valid() bool

Valid reports whether s is one of the two sources.

type Delegate added in v0.8.0

type Delegate struct {
	ID          uuid.UUID `json:"id"`
	Name        string    `json:"name"`
	IdPClientID string    `json:"idp_client_id"`
	Group       string    `json:"group"`
	// Never serialized: a credential hash has no business leaving the process.
	CredentialSHA256 string     `json:"-"`
	RegisteredBy     string     `json:"registered_by"`
	CreatedAt        time.Time  `json:"created_at"`
	LastUsedAt       *time.Time `json:"last_used_at,omitempty"`
	RevokedAt        *time.Time `json:"revoked_at,omitempty"`
	// Credential is the plaintext portal credential, on the register
	// response ONLY; the row stores its hash.
	Credential string `json:"credential,omitempty"`
}

Delegate is a registered portal (migration 0094, #1142): a trusted front-end that may exchange a signed-in person's own identity-provider token for a short delegated token acting for that person. IdPClientID is the portal's own client id at the identity provider, which a subject token must name; Group is the one group a person must be in for the portal to act for them. Revoking the row (RevokedAt) ends every delegated token it minted at once.

type DelegatedToken added in v0.8.0

type DelegatedToken struct {
	ID              uuid.UUID `json:"id"`
	DelegateID      uuid.UUID `json:"delegate_id"`
	Principal       string    `json:"principal"`
	Email           string    `json:"email,omitempty"`
	UserType        string    `json:"user_type"`
	Groups          []string  `json:"groups"`
	GroupsTruncated bool      `json:"groups_truncated"`
	CreatedAt       time.Time `json:"created_at"`
	ExpiresAt       time.Time `json:"expires_at"`
}

DelegatedToken is one live token a Delegate was handed for one person: the identity it replays (always at the user role — a portal never carries admin reach) and when it stops working. Stored only by hash, never refreshed.

type DelegationVia added in v0.8.0

type DelegationVia struct {
	Delegate uuid.UUID `json:"delegate"`
	Grant    uuid.UUID `json:"grant"`
}

DelegationVia names the portal and the delegated token an act was performed through. It is the audit row's data.via and an attach ticket's replayed origin; nil everywhere a person acted for themselves.

type Device added in v0.8.0

type Device struct {
	ID   uuid.UUID `json:"id"`
	Name string    `json:"name"`
	// Never serialized: a credential hash has no business leaving the process.
	CredentialSHA256 string     `json:"-"`
	EnrolledBy       string     `json:"enrolled_by"`
	CreatedAt        time.Time  `json:"created_at"`
	LastSeenAt       *time.Time `json:"last_seen_at,omitempty"`
	RevokedAt        *time.Time `json:"revoked_at,omitempty"`
	LastSeq          int64      `json:"last_seq"`
	LastRowHash      string     `json:"last_row_hash,omitempty"`
}

Device is one enrolled laptop daemon: an inventory row binding a revocable bearer credential to that device's own audit-federation progress. CredentialSHA256 never reaches a row unhashed; a revoked credential's WHERE RevokedAt IS NULL makes it answer exactly like an unknown one (no oracle). LastSeq/LastRowHash are the chain cursor as ingested so far, verified against each new batch's claimed PrevHash before advancing atomically.

type DeviceAck added in v0.8.0

type DeviceAck struct {
	AckedSeq int64 `json:"acked_seq"`
}

type DeviceEnrolRequest added in v0.8.0

type DeviceEnrolRequest struct {
	Token string `json:"token"`
}

DeviceEnrolRequest/DeviceEnrolResponse are POST /devices/enrol's wire shapes; DeviceAck is the ingest/heartbeat routes' recorded-cursor answer.

type DeviceEnrolResponse added in v0.8.0

type DeviceEnrolResponse struct {
	DeviceID uuid.UUID `json:"device_id"`
	Name     string    `json:"name"`
	Token    string    `json:"token"`
}

type DeviceEnrolmentToken added in v0.8.0

type DeviceEnrolmentToken struct {
	ID          uuid.UUID  `json:"id"`
	TokenSHA256 string     `json:"-"`
	DeviceName  string     `json:"device_name"`
	MintedBy    string     `json:"minted_by"`
	CreatedAt   time.Time  `json:"created_at"`
	ExpiresAt   time.Time  `json:"expires_at"`
	ConsumedAt  *time.Time `json:"consumed_at,omitempty"`
	Token       string     `json:"token,omitempty"` // plaintext, mint response ONLY
}

DeviceEnrolmentToken is a single-use admin-minted token a laptop's first boot exchanges for a Device credential. ConsumeEnrolmentToken's conditional UPDATE ... RETURNING (WHERE ConsumedAt IS NULL) makes redemption single-use.

type DriveBackend added in v0.7.0

type DriveBackend string

DriveBackend names WHERE a drive's bytes live and therefore WHICH runner can mount it. Closed and complete; migration 0054's user_drives.backend CHECK is pinned against these constants.

const (
	DriveBackendDockerVolume DriveBackend = "docker_volume" // per-user Docker named volume, created on first use. Managed, Docker only.
	// DriveBackendHostPath binds <host_root>/<home> from a tree the OPERATOR
	// mounted host-side; Wardyn never holds a share credential. Share, Docker only.
	DriveBackendHostPath DriveBackend = "host_path"
	DriveBackendK8sPVC   DriveBackend = "k8s_pvc" // per-user dynamically-provisioned PVC. Managed, Kubernetes only.
	// DriveBackendK8sPVCStatic is an admin-precreated PVC; Wardyn only ever
	// READS it, a missing claim is a refusal not a create. Share, Kubernetes only.
	DriveBackendK8sPVCStatic DriveBackend = "k8s_pvc_static"
)

func (DriveBackend) Kind added in v0.7.0

func (b DriveBackend) Kind() DriveKind

Kind derives the managed/share split from the backend. An unknown backend reads as a SHARE — the conservative half, since Wardyn never creates or deletes a share.

func (DriveBackend) RunnerTarget added in v0.7.0

func (b DriveBackend) RunnerTarget() string

RunnerTarget is the ONE Config.RunnerTarget value that can mount this backend, or "" — lets the write boundary refuse a k8s drive on a Docker deployment (400) rather than store a row that can never resolve.

func (DriveBackend) Valid added in v0.7.0

func (b DriveBackend) Valid() bool

Valid reports whether b is one of the four backends, mirroring CapabilitySubjectType.Valid.

type DriveKind added in v0.7.0

type DriveKind string

DriveKind is the managed-vs-share split. DERIVED from the backend and never stored: two columns that must agree are two columns that can disagree.

const (
	DriveKindManaged DriveKind = "managed" // Wardyn allocates the object for the member
	DriveKindShare   DriveKind = "share"   // the object exists already and Wardyn only binds it
)

type DriveMount added in v0.7.0

type DriveMount struct {
	// DriveID is the user_drives row this mount came from; labels and reclaim
	// sweeps group by it.
	DriveID    uuid.UUID    `json:"drive_id"`
	Backend    DriveBackend `json:"backend"`
	ObjectName string       `json:"object_name"` // what the substrate is asked for — volume name, PVC name, or absolute host path
	// HostRoot is THIS drive's own share root, carried on a host_path mount
	// so the driver can assert the bind stays inside it. Does NOT replace the
	// deployment's WARDYN_USER_DRIVE_HOST_ROOTS ceiling (that bounds EVERY
	// drive at once; this refuses a link into a different drive's tree). The
	// driver asserts BOTH.
	HostRoot string `json:"host_root,omitempty"`
	// DriveName is the drive's human name, carried for the AUDIT row alone so
	// a member learns which drive without the operator's absolute host path.
	DriveName    string `json:"drive_name,omitempty"`
	StorageClass string `json:"storage_class,omitempty"` // provisioner a managed k8s_pvc claim asks for; carried since a substrate never reads the database
	HomeName     string `json:"home_name"`               // per-user segment ObjectName was built from
	// SubjectHash is the non-PII fingerprint of the principal this mount was
	// resolved for. DEFENCE IN DEPTH over the managed-backend collision the
	// write boundary refuses: a stored row predating that refusal can still
	// resolve two principals onto one volume, so the driver stamps this and
	// refuses the second. A digest, never the claim — a label is echoed by
	// `docker volume inspect` to anybody who can reach the daemon.
	SubjectHash string
	Target      string             `json:"target"` // reserved in-container path (runner.DriveTarget), carried so a runner never hard-codes it
	ReadOnly    bool               `json:"read_only,omitempty"`
	SizeMiB     int                `json:"size_mib,omitempty"`
	Enforcement StorageEnforcement `json:"enforcement"`
}

DriveMount is the RESOLVED answer the runner acts on: one principal's drive, folded with grant overrides and narrowed by their run request, in the shape a substrate can execute without re-reading a row.

A SEPARATE type from ResolvedDrive: that one is admin-facing and carries the whole drive/grant rows; this is run-facing and carries only what a mount needs. ReadOnly rather than Writable, since every mount type the substrates speak says ReadOnly. Does NOT ride RunPolicySpec.WorkspaceMounts: a drive is per-PRINCIPAL and a workspace mount is per-ROW.

type DriveObjectScheme added in v0.8.0

type DriveObjectScheme string

DriveObjectScheme names which half of DriveObjectName's minted form carries the drive — the variable-width slug (migration 0054, `wardyn-drive-<drive-slug>-<home>`) or the drive's own fixed-width id (migration 0067, `wardyn-drive-<drive-id-hex>-<home>`). Closed; pinned against user_drives.object_scheme's CHECK.

IT IS NOT CLIENT-AUTHORED. store.UpsertUserDrive derives it entirely and never reads this field off the request — neither substrate can rename a storage object, so a row keeps whatever scheme it was created under. It rides the wire only so a GET and its round-tripped PUT agree; driveIdentityFields compares it, so a forged flip answers 409.

const (
	// DriveObjectSchemeSlug is the original form: DriveSlug at a variable
	// offset before <home>. The column's DEFAULT; every pre-0067 row reads as
	// this and stays this permanently (DriveSlug is not injective).
	DriveObjectSchemeSlug DriveObjectScheme = "slug"
	// DriveObjectSchemeID is the drive's own UUID, dashless (DriveObjectID),
	// at a FIXED offset before <home>. Every row created from 0067 onward
	// gets this unconditionally.
	DriveObjectSchemeID DriveObjectScheme = "id"
)

func (DriveObjectScheme) Valid added in v0.8.0

func (s DriveObjectScheme) Valid() bool

Valid reports whether s is one of the two schemes.

type DriveReclaim added in v0.7.0

type DriveReclaim string

DriveReclaim is the DECLARED INTENT for a drive's objects when a grant goes away. v1 executes it by documented operator command, not by code: nothing in the control plane deletes a volume or a PVC.

const (
	DriveReclaimRetain DriveReclaim = "retain" // keeps the object after the grant is removed — the default
	DriveReclaimDelete DriveReclaim = "delete" // records that the object SHOULD be reclaimed — a runbook note, not an action
)

func (DriveReclaim) Valid added in v0.7.0

func (r DriveReclaim) Valid() bool

Valid reports whether r is one of the two reclaim intents.

type EgressRedirect added in v0.5.0

type EgressRedirect struct {
	// From is the public/upstream URL or host being redirected away from.
	From string `json:"from"`
	// To is the corporate-internal URL or host every matching run's egress
	// substitutes to; for an Ecosystem row it's also emitted into that
	// ecosystem's config file (the old ArtifactOverride.BaseURL).
	To string `json:"to"`
	// TokenSecretRef optionally names a secret injected proxy-side as a
	// Bearer token for To's host (the sandbox never holds it). Mutually
	// exclusive with TokenIntegrationRef.
	TokenSecretRef string `json:"token_secret_ref,omitempty"`
	// TokenIntegrationRef optionally names an Integration
	// (SiteConfig.Integrations[i].ID) to take this redirect's token FROM
	// instead of a bare secret. The redirect points AT the integration
	// rather than restating its secret, inheriting its Header/Format so a
	// feed authenticating with something other than "Authorization: Bearer"
	// finally can. Naming nothing, a disabled row, or one with no header
	// credential degrades to redirect-without-token.
	TokenIntegrationRef string `json:"token_integration_ref,omitempty"`
	// Ecosystem, when set, is one of six package-manager ecosystems
	// ("npm"|"pip"|"cargo"|"maven"|"go"|"nuget") this redirect ALSO emits a
	// config file for. Empty means NETWORK-ONLY: no config file is emitted.
	Ecosystem string `json:"ecosystem,omitempty"`
}

EgressRedirect is one outbound redirect: requests to From are substituted to To (From's host dropped from egress, To's host allowed), with an optional token injected proxy-side for To's host. From/To share ArtifactOverride.BaseURL's validation discipline — keep any future change there in sync, since workspacescan.EmitArtifactConfig relies on it for raw string interpolation into .npmrc/settings.xml/etc.

type EphemeralProvider added in v0.7.2

type EphemeralProvider struct {
	DefaultDiskMiB int `json:"default_disk_mib,omitempty"` // 0 = unset
	// MaxDiskMiB: ceiling a larger request is clamped to (0 = unlimited); a
	// DefaultDiskMiB above it is refused at the write boundary.
	MaxDiskMiB int `json:"max_disk_mib,omitempty"`
}

EphemeralProvider is the ephemeral-scratch policy: default size, and the ceiling a larger request is clamped to (never refused).

type FederatedAuditEvent added in v0.8.0

type FederatedAuditEvent struct {
	AuditEvent
	Seq int64 `json:"seq"`
}

FederatedAuditEvent is one audit row plus Seq, its local table's own sequence for the row. Seq alone is not identity — a table reset can restart it — so a replay is recognised by seq together with RowHash.

type FirstUseMode

type FirstUseMode string

FirstUseMode controls how the egress proxy handles an UNKNOWN domain — one that is neither explicitly allowed nor denied — under an allowlist policy. It widens the legacy first_use_approval boolean into three explicit modes while staying wire-compatible: UnmarshalJSON still accepts the old bool (true => deny_with_review, false => always_deny). Inert under allow-all egress.

const (
	// FirstUseAlwaysDeny hard-denies an unknown domain and logs it, without ever
	// raising it for human approval. (legacy first_use_approval=false)
	FirstUseAlwaysDeny FirstUseMode = "always_deny"
	// FirstUseDenyWithReview raises a pending approval and denies the in-flight
	// request immediately; once a human approves, a later retry passes. The
	// sandbox connection is never held open. (legacy first_use_approval=true)
	FirstUseDenyWithReview FirstUseMode = "deny_with_review"
	// FirstUseWaitForReview raises a pending approval and HOLDS the connection
	// open until it is approved/denied or the proxy's hold deadline passes — the
	// request transparently completes if approved in time. On deadline it fails
	// closed (403) with the approval left pending, degrading to deny_with_review.
	FirstUseWaitForReview FirstUseMode = "wait_for_review"
)

func (FirstUseMode) Normalize

func (m FirstUseMode) Normalize() FirstUseMode

Normalize maps an empty or unrecognised value to always_deny (fail closed, matching the legacy boolean zero value).

func (FirstUseMode) RaisesApproval

func (m FirstUseMode) RaisesApproval() bool

RaisesApproval reports whether an unknown domain is escalated to a human rather than hard-denied (true for both review modes).

func (*FirstUseMode) UnmarshalJSON

func (m *FirstUseMode) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts BOTH the legacy boolean form (true => deny_with_review, false => always_deny) and the new string form, so old stored policies whose first_use_approval is a JSON boolean keep decoding without a migration.

func (FirstUseMode) Valid

func (m FirstUseMode) Valid() bool

Valid reports whether m is empty (unset => default always_deny) or one of the three known modes. Used to reject a hand-authored policy with a garbage value at the API boundary; runtime reads still fail closed via Normalize.

type GitLane added in v0.7.2

type GitLane string

GitLane is one of the credential lanes a run can clone with. A provider row's Lanes only veto lanes; they never mint or create a new credential.

const (
	GitLaneApp   GitLane = "app"   // GitHub App broker lane (github.com only)
	GitLanePAT   GitLane = "pat"   // stored git-pat-<slug> lane
	GitLaneSSH   GitLane = "ssh"   // stored ssh-key-<slug> lane
	GitLaneEntra GitLane = "entra" // Microsoft Entra lane: run authorizes as the person, against the org's own tenant; Azure DevOps hosted only
)

func (GitLane) Legacy added in v0.7.10

func (l GitLane) Legacy() bool

Legacy reports whether l is admitted by an empty Lanes list; expanding one must ask this, never ClosedGitLanes.

func (GitLane) Valid added in v0.7.2

func (l GitLane) Valid() bool

Valid reports whether l is one of the closed lanes.

type GitPATScope added in v0.8.6

type GitPATScope struct {
	Host       string    `json:"host"`
	SecretName string    `json:"secret_name"`
	Username   string    `json:"username,omitempty"`
	Repos      *[]string `json:"repos,omitempty"`
	Access     string    `json:"access,omitempty"`
	API        bool      `json:"api,omitempty"`
	Forge      string    `json:"forge,omitempty"`
}

GitPATScope is the one decoded shape of a git_pat grant scope. api, broker and composer all read it, so the contract is defined once.

The four narrowing axes have explicit omission defaults (Normalize):

repos   absent = every repository the PAT reaches; an EMPTY list = none
access  write
api     false
forge   generic

Repos is a pointer to a slice so "absent" (nil) and "empty" (non-nil, no entries) stay distinct. Reading an empty list as unset would turn an empty intersection into an unnarrowed grant.

Wardyn narrows the run, not the PAT: the credential itself keeps whatever scope the forge issued it with.

func DecodeGitPATScope added in v0.8.6

func DecodeGitPATScope(raw json.RawMessage) (GitPATScope, error)

DecodeGitPATScope is the lenient read: unknown keys are ignored, so a stored row with a stray key still loads and launches. host and secret_name are required (fail closed). The result is normalized.

func DecodeGitPATScopeStrict added in v0.8.6

func DecodeGitPATScopeStrict(raw json.RawMessage) (GitPATScope, error)

DecodeGitPATScopeStrict is the write-time decode, for request bodies and the boot policy file. Beyond DecodeGitPATScope it refuses an unknown key (a typo such as "repo" would otherwise read as an omission, and an omission means unnarrowed), an access or forge outside its enum, a malformed repos entry, and api: true on the generic forge, which has no API table. Whether a Bitbucket Server API grant is allowed is a deployment flag, which the api package checks at write.

func (GitPATScope) Narrowed added in v0.8.6

func (s GitPATScope) Narrowed() bool

Narrowed reports whether the scope narrows the run below the PAT: it sets repos (an empty list included), sets access read, or sets api true. Call it on a normalized scope.

func (GitPATScope) Normalize added in v0.8.6

func (s GitPATScope) Normalize() GitPATScope

Normalize applies the omission defaults and fails closed on a value outside an enum: an unknown access reads as read, an unknown forge as generic (the strictest path identity). Strict decoding refuses both before they can be stored; this only matters for a row that predates the check. Repos is returned as is, because absent and empty mean different things.

func (GitPATScope) SetsAnyAxis added in v0.8.6

func (s GitPATScope) SetsAnyAxis() bool

SetsAnyAxis is Narrowed, or a forge other than generic: the test for a scope a lane that ignores the four axes must refuse rather than carry silently.

type GitProvider added in v0.7.2

type GitProvider struct {
	ID   string          `json:"id"`   // operator-chosen stable slug, unique within Git, held to integrationRefRE; a capability grant names a provider by this id
	Kind GitProviderKind `json:"kind"` // which forge's bespoke behaviour this row carries
	// Disabled turns the row off without deleting it (zero value enabled); a
	// disabled row still claims its hosts rather than falling through to ScmHosts.
	Disabled bool `json:"disabled,omitempty"`
	// BaseURLs are the https addresses this row admits: a repo is admitted
	// when its clone URL's scheme/host match one of these and its path equals
	// or extends that base URL's path at a "/" boundary. 1-8 entries; no
	// port, credentials, query or fragment; at most two path segments.
	BaseURLs []string `json:"base_urls"`
	// Lanes are the credential lanes usable for this provider; empty means
	// every legacy lane (LegacyGitLanes) — the field narrows, never widens.
	Lanes            []GitLane        `json:"lanes,omitempty"`
	CredentialSource CredentialSource `json:"credential_source,omitempty"` // whose credential the lanes use; empty = CredentialSourceShared, CredentialSourcePerUser requires GitLaneEntra, or GitLanePAT on an Azure DevOps row (a Server row, which has no Entra sign-in)
	Entra            *ADOEntraConfig  `json:"entra,omitempty"`             // present only on, and required by, a row whose Lanes name GitLaneEntra
}

GitProvider is one git-provider row: a kind, the base URLs it admits, and the credential lanes it permits.

type GitProviderKind added in v0.7.2

type GitProviderKind string

GitProviderKind is the closed set of git forges Wardyn has bespoke behaviour for; a self-hosted forge is a github (GHES) or azure_devops (ADO Server) row, not a third kind.

const (
	GitProviderGitHub      GitProviderKind = "github"       // github.com or GitHub Enterprise Server
	GitProviderAzureDevOps GitProviderKind = "azure_devops" // dev.azure.com, org.visualstudio.com, or ADO Server
)

func (GitProviderKind) Valid added in v0.7.2

func (k GitProviderKind) Valid() bool

Valid reports whether k is one of the closed kinds.

type GovernanceAssignment added in v0.7.0

type GovernanceAssignment struct {
	ID          uuid.UUID             `json:"id"`
	SubjectType CapabilitySubjectType `json:"subject_type"`
	Subject     string                `json:"subject"`
	ProfileID   uuid.UUID             `json:"profile_id"`
	Priority    int                   `json:"priority"`
	CreatedAt   time.Time             `json:"created_at"`
	CreatedBy   string                `json:"created_by,omitempty"`
}

GovernanceAssignment binds one profile to one subject (migration 0052's governance_assignments row).

SubjectType REUSES CapabilitySubjectType — the same vocabulary capability_grants is written against; a second enum for the same thing is the dual-matcher drift this codebase warns against elsewhere.

Priority breaks ties WITHIN a tier (higher wins), the group tier's working lever since a member is often in several groups. Never crosses tiers: a user-tier row beats every group-tier row at any priority.

type GovernanceChange added in v0.8.6

type GovernanceChange struct {
	ID         uuid.UUID       `json:"id"`
	TargetKind string          `json:"target_kind"`
	Op         string          `json:"op"`
	TargetKey  string          `json:"target_key"`
	State      string          `json:"state"`
	ProposedBy string          `json:"proposed_by"`
	ProposedAt time.Time       `json:"proposed_at"`
	ExpiresAt  time.Time       `json:"expires_at"`
	Diff       json.RawMessage `json:"diff"`
	DecidedBy  string          `json:"decided_by,omitempty"`
	DecidedAt  *time.Time      `json:"decided_at,omitempty"`
	// Reason is a rejection's optional explanation.
	Reason string `json:"reason,omitempty"`

	// Payload, hashes and emails stay server-side.
	Payload         json.RawMessage `json:"-"`
	BaseHash        string          `json:"-"`
	DeploymentHash  string          `json:"-"`
	ProposedByEmail string          `json:"-"`
	DecidedByEmail  string          `json:"-"`
}

GovernanceChange is one governance write held for a second human (migration 0126). Payload is the validated request, replayed on approval; Diff is the server-rendered before/after the reviewer reads. Neither carries a secret value.

The four personal fields (ProposedBy, ProposedByEmail, DecidedBy, DecidedByEmail) are cleared by the audit_personal_fields erasure scope. The emails never reach a response: they exist for the distinct-human comparison and the erasure match only.

type GovernanceLimits added in v0.7.0

type GovernanceLimits struct {
	// DenyTaskModeExec refuses task_mode=exec for a member under this profile.
	DenyTaskModeExec bool `json:"deny_task_mode_exec,omitempty"`
	// DenyInteractive refuses an interactive run under this profile.
	// Evaluated against POST-COERCION interactivity, since omitting the
	// task coerces to interactive later — a raw "did the caller ask" check
	// would be evaded by leaving the field out. It also refuses the terminal
	// attach and SSH into a run created under this profile, since a task or
	// exec run still has a sandbox to open a shell in (#1392).
	DenyInteractive bool `json:"deny_interactive,omitempty"`
	// DenyUIApps strips ui_apps from a run under this profile at create, and
	// the UI gateway refuses a session into a run created under it. A limit
	// rather than an empty ceiling ui_apps, which stays "no opinion" so
	// existing profiles keep their behaviour (#1391).
	DenyUIApps bool `json:"deny_ui_apps,omitempty"`
	// MaxConcurrentRuns caps how many NON-TERMINAL runs a member under this
	// profile may hold at once. 0 is unlimited.
	MaxConcurrentRuns int `json:"max_concurrent_runs,omitempty"`
	// DenyUserDrive refuses a USER DRIVE mount under this profile, whatever
	// an admin allocated. A DOOR, NOT A QUOTA: a drive outlives the run, so
	// "may this principal persist anything at all" is the question (403 +
	// authz.denied), not "how big".
	DenyUserDrive bool `json:"deny_user_drive,omitempty"`
	// MaxCPUMillis and MaxMemoryMiB cap the sandbox's CPU (millicores) and memory a run under this
	// profile may be given, whatever its ceiling's own resources or a member's request say. 0 is
	// unlimited. A CLAMP, NOT A DOOR, like MaxEphemeralDiskMiB. Folded into composer.Clamp at
	// create and preflight and re-applied at dispatch; assigned members only, operators exempt.
	MaxCPUMillis int `json:"max_cpu_millis,omitempty"`
	MaxMemoryMiB int `json:"max_memory_mib,omitempty"`
	// MaxEphemeralDiskMiB caps the EPHEMERAL scratch (the writable layer when no drive is
	// mounted) a run under this profile may be given. 0 is unlimited. A CLAMP, NOT A DOOR: no
	// authz.denied at the bound. Enforced at dispatch only (runs_dispatch.go), folded with the
	// deployment's own storage.ephemeral.max_disk_mib; assigned members only, operators exempt.
	// Whether it actually binds depends on the substrate (Docker's overlay2 doesn't enforce
	// size at all) — render through StorageEnforcement, never claim a cap the substrate doesn't keep.
	MaxEphemeralDiskMiB int `json:"max_ephemeral_disk_mib,omitempty"`
	// MaxDriveSizeMiB caps how large a USER DRIVE may be under this profile. 0 is unlimited —
	// DenyUserDrive's twin ("may persist" vs "how much"). PER PRINCIPAL: a drive is one tree
	// belonging to one subject; a team-shared drive's ceiling is not the sum of its members'
	// and can't be derived from one, so sharing (0.8) adds a scope axis instead. Clamped in
	// newResolvedDrive, the one place holding both facts — never at grant write, where the
	// subject is claims-resolved and unreadable from the row. Folds with
	// storage.user_drive.max_size_mib in one min(), so launch, /me and preview can't disagree.
	MaxDriveSizeMiB int `json:"max_drive_size_mib,omitempty"`
	// AutonomyRubric maps a run's posture to a permitted AutonomyLevel
	// (0.8, #77). A POINTER so an unset rubric doesn't put
	// `"autonomy_rubric":{}` on every profile's wire body. Nil means "no
	// rubric" (resolveRunAutonomy, #97, treats it like no assigned profile).
	AutonomyRubric *AutonomyRubric `json:"autonomy_rubric,omitempty"`
	// RunLimits is embedded, so its seven fields sit flat on the limits wire
	// object beside the ones above — the same field set a run captures at
	// create (AgentRun.RunLimits).
	RunLimits
}

GovernanceLimits carries the autonomy switches that BOUND A REQUEST rather than a policy (RunPolicySpec bounds what a sandbox may reach once running, enforced by the proxy). These refuse a run SHAPE before it exists, or (DenyUserDrive) leave state behind:

  • DenyTaskModeExec: task_mode=exec has no agent and no toolgate, so no tool_rules ceiling binds it.
  • DenyInteractive: an interactive run refuses tool_approvals=hold by design, so tool_rules can't express "supervised" there either.
  • DenyUIApps: a UI app is relay access into the sandbox that no tool_rules ceiling sees.
  • DenyUserDrive: a drive OUTLIVES the run, so no tool_rules ceiling describes the escape — the storage itself is the escape.
  • MaxConcurrentRuns is a QUOTA, not a door: 422 with no authz.denied, not 403.
  • MaxCPUMillis/MaxMemoryMiB/MaxEphemeralDiskMiB/MaxDriveSizeMiB are neither: they CLAMP — capped and told so, never refused (disk_mib is policy-authored, so refusing would break every stored policy the day a limit is first written).

A CLOSED struct with `omitempty` on every field, not a map: small, complete, validated by the Go type itself, no DB CHECK needed. Every zero value means "unrestricted", so an omitted limits object behaves exactly as before this struct existed.

type GovernanceProfile added in v0.7.0

type GovernanceProfile struct {
	ID        uuid.UUID        `json:"id"`
	Name      string           `json:"name"`
	Ceiling   RunPolicySpec    `json:"ceiling"`
	Limits    GovernanceLimits `json:"limits"`
	CreatedAt time.Time        `json:"created_at"`
	UpdatedAt time.Time        `json:"updated_at"`
	CreatedBy string           `json:"created_by,omitempty"`
	// Contact is who owns the profile and how to ask for a change; nil is none.
	// Validated on write and re-validated on every read (internal/policyref).
	// Never inherited from a base: disclosing a base's contact would disclose the base.
	Contact *policyref.Contact `json:"contact,omitempty"`
	// ContactSet is a write instruction, never stored or serialized: true makes an
	// upsert write Contact (nil clears it), false keeps the stored value.
	ContactSet bool `json:"-"`
	// BaseProfileID is the profile this one composes on; nil with a non-nil Overlay means the
	// deployment default. Only a composed profile has one.
	BaseProfileID *uuid.UUID `json:"base_profile_id,omitempty"`
	// Overlay marks the profile composed. A non-nil pointer to an empty overlay is valid: it is the
	// base unchanged.
	Overlay *CeilingOverlay `json:"overlay,omitempty"`
	// OverlayLimits narrows the base's limits. Only with an Overlay; nil is no narrowing.
	OverlayLimits *LimitsOverlay `json:"overlay_limits,omitempty"`
}

GovernanceProfile is one named, assignable ceiling (the governance_profiles row of migrations 0052 and 0125).

A STANDALONE profile (Overlay nil) is a full RunPolicySpec with replacement semantics: an assigned profile IS the principal's ceiling; with no assignment a principal falls through to Config.DefaultPolicy byte for byte. A COMPOSED profile (Overlay set) stores no raw Ceiling or Limits, only a base (BaseProfileID, or the deployment default when nil) and an overlay that can only narrow it; the effective ceiling is composer.ApplyOverlay over the resolved base, computed whenever authority is read (internal/api/governance_compose.go is the only reader). composer.Clamp is NOT that meet (it drops workspace_mounts unconditionally, is order-dependent through llm_inspection and defaults unnamed tools to hold), so composition has its own function.

Name is the UNIQUE human handle: what an admin assigns by, what the console lists, and the resolver's last ORDER BY tie-break.

func (GovernanceProfile) Composed added in v0.8.6

func (p GovernanceProfile) Composed() bool

Composed reports whether the profile is a base plus an overlay rather than a standalone ceiling.

type GrantKind

type GrantKind string

GrantKind enumerates broker-mintable credential kinds.

const (
	GrantGitHubToken GrantKind = "github_token"
	GrantCloudSTS    GrantKind = "cloud_sts" // HARD-REQUIRES SPIRE identity provider
	GrantAPIKey      GrantKind = "api_key"   // proxy-side injection only
	// GrantGitPAT returns a stored PAT to the git credential helper for a non-GitHub host (ADO/GitLab), an opaque CONNECT tunnel the proxy can't inject into.
	GrantGitPAT GrantKind = "git_pat"
	// GrantSSHKey materializes a resident, agent-readable SSH private key for git-over-SSH — a DOCUMENTED EXCEPTION to the no-resident-secret invariant
	// (no credential-helper seam for SSH). agent-run writes it 0400 just before the clone and wipes it right after; residual risk is code running as the
	// agent uid during that window. See broker.mintSSHKey.
	GrantSSHKey GrantKind = "ssh_key"
	// GrantEnvSecret places a stored secret into the sandbox environment at dispatch — the second DOCUMENTED EXCEPTION to the no-resident-secret invariant.
	// Unlike every other kind: NOT BROKERED (no mint/approval/TTL/JTI); RESIDENT FOR THE WHOLE RUN (readable via /proc/self/environ as the agent uid);
	// NO REVOCATION (killing the run doesn't un-disclose it). ADMIN-ONLY by default — see threatmodel/THREAT-MODEL.md §5.1a.
	GrantEnvSecret GrantKind = "env_secret"
)

type GrantSpec

type GrantSpec struct {
	Kind GrantKind `json:"kind"`
	// Scope is kind-specific: {repos,permissions} for github_token; {host,header} for api_key;
	// {host,secret_name,username?} for git_pat; {host,key_secret_ref,username?,known_hosts_secret_ref?} for ssh_key.
	Scope json.RawMessage `json:"scope"`
	// TTL of the minted credential. Max (and default) 1h.
	TTLSeconds int `json:"ttl_seconds,omitempty"`
	// RequiresApproval forces a human approval to mint (vs auto-mint on policy).
	RequiresApproval bool `json:"requires_approval"`
	OwnerOnly        bool `json:"owner_only,omitempty"` // stored secret from the run owner's own row only, never the operator's
}

GrantSpec is a credential scope description. The broker enforces the invariant: a minted credential's scope is exactly the approved scope — never wider.

type HomeTemplate added in v0.7.0

type HomeTemplate string

HomeTemplate names WHICH identity a drive's per-user home name is derived from. Closed; pinned against user_drives.home_template's CHECK.

const (
	// HomeTemplateHash is `d-` + a truncated sha256 over the drive id and the
	// subject: deterministic, DNS-1123-safe, leaks no identity. The default,
	// and the ONLY template a MANAGED backend may use — ValidateUserDrive
	// refuses the others on managed.
	HomeTemplateHash HomeTemplate = "hash"
	// HomeTemplateSub uses the sign-in subject claim verbatim, lowercased.
	// SHARE backends only: two subjects differing only in case collapse to
	// one home.
	HomeTemplateSub HomeTemplate = "sub"
	// HomeTemplateEmailLocal uses the part of the email claim before the "@"
	// (alice@corp -> alice). SHARE backends only: the domain is dropped, so
	// alice@corp.example and alice@partner.example name ONE directory — the
	// answer is a home_override on the colliding person's allocation.
	//
	// THERE IS DELIBERATELY NO WHOLE-EMAIL TEMPLATE: driveHomeSegmentRe
	// excludes "@", so it could only ever resolve for a claim that was not an
	// address; anything else is a per-user home_override.
	HomeTemplateEmailLocal HomeTemplate = "email_local"
)

func (HomeTemplate) Valid added in v0.7.0

func (t HomeTemplate) Valid() bool

Valid reports whether t is one of the three templates.

type Integration added in v0.5.0

type Integration struct {
	// ID is an operator-chosen stable slug, unique within
	// SiteConfig.Integrations (no generated uuid — egress redirects'
	// token_integration_ref and workspace requirements point at it).
	ID   string `json:"id"`
	Name string `json:"name"`
	// Kind is the ONE field saying what this connects to: a closed-set kind
	// (ClosedIntegrationKinds) with bespoke behavior, or any other slug (generic).
	Kind string `json:"kind"`
	// Disabled turns the integration off without deleting its configuration.
	Disabled bool `json:"disabled,omitempty"`
	// Secrets are the required secrets, each with its delivery; refs/names only.
	Secrets []IntegrationSecret `json:"secrets,omitempty"`
	// Egress is WHERE THE SYSTEM LIVES: hosts this integration may reach
	// (proxy.ValidDomainEntry syntax: exact host or leading-"*." wildcard,
	// optionally ":port"). TRUST BOUNDARY: a wildcard opens the path but
	// never carries the credential — proxy-side injection needs an exact
	// entry, so a proxy_header secret is rejected at write time if any
	// egress entry is a wildcard or has a port.
	Egress []string `json:"egress,omitempty"`
	// Config is non-secret, kind-validated configuration; closed kinds accept
	// only known keys (unknown 400s by name at write), generic kinds take any
	// string keys.
	Config map[string]any `json:"config,omitempty"`
	Docs   string         `json:"docs,omitempty"`
	// DisabledCapabilities lists capability names this integration does NOT support.
	DisabledCapabilities []string  `json:"disabled_capabilities,omitempty"`
	CreatedAt            time.Time `json:"created_at"`
	UpdatedAt            time.Time `json:"updated_at"`
	// contains filtered or unexported fields
}

Integration is one operator-configured external connection: a base component (required secrets each with its delivery, required egress, non-secret config) extended by kind. Secrets holds names, never values; the broker/proxy resolve the named secret at dispatch time. UnmarshalJSON also accepts the pre-base-component shape, folding it forward one-way (write-new; see foldLegacyIntegration).

func (Integration) CredentialsMap added in v0.5.0

func (in Integration) CredentialsMap() map[string]string

CredentialsMap flattens Secrets into a role → secret_name map for the ONE consumer that wants this shape: the integration.delete audit payload.

func (Integration) HeaderSecret added in v0.5.0

func (in Integration) HeaderSecret() (secretName, header, format string, ok bool)

HeaderSecret returns the first proxy_header-delivered secret row as the (secretName, header, format) triple injection consumes. An empty stored format reads as "%s", not injectionRuleFromScope's "Bearer %s" default.

func (Integration) RoleSecret added in v0.5.0

func (in Integration) RoleSecret(role string) string

RoleSecret returns the secret NAME stored for role ("" when not configured).

func (*Integration) UnmarshalJSON added in v0.5.0

func (in *Integration) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts BOTH the base-component shape (discriminated by its "kind" key) and the legacy {category, type, hosts, header, credentials} shape, folding the latter forward (foldLegacyIntegration) — the ONE read-time migration chokepoint; marshal emits only the new shape.

type IntegrationDelivery added in v0.5.0

type IntegrationDelivery struct {
	Mode string `json:"mode"` // proxy_header | resident_file | resident_env
	// Header/Format apply to proxy_header: field name and value template (one
	// %s; empty means the raw secret IS the value).
	Header string `json:"header,omitempty"`
	Format string `json:"format,omitempty"`
	// Path applies to resident_file: the in-sandbox path the secret lands at.
	Path string `json:"path,omitempty"`
	// Var applies to resident_env: the environment variable name.
	Var string `json:"var,omitempty"`
}

IntegrationDelivery says how ONE secret reaches the run; the mode decides which other fields apply.

type IntegrationList added in v0.5.0

type IntegrationList []Integration

IntegrationList is SiteConfig's integrations slice with the read-time migration applied at decode: rows folded from legacy artifact_mirror/ host_proxy categories are DROPPED (network topology, homed under Corporate network). A new-shape row is never dropped, whatever its kind.

func (*IntegrationList) UnmarshalJSON added in v0.5.0

func (l *IntegrationList) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes the rows, then drops legacy topology rows.

type IntegrationSecret added in v0.5.0

type IntegrationSecret struct {
	Role       string               `json:"role"`
	SecretName string               `json:"secret_name"`
	Delivery   *IntegrationDelivery `json:"delivery,omitempty"`
}

IntegrationSecret is one required secret of an integration: a role, a ref into the secret store (a NAME, never a value), and how it's delivered. A nil Delivery is allowed on CLOSED kinds only (their own bespoke transport delivers it).

type InternalHost added in v0.7.0

type InternalHost struct {
	// HostSuffix matches a request host by label suffix: itself, or any host
	// ending in "."+HostSuffix (never a mid-label substring match).
	HostSuffix string `json:"host_suffix"`
	// CIDRs scopes the lift to these ranges only, each inside RFC1918,
	// fc00::/7, or 100.64.0.0/10 (ipguard.Liftable) — never loopback/
	// link-local/metadata/multicast/NAT64. Empty lifts the full Liftable set.
	CIDRs []string `json:"cidrs,omitempty"`
}

InternalHost is one SiteConfig.InternalHosts entry — see that field's doc.

type LLMInspectionSpec

type LLMInspectionSpec struct {
	// Mode is "off" (default), "alert" (scan + audit, forward unchanged), or
	// "block" (a qualifying finding refuses the request). "" == "off".
	Mode string `json:"mode"`
	// WorkspaceSecretNames are operator-declared secret NAMES, resolved against the at-rest secret
	// store — the field an operator/admin actually AUTHORS. A name is not sensitive the way a value
	// is, so it may freely appear in a stored policy row. Resolved to WorkspaceSecretValues ONLY at
	// dispatch, in memory, on the per-dispatch policy copy handed to the proxy.
	WorkspaceSecretNames []string `json:"workspace_secret_names,omitempty"`
	// WorkspaceSecretValues are the RESOLVED secret VALUES the run must not leak into a prompt — the
	// v1 detection corpus the proxy actually matches against. NOT an authoring field:
	// validatePolicySpec refuses a non-empty value here on every policy write. Populated ONLY by
	// dispatch, in memory; every other copy carries names only, values redacted to a count. NEVER
	// logged. Values shorter than the masking floor are ignored.
	WorkspaceSecretValues []string `json:"workspace_secret_values,omitempty"`
	// DetectSecrets enables the known-secret detector (exact match against WorkspaceSecretValues).
	// At least one Detect* must be true when Mode != off.
	DetectSecrets bool `json:"detect_secrets,omitempty"`
	// DetectSecretPatterns enables the regex catalog of well-known secret formats (AWS/GitHub/Slack/
	// Google keys, PEM keys, JWTs, Stripe) — higher precision than entropy but can false-positive on
	// example/test keys in code.
	DetectSecretPatterns bool `json:"detect_secret_patterns,omitempty"`
	// DetectEntropy enables the Shannon-entropy detector (high FP in code; medium
	// severity so a strict block_min_severity can exclude it). Off by default.
	DetectEntropy bool `json:"detect_entropy,omitempty"`
	// DetectPII enables the regex/Luhn PII detector (best-effort, high false-negative recall — a
	// visibility signal, never a control). Off by default.
	DetectPII bool `json:"detect_pii,omitempty"`
	// DetectorSidecarURL, when set, adds an out-of-process detection sidecar the proxy POSTs each
	// span to. Trusted operator config. A sidecar error/timeout/non-200 respects on_scanner_error
	// like any other scanner error: fail-OPEN by default, fail-CLOSED when on_scanner_error=block —
	// so block mode with the sidecar as the SOLE detector DOES guarantee a block on sidecar failure.
	DetectorSidecarURL string `json:"detector_sidecar_url,omitempty"`
	// ClassifiedMarkers are operator-defined literal markers (e.g. "INTERNAL ONLY",
	// "CONFIDENTIAL//NOFORN") whose presence in outbound content flags a
	// classified-content leak. Case-insensitive substring match; category "classified".
	ClassifiedMarkers []string `json:"classified_markers,omitempty"`
	// ScanAttachments opts into decoding+scanning base64 image/document attachment
	// bytes in a prompt (off by default — binary, large, high-FP).
	ScanAttachments bool `json:"scan_attachments,omitempty"`
	// InspectForwardEgress extends inspection from the LLM routes to the GENERIC plaintext-HTTP
	// forward path, so a custom (non-LLM) HTTP connector's POST/PUT body is scanned too. Off by
	// default. (HTTPS connectors tunnel via opaque CONNECT and remain uninspected unless MITM'd.)
	InspectForwardEgress bool `json:"inspect_forward_egress,omitempty"`
	// MaxScanBytes caps the size of a single extracted span scanned; 0 => a
	// built-in default. A larger span is skipped (fail-open) and recorded.
	MaxScanBytes int `json:"max_scan_bytes,omitempty"`
	// OnScannerError selects behavior when the scanner ERRORS (e.g. an
	// unparseable body): "pass" (default, fail-open) or "block".
	OnScannerError string `json:"on_scanner_error,omitempty"`
	// RequireInspectableLLM, when true, refuses to schedule a run whose resolved LLM transport is
	// opaque (subscription-OAuth/Bedrock CONNECT) and therefore cannot be inspected — fail-closed,
	// like MinConfinementClass. Default false only WARNS (the common subscription user isn't punished).
	RequireInspectableLLM bool `json:"require_inspectable_llm,omitempty"`
	// InterceptTLS opts the run into TLS-MITM of opaque CONNECT tunnels to known LLM hosts
	// (Anthropic/OpenAI), making the subscription-OAuth path inspectable. TRUST BOUNDARY: the
	// control plane provisions a per-run CA — the PRIVATE key goes only to the proxy sidecar; the
	// sandbox trusts only the CA's PUBLIC cert. Adds a CA trust dependency inside the sandbox. Off by default.
	InterceptTLS bool `json:"intercept_tls,omitempty"`
	// BlockMinSeverity is the minimum finding severity that triggers a block in
	// "block" mode ("low"|"medium"|"high"|"critical"; "" => low).
	BlockMinSeverity string `json:"block_min_severity,omitempty"`
}

LLMInspectionSpec configures optional outbound LLM prompt inspection for a run. The zero value (or a nil *LLMInspectionSpec on the policy) means OFF.

type LaunchPreset added in v0.8.0

type LaunchPreset struct {
	Name        string          `json:"name"`
	Version     int             `json:"version"`
	Description string          `json:"description,omitempty"`
	UserTypes   []string        `json:"user_types"`
	Request     json.RawMessage `json:"request"`
	CreatedAt   time.Time       `json:"created_at"`
	UpdatedAt   time.Time       `json:"updated_at"`
	CreatedBy   string          `json:"created_by,omitempty"`
	UpdatedBy   string          `json:"updated_by,omitempty"`
}

LaunchPreset is one row of launch_presets (migration 0087): an admin-managed, named bundle of existing POST /runs fields. Request is the stored create-run body, kept raw here because its Go type lives in the public SDK (pkg/client.CreateRunRequest), which imports this package.

type LimitsOverlay added in v0.8.6

type LimitsOverlay struct {
	DenyTaskModeExec    *bool           `json:"deny_task_mode_exec,omitempty"`
	DenyInteractive     *bool           `json:"deny_interactive,omitempty"`
	DenyUIApps          *bool           `json:"deny_ui_apps,omitempty"`
	MaxConcurrentRuns   *int            `json:"max_concurrent_runs,omitempty"`
	DenyUserDrive       *bool           `json:"deny_user_drive,omitempty"`
	MaxCPUMillis        *int            `json:"max_cpu_millis,omitempty"`
	MaxMemoryMiB        *int            `json:"max_memory_mib,omitempty"`
	MaxEphemeralDiskMiB *int            `json:"max_ephemeral_disk_mib,omitempty"`
	MaxDriveSizeMiB     *int            `json:"max_drive_size_mib,omitempty"`
	AutonomyRubric      *AutonomyRubric `json:"autonomy_rubric,omitempty"`
	MaxEndAheadSec      *int            `json:"max_end_ahead_sec,omitempty"`
	DefaultEndSec       *int            `json:"default_end_sec,omitempty"`
	AllowNoEnd          *bool           `json:"allow_no_end,omitempty"`
	MaxWaitSec          *int            `json:"max_wait_sec,omitempty"`
	DefaultWaitSec      *int            `json:"default_wait_sec,omitempty"`
	UserChangesLimits   *bool           `json:"user_changes_limits,omitempty"`
	PauseIdleAfterSec   *int            `json:"pause_idle_after_sec,omitempty"`
}

LimitsOverlay is CeilingOverlay's counterpart for GovernanceLimits. The embedded RunLimits fields sit flat on the wire, as they do on the limits object itself.

func DecodeLimitsOverlay added in v0.8.6

func DecodeLimitsOverlay(raw []byte) (LimitsOverlay, error)

DecodeLimitsOverlay strictly decodes a stored or submitted limits overlay.

type LostReason added in v0.8.0

type LostReason string

LostReason says why a kept run lost its sandbox (AgentRun.LostReason).

const (
	// LostEnded is a run whose lease ran out (AgentRun.EndsAt passed): stopped
	// and kept for the ended-run grace.
	LostEnded LostReason = "ended"
	// LostReboot is an interactive run whose agent container exited under it
	// (a host reboot, a Docker Desktop restart, a long suspend) but still
	// exists: kept with its files and its proxy stopped.
	LostReboot LostReason = "reboot"
	// LostOutage is an interactive run whose run token lapsed because the
	// control plane was unreachable past the token's life: its proxy is stopped
	// so it has no egress, and its agent is left running.
	LostOutage LostReason = "outage"
)

type ModelProvider added in v0.8.0

type ModelProvider struct {
	ID string `json:"id"` // admin's slug ("corp-gateway"); unique within the block
	// UID is SERVER-OWNED: minted on first write, never reissued once deleted. Every person's credential is
	// keyed by it, so a submitted value is ignored rather than trusted.
	UID  string            `json:"uid,omitempty"`
	Name string            `json:"name,omitempty"` // shown when people choose it ("Corp gateway")
	Kind ModelProviderKind `json:"kind"`
	// Disabled is negative-sense so the zero value is ENABLED. A disabled provider may remain an agent's
	// default: that default becomes a refused sentinel, never a fall-through to another provider.
	Disabled bool `json:"disabled,omitempty"`
	// BaseURL: required for custom_endpoint, an optional gateway override for Anthropic/OpenAI, never set
	// on Bedrock (its address is Bedrock.BaseURL).
	BaseURL string           `json:"base_url,omitempty"`
	Auth    *ProviderAuth    `json:"auth,omitempty"`    // how each person's token is sent; custom_endpoint only
	Bedrock *BedrockSettings `json:"bedrock,omitempty"` // region, address and sign-in setup for a Bedrock kind
	Azure   *AzureSettings   `json:"azure,omitempty"`   // endpoint and route for azure_foundry
	// Harnesses are the harnesses this provider serves, with per-pairing settings.
	Harnesses []ProviderHarness `json:"harnesses,omitempty"`
}

ModelProvider is one admin-configured provider.

func (ModelProvider) Serves added in v0.8.0

func (p ModelProvider) Serves(harness string) bool

Serves reports whether harness is enabled for this provider; says nothing about Disabled.

type ModelProviderKind added in v0.8.0

type ModelProviderKind string

ModelProviderKind is what kind of credential each person brings to a provider, and so which dispatch lane serves it. A closed set.

const (
	ModelProviderAnthropicSubscription ModelProviderKind = "anthropic_subscription" // own Claude subscription, via container sign-in
	ModelProviderBedrockSSO            ModelProviderKind = "bedrock_sso"            // own AWS sign-in via admin's start URL + account/role pin
	ModelProviderAnthropicAPIKey       ModelProviderKind = "anthropic_api_key"      // own Anthropic API key
	ModelProviderOpenAIAPIKey          ModelProviderKind = "openai_api_key"         // own OpenAI API key
	ModelProviderBedrockBearer         ModelProviderKind = "bedrock_bearer"         // own Bedrock API key
	// ModelProviderCustomEndpoint: admin's own endpoint, reached with each person's own token/PAT.
	ModelProviderCustomEndpoint ModelProviderKind = "custom_endpoint"
	// ModelProviderAzureFoundry: each person's own Entra sign-in, captured for one Azure resource audience.
	// Writable only while validateModelProviders' activation flag (azureFoundryGateReady) is true.
	ModelProviderAzureFoundry ModelProviderKind = "azure_foundry"
)

func (ModelProviderKind) IsBedrock added in v0.8.0

func (k ModelProviderKind) IsBedrock() bool

IsBedrock reports whether k is one of the two Bedrock kinds (address/sign-in live in ModelProvider.Bedrock).

func (ModelProviderKind) Valid added in v0.8.0

func (k ModelProviderKind) Valid() bool

Valid reports whether k is one of the closed kinds.

type ModelProviders added in v0.8.0

type ModelProviders struct {
	Providers []ModelProvider `json:"providers,omitempty"`
}

ModelProviders is the org's model-provider CONFIGURATION: which credential kinds this deployment supports, where each sends requests, and which harnesses may use it. Configuration ONLY — no credential lives on a record, every person supplies their own. The zero value is load-bearing: with no block, run create and dispatch keep the existing lane-resolution path unchanged. Validation lives in internal/api.

func (*ModelProviders) Empty added in v0.8.0

func (p *ModelProviders) Empty() bool

Empty reports whether p carries no provider — the shape a PUT uses to CLEAR the block, normalized back to nil so a later GET doesn't render "model_providers":{} forever.

type PauseReason added in v0.8.0

type PauseReason string

PauseReason says why a run's agent is frozen (AgentRun.PausedReason).

const (
	PauseWaiting PauseReason = "waiting" // parked on an open request nobody has answered
	PauseIdle    PauseReason = "idle"    // unused past pause_idle_after_sec, CPU quiet
)

type Person added in v0.8.0

type Person struct {
	Principal       string     `json:"principal"`
	Email           string     `json:"email,omitempty"`
	Issuer          string     `json:"issuer,omitempty"`
	TenantID        string     `json:"tenant_id,omitempty"`
	ObjectID        string     `json:"object_id,omitempty"`
	CreatedBy       string     `json:"created_by"`
	CreatedAt       time.Time  `json:"created_at"`
	FirstSignedInAt *time.Time `json:"first_signed_in_at,omitempty"`
}

Person is an identity an admin created or confirmed before its first sign-in, keyed by the identity provider's subject — or, on Entra ID, by Issuer, TenantID and ObjectID, all three set or none (#1195).

type PersonList added in v0.8.6

type PersonList struct {
	People     []PersonSummary `json:"people"`
	NextCursor string          `json:"next_cursor,omitempty"`
}

PersonList is one page of GET /people. NextCursor is empty on the last page.

type PersonSummary added in v0.8.6

type PersonSummary struct {
	Principal       string     `json:"principal"`
	Email           string     `json:"email,omitempty"`
	IssuerKind      string     `json:"issuer_kind"`
	PreCreated      bool       `json:"pre_created"`
	FirstSignedInAt *time.Time `json:"first_signed_in_at,omitempty"`
	LastSignedInAt  *time.Time `json:"last_signed_in_at,omitempty"`
	DeactivatedAt   *time.Time `json:"deactivated_at,omitempty"`
	Role            string     `json:"role,omitempty"`
	ActiveSessions  int        `json:"active_sessions"`
	APITokens       int        `json:"api_tokens"`
	SSHKeys         int        `json:"ssh_keys"`
	Credentials     int        `json:"credentials"`
	ActiveRuns      int        `json:"active_runs"`
}

PersonSummary is one row of GET /people: a person this deployment knows, with the counts behind the leaver actions. ActiveSessions is 0 or 1: sessions are stateless cookies, so it says whether the last sign-in could still hold a live one, not how many cookies are out. Role is the role the person's email and last verified groups derive: "denied" when sign-in would refuse them, "unknown" when that sign-in's groups were truncated, and empty when sign-in is not SSO.

type ProviderAuth added in v0.8.0

type ProviderAuth struct {
	Header string `json:"header,omitempty"`
	Format string `json:"format,omitempty"`
}

ProviderAuth is how a custom endpoint expects each person's token: header and value format ("Bearer %s").

type ProviderHarness added in v0.8.0

type ProviderHarness struct {
	Harness string `json:"harness"` // harness-catalog id ("claude-code", "codex-cli")
	// Model: model id this harness uses on this provider; admin-set, no member override. Required
	// (an inference profile) on a Bedrock kind.
	Model string `json:"model,omitempty"`
	// FastModel is the deployment the Messages harness uses for its small-model alias; azure_foundry only,
	// on that harness only. Unset means Model.
	FastModel string `json:"fast_model,omitempty"`
	Path      string `json:"path,omitempty"` // where a custom endpoint serves this harness's API dialect, under BaseURL; may not change the host
	// AuthHeader and AuthFormat override the provider's Auth for this harness alone (custom_endpoint only).
	AuthHeader string `json:"auth_header,omitempty"`
	AuthFormat string `json:"auth_format,omitempty"`
}

ProviderHarness is one harness a provider is enabled for, and the settings for that pairing.

type PushContentScope added in v0.8.0

type PushContentScope struct {
	// Repo is the repository as the run's grant names it: "github.com/<owner>/<repo>"
	// on the GitHub App lane, "<host>/<path>" on the git_pat lane (Azure DevOps:
	// "dev.azure.com/<org>/<project>/_git/<repo>").
	Repo string `json:"repo"`
	// Branch is the ref the push updates, "refs/heads/…"; several names join
	// sorted by ", ".
	Branch string `json:"branch"`
	// ActsAs is the credential the forwarded push authenticates with, as
	// "<grant kind>:<grant id>" — "github_token:<uuid>" or "git_pat:<uuid>".
	ActsAs string `json:"acts_as"`
	// Paths are the first PushContentMaxPaths review-matched paths, sorted,
	// each git-quoted (core.quotePath on, the sidecar's quotePath) so every
	// path is valid UTF-8 on the wire. A path ending in "/" is a directory the
	// push does not carry.
	Paths []string `json:"paths"`
	// PathsTotal is how many paths matched a review pattern in all.
	PathsTotal int `json:"paths_total"`
	// Commits are the object ids the push sets its refs to, sorted.
	Commits []string `json:"commits"`
	// Updates pairs each ref the push updates with the object id it sets it
	// to — all zeros for a delete — sorted by ref. Branch and Commits name the
	// same refs and ids for the card; Updates is what binds one to the other,
	// so the same commits sent to the same refs in a different assignment are
	// a different scope. A row raised before it existed has none and matches
	// no new raise.
	Updates []PushRefUpdate `json:"updates"`
	// PathsDigest is the lower-case hex SHA-256 over every review-matched path,
	// sorted, each followed by a NUL byte (a git path cannot contain one).
	PathsDigest string `json:"paths_digest"`

	// ActsAsKind and ActsAsLabel are SERVER-SET: the control plane resolves
	// ActsAs against the run's own grants and stamps them before the row is
	// stored, refusing a raise that carries either — a sidecar's word on who a
	// push acts as is not taken. ActsAsKind is one of the PushActsAs*
	// constants; ActsAsLabel is the principal the console names.
	ActsAsKind  string `json:"acts_as_kind,omitempty"`
	ActsAsLabel string `json:"acts_as_label,omitempty"`
}

PushContentScope is the requested_scope of an ApprovalPushContent row — the bytes the console renders on the push card, stored verbatim.

The dedup key is the whole scope: approval.RequestApproval and the partial unique index from migration 0022 both compare it, so two raises of the same push collapse into one PENDING row. What makes it the right key is Commits plus PathsDigest: commits are content addresses, so a repack of the same commits (git's retry after an approval) yields the same scope where a pack digest would not, and PathsDigest covers every review-matched path, not just the ten Paths names, so two pushes whose first ten matches agree are still two questions.

func (PushContentScope) Validate added in v0.8.0

func (s PushContentScope) Validate() error

Validate checks the shape the proxy writes, so a row the console renders can be trusted to be one: every bound here is one the sidecar already honours.

type PushPathList added in v0.8.0

type PushPathList struct {
	Paths     []string `json:"paths"`
	Truncated bool     `json:"truncated"`
}

PushPathList is a held push's complete review-matched path list, sorted and bounded: every path when Truncated is false, else the first ones that fit the bounds. Sent by the sidecar beside the push_content requested_scope; the control plane verifies it against that scope (VerifyAgainst) and keeps it immutable per approval.

func NewPushPathList added in v0.8.0

func NewPushPathList(paths []string) PushPathList

NewPushPathList bounds paths, which are sorted and distinct.

func (PushPathList) VerifyAgainst added in v0.8.0

func (l PushPathList) VerifyAgainst(s PushContentScope) error

VerifyAgainst checks l is the list s was built from: sorted, distinct and within bounds, opening with every one of s.Paths, and — unless Truncated — exactly PathsTotal paths whose digest is s.PathsDigest. A truncated list must also have been cut by a bound (PushPathListMaxPaths paths, or within pushPathListSlack of PushPathListMaxBytes); which path follows the cut is still the sidecar's word, since nothing the scope carries names it.

type PushRefUpdate added in v0.8.0

type PushRefUpdate struct {
	Ref string `json:"ref"`
	New string `json:"new"`
}

PushRefUpdate is one ref a held push sets and the object id it sets it to.

type PushRulesOverlay added in v0.8.6

type PushRulesOverlay struct {
	DenyPaths          *[]string `json:"deny_paths,omitempty"`
	MaxInspectPackMiB  *int      `json:"max_inspect_pack_mib,omitempty"`
	RequireReviewPaths *[]string `json:"require_review_paths,omitempty"`
	HoldSeconds        *int      `json:"hold_seconds,omitempty"`
	DenyNewExecutables *bool     `json:"deny_new_executables,omitempty"`
	MaxFileSizeMiB     *int      `json:"max_file_size_mib,omitempty"`
}

PushRulesOverlay mirrors PushRulesSpec with per-field presence.

type PushRulesSpec added in v0.8.0

type PushRulesSpec struct {
	// DenyPaths are path patterns (e.g. ".github/workflows/**") the broker
	// refuses in a push: "**" crosses path segments, "*" and "?" do not, and
	// the pattern is anchored at the repository root. A trailing "/" means
	// everything beneath the directory (DenyPathSegments).
	DenyPaths []string `json:"deny_paths,omitempty"`
	// MaxInspectPackMiB caps how much of an incoming push the broker buffers
	// before refusing it as too large. 0/absent takes the broker's own default,
	// below the authored maximum so raising this is a real remedy; bounded
	// 0..64 by validatePolicySpec.
	MaxInspectPackMiB int `json:"max_inspect_pack_mib,omitempty"`
	// RequireReviewPaths are patterns in DenyPaths' language whose match HOLDS
	// a push for an admin's decision instead of refusing it. A deny match wins
	// over a review match; an unattended run refuses rather than holding.
	RequireReviewPaths []string `json:"require_review_paths,omitempty"`
	// HoldSeconds is how long a held push waits for that decision before it
	// is refused. 0/absent means 120; bounded 0..600 by validatePolicySpec.
	HoldSeconds int `json:"hold_seconds,omitempty"`
	// DenyNewExecutables is reserved: the broker cannot yet tell a newly added executable from an
	// edit to an existing one, so validation refuses it when true rather than store a rule nothing
	// enforces, and IsSet does not count it, so a stored true never makes the broker inspect for
	// a rule it cannot apply.
	DenyNewExecutables bool `json:"deny_new_executables,omitempty"`
	// MaxFileSizeMiB refuses a push that introduces a file larger than this many MiB, or one whose
	// size the pack does not carry and the forge cannot show unchanged. 0/absent is off; bounded
	// 0..1024 by validatePolicySpec.
	MaxFileSizeMiB int `json:"max_file_size_mib,omitempty"`
}

PushRulesSpec declares content rules for a run's brokered git pushes — the counterpart to GitPushAnyBranch's branch confinement: WHAT a push may touch, not WHERE it may land. A closed struct, deliberately not a free-form rules map: an open map evades the strict-field JSON decoder and the doc-census test, so a typo'd key would silently do nothing instead of failing at write time. This type stores and validates; how DenyPathSegments entries MATCH is the git broker's (internal/egress/proxy/push_rules.go), reading them on both brokered lanes.

func (*PushRulesSpec) IsSet added in v0.8.0

func (s *PushRulesSpec) IsSet() bool

IsSet reports whether this spec carries an actual rule — NOT a bare != nil. An all-zero-but-non-nil *PushRulesSpec (a literal `push_rules: {}`) says nothing about what a push may touch and must read exactly like an absent one wherever consulted (composer's clamp/risk grade, the broker's advertisement/enforcement) — one method so those readers can't drift.

type ResolvedDrive added in v0.7.0

type ResolvedDrive struct {
	Drive    UserDrive
	Grant    UserDriveGrant
	Tier     CapabilitySubjectType
	HomeName string // per-user segment: the subdirectory of a share, or the suffix of a managed object's name
	// SubjectHash fingerprints the principal HomeName was derived FROM
	// (DriveSubjectHash), carried so the runner can stamp it and refuse one
	// stamped for somebody else.
	SubjectHash string `json:"subject_hash,omitempty"`
	ObjectName  string // what the runner asks the substrate for — a volume name, a PVC name, or an absolute host path
	SizeMiB     int
	Writable    bool
	Enforcement StorageEnforcement
	// Paused is set when the grant that WON is disabled: nothing is derived
	// and nothing may be mounted, but the row still wins its tier (DESIGN
	// §2.2) rather than falling through.
	Paused bool
}

ResolvedDrive is THE answer for one principal: the winning drive, the grant that won, the tier it won at, and everything derived from the pair. Built once by the API resolver so no consumer re-derives a home name or override. Writable is already folded but NOT yet narrowed by the run request.

NO json tags: this type crosses no wire. The preview and /me each compose their own response struct from it.

type ResolvedInjection added in v0.4.4

type ResolvedInjection struct {
	Host      string `json:"host"`
	Header    string `json:"header"`
	Value     string `json:"value"`
	JTI       string `json:"jti"`
	ExpiresAt int64  `json:"expires_at,omitempty"`
	// Organisation is the ADO org a per-person credential was dispatched for — informational only;
	// the proxy's REST gate pins the org from its own dispatch-time config, never from a resolve.
	Organisation string `json:"organisation,omitempty"`
	// Capabilities is the same lane's granted capability set, read once to confirm an asked-for
	// capability came back granted. Empty on other lanes.
	Capabilities []string `json:"capabilities,omitempty"`
}

ResolvedInjection is the wire contract of GET /api/v1/internal/injection/{grantID}: header name plus the server-formatted secret value. ExpiresAt (unix ms, 0 = never) marks a credential the proxy must re-resolve before it lapses; only a single-use approval-gated grant leaves it 0.

type ResourceLimits

type ResourceLimits struct {
	CPUMillis int `json:"cpu_millis,omitempty"` // milli-CPU; 2000 = 2 vCPU
	MemoryMiB int `json:"memory_mib,omitempty"` // hard memory cap in MiB
	PidsLimit int `json:"pids_limit,omitempty"` // max processes/threads (fork-bomb guard)
	DiskMiB   int `json:"disk_mib,omitempty"`   // writable storage cap in MiB (best-effort)
}

ResourceLimits caps a sandbox's resource consumption. A ZERO field means "use the platform default": the dispatch path fills conservative defaults (e.g. 2000m CPU, 4096 MiB, 512 PIDs) so even a policy that sets no limits still runs capped. DiskMiB is best-effort and depends on the storage driver supporting a per-container quota (fail-closed/warn when a cap is demanded but unsupported).

type ResourcesOverlay added in v0.8.6

type ResourcesOverlay struct {
	CPUMillis *int `json:"cpu_millis,omitempty"`
	MemoryMiB *int `json:"memory_mib,omitempty"`
	PidsLimit *int `json:"pids_limit,omitempty"`
	DiskMiB   *int `json:"disk_mib,omitempty"`
}

ResourcesOverlay mirrors ResourceLimits with per-field presence.

type RoleMapping added in v0.7.0

type RoleMapping struct {
	ID                 uuid.UUID `json:"id"`
	Value              string    `json:"value"`
	Role               string    `json:"role"`
	UserType           string    `json:"user_type,omitempty"`
	MigratedFromMember bool      `json:"migrated_from_member,omitempty"`
	CreatedAt          time.Time `json:"created_at"`
	CreatedBy          string    `json:"created_by,omitempty"`
}

RoleMapping is one console-managed (Getting Started -> People) row of migration 0051's role_mappings table: "value maps to role". This is the STORE'S wire type, carrying id/timestamps/provenance — distinct on purpose from internal/auth/oidc's own RoleMapping (just Value/Role), which stays dependency-free of this package (see that type's doc comment) the same way oidc.SessionRevocations keeps oidc dependency-free of store; the API layer converts between the two, mirroring however SessionRevocations bridges store -> oidc today.

Value is expected already canonical (trimmed, lowercased, ASCII) by the API write boundary that owns writes to this table — see the migration comment.

UserType is the row's user type when Role is the user tier (migration 0070_user_tier_rename's column, a foreign key to user_types); "" on a tier row, and on a user row written before types existed, which reads as the built-in "standard".

MigratedFromMember (migration 0098) marks a row that rewrite rewrote from role='member' rather than one an admin actually saved on the built-in type — the store's UpsertRoleMapping clears it on any write that flips the row in place, since choosing a real type is what the marker exists to prompt.

type RunAttention added in v0.8.0

type RunAttention struct {
	Kind    AttentionKind `json:"kind"`
	By      AttentionBy   `json:"by"`
	Pending int           `json:"pending"`
}

RunAttention is GET /runs and GET /me/attention's projection onto one live run: what it's waiting on, who can clear it (in the caller's view), and how many PENDING approvals it carries. Nil when attention() has nothing to say (terminal, lease-ended, nothing PENDING held).

type RunLayout added in v0.5.0

type RunLayout struct {
	Preset    string            `json:"preset"`
	Layout    []RunLayoutWidget `json:"layout"`
	UpdatedAt time.Time         `json:"updated_at"`
}

RunLayout is a human's saved widget arrangement for the run-detail cockpit (GET/PUT /api/v1/me/run-layout), one row per (principal, preset) — see migration 0037_ui_layouts.sql and internal/api/ui_layout.go's doc comment for why this is a server row and not ui/src/app/lib/storage.ts's localStorage. Principal deliberately does not appear here: it is the row's SCOPE (how internal/store looks a layout up), never a value the API echoes back or a caller supplies — internal/api/ui_layout.go scopes every read and write by principalFromRequest(r) alone.

type RunLayoutWidget added in v0.5.0

type RunLayoutWidget struct {
	Widget string `json:"widget"`
	X      int    `json:"x"`
	Y      int    `json:"y"`
	W      int    `json:"w"`
	H      int    `json:"h"`
}

RunLayoutWidget places one evidence widget on the run cockpit's grid. Widget is validated server-side against a closed id set (internal/api/ui_layout.go's runLayoutWidgetIDs) before a layout is ever stored — an id no build renders would otherwise be a widget that silently vanishes on restore, with nothing to explain why.

type RunLimits added in v0.8.0

type RunLimits struct {
	// MaxEndAheadSec: furthest ahead of NOW a run's end may be set (0 = no limit).
	// Extending within it never needs UserChangesLimits — extending is the lease.
	MaxEndAheadSec int `json:"max_end_ahead_sec,omitempty"`
	// DefaultEndSec is a new run's end, from create; 0 means MaxEndAheadSec, and
	// a run has no end when both are 0.
	DefaultEndSec int `json:"default_end_sec,omitempty"`
	// AllowNoEnd offers "No end" to a user who may change limits.
	AllowNoEnd bool `json:"allow_no_end,omitempty"`
	// MaxWaitSec/DefaultWaitSec: longest and default wait for a decision; 0 =
	// the deployment's approval expiry, which also caps both.
	MaxWaitSec     int `json:"max_wait_sec,omitempty"`
	DefaultWaitSec int `json:"default_wait_sec,omitempty"`
	// UserChangesLimits is the one gate: the user may shorten the end, set No
	// end, or change the wait.
	UserChangesLimits bool `json:"user_changes_limits,omitempty"`
	// PauseIdleAfterSec pauses an unused run after this long (0 pauses only
	// runs waiting for a decision).
	PauseIdleAfterSec int `json:"pause_idle_after_sec,omitempty"`
}

RunLimits bound how long a run lives and how long it waits for a decision (long-holds design rev 4, §2.2). Zero values keep today's behaviour: no end, the deployment's approval expiry as the wait, no idle pause.

They bind every run under a profile, including a security admin's; only a super admin's run skips them (effectiveCeiling resolves no profile for an operator).

type RunPolicy

type RunPolicy struct {
	ID        uuid.UUID     `json:"id"`
	Name      string        `json:"name"`
	CreatedAt time.Time     `json:"created_at"`
	UpdatedAt time.Time     `json:"updated_at"`
	Spec      RunPolicySpec `json:"spec"`
}

RunPolicy is the declarative policy attached to runs: egress allowlist, approval rules, and the maximum credential scopes a run may be granted. Workspace-local configuration may only NARROW a policy, never widen it.

type RunPolicySpec

type RunPolicySpec struct {
	// AllowedDomains is the L2 egress allowlist (exact hosts or "*." wildcards).
	AllowedDomains []string `json:"allowed_domains"`
	// DeniedDomains always wins over AllowedDomains.
	DeniedDomains []string `json:"denied_domains,omitempty"`
	// AllowAllEgress switches L2 egress from default-deny to "allow all (deny-list only)" mode: the
	// proxy allows ANY non-denied PUBLIC host, AllowedDomains may be empty. SECURITY: denied_domains
	// STILL wins, the SSRF/private-IP guard is unaffected, and credential injection STILL requires
	// an EXACT allowlist entry, so allow-all never widens where a secret may be injected.
	AllowAllEgress bool `json:"allow_all_egress,omitempty"`
	// FirstUseApproval controls how an unknown domain is handled: always_deny,
	// deny_with_review, or wait_for_review. Accepts the legacy boolean on the
	// wire. Inert under allow-all.
	FirstUseApproval FirstUseMode `json:"first_use_approval"`
	// FirstUseHoldSeconds bounds how long a wait_for_review connection is HELD
	// open awaiting a decision before the proxy refuses it. 0/absent keeps the
	// built-in 30s default; a positive value overrides it. Only wait_for_review
	// holds. See internal/egress/proxy configureHold.
	FirstUseHoldSeconds int `json:"first_use_hold_seconds,omitempty"`
	// MaxHolds caps concurrent wait_for_review holds. 0/absent keeps the
	// built-in 16 default. The (N+1)th concurrent held connection fails fast
	// rather than consuming an unbounded goroutine.
	MaxHolds int `json:"max_holds,omitempty"`
	// AllowedMethods optionally restricts HTTP methods (empty = all).
	AllowedMethods []string `json:"allowed_methods,omitempty"`
	// MinConfinementClass refuses to launch below this class.
	MinConfinementClass ConfinementClass `json:"min_confinement_class"`
	// EligibleGrants is the ceiling of credential scopes a run may request.
	EligibleGrants []GrantSpec `json:"eligible_grants,omitempty"`
	// AutoStopAfter stops idle sandboxes after this many seconds. 0/absent
	// means never reaped, same as an explicit negative (the reaper skips <=
	// 0); negative exists only to state never-reap intent explicitly.
	AutoStopAfterSec int `json:"auto_stop_after_sec,omitempty"`
	// WorkspaceMounts are host bind mounts injected into the sandbox. TRUST BOUNDARY: authored only
	// on a stored policy or inline create-run request by an admin/SSO-gated operator, never chosen
	// by the in-sandbox agent. validatePolicySpec runs runner.ValidateMount (the same deny-list the
	// docker driver enforces) at write time and again, defense-in-depth, at sandbox-create.
	WorkspaceMounts []WorkspaceMount `json:"workspace_mounts,omitempty"`
	// WorkspaceRepos are additional git repos attached to a run, paralleling WorkspaceMounts for
	// git-cloned (rather than bind-mounted) sources. Same admin/inline authoring surface and trust
	// boundary as WorkspaceMounts: never agent-chosen. validatePolicySpec enforces a unique-target
	// invariant across ALL WorkspaceMounts + WorkspaceRepos dests, so a clone can never land on a
	// bind target or shadow another repo's checkout.
	WorkspaceRepos []WorkspaceRepo `json:"workspace_repos,omitempty"`
	// LLMInspection optionally enables OUTBOUND content inspection at the proxy for brokered LLM
	// routes. Nil/omitted => OFF (the safe default). A guardrail + visibility layer, NOT
	// exfiltration prevention (see internal/contentscan + threatmodel §5.1).
	LLMInspection *LLMInspectionSpec `json:"llm_inspection,omitempty"`
	// UIApps declares the in-sandbox loopback HTTP apps the UI gateway may relay to a browser.
	// OPERATOR-AUTHORED, same trust boundary as WorkspaceMounts: an app is a NAME, a loopback PORT
	// and a PATH — never a command string. The gateway serves ONLY a declared port; `ssh -L` stays
	// the undeclared-port escape hatch.
	UIApps []UIApp `json:"ui_apps,omitempty"`
	// Resources caps sandbox CPU/memory/PIDs/disk. Nil, or a zero field, means "use the platform
	// default" — dispatch fills conservative defaults so EVERY run is capped, the multi-tenant
	// safety control that keeps one runaway/prompt-injected agent from OOM-killing the host,
	// fork-bombing it, or filling storage and taking down sibling runs.
	Resources *ResourceLimits `json:"resources,omitempty"`
	// ToolRules narrows an autonomous run's tool use from a BINARY (`tool_approvals`'s `auto`/`hold`)
	// to a policy: naming a TOOL and an effect (allow/hold/deny) lets "read freely, ask before shell,
	// never fetch the web" become expressible. Operator-authored and evaluated outside the sandbox
	// (proxy-side), so a compromised agent can't rewrite its own rules; it only narrows, and applies
	// only to a run already in `hold`, so a bad rule at worst asks more or refuses a call. Empty
	// preserves today's behaviour.
	ToolRules []ToolRule `json:"tool_rules,omitempty"`
	// GitPushAnyBranch turns OFF branch-namespace confinement for THIS run's brokered pushes (GitHub
	// App, opted-in git_pat and Azure DevOps Entra lanes). By default the broker forwards a push only
	// under refs/heads/wardyn/<run-id>/, so an agent can't rewrite main — right for an autonomous run,
	// wrong for a human-driven sandbox using an external tool with its own branch names. true =
	// forward to any branch the granted token may write, audited per push; the grant's own GitHub
	// ruleset still bounds what the token can touch. Operator-authored, never agent-settable.
	//
	// ponytail: whole-namespace off, not a per-run allowed-prefix list — add a
	// prefix list when someone needs an external tool AND confinement at once.
	GitPushAnyBranch bool `json:"git_push_any_branch,omitempty"`
	// PushRules declares CONTENT rules for this run's brokered git pushes — WHAT a push may touch,
	// alongside GitPushAnyBranch's WHERE. Nil (every pre-existing policy) means no content rules,
	// byte-identical to that prior wire shape, since every reader keys off IsSet. A policy that sets
	// PushRules while this run's only git-capable grant is ssh_key is legal but UNENFORCEABLE (SSH
	// has no broker seam) — composer.Grade surfaces that as a medium-risk WARNING, never a refusal.
	PushRules *PushRulesSpec `json:"push_rules,omitempty"`
	// AzureDevOpsCapabilities chooses THIS run's capabilities on the per-person Azure DevOps lane,
	// in place of the provider row's default_profile — which is what lets a saved policy act as a
	// saved access profile. It chooses only WITHIN the row's capability_ceiling: dispatch refuses a
	// run naming anything outside it, and the resolver re-checks the live ceiling on every request.
	// Empty/absent keeps the row's default_profile. Inert for a run not on that lane.
	AzureDevOpsCapabilities []adoscope.Capability `json:"azure_devops_capabilities,omitempty"`
}

func (RunPolicySpec) Clone added in v0.3.1

func (s RunPolicySpec) Clone() RunPolicySpec

Clone returns a deep copy: every slice/pointer field is reallocated, so the copy shares no backing array with the receiver. A plain `spec := other` is a SHALLOW copy — slice headers still point at the original's backing array, making the process-global default policy aliasable: a per-run `append` to AllowedDomains wrote into the shared spare capacity, so two concurrent create-runs raced on the same element (one run's egress domain silently replacing another's), and any in-place mutation leaked into every later run. Callers deriving a per-run/per-request spec from a shared one must Clone first.

type RunState

type RunState string

RunState is the AgentRun lifecycle state machine.

const (
	RunPending  RunState = "PENDING"
	RunStarting RunState = "STARTING"
	RunRunning  RunState = "RUNNING"
	// RunWaiting is reserved: unused, but kept wired end-to-end for the approval-gated-run feature.
	RunWaiting  RunState = "WAITING_FOR_CONFIRMATION"
	RunStopped  RunState = "STOPPED"
	RunArchived RunState = "ARCHIVED"
	RunFailed   RunState = "FAILED"
	RunKilled   RunState = "KILLED"
	// RunCompleted is terminal success (exit 0); a non-zero exit becomes RunFailed instead.
	RunCompleted RunState = "COMPLETED"
)

func (RunState) IsTerminal added in v0.4.4

func (s RunState) IsTerminal() bool

IsTerminal reports whether the run has ended. SINGLE source of the terminal set — the UI's copy (runs.ts) is pinned equal by TestTerminalRunStates_UIParity.

type SSHPublicKey added in v0.5.0

type SSHPublicKey struct {
	Fingerprint   string     `json:"fingerprint"`
	Principal     string     `json:"principal"`
	Name          string     `json:"name"`
	PublicKey     string     `json:"public_key"` // authorized_keys line; never a secret
	Role          string     `json:"role"`
	RoleCheckedAt *time.Time `json:"role_checked_at,omitempty"`
	Capped        bool       `json:"capped"`
	CreatedAt     time.Time  `json:"created_at"`
}

SSHPublicKey is a human's registered public key for the SSH gateway, distinct from GrantSpec's ssh_key grant (a resident private key for git-over-SSH — see GrantSSHKey). This is the gateway's trust root: it authenticates an incoming connection ONLY against rows here, then authorizes owner-or-admin. Fingerprint is computed server-side from the parsed key, never client-supplied. Role is the registering session's own role, stamped at registration and refreshed on every OIDC login — a BOUNDED-STALE stamp, not a live check: a demoted admin's key keeps its override until RoleCheckedAt exceeds WARDYN_SSH_ROLE_TTL, or re-registration; nil RoleCheckedAt reads as infinitely stale. Capped marks a key registered while an admin's session was in the user view: Role stays user through every re-stamp, and the admin override is never granted.

type SiteBranding added in v0.8.4

type SiteBranding struct {
	LogoPath string `json:"logo_path,omitempty"`
}

SiteBranding is SiteConfig.Branding. LogoPath is an absolute path on the wardynd host to an SVG or PNG file; it is read, checked as a console upload is, and stored as the branding logo at every apply. Naming it makes the file the logo's owner: the console offers no Remove logo while it is set. Taking it out of the document (and applying) removes the logo it delivered.

type SiteConfig

type SiteConfig struct {
	// UpstreamProxySecretRef names a secret holding the corporate upstream
	// proxy URL, or "" for none. UpstreamProxyURL wins if both are set (not
	// rejected, so an operator can migrate between the two).
	UpstreamProxySecretRef string `json:"upstream_proxy_secret_ref,omitempty"`
	// UpstreamProxyURL is the corporate upstream proxy URL written IN THE
	// CLEAR (http only) — topology, not a credential. MUST NOT embed
	// userinfo (validateSiteConfig rejects that).
	UpstreamProxyURL string `json:"upstream_proxy_url,omitempty"`
	// UpstreamProxyNoProxy is the upstream proxy's BYPASS list (like
	// NO_PROXY): destinations wardyn-proxy dials DIRECTLY, matched by
	// host/domain-suffix or CIDR. Exists because a corporate forward proxy
	// won't CONNECT to an internal address (breaks PrivateLink estates).
	//
	// SECURITY: a bypassed dial still hits the unconditional private/
	// reserved-IP SSRF guard (InternalHosts is the only lift) and grants no
	// policy allow. "*" is REFUSED at write time; empty (default) is
	// unchanged behavior.
	UpstreamProxyNoProxy []string `json:"upstream_proxy_no_proxy,omitempty"`
	// ArtifactOverrides maps an ecosystem ("npm"|"pip"|"cargo"|"maven"|"go"|
	// "nuget") to its corporate artifact-registry redirect.
	//
	// Deprecated: superseded by EgressRedirects; kept only so a
	// pre-EgressRedirects document still decodes. handlePutSiteConfig and
	// migration 0030 each fold a set value into EgressRedirects once.
	ArtifactOverrides map[string]ArtifactOverride `json:"artifact_overrides,omitempty"`
	// EgressRedirects is the operator's outbound redirect list: FROM a
	// public/upstream URL or host TO a corporate-internal replacement.
	// Ecosystem set gets the full behavior (config file + substitution +
	// token injection); Ecosystem "" is network-only (no config file).
	EgressRedirects []EgressRedirect `json:"egress_redirects,omitempty"`
	// ScmHosts are the operator's default SCM hosts (e.g. "dev.azure.com",
	// "github.example.com") the SCM Provider step / egress bundling consult.
	ScmHosts []string `json:"scm_hosts,omitempty"`
	// Integrations are the operator-configured external connections (AI
	// providers, SCM hosts, generic connections), the generalized
	// replacement the legacy fields above are migrating toward. Both are
	// read; nothing removes the legacy fields yet.
	Integrations IntegrationList `json:"integrations,omitempty"`
	// InternalHosts declares an internal hostname the proxy's unconditional
	// private/reserved-IP SSRF guard would otherwise refuse; each entry
	// LIFTS the guard for matching addresses.
	//
	// SECURITY: leave CIDRs empty unless you know the sandbox's actual
	// addresses — empty is the full liftable set, the safe default.
	// Loopback/link-local/metadata/multicast/NAT64 stay denied regardless.
	// This only lifts the SSRF builtin; policy still decides reachability.
	// Admin-only, validated against ipguard.Liftable. Empty => no lift.
	InternalHosts []InternalHost `json:"internal_hosts,omitempty"`
	// WorkspaceProviders is the org's workspace-provider POLICY: which git
	// hosts a run may clone from, with which credential lanes, plus storage
	// ceilings. A POINTER since a value struct's omitempty is a no-op
	// (would break the byte-identical-GET claim). Nil is legacy open mode;
	// PUT /site-config's absent-key/explicit-{} makes it MDM-deliverable.
	WorkspaceProviders *WorkspaceProviders `json:"workspace_providers,omitempty"`
	// AgentProviders is the org's agent-enablement POLICY: which coding
	// agents this deployment offers, each one's model-access lane, and
	// whether that credential is shared or per-person. A POINTER for the
	// same byte-identical-GET reason as WorkspaceProviders, with the same
	// absent-key/explicit-{} write terms. Nil is legacy open mode.
	AgentProviders *AgentProviders `json:"agent_providers,omitempty"`
	// ModelProviders is the org's model-provider configuration — a POINTER
	// for the same byte-identical-GET reason. Nil keeps today's
	// lane-resolution path.
	ModelProviders *ModelProviders `json:"model_providers,omitempty"`
	// SignInHelpText and SignInHelpURL are the admin's "what to do next",
	// shown on the sign-in page for refusals a person can't clear alone.
	//
	// SECURITY: PUBLIC — the anonymous /healthz publishes both, so they must
	// never name internal systems. Text <=1,000 chars, no control chars;
	// URL is http(s) only.
	SignInHelpText string `json:"sign_in_help_text,omitempty"`
	SignInHelpURL  string `json:"sign_in_help_url,omitempty"`
	// PolicyHelp is the contact a signed-in person is shown when a deployment-wide
	// rule (no profile binds them) refuses them. A POINTER for the same
	// byte-identical-GET reason as the provider blocks.
	//
	// NOT public: unlike the sign-in help pair, /healthz never publishes it.
	// Validated by policyref.Validate at PUT; a stored value is re-validated by
	// policyref.Project on every read.
	PolicyHelp *policyref.Contact `json:"policy_help,omitempty"`
	// Branding is the part of the console branding (#1215) a site config can
	// carry: today only the logo, as a file wardynd reads when this document is
	// applied. A POINTER for the same byte-identical-GET reason as the provider
	// blocks; nil is "the site config says nothing about branding". Name and
	// colours stay with the Branding card (PUT /branding/settings).
	Branding *SiteBranding `json:"branding,omitempty"`
	// EffectiveScmHosts is READ-ONLY, SERVER-OWNED: ScmHosts minus hosts a
	// provider row claims, union enabled providers' hosts. A real field
	// (not a wrapper key) so a get|set round trip still decodes under
	// DisallowUnknownFields. PUT ignores a submitted value; never stored.
	EffectiveScmHosts []string `json:"effective_scm_hosts,omitempty"`
	// WithheldScmHosts is READ-ONLY, SERVER-OWNED, on the same terms as
	// EffectiveScmHosts: each host a DISABLED provider row claims that
	// EffectiveScmHosts therefore leaves out, with the row that withholds it, so
	// an admin is told why a host is missing rather than left to look for it.
	// Projected on read, ignored on PUT, never stored.
	WithheldScmHosts []WithheldScmHost `json:"withheld_scm_hosts,omitempty"`
	// OnboardingCompletedAt records when an operator finished (or left) the
	// Getting Started funnel on THIS INSTALL. Nil until then.
	//
	// Server-side because a localStorage flag outlived a wiped database and
	// was scoped per-origin (127.0.0.1 vs localhost disagreed).
	//
	// NOT settable via PUT /site-config — the handler ignores a submitted
	// value and carries the stored one forward; a dropped value is reported
	// as onboarding_completed_at_ignored.
	OnboardingCompletedAt *time.Time `json:"onboarding_completed_at,omitempty"`
}

SiteConfig is the operator-wide, admin-authored baseline every run inherits: a corporate upstream proxy, egress redirects (package-registry mirrors and any outbound URL/host/IP redirect), and default SCM hosts. One singleton per operator; GetSiteConfig returns the zero value ("unconfigured") when none has been written yet.

SECURITY: secret VALUES never live here — only secret NAMES (refs) the broker/proxy resolve at dispatch/injection time, mirroring GrantSpec.Scope.

type Source added in v0.5.0

type Source struct {
	ID   uuid.UUID  `json:"id"`
	Kind SourceKind `json:"kind"`
	// Locator is the identity: a host dir path (local_dir, trimmed) or
	// canonical repo slug/clone URL (repo, lowercased); with Ref, unique in the store.
	Locator string `json:"locator"`
	// Ref is an optional git ref (repo only); part of identity, so the same repo at two refs is two sources with two contracts.
	Ref          string                          `json:"ref,omitempty"`
	Name         string                          `json:"name"`
	Requirements map[string]WorkspaceRequirement `json:"requirements,omitempty"` // this source's own contract; see the type doc above
	// Profile is this source's scan result, opaque here (never interpreted, only
	// persisted/returned). json.RawMessage, not []byte: a bare []byte gets
	// base64-encoded by encoding/json (WIRE-2), shipping it to GET /sources as
	// an opaque string instead of real JSON.
	Profile     json.RawMessage `json:"profile,omitempty"`
	Status      WorkspaceStatus `json:"status"`                  // scan lifecycle: pending_scan | scanning | scanned | error
	ActiveRunID *uuid.UUID      `json:"active_run_id,omitempty"` // fences this source's in-flight scan run
	CreatedAt   time.Time       `json:"created_at"`
	UpdatedAt   time.Time       `json:"updated_at"`
}

Source is one tier-1 library entry: a repo or dir configured once and attached to many workspaces. Requirements is what it expects of any sandbox using it, in Workspace.Requirements's "<type>:<key>" grammar, minus integration: keys (those compose only at the tier-3 aggregate).

type SourceKind added in v0.5.0

type SourceKind string

SourceKind is a library source's kind. Ephemeral scratch isn't a kind here: it's a per-workspace inline attachment, never a library entity.

const (
	SourceLocalDir SourceKind = "local_dir"
	SourceRepo     SourceKind = "repo"
)

type StorageEnforcement added in v0.7.0

type StorageEnforcement string

StorageEnforcement names WHAT ACTUALLY BINDS BYTES for a drive, so a size nothing enforces is never rendered as a limit: "Wardyn never enforces a drive's size itself. On Kubernetes the size is the volume request and the storage class decides whether it binds. On Docker a managed drive has no byte cap. A share is bounded by its own quota. The size you see is the allocation, not a guarantee." Surfaced via runner.Capabilities.EphemeralDiskEnforcement on the admin setup status.

const (
	// StorageEnforcementFilesystem: the filesystem itself refuses the write
	// (XFS project quota). NOTHING in v1 reports this yet.
	StorageEnforcementFilesystem StorageEnforcement = "filesystem"
	// StorageEnforcementEviction: the kubelet enforces it, MEASURED
	// PERIODICALLY — over the limit the POD IS KILLED, not the write; the
	// agent never sees ENOSPC and in-flight work is lost.
	StorageEnforcementEviction StorageEnforcement = "eviction"
	// StorageEnforcementRequest: the size is a scheduling REQUEST — a block
	// storage class binds it, an NFS/EFS provisioner enforces nothing.
	StorageEnforcementRequest  StorageEnforcement = "request"
	StorageEnforcementExternal StorageEnforcement = "external" // something outside Wardyn binds it; Wardyn claims nothing
	StorageEnforcementNone     StorageEnforcement = "none"     // nothing binds it (e.g. a Docker named volume)
)

func EnforcementFor added in v0.7.0

func EnforcementFor(b DriveBackend) StorageEnforcement

EnforcementFor maps a backend to what actually binds its bytes. An unknown backend reads as `none` — the one answer that cannot mislead an admin into trusting a cap.

type StorageProviders added in v0.7.2

type StorageProviders struct {
	Ephemeral *EphemeralProvider `json:"ephemeral,omitempty"`  // bounds writable scratch when a run mounts no drive
	UserDrive *UserDriveProvider `json:"user_drive,omitempty"` // org switch and ceiling over user drives
}

StorageProviders is the file-system half of WorkspaceProviders; each nil sub-block is legacy behaviour for that half.

type ToolEffect added in v0.7.0

type ToolEffect string

ToolEffect is what a matching ToolRule does with the call.

const (
	// ToolAllow runs the call with no human in the loop. It is still recorded.
	ToolAllow ToolEffect = "allow"
	// ToolHold raises an approval and blocks — today's behaviour for every call.
	ToolHold ToolEffect = "hold"
	// ToolDeny refuses the call without asking anyone.
	ToolDeny ToolEffect = "deny"
)

type ToolRule added in v0.7.0

type ToolRule struct {
	Tool   string     `json:"tool"`
	Effect ToolEffect `json:"effect"`
}

ToolRule is one entry in ToolRules: a tool NAME and what to do with it. Tool is matched exactly and case-sensitively against the name the harness reports (e.g. "Read", "Bash", "WebFetch") — deliberately not a pattern, since a glob invites a rule that reads narrower than it matches, and the set of tools a harness exposes is small and enumerable. The one wildcard is the literal "*", which sets the default for unmatched tools.

type UIApp added in v0.6.0

type UIApp struct {
	Name string `json:"name"`
	Port int    `json:"port"`
	Path string `json:"path,omitempty"`
}

UIApp is one declared in-sandbox HTTP app the UI gateway may relay to (RunPolicySpec.UIApps). Three fields, no command string: Name identifies the app to the operator and the launcher convention (/usr/local/bin/wardyn-ui-<name>), constrained to a short lower-case slug since it reaches a filesystem path and a URL query (never a slash, space, or ".."); Port is the port the app listens on INSIDE the sandbox, on 127.0.0.1 (the sandbox has no other reachable address); Path is where the app's UI lives, the landing path after the gateway's ticket handoff — empty means "/".

func (UIApp) PathOrRoot added in v0.6.0

func (a UIApp) PathOrRoot() string

PathOrRoot is Path with the empty default applied.

type UserDrive added in v0.7.0

type UserDrive struct {
	ID      uuid.UUID    `json:"id"`
	Name    string       `json:"name"`
	Backend DriveBackend `json:"backend"`
	// HostRoot is the operator-mounted tree a host_path drive binds a
	// subdirectory of; empty for every other backend. Authored by an admin
	// and bound into OTHER PEOPLE's sandboxes, so the security ceiling is the
	// env allowlist (UserDriveHostRootCheck), never this column alone.
	HostRoot     string       `json:"host_root,omitempty"`
	StorageClass string       `json:"storage_class,omitempty"` // k8s_pvc provisioner to request; "" = cluster default
	HomeTemplate HomeTemplate `json:"home_template"`
	// ObjectScheme names which half of a minted object name carries the
	// drive — see DriveObjectScheme. NOT client-authored: on the wire only so
	// a GET and round-tripped PUT agree.
	ObjectScheme DriveObjectScheme `json:"object_scheme,omitempty"`
	// SizeMiB is the ALLOCATION, not a guarantee — see StorageEnforcement. 0
	// means "no allocation shown" except on k8s_pvc, where it IS
	// requests.storage.
	SizeMiB   int          `json:"size_mib,omitempty"`
	Writable  bool         `json:"writable,omitempty"`
	Reclaim   DriveReclaim `json:"reclaim"`
	CreatedAt time.Time    `json:"created_at"`
	UpdatedAt time.Time    `json:"updated_at"`
	CreatedBy string       `json:"created_by,omitempty"`
}

UserDrive is one admin-registered drive (migration 0054's user_drives row). Name is the UNIQUE human handle: what an admin allocates by, what the console lists, and the last tie-break in the resolver's ORDER BY. Writable defaults to FALSE; a grant or run may narrow it, never widen it.

type UserDriveGrant added in v0.7.0

type UserDriveGrant struct {
	ID              uuid.UUID             `json:"id"`
	SubjectType     CapabilitySubjectType `json:"subject_type"`
	Subject         string                `json:"subject"`
	DriveID         uuid.UUID             `json:"drive_id"`
	Priority        int                   `json:"priority"`                    // breaks ties WITHIN a tier (higher wins); does NOT cross tiers
	SizeMiBOverride int                   `json:"size_mib_override,omitempty"` // 0 means "use the drive's"
	// WritableOverride is TRI-STATE: nil is "use the drive's posture",
	// explicit false is "read only even on a writable drive".
	WritableOverride *bool `json:"writable_override,omitempty"`
	// HomeOverride names this subject's directory verbatim. USER TIER ONLY:
	// on a group/all row it would give every member ONE home.
	HomeOverride string `json:"home_override,omitempty"`
	// Enabled is the admin's off switch. A disabled row still wins its tier:
	// the winner's bit folds to ResolvedDrive.Paused rather than silently
	// falling through to the wider row beneath it.
	Enabled   bool      `json:"enabled"`
	CreatedAt time.Time `json:"created_at"`
	CreatedBy string    `json:"created_by,omitempty"`
}

UserDriveGrant allocates one drive to one subject (migration 0054's user_drive_grants row). SubjectType REUSES CapabilitySubjectType, the same vocabulary capability_grants/governance_assignments use. Every override is a column on the BINDING row, since it has no meaning apart from the (subject, drive) pair.

type UserDriveHostRootCheck added in v0.7.0

type UserDriveHostRootCheck func(hostRoot string) error

UserDriveHostRootCheck is the signature of the deployment's ENV CEILING over admin-authored host_path drives (WARDYN_USER_DRIVE_HOST_ROOTS), returning nil when hostRoot is inside an allowed root.

A hook rather than a rule inside ValidateUserDrive: this package cannot read the environment, and site config (a full-replace row) is the wrong home for a security ceiling. An UNSET env means no host_path drive may be authored at all.

type UserDriveListItem added in v0.7.0

type UserDriveListItem struct {
	UserDrive
	GrantCount int `json:"grant_count"`
}

UserDriveListItem is one row of the admin Drives table. GrantCount is a READ-SIDE derivation kept off UserDrive so a GET/PUT round-trip cannot resurrect a stale count — what makes the delete-if-no-grants check honest.

type UserDriveProvider added in v0.7.2

type UserDriveProvider struct {
	Disabled   bool `json:"disabled,omitempty"`     // turns drives off deployment-wide, negative-sense like GitProvider.Disabled, so zero value is today
	MaxSizeMiB int  `json:"max_size_mib,omitempty"` // largest drive allowed, refused at admin write boundary and clamped at resolve; 0 = no ceiling
}

UserDriveProvider is the org-level user-drive switch and ceiling. Disabled is a different question from GovernanceLimits.DenyUserDrive; the two are checked in order: this org switch first (422, refused-backend), then the per-profile door (403 + authz.denied).

type UserType added in v0.8.0

type UserType struct {
	// ID is the immutable slug role-map values, grant subjects and token
	// stamps refer to.
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	// Priority breaks a sign-in tie between two custom types (higher wins).
	// Always 0 on the built-in type, which never takes part in a tie.
	Priority  int       `json:"priority"`
	BuiltIn   bool      `json:"built_in"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	CreatedBy string    `json:"created_by,omitempty"`
}

UserType is one row of the user_types table: an org-defined kind of person ("Portfolio manager"). It carries no controls of its own — governance assignments, capability grants and drive grants name it as a subject.

type WithheldScmHost added in v0.8.4

type WithheldScmHost struct {
	Host         string          `json:"host"`
	ProviderID   string          `json:"provider_id"`
	ProviderKind GitProviderKind `json:"provider_kind"`
}

WithheldScmHost is one entry of SiteConfig.WithheldScmHosts: a host kept out of the effective set because the named provider row is turned off. The row is named the way a refused clone names it, by kind and id.

type Workspace

type Workspace struct {
	ID   uuid.UUID `json:"id"`
	Name string    `json:"name"`
	// Sources is the composition: one-or-more local_dir/repo/ephemeral entries.
	Sources []WorkspaceSource `json:"sources"`
	// BaseImage is the base-image choice; nil means the platform default
	// convention image for the detected/scanned stack.
	BaseImage *WorkspaceBaseImage `json:"base_image,omitempty"`
	// Requirements is the declared requirements contract, keyed by a fixed
	// grammar of "<type>:<key>":
	//
	//	secret:NAME       a named secret must be resolvable, e.g. "secret:acme-anthropic-key"
	//	egress:host       the host must be reachable (bare host or "*."-wildcard), e.g. "egress:api.github.com"
	//	write:/host/path  the HOST path must be mounted writable, e.g. "write:/home/user/repo"
	//	integration:ID    the named Integration must be granted, e.g. "integration:git_host:github.com"
	//
	// Split on the FIRST colon only, since a write:/integration: key may itself
	// contain colons. An overlay on attached sources' own contracts;
	// EffectiveRequirements is the folded result a run consumes.
	Requirements map[string]WorkspaceRequirement `json:"requirements,omitempty"`

	// Attachments is the tier-3 composition: ordered source refs and inline
	// ephemeral rows with per-attachment target/writable/overrides. Empty on
	// a pre-split row, falling back to Sources.
	Attachments []WorkspaceAttachment `json:"attachments,omitempty"`
	// BaseImageID references the shared base-image catalog (tier 2); nil means
	// "recommended" (per-workspace derived build) by design, not absence.
	BaseImageID *uuid.UUID `json:"base_image_id,omitempty"`
	// EffectiveRequirements is derived, read-only, never persisted: the
	// FoldWorkspaceContract result over attachments + source contracts + the
	// overlay above (equals Requirements on a pre-split row).
	EffectiveRequirements map[string]WorkspaceRequirement `json:"effective_requirements,omitempty"`

	// Kind mirrors Sources[0].Type (single-source only).
	Kind WorkspaceKind `json:"kind"`
	// Source mirrors Sources[0]'s host path (local_dir) or repo slug/URL (repo, single-source only).
	Source string `json:"source"`
	// Ref mirrors Sources[0].Ref (single-source repo only).
	Ref string `json:"ref,omitempty"`
	// DefaultTarget mirrors Sources[0].Target (single-source only).
	DefaultTarget string `json:"default_target,omitempty"`

	// Profile is internal/workspacescan's WorkspaceProfile, opaque here; nil/empty until scanned.
	Profile json.RawMessage `json:"profile,omitempty"`
	// ImageRef is the resolved/generated image for this workspace's profile; empty until scanned/built.
	ImageRef string `json:"image_ref,omitempty"`
	// BuiltProfileHash is the profile hash ImageRef was built from, the
	// build-once/reuse-many cache key (rebuild only when it changes).
	BuiltProfileHash string `json:"built_profile_hash,omitempty"`
	// ApprovedEgress is the operator-owned list of egress hosts explicitly
	// promoted, unioned into a run's allowlist. Never written by a scan;
	// cleared when the composition changes.
	ApprovedEgress []string `json:"approved_egress,omitempty"`
	// DeniedEgress mirrors ApprovedEgress: hosts explicitly blocked, folded
	// into denied_domains at create (deny beats allow/allow_all_egress/a
	// runtime approval alike). Unlike ApprovedEgress, not cleared on source
	// change (migration 0040) — a deny needs no re-review.
	DeniedEgress []string `json:"denied_egress,omitempty"`
	// EgressEditedAt is when an operator last REPLACED one of the two lists
	// above — read by the boot heal, which without it would let a decided
	// `always` approval re-widen a just-removed host every restart. Stamped
	// only by those two scoped setters.
	EgressEditedAt *time.Time `json:"egress_edited_at,omitempty"`
	// ActiveRunID is the in-flight scan/record run, so the import panel can
	// poll "is my step still running" without scanning all runs.
	ActiveRunID *uuid.UUID `json:"active_run_id,omitempty"`
	// RecordResults is the per-task Record Mode state; opaque
	// map[string]api-owned JSON. Written only via SetWorkspaceRecordResult;
	// cleared on composition change.
	RecordResults json.RawMessage `json:"record_results,omitempty"`
	// LLMCred is the model/harness credential binding a run inherits (see
	// WorkspaceLLMCred). Written only via the scoped SetWorkspaceLLMCred.
	LLMCred *WorkspaceLLMCred `json:"llm_cred,omitempty"`
	// OwnedBy is the member who created this workspace (lowercased OIDC sub or
	// email); empty means operator-owned. TRUST BOUNDARY: ownsWorkspaceOrAdmin
	// keys on non-empty — only that member or an admin may read/write it, else
	// the same 404 as a missing workspace. Set only by handleCreateWorkspace.
	OwnedBy   string          `json:"owned_by,omitempty"`
	Status    WorkspaceStatus `json:"status"`
	CreatedAt time.Time       `json:"created_at"`
	UpdatedAt time.Time       `json:"updated_at"`
	// AvailableToYou (#1267): per-caller bit — would the same decide path a
	// launch runs admit THIS workspace for this reader? Computed fresh per GET
	// (workspaceAvailableToCaller), never stored or accepted on write; always
	// true for an operator. Pointer with omitempty: only the two GET stampers
	// set it, so a plain bool would ship a false "unavailable" elsewhere — the
	// console's "absent means fall back" contract needs the omitted key.
	AvailableToYou *bool `json:"available_to_you,omitempty"`
}

Workspace is an onboarded, admin-reviewed composition of one-or-more sources (local_dir | repo | ephemeral — the ephemeral floor means Sources is never empty), a base image choice, and a requirements contract. Import scans the composition once into an opaque profile (internal/workspacescan WorkspaceProfile); runs thereafter reference the workspace by id. Profile/BuiltProfileHash are empty until Status leaves pending_scan.

type WorkspaceAttachment added in v0.5.0

type WorkspaceAttachment struct {
	SourceID  *uuid.UUID `json:"source_id,omitempty"`
	Ephemeral bool       `json:"ephemeral,omitempty"` // marks an inline scratch row (never a library entity)
	Target    string     `json:"target,omitempty"`    // in-sandbox mount/clone/scratch path for this attachment
	Writable  bool       `json:"writable,omitempty"`  // opts a local_dir attachment into read-write, per-attachment not per-source
	// Overrides is this workspace's stance on requirement keys the attached
	// source declares: reqKey -> off|optional|required. Never edits the
	// shared source; keys the source doesn't declare are ignored harmlessly.
	Overrides map[string]string `json:"overrides,omitempty"`
}

WorkspaceAttachment is one tier-3 composition row: either a library source (SourceID set) or an inline ephemeral row (Ephemeral true, SourceID nil); order is load-bearing, attachments[0] is the primary.

type WorkspaceBaseImage added in v0.5.0

type WorkspaceBaseImage struct {
	// Kind is "recommended" (convention image for the detected stack),
	// "registry" (operator-picked, named by Image), "custom" (Image is a base,
	// see Steps), or "byo" (Image used verbatim).
	Kind string `json:"kind"`
	// Image is the base image reference. Meaning depends on Kind: the FROM for
	// "custom", the image itself for "registry"/"byo", unused for "recommended".
	Image string `json:"image,omitempty"`
	// Steps is CATALOG IDENTITY ONLY, never applied to an image. SECURITY:
	// running operator-authored RUN lines would be host-side RCE
	// (assertWrapSafeBase also refuses ONBUILD), so "custom" builds
	// identically to "byo". Must stay: part of base_images' UNIQUE(kind, image)
	// index; removing it mints duplicate rows.
	Steps []string `json:"steps,omitempty"`
}

WorkspaceBaseImage is a Workspace's base-image choice: what the sandbox's container image is built FROM, independent of its Sources.

type WorkspaceKind

type WorkspaceKind string

WorkspaceKind discriminates an onboarded Workspace's legacy, single-source shape; a DERIVED READ-ONLY value mirroring Sources[0] (see Workspace.Kind) — a multi-source Workspace has no single Kind.

const (
	WorkspaceKindLocalDir WorkspaceKind = "local_dir"
	WorkspaceKindRepo     WorkspaceKind = "repo"
	// WorkspaceKindContainer was the pre-composition-model shape (base image, no
	// mount); migration 0029 rewrites every stored row into the composition
	// shape. Deprecated: kept as an anchor for 0029's backfill SQL and any
	// pre-0029 payload that still decodes "container".
	WorkspaceKindContainer WorkspaceKind = "container"
)

type WorkspaceLLMCred added in v0.4.0

type WorkspaceLLMCred struct {
	// ProviderRef pins a run to one model provider (by id). A disallowed or
	// ineligible pin refuses the run rather than falling through to another
	// provider.
	ProviderRef string `json:"provider_ref,omitempty"`
	// ProviderUnavailable is set by the server on a READ only, in place of
	// ProviderRef, for a caller "Available to" does not admit to the pinned
	// provider: the workspace is pinned, to a provider they cannot use, and
	// its id is not theirs to learn (#1018). A write carrying it is refused.
	ProviderUnavailable bool `json:"provider_unavailable,omitempty"`
}

WorkspaceLLMCred is the operator-owned model binding on a workspace: a run that picks it uses the pinned model provider unless it names another. Nil or ProviderRef="" means no pin.

type WorkspaceMount

type WorkspaceMount struct {
	Source   string `json:"source"`
	Target   string `json:"target"`
	ReadOnly *bool  `json:"read_only,omitempty"`
}

WorkspaceMount is one operator/policy-controlled host bind mount. Source is a host path (must be an absolute, cleaned path not under a denied location); Target is the in-container path (must be under an allowed prefix, e.g. /home/agent, /work, or /workspace). ReadOnly is a *bool so the safe default is read-only: omitted (nil) means read-only, and read-write requires the policy author to explicitly set "read_only": false (a plain bool would default to the unsafe direction). Use ReadOnlyOrDefault to resolve it.

func (WorkspaceMount) ReadOnlyOrDefault

func (m WorkspaceMount) ReadOnlyOrDefault() bool

ReadOnlyOrDefault resolves the mount's effective read-only flag: an OMITTED (nil) read_only defaults to true (read-only), the safe default. Read-write is returned only when the policy explicitly set read_only=false.

type WorkspaceProviders added in v0.7.2

type WorkspaceProviders struct {
	// Git rows: hosts a run may clone from and the credential lanes usable there.
	Git []GitProvider `json:"git,omitempty"`
	// Storage is the file-system half. A nil block is legacy behaviour.
	Storage *StorageProviders `json:"storage,omitempty"`
	// GitPatBrokerEnabled is read-only and server-owned: projected from
	// WARDYN_GIT_PAT_BROKER on every GET, never stored or accepted on a PUT.
	// A pointer since nil ("never projected") differs from a present false.
	GitPatBrokerEnabled *bool `json:"git_pat_broker_enabled,omitempty"`
}

WorkspaceProviders is the org's workspace-provider policy: what kinds of work a sandbox may be given and the ceilings it's held to. The zero value is legacy open mode (load-bearing for upgraded installs), never derived from legacy SiteConfig.ScmHosts; URL validation lives in internal/api.

func (*WorkspaceProviders) Empty added in v0.7.2

func (p *WorkspaceProviders) Empty() bool

Empty reports whether p carries no policy — the shape a PUT uses to clear the block; the write boundary normalizes an empty block to nil.

type WorkspaceRepo

type WorkspaceRepo struct {
	Repo   string `json:"repo"`
	Target string `json:"target,omitempty"`
	Ref    string `json:"ref,omitempty"`
}

WorkspaceRepo is one operator/policy-controlled git repo attached to a run, paralleling WorkspaceMount for git-cloned (rather than bind-mounted) sources. Repo is a slug/URL validated the same way the legacy single-repo AgentRun.Repo field is. Target is an optional in-container clone destination; empty defers to the ~/work/<name> convention (not resolved here). Ref is an optional git ref (branch/tag/sha); empty clones the remote's default branch. Carried as a 4th tab-separated field in WARDYN_REPOS for agent-run-lib.sh's clone_one.

type WorkspaceRequirement added in v0.5.0

type WorkspaceRequirement struct {
	// Level is "required" (a run cannot proceed without it) or "optional"
	// (a run may proceed without it, degraded).
	Level string `json:"level"`
	// Provenance is "scan_seeded" (the scanner detected the need) or
	// "operator_set" (an operator declared it directly).
	Provenance string `json:"provenance"`
}

WorkspaceRequirement is one entry in a Workspace's requirements contract, keyed by the grammar documented on Workspace.Requirements.

type WorkspaceSource added in v0.5.0

type WorkspaceSource struct {
	Type WorkspaceSourceType `json:"type"`
	// Path is the host directory path (local_dir only).
	Path string `json:"path,omitempty"`
	// Source is the repo slug/URL (repo only).
	Source string `json:"source,omitempty"`
	// Ref is an optional git ref (branch/tag/sha, repo only).
	Ref string `json:"ref,omitempty"`
	// Target is the mount/clone/scratch-dir path inside the sandbox.
	Target string `json:"target,omitempty"`
	// Writable opts a local_dir source into read-write (default false =
	// read-only; a plain bool since false is already the safe default).
	Writable bool `json:"writable,omitempty"`
	// Overrides is this attachment's stance on requirement keys its source
	// declares: reqKey -> off|optional|required (workspace_contract.go's
	// AttachmentOverride* constants). nil means "say nothing" (server carries
	// forward the prior override); an explicit map REPLACES it outright.
	Overrides map[string]string `json:"overrides,omitempty"`
	// Admitted is a server-owned read projection, never persisted or from a
	// write body: does policy admit this clone URL today? A pointer so
	// "no answer" (absent, no provider row) differs from "not admitted".
	Admitted *bool `json:"admitted,omitempty"`
}

WorkspaceSource is one entry in a Workspace's composition (multiples of the same Type allowed). Meaningful fields depend on Type:

local_dir  Path (host path), Target, Writable
repo       Source (slug/URL), Ref, Target
ephemeral  Target only — no Path, no Source, never Writable

type WorkspaceSourceType added in v0.5.0

type WorkspaceSourceType string

WorkspaceSourceType discriminates one entry in a Workspace's composition.

const (
	WorkspaceSourceTypeLocalDir WorkspaceSourceType = "local_dir" // binds a host directory
	WorkspaceSourceTypeRepo     WorkspaceSourceType = "repo"      // clones a git repo
	// WorkspaceSourceTypeEphemeral is a sandbox-lifetime scratch dir, the
	// composition floor ensuring "one-or-more sources" never means zero.
	WorkspaceSourceTypeEphemeral WorkspaceSourceType = "ephemeral"
)

type WorkspaceStatus

type WorkspaceStatus string

WorkspaceStatus is the onboarding/scan lifecycle of a Workspace: not-yet- scanned, mid-scan, scanned, or errored. Later import-pipeline stages are RETIRED (migration 0029 collapsed every row back to scanned/pending_scan).

const (
	WorkspacePendingScan WorkspaceStatus = "pending_scan" // onboarded, not yet scanned
	WorkspaceScanning    WorkspaceStatus = "scanning"     // scan run in flight
	WorkspaceScanned     WorkspaceStatus = "scanned"      // profile derived, ready to use
	WorkspaceError       WorkspaceStatus = "error"        // last scan attempt failed
)

Jump to

Keyboard shortcuts

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