api

package
v0.4.4 Latest Latest
Warning

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

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

Documentation

Overview

Package api wires Wardyn's control-plane REST surface (the wardynd binary). It contains ZERO target-specific code (the parity rule): it talks to runners only through the runner.Runner interface, and to identity/secrets/broker only through their contract interfaces. Every security decision fails closed.

Route map (see the REST contract in the architecture brief):

Public (admin bearer):
  POST /api/v1/runs ; GET /api/v1/runs ; GET /api/v1/runs/{id}
  GET  /api/v1/runs/{id}/grants
  POST /api/v1/runs/{id}/kill
  GET  /api/v1/runs/{id}/attach   (WebSocket: interactive PTY)
  GET  /api/v1/approvals?state=&run_id= ; POST /api/v1/approvals/{id}/approve|deny
  GET  /api/v1/audit?run_id=&since=&until=&action=&action_prefix=&actor_type=&outcome=
  POST /api/v1/policies ; GET /api/v1/policies ; GET /api/v1/policies/{id}
  PUT  /api/v1/policies/{id} ; DELETE /api/v1/policies/{id}
  GET  /metrics                   (Prometheus text exposition)
Anonymous:
  GET  /healthz
Internal (run-token bearer, identity.Provider.Verify aud="wardyn-internal"):
  POST /api/v1/internal/decisions
  POST /api/v1/internal/approvals ; GET /api/v1/internal/approvals/{id}
  POST /api/v1/internal/credentials/mint
Ground-truth (host-sensor bearer, identity.Provider.Verify aud="wardyn-groundtruth"):
  POST /api/v1/internal/groundtruth   (eBPF/Tetragon kernel-event batch)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func LoadPolicySpec

func LoadPolicySpec(path string) (types.RunPolicySpec, error)

LoadPolicySpec reads and validates a RunPolicySpec from a JSON file. Used by wardynd to seed the default policy from examples/policies/default.json.

func NewManagedCredProvider added in v0.2.0

func NewManagedCredProvider(store secretstore.Store, provider string) subscription.Provider

NewManagedCredProvider builds a managed subscription provider over store for a provider id (e.g. "anthropic"). Returns nil when store is nil (managed mode simply unavailable).

Types

type ApprovalService

type ApprovalService interface {
	Request(ctx context.Context, req types.ApprovalRequest) (types.ApprovalRequest, error)
	Decide(ctx context.Context, id uuid.UUID, approve bool, decidedByType types.ActorType, decidedBy, reason string) (types.ApprovalRequest, error)
	Get(ctx context.Context, id uuid.UUID) (types.ApprovalRequest, error)
	List(ctx context.Context, state types.ApprovalState) ([]types.ApprovalRequest, error)
}

ApprovalService is the narrow approval FSM surface the API depends on. It is satisfied by package-level wrappers over internal/approval (see wardynd wiring), keeping the API decoupled from concrete storage.

type AuditSpool

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

AuditSpool durably records audit events whose PRIMARY store write failed, so a security event is never silently lost (C1). The control-plane audit log is the system of record; silently dropping a credential.mint / run.kill / egress.deny event when the database blips would let a run proceed while reporting success — exactly the failure a governance tool must not have.

It is a mutex-guarded append-only JSONL file: intentionally simple, fit for the local-first single-host deployment. A background drain loop (StartDrain, wired from the API server) replays spooled events back into the durable store once it recovers and empties the file, so the queryable audit trail becomes complete again automatically — the write-only sink is no longer operationally inert. A nil *AuditSpool disables spooling (a failed primary write is then only logged).

func NewAuditSpool

func NewAuditSpool(path string) (*AuditSpool, error)

NewAuditSpool opens (creating if needed) an append-only JSONL spool at path.

func (*AuditSpool) Append

func (a *AuditSpool) Append(ev types.AuditEvent) error

Append writes one event as a single JSON line and fsyncs it, so a crash right after a primary-write failure still preserves the event. It returns an error only when even the fallback write fails (the last-resort signal the event is lost).

func (*AuditSpool) Drain added in v0.3.1

func (a *AuditSpool) Drain(ctx context.Context, rec audit.Recorder, batch int) (int, error)

Drain replays up to batch spooled events into rec (the DURABLE store recorder) and removes exactly those it confirmed, leaving the rest for the next call. It returns the number of events replayed. On the first replay error it stops and keeps every not-yet-confirmed line on disk (including the one that failed), so a still-down store just leaves the spool untouched to retry later.

rec MUST be a raw durable recorder (e.g. store.Recorder) — NOT the spooling chain: Drain holds the spool lock across the whole operation, so a recorder that re-entered Append on failure would deadlock. Holding the lock also makes it safe against concurrent Append (a failed write during a drain blocks briefly instead of racing the truncate); the bounded batch keeps that hold short.

at-least-once. A crash between rec.Record succeeding and the on-disk trim can re-replay a duplicate on the next Drain; give InsertAuditEvent an `ON CONFLICT (id) DO NOTHING` and that becomes exactly-once.

func (*AuditSpool) StartDrain added in v0.3.1

