api

package
v0.8.6 Latest Latest
Warning

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

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

Documentation

Overview

Access / role-mapping CRUD (migration 0051): the console's Getting Started -> People editor over the store half of internal/auth/oidc's RoleMappingSource. Modeled on permissions.go's shape — one read that shows the whole picture, small validated writes, an audit event per write — but with two guards permissions.go has no analogue for: this table decides who derives admin at all, so a bad write here can either silently widen access (a collision with the operator allowlist) or lock the acting admin out of their own deployment (see the posture-flip and lockout guards below).

Role mappings under four-eyes (gov4-b2): the gates a role-mapping write passes before the store, shared by a direct write and an approval, and the held-change halves of POST and DELETE /access/mappings. A role mapping is written on the operatorOnly tier, so only a super admin proposes one and only a super admin approves one (operatorApprover); a security admin is refused admin_surface and never sees one in the change list.

The lockout guard runs only where a write is applied, against the claim snapshot of the human whose request applies it: on an approval that is the approver, never the proposer, who may have been demoted since and whom the second human exists to check.

The contract a `minted_pat` run's personal access token is built against: the value the vssps API returns, the client that creates and revokes it, the refusal it can come back with, and the reasons and audit actions every lane that touches one uses. It holds no behaviour beyond naming these: the client (a vssps caller), the org check, dispatch, renewal and the sweeps each live with their own lane and code against this file.

Agent providers: the org-level policy for WHICH coding agents this deployment offers and the model provider each uses by default. This file owns the write boundary — validation, the two endpoints, the audit row — and the predicates every later site reads:

agentProvidersConfigured(sc)       is this install in legacy open mode?
agentProviderFor(sc, agentID)      the row governing this agent, if any
(*Server) agentRosterRefusal(...)  the launch-path refusal, one spelling

The predicates live here, next to the validation that shapes the rows they read, so there is exactly ONE spelling of "is this agent offered" — the call sites (run create, the record launcher, the setup roster) only ask.

What this file does not do, so the seams are findable: it never chooses a run's model provider (chooseModelProvider reads the default from here), never captures a credential, and never renders a console surface.

Per-user API tokens (migration 0045): the THIRD auth branch of the public API, plus the self-service and admin CRUD around it.

The problem it solves: before this, the only non-interactive credential was WARDYN_ADMIN_TOKEN — one shared string, deployment-wide admin, attributable to nobody. Every script and CI job that touched the API shared it, and the audit log recorded "admin-token" for all of them. An api token is the opposite: it belongs to ONE human, carries THEIR role, and is revocable on its own.

The load-bearing property is that authenticating with one is indistinguishable downstream from authenticating with that human's SSO session. apiTokenAuth publishes the identity through withHumanIdentity — the same function the session branch calls — so ownership (AgentRun.CreatedBy), the admin gate (isOperator) and capability grants (capabilitySubjects) all bind to the owning human for free, with no per-feature token awareness anywhere. A token is NEVER the admin identity: minting one requires a verified human in the first place (handleCreateAPIToken), so there is no path by which the shared admin token becomes a per-user one.

#1197: the hold projection (projectHolds) and the pure "could THIS caller decide THIS approval" predicate (mayDecide) the attention rule's row 8 needs. New file: approvals.go is at 985/1000 lines (scripts/check-file-size.sh's cap), and both of these are read-only mirrors of gates decide() itself owns — keeping them beside decide() would invite editing one copy and not the other.

GET /approvals' opt-in ?view= (#1197). Split out of approvals.go, which sits at the file-size gate's cap.

The durable write-back: what a decided approval leaves behind on a WORKSPACE. An `always` decision and a verify-loop approval both edit a workspace the operator may not be looking at, and share one contract: fail silent but audited. The decision already stands when these run, so none may fail the request; every give-up path must leave an audit row instead.

internal/api/approvals_reconcile.go is the BOOT-TIME twin: it replays the same `always` verdicts and calls this file's two direction-specific reject predicates so the two cannot disagree about what a workspace will accept.

A holder's exec: opened AFTER the registry has decided who writes, replaced when an observer is promoted, and the size every observer follows.

Order is the point. The exec used to be created first and the registry consulted second, so an observer's tmux client existed (and, under tmux's `window-size latest`, resized the shared window) before anyone knew it was an observer. Now registration comes first, the role picks the attach options (a writer at its own size, an observer with tmux's ignore-size flag and the writer's size), and a promotion swaps the observer's exec for a writer's.

Attach-session holder registry + audited take-over.

The problem this exists to fix: attach is a SHARED tmux session. handleAttachWS (attach.go) opens a fresh Runner.Attach per client against the same persistent session, so opening the run page while a `wardyn run attach` holds it from a CLI means two clients silently compete for one PTY — and neither can observe the other. The Redraw button in ui/src/app/components/attach-terminal.tsx exists only to clean up the tmux clamp that competition leaves behind.

The honest version: name the holder, admit the second client READ-ONLY, hand it the PTY in place when the holder leaves, and make displacing them an audited act.

AWS SSO account/role PINNING — the control-plane half of the defect below.

The defect: cmd/wardyn-aws-sso took AccountList[0]/RoleList[0], so a cloud team granting an unrelated SSO entitlement inserted an element at index 0 and silently re-pointed the AWS identity every run in the deployment signed with. The helper now verifies a pin or asks the person; THIS file is why it cannot be talked out of that from inside the sandbox.

Three things live here, and they are one idea seen from three sides:

awsSSOPin / awsSSOPinEnv  the admin's provider pin, carried to the login
                          sandbox as launch env (never as sandbox input)
bindCaptureToPin          the upload's refusal predicate: the blob must
                          agree with the LAUNCH-TIME stamp and with the
                          account the configured Bedrock model lives in
refuseCapture             the audited refusal, so a rejected capture is a
                          row in the trail rather than a 400 nobody sees

Why refuse and not rewrite. The stored blob is baked VERBATIM into every later Bedrock run's ~/.aws/config (awsSSOConfigFileContents). Rewriting a disagreeing capture would record a session the person never saw and would only move the IAM 403 from capture time back to run time — the finding's own complaint, ten retries deep inside somebody else's terminal.

Stored-credential metadata (credential-storage design CS-6): the sinks stamp when a person's credential was last used for a run, admins read who holds a credential for which model provider, and each person reads their own on provider_access. Every answer comes from the rows' metadata columns — no value, data key or store reference is read to build one.

Where a run's model credential lives, graded once by the server.

An unconditional static claim — "Minted at launch, injected by the proxy. Never written into the sandbox." — is a FALSE ASSURANCE: it is read by the person deciding whether a per-user AWS credential may sit inside a shared-kernel container, at the moment they decide. So the console repeats what this file answers from the model provider the run chose.

