types

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package types defines Wardyn's core domain vocabulary: the four nouns (AgentRun, RunPolicy, CredentialGrant, ApprovalRequest) plus the audit event shape. These types are the single source of truth shared by the control plane, runners, sidecars, and (on Kubernetes) the CRD layer.

Index

Constants

View Source
const (
	IntegrationKindAnthropicAPIKey       = "anthropic_api_key"
	IntegrationKindAnthropicSubscription = "anthropic_subscription"
	IntegrationKindBedrock               = "bedrock"
	IntegrationKindOpenAIAPIKey          = "openai_api_key"
	// IntegrationKindAzureOpenAI is GONE as of 0.5. Its single capability was
	// powering Wardyn's own features — the AI Run Composer — which no longer
	// exists; no agent tool can be pointed at an Azure OpenAI deployment (see
	// harness.go's reasonXAzureHarness). A stored row of the old kind now fails
	// closed at validateIntegrationWrite like any other unknown kind, and an
	// azure-openai-key secret is left untouched but inert.
	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 Wardyn has bespoke behavior for (provider transports, brokered clone lanes); ANY other kind is a GENERIC open slug (shape-validated), whose row carries its own secrets/egress/config — which is everything the runtime needs. UI grouping DERIVES from kind (AI set → "AI providers", github_app/ git_host → "Source control", else → "Connections"); there is no stored category.

View Source
const (
	// DeliveryProxyHeader: the egress proxy presents the secret in an HTTP
	// header on requests bound for the integration's egress hosts. The sandbox
	// never holds it. The default, never-resident lane.
	DeliveryProxyHeader = "proxy_header"
	// DeliveryResidentFile: the secret is materialized as a file inside the
	// sandbox — a disclosed exception to never-resident.
	DeliveryResidentFile = "resident_file"
	// DeliveryResidentEnv: the secret is exported as an environment variable
	// inside the sandbox — a disclosed exception to never-resident.
	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 attached source declares.

View Source
const IntegrationCredentialToken = "token"

IntegrationCredentialToken is the secret ROLE a generic header-delivered credential uses — the proxy-injected lane every header-authenticating system shares. The closed kinds keep their own role names, which encode a lane the runtime treats differently ("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 (see internal/api/harnesscred.go). This is what lets a COMPOSE/ containerized deployment — whose distroless wardynd has 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 its SOURCE (managed store vs resident host ~/.claude), so the audit trail names which one credentialed a run.

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

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

Variables

View Source
var (
	// ErrApprovalAlreadyDecided: a decision was attempted on an approval that
	// has already left PENDING. Fail closed — never let a second decision
	// silently 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, i.e. a concurrent raise lost the
	// race. It is a dedup signal (re-read the winner), NOT a hard failure.
	ErrDuplicatePendingApproval = errors.New("duplicate pending approval")
)

The approval sentinels live here, in the one package both internal/store and internal/approval already import, so the FSM can errors.Is a store error instead of matching its message text (which it used to do, silently breaking the moment either message was reworded or wrapped). store.ErrAlreadyDecided / approval.ErrAlreadyDecided and store.ErrDuplicatePending are aliases of these.

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. A write naming one is validated against that kind's contract (config keys, DefaultFor eligibility); anything else is refused (validateIntegrationWrite).

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

ConfinementClassNames maps each wire code to the friendly display label the UI shows instead (ui/src/app/components/wardyn/cc-meta.ts's CC_META labels) — the Go-side source of truth for that mapping, so the CLI's --confinement alias parsing and the /healthz payload can't drift apart.

Functions

func AIProviderKind added in v0.5.0

func AIProviderKind(kind string) bool

AIProviderKind reports whether kind is one of the five AI provider flavors — the set eligible for DefaultFor marks and the run-time model-credential fold (applyIntegrationCreds), and the "AI providers" UI group.

func ClosedIntegrationKindList added in v0.5.0

func ClosedIntegrationKindList() []string

ClosedIntegrationKindList is ClosedIntegrationKinds in a stable order, for the "want one of: …" half of a rejected write's error. Sorted so the message is deterministic across map iterations.

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, evaluated in order:

  1. Each attachment in order contributes its source's contract. A dangling SourceID (source deleted out from under it) contributes nothing — the mount gate is where that failure surfaces loudly, not here.
  2. An override of "off" drops that source's contribution for that key.
  3. An override lane (optional|required) replaces the source's lane for that contribution.
  4. SECURITY: a write:<path> key whose path is not the source's own Locator is DROPPED. applyWriteNarrowing widens ANY mount whose source path matches, so without this a shared source could declare write on a path it doesn't own and silently widen a sibling mount in every workspace that attaches it. A source may only claim write on itself.
  5. Collisions across attachments merge: Level = strongest (required beats optional); Provenance = WEAKEST (scan_seeded beats operator_set). Fail-closed on purpose: the run-create trust boundary auto-mints only operator_set secrets, so if ANY contributor's row came from reading untrusted repo content, the merged row must not auto-grant. A source's own operator_set row alone still auto-grants — an operator editing a shared source's contract is a direct operator act. ponytail: fail-closed on provenance collision; the workspace overlay is the one-row override if this is ever too strict.
  6. An overlay row REPLACES the merged value outright (level AND provenance). The overlay is what this workspace additionally declares or restates; overrides are how it refuses or re-lanes a source's row. The overlay deliberately cannot REMOVE a key — "off" is for that.

Pure: no store, no clock. With no attachments (or all-ephemeral), the result is exactly the overlay — the identity that makes migration 0031 provably behavior-identical for every pre-split workspace.

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 legally contain colons) and reports whether the type token is known. This is the grammar's own parser, living beside the grammar; internal/api aliases it for its call sites.

Types

type ActorType

type ActorType string

ActorType distinguishes who performed an action in the audit stream. This is the attribution field the incumbents lack.

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

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 is the run's human NAME. Runs that share a title are grouped in the
	// console's run list. Empty for legacy rows and for system runs (scan,
	// harness login, workspace record/verify) — the console falls back to Task,
	// which is the only identity an untitled run has ever had.
	Title string `json:"title,omitempty"`
	// Description is optional free-text context: why this run exists. Never
	// interpreted by the control plane, only displayed.
	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 is the RESOLVED sandbox image this run dispatched with (convention
	// image, devcontainer build, workspace-built, or BYOI-wrapped), persisted
	// for provenance. Written by a scoped update after image resolution; empty
	// for legacy rows and runs that never reached resolution.
	Image string `json:"image,omitempty"`
	// Interactive marks a run created for human-driven use: the sandbox is brought
	// up RUNNING but no agent task is exec'd and no completion watcher is started
	// (the human drives via `wardyn attach`). A non-interactive run execs the agent
	// with the task and is watched to completion. This is a first-class,
	// sandbox-determining choice — see internal/api dispatch.
	Interactive bool `json:"interactive"`
	// WorkspacePath is the primary host directory this run operates in (the first
	// local WorkspaceMount source resolved from its policy), denormalized here so
	// the control plane can DISCOURAGE — warn, never block — launching a second
	// independent agent against a host directory another active run already uses.
	// Empty for runs with no local host workspace (git-clone / ephemeral).
	WorkspacePath string `json:"workspace_path,omitempty"`
	// WorkspaceID, when set, marks this run as a governed SCAN run for that
	// onboarded workspace: the driver runs wardyn-scan after cloning instead of the
	// agent, and the scan-result endpoint persists the derived profile onto this
	// workspace from this TRUSTED linkage (not sandbox input). Nil for ordinary runs.
	WorkspaceID *uuid.UUID `json:"workspace_id,omitempty"`
	// WorkspaceIDs is the READ-ONLY denormalization of the onboarded workspaces
	// this run's RESOLVED policy spec referenced at create time
	// (referencedWorkspaces over spec.WorkspaceMounts + spec.WorkspaceRepos).
	// Set only by handleCreateRun (internal/api/runs.go); the four internal
	// step-run call sites (record/verify, scan, harness login, probe) leave it
	// nil. Distinct from WorkspaceID above, which stays the scan/verify/
	// record-only TRUSTED linkage a sandbox upload authorizes on: this column
	// grants nothing, is never sandbox input, and exists only to answer "which
	// workspace(s) does an `always` egress-approval decision persist to". Nil
	// for a run that references no onboarded workspace.
	WorkspaceIDs []uuid.UUID `json:"workspace_ids,omitempty"`
	// SourceID is the TRUSTED run→library-source linkage for a per-source scan
	// run (the three-tier retarget): the scan-facts upload authorizes on it the
	// way workspace runs authorize on WorkspaceID. Never set by user runs.
	SourceID *uuid.UUID `json:"source_id,omitempty"`
	// AutoStopAfterSec is the run's EFFECTIVE idle auto-stop cap, captured from the
	// resolved RunPolicySpec at creation (frozen for the run's life). The idle
	// reaper reads it from the run row so a run launched with an inline/default
	// policy — which has no stored policy_id to JOIN — is reaped like any other.
	// 0 = never auto-stop; <0 = explicitly never (interactive). See adapters.go.
	AutoStopAfterSec int `json:"auto_stop_after_sec,omitempty"`
	// AgentExecID is the docker exec id of the agent process for exec-based runs
	// (the default idle-container + `docker exec` path). Empty for exec-less /
	// main-process substrates (krun) and before Exec runs. Persisted so the crash
	// reconciler can observe AGENT liveness via ExecInspect across a wardynd
	// restart: for an idle-container run the container is `sleep infinity`, so
	// container liveness != agent liveness, and the exec id otherwise lived only in
	// the driver's in-memory map — lost on restart, stranding the run.
	AgentExecID string `json:"agent_exec_id,omitempty"`
}

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

type ApprovalDecision added in v0.5.0

type ApprovalDecision struct {
	State     ApprovalState
	DecidedBy string
	Reason    string
	// Scope and ExpiresAt are meaningful only for an egress_domain approval;
	// every other kind leaves them at the zero value. See ApprovalScope.
	Scope     ApprovalScope
	ExpiresAt *time.Time
}

ApprovalDecision is what a human — or the sweeper, for a stale-PENDING expiry — is deciding: which state the approval moves to, who decided and why, and (egress_domain approvals only) how far the decision reaches. It is threaded as ONE value through DecideApproval and ApprovalService.Decide rather than growing those signatures to five-plus positional parameters.

ExpireStale is the one caller that leaves Scope at its zero value (""), never ScopeRun: an expiry is a sweep nobody decided, and ApprovalScope's own doc already explains why an unmade decision must never be asserted as "run"-scoped.

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

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 human's decision reaches: once (one
	// connection), run (rest of this run — the default and the legacy meaning),
	// until (this run, until DecisionExpiresAt), or always (persisted onto the
	// workspace so future runs inherit it). Empty on a PENDING row — a decision
	// nobody has made yet has no scope — which is why the column is
	// NOT NULL DEFAULT ” rather than DEFAULT 'run'.
	//
	// Named decision_scope, NOT scope: RequestedScope above is the unrelated
	// host JSON, and it is part of the PENDING dedup unique index.
	DecisionScope ApprovalScope `json:"decision_scope,omitempty"`
	// DecisionExpiresAt is set only when DecisionScope is until. Nullable, so it
	// scans into a pointer the way DecidedAt does. NOT the same clock as the
	// EXPIRED state / approval.expire sweeper, which ages out stale PENDING
	// requests — this bounds a GRANT that was actually made.
	DecisionExpiresAt *time.Time `json:"decision_expires_at,omitempty"`
}

ApprovalRequest is a blocking human-in-the-loop gate. RequestedScope is EXACTLY what the approver saw; the broker writes MintedJTI back in the same transaction as the mint, yielding the provable join "approval X by human Y minted credential Z".

type ApprovalScope added in v0.5.0

type ApprovalScope string

ApprovalScope is how far a human's approve/deny decision reaches. It is ORTHOGONAL to FirstUseMode: that policy setting decides whether an unknown host is escalated to a human at all; this decides the blast radius of the answer. Only egress_domain approvals carry a non-default scope — a credential mints exactly once by construction and a tool_call is bounded by the clamp.

const (
	// ScopeOnce releases exactly one connection. For HTTPS that is one CONNECT
	// tunnel (which may carry many requests); for plain HTTP the proxy
	// re-evaluates per request, so it really is one request there.
	ScopeOnce ApprovalScope = "once"
	// ScopeRun holds for the rest of the run. This is the legacy meaning of
	// every decision made before scopes existed, and stays the default.
	ScopeRun ApprovalScope = "run"
	// ScopeUntil holds for the rest of the run OR until DecisionExpiresAt,
	// whichever comes first. Requires DecisionExpiresAt.
	ScopeUntil ApprovalScope = "until"
	// ScopeAlways additionally persists the host onto the run's workspace
	// (approved_egress / denied_egress) 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, and it treats its two "not a known scope" cases DIFFERENTLY on purpose:

  • EMPTY means "an old client, or a row written before this column existed". Those already meant run-scoped, so widening them to anything else would silently change shipped behavior. Empty => ScopeRun.
  • UNKNOWN NON-EMPTY can only come from a NEWER control plane writing a scope this binary does not understand (Valid() rejects garbage at the boundary). Treating that as ScopeRun would be fail-OPEN across versions, so it floors to the tightest scope instead. Unknown => ScopeOnce.

Honest caveat: "tightest" is allow-shaped. On a DENY, once is the WIDER choice (it re-raises; run stays denied), so an unknown scope from the future turns a cached deny into a re-raise. That fails closed at the proxy — the re-raise is denied again under deny_with_review — but it does cost queue noise, so this is not uniformly fail-closed and should not be described as if it were.

func (ApprovalScope) Valid added in v0.5.0

func (s ApprovalScope) Valid() bool

Valid reports whether s is empty (unset => legacy run-scoped) or one of the four known scopes. Used to reject a garbage value at the API write boundary, mirroring FirstUseMode.Valid; 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"
)

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 (BaseURL -> To, unchanged semantics). See SiteConfig.ArtifactOverrides for why the type is kept.

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

AuditEvent is one append-only audit record. Every credential mint/revoke, approval decision, policy change, egress decision, and lifecycle change emits one. Events carry the delegation chain (human sub + agent run).

Action namespaces (the dotted-verb prefix discriminates the source stream):

credential.*  identity.*  approval.*  policy.*  egress.*  run.* recording.*
    — control-plane / agent self-report events (Postgres event log + PTY).
kernel.*      — the eBPF/Tetragon GROUND-TRUTH stream (the tamper-proof
                second stream). Emitted ONLY by the host-scoped sensor
                (cmd/wardyn-tetragon-ingest) via POST /api/v1/internal/
                groundtruth, which FORCES actor_type=system +
                actor="wardyn-tetragon-ingest" and rejects any action that
                does not carry the "kernel." prefix. The defined kernel.*
                actions are:
                  kernel.process.exec    — observed execve
                  kernel.network.connect — observed outbound TCP connect
                  kernel.file.write      — observed write to a sensitive path
                  kernel.sensor.heartbeat— sensor liveness (run_id NULL)
                  kernel.sensor.blind    — host eBPF blind to a run (CC3/Kata)

Data shape for the kernel.* (ebpf) stream. audit_events.data is JSONB, so this requires NO schema change — it is a documented convention over the existing column. Every kernel.* event carries:

{
  "stream": "ebpf",                 // discriminates from agent self-report
  "subtype": "process_exec" | "network_connect" | "file_write" | ...,
  "cgroup_id": <uint64>,            // kernel cgroup id (omitempty)
  "container_id": "<id>",           // attributed container (omitempty)
  "argv": [...] | "dst": "ip:port" | "path": "/...",  // kind-specific
  "loader": true,                   // exec of a dynamic linker (ld-linux/
                                    // ld-musl) — the documented ld-linux/
                                    // mmap bypass surface, FLAGGED not blocked
  "correlation": "mapped" | "unmapped",  // unmapped => run_id NULL, never
                                          // silently dropped (visible blindness)
  "reason": "...",                  // sensor.blind / failure detail (omitempty)
  "dropped_total": <uint64>         // heartbeat only: sensor backpressure drops
}

Outcome stays within the existing CHECK ("success"|"failure"|"denied"): the kernel stream uses "success" for normal observations and "failure" for the unexpected — an unmapped/escape signal such as a connect to a private/ link-local/metadata (non-proxy) address, or a sensor.blind coverage gap. It never uses "denied": this is a DETECTION stream, it does not block. See internal/groundtruth for the canonical action/data definitions.

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 is the ref: the image itself (registry/byo) or the FROM (custom).
	Image string `json:"image"`
	// Steps are Dockerfile lines layered on Image (custom only).
	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", which is a per-workspace DERIVED build (computed from that workspace's merged source profiles) and so has no catalog identity; the store enforces this with a CHECK constraint so it is structural, not a convention.

type ConfinementClass

type ConfinementClass string

ConfinementClass declares how strongly a sandbox substrate can actually confine an agent. Policy may refuse high-trust credential scopes to weak classes; the UI must always display the class. 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 the runsc runtime registered; see `wardyn setup wall`).
	CC2 ConfinementClass = "CC2"
	// CC3: Kata microVM (requires /dev/kvm).
	CC3 ConfinementClass = "CC3"
)