func (a *AuditSpool) StartDrain(ctx context.Context, rec audit.Recorder, interval time.Duration, batch int)

StartDrain runs Drain on a ticker until ctx is cancelled, replaying spooled events into rec once the store recovers. Each tick drains repeatedly (yielding the lock between batches so Append is not starved) until the backlog clears or a replay error defers the rest to the next tick. It blocks; run it in a goroutine.

type ComponentInfo

type ComponentInfo struct {
	Selected string `json:"selected"`
	// Available lists every implementation self-registered in this build's seam
	// registry (so /healthz truthfully shows what THIS binary can run — e.g. a
	// tagless build advertises sandbox.available=[]). Empty for seams without a
	// registry (policy_engine today).
	Available []string `json:"available,omitempty"`
	Source    string   `json:"source,omitempty"`
}

ComponentInfo describes one pluggable seam's selection for /healthz. Runtime facts only: Selected is ALWAYS the actual running implementation. The recommended-vs-shipped split is prose and lives in docs/PLUGGABILITY.md + ROADMAP.md. Source is "default" or "configured".

type ComposerBackendReadiness

type ComposerBackendReadiness struct {
	Name        string `json:"name"`
	Provider    string `json:"provider"`
	Model       string `json:"model"`
	Wire        string `json:"wire"`
	Transport   string `json:"transport,omitempty"` // normalized (HTTP wires => "api"); cli tool / fake variant
	Auth        string `json:"auth,omitempty"`      // openai azure only: apikey|entra
	Enabled     bool   `json:"enabled"`
	NeedsKey    bool   `json:"needs_key"`
	KeySecret   string `json:"key_secret,omitempty"`
	KeyResolved bool   `json:"key_resolved"`
}

ComposerBackendReadiness is the boot-snapshot readiness of one configured composer backend. KeySecret is a secret NAME (never a value); KeyResolved is whether that secret (or the env fallback) was present at boot.

type Config

