types

package
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 17 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 (
	// 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 (
	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 (
	IntegrationKindAnthropicAPIKey       = "anthropic_api_key"
	IntegrationKindAnthropicSubscription = "anthropic_subscription"
	IntegrationKindBedrock               = "bedrock"
	IntegrationKindOpenAIAPIKey          = "openai_api_key"
	// IntegrationKindAzureOpenAI is GONE as of 0.5 (its one caller, the AI Run
	// Composer, no longer exists); a stored row of this kind now fails closed
	// like any other unknown kind.
	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 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 AgentProviderTypeBedrock = "bedrock"

AgentProviderTypeBedrock is the COARSE provider type the four bedrock_* values fold to — the vocabulary of harnessDef.ProviderTypes.

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

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.

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.

View Source
var ClosedADOTokenModes = map[ADOTokenMode]bool{
	ADOTokenModeBearer: true,
}

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

ClosedAgentMechanisms is the closed mechanism set — the only values a write may name; validateAgentProviders refuses anything else.

ClosedCredentialSources is the closed source set — see ClosedAgentMechanisms.

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). The four AI kinds left it in 0.8: model access is a model provider, and a stored AI row is kept only for the conversion to read (AIProviderKind).

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.

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.

PerUserMechanisms is the closed set of lanes a per_user row may name: the ones whose credential a member can actually hold as their OWN — the captured SSO session, or the bearer key written under their own principal. SECURITY: every other lane is an operator-namespace read, so declaring it per_user would promise one credential per person and silently serve the admin's instead — the substitution per_user exists to refuse.

Functions

func AIProviderKind added in v0.5.0

func AIProviderKind(kind string) bool

AIProviderKind reports whether kind is one of the retired AI provider flavors. No resolver hands such a row out any more; it names what the conversion to model providers reads.

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 ClosedAgentMechanismList added in v0.7.2

func ClosedAgentMechanismList() []string

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

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

func PerUserMechanismList() []string

PerUserMechanismList is PerUserMechanisms in a stable order — see ClosedAgentMechanismList.

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.ProfileRead
	TokenMode         ADOTokenMode          `json:"token_mode,omitempty"`      // empty reads as ADOTokenModeBearer
	// 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) 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.ProfileRead 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 an Entra-lane run presents itself to Azure DevOps: one legal value today, kept as 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; zero value, only mode accepted
	// ADOTokenModeMintedPAT would exchange the Entra token for a short-lived
	// PAT per run. Refused and absent from ClosedADOTokenModes: Azure DevOps
	// mints PATs only for Microsoft's own first-party clients (measured: 401
	// TF400813). Stays named so the refusal can say why.
	ADOTokenModeMintedPAT ADOTokenMode = "minted_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"`
	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
}

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 AgentMechanism added in v0.7.2

type AgentMechanism string

AgentMechanism is the ONE model-access lane an agent's runs may use, drawn from the lanes dispatch already has: three Anthropic/OpenAI ones, Bedrock's four sub-lanes, and none (BYOA).

The four bedrock_* values FOLD to the single coarse "bedrock" the harness catalog is keyed by; spelled out here because the sub-lane is what the admin chose and what a refusal must name — "Bedrock is configured" told a customer nothing.

const (
	// AgentMechanismAnthropicSubscription is the managed container-login
	// subscription token (setup-token), injected at the proxy.
	AgentMechanismAnthropicSubscription AgentMechanism = "anthropic_subscription"
	// AgentMechanismAnthropicAPIKey is the stored anthropic-api-key lane.
	AgentMechanismAnthropicAPIKey AgentMechanism = "anthropic_api_key"
	// AgentMechanismOpenAIAPIKey is the stored openai-api-key lane.
	AgentMechanismOpenAIAPIKey AgentMechanism = "openai_api_key"
	// AgentMechanismBedrockBearer is the stored bedrock-api-key bearer lane.
	AgentMechanismBedrockBearer AgentMechanism = "bedrock_bearer"
	// AgentMechanismBedrockSSO is a captured AWS SSO session — per-principal,
	// captured by signing in rather than stored. See PerUserMechanisms.
	AgentMechanismBedrockSSO AgentMechanism = "bedrock_sso"
	// AgentMechanismBedrockEnv is the daemon's own AWS environment.
	AgentMechanismBedrockEnv AgentMechanism = "bedrock_env"
	// AgentMechanismBedrockAWSDir is the host ~/.aws mount (read-only by
	// contract, never refreshed by Wardyn).
	AgentMechanismBedrockAWSDir AgentMechanism = "bedrock_aws_dir"
	// AgentMechanismNone is BYOA: Wardyn wires no credential. The only
	// mechanism a non-catalog agent (a WARDYN_AGENT_IMAGES key) may name.
	AgentMechanismNone AgentMechanism = "none"
)

func (AgentMechanism) ProviderType added in v0.7.2

func (m AgentMechanism) ProviderType() string

ProviderType folds m onto the COARSE ai-integration type the harness catalog is keyed by: every bedrock_* sub-lane becomes "bedrock"; the three Anthropic/OpenAI names map 1:1. AgentMechanismNone folds to itself and is decided by the harness row's NoManagedAuth flag instead.

func (AgentMechanism) Valid added in v0.7.2

func (m AgentMechanism) Valid() bool

Valid reports whether m is one of the closed mechanisms.

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"`
	// Mechanism is the one lane this agent's runs may use. SECURITY: NO
	// cross-mechanism fallback once declared — a run whose lane is dead is
	// refused naming the lane and its state, never silently served by a
	// different vendor's credential.
	Mechanism AgentMechanism `json:"mechanism"`
	// CredentialSource is whose credential that lane uses. Empty reads as
	// CredentialSourceShared.
	CredentialSource CredentialSource `json:"credential_source,omitempty"`
	// SSOStartURL is the AWS access portal every principal signs in against,
	// REQUIRED for bedrock_sso + per_user and forbidden otherwise.
	// SECURITY, ADMIN-OWNED: a member's login launch ignores the request's
	// start URL and uses this one, so a member can't bind their capture to a
	// foreign IdP/account.
	SSOStartURL string `json:"sso_start_url,omitempty"`
	// SSOAccountID and SSORoleName pin WHICH AWS account and role a sign-in
	// for this row may capture. Optional, set together, permitted only
	// alongside SSOStartURL. Admin-owned for the same reason: the sign-in
	// proposes, the roster disposes.
	SSOAccountID string `json:"sso_account_id,omitempty"`
	SSORoleName  string `json:"sso_role_name,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. Empty today; unread by run
	// create/dispatch yet.
	DefaultProvider string `json:"default_provider,omitempty"`
}

AgentProvider is one agent row: which agent, whether it is offered, the ONE lane its runs reach a model on, and whose credential that is.

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, how each reaches its model, and whether that credential is shared or per-person. 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 and mechanism are checked against the harness catalog / boot image map — 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:"-"` // mirrors identity.Claims.OperatorOwned so dispatch/revive re-mint read what create decided
	// 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 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 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"`
}

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 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 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
	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 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 an agent's runs 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. Permitted for PerUserMechanisms only.
	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 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
	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 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.
	DenyInteractive bool `json:"deny_interactive,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"`
	// 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.
  • 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.
  • 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"`
}