Delegated run management (#1142): a registered portal acts for the person signed in to it. The portal exchanges that person's own live identity- provider token for a ten-minute delegated token (delegation_exchange.go); this file is the lane that token authenticates on.

The invariant is "no impersonation; delegation is recorded as delegation". The lane publishes the PERSON through withHumanIdentity — so ownership, secrets, drives and the governance ceiling all resolve on them with no per-feature awareness — clamped to the user role, and marks the context as delegated, which does three things no handler has to remember:

  • delegationAllowed confines the token to a fixed list of routes; every other route, including any added later, answers 403 delegation_scope;
  • isOperator and isSecurityOperator refuse a delegated context outright;
  • recordAudit (and audit.DelegationRecorder, for every other writer on the shared chain) stamps data.via = {delegate, grant} on every row the request writes, so the portal is on the record beside the person.

The bounds on the failure rows the device routes write — auth.fail's pair (authFailedLimiter: how fast; auth_failed_coalesce.go: how many of the same thing) applied per stream. The anonymous enrolment route is ONE stream, charged to the process-wide bucket auth.fail pays. Each enrolled device's ingest refusals are a stream of their own, charged to a per-device bucket, so one member's laptop spends only its own budget.

Hybrid enrolment's organisation side (docs/design/0.8/PLAN.md, epic #78): the route mount and the admin surface — mint, list and revoke enrolment tokens, list the enrolled devices, revoke one. The device-facing routes are in devices_auth.go.

The device-facing routes of hybrid enrolment (docs/design/0.8/PLAN.md): all an enrolled laptop's wardynd — a DAEMON, never a person — can reach. The anonymous first-boot exchange, the deviceAuth middleware, audit ingest and the heartbeat all live in this one file on purpose: TestDevices_DeviceRoutesNeverTouchHumanIdentity parses it and fails if it ever names a human-identity publisher, an operator predicate or a request- actor helper. A device request carries no OIDC human, and isOperator's no-human arm reads exactly that as the admin token — so these routes are safe only because they never ask. The admin half (mint, inventory, revoke) is in devices.go.

Directory autocomplete: the HTTP half of internal/directory.

One route, GET /access/directory/search, proxying the configured connector so the console can offer a picker for every "who" field instead of a free-text box. The value of that is not convenience — it is that an Entra `groups` claim carries an OBJECT GUID and an App Role arrives as its manifest value, so a hand-typed "Platform Engineering" binds NOTHING and fails open to whatever the unassigned default is. The picker renders Entry.DisplayName and stores Entry.ClaimValue, which is the whole point of the two fields.

Tier: securityOps, not an open read (routes.go). A directory search discloses org structure — who exists, which groups they are in — and while a security admin assigns governance to these very people (so the disclosure is acceptable and stated), it is not something every authenticated member should be able to enumerate.

If-Match/ETag optimistic concurrency for the whole-document-replace surfaces PUT /permissions/enforcement and PUT /site-config. Their in-process mutexes (handlePutSiteConfig's site-config lock, handlePutCapabilityEnforcement's capability-enforcement lock) already stop two concurrent PUTs clobbering each other; If-Match adds a CLIENT-side guarantee: a caller that read the document is told 412 rather than silently overwriting a write that landed between its GET and its PUT. A caller that sends no If-Match works as before (additive).

The ETag is a content hash, never a stored version column: no migration, and correct for any writer through the same Get/Put pair (including `wardyn site-config set`), with nothing new to keep in sync.

Governance-profile CRUD (migration 0052): the admin-facing surface over governance_profiles + governance_assignments — named, assignable ceilings and the rows binding them to a user, a group, or everyone.

Modeled on access.go / permissions.go: one read that shows the whole picture, small validated writes, an audit event per write. Two things are specific to this table and neither is optional — the monotone-⊆ bound on a profile's eligible grants (governance_grantbound.go), which stops a profile from MINTING credential eligibility the deployment never provisioned, and the ON DELETE RESTRICT surfaced as a 409, which stops a profile delete from silently widening everyone it bound.

Ceiling helpers beside governance.go's CRUD and resolver: the ADVISORY omission warnings a profile write returns (what the DEPLOYMENT ceiling carries that the profile does not), and the per-request memo that makes every ceiling-bounded seam in one request answer from a single resolve.

Its own file by seam (the file-size gate); no behaviour lives here that governance.go's own doc does not describe.

Four-eyes on governance writes. With WARDYN_GOVERNANCE_SECOND_HUMAN on, a covered write (a governance profile or assignment today) is decoded and validated exactly as before and then STORED as a pending change (migration 0126) and answered 202; it applies only when a distinct, authorised human approves it, in one transaction (governance_changes.go). With the switch off nothing here runs and every covered route answers byte for byte as it did.

This file is the gate every covered write goes through, the proposal that stores one, the diff the reviewer reads, and the per-target table of who may approve.

The key-domain assignment target kind: the half that HOLDS a set or delete (the handler has already decoded and validated it as a direct write is) and the half that APPLIES a held one inside the decision transaction, holding the assignment lock the direct write holds, re-validating against the assignments it finds and writing through the key-domain service's Querier forms.

No exemption: moving where a person's next keys are made is a custody decision either way.

The two target kinds gov4-b1 covers: a governance profile (create, update, delete) and a governance assignment (upsert, delete). For each, the half that HOLDS a write (decode and validation are the handler's own, unchanged; here the write is run as a dry run, abandoned, and stored as a pending change with its diff) and the half that APPLIES a held one inside the decision transaction. Both halves call the store's Querier forms, so a held change runs the same SQL a direct write does.

The permission-side target kinds gov4-b2 covers: a capability grant (upsert, delete), the enforcement map (replace), a value's availability (set) and a user type's priority (update). For each, the half that HOLDS a write (the handler has already decoded and validated it as a direct write is) and the half that APPLIES a held one inside the decision transaction, which re-validates against the state it finds and writes through the store's Querier forms.

No kind here has a narrowing exemption: deleting a deny grant, lifting a restriction and turning enforcement on each widen, and nothing proves a given write is not one of those.

The /governance/changes routes: list and read the changes held for a second human, and approve or reject one. Registered on securityOps; the per-target approver table (governanceChangeKinds) decides inside each handler who may act on which kind, so a kind that only a super admin may write is never shown to, or decided by, a security admin.

A decision is ONE transaction (store.DecideGovernanceChange): lock the change row; require it pending and unexpired; re-check that the approver is still who they were (a revoked token, a session cut off, a role that no longer passes); compare the target and the deployment default with what the proposal reviewed (409 governance_change_stale on a mismatch); apply the write through the store's Querier forms; move the state; commit. The audit rows follow the commit. A failure anywhere before the commit leaves the target unwritten and the change pending.

Composed governance profiles (migration 0125): a profile may name a base (another profile, or the deployment default) and carry an overlay that can only narrow it. This file is the ONLY reader of a profile's authority. A composed row stores no ceiling or limits of its own, so a reader that took the stored row as authority would read it as empty (every limit unset) and fail OPEN; the resolver below composes from the deployment down, and a source guard (governance_compose_census_test.go) fails any other file in this package that calls the store's raw profile reads. types.GovernanceProfile is the stored row and ResolvedProfile is what every reader takes, so a reader left on the row stops compiling.

A chain that cannot be read or composed returns an error and never a partial chain: every caller takes the branch it already has for a store failure, and a composition the meet cannot satisfy (overlayUnsatisfiableError) is the explicit deny-all state, refused with the authz reason governance_overlay_unsatisfiable.

The structural bound on a governance profile's eligible grants: a profile may NARROW the deployment's credential eligibility, never MINT new eligibility.

The dominance contract itself is composer.GrantWithin; this file is the api's two entry points to it.

The governance-profile write door: POST/PUT/DELETE /governance/profiles. A profile is standalone (a whole ceiling) or composed (a base plus an overlay that can only narrow it, migration 0125). Every write decides its row inside one store transaction under the profile-graph lock (store.WriteGovernanceProfile), from the graph as that transaction reads it, so concurrent writes cannot together make a cycle or a chain deeper than three.

groundtruth_capture.go — what a REVIEWER is told about how much of a capture the kernel actually corroborated.

Two facts get separate caveats: the eBPF sensor is host-wide and its heartbeat is global, so folding it into a per-run caveat lets the host's review-time state stand in for the run's. The capture's own claim comes from the exec/connect/file-write lines the sensor bound to THIS run; the host's state is reported as what it is: review-time state.

Each person's own credential for a model provider: the reserved UID-keyed names it is stored under, the strict read that never falls back to the operator's row, the door that writes it into the caller's own namespace, and rule 8's purge — an address, scheme or kind change, or a deletion, removes every person's credential for that provider. Nothing at run create or dispatch reads these rows yet (MP-7 onward).

Model providers: the admin-written records that say which kinds of model credential this deployment supports, where each one sends requests and which harnesses may use it. This file owns the write boundary every door runs — normalization, validation and the server-owned UID. Configuration ONLY: no credential is ever read or written here, and nothing at run create or dispatch reads these records yet, so a nil block changes nothing.

The model-provider endpoints: GET/PUT /model-providers (admin) and the member-safe projection every person reads on GET /setup/status. The write boundary itself — normalization, validation, UIDs — is model_providers.go's.

People an admin sets up before their first sign-in (#1157). The admin-minted token that once rode with them is gone (#1477): a person signs in and creates their own, and a token already minted that way is listed for revocation.

The keying rule is the one sign-in already uses: a person's principal IS the identity provider's `sub`, exact and case-sensitive (oidc's CallbackHandler publishes idToken.Subject and nothing else). So a pre-created person is keyed by that subject, a sign-in attaches to it by subject equality alone, and the email the admin records is an assertion used only to derive the role before the person's own claims exist. An email never attaches anyone: a different person signing in with the same email carries a different subject and lands on a different principal.

Entra ID's `sub` is pairwise, unknowable before the first sign-in, so there a person may instead be set up by tenant and object id (#1195): their principal is "entra:<tid>:<oid>", and a sign-in becomes it only when its issuer, `tid` and `oid` claims equal the row's exactly (peopleKeying).

Permissioning CRUD: the admin-facing surface over capability_grants + capability_enforcement (migration 0042) and the member-safe read of a caller's own effective set. The resolver these routes feed lives in capabilities.go; this file is only validation + persistence + audit for the four writes plus the two reads.

Per-resource availability ("Available to", user-types design 2.6): the restricted bit on one value of one restrictable kind. Who is listed is the allow rows in capability_grants, written through POST /permissions/grants; this file only turns the bit on and off. capBatch.decide's step 3 reads it.

Launch presets (#1143, migration 0087): an admin-managed, named, versioned bundle of existing POST /runs fields that a launcher names instead of sending the whole spec. The server expands a preset into the equivalent explicit request BEFORE any create-path gate runs, so a preset carries no capability of its own: the caller's ceiling, capabilities, owner-scoped secrets and drive apply exactly as they would to the explicit body.

provider_access.go grades every model provider a person is granted (MP-12). It reuses modelaccess.go's vocabulary and its Bedrock/subscription dispatch siblings' scope and read helpers (provider_bedrock.go, provider_subscription.go) rather than re-deriving them, so a state here and the refusal dispatch gives for the same credential can never disagree.

The Bedrock arms of provider dispatch (MP-9): a run that chose a bedrock_sso or bedrock_bearer model provider reaches Bedrock with the region, model and base URL the provider record names, on its OWNER's own credential — their AWS sign-in (wardyn-provider-<uid>-sso) or their own Bedrock API key (wardyn-provider-<uid>-key), read strictly from their namespace. The kind names the one lane: the operator's host ~/.aws mount and static SigV4 credentials are never reached.

The sign-in doors keyed by model provider (MP-13). POST /model-providers/{id}/sign-in launches the login sandbox for a bedrock_sso or anthropic_subscription provider; its capture lands in the caller's own namespace under that provider's UID-keyed name (wardyn-provider-<uid>-sso or -oauth), never the roster's harness name and never the operator's. An AWS sign-in is stored by the sandbox's own helper upload (ssotoken.go, bound to the provider as it read at launch); a Claude sign-in by PUT on the same path, with the setup-token the sandbox printed, bound to that sign-in's own run.

Dispatch for a run that chose a model provider, and the per-person Claude subscription arm (MP-8). A chosen provider's arm is the only credential author for its run: none of the legacy lanes (the host ~/.claude mount, the operator's managed token, Bedrock, the operator's api key) credentials it.

The subscription arm serves the run owner's OWN Claude sign-in, stored under wardyn-provider-<uid>-oauth in their own namespace, and nobody else's. The shared-subscription posture (SubscriptionPostureOK) does not apply to it: that posture stops one person's subscription serving others, which this lane cannot do. perPersonSubscriptionPosture replaces it for this lane.

#1197: the attention rule (attention/heldCandidates), its projection onto a page of runs (projectAttention), and GET /me/attention (handleMeAttention). New file, beside approvals_decidable.go, for the same reason: approvals.go and runs_policy.go are both close to scripts/check-file-size.sh's 1000-line cap.

Per-run lifecycle events (#1144): GET /api/v1/runs/{id}/events, a text/event-stream of the closed client.RunEvent vocabulary, fed by the state moves dispatch and the terminal paths already make (casRunState, the dispatcher's OnWaiting, the idle reaper's call into this package). The feed is an in-memory ring per run — resumable with Last-Event-ID within the daemon's lifetime, no table behind it. Each replica announces the events it emits to the others over NOTIFY (live_bus.go), so a reader on any replica follows a run another dispatches; a notice is a hint, and the keepalive re-read of the store ends a stream whose notice was lost. Event ids count within one replica's ring, so a Last-Event-ID is not portable across replicas.

Per-file diff stat for a run's workspace — the Files-changed widget on the run-detail cockpit.

Why not workspacescan: that package is an ONBOARDING scanner (what a repo needs), not a differ, and it reads the host filesystem. This read has to answer "what has the agent changed so far", which is a question about the sandbox's own working tree, mid-run.

Why not the host filesystem: run.WorkspacePath is the HOST directory the workspace is bind-mounted from, so reading it from wardynd would work on docker-on-this-host and silently return nothing on k8s (where the mount is a PVC on another node) — the daemon must never assume it shares a filesystem with the sandbox.

So: one ExecStream into the sandbox, running git. Substrate-agnostic — it works wherever the SSH gateway's exec channels already work.

Sandbox resource usage — CPU, memory, disk written, disk used, process count — for the Sandbox widget on the run-detail cockpit.

WHY NOT a Runner.Resources interface method: that buys a moby ContainerStats path, a k8s metrics-API path (which needs metrics-server installed — often it is not), and two more fakes, all to read numbers the kernel already publishes INSIDE the sandbox. One ExecStream over cgroup v2 + procfs is substrate- agnostic and works wherever the SSH gateway's exec channels already work.

ponytail: cgroup v2 + procfs via one exec, not a per-substrate Runner method. Upgrade path is a Runner.Resources seam if some substrate ever needs a non-exec read (a substrate with no exec at all, say).

The model-credential refusal class and the two questions every door asks before choosing a model provider: is this a model run, and what does the site config say. The declared-mechanism gate that used to live here compared a fixed lane precedence against the roster; the model provider a run chooses replaced both (multi-provider design §2.5).

Dispatch of a run's model credential (multi-provider design §2.5, §2.8): the one gate, the one strip, and the per-kind arms. A model run is credentialed by the provider it chose, from its owner's own credential, or by nothing. The key and endpoint kinds' arm lives here; the subscription arm in provider_subscription.go and the Bedrock arms in provider_bedrock.go.

Dispatch-time secret resolution: the two phases that read a STORED SECRET's VALUE out of the store and put it somewhere the run can reach — the LLM inspection corpus (in-process, for the detector) and env_secret (the sandbox process environment). Split out of runs_dispatch.go for the 1000-line file-size gate (scripts/check-file-size.sh), joining the llm/mounts/gitbroker/ ceiling halves already split the same way.

They belong together because they are the only two places in dispatch that hold PLAINTEXT, and they share the rules that follow from it: every resolved value is registered with the run's mask registry so a verbatim leak into PTY capture or any audit event is scrubbed; no value ever enters the audit stream (the events carry NAMES only); and each documents its own fail direction — LLM inspection fails OPEN because a missing name costs detection coverage, env_secret fails CLOSED per grant because a silently absent variable surfaces as an unauthenticated API call the agent reports as a task failure.

The opt-in GET /runs query surface beyond ?limit=&offset= (#1197 L1a): view/owner/status/ended_within/include_killed/workspace/q. Split out of runs_policy.go so handleListRuns's own branch (any of these present -> the filtered path; none present -> byte-identical pre-#1197 behaviour) stays a short read.

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.

Routes: see routes.go for the table. The authoritative, always-current classification of every one of them — which role gate each sits behind — is authz_test.go's chi.Walk-enumerated matrix, which cannot go stale because it walks the router itself.

The SSH gateway: `ssh <run-id>@<advertise-host>` lands in the same tmux session and masked live recorder the web terminal (attach.go) uses. Lives here, next to attach.go, rather than a new package, because the SSH caller must go through the SAME runIsUnrecordable gate and liveMaskWriter the web terminal does — splitting them across packages would let a second caller miss that gate.

Channel-level protocol handling (session requests, exec/sftp/direct-tcpip bridging) is sshgateway_channels.go; this file is connection lifecycle: listener bring-up, the DoS bounds, and public-key auth+authz.

Channel-level SSH gateway protocol handling: the "session" channel's request loop (pty-req/window-change/env/shell/exec/subsystem) and the bridges to the sandbox — Attach for shell, runner.ExecStream for exec/sftp/direct-tcpip. See sshgateway.go for connection lifecycle + auth.

Self-service SSH key management: GET/POST/DELETE /api/v1/me/ssh-keys[/{fingerprint}]. This is the REST counterpart to the SSH gateway: the gateway that AUTHENTICATES against these rows lives in sshgateway.go. Any authenticated human manages their OWN keys (scoped by principal, both at the store and here) — this is deliberately NOT operator-gated, unlike secret/policy/workspace writes: an SSH key is a personal credential binding, not deployment configuration.

Per-user run-detail widget layout. The run cockpit lets a user arrange the evidence widgets; that arrangement is theirs and has to survive a new machine, so it is a server row keyed by principal — ui/src/app/lib/storage.ts is localStorage and is explicitly NOT the answer here.

The UI-sandbox gateway (pillar 4): a governed, ticket-gated HTTP relay from an operator's browser to ONE declared loopback port inside a run's sandbox (a code editor, a dev server), riding the SAME exec lane the SSH gateway's `-L` forward already uses — `socat - TCP:127.0.0.1:<port>` over Runner.ExecStream, wrapped as a net.Conn by execConn. There is no pod-IP or container-IP dial, no NetworkPolicy delta and no new network path out of the sandbox (invariant 3): the sandbox's own network is unchanged, and the bytes travel control-plane → substrate exec → container, exactly like an attach.

It lives on a SECOND LISTENER (WARDYN_UI_SANDBOX_LISTEN, empty = off = no listener, mirroring the SSH gateway), and that is a security control, not a deployment convenience: relayed content is SANDBOX-AUTHORED JavaScript. On the console origin it could read the console's own storage (threat model "Console auth token storage") and drive every admin action the operator can. A second origin is what keeps it out. Boot REFUSES a UI listen address equal to the console's (cmd/wardynd, validateUISandboxConfig).

The listener has exactly ONE authentication mechanism and never falls through to the console's session cookie or admin bearer:

POST /runs/{id}/attach/ticket  (existing, run owner, single-use, 30s)
  → POST <ui-origin>/__wardyn/bind (console fetch; binds the ticket to
    this browser, uigateway_bind.go)
  → POST <ui-origin>/__wardyn/enter (form: run, app, ticket; or the GET form)
  → cookie wardyn_ui_sess (HttpOnly, SameSite=Lax, Path=/r/<run-id>/<app>/)
  → 302 /r/<run-id>/<app>/<app path>  … every later request rides the cookie

The APP is in the path, and the cookie is scoped to it, because a run may declare up to 8 ui_apps: see uigateway_session.go, which owns the cookie.

Cookies are not port-scoped, so a shared hostname would let sandbox content see console cookies and vice versa: every forwarded request has ALL wardyn_* cookies plus Authorization and ?ticket stripped (and any other cookie the operator's UICookiePolicy strips), and every response has Set-Cookie: wardyn_* dropped (cookie tossing), along with any Set-Cookie carrying a Domain attribute or no name (uiSetCookieAllowed, uigateway_cookies.go). Both directions are pinned by tests in uigateway_test.go and uigateway_cookies_test.go.

No content is recorded. ui.authorize/ui.start/ui.open/ui.close say that a human opened and closed an app; there is no keystroke, screen or page capture on this path, and these actions are deliberately distinct from session.attach so a relay session can never appear in the recording picker as if it were one.

The enter ticket's BROWSER BINDING (#1241). A ticket proves who minted it and for which run; on its own it says nothing about which browser presents it. So any user could mint a ticket for their OWN run and drive a victim's browser through the enter hand-off (a link or an auto-submitted form), landing the victim on a session for the attacker's app — login CSRF. In path mode that app then shares an origin with the victim's own relayed apps.

The binding closes that. Before it submits the enter form, the console makes ONE credentialed fetch to the gateway:

POST <ui-origin>/__wardyn/bind   body: ticket=<token>   (from the console page)
  → Set-Cookie wardyn_ui_bind_<id>=<HMAC(ticket)>; HttpOnly; SameSite=Strict;
    Path=/__wardyn/enter; Max-Age=30

and enter (GET and POST) refuses a ticket whose cookie is not there. Why the cookie lives on the GATEWAY origin: that is the only origin enter can read a cookie from, and a cookie the console origin set never reaches it (different host, and a shared parent-domain cookie would reach every sibling host too).

What stops an attacker's page doing the same bind in the victim's browser:

  • uiBindRefusal: the bind must be a same-site fetch (Fetch Metadata, which page script cannot write) and, when the console's URL is configured, carry the console's own Origin. An attacker's page is cross-site, so it gets no cookie.
  • SameSite=Strict: even a binding planted by a cross-site top-level navigation is never sent on an enter that a cross-site page started.
  • The cookie value is an HMAC under the gateway's session key, so it is worth nothing for any other ticket, and enter clears it on use. The ticket is single-use, so the binding is too.

This is why console and gateway must be SAME-SITE (one registrable domain, same scheme): a cross-site console's fetch cannot set a first-party cookie on the gateway at all, and Open fails with the console's bind error instead.

The UI-sandbox relay SESSION: what the wardyn_ui_sess cookie carries, which URLs it is a credential for, and what is re-checked before it opens another connection. uigateway.go is the transport (enter, relay, dial, launcher); this file is the credential.

Two invariants live here and nowhere else:

  • A session names ONE APP, so it is scoped to one app. The relay path is /r/<run-id>/<app>/… and the cookie's Path is /r/<run-id>/<app>/ — a run may declare up to 8 ui_apps, and a single per-run cookie meant entering the second app REPLACED the first app's session in the browser (one name, one Path), after which the first tab's own requests dialed the second app's port. Per-app Paths let the two coexist; the sess.App check in handleUIRelay is the server-side half, since a browser's path match is the only thing keeping them apart and a non-browser client has none.

  • A session is BOUNDED-STALE, not a frozen bearer. It carries its own issued-at, so the operator's WARDYN_UI_SANDBOX_SESSION_TTL applies to cookies already in browsers (the relay's sibling of WARDYN_SSH_ROLE_TTL), and uiDial re-asserts role/ownership against the freshly-loaded run and asks SessionRevocations before every NEW connection. A cookie in the pre-0.7.4 format has no issued-at to bound or compare, so it fails CLOSED.

The deployment's host-root ceiling over a host_path drive, and the one gate that has to look at the OTHER drive rows.

Split out of user_drives.go because these two are the only parts of that file that reason about the HOST FILESYSTEM rather than about a row: the operator's WARDYN_USER_DRIVE_HOST_ROOTS ceiling, whether any configured root can hold a drive at all, and whether two drives' roots share a tree once symlinks are resolved. The CRUD handlers, their request shapes and their audit rows stay next to each other in user_drives.go.

The admin doors' half of the share bound.

The ADMIN doors — GET /drives, the host_root ceiling on a drive write, and the nesting gate's symlink resolution — run uncancellable EvalSymlinks/stat calls on a server that deliberately sets no WriteTimeout, so a hung hard-mounted NAS would hang them and leak a kernel thread per attempt. They share the member doors' driveShareProbe bound and strand memory. The dead-root rules still live only in runner.UserDriveHostRootCheck; this file decides only HOW LONG to wait. The key is "root:"+root, the member path's key for the same subject (driveShareBindFailure), deliberately: one hung share is one strand, whoever asks, so admin and member callers short-circuit on each other's probe.

User-drive CRUD (migration 0054): the admin-facing surface over user_drives + user_drive_grants — the storage an admin registers and the rows allocating one to a user, a group, or everyone.

Modeled on governance.go, deliberately and almost line for line: one read that shows the whole picture, small validated writes, an audit event per write, and the ON DELETE RESTRICT surfaced as a 409. The two tables answer the same shape of question about the same subject vocabulary, so a reader who has understood one has understood this one — and the two cannot drift on the thing that matters, which is what "who" means.

Two things are specific to this table, and neither is optional:

  1. The env ceiling. A host_path drive's host_root is authored HERE, in the database, and then bound into OTHER PEOPLE's sandboxes. So the write boundary composes two checks: types.ValidateUserDrive for the row's shape and runner.UserDriveHostRootCheck for the deployment's operator/MDM-set ceiling over it. A deployment that sets no roots can author no host_path drive at all — an admin cannot widen the ceiling from inside the product.

  2. A new grant is enabled. The store writes UserDriveGrant.Enabled verbatim, so a zero-valued grant is a PAUSED one. `enabled` therefore decodes as a *bool defaulting true here, at the request boundary, which is the only place that can tell "the client said false" from "the client said nothing" — see userDriveGrantRequest.Enabled.

Four routes stay super-only: the ones that name a host path (host_root) or a cluster storage class — creating, listing, updating, and removing the drive itself. The other three — granting an allocation, revoking one, and previewing whose drive resolves — moved to securityOps in 0.8 (issue #168): none of the three names a host path. The tier argument for each route is written at the mountUserDriveRoutes call that decides it (routes.go).

POST /drives/preview — the ADMIN's answer to "whose drive resolves for these claims, and would it actually mount".

Split out of user_drives_resolve.go when that file crossed the 1000-line ceiling scripts/check-file-size.sh holds, at the seam its own banner already drew: everything here is the preview ENDPOINT (its response shape, its warning, its three gates), and everything left behind is the resolver the launch path and GET /me share with it. The parity guard (TestDriveDoorsAnswerOverTheSameSentinels) reads writeDriveError and driveUnavailableReason out of the resolver file, which is why those two stayed there rather than travelling with the surface that writes one of them.

POST /drives/{id}/reclaim — the one verb in the product that DESTROYS a member's stored bytes.

Its own file rather than a ninth handler in user_drives.go, for the reason user_drives_preview.go is its own file: that one is at the 1000-line ceiling scripts/check-file-size.sh holds, and the allowlist is for pre-existing files only. The seam is also the right one — everything here is the destroy path and its refusals, and nothing else in the family shares them.

WHY A VERB EXISTS AT ALL. Deleting a drive removed its row and left the volume or the claim on the substrate with nothing in the product able to name it afterwards, so the storage a departing person left behind was reclaimed by hand off a `POST /drives/preview` answer or not at all (#166). The verb takes the guess out of it.

WHY IT IS FENCED THE WAY IT IS. Four rails, and each one is load-bearing:

  1. On Kubernetes wardynd does not even hold the verb unless an operator grants it: the chart's Role carries `persistentvolumeclaims: [get, create]` and adds `delete` only under `drives.reclaim.enabled` (deploy/helm/wardyn/templates/rbac.yaml), so a stock install answers the apiserver's own 403 underneath everything below.
  2. SUPER-ADMIN ONLY, like every other /drives route and for a sharper reason: this one is irreversible.
  3. A `drive.reclaim` audit row per attempt — success, refusal and failure alike — naming the drive, the person, the backend, the object and what happened to it. A destructive act with no durable record of WHICH bytes went is not auditable after the fact, because the row it describes is gone from the database too.
  4. NO CONSOLE BUTTON. A destructive confirmation is a screen, and this one has no approved mock (docs/design/CONSOLE-RULES.md's mock-first rule), so the surface is the API and the CLI.

The org switch (`storage.user_drive.disabled`) is deliberately NOT a gate here, unlike on every mount-shaped surface in this family. Turning drives off deployment-wide is exactly when an operator needs to clean the objects up, and a switch that blocked the cleanup would strand the bytes it was flipped to stop using.

User-drive resolution (migration 0054): which drive — if any — belongs to the caller, what its per-user object is called, and what the size beside it actually means.

It sits beside effectiveCeiling (governance.go) deliberately: both answer "what does this deployment say about THIS principal", both resolve through the same capabilitySubjects identity and the same subject vocabulary, and both must fail CLOSED in the same three shapes. Sharing the shapes is what keeps a drive from becoming the one governed surface that quietly falls open when the database hiccups.

The member never names a path. A run request carries only `drive: {enabled, read_only}`; everything below is derived from the authenticated identity, so "mount /home/someone-else" is not a request this surface can express.

The RUN seam for user drives (migration 0054): turning `drive: {enabled, read_only}` on a create-run request into the mount a runner executes, or into the one refusal that explains why there is none.

Its own file, beside runs_create.go rather than inside it, for the reason runs_dispatch_ceiling.go and runs_create_validate.go are their own files: the 1000-line gate, and the fact that a security seam reads better whole than scattered through a handler that is already doing eleven other things.

The three refusal shapes, and why they differ.

The whole design of this seam is one table, and each row is a decision that could have gone the other way:

the profile's door is shut       403 + authz.denied   refuse
the group snapshot is unreadable 403 groups_snapshot_stale
everything else                  422, NO audit        denyUserRunQuota

The 403/422 split is the one denyUserRunQuota already draws and it is about the CALLER, not the severity: a door refusal says "you asked for something you may not have", which is an authorization event a SIEM should see. "No drive is allocated to you" says the caller is perfectly authorized and there is simply nothing to mount — an unmet precondition. Auditing that as authz.denied would fill the denial stream with rows about members who did nothing wrong, which is how a real denial stops standing out.

Enabled but unmountable is an error, not a quiet launch.

Every arm below REFUSES the run rather than launching it driveless. The alternative reads friendlier and is worse: a member ticks "mount my drive", the run starts, the agent works for an hour and writes its output into a container layer that is deleted at teardown. Asking for storage and silently not getting it is how work is lost, so the request is a REQUIREMENT.

Where the resolved mount lives between create and dispatch.

On dispatchParams, next to the governance ceiling's own dispatch-time input (the dispatchCeiling argument dispatchRun requires) — NOT on the run row, and there is no migration for it. Three facts decide that, and the third is the one that closes the question:

  1. The create-then-dispatch window belongs to ONE launch. handleCreateRun answers 201 once the run row exists and hands this mount to the detached finishCreateRunLaunch (runs_create_launch.go), which builds the image and dispatches; nothing in the tree re-dispatches a run later (reconcile FAILS in-flight pre-dispatch runs, it never resumes one). So the grant that resolved here is the grant that launch binds — a revocation landing during the build does not reach it, as below.
  2. Re-resolving at dispatch is IMPOSSIBLE anyway, which is why the ceiling does not do it either. Resolution keys on capabilitySubjects — the caller's OIDC sub, email and group snapshot — and the run row carries only CreatedBy, one principal string. There is no stored identity to re-resolve FROM.
  3. Persisting the request flag on the run row instead would therefore buy nothing readable: a column whose only consumer would have to re-resolve from an identity the row does not carry.

The consequence is stated rather than hidden: revoking a grant does not reach INTO a run that is already dispatching. It takes effect on the next run, which is the same guarantee reassertCeilingDenies gives for a ceiling, and the same one the run's own persisted policy gives for everything else.

The user type as a subject (user-types design §2.3, §7): the caller's one type is matched by capability grants, governance assignments and drive grants beside their user and group identities.

Workspace providers: the org-level policy for WHERE work may come from (git providers and their allowed base URLs) and HOW BIG it may get (the storage ceilings). This file owns the write boundary — validation, the two endpoints, the audit row — and the two PREDICATES every admission site reads:

providersConfigured(sc)      is this install in legacy open mode?
providerFor(sc, cloneURL)    is this repo admitted, and by which row?
effectiveScmHosts(sc)        which git hosts does this deployment admit?

The predicates live here, next to the validation that shapes the rows they read, so there is exactly ONE spelling of the match rule and of the host-claim precedence — the call sites (workspace create, POST /sources, run create, the record/scan launchers, the Build step) only ask.

The Entra lane's half of the workspace-provider write boundary: the rules a row carrying `lanes: [entra]`, an `entra` block or `credential_source: per_user` is held to.

It is a SEPARATE FILE from workspace_providers.go for one reason — scripts/check-file-size.sh — and not because the rules are separable: they run inside validateWorkspaceProviders like every other row rule, on both doors, in one pass.

The SSH lane's path-scoping rule (#380): split out of workspace_providers.go by seam (check-file-size.sh) rather than trimmed for length. sshLaneExceeds PathScope is the shared predicate validateProviderLanes refuses on (the console door, always) and the site-config door grandfathers instead of refusing (F2) — sshLaneWidePastPathRows/logWarnSSHLaneWidePastPath are that door's report-and-warn half, read AFTER validation on the block about to be persisted.

Index

Constants

View Source
const (
	// EntraAuthorityOverrideRefusal is the refusal when the override is set
	// without the acknowledgement. %q is the offending value. Lower-case on
	// purpose (staticcheck ST1005): this is a Go error string returned from
	// fmt.Errorf, not an HTTP/console sentence, and Go convention refuses a
	// capitalised one so it composes when a caller wraps it with %w.
	EntraAuthorityOverrideRefusal = "refusing the Entra authority override %q — " +
		"it re-points the Azure DevOps sign-in's authorization, token and discovery endpoints at a server of " +
		"your choosing, which is a TEST hatch and never a production posture; unset it, or explicitly set " +
		"WARDYN_ALLOW_TEST_ENDPOINTS=true to acknowledge that this deployment is a test deployment"
	// EntraAuthorityOverrideWarn is what a deployment carrying the hatch should
	// log at boot. It opens with a greppable literal, because the thing that
	// must never happen is this posture going unnoticed in an inherited values
	// file.
	EntraAuthorityOverrideWarn = "wardynd: TEST HATCH ACTIVE — the Entra authority override re-points the Azure DevOps " +
		"sign-in's authorization, token and discovery endpoints at this URL. No Microsoft endpoint is contacted. " +
		"This is never a production posture; unset it and WARDYN_ALLOW_TEST_ENDPOINTS on any deployment holding a real credential."
)

The TEST-ONLY Entra authority override, and the boot refusal/warning that gate it. It exists for the reason the AWS SSO endpoint override exists: without it, "a person signs in and their Azure DevOps access is captured" cannot be exercised outside a real tenant, because every endpoint is derived from login.microsoftonline.com and nothing could redirect it at a fake.

It is refused unless WARDYN_ALLOW_TEST_ENDPOINTS is also set — the same two deliberate acts that hatch already requires — and everything a typo could hide (a missing scheme, an embedded credential, a path, a query, a fragment) is refused, so a mistake fails at the door rather than by dialing the wrong place with a real credential.

View Source
const (
	// AWSSSOEndpointOverrideRefusal is the BOOT REFUSAL when the override is set
	// without the acknowledgement. It names both vars because the operator
	// reading it on a crash-looping pod has to decide which one they meant. %q is
	// the offending value.
	AWSSSOEndpointOverrideRefusal = "refusing to start: WARDYN_AWS_SSO_ENDPOINT_OVERRIDE is set to %q — " +
		"it re-points AWS IAM Identity Center at a server of your choosing for the containerized login " +
		"AND for every Bedrock run's credential exchange, which is a TEST hatch and never a production posture; " +
		"unset it, or explicitly set WARDYN_ALLOW_TEST_ENDPOINTS=true to acknowledge that this deployment is a test deployment"
	// AWSSSOEndpointOverrideWarn is the BOOT WARN every boot carrying the hatch
	// logs. It opens with a literal an operator (and scripts/kind-sso-walk.sh)
	// can grep for, because the thing that must never happen is this posture
	// going unnoticed in an inherited values file.
	AWSSSOEndpointOverrideWarn = "wardynd: TEST HATCH ACTIVE — WARDYN_AWS_SSO_ENDPOINT_OVERRIDE re-points " +
		"AWS IAM Identity Center (sso-oidc AND the sso portal) at this URL for the containerized login, " +
		"for every Bedrock run's credential exchange and for dispatch-time token renewal. " +
		"No AWS SSO endpoint is contacted. This is never a production posture; unset it and " +
		"WARDYN_ALLOW_TEST_ENDPOINTS on any deployment holding a real credential."
)
View Source
const (
	ADOEntraCallbackPath = "/api/v1" + adoSignInCallbackRoute
)

adoSignInCallbackRoute is the callback's route inside the /api/v1 group, and ADOEntraCallbackPath is the same route as the ABSOLUTE path a browser is sent back to — exported for cmd/wardynd, which composes the registered redirect URL from it rather than typing the path a second time.

View Source
const AWSSSOSpentTokenRetention = 90 * 24 * time.Hour

AWSSSOSpentTokenRetention bounds how long a persisted spent-token row is kept before the reaper prunes it (store.AWSSSOSpentTokenStore's PruneAWSSSOSpentTokens; the prune call site is cmd/wardynd's reapTickLock, which reuses the lifecycle reaper's existing per-tick advisory lock rather than adding a new timer — mirrors RunSecretGrace's exported-constant shape). AWS issues an SSO-OIDC client registration valid for 90 days from creation; past that the refresh token behind a spent fingerprint could never have been redeemed again anyway (registrationLapsed, above, already refuses a lapsed registration before this map is ever consulted), so nothing is lost by forgetting the row once its registration could not still be live.

View Source
const HTTPShutdownTimeout = 15 * time.Second

HTTPShutdownTimeout bounds cmd/wardynd's http.Server.Shutdown, the step that runs before WaitBackground. The two budgets run back to back, so together with the audit sinks' final flush they set the shortest grace period wardynd needs before SIGKILL. deploy/helm's terminationGracePeriodSeconds and deploy/compose's stop_grace_period are 70s for that reason, and TestShutdownGraceCoversTheBudget fails if either falls below the sum.

View Source
const InternalPathPrefix = "/api/v1/internal/"

InternalPathPrefix is the run-token and sensor-bearer surface every proxy and the ground-truth ingest call: the only API routes the internal listener serves.

View Source
const (

	// ReasonADOPATNeedsConsoleApp is S1 (ErrADOMintNeedsSecret): a minted_pat
	// row the console cannot redeem with its own secret. Exported for the boot
	// log in cmd/wardynd.
	ReasonADOPATNeedsConsoleApp = "ado_pat_needs_console_app"
)

/scm/azure-devops/signin and its callback (ado_entra.go): the console's own Azure DevOps per-person sign-in doors, distinct from the ADOEntraFailure enum a REDEMPTION classifies as (ado_entra_store.go, its own documented guard exception). DELETE /scm/azure-devops/connection (ado_pat_console.go) reuses the no-session and unconfigured values.

View Source
const RetiredModelCredentialRefusal = "model access is configured under Settings → Model providers, and each person connects their own credential there"

RetiredModelCredentialRefusal is the one sentence both the boot refusal of a retired variable and a PUT of a retired credential name end with.

View Source
const RunSecretGrace = time.Hour

RunSecretGrace is how long a run must have been terminal before its masking corpus is evicted. It is a GRACE, not a retention policy: the late readers secretmask.Registry.Evict names (the audit recorder masking a finalize event, a teardown_error from a retried sandbox sweep, a recording uploaded as the session closes) all run within seconds-to-minutes of the terminal transition, and masking fails OPEN, so evicting too early unmasks exactly the events most likely to quote a credential. An hour buys those readers room while still bounding how long plaintext lives in daemon memory.

Variables

View Source
var (
	// ErrADOEntraNotCaptured: this principal has no stored Azure DevOps
	// sign-in. Distinct from a dead one — nothing was ever there.
	ErrADOEntraNotCaptured = errors.New("no Azure DevOps sign-in has been captured for this principal")
	// ErrADOEntraDeadCredential: the refresh token is gone (revoked, expired,
	// or already rotated away). Nothing Wardyn can do renews it.
	ErrADOEntraDeadCredential = errors.New("the captured Azure DevOps sign-in can no longer be renewed")
	// ErrADOEntraConsentRequired: the app is not consented for what was asked.
	ErrADOEntraConsentRequired = errors.New("access to Azure DevOps requires consent that has not been granted")
	// ErrADOEntraInteractionRequired: a Conditional Access policy wants the
	// human present. A control-plane renewal structurally cannot satisfy it.
	ErrADOEntraInteractionRequired = errors.New("access to Azure DevOps requires interactive sign-in")
	// ErrADOEntraUnavailable: the request did not complete, or completed
	// unusably. The credential is untouched and the next attempt redeems it
	// normally.
	ErrADOEntraUnavailable = errors.New("renewing the captured Azure DevOps sign-in did not complete")
	// ErrADOEntraStoreRefused: the secret store answered and refused the
	// stored sign-in (moved, changed at the store, or Wardyn's access to it
	// revoked). Definitive, unlike ErrADOEntraUnavailable: a store that refuses
	// must stop the run's use of the credential at once (credential-storage
	// design K8, rule 21).
	ErrADOEntraStoreRefused = errors.New("the secret store refused the captured Azure DevOps sign-in")
)

The failure classes a caller answers differently, as sentinels so a caller can switch on them with errors.Is and a log line still names the code the authority sent.

They are DISTINCT because the answers are: a dead credential must be re-captured, a consent refusal needs an admin or a fresh consent prompt, an interaction refusal needs the human at a keyboard on a compliant device, and an unavailable authority needs nothing but a retry, and a store that refuses the stored sign-in must stop its use at once. Collapsing any pair of them turns a transient outage into a fleet-wide re-sign-in.

View Source
var ErrADOMintNeedsSecret = errors.New("per-run tokens need your Wardyn app registration to have a client secret: " +
	"the row must name the console's own tenant and client, and WARDYN_OIDC_CLIENT_SECRET must be set")

ErrADOMintNeedsSecret is S1: a sign-in that can create personal access tokens must be redeemable only with a secret wardynd holds. The console's secret is sent to the console's own app registration and no other, so a minted_pat row must name that application in the sign-in tenant AND the secret must be set; otherwise its refresh token would mint as a public client.

Functions

func AWSSSOProxyInjectFlagDefault added in v0.7.6

func AWSSSOProxyInjectFlagDefault() string

AWSSSOProxyInjectFlagDefault is the DEFAULT the boot flag advertises, derived from the one constant rather than typed a second time.

It exists because the "one-line flip" was a LIE without it: cmd/wardynd's flag hard-coded the string "on", so with nothing set in the environment the resolver was handed "on" and never consulted the constant at all — flipping awsSSOProxyInjectDefaultOn to false would have shipped the lane ON with every document saying off. The flag now advertises this, so the constant decides the unset case, the -h text and the ENV.md default together.

func AdminTokenPrincipal added in v0.7.3

func AdminTokenPrincipal() string

AdminTokenPrincipal exports adminTokenPrincipal for cmd/wardynd's boot-time validation: a -local-operator value equal to it would collide with this mechanism principal, refusing the SAME operator seat at a provider sign-in that boot just accepted.

func BedrockModelARNNamesNoAccount added in v0.7.3

func BedrockModelARNNamesNoAccount(model string) bool

BedrockModelARNNamesNoAccount reports whether the configured Bedrock model LOOKS like an ARN and yet yields no account — an 11- or 13-digit account field, an uppercase `ARN:`, a truncated paste.

bedrockModelAccount fails OPEN on those, which is deliberate (it is the same answer a bare cross-region profile id gets, and it is what keeps an upgrade from taking capture away from every non-ARN deployment). But failing open on a TYPO is silent: the account check is simply off, at both doors, with nothing to see. wardynd logs this once at boot so "off" is a decision somebody can read rather than a thing nobody notices.

func CapabilitySecondHumanEnabled added in v0.8.4

func CapabilitySecondHumanEnabled() bool

CapabilitySecondHumanEnabled is EgressSecondHumanEnabled for the capability switch, exported for the same single boot-time local-mode check.

func EgressSecondHumanEnabled added in v0.7.0

func EgressSecondHumanEnabled() bool

EgressSecondHumanEnabled reports whether the four-eyes egress switch is on.

Exported for exactly one caller — cmd/wardynd's boot-time local-mode check, which warns when the switch is combined with a mode that cannot enforce it. It is a function rather than a second cliutil.EnvBool at the boot site so the env NAME and the truthiness rule keep ONE definition: a boot guard that disagreed with the runtime gate about what "on" means would warn about a deployment that is fine, or stay silent for one that is not.

func GovernanceSecondHumanEnabled added in v0.8.6

func GovernanceSecondHumanEnabled() bool

GovernanceSecondHumanEnabled reports whether the governance four-eyes switch is on. Exported for cmd/wardynd's boot checks, so the env name and the truthiness rule keep one definition.

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 NewIdentityGate added in v0.8.6

func NewIdentityGate(st identityGateStore) oidc.IdentityGate

NewIdentityGate is oidc.IdentityGate over the store's identity rows.

func OriginHostReadable added in v0.7.3

func OriginHostReadable(raw string) bool

OriginHostReadable reports whether raw is a serialised origin (or a URL) whose host originHost can read — exported for cmd/wardynd's boot-time WARDYN_OIDC_REDIRECT_URL check, so boot and the guard fail closed on exactly the same shapes rather than on two hand-kept lists.

func ReservedPlatformSecret added in v0.6.0

func ReservedPlatformSecret(name string) bool

ReservedPlatformSecret reports whether name is one of this package's platform-internal reserved keys — the base set both the generic-secrets-API guard and every credential sink build on, so a true here means refused at all of them.

Exported for ONE caller: cmd/wardynd's TestPlatformSecretsAreReservedEverywhere, the only place that can see both this set and the daemon's own platform-key constants (a `package main` cannot be imported, so the test cannot live here).

func ResolveAWSSSOProxyInject added in v0.7.6

func ResolveAWSSSOProxyInject(raw string) bool

ResolveAWSSSOProxyInject reads the kill switch. Exported for cmd/wardynd, which owns every env read.

A value nobody typed (empty, or anything that is not one of the two words) is the DEFAULT rather than a boot refusal: this switch's whole purpose is to be reachable in a hurry, and a deployment that mistypes it should get the documented default plus an ENV.md row to read, not a crash-looping daemon in the middle of an incident. The two words are spelled `on` and `off` — the ENV.md convention every other Wardyn mode switch uses — and `true`/`false` are accepted because an operator reaching for a boolean is not wrong enough to be refused.

func RetiredADOSharedSecretNames added in v0.8.2

func RetiredADOSharedSecretNames(sc types.SiteConfig) (names, skipped []string)

RetiredADOSharedSecretNames is the list cmd/wardynd's boot sweep deletes from every namespace (#1429): for each Azure DevOps host, the stored shared token (git-pat-<host slug>), SSH key (ssh-key-<host slug>) and its known-hosts secret (known-hosts-<host slug>). A person's own token and the tokens Wardyn creates live under other, sealed names, so none of them is on this list.

A host whose slug a non-Azure DevOps row (or github.com) also has is NOT on it: the name could be that forge's credential. Those hosts come back as skipped, for the sweep to log, so an admin can remove a shared Azure DevOps credential there by hand.

func RetiredModelCredentialNames added in v0.8.2

func RetiredModelCredentialNames() []string

RetiredModelCredentialNames is the list cmd/wardynd's boot sweep deletes.

func ValidateAWSSSOEndpointOverride added in v0.7.4

func ValidateAWSSSOEndpointOverride(raw string, allowTestEndpoints bool) (string, error)

ValidateAWSSSOEndpointOverride validates WARDYN_AWS_SSO_ENDPOINT_OVERRIDE and returns the normalized base URL for Config.AWSSSOEndpointOverride. Empty => ("", nil), byte-identical to a deployment that never heard of this knob.

allowTestEndpoints is WARDYN_ALLOW_TEST_ENDPOINTS; a non-empty override without it REFUSES BOOT, so this posture takes two deliberate acts. Looser than validateOneLLMGateway only where a TEST endpoint differs: plain http:// is accepted (the fake serves no TLS; proven against the real AWS CLI v2 in test/awsssofake/docker.go) and a loopback/private address is not refused (a ClusterIP is private). A missing scheme, embedded credential, path, query or fragment is still refused, so a typo fails at boot.

func ValidateDemoVideoBaseURL added in v0.8.0

func ValidateDemoVideoBaseURL(raw string) (string, error)

ValidateDemoVideoBaseURL validates WARDYN_DEMO_VIDEO_BASE_URL — the base URL an air-gapped deployment re-points the Getting Started demo episodes at, since github.com is unreachable there — and returns its normalized form for api.Config.DemoVideoBaseURL. Empty => ("", nil), byte-identical to today (episodeUrl's hardcoded github.com download URL, and the CSP's two hardcoded GitHub hosts).

It delegates to validateOneLLMGateway rather than growing a second rule set, with publicHost "" (rule 5, "must not equal the public provider host", has nothing to compare against for this knob — an empty publicHost can never equal a host rule 3 has already required to be non-empty, so the rule is a harmless no-op here) and allowPlainHTTP false (no test hatch for this knob): https:// only, no userinfo, non-empty host, no query, no fragment — the same fail-closed-at-boot posture as the model gateways.

func ValidateEntraAuthorityOverride added in v0.7.10

func ValidateEntraAuthorityOverride(raw string, allowTestEndpoints bool) (string, error)

ValidateEntraAuthorityOverride validates a test-only Entra authority and returns the normalized base URL. Empty => ("", nil), byte-identical to a deployment that never heard of it.

The rules are the AWS SSO override's, and loosened in the same two places a TEST endpoint differs from a production one: plain http:// is accepted (a local fake serves no TLS) and a loopback or private address is not refused (an in-cluster Service address is private by construction).

func ValidateEnvToggles added in v0.8.0

func ValidateEnvToggles()

ValidateEnvToggles reads each envToggles switch once, so a value that is neither truthy nor falsey exits 2 at boot, in every auth mode, rather than killing the daemon mid-request later. cmd/wardynd calls it right after flag parsing.

Types

type ADOEntraAccess added in v0.7.10

type ADOEntraAccess struct {
	AccessToken string
	Scopes      []string
	ExpiresAt   time.Time
}

ADOEntraAccess is one minted access token and what it may do.

Scopes is THE GRANTED SET THE AUTHORITY REPORTED, and a caller must read it rather than assume it got what it asked for: measured against a real tenant, Entra ignores a narrowing request for the Azure DevOps resource and issues a token carrying every scope the person consented to. The token is therefore the person's own identity with all of their consent, not a per-run capability — what bounds a run is Wardyn's own check in front of the resource. This field is the only honest evidence of what a token can do, and it is what a caller should record.

type ADOEntraConfig added in v0.7.10

type ADOEntraConfig struct {
	// RowID identifies the provider row this capture belongs to. It is part of
	// the stored secret's name, so a deployment offering two Azure DevOps
	// organisations keeps two credentials that cannot overwrite each other.
	RowID string
	// TenantID / ClientID / ClientSecret are the app registration. ClientSecret
	// is optional for a bearer row: a public client authenticates with the code
	// challenge alone. A minted_pat row requires it (ErrADOMintNeedsSecret).
	TenantID     string
	ClientID     string
	ClientSecret string
	// RedirectURL is the absolute callback URL registered on the application.
	// It must be the control plane's own callback route.
	RedirectURL string
	// Scopes is the row's CEILING: the vso.* scopes this deployment may ever
	// ask for. A sign-in requests these (plus offline access and openid); a
	// later redemption may ask for any subset. For a minted_pat row it is
	// exactly adoscope.MintScopes: the sign-in only ever creates tokens.
	Scopes []string
	// TokenMode is the row's token mode; empty reads as bearer. own_pat has
	// no sign-in and is never a source's answer.
	TokenMode types.ADOTokenMode
	// LoginClientID / LoginTenantID are the application and tenant THIS CONSOLE
	// signs people in with. They are the other half of the 0.7.10 boundary: the
	// subject comparison is only sound when the two tokens come from one app
	// registration, so a row naming anything else is refused by name. Empty
	// values refuse every capture, which is the fail-closed direction.
	LoginClientID string
	LoginTenantID string
	// AuthorityOverride re-points every Entra endpoint at another base URL. It
	// is TEST ONLY and honoured only under AllowTestEndpoints — see
	// ValidateEntraAuthorityOverride.
	AuthorityOverride string
	// PATAPIOverride re-points the token lifecycle API (vssps) at another base
	// URL. TEST ONLY, and refused without AllowTestEndpoints (patClient).
	PATAPIOverride string
	// AllowTestEndpoints is WARDYN_ALLOW_TEST_ENDPOINTS, the repo's existing
	// acknowledgement that this deployment is a test deployment. Without it an
	// AuthorityOverride is refused rather than ignored.
	AllowTestEndpoints bool
}

ADOEntraConfig is the Azure DevOps Entra app registration one provider row signs people in against. It is passed in rather than read off a row here on purpose: the provider-row type and its validation live elsewhere, and the sign-in needs nothing from them but these fields.

type ADOEntraFailure added in v0.7.10

type ADOEntraFailure string

ADOEntraFailure is the same classes as a machine-readable label, for a caller that puts the class in a refusal body or an audit row rather than branching on it. "" means no failure.

const (
	ADOEntraFailureNone                ADOEntraFailure = ""
	ADOEntraFailureNotCaptured         ADOEntraFailure = "not_captured"
	ADOEntraFailureDeadCredential      ADOEntraFailure = "dead_credential"
	ADOEntraFailureConsentRequired     ADOEntraFailure = "consent_required"
	ADOEntraFailureInteractionRequired ADOEntraFailure = "interaction_required"
	ADOEntraFailureUnavailable         ADOEntraFailure = "unavailable"
	ADOEntraFailureStoreRefused        ADOEntraFailure = "store_refused"
)

func ADOEntraClassify added in v0.7.10

func ADOEntraClassify(err error) ADOEntraFailure

ADOEntraClassify maps an error from this lane onto its class. An error from somewhere else classifies as unavailable — the one class that means "nothing about the credential is known to be wrong", which is the safe reading of an error this function does not recognise.

type ADOEntraSource added in v0.7.10

type ADOEntraSource func(ctx context.Context) (ADOEntraConfig, bool, error)

ADOEntraSource resolves the Azure DevOps Entra configuration for this deployment. nil (the default) means no Azure DevOps sign-in is offered and both routes refuse; ok=false means the same for a deployment whose row is absent or disabled.

A function rather than a struct field so the provider row stays the single source of truth and this lane never caches a stale copy of it.

type ADOPATAccess added in v0.8.2

type ADOPATAccess struct {
	// LastToken is the newest token Wardyn created for this person on this
	// row (minted_pat only), for the card's "last token" line.
	LastToken *adoLastToken `json:"last_token,omitempty"`
	// DefaultProfile is what a run on the row gets when its policy names no
	// Azure DevOps access: the row's default_profile, or
	// adoscope.ProfileDefault when it names none.
	DefaultProfile []adoscope.Capability `json:"default_profile,omitempty"`
}

ADOPATAccess is what /me/scm-access adds to a per-user row for the token console (the TS ADOPATAccess mirror). SCMAccess embeds it, so its fields sit beside SCMAccess's own on the wire. token_mode is SCMAccess's own field: a minted row reads "minted_pat" there, as an own-token row reads "own_pat".

type ApprovalService

type ApprovalService interface {
	Request(ctx context.Context, req types.ApprovalRequest) (types.ApprovalRequest, error)
	// Decide transitions an approval to decision.State (APPROVED or DENIED —
	// the caller picks; there is no separate approve bool, since
	// types.ApprovalDecision.State already says which). decidedByType is kept
	// as its own parameter rather than folded into ApprovalDecision: it is
	// audit attribution (who/what decided), not a property of the decision
	// itself.
	Decide(ctx context.Context, id uuid.UUID, decidedByType types.ActorType, decision types.ApprovalDecision) (types.ApprovalRequest, error)
	Get(ctx context.Context, id uuid.UUID) (types.ApprovalRequest, error)
	List(ctx context.Context, state types.ApprovalState) ([]types.ApprovalRequest, error)
	// CancelForRun moves every still-PENDING approval of a run that has just
	// reached a terminal state to CANCELLED, returning how many moved per kind. It is
	// part of the terminal cascade, beside identity/broker revocation: an
	// approval whose run has ended is a control that cannot function, and a row
	// left PENDING renders live Approve/Deny buttons in the console. reason names
	// the transition ("run_killed", "run_completed", ...). Idempotent by
	// construction — a second call finds nothing PENDING and emits nothing.
	CancelForRun(ctx context.Context, runID uuid.UUID, reason string) (map[string]int, error)
	// ExpireOne moves one still-PENDING approval to EXPIRED (a no-op once decided):
	// wardyn-toolgate's give-up signal (#811). actor is the withdrawing agent.
	ExpireOne(ctx context.Context, id uuid.UUID, actor, reason string) error
	// CountForRun returns how many approvals a run has raised, in ANY state —
	// the per-run cap handleInternalRequestApproval enforces. A sandbox chooses
	// the hosts it asks about, so without that cap the number of rows one run can
	// create is bounded by nothing.
	CountForRun(ctx context.Context, runID uuid.UUID) (int, 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. 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. The parent directory is created too (MkdirAll, so an already-existing one is a no-op): the flag's own default is the RELATIVE "./data/audit-spool.jsonl" (cmd/wardynd/boot_flags.go), and a fresh working directory or mount with no "data" subdirectory yet would otherwise fail this open outright, silently disabling the fallback spool exactly when a deploy's own defaults haven't pre-created the path for it.

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), removes exactly those it confirmed and returns the count. On a replay error it stops and keeps every unconfirmed line, so a down store leaves the spool to retry. A line rejected spoolPoisonAttempts times running moves to the quarantine sidecar (fsynced there first) and Drain returns errSpoolLineQuarantined, so the move is never silent. Known limit: two ADJACENT unacceptable lines still wedge (the second reads as an outage, the price of never quarantining during one); the suspect is logged by event id every tick, and docs/OPERATIONS.md has the remedy. rec MUST NOT be the spooling chain: Drain holds the spool lock throughout (safe against concurrent Append; the bounded batch keeps the hold short), so re-entering Append would deadlock. At-least-once, bounded by the in-flight batch (saveSpoolCursor fsyncs at every advance). Not `ON CONFLICT (id)`: audit_events has no unique id (42P10), and a unique index cannot be a boot migration without disabling what db.AuditDDLProtected verifies. A duplicate is the benign direction; exactly-once would be an out-of-band CREATE UNIQUE INDEX CONCURRENTLY.

func (*AuditSpool) Lines added in v0.6.0

func (a *AuditSpool) Lines() int

Lines reports how many events are sitting in the spool right now — the audit backlog a store outage builds up, and the count that has to drain back before the queryable trail is complete again. Served as the wardyn_audit_spool_lines gauge on /metrics: a spool that never returns to 0 is a drain that is not working, which nothing else on the scrape surface shows. A nil spool (spooling disabled) reports 0 — an observability gauge, not a correctness path.

Takes no lock, on purpose (hence the atomic): Drain holds a.mu across every rec.Record for up to spoolDrainDeadline, and a blocked store is exactly when this gauge matters. Waiting on the mutex would push /metrics past Prometheus' 10s default scrape_timeout and lose the ENTIRE scrape, wardyn_store_up included, during the very outage it reports. Briefly lagging the file is fine.

func (*AuditSpool) Quarantined added in v0.7.0

func (a *AuditSpool) Quarantined() int64

Quarantined reports how many lines have been moved aside as permanently rejected. A nil spool reports 0. Served as the wardyn_audit_spool_quarantined_total counter on /metrics — the signal that "the spool drained" no longer implies "the trail is complete".

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.

func (*AuditSpool) TornDrops added in v0.6.0

func (a *AuditSpool) TornDrops() int64

TornDrops reports how many spool lines Drain has dropped as unparseable (a torn tail from an ENOSPC/partial write). A nil spool reports 0. Served as the wardyn_audit_spool_torn_total counter on /metrics.

type CapState added in v0.5.0

type CapState string

CapState is one capability's current state.

const (
	CapAvailable  CapState = "available"   // wired and usable right now
	CapOff        CapState = "off"         // disabled (integration- or capability-level)
	CapNeedsSetup CapState = "needs_setup" // possible, but a credential/config gap blocks it today
	CapImpossible CapState = "impossible"  // a protocol fact, never fixable by configuration
)

type Capability added in v0.5.0

type Capability struct {
	ID        string   `json:"id"`
	State     CapState `json:"state"`
	Reason    string   `json:"reason,omitempty"`
	Residency string   `json:"residency,omitempty"`
}

Capability is one cell of the matrix: what (ID), current state, why (Reason — populated for needs_setup/impossible, and optionally to annotate an available cell with a qualifying fact), and where the credential lives when wired (Residency; "" when not applicable — e.g. an impossible cell).

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 Config

type Config struct {
	// SweeperLease, when set, gates the sweeps that must run on one replica (the
	// run pause) to the elected leader and fences them by lease epoch. Nil means
	// this process is the only one sweeping: every pass runs and no fence applies.
	SweeperLease SweeperLease
	// SweepHealth records each background sweep's ticks in the shared sweep_ticks
	// record and reads them back for the sweep gauges and the substrate_health
	// setup row. Nil records nothing and reports no sweep.
	SweepHealth *sweephealth.Tracker
	// 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
	// ApprovalExpiryAfter mirrors WARDYN_APPROVAL_EXPIRY_AFTER, the deployment's
	// ceiling on how long any request waits for a decision. A run's captured
	// wait (captureRunLimits) never exceeds it. 0 means unknown here. Dispatch
	// also mirrors it onto a hold-mode run's sandbox (approval_expiry.go, RL-1).
	ApprovalExpiryAfter time.Duration
	RunLeaseConfig      // the run lease's settings (run_lease_server.go)
	// Broker mints credentials inside the approval-gated transaction.
	Broker MintBroker
	// GitHubRulesets, when set, lets the setup checklist ask GitHub whether the
	// App is ref-confined on a granted repo. Nil omits that row entirely — which
	// is also what happens when no GitHub App is configured, so the vast majority
	// of deployments never make the call.
	GitHubRulesets RefRulesetVerifier
	// 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
	// AuditUnsealer opens the sealed personal fields of the audit rows the
	// audit reads serve (WARDYN_AUDIT_SEAL, internal/audit/seal.go). Nil serves
	// rows as stored.
	AuditUnsealer audit.Unsealer
	// AuditActorSubject is the actor a human caller is stored under on a row
	// written outside the recorder chain (the retention drop's chained event
	// and anchor): under WARDYN_AUDIT_SEAL=full the person's "subject:<id>".
	// Nil stores the principal as it is.
	AuditActorSubject func(ctx context.Context, principal string) (string, error)
	// SubjectKeys is the per-subject key service; person erasure destroys a
	// person's audit-seal key through it. Nil makes the audit_personal_fields
	// scope unavailable.
	SubjectKeys *subjectkey.Manager
	// AuditSinkDrops, when set, reports per-sink audit-delivery drop counts for
	// the wardyn_audit_sink_drops_total metric (cmd/wardynd wires it to the audit
	// Fanout's DropsByName). Nil omits the metric — a deployment with no SIEM
	// sinks configured has nothing to report.
	AuditSinkDrops func() map[string]int64
	// 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
	// MemberMode mirrors WARDYN_USER_DESKTOP (cmd/wardynd's validateMemberModePosture
	// already enforces its precondition at boot). internal/api did not carry this
	// bit before #378/#379: it exists here so handleHealthz can compute
	// token_login — a member-mode desktop's admin token is a PROCESS credential
	// (deploy/desktop/wardyn.env.m-prime.example), never a human sign-in path, so
	// the console must not offer it as one.
	MemberMode bool
	// SSOOnly mirrors WARDYN_SSO_ONLY: the operator's declaration that SSO is the
	// ONLY way into this console. cmd/wardynd's validateSSOOnlyPosture refuses
	// boot unless that is actually true (OIDC configured, admin token/local
	// mode/member mode/no-operator-list override all absent) before this field
	// is ever set, so handleHealthz's sso_only bit — which the sign-in screen
	// reads to drop the admin-token form and the role-derivation caveat — can
	// never overclaim.
	SSOOnly bool
	// GovernAdminRuns mirrors WARDYN_GOVERN_ADMIN_RUNS: an SSO admin's or an
	// admin-role personal token's runs are governed like a member's (see
	// runUngoverned in govern_admin.go). The admin token and local mode stay
	// ungoverned either way.
	GovernAdminRuns bool
	// GovernAdminRunsExempt mirrors WARDYN_GOVERN_ADMIN_RUNS_EXEMPT: the lanes
	// left ungoverned under the switch. The only value is "recording".
	GovernAdminRunsExempt []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
	// TrustedCAPEM is WARDYN_TRUSTED_CA_FILE's content (cmd/wardynd's
	// loadTrustedCA), read once at boot: a PEM bundle of additional roots a
	// corporate TLS-inspecting middlebox signs with. "" (the default) means the
	// knob is unset. Forwarded verbatim to two other trust boundaries dispatch
	// composes per run — the proxy sidecar (runner.ProxyConfig.TrustedCAPEM,
	// which rides WARDYN_PROXY_CONFIG_JSON) and the sandbox's own CA trust
	// (installSandboxTrustedCA, appended to WARDYN_MITM_CA_PEM) — wardynd's OWN
	// outbound TLS trusts it separately, via installTrustedCA mutating
	// http.DefaultTransport at boot. Control-plane-authored only: never a
	// SiteConfig field, never agent-reachable.
	TrustedCAPEM string
	// 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
	// ControlPlaneCAPEM is wardynd's internal CA certificate (internal/hoptls),
	// handed to every proxy as the only root it trusts for ControlPlaneURL.
	// Empty only when ControlPlaneURL is loopback http (a local install).
	ControlPlaneCAPEM 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
	// 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
	// Directory, when set, backs GET /access/directory/search — the
	// console's autocomplete over the identity provider for every "who" field (a
	// governance assignment's subject, the People-step mapping value), so an
	// admin picks a DisplayName and Wardyn stores the ClaimValue instead of
	// hand-typing a group's object GUID.
	//
	// nil is the ABSENT MODE, not a broken one, and it is the default: the
	// handler answers a distinct 503 (directoryUnconfiguredCode) the console
	// reads as "degrade the combobox to a plain text input". Wired only when
	// WARDYN_DIRECTORY_PROVIDER is set (cmd/wardynd's resolveDirectoryConfig,
	// which REFUSES boot rather than handing over a connector that cannot
	// authenticate). Set, it means this daemon can read the WHOLE directory —
	// stated plainly in docs/OPERATIONS.md, because it is a real expansion of
	// the minimal-reach posture rather than a convenience toggle.
	Directory directory.Directory
	// SessionRevocations is the write side of the revoke-a-human-now
	// lever: handleRevokeSessions calls RevokeSub/RevokeAll on it.
	// oidc.Middleware holds the matching READ side (Config.Revocations, wired
	// by the same cmd/wardynd adapter) — this field is nil exactly when OIDC
	// is unconfigured, and the revoke-sessions route only mounts when it is
	// set (see routes.go).
	SessionRevocations oidc.SessionRevocations
	// OperatorEmails is WARDYN_OIDC_OPERATOR_EMAILS, the legacy admin allowlist.
	// internal/api no longer reads this field directly: requireOperator/isOperator
	// (http.go) gate on the session's derived Role instead. The list still
	// matters — cmd/wardynd feeds the SAME value into oidc.Config.LegacyAdminEmails,
	// so an email on it is still an additional RoleAdmin match at OIDC-login role
	// derivation time (see internal/auth/oidc's deriveRole) — it just flows through
	// Session.Role now instead of being re-checked a second time here. Kept on
	// Config (not deleted) so that wiring keeps compiling; read it here only if you
	// need the raw configured list for display (e.g. an admin-facing settings page),
	// never to gate a request.
	OperatorEmails []string
	// AllowEmailMappings is WARDYN_OIDC_ALLOW_EMAIL_MAPPINGS (Q7 adjudication,
	// docs/design/people-access-prompt.md): the console People-step opt-in
	// gate for an email-shaped (contains "@") role-mapping VALUE on
	// POST /access/mappings. Default false refuses one with EMAIL_KEY_REFUSED
	// (access.go) — an SSO/Entra deployment's default posture steers an admin
	// to an App Role or group key instead. Belongs on api.Config, not
	// oidc.Config: it gates a CONSOLE WRITE, not a boot-time role-derivation
	// input, and env WARDYN_OIDC_ROLE_MAP's own email-keyed entries are
	// UNAFFECTED either way (legacy, still boot-warned separately — see
	// buildOptionalFeatures).
	AllowEmailMappings bool
	// UserMounts is the operator/MDM-set posture for MEMBER-authored local_dir
	// binds (WARDYN_USER_WORKSPACE_ROOTS + _MAP + WARDYN_USER_WRITABLE_ROOTS
	// + _DENY, parsed at boot by runner.ParseUserMountPolicy). The ZERO VALUE
	// — the default — means a member may not onboard a host directory at all
	// (repos and operator-owned workspaces are unaffected), which is the
	// fail-closed posture the section-(c) threat model requires. It bounds ONLY
	// mounts on a member-OWNED workspace; an operator's mounts are never
	// narrowed by it. See internal/runner/member_mount.go.
	UserMounts runner.UserMountPolicy
	// UserDriveHostRoots is the operator/MDM-set ceiling over admin-authored
	// host_path USER DRIVES (WARDYN_USER_DRIVE_HOST_ROOTS, parsed at boot by
	// runner.ParseUserDriveHostRoots). The ZERO VALUE — the default — means NO
	// host_path drive may be authored at all, the same fail-closed posture
	// UserMounts takes above and for the same reason: a drive's host root is
	// authored in the database and bound into OTHER PEOPLE's sandboxes, so its
	// ceiling has to live where a console compromise cannot reach it.
	//
	// A []string rather than the types.UserDriveHostRootCheck closure because
	// GET /drives has to report whether a ceiling is CONFIGURED (so the console
	// can disable the host_path option with the reason rather than offering a
	// save that 422s), which a func value cannot answer. The closure is built
	// from it at the write boundary — see Server.userDriveHostRootCheck.
	UserDriveHostRoots []string
	// 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
	// BedrockRegion is read by nothing: the boot Bedrock lane (WARDYN_BEDROCK_*)
	// is retired and a run's region is its provider's. ponytail: kept only so
	// the ssotoken tests that still set it compile; delete it with those lines.
	BedrockRegion string
	// AWSSSOEndpointOverride re-points the two AWS IAM Identity Center services
	// (sso-oidc and the sso portal) at ONE server of the operator's choosing —
	// a TEST hatch, and nothing else. It exists so "a member signs in on
	// Kubernetes and their Bedrock run gets per-user credentials" is provable
	// without a real AWS tenant, by pointing the login sandbox, the dispatch
	// egress list and the dispatch-time token renewal at test/awsssofake running
	// on the cluster. See awssso_endpoint.go for the five derivations it moves
	// and why it is not the global AWS_ENDPOINT_URL.
	//
	// Normalized by ValidateAWSSSOEndpointOverride at boot, which REFUSES a
	// non-empty value unless WARDYN_ALLOW_TEST_ENDPOINTS=true; wardynd also
	// WARNs on every boot that carries it. Empty (the default, and every real
	// deployment) => every SSO derivation is byte-identical to a build that
	// never had this field. A BOOT flag, never a SiteConfig field: a
	// runtime-writable spelling would let an admin re-point a credential
	// exchange with no restart and no boot log.
	AWSSSOEndpointOverride string
	// AllowTestEndpoints is WARDYN_ALLOW_TEST_ENDPOINTS: the only thing that lets
	// a model provider's bedrock.base_url be plain http:// (validateProviderBedrock).
	AllowTestEndpoints bool

	HarnessLoginCPUMillis int // WARDYN_HARNESS_LOGIN_CPU_MILLIS; see harnessLoginResources (harnesscred.go)
	HarnessLoginMemoryMiB int // WARDYN_HARNESS_LOGIN_MEMORY_MIB; ditto — still governance-ceiling capped

	// AWSSSOProxyInject is the kill switch for proxy-side SSO token injection
	// (WARDYN_AWS_SSO_PROXY_INJECT,
	// resolved by ResolveAWSSSOProxyInject at boot): when true a captured-AWS-SSO
	// Bedrock dispatch stages an inert placeholder in the sandbox's token cache
	// and authors a proxy-side injection of the real token onto that run's own
	// portal.sso host, so the session is never resident; when false NEW
	// dispatches stage the real token in the sandbox's own cache instead (a run
	// already dispatched keeps its authored lane until it ends).
	//
	// It is the rollback for an SDK or corporate-MITM surprise without a
	// downgrade. See runs_dispatch_sso_inject.go for the default and docs/ENV.md
	// for the operator-facing contract.
	AWSSSOProxyInject bool
	// 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
	// KeyDomains resolves, lists and writes the key-domain assignments behind
	// /key-domains (migration 0121). Nil answers 501.
	KeyDomains *keydomain.Service
	// KeyDomainKeys names, for each declared key domain, the key that holds it
	// ("Transit key finance", "Key Vault key https://...") for the console's
	// domains table. A domain missing here shows no key.
	KeyDomainKeys map[string]string
	// PrincipalKeys is WARDYN_PRINCIPAL_KEYS=on; /setup/status shows principal_keys.
	PrincipalKeys bool
	// 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
	// MaskManifests, when non-nil, keeps each dispatched run's masking
	// manifest in Postgres and gates the five doors that relay or persist a
	// run's output on it (mask_manifest.go): a run whose corpus it cannot prove
	// complete is refused instead of passed through. Nil keeps no manifests
	// and gates nothing, as a nil MaskRegistry masks nothing.
	MaskManifests *maskmanifest.Manifests
	// HA is WARDYN_HA: several replicas serve this database. It adds the three
	// high-availability rows to /setup/status (setup_checks_ha.go); false adds none.
	HA bool
	// MaskSync reports whether this replica's copy of the shared masking
	// registry is current (the mask_registry_shared row). Read only when HA is set.
	MaskSync MaskSyncProbe
	// LiveBus, when non-nil, carries the notices replicas send each other over Postgres NOTIFY:
	// run lifecycle events, a kill for the replica creating the run's sandbox, and the end of an
	// attach lease (live_bus.go). Nil keeps each replica to its own process, as before.
	LiveBus *livebus.Bus
	// ADORunPATs, when non-nil, keeps each `minted_pat` run's current token in Postgres so every
	// replica serves the one any replica created (ado_run_pat_cache.go). Nil keeps it in this
	// process.
	ADORunPATs *adorunpat.Store
	// ExecOutputTailOff is WARDYN_EXEC_OUTPUT_TAIL=off: no non-interactive run
	// keeps an output tail for GET /runs/{id}/output (run_output.go).
	ExecOutputTailOff bool
	// RunOutputTailBytes is WARDYN_RUN_OUTPUT_TAIL_BYTES: each run's tail size
	// and the cap on ?tail=. Zero defaults to defaultRunOutputTailBytes in New.
	RunOutputTailBytes int
	// ExecOutputTailTTL is WARDYN_EXEC_OUTPUT_TAIL_TTL: how long a run's output
	// tail is kept after its last output. Zero defaults to
	// defaultExecOutputTailTTL in New.
	ExecOutputTailTTL time.Duration
	// RunOutputPersistOff is WARDYN_RUN_OUTPUT_PERSIST=off: the final tail stays
	// in memory only and nothing reaches run_outputs (run_output_final.go). A
	// store that keeps no run outputs behaves the same.
	RunOutputPersistOff bool
	// RunOutputRetention is WARDYN_RUN_OUTPUT_RETENTION_DAYS as a duration: final
	// rows older than it are deleted by the retention sweeper. Zero keeps them
	// forever.
	RunOutputRetention time.Duration
	// ADOEntra resolves the Azure DevOps Entra app registration the per-user
	// sign-in runs against (see ado_entra.go). Nil — the default — means this
	// deployment offers no Azure DevOps sign-in and both of its routes refuse.
	// A function rather than a value so the provider row stays the single
	// source of truth and the sign-in never acts on a cached copy of it.
	ADOEntra ADOEntraSource
	// ADOEntraByRow resolves the row with this id whether or not it is enabled or
	// first, so a token created through a row an admin has since disabled can
	// still be revoked (ado_run_pat_sweep.go). Nil: the revoke uses ADOEntra.
	ADOEntraByRow func(ctx context.Context, rowID string) (ADOEntraConfig, bool, error)
	// AzureFoundryEntra resolves the Entra application an azure_foundry provider
	// row signs people in against, by the row's uid (azure_foundry_entra.go). It
	// answers found=false for a uid that is not an azure_foundry row. The
	// application is always the console's own sign-in application, so a
	// deployment without Entra console login answers an unusable configuration
	// and both legs refuse. Nil: no Azure sign-in is offered.
	AzureFoundryEntra func(ctx context.Context, rowUID string) (ADOEntraConfig, bool, error)
	// ADOLoginFacts is the console's own OIDC client, tenant and whether it holds a secret (S1; nil: none).
	ADOLoginFacts func() (clientID, tenantID string, hasSecret bool)
	// HostResolver is how the model-provider write boundary resolves an azure_foundry endpoint host for its
	// private-address advisory. Nil: the system resolver, bounded to three seconds.
	HostResolver func(host string) ([]net.IP, error)
	// AuditCoalesceWindow folds IDENTICAL consecutive auth.fail audit rows —
	// same boundary, reason, path and peer — into the first row plus one summary
	// row carrying count/first_seen/last_seen (env WARDYN_AUDIT_COALESCE_WINDOW,
	// default 5m at the boot flag; 0 or unset = off, one row per refusal exactly
	// as before). It is a maximum GAP between two consecutive identical rows, not
	// a cap on a streak's duration: the flood this exists for was one row a
	// minute forever from a single retrying sidecar, which the 1/sec rate limiter
	// never trips and which still evicted every real security event out of the
	// console's 1000-row window in minutes. See coalesceAuthFailed (http.go) for
	// the bounds that keep a burst from collapsing into one row. The same
	// window folds the device routes' failure rows (device_audit_bounds.go).
	AuditCoalesceWindow time.Duration
	HostCapacityConfig
	// MaxConcurrentRuns caps non-terminal runs across the whole deployment, every
	// replica and every creation door (env WARDYN_MAX_CONCURRENT_RUNS); 0 or less
	// is unlimited. Past it every door answers 422 run_quota (createRun); POST /runs
	// refuses before minting and audits nothing for it, bar a create that loses
	// the race at the cap, which keeps its identity.mint row.
	MaxConcurrentRuns int
	// PreflightRatePerMin is WARDYN_PREFLIGHT_RATE_PER_MIN: the per-person
	// POST /runs/preflight rate (burst 5). 0 turns the limit off.
	PreflightRatePerMin int
	// Now is overridable in tests; defaults to time.Now.
	Now func() time.Time
	// OrgFederation is the hybrid audit forwarder's status (cmd/wardynd's
	// bootHybrid), nil when WARDYN_ORG_URL is unset. It feeds /healthz's
	// org_federation block, the wardyn_org_federation_lag gauge and the
	// create-run refusal once the organisation has revoked this device.
	OrgFederation func() federation.Status
	// 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
	// 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: the age key was SUPPLIED (WARDYN_AGE_KEY), not generated at boot, or no local key holds anything (store
	// mode, a key service). False, stored secrets are unreadable after a restart: /setup/status warns. Computed in cmd/wardynd.
	AgeKeyDurable bool
	// SecretStoreExternal names the store every credential is written to in store mode ("Vault at
	// vault.example:8200"), "" in local mode; set, /setup/status shows store_external, not the age-key row.
	SecretStoreExternal string
	// SecretKeyService: the key service wrapping every stored data key ("Vault Transit at host"), or "" for the local key; set, /setup/status shows kek_service.
	SecretKeyService string
	// KEKRequired: WARDYN_KEK_REQUIRED; /setup/status shows kek_required_unmet while neither a key service nor an external store holds the credentials.
	KEKRequired bool
	// PlatformKeySeparate: the boot keys have a key of their own: WARDYN_PLATFORM_KEY_FILE in local mode only
	// (under a key service the file counts for nothing), the platform key (and identity) of the key service
	// otherwise. False shows platform_split as a warning (§2.13 c).
	PlatformKeySeparate 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
	// RequireOperatorSetEgress (WARDYN_REQUIRE_OPERATOR_SET_EGRESS, DEFAULT
	// TRUE) makes applyWorkspaceRequirements apply the same provenance gate
	// to a scan_seeded EGRESS requirement that the SECRET side has always applied
	// unconditionally (runs_create.go): only an operator_set requirement is
	// auto-added at launch, and a scan_seeded one — the workspace scanner reading
	// UNTRUSTED repo content — is skipped.
	//
	// Defaulting it TRUE narrows egress for existing workspaces on upgrade, and
	// that cost is deliberate: the secret side calls this exact boundary
	// "security-critical — do not relax", for a reason that applies verbatim to
	// egress — a hostile or simply never-reviewed repo could widen a run's
	// allowlist just by naming a host in a committed file, with no operator ever
	// acting.
	//
	// The upgrade cost is real and bounded: a workspace whose egress
	// requirements are scan_seeded stops having them auto-added, and the run's
	// warnings say which were skipped. The fix is an operator declaring the host
	// (making it operator_set), which is the action the gate exists to require.
	// Set false to auto-add scan_seeded egress requirements again.
	RequireOperatorSetEgress bool
	// DisableGitPATBroker (WARDYN_GIT_PAT_BROKER=off) is the operator escape
	// hatch for the never-resident git_pat lane.
	//
	// With the broker on (the default), a git_pat for a non-GitHub forge is
	// minted PROXY-SIDE and injected on the outbound leg, so the PAT never enters
	// the sandbox — the same posture github_token has always had. Turning it off
	// restores the in-sandbox lane, where the grant id rides the sandbox env and
	// the in-sandbox credential helper mints the PAT into the agent's process.
	//
	// It exists because the broker changes the git TRANSPORT for those hosts (an
	// insteadOf rewrite to a plain-HTTP broker path), and a forge that behaves
	// unexpectedly under that rewrite must not leave a fleet unable to clone.
	// Reverting is a flag flip and a restart, not a redeploy.
	DisableGitPATBroker bool
	// OIDCRoleMapConfigured reports whether WARDYN_OIDC_ROLE_MAP is non-empty —
	// the sso_rbac /setup/status check's gate. Only the presence, never the
	// mapping itself: the API layer has no use for individual entries, only
	// whether the operator has opted into role derivation at all (see
	// internal/auth/oidc's deriveRole). Computed at boot in cmd/wardynd.
	OIDCRoleMapConfigured bool
	// OIDCRedirectURL echoes WARDYN_OIDC_REDIRECT_URL (a URL, not a credential)
	// for the tls_cookie_posture /setup/status check, which flags an https
	// redirect issued while OIDCSecureCookies is still false — the classic
	// behind-an-ingress misconfiguration where WARDYN_TLS_TERMINATED was never
	// set. Computed at boot in cmd/wardynd.
	OIDCRedirectURL string
	// OIDCSecureCookies is the boot-computed secureCookies posture
	// (validateConfig, cmd/wardynd/main.go): true iff wardynd knows the
	// connection is TLS-protected end to end (built-in TLS or
	// WARDYN_TLS_TERMINATED) — the same value threaded into
	// oidc.Config.SecureCookies. Feeds tls_cookie_posture alongside
	// OIDCRedirectURL. Computed at boot in cmd/wardynd.
	OIDCSecureCookies bool
	// BasePath is WARDYN_BASE_PATH ("" = the host root): the prefix Handler
	// mounts every console route under (base_path.go).
	BasePath string
	// SCIM, when non-nil, mounts the SCIM 2.0 Users routes under /scim/v2 (scim_users.go): the way
	// an identity provider suspends and reactivates a person. nil (default) mounts nothing.
	SCIM *SCIMConfig
	// 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
	// SSHListenAddr is WARDYN_SSH_LISTEN: the address the SSH gateway binds
	// (e.g. ":2222"). Empty = off = no listener, no new surface — ServeSSHGateway
	// no-ops when this is empty, so a caller may invoke it unconditionally.
	SSHListenAddr string
	// SSHAdvertiseAddr is WARDYN_SSH_ADVERTISE: the externally-reachable
	// host[:port] surfaced on /healthz and shown in the run-detail "Connect via
	// SSH" pane's `ssh` command — NOT what wardynd binds to (that's
	// SSHListenAddr), since a container/NAT deployment's bind and reachable
	// address routinely differ. Purely advisory copy; the gateway itself never
	// reads it.
	SSHAdvertiseAddr string
	// SSHHostKey is the gateway's persisted ed25519 host key (loadOrCreateSecret
	// pattern, cmd/wardynd), used to derive the ssh.Signer AddHostKey wants and
	// the fingerprint /healthz discloses ("verify on first connect" — public by
	// design, it identifies the server, it authenticates no one). Empty when the
	// gateway is disabled (cmd/wardynd only loads/generates it when
	// SSHListenAddr is set, so a deployment with SSH off never even mints this
	// secret).
	SSHHostKey ed25519.PrivateKey
	// SSHRoleTTL is WARDYN_SSH_ROLE_TTL: how stale a key's role_checked_at
	// (migration 0046) may be before sshAuth's admin-override path refuses it
	// — the bound on the OIDC-login re-check, since SSH itself has no live
	// session to read a current role from. Zero defaults to a sensible 24h in
	// New (matching the flag's own default), so a Config built without going
	// through cmd/wardynd's flags — every test harness, notably — gets the
	// same posture production does rather than an accidental zero-tolerance
	// TTL that fails every override.
	SSHRoleTTL time.Duration
	// APITokenMaxTTL is WARDYN_API_TOKEN_MAX_TTL: the longest lifetime a newly
	// minted API token may have. Zero (the default) means no cap. A mint that
	// asks for no TTL gets this one; a mint that asks for more is clamped to it.
	// It never touches a token already minted.
	APITokenMaxTTL time.Duration
	// RoleStampTTL is WARDYN_ROLE_STAMP_TTL: the oldest an API token's role and
	// group stamp (api_tokens.identity_stamped_at) may be before apiTokenAuth
	// refuses it until its owner signs in again. Zero, the default, is off: no
	// token is refused for the age of its stamp.
	RoleStampTTL time.Duration
	// GovernanceChangeTTL is WARDYN_GOVERNANCE_CHANGE_TTL: how long a governance change held for a
	// second human (WARDYN_GOVERNANCE_SECOND_HUMAN) waits before it expires. Zero means the 72h
	// default.
	GovernanceChangeTTL time.Duration
	// UIListenAddr is WARDYN_UI_SANDBOX_LISTEN: the address the UI-sandbox
	// gateway binds (e.g. ":8081"). Empty = off = no listener, no new surface,
	// mirroring SSHListenAddr. It MUST NOT equal the console's -listen: relayed
	// content is sandbox-authored, and the second listener IS the origin
	// separation that keeps it away from the console's storage (boot refuses —
	// cmd/wardynd's validateUISandboxConfig).
	UIListenAddr string
	// UIAdvertiseURL is WARDYN_UI_SANDBOX_ADVERTISE: the externally-reachable
	// base URL of that listener (e.g. "https://wardyn-ui.example.com"), used to
	// build the enter-URL template /healthz publishes. Advisory copy, like
	// SSHAdvertiseAddr — the gateway binds UIListenAddr, never this.
	UIAdvertiseURL string
	// UIOriginTemplate is WARDYN_UI_SANDBOX_ORIGIN_TEMPLATE: an optional PER-RUN
	// origin (e.g. "https://run-{run}.ui.example.com") for deployments with
	// wildcard DNS. Set, it gives every run its own browser origin — closing the
	// shared-origin residual path mode leaves — and the gateway then REFUSES an
	// enter served on any other host. Empty = shared path-mode origin, where one
	// run's page is separated from another's only by the path-scoped cookie.
	UIOriginTemplate string
	// UISessionTTL is WARDYN_UI_SANDBOX_SESSION_TTL: how long a minted relay
	// session stays usable, and — because the cookie carries its own issued-at
	// — how stale an ALREADY-minted one may be. The UI relay's sibling of
	// SSHRoleTTL: the SSH gateway bounds a stale admin-override stamp, this
	// bounds a stale relay session, and both exist because neither lane has a
	// live console session to re-read a current role from. Zero defaults to
	// defaultUISessionTTL in New (matching the flag's default), so a Config
	// built without going through cmd/wardynd's flags gets the shipped posture
	// rather than a zero TTL that would refuse every session.
	UISessionTTL time.Duration
	// UICookiePolicy is WARDYN_UI_SANDBOX_STRIP_COOKIES: which inbound cookies,
	// beyond the always-stripped wardyn_* namespace, the relay forwards to a
	// sandbox app. The zero value forwards every other cookie.
	UICookiePolicy UICookiePolicy
	// UISessionKey signs the wardyn_ui_sess relay cookie (HMAC-SHA256, >= 32
	// bytes, the loadOrCreateSecret pattern). Nil/short = gateway disabled: a
	// cookie that cannot be signed must never be issued.
	UISessionKey []byte
	// RunConfigKey seals each run's stored proxy config (32 bytes, the
	// wardyn-run-config-key boot key; run_proxy_config.go). Nil: none is kept.
	RunConfigKey []byte
	// DemoVideoBaseURL is WARDYN_DEMO_VIDEO_BASE_URL, validated at boot by
	// ValidateDemoVideoBaseURL (same seven-rule shape as an internal model
	// gateway: https://, no userinfo, no query/fragment). It re-points the
	// Getting Started demo episodes at an operator-run mirror for an
	// air-gapped deployment, where github.com is unreachable — both the
	// download URL episodeUrl's /healthz-reading caller builds and the CSP's
	// media-src this base's host is echoed into (cspMediaSrc,
	// security_headers.go). Empty (the default) = the two hardcoded GitHub
	// hosts, byte-identical to today. Control-plane-authored only, same trust
	// boundary as TrustedCAPEM above — never a SiteConfig field,
	// never agent-reachable.
	DemoVideoBaseURL string
}

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

type HostCapacityConfig struct {
	// HostCapacity refuses every run launch while the host is over its memory
	// or load limits (WARDYN_HOST_*); nil admits everything.
	HostCapacity *hostcapacity.Guard
}

HostCapacityConfig is Config's host-capacity admission, embedded so its wiring lives beside the feature rather than in server.go.

type ImageBuildSweeper added in v0.5.0

type ImageBuildSweeper interface {
	SweepOrphanedBuilds(ctx context.Context) error
}

ImageBuildSweeper is an OPTIONAL capability an ImageBuilder may additionally implement: a boot-time sweep that force-removes build containers orphaned by a crashed or restarted wardynd process. It is a separate interface (rather than a fourth ImageBuilder method) because api must stay target-agnostic — the concrete envbuild package is docker-tagged, so this is checked via type assertion in sweepOrphanedBuilds below, the same optional-capability pattern as store.RunWatcherLeaser. An ImageBuilder that doesn't implement it (nil, a test fake, or a future non-docker target) is simply never swept.

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. logSink, when non-nil, receives build
	// output lines as they happen (e.g. the wizard Build step's in-memory log);
	// nil preserves the implementation's own default (wardynd's slog).
	BuildDevcontainer(ctx context.Context, repoURL, ref, outputTag string, logSink io.Writer) (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. logSink: see BuildDevcontainer.
	BuildFromDevcontainerFiles(ctx context.Context, files map[string]string, outputTag string, logSink io.Writer) (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.
	// logSink: see BuildDevcontainer.
	FinalizeBase(ctx context.Context, baseRef, outputTag string, logSink io.Writer) (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 MaskSyncProbe added in v0.8.6

type MaskSyncProbe interface {
	Synced(ctx context.Context) error
}

MaskSyncProbe is the part of the shared masking registry the mask_registry_shared row asks about: nil when this replica's LISTEN connection is up and its cursor has reached the committed generation, an error otherwise.

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"`
	// Clean is the server-side recordmode.CleanReplay verdict, stamped ONLY for
	// a CONFINED entry (nil for an open recording — the verdict only means
	// something under confinement). Absent (nil) also covers old rows written
	// before this field existed: unknown, not "not clean" — render neutral,
	// never red. Clean means clean for what was replayed: reconcile finalizes
	// on any terminal state, so a replay the operator ends early earns the
	// same verdict as a full one.
	Clean *bool `json:"clean,omitempty"`
	// Caught is the number of observed domains with DenyCount>0 or
	// PendingCount>0, stamped alongside Clean — CONFINED entries only (0/unset
	// for an open recording).
	Caught int `json:"caught,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 at least one of this task's observed hosts was
	// merged into the workspace's `egress:` requirement rows (operator action,
	// never automatic; possibly a subset — the flag is a boolean, not a count).
	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 RefRulesetVerifier added in v0.5.0

type RefRulesetVerifier interface {
	VerifyRefRuleset(ctx context.Context, repo string) (confined bool, detail string, err error)
}

RefRulesetVerifier answers whether GitHub itself confines the App's writes on a repo to the run branch namespace. Satisfied by broker.GitHubMinter. It is a SEPARATE, optional Config field rather than a method on MintBroker because it is the only thing in the API layer that calls an external network service, and the /setup/status checklist must degrade to "unknown" — never "fail" — when it is absent or errors.

type ResolvedProfile added in v0.8.6

type ResolvedProfile struct {
	ID       uuid.UUID
	Name     string
	Contact  *policyref.Contact // the leaf's own: never inherited from a base
	Composed bool
	// Chain is the leaf first, then each base: admin surfaces only.
	Chain   []profileLink
	Ceiling types.RunPolicySpec
	Limits  types.GovernanceLimits
	// Warnings name the LEAF only and never a base's name, id, overlay or contact: they reach a
	// member's 201 clamp_warnings.
	Warnings []string
	// AdminWarnings carry the base-level detail for GET /governance and the 409 bodies.
	AdminWarnings []string
}

ResolvedProfile is one profile as authority: the leaf's identity and contact beside the EFFECTIVE content composed from its chain. It carries nothing a member may not see except through AdminWarnings, which names bases and is never put on a member surface.

type RunLeaseConfig added in v0.8.0

type RunLeaseConfig struct {
	// EndedRunGrace mirrors WARDYN_ENDED_RUN_GRACE: how long a run its lease
	// ended keeps its files (stopped, no network) before it is torn down. 0
	// tears a run down at its end.
	EndedRunGrace time.Duration
}

RunLeaseConfig is the run lease's part of Config (run_lease.go), embedded so its fields read as Config's own.

type SCIMConfig added in v0.8.6

type SCIMConfig struct {
	// Token and TokenNext are the bearers the routes accept (WARDYN_SCIM_TOKEN, and the second one
	// rotation needs). TokenNext may be empty; Token may not.
	Token, TokenNext string
	// Issuer and Tenant key the identity rows a SCIM user is matched against: the OIDC issuer URL
	// sign-ins record, and the tenant id derived from it.
	Issuer, Tenant string
	// PurgeAfter is how long after a suspension the purge sweeper purges the person
	// (WARDYN_SCIM_PURGE_AFTER); zero disables the automatic purge, and DELETE still purges.
	PurgeAfter time.Duration
	// KeepWorkspaces leaves a purged person's workspaces with them (WARDYN_SCIM_LEAVER_WORKSPACES
	// set to keep); false hands them to the operator.
	KeepWorkspaces bool
}

SCIMConfig mounts the SCIM Users routes. Boot validates it (cmd/wardynd/boot_scim.go): a token of at least 32 bytes, an Entra issuer whose tenant is a single tenant, HTTPS only.

type SCMAccess added in v0.7.10

type SCMAccess struct {
	// State is one of the six modelaccess.go values.
	State string `json:"state"`
	// Source is "org" or "separate" — set for a per-user connection that is
	// live or expired_signin (which door to go back through).
	Source string `json:"source,omitempty"`
	// Cause narrows `expired_signin` (scmAccessCauseEnded,
	// scmAccessCauseConsentNeeded).
	Cause string `json:"cause,omitempty"`
	// Org is the Azure DevOps address this row clones from (the row's first
	// base URL) — the {org} the connect and launch dialogs name.
	//
	// NO ROW ID. workspace_admission.go's disclosure rule ("the kind, never
	// the row id") applies here as it does to admitSSHHostLevel's 201 warning;
	// Org is the one deliberate exception the frozen copy needs
	// (LAUNCH_DIALOG_BODY, CONNECT_DIALOG_BODY, PANEL_ORG interpolate {org}
	// verbatim). Org + Kind is the stable key over the /me/scm-access array.
	Org string `json:"org,omitempty"`
	// Kind is the provider kind this row is ("azure_devops", always, today —
	// scmaccess.go grades Azure DevOps rows only) — paired with Org as the
	// stable key a list of these needs, in place of a row id.
	Kind string `json:"kind,omitempty"`
	// CapabilityCeiling is the row's capability_ceiling: the most any run on it
	// may hold. The policy editor locks every capability outside it, since a
	// policy naming one is refused at launch. Read-only here.
	CapabilityCeiling []adoscope.Capability `json:"capability_ceiling,omitempty"`

	// The fields below are set on a row where each person adds their own token
	// (token_mode own_pat, or an Azure DevOps Server row) and on no other.
	//
	// TokenMode is "own_pat" on both.
	TokenMode string `json:"token_mode,omitempty"`
	// ExpiresOn is the date (YYYY-MM-DD) the person said their token expires —
	// the deadline `expiring` and `expired_signin` name. Absent with no token.
	ExpiresOn string `json:"expires_on,omitempty"`
	// MaxDays is the furthest expiry, in days from today, the row's
	// administrator allows (pat_max_days).
	MaxDays int `json:"max_days,omitempty"`
	// TokenScopes is what to tick on Azure DevOps' own token page, in its own
	// wording ("Code (Read & write)"), derived from the row's ceiling.
	TokenScopes []string `json:"token_scopes,omitempty"`
	// GitOnly is set on an Azure DevOps Server row: its token carries git and
	// nothing else (ado_own_pat_server.go).
	GitOnly bool `json:"git_only,omitempty"`
	// RefusedAt (RFC 3339) is when Azure DevOps first refused the token before
	// the expiry the person entered (a 401 answers a revoked token, an expired
	// one and a missing scope alike). Informational: State is unchanged and
	// the token is still used until ExpiresOn. A new token clears it.
	RefusedAt string `json:"refused_at,omitempty"`

	// ADOPATAccess adds the token console's facts (ado_pat_console.go).
	ADOPATAccess
}

SCMAccess is THIS PRINCIPAL's Azure DevOps access answer for ONE row — the shape the Getting-started chip, the Settings connected panel, the New Run rail's preflight line, and GET /me/scm-access all read instead of each re-deriving it from the provider row and the stored blob.

Nothing in this file produces `live` with an EMPTY Source, but every reader must still treat that combination as "makes no per-person claim" (§7.5's ACCESS_SHARED_NOTE) rather than assume Source is always present.

type SandboxOrphanSweeper added in v0.6.0

type SandboxOrphanSweeper interface {
	SweepOrphanedSandboxes(ctx context.Context, minAge time.Duration, isOrphan func(runID uuid.UUID) bool) (int, error)
}

SandboxOrphanSweeper is an OPTIONAL capability a Runner may implement: a label-keyed sweep of sandbox containers whose run row no longer owns them. A crash BETWEEN CreateSandbox and SetSandboxRef (runs_dispatch.go) leaves the per-run agent/proxy containers + network running under a run row with an EMPTY sandbox_ref — and every ref-keyed teardown (reconcileOrphanedSandbox, SweepTerminalSandboxes) skips a ref-empty row, so those containers leak across every reboot, untracked. This sweep finds them by the wardyn.run-id label the substrate stamps and tears down any whose run isOrphan, past the minAge dispatch grace. Checked by type assertion (like ImageBuildSweeper) so api stays target-agnostic; a Runner without it (nil, a test fake, a future non-docker target) is simply never swept. isOrphan is called by the substrate with each labeled run id; only api can read run rows to answer 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) CancelTerminalRunApprovals added in v0.7.2

func (s *Server) CancelTerminalRunApprovals(ctx context.Context, runID uuid.UUID)

CancelTerminalRunApprovals is cancelRunApprovals' exported seam for the THIRD terminal writer: cmd/wardynd's idle reaper (lifecycleStopper.StopRun) is the then-only writer of RunState=STOPPED, it lives outside this package, and before this it ran the revoke half of the cascade and none of the approval half — so an idle-stopped run's PENDING approvals sat in the queue until the 24h ExpireStale sweep, logged "nobody answered" rather than "the run stopped", and stayed decidable (an `always` approve replayed into the workspace allowlist for a sandbox that is gone). One helper, three callers; the reason is still read back from the run row, so the reaper passes nothing it could get wrong.

It is also the reaper's one call into this package after it wins RUNNING->STOPPED, so the idle stop reaches the run's event feed and revokes the run's Azure DevOps personal access tokens here.

func (*Server) CaptureLoginGrant added in v0.7.10

func (s *Server) CaptureLoginGrant(ctx context.Context, subject string, grant oidc.LoginGrant)

CaptureLoginGrant implements the capture half of oidc.LoginGrantSink: it stores the Azure DevOps refresh token the console login just earned.

THE IDENTITY BINDING IS SATISFIED BY CONSTRUCTION HERE, not skipped. The dedicated sign-in must prove that the id_token it received belongs to the session that started the flow, because those are two different flows. At login they are ONE flow: subject is the verified id_token's own subject, and it is the principal the session is being issued for. There is no second token to disagree with it, so the guarantee is the same one, reached by a shorter road — and the credential still lands under that subject's own namespace and nowhere else.

It reports nothing and refuses nothing. Every early return below is a case where the person is signed in and simply has no Azure DevOps credential yet.

func (*Server) EraseRunOutputs added in v0.8.6

func (s *Server) EraseRunOutputs(ctx context.Context, runIDs []uuid.UUID) error

EraseRunOutputs erases runIDs' output for good: a tombstone and the deletion in one transaction (nothing at all if it fails), then this process's tails are zeroed and their finalisers fenced. Every other replica converges on its next touch, because every write and read checks the tombstone in its own transaction. ar-l1.4 calls it from erasure.Orchestrate.

func (*Server) FinishRunOutput added in v0.8.6

func (s *Server) FinishRunOutput(ctx context.Context, runID uuid.UUID)

FinishRunOutput is both halves of the contract, for a caller outside this package (cmd/wardynd's idle stop) that has already stopped the sandbox.

func (*Server) FlushAuthFailedStreak added in v0.7.2

func (s *Server) FlushAuthFailedStreak()

FlushAuthFailedStreak closes whatever streak is still open and records its summary row. Called at daemon shutdown (cmd/wardynd's serveAndShutdown, after httpSrv.Shutdown has stopped accepting requests and before the audit sinks are closed), mirroring the proxy's flushPrivateIPMemo at the same point in its own stop — and for the same reason: a streak closes only on a different key, the 1000-cap, or the 5m gap timer, so a restart during a steady drip lost up to 999 refusals' COUNT. The opening row is already in the trail, so what was lost was only the volume — during exactly the rollout this instrument exists to measure.

The timer could not have saved it either: a streak's AfterFunc records under BaseCtx, and by shutdown BaseCtx is already cancelled. So this records under context.WithoutCancel(BaseCtx) — the daemon's values (the audit recorder reads them), none of its cancellation.

Ceiling, stated rather than implied, same as the memo's: this is the ORDERLY stop. A SIGKILL, an OOM kill or a pod deleted out from under the process drops the open streak's count — never the refusals themselves, each of which opened its streak with a row that is already recorded. So does a shutdown that lands mid-flood with the rate limiter empty: this row pays the limiter like every other summary (recordAuthFailedSummary), and a refused one is counted in the suppressed series instead — the audit trail's bound outranks a count.

func (*Server) GoBackground added in v0.8.0

func (s *Server) GoBackground(fn func())

GoBackground is goBackground, exported for cmd/wardynd's own long-lived background loops that must not be abandoned mid-cycle by a SIGTERM the way a bare `go` would be — today the terminal-sandbox sweep ticker (runTerminalSandboxSweeper, #710): fn is expected to run until its ctx is cancelled and then return promptly, so WaitBackground's wait for it resolves near-instantly once rootCtx is already cancelled (which it always is by the time serveAndShutdown calls WaitBackground), never by hitting backgroundShutdownBudget the way a genuinely wedged request-detached teardown would.

func (*Server) Handler

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

Handler returns the console listener's handler: the chi router, mounted under Config.BasePath when one is set. Once the proxy hop is TLS (ControlPlaneCAPEM set) the console answers InternalPathPrefix with the same 404 the internal listener gives console routes: every internal caller is pinned to InternalHandler's listener, so a run token or sensor bearer arriving here came from somewhere that is not one (#1263).

func (*Server) HealthSweeps added in v0.8.6

func (s *Server) HealthSweeps() []sweephealth.Sweep

HealthSweeps returns the background sweeps this package owns whose start condition holds on this install: the run watcher needs a runner, and the orphaned build sweep needs an image builder that can sweep. cmd/wardynd registers them with its own, on every replica, whether or not it holds the sweeper lock.

func (*Server) InternalHandler added in v0.8.0

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

InternalHandler returns the chi router at the host root, for the proxy-facing TLS listener. Runs dial that listener directly, never through the operator's reverse proxy, so WARDYN_BASE_PATH does not apply to it.

func (*Server) LoginScopes added in v0.7.10

func (s *Server) LoginScopes(ctx context.Context) []string

LoginScopes implements the request half of oidc.LoginGrantSink: the extra scopes the console login should ask consent for.

It returns the row's whole ceiling plus `offline_access`, never a subset. Consent is the only narrowing that has any effect on this service — a later token request naming a subset is answered with everything consented — so the ceiling asked for here IS the reach the organisation is choosing to grant, and asking for less at login would mean a second consent prompt later for anything left out. `openid` is already in the login's base scopes.

For a minted_pat row the "ceiling" is exactly adoscope.MintScopes (validate holds it to that, S4): the login asks for the two token permissions and never a capability, since a run's access lives in the token it creates. A bearer row's ceiling never names one (validate again).

nil on every doubt: no source wired, no row, an unusable row (S1 included), or a row against another tenant or application. A nil answer leaves the authorization request exactly as it was.

func (*Server) ReconcileOnBoot

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

ReconcileOnBoot rebuilds the safety net that keeps a run from stranding non-terminal forever with a live sandbox and un-revoked credentials (C3), then keeps rebuilding it for the life of the daemon. Three parts, because they are safe under different conditions:

  1. finalizeUndispatchedRuns — at boot AND on a slow cadence after it. A non-terminal run older than undispatchedGrace with no sandbox ref cannot still be provisioning anywhere, so the process that was provisioning it is gone. It is the only reaper of that state, and the process that could reach it may never boot again, so boot-only would strand it forever.
  2. sweepRunWatchers — at boot AND every watcherSweepInterval after. It claims runs whose watcher lease has gone stale, so a run orphaned by a pod that never comes back is adopted by ANY live replica instead of waiting for that pod's own boot, which may never happen. This is what the per-run watcher goroutine (an in-process, un-persisted thing) could never provide alone.
  3. sweepOrphanedBuilds — at boot AND every buildSweepInterval after, independent of s.cfg.Runner: reaps envbuild build containers left behind by a crashed/restarted process (see ImageBuildSweeper). The boot pass alone is NOT enough, because the sweep's own safety gate is an age gate (2*BuildTimeout, envbuild/reaper.go): under systemd Restart=always or a k8s pod restart the process is back within seconds, so at the one moment anything looked, the orphan was too young to reap — and its writable layer would then leak until the next boot, which may be weeks away.

Best-effort: errors are logged, never fatal. Adopted watchers run on the daemon base context so they outlive this call, and observe the AGENT via the persisted agent_exec_id rather than the container — an idle-container exec run whose agent already exited is finalized instead of stranded.

func (*Server) ReconcileWorkspaceEgressDecisions added in v0.6.0

func (s *Server) ReconcileWorkspaceEgressDecisions(ctx context.Context) (int, error)

ReconcileWorkspaceEgressDecisions re-applies decided `always` egress verdicts to the run's primary workspace (persistWorkspaceEgressDecision is not atomic with Decide). AddWorkspaceEgressDecision is an idempotent upsert that also clears the mirror list. Returns the count re-applied. It runs once at boot (cmd/wardynd). ponytail: boot-only heals on the next restart; a periodic tick would heal sooner on a laptop that rarely reboots — add one if that window proves too wide. The operator's newest action must win: per (workspace, host) only the latest DecidedAt is kept, and a decision older than Workspace.EgressEditedAt (stamped by the egress-list PUTs) is skipped — an undone `always` still reads APPROVED, so a per-approval "applied" marker could not tell. Decided rows are never deleted: cap this with an incremental scan if the table grows large.

func (*Server) RecordCredentialReauthExpired added in v0.7.6

func (s *Server) RecordCredentialReauthExpired(n int)

RecordCredentialReauthExpired counts credential_reauth rows the approval sweeper aged out. Exported for cmd/wardynd's sweeper goroutine, which is the ONLY place this transition happens: by the time a row expires the sidecar that held for it gave up hours ago, so no resolve will ever meet it and counting at a resolve would count nothing at all.

func (*Server) RedeemADOEntraAccess added in v0.7.10

func (s *Server) RedeemADOEntraAccess(ctx context.Context, cfg ADOEntraConfig, owner string, scopes []string) (ADOEntraAccess, error)

RedeemADOEntraAccess mints an Azure DevOps access token for owner, REQUESTING only the scopes named.

IT DOES NOT PROMISE A NARROW TOKEN, and no caller may act as though it does. Measured against a real tenant, Entra ignores the requested subset for the Azure DevOps resource: the token comes back carrying every scope the person consented to, whatever was asked for. The subset is still sent because that is what a correct client sends, it costs nothing, and it is what would narrow if this ever changes — but the truth a caller must build on is the returned ADOEntraAccess.Scopes, and the layer that actually bounds a run is Wardyn's own capability check in front of the resource, not the token.

`.default` is refused regardless: it asks for every permission the application holds rather than naming anything, so a request carrying one says nothing about what the caller believed it needed.

SINGLE-FLIGHT, and the read happens INSIDE the lock: a second caller that had already read the pre-rotation blob would otherwise redeem the spent token, take an invalid_grant, and classify a perfectly good credential as dead. Re-reading under the lock means the second caller redeems what the first one persisted.

A Put that fails AFTER a successful redeem does NOT fail the caller: the rotated token is already live at Entra and the presented one is already spent, so refusing would throw away a credential we hold. The caller is served from memory and the persist failure is logged — the next caller will find the stored token dead and say so honestly, which is strictly better than pretending this one failed.

func (*Server) RedeemAzureFoundryAccess added in v0.8.6

func (s *Server) RedeemAzureFoundryAccess(ctx context.Context, cfg ADOEntraConfig, owner, uid, audience string) (ADOEntraAccess, error)

RedeemAzureFoundryAccess mints an access token for owner, for the audience this row's capture names. The audience comes from the dispatch snapshot, so it is refused unless it is one of the two closed literals BEFORE a token request is built: a tampered snapshot cannot point a person's refresh token at a resource the table does not name. cfg is the console application (the by-uid source's answer).

The single-flight is keyed by owner and row uid, exactly as Azure DevOps' is by owner and row.

func (*Server) ServeSSHGateway added in v0.5.0

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

ServeSSHGateway blocks running the SSH gateway's accept loop until ctx is cancelled or the listener fails unrecoverably. A no-op (nil error) when the gateway is disabled — WARDYN_SSH_LISTEN empty = off = no listener, no new surface — so cmd/wardynd may call this unconditionally once it has decided whether to launch it in a goroutine at all.

func (*Server) SnapshotRunPane added in v0.8.6

func (s *Server) SnapshotRunPane(ctx context.Context, runID uuid.UUID)

SnapshotRunPane is the graceful-stop half of the output contract, for a caller outside this package (cmd/wardynd's idle and max-age stops): it runs after the run's revocations and before StopSandbox, and does nothing for a run that keeps no snapshot.

func (*Server) SweepExpiredCredentials added in v0.8.0

func (s *Server) SweepExpiredCredentials(ctx context.Context) (int, error)

SweepExpiredCredentials deletes every stored credential whose expiry has passed, audits each as credential.expired.delete, and returns how many it deleted. cmd/wardynd calls it daily. A row it could not delete is kept, and the next sweep tries it again. A store that cannot sweep — no DeleteExpired, or a wrapper answering secretstore.ErrNoExpirySweep — is logged at Error once.

The error is the store's: a delete that failed for any row, or a scan that failed, so the tick does not count as a success. A store that cannot sweep at all is not an error here, because that is a fact about the deployment, announced above, not a tick that failed.

func (*Server) SweepRunOutputs added in v0.8.6

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

SweepRunOutputs is one pass of the retention sweeper: it deletes final rows older than the retention window, then resolves the pending rows of terminal runs whose claim went stale to capture-gap rows (when persistence is on, the only time pending rows are written). A tail this process still holds is the finisher's own, and is left alone.

func (*Server) SweepRunSecrets added in v0.6.0

func (s *Server) SweepRunSecrets(ctx context.Context) (int, error)

SweepRunSecrets drops the per-run plaintext secret corpora held in memory for runs that went terminal more than RunSecretGrace ago, and reports how many it evicted.

Eligibility is read from the run's own state rather than hooked onto a terminal-transition call site, so every path that ends a run (watcher, kill, idle reaper, boot reconcile, failAndRevoke) is covered by construction and no new one can be forgotten. It fails CLOSED on anything it cannot positively prove terminal-and-cold: a store error, or a held run id with no row in the listing, keeps its secrets.

It also drops the process-wide mask copies of credentials that were refreshed or deleted more than RunSecretGrace ago (secretmask.Registry.SweepGlobals), on the same grace and for the same reason.

The error is the run listing's: a sweep that could not look at the runs evicted nothing, and says so, so that it is not counted as a clean tick.

func (*Server) SweepSCIMPurge added in v0.8.6

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

SweepSCIMPurge is one tick of the purge sweeper, run on the elected sweeper leader: it purges every suspended person past their purge_after (unless WARDYN_SCIM_PURGE_AFTER is zero) and resumes any suspension, purge or group removal whose identity provider stopped retrying. Idempotent, and a failure on one identity does not stop the others.

func (*Server) SweepTerminalSandboxes added in v0.5.0

func (s *Server) SweepTerminalSandboxes(ctx context.Context) (int, error)

SweepTerminalSandboxes is the retry surface for two gaps in the same family: a failed StopSandbox/RevokeRun step inside a prior finalize (finalizeRunTail) or kill (handleKillRun) leaves a terminal run row with a sandbox nothing else revisits — ReconcileOnBoot skips terminal runs outright (reconcile.go), and handleKillRun 409s a non-KILLED terminal run rather than risk corrupting its recorded outcome — and, separately, killTeardownTail itself can be interrupted (a shutdown past its grace, a crash) after the KILLED CAS has already landed but before the teardown/revoke/audit tail finishes, leaving a run correctly marked KILLED with no run.kill row at all (see recoverAbandonedKillTail).

This PROBES the runner for every terminal run with a SandboxRef (never trusts the row's own state — the row is terminal by definition, so only a live probe can tell orphaned from settled), tears down + re-runs the idempotent revoke cascade for anything still reported running, retries any KILLED row whose own kill tail looks abandoned, and reports how many it swept. Read-only on run STATE: it never transitions a run (a terminal row's recorded outcome is untouched either way), only the runner + broker/ identity + audit side effects finalizeRunTail/killTeardownTail already perform for every other terminal transition.

Caller: the operator route (POST /admin/sandboxes/sweep, unbounded — an operator asked for it once) and, bounded, the periodic ticker (cmd/wardynd's runTerminalSandboxSweeper, via SweepTerminalSandboxesPage below).

func (*Server) SweepTerminalSandboxesPage added in v0.8.0

func (s *Server) SweepTerminalSandboxesPage(ctx context.Context, page store.Page) (swept, pageLen int, err error)

SweepTerminalSandboxesPage is SweepTerminalSandboxes bounded to one page of runs, for the periodic ticker: unlike the operator-triggered SweepTerminalSandboxes, which is deliberately unbounded because a human asked for it once, a ticker's cost must never grow with run history. It pages through store.Pager.ListRunsPage (the same ordering ListRuns' unbounded read already uses, created_at DESC) — the caller advances page.Offset by pageLen each tick and wraps back to 0 once pageLen comes back short of page.Limit, so repeated ticks eventually cover the whole table at bounded per-tick cost. Falls back to sweeping every run in one pass when the store does not implement store.Pager (a fake without it, in tests) — real PG always does.

func (*Server) UIGatewayHandler added in v0.6.0

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

UIGatewayHandler returns the gateway's http.Handler, or nil when the gateway is disabled (cmd/wardynd then starts no listener). It is deliberately NOT mounted on the console router: these routes must exist ONLY on the second origin, and Server.Handler() must 404 them — pinned by test.

Consequence, stated because it is easy to miss: being off the chi router also makes this surface invisible to TestAuthzMatrix, which discovers routes by walking that router. A green authz matrix says NOTHING about relay authorization. The ticket/session tests in uigateway_test.go are this surface's own pin, exactly as sshgateway_test.go is the SSH gateway's, and the matrix's own doctrine comment now names both boundaries.

func (*Server) WaitBackground added in v0.8.0

func (s *Server) WaitBackground()

WaitBackground blocks until every goroutine started through goBackground has returned, or until backgroundShutdownBudget elapses — whichever comes first. Bounded because this runs on the shutdown path with an operator waiting: a genuinely wedged teardown (a runner call that never returns) must not turn an orderly stop into a hang. A hit budget is logged rather than silent, so an abandoned kill cascade or launch is visible in the log instead of just disappearing.

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"`
	// GovernAdminRuns is WARDYN_GOVERN_ADMIN_RUNS and GovernAdminRunsExempt the
	// lanes WARDYN_GOVERN_ADMIN_RUNS_EXEMPT leaves ungoverned. Neither is a
	// secret, and redactSetupStatusForUser leaves Auth alone, so every signed-in
	// person can read the posture their own runs are under.
	GovernAdminRuns       bool     `json:"govern_admin_runs"`
	GovernAdminRunsExempt []string `json:"govern_admin_runs_exempt,omitempty"`
}

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

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"`
	Blocking bool   `json:"blocking,omitempty"`
	// Cause narrows a row that can warn for more than one reason, the same
	// shape as SCMAccess.cause on the wire (ui/src/app/lib/types/setup.ts) — a
	// machine key, never prose, so a console reader can pick per-cause copy
	// without string-matching Detail. sso_rbac sets it ("default_role", #491:
	// #484's original no-role-map-and-no-admin-list warn leaves it empty), and so
	// does substrate_health ("runner_unreachable", "runner_auth", "sweep_stale").
	Cause string `json:"cause,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).

Blocking decides ONE thing: whether the console must not open on this install at all — setupGateActive (the console's setup gate) redirects every route into the funnel while any row carries it, until onboarding completes. It is set on exactly three arms: runnerCheck's fail (no runner configured, so no run can happen; a configured runner whose Capabilities() errored is a substrate fault and never blocks), confinementFloorCheck's warn (a floor the runner cannot meet refuses every run before it launches), and ssoRBACCheck's warn (OIDC with no role mapping makes every signed-in human an admin, and the funnel's People step is where that is fixed).

It is NOT "this needs fixing" — nearly every warn/fail row does — and NOT a severity ranking. Two families must never carry it. Rows graded through the CALLER's own credential rather than the install: llmProviderCheck's per_user and provider arms, bedrockProviderCheck's per_user arm, providerAccessCheck and awsSSOCredentialRow (the caller's own AWS SSO session). Confiscating the console over a fact about one person is exactly what this flag exists to prevent. And advisory install rows, whose grade belongs on every surface that renders them but whose fix is nobody's emergency: the SCM safest-path ladder, an ephemeral age key, TLS cookie posture, an acknowledged egress canary. (harnessCredential- Check is install-wide, not per-person — its managed-subscription arm reads an unscoped blob — and is advisory for the second reason, not the first.)

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 (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 SetupHarnessTool added in v0.5.0

type SetupHarnessTool struct {
	ID            string `json:"id"`
	Display       string `json:"display"`
	HasGateway    bool   `json:"has_gateway"`
	HasLogin      bool   `json:"has_login"`
	NoManagedAuth bool   `json:"no_managed_auth,omitempty"`
	// Enabled is the ADMIN'S answer for this row: may a run name this agent?
	// True for every row when no AgentProviders block exists (legacy open mode),
	// otherwise the row's own state — and false for a catalog agent the block
	// does not mention at all.
	//
	// NOT omitempty, and that is the whole point: false is the value that has to
	// reach the console, which renders a disabled row disabled with a reason
	// rather than hiding it. A roster that silently drops agents is how "Claude
	// Code is just gone" becomes a support ticket.
	Enabled bool `json:"enabled"`
	// ProvidersUngranted (#1052) is true when at least one enabled model
	// provider serves this harness but none of the ENABLED providers in this
	// caller's own granted set (setupModelProviderState's capVisible-narrowed
	// model_providers) serve it — canon.md's R5b, "no provider serves this
	// person for this harness at all". Enabled rows only on both sides: a
	// provider the caller is granted but an admin turned off serves nobody, so
	// it does not answer a different enabled provider they are not granted. Distinct from R9 (nothing serves the harness at all,
	// which leaves this false): /setup/status's model_providers list is
	// already narrowed to granted providers before the wire, so without this
	// fact the two states were indistinguishable to the console.
	ProvidersUngranted bool `json:"providers_ungranted,omitempty"`
}

SetupHarnessTool is one coding-agent harness's static catalog metadata (harnessCatalog, harness.go): display name, plus whether Wardyn can wire it a managed model credential (Gateway) and/or a container-login subscription (Login), or whether it is the BYOA row (NoManagedAuth). This is the STATIC "what tools does Wardyn know how to run" catalog.

type SetupIntegration added in v0.5.0

type SetupIntegration struct {
	Capabilities []Capability `json:"capabilities"`
	// contains filtered or unexported fields
}

SetupIntegration is one integration as the UI sees it: the effective row (stored or legacy-derived, integrationRow) plus its live capability matrix. Shared wire shape for GET /integrations and SetupStatus.Integrations, so there is exactly one representation of "an integration" on the wire.

func (*SetupIntegration) UnmarshalJSON added in v0.5.0

func (si *SetupIntegration) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes the embedded row (see integrationRow.UnmarshalJSON — the embedded Integration's custom decoder would otherwise be promoted here and swallow capabilities) and then the wrapper's own field.

type SetupItem

type SetupItem struct {
	Kind       string    `json:"kind"` // "llm_access" | "secret" | "workspace" | "workspace_secret" | "repo_credential" | "egress" | "backend"
	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 SetupModelProvider added in v0.8.0

type SetupModelProvider struct {
	ID   string `json:"id"`
	Name string `json:"name,omitempty"`
	Kind string `json:"kind"`
	// Disabled is published, never hidden: a turned-off default is why this
	// person's runs are refused, and they need to see it.
	Disabled bool `json:"disabled,omitempty"`
	// Harnesses are the ones this provider serves that this person may launch.
	Harnesses []string `json:"harnesses"`
	// DefaultFor are the harnesses whose roster default this provider is.
	DefaultFor []string `json:"default_for,omitempty"`
	Host       string   `json:"host"`
}

SetupModelProvider is one model provider as a person sees it on /setup/status: enough to choose it, and — D7 — the host their own credential would be sent to (the host only, never a path). No start URL, account pin, header scheme or UID. Built the same for admins and members, so there is nothing for the member redaction to strip.

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

SetupProvider is a coding-agent CLI (claude|codex) detected on the wardynd host's PATH.

type SetupProviderAccess added in v0.8.0

type SetupProviderAccess struct {
	Provider    string     `json:"provider"`
	State       string     `json:"state"`
	Action      string     `json:"action,omitempty"`
	Deadline    string     `json:"deadline,omitempty"`
	SourceRunID string     `json:"source_run_id,omitempty"`
	AddedAt     *time.Time `json:"added_at,omitempty"`
	LastUsedAt  *time.Time `json:"last_used_at,omitempty"`
}

SetupProviderAccess is one provider's connection state for the caller — the wire shape provider_access: [{provider, state, action, deadline}] the design names: a provider id, one of modelaccess.go's five states, an already-composed sentence, and (only when the state names one) a deadline instant. No secret name, no start URL, no host. The one place a pinned account and role appear is the pin-mismatch action, which names both pairs to the caller — members included — because they must pick the pinned pair when they sign in again.

SourceRunID is the sign-in run whose capture the caller's stored credential for this provider is (#993): stamped server-side from that run's own token when the capture lands (ssotoken.go, provider_signin.go), read from the caller's own provider-scoped store, so the console confirms THIS sign-in from the credential itself rather than from its best-effort audit row. Absent for a kind with no sign-in (a typed key) and for no capture.

AddedAt and LastUsedAt are the caller's own stored credential's metadata (CS-6), absent when none is stored.

type SetupRunner

type SetupRunner struct {
	Driver                string            `json:"driver"`
	ConfinementClasses    []string          `json:"confinement_classes"`
	ConfinementSubstrates map[string]string `json:"confinement_substrates,omitempty"`
	// EphemeralDiskEnforcement is what actually binds a run's disk_mib on this
	// deployment — `filesystem`, `eviction` or `none`; absent reads as `none`
	// (runner.Capabilities.EphemeralDiskEnforcement, weakest across substrates).
	// The Workspace Providers screen renders it beside default_disk_mib so an
	// admin setting a number can see whether anything will hold it.
	//
	// Operator-only: redactSetupStatusForUser rebuilds this struct with
	// ConfinementClasses and Kubernetes alone, so the word never reaches a
	// member. It is deliberately absent from the ANONYMOUS /healthz, which
	// composes its own body field by field.
	EphemeralDiskEnforcement types.StorageEnforcement `json:"ephemeral_disk_enforcement,omitempty"`
	// Kubernetes is the ONE substrate bit a member may read: the runner is the
	// Kubernetes driver. Driver itself stays operator-only, so without this a
	// member's console cannot tell a Kubernetes install apart and would hand
	// them the Docker host's /dev/kvm remedy for Vault. A boolean, deliberately
	// not the driver name or anything a substrate reports about itself.
	Kubernetes bool `json:"kubernetes,omitempty"`
	// SandboxStart is the Kubernetes sandbox start deadlines (WARDYN_SANDBOX_START_TIMEOUT,
	// WARDYN_SANDBOX_CAPACITY_WAIT), kept in a member's redacted body because the run page's
	// "taking longer than expected" bound follows them. Absent off Kubernetes.
	SandboxStart *SetupSandboxStart `json:"sandbox_start,omitempty"`
	// contains filtered or unexported fields
}

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

type SetupSandboxStart added in v0.8.6

type SetupSandboxStart struct {
	StartTimeoutSeconds int `json:"start_timeout_seconds"`
	CapacityWaitSeconds int `json:"capacity_wait_seconds"`
}

SetupSandboxStart is how long a Kubernetes sandbox has to start, and how much longer one that no machine has room for waits. Seconds on the wire; a capacity wait of 0 is the wait switched off.

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"`
	// 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"`
	// CredentialStorage names the KIND of store credentials live in — never a
	// host, path or vault name (design F-3): local/key_service/vault/key_vault.
	// Unlike Checks (the same fact as admin-only detail), this is KEPT through
	// redactSetupStatusForUser — every person reads it, not just an admin.
	CredentialStorage string `json:"credential_storage,omitempty"`
	// HasRuns drives the wizard's "launch your first run" done state.
	HasRuns bool `json:"has_runs"`
	// OnboardingComplete reports whether an operator has finished (or
	// deliberately left) the Getting Started funnel on this install —
	// SiteConfig.OnboardingCompletedAt, flattened to the only bit the console
	// needs. It is a fact about the install, not about the browser: browser
	// localStorage would outlive wiped databases and disagree with itself
	// between 127.0.0.1 and localhost (different origins, different storage).
	// The first-run landing, the welcome hero and the setup gate all read THIS.
	OnboardingComplete bool `json:"onboarding_complete"`
	// 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"`
	// Integrations is the effective integration set (stored ∪ legacy-derived,
	// see effectiveIntegrations in integrations.go) with each row's live
	// capability matrix — the same shape GET /api/v1/integrations returns,
	// folded in here so the wizard needs one fewer round trip. ADDITIVE field;
	// omitted when empty.
	Integrations []SetupIntegration `json:"integrations,omitempty"`
	// Harnesses is the STATIC coding-agent harness catalog (harnessCatalog,
	// harness.go) — which tools Wardyn knows how to run and whether it can
	// wire each one a managed model credential or a container-login
	// subscription. ADDITIVE field; omitted when empty.
	Harnesses []SetupHarnessTool `json:"harnesses,omitempty"`
	// LLMReady is the server-computed "does SOME run's LLM access path exist"
	// verdict (HIGH-4 review fix): at least one enabled model provider serves an
	// agent (llmPathExists). A DEPLOYMENT fact — whether THIS person can use one
	// is ProviderAccess. It exists because a MEMBER'S redacted response
	// (redactSetupStatusForUser) drops the checks detail that would otherwise
	// let the console derive this itself — LLMReady is computed BEFORE redaction
	// and deliberately left untouched BY it, so the console's readiness chip /
	// new-run banner / demo gating keep working for a member. Kept in exact sync
	// with ui/src/app/lib/types.ts's SetupStatus.
	LLMReady bool `json:"llm_ready"`
	// ChecksRedacted marks a body whose Checks/Providers/Secrets detail was
	// stripped for this caller's tier — an empty list here is withheld, not a fact.
	ChecksRedacted bool `json:"checks_redacted,omitempty"`
	// SCMAccess is THIS PRINCIPAL's Azure DevOps access state — scmaccess.go's
	// computeSCMAccess, ProviderAccess's sibling for #386. Zero value (state "")
	// when no Azure DevOps row is configured; safe for a member by
	// construction (their own state, computed from their own OIDC subject).
	SCMAccess SCMAccess `json:"scm_access,omitzero"`
	// TrustedCACerts is the number of additional roots WARDYN_TRUSTED_CA_FILE
	// loaded at boot (0 = unset). Derived from Config.TrustedCAPEM, never a
	// second boot-time field — see handleSetupStatus. Go + test only: no
	// console reader exists yet (the ui/src/app/lib/types.ts mirror is
	// hand-maintained, added when the Network step renders it) and
	// redactSetupStatusForUser does not zero it — a bare count carries no
	// PEM content, host name, or other detail members are barred from.
	TrustedCACerts int `json:"trusted_ca_certs,omitempty"`
	// ModelProviders is the model providers THIS PRINCIPAL may use, in the
	// member-safe shape (SetupModelProvider) — the same for every tier, so the
	// member redaction has nothing to strip. `omitzero`, not `omitempty`:
	// setupModelProviders returns nil when there is no provider block at all
	// (the field is then absent, exactly as before), and a non-nil, possibly
	// empty slice whenever a block exists — including one this caller is
	// granted nothing from, or one capVisible's own filter-error path
	// answered with rows[:0:0] — so the field is then present as `[]`. A
	// console reading this field can tell "no block" from "granted none"
	// only because of that distinction; `omitempty` could not make it (it
	// drops both nil and an empty-but-present slice alike).
	ModelProviders []SetupModelProvider `json:"model_providers,omitzero"`
	// ProviderAccess is THIS PRINCIPAL's connection state for every provider in
	// ModelProviders (MP-12) — one row per provider. Getting started and the
	// setup checklist read this, so a person granted several providers sees all
	// of them. Kept for members: a state
	// name, an already-composed action sentence, and a deadline instant — no
	// secret names, no start URL. The pin-mismatch action is the one place the
	// pinned account and role appear (SetupProviderAccess's doc). Absent with
	// no provider block.
	ProviderAccess []SetupProviderAccess `json:"provider_access,omitempty"`
}

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

type SweeperLease added in v0.8.6

type SweeperLease interface {
	Join() (ctx context.Context, epoch int64, end func(), ok bool)
	Current(ctx context.Context, epoch int64) (bool, error)
}

SweeperLease is the leader election the sweeps that must run once share (db.SweeperLeader). Join joins the current term; ok is false on a follower. The returned context ends when the lease is lost, and end must be called when the work is done, because the leader waits for every end before it releases its lock. Current reports whether epoch is still the durable one: false once a newer leader has acquired the lease.

type UICookiePolicy added in v0.8.0

type UICookiePolicy struct {
	// Allow true forwards ONLY the cookies Names matches; false strips them.
	Allow bool
	// Names are exact cookie names, or prefixes when they end in "*".
	// Matching is case-sensitive, as browsers treat cookie names.
	Names []string
}

UICookiePolicy is WARDYN_UI_SANDBOX_STRIP_COOKIES parsed: which inbound cookies, beyond the wardyn_* namespace that is stripped unconditionally, the relay forwards to a sandbox app. The zero value is the default and forwards every other cookie.

It exists for a relay host under a registrable domain shared with other applications: a browser attaches a sibling host's Domain= cookies to the relay's requests, and the Cookie header carries only name=value, so a cookie's origin cannot be recovered — the operator names the cookies instead.

func ParseUICookiePolicy added in v0.8.0

func ParseUICookiePolicy(raw string) (UICookiePolicy, error)

ParseUICookiePolicy parses "allow:<names>" or "deny:<names>", a comma-separated list of cookie names or "prefix*" entries. Empty is the default policy. Boot refuses anything else (cmd/wardynd, validateUISandboxConfig), including an allow entry inside the wardyn_* namespace: those cookies are never forwarded, and a policy that reads as if it forwarded one is a misconfiguration to name, not to ignore.

Source Files

Jump to

Keyboard shortcuts

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