type Config struct {
	// Store is the abstract persistence seam (run/policy/grant/approval/audit
	// CRUD + reads). The control plane talks to this instead of *pgxpool.Pool
	// directly, so a future pure-Go backend can be swapped in. Defaults to a
	// store.NewPG(Pool) adapter when wired from wardynd.
	Store store.Store
	// Identity mints/verifies/revokes per-run identities (embedded by default).
	Identity identity.Provider
	// Approvals is the approval FSM service.
	Approvals ApprovalService
	// Broker mints credentials inside the approval-gated transaction.
	Broker MintBroker
	// Audit records control-plane-originated audit events. The recorder handed in
	// is the shared masking → spooling → store/fanout chain (see cmd/wardynd), so a
	// failed durable write is masked, logged loudly, and spooled to the local
	// append-only fallback for EVERY writer — the API layer no longer spools itself.
	Audit audit.Recorder
	// AuditSpool is the SAME durable fallback spool the recorder chain appends to on
	// a failed store write (see cmd/wardynd buildAuditChain). When set together with
	// AuditDrainRecorder, New starts a background loop that replays spooled events
	// back into the durable store once it recovers and empties the file, so the
	// queryable audit trail heals automatically. Nil disables the drain.
	AuditSpool *AuditSpool
	// AuditDrainRecorder is the RAW durable recorder (store.Recorder — NOT the
	// spooling chain) the spool drain replays into. It must bypass the spool to
	// avoid a re-spool loop / lock re-entry; a nil recorder disables the drain.
	AuditDrainRecorder audit.Recorder
	// Runner launches sandboxes. Nil => headless API-only mode.
	Runner runner.Runner
	// AdminToken gates the public API (constant-time bearer compare). Empty
	// disables the public API entirely (fail closed) except /healthz.
	AdminToken string
	// LocalMode enables LOCAL HOST MODE: the public-API auth (humanOrAdminAuth)
	// is bypassed entirely and every admin-gated action is attributed to
	// LocalOperator. This is the single-developer localhost path — no SSO, no
	// token, no Dex. It NEVER affects internalAuth (sidecar/run-token
	// verification), so the sidecar callback path is unchanged. The daemon
	// (cmd/wardynd) refuses LocalMode when bound to an EXPLICIT public IP, but
	// only WARNS (does not refuse) on an unspecified bind (0.0.0.0, the
	// WARDYN_LISTEN default) — operators must bind/publish loopback-only for a
	// real guarantee (the Compose default already publishes 127.0.0.1).
	LocalMode bool
	// LocalOperator is the principal stamped on runs/approvals/audit in
	// LocalMode (e.g. "local:<os-user>"). Ignored unless LocalMode is true.
	LocalOperator string
	// TrustDomain is surfaced in /healthz and used for run SPIFFE ids.
	TrustDomain string
	// DefaultPolicy is applied to runs created without an explicit policy_id.
	DefaultPolicy types.RunPolicySpec
	// RunnerTarget records which target a run is dispatched to ("docker"|"k8s"),
	// or "none" for a headless control plane (-runner none: runs stay PENDING).
	// Defaults to "docker".
	RunnerTarget string
	// UIDir, when set, serves a SPA from this directory at "/".
	UIDir string
	// ControlPlaneURL is the externally-reachable base URL handed to sidecars
	// (proxy config) so they can call the internal endpoints.
	ControlPlaneURL string
	// ProxyURL, when set, overrides the WARDYN_PROXY_URL injected into sandbox
	// env. Defaults to "http://wardyn-proxy:3128" (the per-run proxy sidecar
	// hostname set by the docker driver). Non-secret: it is a network address,
	// not a credential.
	ProxyURL string
	// RecordingStore, when set, serves PTY session replays under
	// GET /api/v1/runs/{id}/recording/{id} (admin-gated) and accepts uploads
	// via PUT /api/v1/runs/{id}/recording (run-token auth).
	RecordingStore recording.Store
	// OIDC, when set, enables human SSO: it mounts /auth/login,/auth/callback,
	// /auth/logout and composes oidc.Middleware in front of the admin-gated API
	// so a valid session cookie OR the admin bearer token authenticates a caller.
	// The admin token still works for the CLI when OIDC is configured.
	OIDC *oidc.Authenticator
	// ImageBuilder, when set, builds a per-run sandbox image from the
	// devcontainer_repo in a create-run request. Nil disables devcontainer
	// builds (the request degrades to the convention image).
	ImageBuilder ImageBuilder
	// AgentImages, when set, is an agent-name -> OCI image-ref map that
	// overrides the ghcr convention image for named agents. Agents not present
	// in the map fall back to the convention. Validated at server construction
	// (must parse if set); nil disables the override and the convention is used
	// for every agent.
	AgentImages map[string]string
	// AgentAnthropicModel, when set, pins the ANTHROPIC_MODEL env inside a
	// claude-code sandbox (e.g. "opus") so the agent uses a specific model rather
	// than the account/CLI default (which a promo can push to a cheaper model like
	// Fable). Empty = unset; the CLI's own default is used. Applies in both
	// subscription and api-key auth modes.
	AgentAnthropicModel string
	// BedrockRegion / BedrockModel, when BOTH set, opt a claude-code run into the
	// Amazon Bedrock Anthropic transport (CLAUDE_CODE_USE_BEDROCK) instead of the
	// default api-key/proxy-inject path — an enterprise path with no direct
	// Anthropic egress, billed via AWS. BedrockModel is a Bedrock model id (a
	// cross-region inference-profile id like "us.anthropic.claude-..." is what
	// claude-code actually expects, not a bare foundation-model id). Boot-time
	// config only (mirrors AgentAnthropicModel — no live admin write path); the
	// AWS credentials themselves come from the secret store (aws-access-key-id /
	// aws-secret-access-key / optional aws-session-token), read directly at
	// dispatch time because Bedrock's AWS SigV4 request signing can't be
	// proxy-injected the way a static x-api-key header can (see runs.go
	// resolveBedrockAuth). Empty BedrockRegion or BedrockModel disables Bedrock
	// entirely; a subscription-mode run always takes priority over Bedrock.
	BedrockRegion string
	BedrockModel  string
	// BedrockAWSConfigDir, when set, bind-mounts a host AWS config directory
	// (a `~/.aws`) READ-ONLY into the sandbox at /home/agent/.aws, so the AWS
	// SDK inside the run resolves credentials itself — including short-lived AWS
	// SSO / IAM Identity Center sessions, which it refreshes on demand. This is
	// the HOST-MODE alternative to pasting static aws-access-key-id/-secret
	// secrets (which expire under SSO and must be re-pasted): with the mount,
	// `aws sso login` on the host is enough and nothing is stored in Wardyn.
	// It is OFF by default. Host-mode setup.sh auto-detects ~/.aws; the compose
	// stack supports it too via the WARDYN_BEDROCK_AWS_DIR bind (same
	// host==container path, :ro — see deploy/compose/docker-compose.yaml), an
	// opt-in the operator sets explicitly. Because it mounts the operator's
	// ambient cloud credentials into runs, it is a single-user / self-hosted
	// choice, not for a shared multi-tenant service (invariant 1) — the deliberate
	// residency tradeoff already accepted for the ~/.claude subscription mount.
	// Empty = disabled. Takes precedence over the resident static-key path but not
	// over a bedrock-api-key bearer.
	BedrockAWSConfigDir string
	// BedrockAWSProfile, when set, is passed as AWS_PROFILE into the sandbox so
	// the SDK selects a named profile from the mounted config (common with SSO:
	// `aws sso login --profile X`). Only meaningful with BedrockAWSConfigDir.
	BedrockAWSProfile string
	// BedrockAWSSSORegion is the AWS region whose SSO endpoints (oidc.<r>,
	// portal.sso.<r>) the sandbox is allowed to reach so the SDK can exchange an
	// SSO token for role credentials. It often differs from BedrockRegion.
	// Empty defaults to BedrockRegion. Used by the BedrockAWSConfigDir mount path
	// AND by the containerized `aws sso login` (harnessLogin.loginEgress), which
	// has no other way to know which regional SSO endpoints to allow.
	BedrockAWSSSORegion string
	// Secrets is the at-rest secret store. It backs the admin secret-management
	// endpoints (PUT/DELETE/list — values are NEVER readable via the API) and
	// the internal injection-resolve endpoint the proxy calls at startup. Nil
	// disables both surfaces.
	Secrets secretstore.Store
	// MaskRegistry, when non-nil, is used to mask verbatim secret values from
	// PTY capture / asciicast uploads before they reach the RecordingStore.
	// A nil registry disables masking (existing tests stay green).
	MaskRegistry *secretmask.Registry
	// SubscriptionToken, when non-nil, yields the operator's LIVE Anthropic
	// subscription OAuth access token from the resident ~/.claude credentials.
	// The internal injection-resolve endpoint uses it to inject a fresh token
	// per request for subscription runs (secret name subscriptionOAuthSecret),
	// so the sandbox holds only an inert sentinel instead of a copy that goes
	// stale. Nil disables the subscription-injection path (falls back to the
	// resident-copy behavior).
	SubscriptionToken subscription.Provider
	// ManagedToken, when non-nil, yields the Wardyn-MANAGED Anthropic subscription
	// token — a long-lived `claude setup-token` the operator captured via the
	// container-login flow, stored age-encrypted. The injection sink resolves the
	// types.ManagedOAuthSecret sentinel through it, exactly like SubscriptionToken
	// resolves the resident-host sentinel. This is what credentials a subscription
	// run in a COMPOSE deployment whose distroless wardynd has no host ~/.claude.
	// Nil disables the managed-injection path.
	ManagedToken subscription.Provider
	// DisableSubscriptionInject is the operator ESCAPE HATCH: when true (env
	// WARDYN_SUBSCRIPTION_INJECT=off), subscription runs keep the legacy
	// resident-copy behavior (the mounted credential, which can go stale) instead
	// of auto-enabling TLS-MITM + injecting the live host token. Default false =
	// the safe proxy-side default whenever a SubscriptionToken provider is wired.
	DisableSubscriptionInject bool
	// Now is overridable in tests; defaults to time.Now.
	Now func() time.Time
	// BaseCtx is the process-lifetime base context used for detached background
	// work that MUST outlive the request that started it — specifically the
	// completion watcher dispatch starts after Exec. The request/dispatch ctx is
	// cancelled when the HTTP handler returns, which would kill a watcher
	// immediately; BaseCtx (threaded from main.go's rootCtx) keeps it alive for
	// the lifetime of the daemon and is cancelled on shutdown. Defaults to
	// context.Background() when unset (the watcher then only stops on process
	// exit).
	BaseCtx context.Context
	// Composer, when set and Enabled(), powers the AI Run Composer endpoints
	// (POST /api/v1/runs/compose, GET /api/v1/composer/backends): a registry of
	// LLM backends turns a natural-language task description into a PROPOSED
	// {run, inline_policy} that Wardyn risk-grades deterministically and clamps to
	// DefaultPolicy before returning for human approval. Nil / not-Enabled
	// disables the endpoints (404), so the feature is strictly opt-in.
	Composer *composer.Registry
	// Components advertises, per pluggable seam (identity, secret_store,
	// recording, policy_engine, sandbox, ...), the SELECTED running implementation
	// and the recommended production default, for honest /healthz visibility. Nil
	// => the components object is omitted.
	Components map[string]ComponentInfo
	// AgeKeyDurable reports whether the secret store's age key was SUPPLIED
	// (WARDYN_AGE_KEY/-age-key non-empty) vs ephemerally generated at boot. When
	// false, stored secrets are unreadable after a restart — surfaced by
	// /setup/status as a durability warning. Computed at boot in cmd/wardynd.
	AgeKeyDurable bool
	// LocalLoopback reports whether the HTTP listen address binds only loopback.
	// It feeds SetupAuth.LocalLoopback so the wizard can explain the local-mode
	// posture. Computed at boot in cmd/wardynd (listenIsLoopback).
	LocalLoopback bool
	// LocalTrustForwarder, when true, tells the LocalMode no-auth bypass to accept a
	// NON-loopback request peer (r.RemoteAddr). It exists for the compose/team
	// deployment ONLY: there wardynd binds 0.0.0.0 inside a container but the host
	// publishes the port loopback-only (127.0.0.1:PORT), so a host UI/CLI request
	// arrives at wardynd from the docker bridge gateway, not loopback. The LAN
	// protection in that topology is the loopback PUBLISH (a LAN peer cannot reach a
	// 127.0.0.1-bound host port at all), not the peer check — so the peer gate is a
	// false positive there. The DNS-rebinding Host gate still applies. NEVER set this
	// for a directly-bound host-mode wardynd on 0.0.0.0: that would re-open the LAN
	// no-auth exposure the peer gate closes. Default false; set by compose only.
	LocalTrustForwarder bool
	// ComposerBackends is the BOOT-snapshot readiness of every configured composer
	// backend (including disabled + needs-key ones the live registry can't show).
	// Surfaced by /setup/status. Nil when the composer is unconfigured.
	ComposerBackends []ComposerBackendReadiness
	// ScanAIAdvisor, when non-nil, enables the ADVISORY AI workspace-scan fallback
	// (internal/workspacescan/ai.go): after the deterministic DeriveProfile, when
	// the profile is low-confidence or left unrecognized samples (ShouldAdvise),
	// this gap-fills EMPTY fields only and can only RAISE NeedsReview — it never
	// overrides a deterministic fact and FAILS OPEN (any error keeps the
	// deterministic profile unchanged and the upload still succeeds). Nil (default)
	// = feature OFF, byte-identical to the deterministic-only behavior. Production
	// wires it (from WARDYN_SCAN_AI_ADVISOR) to a workspacescan.AdviseProfile
	// closure; it doubles as the test seam so tests inject a fake instead of
	// shelling out to a real coding-agent CLI.
	ScanAIAdvisor func(context.Context, workspacescan.ScanFacts, workspacescan.WorkspaceProfile) workspacescan.WorkspaceProfile
}