func (ConfinementClass) Rank

func (c ConfinementClass) Rank() int

Rank orders Confinement Classes weakest→strongest (CC1<CC2<CC3). Unrecognised values rank 0 (below CC1) so they never satisfy a real minimum — callers that gate on a minimum class fail closed. Matching is exact; normalise the string first if the input is untrusted.

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. Eligibility is not issuance: minting happens only via the broker, and for RequiresApproval grants only inside the same DB transaction that verifies an APPROVED ApprovalRequest for this run+scope.

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 is
	// substituted to. For an Ecosystem row this is also the value emitted into
	// that ecosystem's config file (the old ArtifactOverride.BaseURL).
	To string `json:"to"`
	// TokenSecretRef optionally names a secret whose value is injected
	// proxy-side as a Bearer token for To's host (the sandbox never holds it).
	// Mutually exclusive with TokenIntegrationRef — validateSiteConfig rejects a
	// row that sets both, since they answer the same question two ways.
	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 naming
	// a bare secret in TokenSecretRef.
	//
	// This is the seam between the two surfaces, and it exists because a private
	// registry is genuinely both things: a SYSTEM you authenticate to, and
	// sometimes the DESTINATION a public endpoint is rerouted to. The rule that
	// keeps them from duplicating each other — the integration owns the system
	// and its credential; the redirect owns rerouting a public endpoint to it.
	// So a redirect points AT the integration rather than restating its secret.
	//
	// It carries more than the secret name: the integration's Header and Format
	// come with it, so a feed that authenticates with something other than
	// "Authorization: Bearer" finally can (the bare-secret path below is
	// hardcoded to that shape). A ref naming nothing, a disabled row, or one with
	// no header credential degrades to redirect-WITHOUT-token, exactly as a
	// dangling TokenSecretRef already does — a redirect that still reroutes is
	// more useful than a run that fails.
	TokenIntegrationRef string `json:"token_integration_ref,omitempty"`
	// Ecosystem, when set, is one of the six package-manager ecosystems
	// ("npm"|"pip"|"cargo"|"maven"|"go"|"nuget") this redirect ALSO emits a
	// per-tool config file for, in addition to the egress substitution and
	// token injection every redirect gets. Empty means NETWORK-ONLY: no config
	// file is emitted (see SiteConfig.EgressRedirects).
	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. See SiteConfig.EgressRedirects for the two-tier Ecosystem behavior. From/To are validated (validateSiteConfig) with the same control-char/shell-metacharacter/real-host discipline as the legacy ArtifactOverride.BaseURL — either a full http(s) URL (validSiteURL) or a bare host (validSiteHost); workspacescan.EmitArtifactConfig relies on that safety for its raw string interpolation into .npmrc/settings.xml/etc, so keep any future validation change there in sync.

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), so existing stored JSONB policies decode unchanged. It is 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 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 Personal Access Token VALUE to the git
	// credential helper (username/password) for a matched non-GitHub git host
	// (Azure DevOps / GitLab). This is the OPPOSITE of api_key: git-over-HTTPS to
	// those hosts is an opaque CONNECT tunnel the proxy cannot inject Basic-auth
	// into, so the credential must reach git via the helper — like github_token.
	GrantGitPAT GrantKind = "git_pat"
	// GrantSSHKey materializes a RESIDENT, agent-readable SSH private key so a run
	// can clone git-over-SSH. It is a DOCUMENTED EXCEPTION to the no-resident-
	// secret invariant: git's SSH transport has NO credential-helper seam (git
	// credential.helper is HTTP-only), so neither the git_pat helper trick nor
	// api_key proxy-side injection can carry the key — it MUST land as a 0400 file
	// the ssh client reads. agent-run writes it just before the clone and wipes it
	// right after (see deploy/images/*/agent-run), so the readable window is the
	// clone only. Transport is SSH-over-443 (ssh.github.com / ssh.dev.azure.com)
	// through the wardyn-proxy CONNECT tunnel — no port-22 egress. Residual risk is
	// the same posture as WARDYN_GIT_HELPER_SECRET: code running AS the agent uid
	// can read the key during that window. See broker.mintSSHKey + threat model.
	GrantSSHKey GrantKind = "ssh_key"
)