GovernanceProfile is one named, assignable ceiling (migration 0052's governance_profiles row).

Ceiling is a full RunPolicySpec with REPLACEMENT semantics, not composition: an assigned profile IS the principal's ceiling; with no assignment a principal falls through to Config.DefaultPolicy byte for byte. composer.Clamp is NOT a lattice meet (drops workspace_mounts unconditionally, is order-dependent through llm_inspection, defaults unnamed tools to hold), so a "composed" ceiling would silently lose fields and couldn't express an autonomous profile.

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

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 —
	// WorkspaceLLMCred.IntegrationRef/DefaultFor 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"`
	// DefaultFor lists what this is the operator-chosen default for:
	// "agent_runs" and/or "wardyn_features" (Wardyn's own internal usage).
	DefaultFor []string  `json:"default_for,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.

func AIKeyDelivery added in v0.5.0

func AIKeyDelivery(kind string) *IntegrationDelivery

AIKeyDelivery is the proxy-header injection convention an AI api-key kind's "api_key" secret rides (mirrors the harness catalog's Gateway rows). nil for a kind with no proxy-header lane.

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

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

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

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. DenyNewExecutables and MaxFileSizeMiB are reserved for later. 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 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 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"`
	// 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"`
	// 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 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 WorkspaceBedrockRef added in v0.4.0

type WorkspaceBedrockRef struct {
	Region string `json:"region,omitempty"`
	Model  string `json:"model,omitempty"`
}

WorkspaceBedrockRef is a workspace's Bedrock model selection (non-secret; AWS credentials are unchanged).

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 {
	// IntegrationRef names the Integration model/harness access resolves through.
	IntegrationRef string `json:"integration_ref,omitempty"`
	// ProviderRef pins a run to one model provider (by id), replacing
	// IntegrationRef once AI-kind integrations retire. A disallowed/ineligible
	// pin refuses the run rather than falling through to another provider.
	ProviderRef string `json:"provider_ref,omitempty"`
}

WorkspaceLLMCred is the operator-owned model/harness credential binding on a workspace: a run that picks it inherits model access via the named Integration (refs/names only). Nil/IntegrationRef="" means no binding; the run uses the global provider config.

func (*WorkspaceLLMCred) UnmarshalJSON added in v0.5.0

func (c *WorkspaceLLMCred) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes WorkspaceLLMCred, tolerating the pre-Integration wire shape ({"mode":...,"api_key_secret":...,"bedrock":{...}}); those fields have no home here, so they're silently dropped, yielding an empty IntegrationRef rather than a decode error.

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