Config holds the API server's non-secret configuration and injected collaborators. All interface fields except Runner are required; Runner may be nil for headless API-only operation (runs stay PENDING with a clear message).

type ImageBuilder

type ImageBuilder interface {
	// BuildDevcontainer builds the devcontainer for repoURL@ref and returns the
	// local image reference to run. outputTag is the deterministic per-run tag
	// the result is committed under.
	BuildDevcontainer(ctx context.Context, repoURL, ref, outputTag string) (imageRef string, err error)
	// BuildFromDevcontainerFiles builds an image from IN-MEMORY generated
	// devcontainer files (relative path -> content, e.g.
	// ".devcontainer/devcontainer.json") rather than a repo checkout, returning
	// the local image reference. It drives the SAME hardened envbuilder path as
	// BuildDevcontainer. Used for an onboarded workspace WITHOUT a wired
	// devcontainer, where internal/workspacescan generates a minimal one from the
	// detected profile. outputTag is the deterministic profile-hash-keyed tag
	// the result is committed under.
	BuildFromDevcontainerFiles(ctx context.Context, files map[string]string, outputTag string) (imageRef string, err error)
	// FinalizeBase wraps an arbitrary USER-supplied base image (Bring Your Own
	// Image) with Wardyn's runner tools + a cleared ENTRYPOINT, returning the
	// runnable local image reference. No untrusted build, no registry push — just
	// the trusted FROM+COPY finalize stage; the base is pulled only if absent, so
	// a host-pre-pulled private image works. outputTag is the per-run tag.
	FinalizeBase(ctx context.Context, baseRef, outputTag string) (imageRef string, err error)
}

