Releases: cjohnstoniv/wardyn
Release list
v0.6.6
A follow-up to 0.6.5 for the k8s runner: on a cluster where the runs namespace cannot reach the
control plane, the setup connectivity probe could never pass, and nothing on the console said why.
Reported by the same enterprise adopter on managed Kubernetes after verifying every 0.6.5 item on
their deployment.
Fixed
- The connectivity probe timed out before an exec-mode run could finish. Every exec is
wrapped bywardyn-rec, which uploads the session cast to the control plane through the
proxy after the task exits. That upload waited up to 60s when the control plane was
unreachable, the probe waited only 50s, so on such a cluster the probe killed its own run at
+50s every time — reported as "did not finish within 50s" under the proxy heading, with the
task long since done. The recorder's upload is now bounded (5s connect, 20s total; delivery
stays non-fatal), the probe's budget is 90s (runner backstop 120s), and a run that started but
never reported back is its owntimed_outverdict, whose detail carries the sandbox agent's
state at the deadline and points atWARDYN_CONTROL_PLANE_URL. The recorder bound ships
inside the agent images: rebuild any image pinned throughWARDYN_AGENT_IMAGESfrom 0.6.6 (or
pull the publishedagent-base:0.6.6) — a pre-0.6.6 image keeps the 60s upload tail, which
the 90s budget covers only for a fast task. - A k8s exec container that never starts no longer hangs the run.
Waitpolled aWaiting
ephemeral container forever; a hard start failure (CreateContainerConfigError,
ErrImagePull, …) now fails the run withexec_started: falsein itsrun.completeevent,
the probe reports it asnot_runnaming the reason, andAgentStatussurfaces the waiting
reason. - The probe run is labelled
base, the image key it actually dispatches, instead of
claude-code; the "Agent image toolchains" setup row names both the claude-code harness
image and the probe'sbaseimage.
Added
warningon a passing probe when its recording never landed. Egress can work while the
proxy pod cannot reach the control plane — every run then completes and silently loses its
session recording. The probe now checks that its own cast arrived and, if not, says so with
theWARDYN_CONTROL_PLANE_URLto check.- Console Ingress read timeout guidance. The probe is one HTTP request of up to 90s;
ingress-nginx's default 60sproxy-read-timeoutturns the verdict into a 504. The chart's
values and README show the annotation to set.
v0.6.5
A patch for the k8s runner on managed, multi-tenant Kubernetes — where a platform team owns RBAC
and namespaces, a baseline default-deny NetworkPolicy already exists, the Postgres DSN Secret is
operator-owned, and packages come from an allowlist mirror. Reported by an enterprise adopter on
managed Kubernetes; every item below was verified against the code before it was fixed.
Security
golang.org/x/crypto0.54.0 → 0.55.0 (GO-2026-6303). The SSH gateway's
ssh.NewServerConnreaches the code path where the source-address critical
option was not enforced for non-public-key auth callbacks;govulncheck
reports it as reachable and would block the merge gate. Fixed upstream in
0.55.0;x/netandx/textmove with it as indirect dependencies.- The chart's NetworkPolicy governs the control-plane pod only. Its
podSelectormatched
app.kubernetes.io/name+instance— the two labels the shared helper puts on every pod the
release creates, and that sandbox pods can carry through caller-supplied labels. Any pod in the
release namespace carrying those two labels inherited the control plane's ingress rules (every non-http inbound port
denied) and, because NetworkPolicy allows are additive, its egress allowances. The selector now
also pinsapp.kubernetes.io/component: control-plane, which the pod template already carried; the
Deployment's own immutablespec.selectoris untouched, so upgrades apply cleanly.
Added
-
k8s.rbac.create(defaulttrue).falserenders no Role/RoleBinding/ClusterRole/
ClusterRoleBinding and nothing ink8s.runsNamespace, for a platform that provisions runner RBAC
out of band and refuses cluster-scoped objects from tenants. TheserviceAccount.create=false
without a name refusal stays either way. -
ingress.*— an optional Ingress for the console'shttpport (class, annotations, hosts,
TLS). Off by default; the render is unchanged until enabled. The UI-sandbox gateway keeps its own
hand-authored Ingress on its own hostname by design. -
secrets.ageKeySecretRef— the age identity from its own Secret, independent of the DSN
Secret, for a DSN a managed-Postgres operator owns and no one can add a key to. Naming it alongside
ageKeyFromSecret/ageKeyis refused at render: two Secrets, one identity, and booting under the
wrong one is unrecoverable. -
defaultPolicy— the default RunPolicy as JSON text (--set-file defaultPolicy=my.json),
rendered into a ConfigMap, mounted read-only, withWARDYN_DEFAULT_POLICYpointed at it and a
checksum annotation that rolls the pod on change. Until now the only chart-level choice was one of
the files baked into the image, whose shipped floor is CC2 — unadvertised on any cluster with no
k8s.runtimeClassespinned. -
WARDYN_K8S_ACK_AMBIENT_DEFAULT_DENY=1— an acknowledgement, distinct from
WARDYN_K8S_ALLOW_UNENFORCED_NETPOL, for the one canary shape a tenant cannot fix: the baseline
phase's pod ran and could not reach the API server because the namespace already carries a
default-deny NetworkPolicy the platform team owns. Boot proceeds, the log says loudly that
enforcement is acknowledged rather than proven, and the setup page shows it as awarnrow. A
canary pod that never started still refuses boot with no override. The refusal message now names
the acknowledgement next to the exemption it already named. -
A
confinement_floorsetup row that warns when the default policy's floor is a class this
runner does not advertise — every run on the default policy would be refused before launch — and
names the two remedies (lower the floor viadefaultPolicy/WARDYN_DEFAULT_POLICY, or pin a
RuntimeClass). -
A
not_runverdict for the setup connectivity probe, distinct fromblocked: the probe
sandbox never started (an image pull, a confinement class this host cannot enforce), so nothing was
learned about the network. The console says so instead of "fix the proxy". -
OIDC public clients.
WARDYN_OIDC_CLIENT_SECRETis optional; without it the token exchange
runs as a public client (client_idin the body, PKCE S256 — which every login already sent). -
GOPROXYbuild arg on every Go builder stage, plumbed throughmakeand Compose like
NPM_REGISTRY; empty is identical to unset. -
The release pipeline can be rehearsed.
release.ymlgains a
workflow_dispatchwithdry_run(default true): it builds every image, the
CLI cross-builds and the chart package, runs the SBOM merge and its zero-npm
assertion, and pushes, signs, attests and uploads nothing.This pipeline could previously only be exercised by tagging, so its bugs were
unobservable until a real tag pushed — which is why 0.6.2 shipped images with no
provenance and 0.6.3 shipped an SBOM that understated its own contents. Both
would have failed a dry run.make release-checkwas green every time, because
it validates the repository, not the workflow.
Fixed
- The setup connectivity probe blamed the proxy for its own failures. It pulled
agent-claude-code— an image the project deliberately stopped publishing in 0.6.2 — and dispatched
at the default policy's CC2 floor, so on a stock managed cluster it failed at the image pull or at
no confinement substrate can enforce class "CC2", and both surfaced as Blocked under the
proxy heading with "fix the proxy above". It now runs the publishedagent-baseimage (override
keybaseinWARDYN_AGENT_IMAGES) at the strongest class the runner actually advertises — the
probe tests egress, not the floor — and a sandbox that never started isnot_run, neverblocked.
agent-base'sagent-runstub honoursWARDYN_TASK_MODE=execso it can carry the probe;
make setupbuildswardyn/agent-base:localon the from-source path and the Compose stack maps
thebasekey to it. - A failed
run.completeread as a clean exit. The probe decoded the failure event's missing
exit_codeas0and reportedreachedfor a run whose watcher had errored. email_verifiedabsent is no longer "false". WithWARDYN_OIDC_EMAIL_DOMAINSset, an
id_token with noemail_verifiedclaim at all — the norm for Entra ID — denied every login with
the message for a claim the IdP had set tofalse. Absent is its ownemail_verified_absent
outcome: the operator gets a server-side warning naming the claim, the issuer and the variable; the
user is told the provider sent no claim and to ask for App Roles instead of "verify your email".
The sign-in copy also named a variable that does not exist (WARDYN_OIDC_ALLOWED_EMAIL_DOMAINS).- The setup barrier picker says it is a browser-local default. Its instruction read as if it
set the server's floor; it never did (the footnote below it already said so). NPM_REGISTRYwas bypassed by the npm self-upgrade. The agent image Dockerfiles ran
npm install -g npm@<version>beforenpm config set registry, so behind a mirror that does not
proxy the public registry the build failed on its first install. The registry is set first.
Not in this patch: an operator-configurable model-provider base URL (an internal OpenAI-compatible
gateway as a first-class provider) — the supported path today is the EgressRedirect header-injection
lane, documented in docs/OPERATIONS.md; a Gateway-API HTTPRoute variant of ingress.*.
v0.6.4
fix(sbom): merge the UI lockfile, and correct what 0.6.2 claimed
The released sbom-wardynd.cdx.json for 0.6.3 carries 1,070 components: 114 Go
modules, 4 Debian packages, and ZERO of the 611 npm packages actually in the
bundle. 0.6.2's notes said switching syft from the source tree to the pushed
image fixed that. It did not, and could not: wardynd embeds the console as a Vite
bundle, which strips every package.json, so there is no manifest in the image for
any scanner to find however you point it. Changing the scan TARGET fixed the
OS-package blindness and nothing else.
The plan I wrote said the fix was to merge a lockfile-derived scan. I implemented
the target change, saw "scans the image now", and did not check the number — the
one number the whole change was about.
The lockfile is the only place those versions still exist, so the release merges
syft dir:ui into the image SBOM. Rehearsed against the actual 0.6.3 artifact:
1,681 components, 611 npm, 114 golang, 4 deb.
The job now FAILS when the merge leaves zero npm packages. An SBOM that
understates what it describes is the failure this whole campaign is about, and it
shipped anyway because nothing asserted the outcome — only that a file existed.
Signed-off-by: cjohnstoniv cjohnstoniv@users.noreply.github.com
v0.6.1
First patch on 0.6. Three CI jobs that had never run before the v0.6.0 push went
red on it; two were real, pre-existing defects. Plus the security sweep and a
documentation regression.
Fixed
- The desktop tier could not boot on older Docker Compose.
deploy/desktop/docker-compose.yamlinclude:d the base compose file and then
re-declaredservices.wardyndto add one volume — a service-name collision that
newer Compose merges and older Compose refuses outright
(services.wardynd conflicts with imported resource). The mount moves into the
base file as${WARDYN_MANAGED_DIR:-…}:/etc/wardyn:ro, the same
variable-with-harmless-default idiom that file already uses for
WARDYN_WORKSPACES_ROOTandWARDYN_BEDROCK_AWS_DIR; the desktop file is now
include:-only, so nothing can collide at any Compose version.
wardyn-desktop.shexports the variable, and every non-desktop deployment
leaves it unset and mounts nothing. make compose-confignow validates the desktop entrypoint too. It only ever
parsed the base file and the CI overlay, and the desktop guard was a text grep
that never asked Compose to resolve theinclude:— which is why the collision
above was invisible to every daemon-free gate and surfaced first in CI.- The agent images' bundled
npmcarried a CRITICAL.node-tar7.5.11
(CVE-2026-59873, gzip-bomb DoS) ships inside npm's own vendorednode_modules,
so no application-level pin reaches it — and the current
node:22-bookworm-slimstill ships it.claude-codeandcodex-clinow
installnpm@11.19.0, which vendors the patched 7.5.19.aws-ssois
debian-based with no npm and is unaffected. - Five development-scope advisories pinned forward via
pnpm.overrides, all
at patch level with no direct-dependency bump:brace-expansion2.1.2/5.0.7,
undici7.29.0,postcss8.5.23,mermaid11.16.1,dompurify3.4.13. These
are build/test tooling and the docs diagram gate — nothing inui/srcimports
any of them, so the shipped bundle is unchanged.make npm-auditis unchanged
and still--prod --audit-level=highby design: development-scope tooling is
outside its scope by construction, not by suppression, and there is no ignore
list.
Changed
- The Quickstart no longer implies a model is required. Connecting a model was
sequenced as step 2 of getting started with no qualifier, while the code has
reported it as optional and explicitly non-blocking since 0.4 (setup status
renders it INFO, never a gap to clear). The model step now follows a working
wardyn run, is introduced as optional, and says outright that skipping it is a
supported end state. The run example now explains that--task-mode execmeans
no agent and no model, and that--agentnames a sandbox image rather than
asserting an AI runs the task. - Wardyn describes itself as a governed-sandbox control plane for any workload
on the surfaces that still said "for coding agents" — the Helm chart's
description and keywords, and the GitHub repository description. Coding agents
remain the flagship use; they were never the whole product, and the console's
own copy already said so. ROADMAP.mdnames the underlying wart:POST /runsrequires anagentfield
even fortask_mode=execruns that have no agent, which is a wire-contract
change to fix.
The recorded walkthrough series is unchanged in this release and remains attached to v0.6.0; the README links point there.
v0.6.0
Added
- Members onboard their own workspaces from the console. The Workspaces
screen and the New-Run wizard's Add-a-workspace dialog now work for a member
session against the member-scoped routes: create and scan your OWN
workspaces, with the admin-set mount boundary shown in place — the daemon
tells the console the member's local-dir root (member_local_dir_rooton
GET /me), and the path field says so. The writable checkbox does not
exist for members (the request never carrieswritable; the server-side
allowlist is the boundary either way). The mock at
docs/design/ui-batch2-mock.mdis the design source of truth for every
string. - Workspace reassignment (offboarding).
POST /workspaces/{id}/reassign
(admin-only) moves a member-owned workspace to operator ownership
(owned_by=''), auditedworkspace.reassignwithfrom_owner. Members get
the same constant 403 as every admin-only workspace route — deliberately
more existence-blind than a 404 split. - No impersonation in the audit trail. When an admin acts on a
member-owned workspace, the audit actor stays the ADMIN's identity, and the
event carriesworkspace_ownernaming the member — cross-user admin access
is queryable (?actor=plusworkspace_owner≠ actor), pinned by a guard
test at every workspace write site. - A failed run says why, in the console. The run page's FAILED state
renders thefailure_hintthe backend has stamped since migration0044—
bare server text beside the state badge, nothing when there is no hint.
The unlisted-host copy in the New-Run wizard now describes what the proxy
actually does (refused-and-raised, approve once, retry gets through), and
the egress panel names the agent CLI's telemetry endpoint for what it is. - Desktop install lane.
deploy/desktop/gainsinstall.sh(managed
dir, per-device age key minted at install — never distributed via MDM),
com.wardyn.daemon.plist(launchd), andwardyn-desktop.sh
(docker compose --env-file … -p wardyn-desktop up -d --no-build --pull always, healthz wait, idempotentwardyn site-config apply).
wardyn_pick_docker_hostnow recognizes a Colima socket the way it does
Rancher's — without it, confinement silently collapses on Colima Macs. - Desktop honesty gates.
scripts/test-desktop-profile.shjoins
make test-scripts(which CI runs), and adesktop-envelopeCI job boots
compose with the example profile and asserts the managed policy file is
exactly what the daemon serves, an unpoliced run resolves to the ceiling,
and profile synthesis stays clamped. The one scripted macOS smoke run is an
operator step documented indocs/DESKTOP.md— run once, paste output; CI
does not cover it and the doc says so. - ROADMAP truth. The desktop slice moves to 0.6; interactive
tool-approvals→console is marked deferred to 0.7 with its reasons (the
prompt-tool contract is non-interactive-only, the hook alternative fails
open on timeout, and a self-service member who could approve can already
attach). - Per-user API tokens. A signed-in human mints
wdn_…bearer tokens for
themselves (POST /me/tokensreturns the plaintext exactly once;GET/
DELETE /me/tokens); an admin can list and revoke anyone's (/tokens).
Only the SHA-256 is stored (migration0045). A token authenticates as
the human who minted it — it publishes the same context the OIDC session
does, so grants, RBAC and ownership bind identically and a member's token
can never reach an admin route. Auditedtoken.create/token.revoke. wardynd -rotate-age-key <path>: age-key rotation as a maintenance
mode. With the daemon stopped, mints a new age identity, re-encrypts every
stored secret in one transaction (any row that fails to decrypt aborts the
whole rotation), swaps the key file atomically and exits. ThewardynCLI
never sees the key. Auditedsecret.rekey(count only, no names).- Hash-chained audit log (migration
0047). Every new event carries
prev_hash/row_hash(SHA-256 over the previous hash and the row's
immutable fields, computed by Postgres under a transaction-scoped advisory
lock so chain order equals commit order). The head hash rides the audit-sink
stream so an external SIEM can detect truncation;GET /audit/chain/verify
(admin) walks the chain and reports the first break. Tamper-evident, not
tamper-proof — a database owner can rewrite the whole chain; the threat
model says so. env_secretgrant kind. Injects a named stored secret as a sandbox
environment variable at dispatch. Resident for the run's lifetime and not
revocable mid-run — its own threat-model row — so it is admin-only unless
WARDYN_ALLOW_MEMBER_ENV_SECRETopens it to members. The grant-pairing
table is now closed: an unknown grant kind is refused instead of falling
through unclamped.git_patper-run lease. Agit_patapproval decided with
decision_scope=runre-mints for the rest of that run under the one
decision; mints are stampedleaseincredential.mint. The scope is
compared as stored — a legacy approval never silently becomes a lease.- Second-human egress approval.
WARDYN_EGRESS_SECOND_HUMAN=1refuses an
egress decision by the run's own creator (authz.denied,
reason: second_human_required). The shared admin token has no per-human
identity and bypasses the rule — that bypass is audited
(approval.second_human.bypass) and documented as break-glass, not hidden. auth.failedaudit event. Admin-token 401s and rejected OIDC session
cookies used to fail silently; they now emit a content-freeauth.failed
(reason, source IP, path) behind a process-local token bucket so a scanner
cannot flood the append-only log.WARDYN_AUDIT_SOURCEstamps a staticsourcefield on every event a
sink serializes — one SIEM index can tell instances apart. Sink payloads
only, never Postgres. OPERATIONS.md gains Splunk HEC / generic-webhook
recipes.WARDYN_REQUIRE_OPERATOR_SET_EGRESS(default off) applies the
scan-seeded provenance guard that already protected secrets to egress
domains: a run may not carry egress the operator never set.- Setup status grades the permissioning posture — the fail-open
enforcement switches are scored as a check, informational and non-blocking. If-MatchonPUT /permissions/enforcementand site-config apply.
Both whole-replace surfaces return anETag; a staleIf-Matchis refused
with 412 before the write reaches the store. Omitting the header keeps
today's behaviour.- SSH admin override is bounded-stale, not permanent (migration
0046).
Every OIDC login re-stampsrole/role_checked_aton the principal's
registered keys; the gateway refuses the override once the stamp is older
thanWARDYN_SSH_ROLE_TTL(default 24h). Keys registered before 0.6 carry
no stamp and never gain the override until their owner logs in again. A
direct-SQL admin-key registration must stamprole_checked_attoo — the
API path already does; a barerole='admin'insert is correctly refused as
never-checked, anddocs/SSH.mdnow says so explicitly. - Session revocation.
POST /sessions/revoke(admin;wardyn sessions revoke --sub … | --all) invalidates every current console session for one
principal or for everyone, effective immediately (migration0049).
Sessions are stateless cookies, so revocation is a per-principal cutoff
time the middleware checks on every request — fail-closed when the store
errors. It also revokes every unrevoked API token the target holds: a
wdn_bearer is that human's session in another form, so "revoke a human
now" covers both in one call. Auditedsession.revoke(with
tokens_revoked); a request presenting a revoked cookie surfaces as
auth.failedwithreason: revoked_session. wardyn support-bundlegathers version, healthz, setup status, a bounded
audit tail and the compose config — secret values redacted, including
commented-out lines — into a tar.gz for a support ticket.- OIDC token exchange retries transient IdP errors (5xx/timeout, at most
three attempts with backoff) and distinguishes them from a configuration
error on the error page. - linux/arm64 images.
wardynd,wardyn-proxyand the agent images build
forlinux/amd64,linux/arm64(pure-Go cross-compile; QEMU only for runtime
stages); every base-image pin is an index digest, enforced by
scripts/test-image-pins.sh. The release lane signs and attaches an SBOM
for the agent images too, the agent CLI is pinned to an exact version, and
CI runs a trivy scan (CRITICAL fails; accepted CVEs live in.trivyignore). - Managed desktop envelope.
deploy/desktop/wardyn.env.example+
docs/DESKTOP.md: a local daemon per laptop under an MDM-managed policy
file, developer = operator. The ceiling is stated verbatim — the developer
is not the adversary in this tier — and the operator-unclamped inline-policy
path is named, not hidden. - Member-owned workspaces (backend, migration
0048). A member may now
create, update, delete and scan their own workspaces (owned_by); a foreign
member's workspace answers the byte-identical 404 a missing one does. A
member'slocal_dirmounts are allowed only under operator/MDM-set roots
(WARDYN_MEMBER_WORKSPACE_ROOTS, or a per-member
WARDYN_MEMBER_WORKSPACE_ROOTS_MAPthat replaces the shared list),
canonicalized at bind time (symlink and..escapes refused,$HOME
dotfiles denied), and writable only underWARDYN_MEMBER_WRITABLE_ROOTS
minusWARDYN_MEMBER_WRITABLE_DENY— both unset means no writable member
mount at all.WARDYN_MEMBER_MODE=1refuses to start alongside local mode.
The console flow and the reassign action follow in the next stage. - **The
wait_for_reviewhold window and concurrency are configurable...
v0.5.0
Security
- Go-live hardening: 29 confirmed ship-blockers closed across two adversarial
review waves, each with a regression test proven to fail on the pre-fix
commit. The load-bearing ones: a member'sinline_policyllm_inspection
block is now clamped under the default ceiling (itsdetector_sidecar_url
could otherwise become an un-allowlisted egress channel), and its secret
corpus is referenced by name and resolved only at dispatch — never stored in
a policy row, written to the append-only audit log, or copied into a
compose/profile proposal; the git-broker's value-returning mint lanes refuse
the GitHub App private key and the other reserved platform secrets; a
disabled integration no longer grants a run model access; an explicit
WARDYN_LOCAL_MODE=trueno longer silently disables a configured OIDC/RBAC
deployment (boot refuses the contradiction); a client-settablerun.Taskcan
no longer forge the operator-reserved harness-login path; SSHssh.auth
success is audited only after signature verification, not at key-offer time;
an empty-ceilinggithub_tokenrepo list is deny-all for a hand-authored
spec; a tokened corp-mirror redirect is dialed to its real port, not always
443; the exec-less (krun/CC3) sandbox path now applies the same fail-closed
resource-cap gate as the exec path; and the host ground-truth sensor no
longer forwards uncorrelated host-wide kernel events to the audit log/SIEM by
default. - OIDC sessions now carry a derived admin/member role (
WARDYN_OIDC_ROLE_MAP,
internal/auth/oidc'sderiveRole). Upgrading forces one SSO re-login: a pre-0.5
session cookie carries no role and now decodes as no session (decodeSession), never
as an authenticated session with an undefined role. - Authorization enforcement: the admin/member role is now enforced, plus
owner-or-admin scoping.requireOperator/isOperatorgate on the session's
role instead of re-checkingWARDYN_OIDC_OPERATOR_EMAILSdirectly (the
allowlist still works — it feeds role derivation viaLegacyAdminEmails, one
source of truth instead of two).GET /metricsnow carries the admin gate
explicitly. A member is scoped to their OWN runs/approvals onGET /runs,
GET /approvals(unscoped), andGET /audit(?run_id=of an owned run,
else an empty result — never a cross-user leak);GET/kill/profile/grantson
a run, the recording replay, an approval decide, and the attach-ticket mint
all use an owner-or-admin gate that answers a foreign resource with the
byte-identical 404 a missing one gets (no existence oracle). Attach tickets
now carry the minting principal's role (migration 0034), since the
interactive-attach WebSocket's?ticket=lane authenticates entirely off the
ticket and never runs the normal session check.GET /setup/statusredacts
operator-diagnostic detail (environment checks, resident CLI detection,
secret names, runner detail) for a member. A member'sinline_policyon
POST /runs(and its preflight dry-run) is now clamped to the operator's
default policy ceiling before resolution, and bringing a custom sandbox
image (image) is admin-only. Two routes move from admin-only to
owner-or-admin: minting an attach ticket and deciding an approval, both
restricted to the run's own creator (or an admin) either way. A new
authz.deniedaudit action records a member's admin-surface or BYOI
denials (not a foreign-resource 404 — that stays silent by design, matching
the no-existence-oracle rule above).
Added
-
An interactive run can start on a seed, at boot. The run's
task—
previously ignored for an interactive run — is now its optional boot seed,
interpreted perinteractive_start: with"agent"the sandbox starts the
agent CLI on that prompt in a persistent tmux session the moment it boots
(supervised: the agent reads and plans, then parks its first tool approval
in the pane until you attach — setseed_auto_toolsto let it use tools
unsupervised before you join); with"shell"the seed runs as a startup
command before the terminal is yours. Attaching joins the live session.
Empty task = today's idle sandbox, unchanged. Server-launched runs
(record/verify/login) are excluded from seeding by construction. The seed
travels as env into the sandbox and is consumed at boot — needs an image
rebuild (make agent-images-core); an older image ignores it and comes
up idle. The CLI gains this with zero new flags:wardyn run --interactive
with a task now seeds. -
Autonomous Claude runs can park every tool action on a human:
tool_approvals: "hold". Instead of--dangerously-skip-permissions,
the run's claude executes under--permission-mode manualwith an
in-sandbox relay (wardyn-toolgate, a stdio MCP permission-prompt tool)
that raises each gated tool use as atool_callapproval — the exact
command or edit as the decision context — and blocks until an operator
approves or denies it in the console (the run cockpit's approval strip now
shows tool holds beside egress holds). Deny and expiry both refuse the
action and the run continues; a relay that cannot reach the control plane
denies rather than proceeds. Default stays"auto"(the sandbox is the
boundary);holdis per-run, Claude-only (codex has no external approval
contract), and read-only commands the harness itself deems safe still run
without asking. -
Egress approvals carry a decision scope:
once,run,until, or
always.POST /approvals/{id}/approveand/denyaccept
decision_scope(plusdecision_expires_atforuntil) on an
egress_domainapproval;wardyn approve/wardyn denygain--scope/
--until, the SDK gainsDecisionOpts(pkg/client), and the console's
approval queue gains a scope picker (Once / This run / Until… / Always)
beside Approve/Deny. Omit the field and nothing changes — the default
staysrun, today's original behavior, held in the proxy's per-host cache
for the rest of the run.oncereleases a single connection (one CONNECT
tunnel on HTTPS; one request on plain HTTP) and is spent on first use;
untilis the same, bounded by adecision_expires_atup to 30 days out
and enforced by the run's own proxy sidecar;alwaysis
operator-only and persists the host onto the target workspace's
approved_egress/denied_egress(migrations
0039_approval_decision_scope.sql,0040_workspace_denied_egress.sql,
0041_run_workspace_ids.sql) so every future run against that workspace
inherits the decision instead of re-raising it — deny beats allow, as
everywhere else in the proxy. NewPUT /workspaces/{id}/denied-egress
(full-replace, mirrorsapproved-egress) is the only way to undo a
permanent deny, including one that broke a workspace's own credential
injection. -
Runs have a name.
POST /api/v1/runsacceptstitleanddescription,
both persisted on the run (migration0038_run_title.sql) and returned by
every read. Runs that share a title are grouped on the Runs board — which
now groups by title rather than by state; the triage the state sections
provided survives as the state facet, attention-first group ordering, and
per-state counts in each group header.wardyn rungains--title/
--description. Both fields are optional on the wire and required in the
console: the site-config probe, harness login and workspace record/verify all
create runs with no human to name them, so a server-side requirement would
break them. Untitled runs — including every run created before this — display
by their task exactly as before. -
An interactive run can open straight into the agent.
interactive_start:"agent"makes the attach shell launch the image's agent CLI
in the prepared workspace, once, on first attach;"shell"(the default) keeps
today's bare terminal. Request-scoped liketask_mode— carried to the sandbox
asWARDYN_INTERACTIVE_STARTand consumed by the image's attach~/.bashrc,
so it covers the console terminal and the SSH gateway alike (both go through
the sameRunner.Attach). Needs an image rebuild —make agent-images-core
— to take effect; until then an older image ignores the variable and degrades
to a shell, which is the previous behavior. -
The Integrations page's Tools tab, the integration→tool "carries"
chips in the workspace wizard (base-image, build, and verify steps), and
the client-side mirrors of the bake conditions. Tools are what the image
carries; the Integrations surface now speaks only to connections. -
The
artifact_mirror/host_proxyderivations. The Integrations surface
no longer synthesizes rows fromEgressRedirects/UpstreamProxySecretRef:
that is network topology, it already has a surface (Corporate network), and
showing it twice made one config look like two. Nothing about how a run
redirects or chains through the corporate proxy changes. -
Kubernetes runner substrate (
internal/runner/k8s,-tags k8s,
WARDYN_RUNNER=k8s): a second, independent confinement substrate behind the
existingsubstrate.Substrateseam — wardynd creates/manages sandboxes as
pods instead of Docker containers. L1 (NetworkPolicy-enforced), not L0
(structural) like Docker: a boot-time two-phase egress canary proves the
cluster's CNI actually enforcesNetworkPolicybefore the substrate will
start at all, refusing to boot otherwise
(WARDYN_K8S_ALLOW_UNENFORCED_NETPOL=1is the loud, logged opt-out). CC1
out of the box;WARDYN_CONFINEMENT_MAP/the chart'sk8s.runtimeClasses
pin CC2/CC3 to a registered RuntimeClass. Not at parity with Docker yet —
no BYOI/devcontainer builds, nolocal_dirmounts, no per-pod PIDs/disk
enforcement, no k8s ground-truth correlator (see
deploy/helm/wardyn/README.md/docs/OPERATIONS.md's "Known gaps"). -
**The Helm chart ...
v0.4.3 — clarity release
A clarity release: no new features, no API changes. A repo-wide audit rewrote the
docs around a single quickstart, redrew the architecture diagram, and fixed the
places where the docs described something the code does not do. It also found two
CI gates that had been passing without checking anything.
Operators upgrading: nothing to do — no schema, config, or interface changed.
Maintainers: the CI merge gate collapsed five single-command jobs into one
gates matrix, so the required status checks on main must be renamed from
govulncheck/staticcheck/gitleaks/licenses/license-headers to
gates (<name>). Until that is applied, a pull request waits forever on contexts
that no longer report. See RELEASING.md, "Repo settings".
Fixed
- The nightly live e2e jobs were passing while their assertions failed. Both
steps pipemakeintoteeto capture evidence, but GitHub's implicit shell has
nopipefail, so the step tooktee's exit code. These are the jobs that prove
the L0 egress boundary, the metadata block, and the kill cascade. make dcoaccepted any non-empty sign-off, includingSigned-off-by: nobody—
the format assertion was lost when the check moved to git's trailer parsing.- The README claimed gVisor/CC2 is your default barrier.
pick_policychecks for
a configured model first, so a gVisor host running a model gets a CC1 floor. All
three outcomes are now stated. docs/ENV.mddocumented an admin-token default that does not exist, so a
reader who set onlyWARDYN_TOKENgot an unauthenticated control plane: the real
default is empty, and an empty token with no OIDC on loopback auto-enables no-auth
local mode. Both the binary and compose defaults are now stated.- The compose README described the
make setupmenu backwards, and told readers
team/SSO was "coming soon" where the roadmap says it is not scheduled. - Example scenario commands now run as written (
--policy,wardyn run list,
wardyn deny, and a policy file that actually parses). - The Helm docs quoted three different, all-wrong chart versions.
- Three API payloads emitted
nullwhere they had emitted[], one of them a live
response body.
Changed
-
The console's default sizing is back to 100%. 0.4.0's 17.6px root font token is
16px again, the sizing that shipped through 0.3.1. Thepx→remtext-utility
conversion stays — those values were computed against a 16px base, so each reproduces
its original size — and--font-sizeinui/src/styles/theme.cssremains the single
knob for anyone who wants the larger console back. -
Docs consolidated and de-duplicated (repo-wide audit). The quickstart is told
once (README → docs/TRY-IT.md);docs/FRESH-START.md,
docs/ADO-GIT-BROKER.mdanddocs/TEST-GAPS.mdare gone (the troubleshooting
table moved into TRY-IT); pre-0.4 release notes live in
CHANGELOG-ARCHIVE.md; new docs/README.md index. -
Diagrams redrawn and gated. The system-overview diagram has no edge
crossings and is inlined in the README (the stalearchitecture.pngis gone);
make diagramsnow label-checks the diagram side too and enforces a style guide. -
Doc accuracy fixes. The default-barrier claim now states the CC1 fallback on
runsc-less hosts; ENV.md documents the real (empty) admin-token default and the
no-auth local-mode consequence; the compose README's setup-menu note was inverted;
example TASK.md commands run as written. -
Build plumbing.
make release-checkis now a strict superset ofmake ci;
make helpis self-documenting; the nightly uploads real e2e evidence; screenshots
are freshness-gated and the README hero shot is script-captured.
v0.4.2 — corporate-network fixes: a hang, a regression, two false claims (pre-alpha)
A follow-up to 0.4.1 from the same adopter, now running the containerized stack on a
corporate laptop end to end. The full report — including the gaps still open — is in
docs/adoption/. One entry below is a
regression 0.4.1 introduced; two more were bugs that had been silent for longer.
Fixed
-
An unreachable corporate proxy no longer hangs an approved request. When the
configured upstream proxy accepted the TCP connection but never answered theCONNECT,
wardyn-proxy waited forever: the handshake read had no deadline, and the MITM path had
already told the agent200 Connection Established. An operator saw an approved egress
sit there with nothing to act on. The handshake is now bounded and a stalled upstream
surfaces as a normal dial failure — deny + 502, logged. This is the reported "200 then
hang" on hosts whose connectivity client binds its proxy to loopback only. -
The
make setupUI fallback no longer reinstalls. 0.4.1 taughtmake setupto build
the UI on the host when the registry can't serve pnpm — but it retried viamake ui, which
reinstalls, and the reinstall's postinstall scripts fetch platform-specific native binaries
(@tailwindcss/oxide-*,@esbuild/*) that a partial mirror also refuses. So the fallback
died anyway, withnode_modulesalready complete. It now rebuilds from an existing
node_moduleswhen the lockfile matches, and only reinstalls when the tree is absent or
stale. Note the ceiling: a fresh clone has nonode_modules, so this fixes the second and
latermake setup, not a cold start. -
The New Run wizard no longer claims you have no model access when you do. With an
operator-configured cloud model (Bedrock), the Access step still defaulted to the api-key
option and warned in red that the run's first model call would 404 — while dispatch was
going to supply model access automatically, and the secret list it pointed at held no model
key. The preflight now asks the same resolver dispatch uses, and Review defers to that
verdict instead of guessing. When the preflight is unavailable the old local check still
applies, so a real gap is never hidden.
Added
-
wardyn setup proxy-relay <listen-port> <proxy-port>. Forwards a reachable port to a
forward proxy bound to127.0.0.1, which no container can reach — a sandbox's loopback is
its own, and on a VM-backed Docker host the runtime VM can't reach the host's loopback
either. Foreground and unsupervised on purpose (Wardyn owns no host daemons); it fails
immediately if nothing is listening on the target port. See
docs/adoption/loopback-only-forward-proxy.md. -
The Host proxy step warns when a detected proxy is loopback-bound, naming the symptom
and the fix, instead of leaving it to be discovered at the first launch. Shows up in
wardyn setup statustoo. -
wardyn site-config get|apply. The corporate baseline (upstream-proxy ref, artifact
mirrors, SCM hosts) lives in Postgres, somake resetandmake reset-alltake it with the
volume — a reset previously came back up looking healthy with egress silently unconfigured.
The baseline is now a file you can keep; it carries secret names, never values.reset-all
also names what it is about to destroy and prints the capture command first.
Changed
-
Writable workspaces name the VM-backed-host caveat. An adopter reported that a
writable: truehost mount is read-only in practice under the Wall tier on macOS. Testing
on a native Linux host with all four runtimes did not reproduce it — runc, gVisor, Kata
and libkrun all wrote successfully — so the cause appears to be the macOS→VM file-sharing
layer beneath the runtime, not the barrier itself. The Workspaces banner now says exactly
that, scoped to VM-backed runtimes, rather than claiming a tier "cannot write" (which would
be a new false statement of the kind these reports keep surfacing). The measured
tier↔writability matrix is in the adoption doc. -
The agent registers its workspace as a git
safe.directory. A bind-mounted checkout is
owned by the host user, whose uid rarely matches the sandbox agent's, so git refused every
command with "detected dubious ownership" — which reads like a Wardyn defect rather than a
uid mismatch.
v0.4.1 — corporate-network onboarding fixes (pre-alpha)
A corporate-network onboarding fix release, from two adopter field reports (kept in
docs/adoption/). Both are onboarding-trust bugs on the containerized
default path: one hard-blocked make setup, the other made the Getting-Started
checklist assert something it never actually checked. No interface changes.
Fixed
-
make setupno longer dies on a registry that can't servepnpm. Behind a
corporate allowlist mirror that hasn't onboarded pnpm, the image's default
ui-buildstage failed with a rawnpm error code E404(or 403) and the stack
never came up — the one command the docs tell a new user to run did not work out of
the box.scripts/up.shnow catches a failed build and retries with the UI built on
your host (make ui+WARDYN_UI_STAGE=ui-prebuilt), explaining each step. The
escape hatch was already there; it just required knowing an env var the failure
never named. An explicitWARDYN_UI_STAGEis still honored and disables the
fallback, and the OSS path with a working registry is unchanged — the retry branch
is never entered. Chose retry over probing the registry first: the 404 is on the
tarball path, so a mirror that proxies metadata answers a probe with 200 and the
build dies anyway. -
Staging a corporate CA no longer breaks the image build. The
ui-buildstage
ranupdate-ca-certificates, but itsnode:*-bookworm-slimbase purges the
ca-certificatespackage in its own build, so the binary is absent — meaning any
operator who followedmake doctor's own advice and stageddeploy/images/corp-ca.pem
hit a hardexit 127, every time, before the pnpm step even ran. That stage now
relies onNODE_EXTRA_CA_CERTSalone (npm/pnpm are its only TLS clients, and they
read it).deploy/images/README.md's snippet — which is what produced the bug —
was corrected so it stops reproducing it. -
The Host proxy step tells the truth on the containerized stack. It reported
"No host-side proxy configuration detected" on hosts unambiguously behind a
corporate proxy, in the same session whoseHTTP_PROXYWardyn's own image build had
just consumed.DetectHostProxy()runs in the wardynd process, and in a distroless
container every tier is structurally blind: the env tier sees only the container's
env (compose forwarded the proxy as a build arg only),HOMEis unset so no
shell profile or tool config is reachable, there is nogitbinary, and the OS/PAC
tier dispatches on the process'sGOOS.make setupnow runs the same detector
on the host (newwardyn setup detect-proxy, from a host-native binary the image
cross-compiles) and seeds the result in, recovering the macOSscutil/PAC,
Windows-registry, shell-profile, git and tool-config tiers that are inherently
host-side. It re-runs on everyup, so it cannot go stale across a network change.
Deliberately not done by forwardingHTTP_PROXYinto wardynd's runtime
environment: Go'snet/httphonors those names process-wide, which would silently
reroute wardynd's own OIDC discovery, audit webhooks, GitHub App minting and AWS
credential chain through the corporate proxy — a live-traffic change to fix a
diagnostic. A run's egress is unaffected either way (the sandbox env is built from
scratch and always points at wardyn-proxy). -
An honest empty result when detection genuinely can't look. When wardynd is
containerized and no host-side reading was seeded, the step no longer asserts a
false negative: it says detection ran inside the container, names what it therefore
could not read, and carries a next step. The step's static lede ("Wardyn detected
these host proxy settings…"), which rendered unconditionally above an empty
result, is gone. Same honesty rule the Vault/KVM copy already followed. -
A set-but-empty
HTTP_PROXYno longer counts as a detected proxy.os.LookupEnv
reports ok forexport HTTP_PROXY=, and every consumer downstream tested presence
only — so an empty value rendered a blank-valued "detected" row. Empty and
whitespace-only values are now filtered, in the one place all tiers route through.
v0.4.0 — containerized setup, container workspaces, Bedrock SSO (pre-alpha)
Wardyn remains pre-alpha: interfaces are not stable and this release changes
several defaults. Read "Upgrading from 0.3.1" below before pulling it onto a
host that was running 0.3.1.
Added
-
The console's default sizing now matches 110% browser zoom. The root font
token moved from 16px to 17.6px and the hardcodedpxtext utilities were
converted torem, so text and spacing scale together rather than the small
labels being left behind. One token (--font-sizeinui/src/styles/theme.css)
rescales the whole console. -
Container as an execution environment. A workspace can now be a container
image (a newcontainerkind,Source= image ref, no mount) alongside a local
directory or repository, and any workspace/container can carry an operator-owned
model/harness credential binding (none|managed|api_key|bedrock— secret
names/refs only, never secret values). A run inherits the model access of the
workspace/container it picks; the binding is folded into the run policy at create
and injected proxy-side, so the sandbox never holds the credential. Onboard and
edit bindings from the Workspaces screen orPUT /workspaces/{id}/llm-cred
(migration0024_workspace_llm_cred). -
YAML policies.
wardyn run --policy-fileandwardyn policy create|update -f
accept JSON or YAML;wardyn policy render -f <file>(or-f -) converts either
to canonical JSON and strictly validates it without touching the control plane.
Commented examples ship inexamples/policies/:sandbox.yaml,
sandbox-claude.yaml, andsandbox-workspace.yaml(a governed agent on a real
local directory — note the mount source must be an onboardedlocal_dir
workspace, andread_onlydefaults totrue, so edits need it setfalse). -
wardyn subscription connect|status|disconnect. Capture aclaude setup-token
from stdin only (never argv, never.env), stored age-encrypted and injected
proxy-side into eligible runs — the sandbox holds only an inert sentinel. Connect
is idempotent (skip-if-live,--reconnectto replace). The reserved secret name
wardyn-harness-anthropic-oauthis blocked from the generic secrets API so this
is the only supported path.WARDYN_SUBSCRIPTION_TOKENseeds it headlessly through
scripts/up.sh/scripts/ci-run.sh. SDK:ConnectManagedSubscription/
DisconnectManagedSubscription. The setup-token is long-lived (~1 year),
age-encrypted and non-rotating — documented, not hidden. -
wardyn setup status: the console's readiness checklist in the terminal
(same/setup/statussource), each unmet check naming the exact next command. -
AI Run Composer on the container path. A composer backend that runs the real
claudeinside a governed one-shot sandbox with the managed subscription injected
proxy-side (never resident), mirroring the governed scan-run pattern — brokered
/internal/compose-results/{runID},WARDYN_COMPOSE_ONLYagent-run mode,
cross-run guard, fail-closed when no subscription is connected, reclaim-on-timeout. -
Containerized AWS SSO login for Bedrock. A fourth Bedrock credential path for
SSO-only orgs: the operator authorizes a device code in any browser and Bedrock
runs get short-lived role credentials — no hostaws sso login, no~/.awsmount.
deploy/images/aws-ssois a dedicated login image (AWS CLI v2, GPG-verified) so
~600 MB of CLI stays out of normal runs;cmd/wardyn-aws-ssouploads the SSO cache
through the brokered internal endpoint;runs_bedrockmaterializes a minimal
synthetic~/.aws(sso-session + hashed cache file). The pane collects your org
portal URL and the server seeds a credential-free[sso-session wardyn]via
WARDYN_AWS_SSO_CONFIG_B64; Wardyn persists no copy. Honest bound: the SSO
token and the derived role credentials are resident in the sandbox (the UI says
so with an amber chip), and Wardyn cannot revoke a captured SSO session. Validated
against a fake sso-oidc/portal built from the real botocore service models, not
against a live IAM Identity Center. -
Standard AWS environment is honored.
WARDYN_BEDROCK_REGION/
WARDYN_BEDROCK_AWS_PROFILEfall back toAWS_REGION/AWS_DEFAULT_REGION/
AWS_PROFILE; the Wardyn-specific names still win. A region alone cannot enable
Bedrock (that also needs a model), so this cannot switch the transport on by surprise. -
Corporate-network image builds. Corp-CA staging (
corp-ca.pem, documented in
deploy/images/README.md) andNPM_REGISTRY/HTTP_PROXY/HTTPS_PROXY/
NO_PROXYare threaded into everydocker buildand through composebuild.args;
Dockerfile.wardyndgains aUI_STAGEselector (ui-builddefault,
ui-prebuiltviaWARDYN_UI_STAGE) so a mirror that cannot serve pnpm consumes a
host-builtui/dist.up.shbuilds agent images one at a time and continues past a
blocked image with a summary instead of aborting the bring-up. corepack does not
work around a mirror missing pnpm (it fetches from the same 404ing registry path)
— hence the prebuilt stage. -
Opt-in native agent-CLI install for corp networks (npm stays the default):
CLAUDE_INSTALL=nativeinstalls the nativeclaudebinary, checksum-verified
against the release manifest fromdownloads.claude.ai(the only host it contacts —
allowlist it) or from a host-staged binary for fully offline builds;
CLAUDE_CODE_VERSIONpins it.CODEX_INSTALL=nativeis staged-only and fails
loudly if no binary is staged, because codex has no Wardyn-verified public download
contract.scripts/stage-agent-binary.shdoes the host-side staging. -
Shared-host concurrency for the compose control plane. Every explicitly named
compose object is parameterized offWARDYN_NS(defaultwardyn, so the
single-user default is byte-for-byte unchanged): container names, the internal
network (WARDYN_INTERNAL_NETWORK, now threaded intowardyndconfig), and the
recordings volume.WARDYN_PG_PORTparameterizes the Postgres host port.
ci-run.shtakes a per-jobCOMPOSE_PROJECT_NAME+WARDYN_NS(unique per
invocation, pinnable viaWARDYN_CI_PROJECT), ephemeral host ports, a
container-health wait, and a project-scopeddown --volumesthat can no longer wipe
another job's stack.make test-e2e-concurrent(scripts/test-concurrent.sh) is the
live two-job acceptance test and passes. Boundary: this is safe for one trusted
operator (e.g. a CI fleet under one service account), not for mutually distrusting
tenants. -
Fail-closed resource caps. On a host where the daemon silently ignores
NanoCPUs/Memory/PidsLimit(cgroup v1, or rootless without controller
delegation), the sandbox ran effectively uncapped and nothing detected it. The
daemon'sContainerCreateresponse warnings are now authoritative: any
"…Limitation discarded" warning refuses the run and rolls the sandbox back.
WARDYN_ALLOW_UNENFORCEABLE_CAPS=1overrides on a trusted host (downgrades to a
loud warning);wardyn doctorsurfaces the signal pre-boot as an advisory hint. -
Run type in the New Run wizard. The Basics step offers "Agent run" vs
"Governed command" (a plain shell command,task_mode=exec, no agent, no LLM
credentials), and bring-your-own base image is promoted out of Advanced into a
first-class field. Picking a container workspace attaches it as the run's base
image (it ridesrun.image, so the backend resolves it back by image ref to inherit
its bound credentials instead of 422ing at the onboarded-mount gate). -
Harness-aware demo. A fifth demo ("The agent in the box") appears on
/demos
once a model/harness provider is connected, scoped to Anthropic egress. Like every
demo it comes up idle and you runclaudeyourself in the attached terminal. The
four keyless egress-boundary demos stay LLM-free and remain the frozen set inside
Getting Started. -
Enterprise onboarding and a corp-aware
doctor. The wizard now offers the
Bedrock credentials the backend already read but never exposed — the preferred
never-resident bearer (bedrock-api-key) and the SSO session token
(aws-session-token), ordered byresolveBedrockAuthprecedence.up.shwires
operator-provided Bedrock config (region/model,~/.awsbind with the uid-1000 ACL
note) intodeploy/compose/.envon the container path. Rancher Desktop is detected
by docker context and its non-bind-mountable~/.rd/docker.sockis remapped to the
in-VM socket, andwardyn setup wallgains a Rancher branch (rdctl shell) instead
of telling a Rancher user to install Colima.doctorwarns when a forward proxy is
set with no corp CA staged (before a three-minute build dies on x509) and asserts the
chosen docker socket is actually bind-mountable, not merely reachable. -
Rootless and Podman: documented and probed. THREAT-MODEL now states the
supported rootless model — rootless Docker/Podman is supported at CC1 only; CC2
and CC3 are refused fail-closed (gVisor needs--TESTONLY-unsafe-nonroot, Kata needs
device passthrough) — and reframes the upstream corp-proxy hop as a supported,
operator-configured egress lane with its bounds stated (it does not make private IPs
reachable; only the resolved-IP re-check is deferred to that proxy). It also answers
the JVM/Deno trust-store question: MITM is not mandatory for any egress class, so
only a client pointed at an explicitly MITM'd host is affected.scripts/test-podman.sh
probes the exact primitives the runner depends on and passes against a live
rootless Podman 4.9.3 (internal bridge blocks off-host egress, runtimes map
readable, host-gateway resolves, caps verified by reading the container's cgroup v2
cpu.max/memory.max/pids.max). -
docs/ADO-GIT-BROKER.md: a reviewed design only for never-resident
proxy-side Azure DevOps git egress. The working l...