type GrantSpec

type GrantSpec struct {
	Kind GrantKind `json:"kind"`
	// Scope is kind-specific: for github_token {"repos":[...],"permissions":{...}},
	// for api_key {"host":"...","header":"..."}, for git_pat
	// {"host":"...","secret_name":"...","username":"<optional>"} — the stored
	// secret_name's PAT value is returned to the git credential helper as the
	// password for host, with username resolved by convention (ADO=pat,
	// GitLab=oauth2) unless overridden — and for ssh_key
	// {"host":"...","key_secret_ref":"...","username":"<optional, default git>",
	// "known_hosts_secret_ref":"<optional>"} — the stored key_secret_ref's private
	// key VALUE is returned to agent-run, which writes it to a 0400 file for the
	// SSH-over-443 clone and wipes it after (see GrantSSHKey).
	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"`
}

GrantSpec is a credential scope description. The broker enforces the invariant: a minted credential's scope is exactly the approved scope — never wider (no scope-widening between request and mint).

type Integration added in v0.5.0

type Integration struct {
	// ID is an operator-chosen stable slug, unique within SiteConfig.Integrations
	// (this is a config sub-object inside the SiteConfig singleton, not its own
	// table, so there is no generated uuid — the operator names it, and
	// WorkspaceLLMCred.IntegrationRef / DefaultFor point at this ID).
	ID   string `json:"id"`
	Name string `json:"name"`
	// Kind is the ONE field that says what this connects to: a closed-set kind
	// (ClosedIntegrationKinds) with bespoke behavior, or any other slug — a
	// generic connection whose row carries its whole contract.
	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: the host entries a run granted this
	// integration may reach. This is the reason a host is on a run's egress
	// allowlist, instead of being hand-listed in every workspace that needs it.
	// Entries use the policy allowlist's own syntax (proxy.ValidDomainEntry):
	// an exact host, a leading-"*." wildcard, either optionally ":port".
	//
	// A WILDCARD OPENS THE PATH BUT NEVER CARRIES THE CREDENTIAL: proxy-side
	// injection requires an EXACT allowlist entry (Policy.AllowedExactHost), so
	// a secret can never leak to a wildcard-matched host. An integration with a
	// proxy_header-delivered secret is therefore rejected at write time if any
	// of its egress entries is a wildcard or carries a port — the alternative
	// is a row that looks credentialed and silently isn't.
	Egress []string `json:"egress,omitempty"`
	// Config is non-secret, kind-validated configuration. Closed kinds accept
	// only their known keys (bedrock ⇒ region/model/auth_lane, github_app ⇒
	// app_id/installation_id/host, anthropic_subscription ⇒ lane — an unknown
	// key 400s by name at write); generic kinds take any string keys.
	Config map[string]any `json:"config,omitempty"`
	// Docs optionally points at whatever documents this system, so whoever
	// comes after the operator who added it can find out what it is.
	Docs string `json:"docs,omitempty"`
	// DisabledCapabilities lists capability names this integration does NOT
	// support, so callers don't need a hardcoded per-provider capability matrix.
	DisabledCapabilities []string `json:"disabled_capabilities,omitempty"`
	// DefaultFor lists what this integration is the operator-chosen default
	// for: "agent_runs" (a new run picks this absent an explicit choice) and/or
	// "wardyn_features" (Wardyn's own internal usage, e.g. scan/record runs).
	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, an optional verification probe — extended by kind. Like every other SiteConfig-doctrine type, Secrets holds secret NAMES (refs), never VALUES — the broker/proxy resolve the named secret at dispatch/ injection time.

STORAGE COMPATIBILITY: UnmarshalJSON also accepts the pre-base-component shape ({category, type, hosts, header, format, credentials, config}) and folds it into this one at read time — one-way, write-new (marshal emits only this shape). 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. ONE consumer left, and it wants exactly this shape: the integration.delete audit payload, recording which credential REFS a deleted row named (the operator's secrets themselves are not deleted). Never a read path — anything that ACTS on a secret reads the Secrets rows, because only they carry the delivery that says how it reaches a run.

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 — the generic proxy-injected credential lane — as the (secretName, header, format) triple injection consumes. An empty stored format reads as "%s" (the raw secret IS the header value; injectionRuleFromScope would otherwise default "" to "Bearer %s", which is wrong for every custom credential header). ok=false when no secret is proxy_header-delivered.

func (Integration) RoleSecret added in v0.5.0

func (in Integration) RoleSecret(role string) string

RoleSecret returns the secret NAME stored for role ("" when the role is 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). This is the ONE read-time migration chokepoint: every decode path — the store's SiteConfig document, a saved wire body — folds here, and marshal emits only the new shape (one-way, write-new). Precedent: foldLegacyArtifactOverrides (internal/api/site_config.go).

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: the HTTP field name the proxy adds,
	// and the value template it wraps the secret in (exactly one %s; empty
	// means the raw secret IS the value, i.e. "%s").
	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. Exactly one mode; the mode decides which of the 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 — the same convention the harness catalog's Gateway rows encode (internal/api/harness.go), recorded here so a folded/derived row states the delivery that actually happens. nil for a kind whose api_key has 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: each row folds individually (see Integration.UnmarshalJSON), and rows folded from the legacy artifact_mirror/host_proxy categories are DROPPED — they are network topology, homed under Corporate network, and this surface stops carrying them. A NEW-shape row is never dropped, whatever its kind slug.

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 (what it is to this system), a ref into the secret store (a NAME, never a value), and how it is delivered. A nil Delivery is allowed on CLOSED kinds only and means the kind's own bespoke transport delivers it (the brokered github_app halves, git_host clone credentials) — hand-written per provider, not declarable here.

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
	// (on a stored policy or an inline_policy). This is the storable form of the
	// detection corpus: a name is not sensitive the way a value is, so it may
	// freely appear in a stored policy row, a policy read, or a compose/profile
	// proposal returned to a caller. Resolved to WorkspaceSecretValues ONLY at
	// dispatch (resolveLLMInspectionSecrets, internal/api/runs_dispatch.go), in
	// memory, on the per-dispatch policy copy handed to the proxy sidecar.
	WorkspaceSecretNames []string `json:"workspace_secret_names,omitempty"`
	// WorkspaceSecretValues are the RESOLVED secret VALUES (e.g. the contents of
	// a mounted .env) the run must not leak into a prompt — the v1 detection
	// corpus the proxy sidecar actually matches against. NOT an authoring
	// field: validatePolicySpec refuses a non-empty value here on every policy
	// write (stored, inline, or WARDYN_DEFAULT_POLICY) — author
	// WorkspaceSecretNames instead. Populated ONLY by dispatch, in memory, on
	// the ephemeral copy of the spec sent to the proxy; every other copy (the
	// stored row, a policy read DTO, a compose/profile proposal, the
	// run.policy.effective audit event) 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 the
	// resolved WorkspaceSecretValues corpus). 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 private 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
	// (e.g. a Presidio / Protect-AI LLM-Guard wrapper) the proxy POSTs each span
	// to. Trusted operator config (not agent-chosen). A sidecar error/timeout/non-200
	// is treated as a scanner error and respects on_scanner_error like any other:
	// fail-OPEN by default (the request still flows), fail-CLOSED (the request is
	// blocked in block mode) 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 (the "proprietary content shouldn't leave" sense of
	// the walled garden). 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 — see threatmodel §5.1a.)
	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 is not 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.
	// 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 — see threatmodel §5.1a. 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 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"`
}

ResolvedInjection is the ONE wire contract of GET /api/v1/internal/injection/{grantID}: the control plane's injection-resolve result, carrying the header name and the FORMATTED secret value (formatting applied server-side). ExpiresAt (unix ms, 0 = never) marks a rotating credential the proxy must re-resolve before it lapses (the subscription OAuth token); a static api-key grant leaves it 0.

It lives here, in the neutral package both sides already import, because the api server (encoder) and the wardyn-proxy (decoder) previously each kept a hand-copied struct. Nothing pinned the two together, so a one-sided rename of expires_at would have silently made the proxy read 0 = "static credential, never re-resolve" and let a live OAuth token lapse mid-run. Sharing the type makes the compiler, not a parity test, the thing that keeps them equal.

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 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 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 (allowlist only) to
	// "allow all (deny-list only)" mode: when true the proxy allows ANY
	// non-denied PUBLIC host, and AllowedDomains may be empty. denied_domains
	// STILL wins. The SSRF/private-IP guard (VetHost/isBlockedIP) is unaffected
	// — allow-all is public hosts only; a host that resolves to a private/
	// loopback/link-local/metadata range is still unconditionally denied. And
	// credential injection STILL requires an EXACT allowlist entry (AllowedExactHost),
	// so allow-all never widens where a secret may be injected — a secret must
	// never leak to an arbitrary host. first_use_approval is inert under allow-all.
	AllowAllEgress bool `json:"allow_all_egress,omitempty"`
	// FirstUseApproval controls how an unknown domain is handled: always_deny
	// (hard-deny, no approval), deny_with_review (raise approval, deny now, retry
	// passes once approved), or wait_for_review (raise approval and hold the
	// connection until decided). Accepts the legacy boolean on the wire. Inert
	// under allow-all.
	FirstUseApproval FirstUseMode `json:"first_use_approval"`
	// 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 (seconds, 0 = platform default). A
	// NEGATIVE value disables idle reaping ("never reap") — this is what an
	// interactive run (which comes up idle, awaiting a human attach) should use,
	// or the reaper will stop it as soon as it looks idle.
	AutoStopAfterSec int `json:"auto_stop_after_sec,omitempty"`
	// WorkspaceMounts are OPERATOR/ADMIN-controlled host bind mounts injected
	// into the sandbox (e.g. a host repo at ~/work that edits persist to). They
	// are admin-gated: a mount may be authored on a stored policy (via the
	// admin-gated policy CRUD) OR INLINE on a create-run request by an admin /
	// SSO-gated human operator (createRunRequest.InlinePolicy) — both flow
	// through this same RunPolicySpec. A mount is NEVER chosen by the in-sandbox
	// agent: the agent-run entrypoint has no access to either authoring surface,
	// so a prompt-injected agent or a malicious in-sandbox actor can never pick
	// what host paths get mounted. validatePolicySpec runs runner.ValidateMount
	// (the same deny-list the docker driver enforces: absolute cleaned Source not
	// under a dangerous host path; Target under an allowed in-container prefix;
	// default read-only) so a bad mount is rejected at policy-write / inline-
	// validate time (HTTP 400) AND again defense-in-depth in the driver at
	// sandbox-create time.
	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 — the
	// multi-workspace run model. Same admin/inline authoring
	// surface and trust boundary as WorkspaceMounts: never agent-chosen.
	// validatePolicySpec validates each set Target via runner.ValidateTarget and
	// 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). Rejecting a repo whose Source is not an
	// ONBOARDED workspace is a later, security-critical wave — this
	// type only adds the structural shape.
	WorkspaceRepos []WorkspaceRepo `json:"workspace_repos,omitempty"`
	// LLMInspection optionally enables OUTBOUND content inspection at the proxy
	// for brokered LLM routes (the "inadvertent-leak guardrail"). Nil/omitted =>
	// OFF (no scanning) — the safe default, mirroring WorkspaceMount.ReadOnly's
	// pointer-means-unset idiom. It is a guardrail + visibility layer, NOT
	// exfiltration prevention (see internal/contentscan + threatmodel §5.1).
	LLMInspection *LLMInspectionSpec `json:"llm_inspection,omitempty"`
	// Resources caps sandbox CPU/memory/PIDs/disk. Nil, or a zero field, means
	// "use the platform default": the dispatch path fills conservative defaults so
	// EVERY run is capped even when a policy sets nothing. These are the basic
	// multi-tenant safety controls that let a FLEET of independent agents coexist —
	// without them one runaway or prompt-injected agent can OOM-kill the host,
	// fork-bomb the host PID space, or fill host storage and take down sibling runs.
	Resources *ResourceLimits `json:"resources,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 — the struct is duplicated but each slice header still points at the original's backing array. That made 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 in the allowlist handed to its proxy), and any in-place mutation leaked into every later run. Callers that derive 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 a RESERVED, not-yet-produced state: no backend path
	// transitions a run into WAITING_FOR_CONFIRMATION today. It is the planned
	// human-in-the-loop gate — a run that pauses for an operator to confirm a
	// risky action mid-flight — and is deliberately kept wired end to end (UI
	// attention badge in App.tsx/runs.tsx, primitives.tsx label, SDK alias, e2e
	// fixtures) so the display + attention semantics are proven before the
	// producer lands with the approval-gated-run feature. Reserved, not dead:
	// removing it would strip a designed seam the UI already renders.
	RunWaiting  RunState = "WAITING_FOR_CONFIRMATION"
	RunStopped  RunState = "STOPPED"
	RunArchived RunState = "ARCHIVED"
	RunFailed   RunState = "FAILED"
	RunKilled   RunState = "KILLED"
	// RunCompleted is the terminal success state: the agent process exited 0.
	// A non-zero exit transitions the run to RunFailed instead. The completion
	// watcher (see internal/api/runs.go dispatch) sets this from RunRunning.
	RunCompleted RunState = "COMPLETED"
)

func (RunState) IsTerminal added in v0.4.4

func (s RunState) IsTerminal() bool

IsTerminal reports whether the run has already ended — it can no longer be killed, stopped or dispatched. This is the SINGLE source of the terminal set: the API's terminal guards, the CLI's `run --wait` exit codes and the e2e polls all read it here (pkg/client aliases RunState, so SDK consumers get it too), because a hand-copied set drifts — omitting COMPLETED once already shipped a live Kill button on finished runs. The UI keeps the only other copy (ui/src/app/lib/types/runs.ts); TestTerminalRunStates_UIParity fails if the two ever disagree.

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
	CreatedAt   time.Time `json:"created_at"`
}

SSHPublicKey is a human's registered public key for the SSH gateway (`GET`/`POST`/`DELETE /api/v1/me/ssh-keys`), distinct from GrantSpec's "ssh_key" grant kind (a RESIDENT private key materialized for git-over-SSH cloning — see GrantSSHKey). This type is the gateway's own trust root: a human self-registers a public key against their principal, and the gateway authenticates an incoming SSH connection ONLY against rows here, then authorizes it owner-only (AgentRun.CreatedBy == Principal — see internal/api/sshgateway.go).

Fingerprint is the SHA256 form (ssh.FingerprintSHA256: "SHA256:<base64>"), computed SERVER-SIDE from the parsed key — never client-supplied — so it is both the natural primary key (a key's fingerprint is intrinsic to its bytes; two rows can never disagree about which key they name) and the value shown to a human for "verify on first connect".

type SiteConfig

type SiteConfig struct {
	// UpstreamProxySecretRef names a secret holding the corporate upstream proxy
	// URL (optionally with embedded user:pass), or "" when no upstream proxy is
	// configured. Mutually exclusive in PRACTICE with UpstreamProxyURL (either
	// may be set; UpstreamProxyURL wins when both are — see
	// resolveUpstreamProxyURL) but not rejected as a validation error, since an
	// operator migrating from one to the other may round-trip both briefly.
	UpstreamProxySecretRef string `json:"upstream_proxy_secret_ref,omitempty"`
	// UpstreamProxyURL is the corporate upstream proxy URL written IN THE CLEAR
	// (http only — see resolveUpstreamProxyURL). A proxy URL is topology, not a
	// credential, and forcing every operator through the write-only secret store
	// means a mistyped URL can never be read back to debug. It MUST NOT embed a
	// userinfo (user:pass@) — validateSiteConfig rejects that at write time with
	// a 400 telling the caller to use UpstreamProxySecretRef instead, which
	// exists precisely for a proxy that DOES need an embedded credential.
	UpstreamProxyURL string `json:"upstream_proxy_url,omitempty"`
	// ArtifactOverrides maps an ecosystem ("npm"|"pip"|"cargo"|"maven"|"go"|
	// "nuget") to its corporate artifact-registry redirect.
	//
	// Deprecated: superseded by EgressRedirects, which generalizes this from
	// package registries to any outbound URL/host. Kept ONLY so the PUT
	// /site-config request decoder and `wardyn site-config apply` keep accepting
	// a document saved before this release — decodeStrict rejects unknown JSON
	// fields, so removing this field would turn every such legacy body into a
	// hard 400 instead of a fold. handlePutSiteConfig folds a non-empty value
	// into EgressRedirects (rejecting a body that sets both) and never persists
	// this field again; migration 0030 performs the same rewrite once, in place,
	// on the one already-stored document. Never populated by a read from
	// storage post-migration — treat a non-empty value outside the fold as
	// legacy request input only.
	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, generalizing
	// ArtifactOverride from package registries to any destination (a container
	// registry, a telemetry/SDK callback host, ...). Each entry is one of two
	// tiers, discriminated by Ecosystem:
	//
	//   - Ecosystem set (one of the ArtifactOverride closed set): FULL behavior,
	//     unchanged from the old ArtifactOverride — a per-tool config file
	//     (.npmrc/pip.conf/.cargo/config.toml/.m2/settings.xml/NuGet.Config/
	//     GOPROXY+GOSUMDB) via workspacescan.EmitArtifactConfig, PLUS egress
	//     substitution, PLUS token injection.
	//   - Ecosystem "" (NETWORK-ONLY): egress substitution (To's host allowed,
	//     From's host dropped) PLUS token injection for To's host, but NO config
	//     file — there is no ".npmrc equivalent" for an arbitrary host (a
	//     container registry, a telemetry endpoint, ...), and inventing one
	//     would be a lie about what Wardyn actually configures.
	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
	// UpstreamProxySecretRef/EgressRedirects are migrating toward. Both the
	// legacy fields and this one are read; nothing here removes the legacy
	// fields yet. IntegrationList folds pre-base-component rows forward at
	// decode and drops legacy artifact_mirror/host_proxy topology rows.
	Integrations IntegrationList `json:"integrations,omitempty"`
}

SiteConfig is the operator-wide, admin-authored baseline every run inherits: a corporate upstream proxy, egress redirects (package-registry mirrors and, more generally, any outbound URL/host/IP redirect), and default SCM hosts. It is the ONE net-new persistence surface the enterprise Getting-Started enhancements introduce (the Host Proxy and Corporate Network / Egress Redirection steps read it; everything else rides secrets + grants). There is exactly one SiteConfig for the operator (a store singleton); GetSiteConfig returns the zero value when none has been written yet — "unconfigured" is a valid, common state, not an error.

Secret VALUES never live here — only secret NAMES (refs) the broker/proxy resolve at dispatch/injection time, mirroring how RunPolicySpec's GrantSpec.Scope references secrets by name rather than embedding them.

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 directory path (local_dir, trailing
	// slashes trimmed) or the canonical repo slug/clone URL (repo, lowercased).
	// Together with Ref it is UNIQUE in the store — the dedupe rule.
	Locator string `json:"locator"`
	// Ref is an optional git ref (repo only; "" otherwise). Part of identity:
	// the same repo at two refs is two sources with two contracts.
	Ref  string `json:"ref,omitempty"`
	Name string `json:"name"`
	// Requirements is this source's OWN contract. See the type doc above.
	Requirements map[string]WorkspaceRequirement `json:"requirements,omitempty"`
	// Profile is this source's scan result (workspacescan.WorkspaceProfile),
	// opaque here — this type never interprets it, only persists/returns it
	// (matching Workspace.Profile's own doc). json.RawMessage, not a plain
	// []byte: encoding/json base64-encodes a bare []byte, which shipped a
	// tier-1 source's scan profile to GET /sources as an opaque base64 string
	// instead of real JSON (WIRE-2) — the one other place a source's profile
	// crossed the wire (source_scan.go) already worked around this by hand
	// (json.RawMessage(fresh.Profile)).
	Profile json.RawMessage `json:"profile,omitempty"`
	// Status is the source's scan lifecycle — the same one-word states the
	// workspace used to own: pending_scan | scanning | scanned | error.
	Status WorkspaceStatus `json:"status"`
	// ActiveRunID fences this source's in-flight scan run, exactly as the
	// workspace's own field fenced whole-workspace scans before the retarget.
	ActiveRunID *uuid.UUID `json:"active_run_id,omitempty"`
	CreatedAt   time.Time  `json:"created_at"`
	UpdatedAt   time.Time  `json:"updated_at"`
}

Source is one tier-1 library entry: a repo or directory configured once and attached to many workspaces. Its Requirements are what THIS source expects of any sandbox it is used in ("a given repo has its expected requirements") — the same "<type>:<key>" grammar Workspace.Requirements documents, MINUS integration: keys (integrations compose at the aggregate, tier 3, by owner decision; the fold would pass them through unharmed, so relaxing later is a validation change only).

type SourceKind added in v0.5.0

type SourceKind string

SourceKind is a library source's kind. Ephemeral scratch is NOT a kind here: an ephemeral row is a per-workspace inline attachment (nothing to configure, scan, or share), never a library entity.

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

type Workspace

type Workspace struct {
	ID   uuid.UUID `json:"id"`
	Name string    `json:"name"`
	// Sources is the workspace's composition: one-or-more local_dir/repo/
	// ephemeral sources.
	Sources []WorkspaceSource `json:"sources"`
	// BaseImage is the workspace's base-image choice. Nil means the platform
	// default convention image for the detected/scanned stack.
	BaseImage *WorkspaceBaseImage `json:"base_image,omitempty"`
	// Requirements is the workspace's declared requirements contract: what
	// secrets/egress/write-access/integrations a run against this workspace
	// needs, keyed by a fixed grammar of "<type>:<key>" where type is one of:
	//
	//	secret:NAME       a named secret must be resolvable (the store secret
	//	                  NAME), e.g. "secret:acme-anthropic-key"
	//	egress:host       the host must be reachable from the sandbox (a bare
	//	                  host or "*."-wildcard, matching RunPolicySpec.
	//	                  AllowedDomains grammar), e.g. "egress:api.github.com" —
	//	                  unioned into a run's allowlist alongside ApprovedEgress
	//	write:/host/path  the given HOST path must be mounted writable, e.g.
	//	                  "write:/home/user/repo" — matches a Sources[] entry
	//	                  whose Path equals /host/path and Writable=true
	//	integration:ID    the named Integration (SiteConfig.Integrations[i].ID)
	//	                  must be granted — its hosts opened and its credential
	//	                  (if any) presented, e.g. "integration:git_host:
	//	                  github.com" (ID itself may contain colons; see the
	//	                  split rule below)
	//
	// The map key is exactly "<type>:<key>", type and key joined by ONE colon.
	// A key parser MUST split on the FIRST colon only (never the last): the
	// type prefix is always one of the four fixed tokens above, so splitting
	// on the first colon unambiguously separates it from the key even though a
	// host path (a write: key) or an adopted legacy id (an integration: key)
	// may itself legally contain colons.
	//
	// THREE-TIER SPLIT: this map is now the workspace's OVERLAY — what this
	// workspace additionally declares or restates on top of what its attached
	// sources' own contracts contribute. EffectiveRequirements below is the
	// folded result a run actually consumes.
	Requirements map[string]WorkspaceRequirement `json:"requirements,omitempty"`

	// Attachments is the tier-3 composition: ordered source references and
	// inline ephemeral rows, each with per-attachment target/writable and the
	// workspace's requirement OVERRIDES for that source (workspace_contract.go).
	// Empty on a pre-split row that hasn't migrated its embedded Sources yet —
	// the store's hydrate pass falls back to the embedded column then.
	Attachments []WorkspaceAttachment `json:"attachments,omitempty"`
	// BaseImageID references the shared base-image catalog (tier 2). Nil means
	// "recommended" — the 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 + attached source
	// contracts + the overlay above. This is the map run-create and preflight
	// consume. Populated by the store's hydrate pass; on a pre-split row it
	// equals Requirements verbatim (the fold's zero-source identity).
	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 the profile hash changes).
	BuiltProfileHash string `json:"built_profile_hash,omitempty"`
	// ApprovedEgress is the OPERATOR-owned list of egress hosts explicitly
	// promoted for this workspace (typically from the scanner's content-derived
	// SuggestedEgress, which is advisory and never auto-allowed). Unioned into a
	// run's allowlist alongside the scanned profile's EgressDomains. Never
	// written by a scan; cleared when the composition changes.
	ApprovedEgress []string `json:"approved_egress,omitempty"`
	// DeniedEgress is the OPERATOR-owned mirror of ApprovedEgress: hosts
	// explicitly blocked for this workspace, written by a `deny · always`
	// approval decision (AddWorkspaceEgressDecision) or the denied-egress PUT
	// (SetWorkspaceDeniedEgress). Folded into a run's denied_domains at
	// create time, where deny beats allow, allow_all_egress, and a runtime
	// first-use approval alike. UNLIKE ApprovedEgress, this is NOT cleared
	// when the workspace's sources change (see migration 0040): that reset
	// rule is right for a WIDENING (a stale allow should fail closed), but
	// applying it to a NARROWING would silently drop an operator's permanent
	// deny the moment content changed — a deny needs no re-review against new
	// content.
	DeniedEgress []string `json:"denied_egress,omitempty"`
	// ActiveRunID is the in-flight scan/record run for this workspace, so the
	// import panel can poll "is my step still running" without scanning all
	// runs. Nil when no import step is executing.
	ActiveRunID *uuid.UUID `json:"active_run_id,omitempty"`
	// RecordResults is the per-task Record Mode state (taskKey → result: run
	// pointer, mode, status, observations captured server-side from the run's
	// audit events). Opaque map[string]api-owned JSON; written only via the
	// scoped SetWorkspaceRecordResult; cleared when the composition changes
	// (recordings were reviewed against the OLD sources).
	RecordResults json.RawMessage `json:"record_results,omitempty"`
	// LLMCred is the OPERATOR-owned model/harness credential binding: a run that
	// picks this workspace inherits it (refs/names only). Nil => no binding.
	// Folded into the run policy at create (applyWorkspaceCreds); written only
	// via the scoped SetWorkspaceLLMCred, mirroring ApprovedEgress.
	LLMCred   *WorkspaceLLMCred `json:"llm_cred,omitempty"`
	Status    WorkspaceStatus   `json:"status"`
	CreatedAt time.Time         `json:"created_at"`
	UpdatedAt time.Time         `json:"updated_at"`
}

Workspace is an onboarded, admin-reviewed COMPOSITION of one-or-more sources (local_dir | repo | ephemeral — an ephemeral scratch dir is the floor, so Sources is never empty) a run may attach, plus a base image choice and a requirements contract. Import scans/reviews the composition ONCE and persists a profile; runs thereafter reference the workspace by id instead of a free-text host path or repo slug. Repos are re-cloned fresh per run but reuse the scan-once Profile.

Profile is internal/workspacescan's WorkspaceProfile serialized opaquely: this type never interprets it, only persists/returns it — internal/workspacescan owns the shape and BuiltProfileHash cache-keying. Both are empty until Status transitions out of pending_scan.

type WorkspaceAttachment added in v0.5.0

type WorkspaceAttachment struct {
	SourceID *uuid.UUID `json:"source_id,omitempty"`
	// Ephemeral marks an inline scratch row (never a library entity).
	Ephemeral bool `json:"ephemeral,omitempty"`
	// Target is the in-sandbox mount/clone/scratch path for THIS attachment.
	Target string `json:"target,omitempty"`
	// Writable opts a local_dir attachment into read-write (per-attachment,
	// not per-source: the same dir can be writable in one workspace and
	// read-only in another).
	Writable bool `json:"writable,omitempty"`
	// Overrides is this workspace's stance on requirement keys the attached
	// source declares: reqKey -> off|optional|required. "off" means THIS
	// workspace refuses that requirement (mount the repo read-only to read
	// code, without its build secrets/egress) — it 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 reference (SourceID set) or an inline ephemeral scratch row (Ephemeral true, SourceID nil). Order is load-bearing — attachments[0] is the primary, the same rule Sources[0] carried before the split.

type WorkspaceBaseImage added in v0.5.0

type WorkspaceBaseImage struct {
	// Kind is "recommended" (Wardyn's own convention image for the detected
	// stack), "registry" (an operator-picked published image, named by Image),
	// "custom" (Image is a base, Steps layers Dockerfile lines on top), or
	// "byo" (Image is used verbatim, no layering).
	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 are additional Dockerfile RUN/ENV/ARG lines layered on Image.
	// "custom" only.
	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; the AWS credentials themselves come from the store / mounted ~/.aws, unchanged).

type WorkspaceKind

type WorkspaceKind string

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

const (
	WorkspaceKindLocalDir WorkspaceKind = "local_dir"
	WorkspaceKindRepo     WorkspaceKind = "repo"
	// WorkspaceKindContainer was the pre-composition-model shape: an onboarded
	// base IMAGE with no mount (Source was the image ref). The composition
	// model replaces it — a "container" workspace is now an ephemeral Source
	// plus a custom BaseImage — so this value is never derived by
	// deriveWorkspaceMirrors and never written for a new workspace; migration
	// 0029 rewrites every stored 'container' row into that shape.
	//
	// Deprecated: kept for one release so 0029's backfill SQL ('container') has
	// a readable Go anchor, and so any pre-0029 client/fixture payload that
	// still carries "kind":"container" keeps decoding.
	WorkspaceKindContainer WorkspaceKind = "container"
)

type WorkspaceLLMCred added in v0.4.0

type WorkspaceLLMCred struct {
	// IntegrationRef names the Integration (SiteConfig.Integrations[i].ID) this
	// workspace's model/harness access resolves through.
	IntegrationRef string `json:"integration_ref,omitempty"`
}

WorkspaceLLMCred is the OPERATOR-owned model/harness credential BINDING on a workspace: a run that picks this workspace inherits this model access via the named Integration (one of the AI-provider kinds) — the generalized replacement for the old inline {mode, api_key_secret, bedrock} shape. Refs/ NAMES only, never secret values (the SiteConfig precedent): the actual secret lives in the store, resolved/injected via the Integration at dispatch. Mirrors ApprovedEgress's operator-owned discipline (never scan- written; cleared on source change). Nil / IntegrationRef="" => no binding; the run uses the global provider config, or is a plain governed command when it needs no model.

func (*WorkspaceLLMCred) UnmarshalJSON added in v0.5.0

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

UnmarshalJSON decodes WorkspaceLLMCred, tolerating the pre-Integration wire shape a workspace's llm_cred JSONB column may still hold from before this type change ({"mode":"managed"|"api_key"|"bedrock","api_key_secret":"...", "bedrock":{...}}): those fields have no home on this type anymore, so they are silently dropped and the result is an EMPTY IntegrationRef ("no binding") rather than a decode error. There is deliberately no reverse mapping (mode=api_key -> a synthesized integration ref) — a legacy binding names no Integration, so it degrades to unbound rather than guessing one.

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: when the field is OMITTED (nil) the mount is mounted read-only. Read-write requires the policy author to EXPLICITLY set "read_only": false. (A plain bool would default to false == read-write, the unsafe direction; the pointer makes "unset" mean read-only.) Use ReadOnlyOrDefault to resolve the effective value.

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 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 (the multi-workspace run model). Repo is a slug/URL validated the same way the legacy single-repo AgentRun.Repo field is (repoFieldSafe + repoCloneURL, runs.go). Target is an optional in-container clone destination; an empty Target defers to the ~/work/<name> convention a later wave wires up (plan B4, WARDYN_REPOS) — it is NOT resolved here. Ref is an optional git ref (branch/tag/sha) — same field WorkspaceSource carries as part of a repo source's identity (workspace.go); an empty Ref clones the remote's default branch, unchanged from before this field existed. Carried as a 4th tab-separated field in WARDYN_REPOS (buildRepoRecords, runs_scm.go) for agent-run-lib.sh's clone_one to check out (W9-S1-3 — previously advertised on the source's identity but never actually honored by any clone).

type WorkspaceRequirement added in v0.5.0

type WorkspaceRequirement struct {
	// Level is "required" (a run against this workspace cannot proceed without
	// it) or "optional" (a run may proceed without it, degraded).
	Level string `json:"level"`
	// Provenance is "scan_seeded" (the workspace 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: a declared need for a secret, an egress host, or host write access, 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, matching WorkspaceMount.ReadOnly's safe default — this field is
	// a plain bool rather than a *bool because false already IS the safe
	// default, so there is no unsafe zero value to guard against). local_dir
	// only.
	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); local_dir/repo only (ephemeral has no
	// library contract to override). nil (the field omitted) means "say
	// nothing here" — the server carries forward whatever this source's
	// attachment already had (upsertAndAttach), so a plain composition edit
	// (e.g. a rename) can never silently wipe an override set on an earlier
	// PUT; an explicit map, empty or not, REPLACES it outright.
	Overrides map[string]string `json:"overrides,omitempty"`
}

WorkspaceSource is one entry in a Workspace's composition — a Workspace is one-or-more of these (multiples of the same Type are allowed, e.g. two local_dir sources mounted at different targets). Which fields are meaningful depends on Type:

local_dir  Path (host path), Target, Writable
repo       Source (slug/URL), Ref, Target
ephemeral  Target only — no Path, no Source, never Writable (there is no
           host location for a scratch dir to be writable back TO)

type WorkspaceSourceType added in v0.5.0

type WorkspaceSourceType string

WorkspaceSourceType discriminates one entry in a Workspace's composition.

const (
	// WorkspaceSourceTypeLocalDir binds a host directory into the sandbox.
	WorkspaceSourceTypeLocalDir WorkspaceSourceType = "local_dir"
	// WorkspaceSourceTypeRepo clones a git repo into the sandbox.
	WorkspaceSourceTypeRepo WorkspaceSourceType = "repo"
	// WorkspaceSourceTypeEphemeral is a scratch directory that exists only for
	// the sandbox's lifetime — no host mount, no clone. It is the composition
	// floor: a Workspace with no other source still has this one, so "one-or-
	// more sources" never means zero.
	WorkspaceSourceTypeEphemeral WorkspaceSourceType = "ephemeral"
)

type WorkspaceStatus

type WorkspaceStatus string

WorkspaceStatus is the onboarding/scan lifecycle of a Workspace. The import-pipeline stages (building/verifying/verify_failed/build_error/ready) a later "Workspace Import v2" wave had added are RETIRED as of the composition model — migration 0029 collapses every stored row back to scanned/pending_scan — so a workspace is either not-yet-scanned, mid-scan, scanned (ready to use), or errored.

const (
	// WorkspacePendingScan is the initial state: onboarded but not yet scanned.
	WorkspacePendingScan WorkspaceStatus = "pending_scan"
	// WorkspaceScanning: a scan run is in flight (repo scan).
	WorkspaceScanning WorkspaceStatus = "scanning"
	// WorkspaceScanned: profile derived; the workspace is ready to use.
	WorkspaceScanned WorkspaceStatus = "scanned"
	// WorkspaceError means the last scan attempt failed.
	WorkspaceError WorkspaceStatus = "error"
)

Jump to

Keyboard shortcuts

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