ImageBuilder builds a per-run sandbox image from a devcontainer repo. It is target-agnostic (the parity rule): the concrete envbuilder implementation is wired in wardynd behind the "docker" build tag, so the control-plane default build carries zero target-specific code. Nil disables devcontainer builds.

type MintBroker

type MintBroker interface {
	MintForGrant(ctx context.Context, caller *identity.Claims, grantID uuid.UUID) (broker.Minted, error)
	RevokeRun(ctx context.Context, runID uuid.UUID) error
}

MintBroker is the credential-mint surface the API depends on (internal/broker).

type RecordTaskResult

type RecordTaskResult struct {
	RunID uuid.UUID `json:"run_id"`
	// Label is the operator-chosen session name (e.g. "build & test"). Persisted
	// because sessions are user-named, not derived — the session key is a slug of
	// this, so the label carries the original display text.
	Label string `json:"label,omitempty"`
	Mode  string `json:"mode"` // auto | interactive
	// Confined distinguishes a VERIFY session (default-deny egress, limited to the
	// workspace's approved set + baseline) from a learning session (open egress).
	// Same interactive attach machinery; the flag flips AllowAllEgress and lets the
	// UI list learning sessions on the Record step and verify sessions on Verify.
	Confined bool `json:"confined,omitempty"`
	// LLMMode + Model record the auth the session actually ran with (the operator's
	// configured provider): subscription | api-key | none, plus the pinned model.
	// Saved with the session so it's visible and a verify replays the SAME auth as
	// the recording (the operator's setup, not a re-derived guess).
	LLMMode    string     `json:"llm_mode,omitempty"`
	Model      string     `json:"model,omitempty"`
	Status     string     `json:"status"` // recording | recorded | record_failed
	StartedAt  time.Time  `json:"started_at"`
	FinishedAt *time.Time `json:"finished_at,omitempty"`
	// Observations is the deterministic recordmode.Capture aggregate of what the
	// run ACTUALLY used, computed server-side from its audit events at
	// termination — never from a sandbox upload.
	Observations *recordmode.Observations `json:"observations,omitempty"`
	// SecretNamesMinted resolves Observations.MintedGrantIDs to the secret /
	// grant names actually exercised, for the "proven used" checklist render.
	SecretNamesMinted []string `json:"secret_names_minted,omitempty"`
	// EgressPromoted marks that this task's observed hosts were merged into the
	// workspace's ApprovedEgress (operator action, never automatic).
	EgressPromoted bool `json:"egress_promoted,omitempty"`
	// KernelSensorBlind: the run executed under CC3/Kata where the host eBPF
	// sensor cannot see — proxy decisions were the sole egress signal.
	KernelSensorBlind bool `json:"kernel_sensor_blind,omitempty"`
	// FailureHint explains a record_failed in operator terms (e.g. the sandbox
	// couldn't reach the control plane, so no evidence landed).
	FailureHint string   `json:"failure_hint,omitempty"`
	Caveats     []string `json:"caveats,omitempty"`
}

