v0.4.0 — containerized setup, container workspaces, Bedrock SSO (pre-alpha)
Pre-releaseWardyn 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 lane today is still the resident
git_pat. The named ceiling: Azure DevOps has no token-minting API, so the operator
PAT's scope is the boundary — never-resident yes, per-repo auto-expiring scoping no.
Not implemented, because the switch touches the credential path and needs a real ADO
PAT and repo to validate.
Changed
make setupis containerized by default. Host mode is now an advanced escape
hatch (WARDYN_SETUP_MODE=local);WARDYN_SETUP_MODE=teamprints a notice and exits.- First-run setup is a mandatory gate. The welcome screen is a single
"Get started" call to action into Getting Started — the "try a 2-minute demo sandbox"
side door and the "skip for now" / "finish later" escapes are gone. While the gate is
active every route except/setupand/demosredirects to/setup, the
Operate/Configure/Forensics nav groups are hidden, and the top-bar New-run action is
hidden. The gate keys on the console being new, not on backend readiness (see
Fixed), and team/SSO deployments are never gated. - The model/harness provider is optional. Only the sandbox barrier is a hard
requirement: thellm_providersetup check is INFO rather than a warning, the step
carries a neutral "Optional" chip with an explicit "Skip — run without a model"
control, and the Review/Launch rollup keys off the barrier alone. The AI Composer and
agent runs need a provider; a governed command (task_mode=exec), a
bring-your-own container, and an interactive run you drive yourself do not. - The model step asks about the harness first. Choosing "container login" no
longer silently commits you to a Claude subscription: you pick the agent harness,
then that harness's credential path, each named for the credential rather than the
mechanism ("Set up Claude subscription" / "Set up Anthropic API key" / "Set up AWS
Bedrock", the last expanding its four modes inline). Each path keeps its posture chip
— green proxy-injected, amber resident. - The setup funnel is ordered by prerequisite, not by theme. Host Proxy and
Artifact Redirect move out of a collapsible "Corporate network" section that sat
after the demos and into Essentials ahead of the model step (connecting a model
needs egress, and so do the demos, so meeting an unconfigured proxy later made Wardyn
look broken rather than unconfigured). Credentials now precedes Workspaces, because
onboarding a private repo needs the git credential to clone. The fast-path "You're
ready — launch your first run now" banner is removed; it duplicated the Launch step
and talked over whatever step was being configured. - Platform-first framing. Wardyn is presented as a governed-sandbox platform for
any workload — scripts, builds, tools, and coding agents — with AI agents as the
flagship use rather than the definition (hero, IntroBlurb, setup tagline, runs copy,
glossary, README). - The AI Run Composer is marked Beta (a chip on the "Describe your task" entry
card and on the "AI Run Composer" review eyebrow). "Configure manually" is unmarked. - The composer's "Proposed setup" review leads with what blocks you. The model's
rationale and advisorymodel_notescollapse behind "Why this setup" disclosures;
real blockers, the deterministic clamp notices, and the risk gate stay primary. - Setup persists the chosen Docker socket.
wardyn_pick_docker_hostwrote
WARDYN_DOCKER_SOCKinto the environment only, so anydocker compose up -d wardynd
run outsideup.shfell back to compose's/var/run/docker.sockdefault. On a
dual-daemon box that daemon has norunsc/kata, so the barrier silently collapsed
from three tiers to Fence-only. The value is now written intodeploy/compose/.env,
so every later compose invocation from any shell drives the same daemon. - Operator-supplied egress domains are validated server-side. A mid-label wildcard
likeoidc.*.amazonaws.comcompiles to an exact hostname no request can equal — the
matcher supports a leading*.suffix or an exact host, nothing else. Until now
the API, inline policies andWARDYN_DEFAULT_POLICYall accepted such an entry (only
the UI checked, client-side), so a policy could carry a silently dead rule. One
predicate,ValidDomainEntry, now runs at thevalidatePolicySpecchokepoint every
operator ingest point routes through. Entries with a port or path attached
(*.example.com:0,*.example.com/path) are rejected too. This can reject a
policy that 0.3.1 accepted — but only entries that never matched anything. - A half-specified per-workspace Bedrock override is rejected at write time. A
Bedrock model id is a region-scoped inference profile, so a region without a model
(or the reverse) fails at invoke. Omitting the block entirely remains the
inherit-everything case.validateWorkspaceLLMCredalso rejects anapi_key_secret
naming a sink-reserved secret. - The resident-secret disclosure is corrected. The threat model claimed secrets
reach the sandbox in "two named, bounded exceptions" and said so in three places that
did not agree on which two. Reading the code, there are eight — including the
AWS SSO token this release adds, which the threat model did not mention at all.
Section 5.1a is now the single authoritative enumeration: what lands, why it cannot
be proxy-injected, what bounds it, and where no bound exists (Wardyn cannot revoke a
captured SSO session, and verbatim-only masking does not match the base64 copy carried
in the env var). The other sites point at it instead of restating a count. - The compose banner's "production path is Kubernetes" claim is corrected: the
Kubernetes data plane is v0.5-planned and cannot create sandboxes yet. - CLI list output prints ids in full.
run listprinted 8-character ids and
run kill <that id>then rejected them ("invalid UUID length: 8"); the same mismatch
brokeapprovals list→approve/denyandpolicy list→run --policy.
The ID column of runs/approvals/policies is no longer truncated; context-only columns
(an approval's RUN column, an audit target) still are. - Personal paths and usernames are scrubbed from the tree (the example policy no longer
pins the author's home directory,docs/FRESH-START.mdno longer names a
pre-rename checkout, local-principal fixtures usealice). No behavior change. - Repo gates are back in truth with the tree: the diagram manifest re-points at
runs_lifecycle.go/runs_dispatch.goafter theruns.gosplit,workspaces.tsx
is split on two single-concern seams (1226 → 609 + 437 + 217 lines) instead of being
allowlisted, and the image-pin gate resolves a bare${VAR}against the Dockerfile's
ownARGdefault before its alias check (soFROM ${UI_STAGE}is no longer a false
positive, and a${VAR}that resolves to a real registry image still needs a digest).
Fixed
- A workspace's model credential binding never reached the run's persisted grants —
and the operator's Claude subscription was billed for it.persistRunGrants
snapshotted the run spec's grants, proxy injections and SCM egress;applyWorkspaceCreds
mutated that same by-value spec fifty-two lines later and nothing re-read it. So a
workspace bound to its ownapi_keycontributed no grant at all and the run fell
through to the control-plane managed subscription — the operator paid for a run the
workspace had explicitly bound to its own key.managedandbedrockbindings could
not displace a competing api-key grant either, so the api key won over the transport
the operator chose. The binding was stored, rendered in the UI and audited; it simply
never took effect. Credential resolution now runs before grants are persisted, and
the tests drivePOST /api/v1/runsand assert on the persisted grants — the only
formulation that could have caught it. One visible consequence: when a workspace's
approved egress already listedapi.anthropic.com, that host is now contributed by the
credential binding, so it no longer appears in therun.workspace.egressaudit's
added_domains. The domain sets dedupe, so the effective policy is unchanged. - Per-workspace Bedrock bindings are actually applied. Until now a workspace's
Bedrock region/model/profile was stored and displayed while dispatch explicitly ignored
it (the UI said so). Region, model and profile are threaded through
resolveBedrockAuthwith the global config as fallback, the region's
bedrock-runtime/bedrockhosts are unioned into the run's egress, the SSO region
falls back to the effective region (not the global one, which would hand a
relocated run the wrong oidc/portal endpoints), and therun.llm.bedrockaudit names
the effective region. Not claimed, because it needs live Bedrock: that a cross-region
inference-profile id resolves in the overridden region, and the bearer-mode exchange
against a realbedrock-runtimeendpoint. - The AWS SSO login pre-allowed three hosts the proxy could never match.
oidc.*.amazonaws.com,portal.sso.*.amazonaws.comanddevice.sso.*.amazonaws.com
are mid-label wildcards, whichclassifyDomaincompiles to literal hostnames matching
nothing — so the login failed on the very hosts it claimed to allow, stored an empty
account/role, and every downstream Bedrock run inheritedsso_account_id =. The
regional hosts are now derived at launch fromBedrockAWSSSORegion(falling back to
BedrockRegion) and recorded in theharness.login.startedaudit. With no region
configured nothing regional is pre-allowed and the two hosts surface as first-use
approvals — deliberately not falling back to*.amazonaws.com, which would hand the
sandbox S3, EC2 and STS to fix a login. Net egress is strictly narrower than what
0.3.1 shipped. - The AWS SSO login sandbox had no
~/.awsat all, while the setup pane auto-typed
aws sso login --no-browser --use-device-code— a command that needs an
sso_start_urlandsso_regionto already exist. No start-URL configuration existed
anywhere (not a flag, not the Config struct, not the UI), so the sandbox was launched
structurally unable to complete its one job, and the only way through was to run
aws configure ssoby hand in the attach terminal, which nothing documented. The pane
now collects the org portal URL and the server seeds an all-or-nothing session block.
The request is refused (400) when the start URL is missing, is not https, or contains
whitespace (a newline would smuggle extra keys into the generated INI), or when no SSO
region is configured — naming the flag to set. - The
aws-ssoimage was in the default agent-image map and offered by the setup UI,
but neither setup path built it, so first use failed at pull against a:localtag
that exists in no registry. Both build loops now build it. - The first-run gate could lock an existing console out on a transient daemon blip.
The gate keyed on!has_runs || !readyand the setup orchestrator does not cache probe
errors, so a momentary Docker hiccup pulled an operator with existing runs out of the
entire console — every route redirecting to/setup, all nav hidden, the only escape on
the funnel's last step. (0.3.1's claim that returning consoles are never gated was not
true.) The gate now keys on the console being new; readiness is left to the soft
auto-open. - The managed Claude subscription never actually worked for runs.
detect_anthropic_modelooked for credentials only under~/.claude, but a managed
subscription materializes its inert sentinel intoCLAUDE_CONFIG_DIR(dispatch points
there because the resident path may mount~/.clauderead-only). The run fell through
to "apikey" mode, which keepsANTHROPIC_BASE_URLon the x-api-key inject gateway while
the proxy injects an OAuth Bearer — surfacing as "401 Invalid bearer token".
CLAUDE_CONFIG_DIRis checked first now, so the run detects "subscription" and tunnels
toapi.anthropic.comfor the proxy swap. - The composer's model-access verdict was gated on the wrong toggle. With a connected
setup-token and the per-run "use subscription" opt-in off, the Review checklist announced
"no model access — this run will do nothing" about a run dispatch would happily
credential. That toggle gates the credential mount path; a managed subscription needs
no mount. Mount gating and verdict are now separate. examples/policies/sandbox-claude.yaml'sgithub_tokengrant hadrepos: [], which
the git-broker rejects ("github token requires at least one repo").- The resource-cap gate ran after
ContainerStart, and its pre-flight probe
false-positived on Podman. An untrusted agent container on an uncapped host was
therefore started and executed for the duration of the check before rollback tore it
down — a host that cannot enforce caps must not launch the container at all, and
started-then-killed does not satisfy that; the gate now sits between create and start.
Separately the probe trusteddocker info'sMemoryLimit/PidsLimit/CPUCfsQuota
booleans, which rootless Podman 4.9.3 under-reports (CpuCfsQuota=falseeven though
the quota binds, verified by reading the container's cgroup) — so Wardyn refused runs
whose caps do enforce. The authoritative post-create warning replaces it; the
docker infobooleans are now only an advisorydoctorhint, with the Podman caveat
spelled out. - The New Run wizard silently dropped bring-your-own-image and
task_mode. Neither
was ever forwarded onto the wire, so BYOI was lost end to end from the UI. - The subscription login URL arrived truncated ("Invalid response_type: missing"):
claude setup-tokenhard-wraps its OAuth URL at the PTY width, cutting both the scraped
link and the fallback link. The login PTY is now forced to 512 columns so it does not
wrap at the source. - The composer review printed two sentences twice: the model-access line (the
setup-checklist row's Detail isllm_access.noteverbatim, and a standalone success
line repeated it — the standalone line now shows only when there is no checklist row to
carry it), and, on the launch-blocked path, the top risk rationale (shown both inline
next to the badge and as the first entry of the "High-risk configuration" list — the
inline copy now shows only when there is no list). - Docker-tagged AWS SSO tests no longer burn a 30-second timeout each against a
:local
image nothing builds; they skip when the image is absent.
Security
- The dex host port was published on
0.0.0.0. It is now loopback-only and
parameterized (WARDYN_DEX_PORT,0for an ephemeral port) so concurrent
sso-profile stacks do not collide; dex only runs under thessocompose profile. - Server-side egress-domain validation (see Changed) closes a defect class where an
operator-authored allowlist entry could be silently dead. Adversarial review caught the
first version of the predicate failing open in exactly the way it was written to
prevent — only the exact-host branch was guarded — so both branches are checked; removing
either guard fails five pinned cases. pkg/client'sHarnessLoginmethod is deleted. It had zero callers, andclient.go
anddocs/sdk.mdboth already listed harness-login as not covered by the SDK — the
method contradicted its own documentation.
Upgrading from 0.3.1
make setupnow brings up the containerized stack. If you were running host mode,
passWARDYN_SETUP_MODE=localexplicitly.- Re-run setup on an existing compose deployment (or hand-edit
deploy/compose/.env):WARDYN_DOCKER_SOCKis now persisted there. If you have more
than one Docker daemon and you skip this, a plaindocker compose up -d wardynddrives
compose's default socket and the barrier silently collapses to Fence-only. - A host that cannot enforce resource caps will now refuse to launch runs. cgroup v1,
or rootless without controller delegation, previously ran uncapped and silent; you will
now seeresource caps not enforceable on this host. Fix the host, or set
WARDYN_ALLOW_UNENFORCEABLE_CAPS=1if you trust the workload — that returns 0.3.1's
behavior, uncapped. - A fresh console cannot be skipped past Getting Started. Automation that drove a new
local-mode console straight to/runsmust complete setup first, or seed the completion
flag. Team/SSO deployments and consoles that already have runs are unaffected. - Check your policies for mid-label wildcards. An
AllowedDomainsentry like
oidc.*.amazonaws.com, or one carrying a port or path, is now rejected at write time by
the API, inline--policy-filepolicies andWARDYN_DEFAULT_POLICY. Such an entry never
matched any request, so rewriting it as a leading*.suffix or an exact host changes
what your policy does, not just whether it saves. - Check any per-workspace Bedrock binding. A half-specified override (region without
model, or model without region) is now rejected on write, and — unlike 0.3.1 — a complete
one is actually applied at dispatch, so a workspace that has carried a stale
region/model since 0.3.1 will start moving its runs there. Omit the block to inherit the
server's global Bedrock config. - Verify which runs your workspace credential bindings bill. Before this release an
api_keybinding was ignored and those runs were billed to the control-plane managed
subscription; after it they use the workspace's key. - Rebuild your agent images (
make agent-images-coreorscripts/up.sh) — theaws-sso
login image is new and is pulled by tag from no registry.