RecordTaskResult is one task's Record Mode state, persisted opaquely in workspaces.record_results (map taskKey → RecordTaskResult). The api layer owns this shape; the store never interprets it.

type Server

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

Server is the control-plane HTTP server. It is safe for concurrent use.

func New

func New(cfg Config) *Server

New constructs a Server and builds its router. It does not start listening.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the configured http.Handler (the chi router).

func (*Server) ReconcileOnBoot

func (s *Server) ReconcileOnBoot(ctx context.Context) error

ReconcileOnBoot re-derives the state of every run left non-terminal by a PREVIOUS wardynd process (a crash, OOM, deploy, or Ctrl-C) so it is not stranded RUNNING forever with a live sandbox and un-revoked credentials (C3). The per-run completion watcher is an in-process goroutine that does not survive a restart; this rebuilds the safety net at boot. Best-effort: errors are logged, never fatal. Re-attached watchers run on the daemon base context so they outlive this call. AgentStatus observes the AGENT (via the persisted agent_exec_id), not just the container, so an idle-container exec run whose agent already exited is finalized instead of stranded: the previous process's in-memory exec map is gone, but the exec id survives on the run row.

func (*Server) RunClaudeCompose added in v0.4.0

func (s *Server) RunClaudeCompose(ctx context.Context, promptJSON []byte) ([]byte, error)

RunClaudeCompose is the sandbox composer backend's late-bound run launcher (set via sandbox.Composer.SetRunClaude in New). It runs the REAL claude binary inside a GOVERNED one-shot run credentialed by the Wardyn-managed subscription injected PROXY-SIDE (never resident) — so a distroless container-mode wardynd with no host claude can do subscription-billed composing, ToS-clean.

It mirrors launchScanRun's mint → CreateRun → dispatch flow, with three deltas: (1) NO WorkspaceID (an ordinary model run — resolveLLMTransport's managed path fires, injecting the token; a scan run makes no model call); (2) the WARDYN_COMPOSE_* env (discriminator + base64 prompt/schema) rides ExtraEnv, the exact same "only a discriminator + non-secret payload changes; clone/grants/EGRESS/recording/LLM-injection are identical" contract as scan/verify/exec; (3) it WAITS for the run to finish and reads the uploaded proposal from the compose-results store.

Fail-closed: no managed subscription connected (managedInjectReady) is a clear error BEFORE any run is created; a run that finishes without uploading a proposal (e.g. the sandbox could not reach the control plane) is a clear error too. The returned bytes are claude's raw stdout wrapper — the caller (sandbox.Composer.Propose) extracts + parses them through the canonical loop.

type SetupAgeKey

type SetupAgeKey struct {
	Durable bool `json:"durable"`
}

SetupAgeKey reports whether the secret store survives a restart (a stable WARDYN_AGE_KEY was supplied vs an ephemeral generated one).

type SetupAuth

type SetupAuth struct {
	Mode          string `json:"mode"`
	LocalLoopback bool   `json:"local_loopback"`
}

SetupAuth is the active public-API auth mode: local (loopback bypass) | sso (OIDC) | token (admin bearer) | disabled (no auth configured, API closed).

type SetupBedrock

type SetupBedrock struct {
	Region string `json:"region,omitempty"`
	Model  string `json:"model,omitempty"`
	// The three credential SOURCES resolveBedrockAuth accepts, in its precedence
	// order (bearer > ~/.aws mount > resident SigV4). ANY one is sufficient — a
	// mount- or bearer-configured host has NO aws-access-key-id/-secret secrets
	// yet is fully ready, so gating readiness on CredsPresent alone wrongly reads
	// "needs setup".
	CredsPresent  bool `json:"creds_present"`  // resident aws-access-key-id + aws-secret-access-key secrets
	AWSMount      bool `json:"aws_mount"`      // host-mode read-only ~/.aws bind-mount (SSO auto-refreshes)
	BearerPresent bool `json:"bearer_present"` // bedrock-api-key bearer token secret (never resident)
	// Ready is the server-computed readiness (region+model+any credential source),
	// echoed so the UI doesn't re-derive — and drift from — this gate.
	Ready bool `json:"ready"`
}

SetupBedrock is the Amazon Bedrock Anthropic-transport readiness snapshot.

type SetupCheck

type SetupCheck struct {
	ID       string `json:"id"`
	Label    string `json:"label"`
	Status   string `json:"status"`
	Platform string `json:"platform,omitempty"`
	Detail   string `json:"detail,omitempty"`
	Fix      string `json:"fix,omitempty"`
}

SetupCheck is one environment/readiness row. Status is ok|warn|fail|info; "info" is a permanent, non-fixable condition (e.g. no /dev/kvm on macOS) that must render as informational, not as a clearable warning. Platform lets the UI show environment-appropriate copy (linux|darwin|windows|wsl|any).

type SetupComposer

type SetupComposer struct {
	Enabled  bool                       `json:"enabled"`
	Default  string                     `json:"default,omitempty"`
	Backends []ComposerBackendReadiness `json:"backends"`
}

SetupComposer is the composer enablement plus each configured backend's readiness (a BOOT snapshot, so it can surface disabled + needs-key states the live registry alone can't show).

type SetupDeployment

type SetupDeployment struct {
	HostLike bool `json:"host_like"`
}

SetupDeployment reports whether the wardynd process itself sees a resident Claude login — true in host mode (run-host.sh: wardynd runs as the operator, ~/.claude + the claude binary are on its own PATH/HOME), false in the compose path (distroless container blind to the host). HONEST framing like detectKVM: this is "does THIS process see a resident claude", not "is it literally run-host.sh" — a compose container with ~/.claude bind-mounted would also read host-like. The UI uses it to fork the getting-started guidance (laptop/local vs team/server) and to explain why the LLM-access check is or isn't green.

type SetupFix

type SetupFix struct {
	Action      string `json:"action"` // "add_secret" | "scan_workspace" | "none"
	SecretName  string `json:"secret_name,omitempty"`
	WorkspaceID string `json:"workspace_id,omitempty"` // ID, not source — api.scanWorkspace takes an id
}

SetupFix is the structured, actionable remedy a UI button drives directly — no prose-parsing. Action "none" means informational only: there is no button, because the only fix is the OPERATOR widening their own ceiling (e.g. a dropped egress domain).

v1 verifies PRESENCE only (Decision 3: declared-present, never "verified" in the UI copy) — a stored secret, an onboarded+scanned workspace, a surviving grant. Live credential verification (does the key actually authenticate?) is a FUTURE seam at the egress proxy (broker-verified calls), not here — this stays a fast, pure read of already-stored state, no live probe and no new injection surface.

type SetupHarness added in v0.2.0

type SetupHarness struct {
	Provider    string `json:"provider"`              // "anthropic" | "aws"
	Captured    bool   `json:"captured"`              // a token blob is stored
	CapturedAt  string `json:"captured_at,omitempty"` // RFC3339, when pasted
	Aging       bool   `json:"aging,omitempty"`       // captured longer ago than harnessTokenAging
	SourceRunID string `json:"source_run_id,omitempty"`
	// ExpiresAt/Expired carry a REAL, machine-readable expiry and are populated
	// only for providers whose credential exposes one (AWS SSO does; an Anthropic
	// setup-token does not, which is the whole reason Aging exists as a
	// conservative age heuristic). Empty here means "this provider can't tell you"
	// — never "it doesn't expire".
	ExpiresAt string `json:"expires_at,omitempty"`
	Expired   bool   `json:"expired,omitempty"`
	// Renewable: the stored credential carries a refresh token, so it can be
	// renewed without a fresh interactive login (AWS `sso-session` profiles).
	// Legacy sso_start_url profiles have none and must be re-logged-in.
	Renewable bool `json:"renewable,omitempty"`
}

SetupHarness is a Wardyn-managed subscription credential's readiness. Derived purely from the stored blob (presence + capture age) — PRESENCE only, honesty law: no green badge implies the token was live-verified. setup-token tokens live ~1yr with no machine-readable expiry, so Aging is a conservative age-based "reconnect soon" flag, never a hard expiry claim.

type SetupItem

type SetupItem struct {
	Kind       string    `json:"kind"` // "llm_access" | "secret" | "workspace" | "workspace_secret" | "repo_credential" | "egress" | "backend" | "config_pair"
	ID         string    `json:"id"`   // stable "<kind>:<key>", e.g. "secret:anthropic-api-key"
	Label      string    `json:"label"`
	RequiredBy string    `json:"required_by"`
	Status     string    `json:"status"` // "satisfied" | "missing" | "unverified"
	Detail     string    `json:"detail,omitempty"`
	Fix        *SetupFix `json:"fix,omitempty"`
	// Residency names WHERE the credential this item concerns actually lives at
	// run time, derived from the FINAL spec's own delivery mechanism (never
	// guessed): "proxy_injected" (an api_key grant — the value never leaves the
	// wardyn-proxy sidecar, injection.go), "resident_mount" (a host credential
	// bind-mounted into the sandbox, e.g. the Claude subscription mount), or
	// "brokered_mint" (a github_token/git_pat grant — the broker mints/resolves
	// a value and hands it directly to the in-sandbox git-credential helper at
	// task time, internal.go handleInternalMint). Empty when not applicable
	// (workspace/egress/backend rows carry no single credential).
	Residency string `json:"residency,omitempty"`
}

SetupItem is a per-requirement readiness verdict for a composed run, computed DETERMINISTICALLY from the FINAL post-clamp spec (never LLM self-assessment) — the same trust rule composer.Grade uses (composer/risk.go). It generalizes the composeLLMAccess pattern (compose.go) to every setup requirement a proposal implies: secrets, onboarded workspaces, repo credentials, egress.

NOT SetupCheck (setup.go) — that type's Fix is free-text prose for a human to read; SetupItem's Fix is a structured action a UI button can drive directly (add_secret/scan_workspace), so it needs its own shape rather than reusing SetupCheck's.

type SetupPlatform

type SetupPlatform struct {
	OS  string `json:"os"`
	WSL bool   `json:"wsl"`
	// KVM: the host exposes /dev/kvm — lets the UI split Vault's "incompatible
	// with this hardware" from a fixable "needs setup" (additive; old UIs ignore).
	KVM bool `json:"kvm"`
}

SetupPlatform is the wardynd host's OS + WSL posture.

type SetupProvider

type SetupProvider struct {
	Tool             string `json:"tool"`
	Installed        bool   `json:"installed"`
	LoggedIn         bool   `json:"logged_in"`
	LoginDetectedVia string `json:"login_detected_via,omitempty"`
	// AuthMode is how the CLI authenticates, when detectable: "subscription" (a
	// resident Claude OAuth token is present — fresh OR expired; freshness lives in
	// the llm_provider check Detail, not here) or "" (unknown; never guessed). The
	// "api_key" value is reserved in the contract but not inferred for a CLI (no
	// cheap honest signal); codex stays "" (no auth-file parse).
	AuthMode string `json:"auth_mode,omitempty"`
}

SetupProvider is a resident coding-agent CLI (claude|codex) detected on PATH. LoggedIn is ADVISORY (a home-dir credential-file heuristic, not a live check).

type SetupRunner

type SetupRunner struct {
	Driver                string            `json:"driver"`
	ConfinementClasses    []string          `json:"confinement_classes"`
	ConfinementSubstrates map[string]string `json:"confinement_substrates,omitempty"`
}

SetupRunner echoes the runner name and the live confinement classes/substrates.

type SetupSecrets

type SetupSecrets struct {
	Present   []string `json:"present"`
	GitHubApp bool     `json:"github_app"`
}

SetupSecrets reports present secret NAMES (reserved names excluded) and a convenience bool for whether both GitHub App secrets are set.

type SetupStatus

type SetupStatus struct {
	// Ready is server-computed and CONSERVATIVE (false when the runner is nil),
	// so the wizard opens rather than hiding a half-configured bootstrap.
	Ready bool `json:"ready"`
	// Checks is the single list of environment/readiness rows the UI renders.
	Checks []SetupCheck `json:"checks"`
	// Auth is the active public-API auth posture.
	Auth SetupAuth `json:"auth"`
	// Runner is the sandbox runner + the confinement classes actually live on
	// this host (from Runner.Capabilities, same source as /healthz).
	Runner SetupRunner `json:"runner"`
	// Composer is the AI Run Composer enablement + per-backend readiness snapshot.
	Composer SetupComposer `json:"composer"`
	// Providers reports resident coding-agent CLIs detected on the wardynd host.
	Providers []SetupProvider `json:"providers"`
	// Secrets reports which known secrets are present (NAMES only, reserved
	// names excluded) — never any value.
	Secrets SetupSecrets `json:"secrets"`
	// AgeKey reports whether the at-rest secret store survives a restart.
	AgeKey SetupAgeKey `json:"age_key"`
	// HasRuns drives the wizard's "launch your first run" done state.
	HasRuns bool `json:"has_runs"`
	// Platform is the OS + WSL posture the environment-step copy keys off.
	Platform SetupPlatform `json:"platform"`
	// HostProxy is the host-side proxy detection (env/shell/git/tool-config/OS)
	// the Host Proxy Getting-Started step renders. Read-only detection; it
	// never configures anything (the upstream-proxy plumbing is separate).
	HostProxy setup.HostProxyDetection `json:"host_proxy"`
	// SCM is the presence-only git-credential posture (gh CLI login, helper,
	// plaintext stores) the ScmProviderStep's ladder recommendations key off.
	SCM setup.SCMPosture `json:"scm"`
	// Bedrock is the AWS Bedrock Anthropic-transport readiness the "Connect a
	// model" step renders alongside the API-key/subscription rows. Region/Model
	// are boot-time operator config (non-secret, safe to echo); the AWS
	// credentials themselves are never echoed — CredsPresent is a bool derived
	// from secret-name presence, same as every other secret in this contract.
	Bedrock SetupBedrock `json:"bedrock"`
	// Deployment reports whether wardynd itself sees a resident Claude login
	// (host mode) or is blind to it (compose/container).
	Deployment SetupDeployment `json:"deployment"`
	// Harness reports per-provider Wardyn-managed subscription credentials
	// captured via container login (setup-token), so the wizard can show a
	// "connected / expiring / reconnect" row that works in compose mode where
	// there is no resident host login. Empty when no managed credential exists.
	Harness []SetupHarness `json:"harness,omitempty"`
}

SetupStatus is the aggregate readiness snapshot for GET /api/v1/setup/status.

Jump to

Keyboard shortcuts

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