The open-source governance control plane for coding agents — identity, controls, and audit are the product; the sandbox is a pluggable commodity. Wardyn governs workload run-identity and tokens: a wardyn authorizes one specific, scoped action — which is exactly what the broker mints.
Name & trademark. "Wardyn" is a working name — a formal trademark clearance (USPTO full-text + GitHub org / domain / package handles) is still pending, so the name may change before a 1.0. The module path
github.com/cjohnstoniv/wardynis a personal namespace for now; re-homing to a dedicated org later is a one-line change.
Apache-2.0 everything. No enterprise/ directory. No hosted backend either —
it runs on your infrastructure or it doesn't run. CNCF Sandbox is the
governance target.
Coding agents (Claude Code, Codex CLI, and their successors) today inherit the full developer credential they are launched under. A prompt-injected agent, a poisoned dependency, or a compromised MCP server inherits that same credential — the same repos, the same cloud access, the same blast radius.
Wardyn is the governance layer that sits between a human operator and a running agent. It provides:
-
Per-run identity. Every agent execution gets a SPIFFE ID (
spiffe://<trust-domain>/agent-run/<id>) that is distinct from the human who spawned it. The human'ssub, the agent run'sact, and the accountablesponsortravel together in every token, every commit, and every audit event. The agent never replaces the human in the attribution chain — it is added to it. [shipped] (Embedded JWT-SVID issuer; a SPIRE-backed issuer is [v0.5+ — planned].) -
Broker-minted scoped credentials. The agent never holds a credential. The token broker mints short-lived, repo-scoped, permission-down-scoped credentials on demand, injected proxy-side so the agent process never sees a bearer token. Approval-required grants mint inside the same Postgres transaction that verifies an
APPROVEDApprovalRequestfor that exact run+scope — no widening between what the approver saw and what was minted. [shipped] -
Layered egress. L0 structural [shipped] (no default route; sandbox egress only via
wardyn-proxy) → L1 default-deny L3/L4 (nftables / NetworkPolicy) [v0.5+ — planned] → L2wardyn-proxy[shipped] (L7 domain allowlist, method rules, proxy-side credential injection, first-use approval) → L3 MCP/tool gateway [v0.5+ — planned].HTTP_PROXY/HTTPS_PROXYARE set in the sandbox (for client compatibility), but the documented env-var-bypass class is defended structurally at L0: each run's network is gatewayless (Internal:true), so an agent that ignores the proxy env vars has no default route and reaches nothing — the only path off-host is thewardyn-proxysidecar. The bypass is designed out by the absence of a route, not by the absence of the env var. -
Three-stream append-only audit. The control-plane event log (Postgres trigger blocks UPDATE/DELETE) [shipped], PTY session replay via
wardyn-rec[shipped], and the eBPF/Tetragon ground-truth stream (kernel.*events from a host sensor, correlated onrun_id) [shipped]. (The ground-truth stream's control-plane plumbing + honest-degradation ship and are unit-tested; its Tetragon event mapper is validated against documented export shapes but not yet against a live Tetragon deployment — seethreatmodel/residual #12.) The ground-truth stream is detection, not prevention — it flags theld-linux/mmaploader bypass rather than claiming to block exec — and is honestly degradable: a host without the sensor reports/healthzebpf_groundtruth=unavailable, and it is blind inside CC3/Kata guests (a one-timekernel.sensor.blindevent, emitted automatically bywardyndfor every CC3 run, makes that gap visible). Every stream is keyed onrun_id. SIEM export ships as JSON webhook/syslog/file [shipped]; OTLP and OCSF v1.8ai_operationsinks are [v0.5+ — planned]. Audit export is free and never paywalled. -
Confinement Classes. In the UI these appear by their friendly names: Fence = CC1, Wall = CC2, Vault = CC3. CC1/Fence (hardened shared-kernel runc) [shipped], CC2/Wall (gVisor userspace kernel — default) [shipped], CC3/Vault (Kata microVM) [experimental] — live-proven end to end on a Docker daemon with the Kata runtime registered (needs
/dev/kvm; not available on Docker Desktop); packaged/GA hardening is a roadmap item. Policy can mandate a minimum class; the control plane refuses to schedule a run on a substrate that cannot satisfy the policy.Runs anywhere Docker runs. Fence needs only Docker — that's the floor. Wall is the default and needs gVisor's
runscon the host. Vault is the strongest tier and needs/dev/kvmplus a Kata runtime. Pick the floor your hardware supports (wardyn setup wall/wardyn setup vaultprints the exact steps for this machine); the control plane refuses to schedule a run below your policy's minimum.
A trusted control plane (wardynd + Postgres) launches each agent into an
untrusted, gatewayless sandbox whose only path out is the wardyn-proxy
sidecar; credentials are injected at the proxy, and every decision and PTY
session streams back into the append-only audit log.
Coding agents today inherit developer credentials at launch. No purpose-built, open-source governance layer existed to scope, gate, and attribute what an agent can do. The incumbent tools have filled parts of the gap in ways that create new problems:
- Existing governance controls ship as paid tiers of commercial platforms, closed-source services, or vendor-bundled runtimes — you cannot audit what you cannot read, and you cannot run it on your own infrastructure without a commercial agreement.
- Existing sandboxing tools have overclaimed their isolation properties: in
documented red-team exercises, hook-based in-agent enforcement was bypassed
via the dynamic linker (
ld-linux/mmap); at least one commercially-shipped sandbox escaped via a CVE in the container runtime.
Wardyn's thesis is that the real boundary is structural (no network path, no
resident credentials, enforcement outside the agent process) and that honest
disclosure of residual risks is a feature, not a liability. See
threatmodel/.
The substrate is a pluggable commodity: every major subsystem (sandbox, identity,
secrets, egress, policy, audit) sits behind a seam with a blessed default Wardyn
tests with and documented swappable alternates. See
docs/PLUGGABILITY.md.
human sub → agent-run SPIFFE ID → scoped minted credential
wardynd mints a RunIdentity at sandbox start. Sidecars (wardyn-proxy,
wardyn-rec) authenticate to the control plane using the run token; the
embedded identity provider (or SPIRE at v0.5) verifies with audience
wardyn-internal. Every downstream token and commit carries the full
delegation chain.
A CredentialGrant records what a run is eligible for — it is not
issuance. When RequiresApproval is set, the broker mints the credential
only inside the same Postgres transaction that:
- Reads
approvals.state = 'APPROVED'for thisrun_id+grant_id. - Writes
approvals.minted_jtiback.
The minted scope equals exactly the scope the approver saw. No scope widening is possible between approval and mint.
| Layer | Mechanism | What it stops |
|---|---|---|
| L0 structural | Sandbox network is gatewayless (Internal:true); the only off-host path is the wardyn-proxy sidecar |
HTTP_PROXY env-var bypass class (no route exists to bypass to); direct IP egress |
| L1 default-deny | nftables / NetworkPolicy; block 169.254.169.254 |
Non-HTTP tunnels; metadata-server theft; DNS rebinding |
| L2 wardyn-proxy | Domain allowlist (exact + *. wildcard); method rules; first-use approval (always_deny / deny_with_review / wait_for_review, which holds the connection for a live operator decision); proxy-side credential injection |
L7 exfil to unlisted domains; token leakage into sandbox |
| L3 MCP gateway | Per-tool call approval and logging (v0.5) | Tool-call egress that bypasses the network proxy |
The same proxy seam also carries the corporate-environment lanes: an optional
upstream (parent) proxy hop, per-ecosystem artifact-mirror redirection with
proxy-side token injection (the sandbox never holds the Artifactory/Nexus
token), and SCM credentials brokered per-clone via wardyn-git-helper.
On run stop (including failure), wardynd cascades in a fixed order: sandbox
teardown → run-token deny-listing (the embedded identity provider; SPIRE entry
deletion arrives with SPIRE at v0.5) → revocation of minted broker
credentials → durable state transition. The cascade fires automatically; it is
not optional.
audit_events(Postgres, append-only —UPDATE/DELETEtrigger raises exception). System of record.- eBPF/Tetragon ground-truth stream — tamper-proof counterpart to agent
self-report. A host-scoped sidecar (
wardyn-tetragon-ingest) tails Tetragon's JSON export, correlates each kernel event to a run via thewardyn.run-idcontainer label, and POSTskernel.*events to the control plane (POST /api/v1/internal/groundtruth), which records them append-only — so they land in Postgres AND fan to every SIEM sink with zero new fanout code. Detection, not prevention (theld-linux/mmaploader bypass is flagged, never blocked). Honest degradation:/healthzreportsebpf_groundtruthhealthy/degraded/unavailabledriven by the most recent sensor heartbeat; host eBPF is blind inside CC3/Kata guests (kernel.sensor.blindmakes that visible). Seedeploy/compose(--profile groundtruth) andinternal/groundtruth. - PTY session replay via
wardyn-rec(execsasciinema; GPL subprocess, never linked).
Each stream is keyed on run_id and exported to customer SIEM free.
The primary way to onboard real work: record a named interactive session in a workspace (with model access), then rerun it as a governed profile — the New Run dialog offers the workspace's recorded sessions as fast-track profiles, and a recorded profile's observed egress is loaded into the run's allowlist for review. Verify does NOT replay the recorded session itself — it launches a separate confined run (default-deny egress + live approvals) that executes the workspace's operator-approved setup commands (install/build/test/lint) in the built devcontainer image, to prove the environment/build works before you rely on the profile.
AI Run Composer (optional, UI currently hidden)
Describe a task in plain English and Wardyn proposes a confined run — agent, repo,
confinement class, egress allowlist, and credential grants — then grades it
deterministically for you to review and launch. It's advisory (the backend never
sees the run's credentials) and off by default. The backend enables via
WARDYN_COMPOSER_CONFIG (see examples/composer-configs/:
fake.json needs no API key; Claude CLI / Anthropic / OpenAI backends give real
prompt-driven proposals), and the Describe surface in the UI is additionally
gated behind the COMPOSER_UI_ENABLED build flag (ui/src/app/lib/features.ts),
which is off in this release — recorded profiles above are the primary path.
Wardyn publishes what it does not defend against. The full published
residual-risk list is in threatmodel/THREAT-MODEL.md.
Notable residual risks:
- The model-API channel is an unavoidable data-exit path. The LLM gateway logs every prompt/token/tool call but cannot prevent an agent from encoding data into a prompt to a model it is permitted to call.
- Domain fronting and DNS-tunnel exfil are not closed below the optional L2 TLS-intercept tier (unbuilt; v0.5 target).
- Tier-1 hardened-runc shares the host kernel. A kernel 0-day on a runc-only host defeats the sandbox boundary. Wardyn defaults to CC2 (gVisor) for this reason.
- The 1-hour minted-token window before kill-switch revocation is a real exposure window that cannot be fully eliminated, only minimized by TTL.
- Docker with the
composev2 plugin (Docker Desktop or a native Docker Engine). Fence/CC1 runs need nothing more; Wall/CC2 adds gVisor'srunsc, Vault/CC3 adds/dev/kvm+ a Kata runtime —wardyn setup wall|vaultprints the exact steps for your machine. - Go 1.26+ and Node 22 + pnpm 9 — only for building from source / host mode (the team/compose path builds inside containers).
- Postgres is included in the compose file — nothing external to install, no hosted service to sign up for.
Status: pre-alpha. Interfaces are not stable. Do not run production workloads.
git clone https://github.com/cjohnstoniv/wardyn
cd wardyn
make setup # one interactive installer: detects your host, asks Local vs Team, launches the right modemake setup (== scripts/setup.sh) asks the one genuine human choice and does
the rest:
- Local (host mode) — the default; recommended for personal use. Sandbox
agents run on this machine with your Claude login: setup stages your
existing
claudecredentials for sandboxes (the live token is injected at the proxy on the wire, so no long-lived or refreshable secret is resident in the sandbox — only an inert, proxy-overridden sentinel), buildsbin/wardynd+ the UI on first run, reuses or starts the compose Postgres on loopback, then launcheswardyndas a background host process (PID + log under~/.wardyn/) and openshttp://localhost:8080. It also detects which confinement barriers this host can run and prints the exact commands for anything missing (gVisor/Kata installs need sudo — the script never runs sudo itself). Stop it withmake stop-host. - Team (compose) —
wardyndruns sealed in a container as a shared service (==scripts/up.sh up): read-onlymake doctorpreflight, builds thewardyndimage, mints/persists a secret-store age key, auto-picks a confinement policy (CC2'sdefault.jsonifrunscis available, else CC1'sdemo.json), startspostgres+wardyndin no-login local mode, opens the browser as soon as the UI is live (WARDYN_UP_NO_BROWSER=1to skip), and builds the per-run images after (WARDYN_UP_SKIP_RUN_IMAGES=1skips). Tear down withmake compose-down— this keeps your local data (runs and the append-only audit log persist by design).
Non-interactive: WARDYN_SETUP_MODE=local|team make setup, or call the
shortcuts make setup-host / make setup-team directly. Re-run make doctor
any time — it's read-only. To deliberately start from an empty Runs list,
make reset wipes the volumes (Postgres + recordings) and re-runs setup.
On your desktop, Wardyn never adds power: a sandbox can at most reach what you — the operating user on this machine — already can, the operator policy clamps that down to what you allow at all, and each run is granted only the minimal subset (scoped credential, task egress allowlist, onboarded mounts) its task actually needs.
- WSL (Windows Subsystem for Linux): run
make setupinside your WSL distro's shell, not cmd.exe/PowerShell — the control plane runs in Linux, andmake setupopens the UI in your Windows browser automatically (wslview/explorer.exe). - Native Windows: install WSL2
and Docker Desktop with WSL integration enabled, then run
make setupinside the WSL distro.make doctordetects a native Windows shell and blocks with this same guidance rather than failing confusingly partway through.
Want the CI-tested SSO/Dex + bearer-token demo instead (builds an agent image and scripts a real governed run end to end)?
make agent-images # build the agent OCI images the demo run launches
make demo # build images, bring the stack up, create a governed demo run, print its audit trailmake demo starts postgres + dex + wardynd, creates a real governed run,
and prints its audit trail. To bring the raw stack up yourself instead (no
demo run) use the Compose file directly — note the extension is .yaml, not
.yml:
docker compose -f deploy/compose/docker-compose.yaml upThe control plane listens on http://localhost:8080. Postgres is the only
required dependency; it is included in the Compose file. Dex (human SSO) is an
opt-in compose profile (sso) — add --profile sso (or name dex
explicitly) to start it; make setup's default local-mode path never does.
Both make setup paths already run without a login. To bring up the compose
stack by hand with the same behavior, set WARDYN_LOCAL_MODE to skip
SSO/Dex and the bearer token entirely — open the UI on localhost and spawn
agents with no login:
WARDYN_LOCAL_MODE=true docker compose -f deploy/compose/docker-compose.yaml up postgres wardynd
# then open http://localhost:8080 — no token, no loginActions are attributed to the local operator (local:<os-user>, or set
WARDYN_LOCAL_OPERATOR), so the sub/sponsor/decided_by/audit attribution chain
stays meaningful. Sidecar/run-token auth is unaffected. Local mode refuses to start
on an explicit publicly-routable IP (it will not serve a no-auth API on a public
IP), but on an unspecified bind (0.0.0.0, the WARDYN_LISTEN default) it only
warns — it does not refuse — because that bind might be host-firewalled or
purely a docker-bridge address. For a real guarantee, bind/publish loopback-only
(the Compose default already publishes 127.0.0.1) — do not rely on the warning
alone, and keep local mode on a trusted single-dev machine. The wardyn CLI also
works with no token against a local-mode daemon.
go install github.com/cjohnstoniv/wardyn/cmd/wardyn@latest installs the CLI —
the supported go install target. wardynd is not: a bare go install of
it silently yields a binary with no embedded UI and no sandbox runner (it needs
-tags docker plus a separately built ui/dist) — run the control plane via
make setup or the container image instead.
The API is fail-closed behind a bearer token (it also accepts a valid OIDC
session cookie). The Compose default is WARDYN_ADMIN_TOKEN=demo-admin-token;
override it by exporting WARDYN_ADMIN_TOKEN before make demo. The wardyn
CLI reads WARDYN_ADMIN_TOKEN, so the headless path is simplest:
export WARDYN_URL=http://localhost:8080 WARDYN_ADMIN_TOKEN=demo-admin-token
# Create a governed run
wardyn run --agent claude-code --repo octocat/hello-world --task "fix the flaky test"
# List runs and watch state
wardyn runs list
# Inspect a run's audit trail
wardyn audit --run <id>
# Approve a pending approval (id from the Approvals UI, or the API below)
wardyn approve <approval-id> --reason "reviewed scope, looks correct"The equivalent raw HTTP — the same token sent as a bearer header on every
call (omitting it returns 401):
# Create a run
curl -s -X POST http://localhost:8080/api/v1/runs \
-H 'Authorization: Bearer demo-admin-token' \
-H 'Content-Type: application/json' \
-d '{"agent":"claude-code","repo":"org/repo","task":"fix the flaky test"}'
# List pending approvals
curl -s -H 'Authorization: Bearer demo-admin-token' \
'http://localhost:8080/api/v1/approvals?state=PENDING'
# Approve
curl -s -X POST http://localhost:8080/api/v1/approvals/<id>/approve \
-H 'Authorization: Bearer demo-admin-token' \
-H 'Content-Type: application/json' \
-d '{"reason":"reviewed scope, looks correct"}'New to Wardyn? docs/TRY-IT.md is a guided 10-minute
walkthrough (governance demo with no keys, then a real Claude Code run), and
examples/ holds the demo policies and workspaces.
Two paths — only one runs sandboxes today:
| Path | Location | Status |
|---|---|---|
| Docker Compose | deploy/compose/ |
[shipped] — the only working data plane today |
| Helm chart (one blessed chart) | deploy/helm/wardyn/ |
[v0.5 — planned] — render-checked only (helm lint + helm template in CI); there is no Kubernetes runner driver yet, so the chart deploys the control plane but cannot create sandboxes yet |
No "bring your own arbitrary Kubernetes manifests." The parity rule: a feature is not done until it passes the conformance suite on both Docker and kind — today only the Docker target runs functionally (see Status below).
| Milestone | Highlights | Status |
|---|---|---|
| v0.1 | Per-run identity (embedded provider), approval FSM, credential broker, L2 egress proxy, append-only Postgres audit + PTY replay, CC1/CC2 confinement gating, Compose deploy | Shipped (pre-alpha) |
| v0.2 | Open-source pilot bar (Docker-only): secret-output masking, eBPF/Tetragon ground-truth audit stream, pinned seccomp + AppArmor, interactive attach sessions, policy CRUD, run-completion state, control-plane TLS, real conformance gate + supply-chain CI | In progress |
| v0.5 | SPIRE identity provider, OpenBao secret store, L3 MCP/tool gateway, L2 TLS-intercept, Helm chart, cloud STS federation, OTLP/OCSF SIEM sinks | Planned |
| v1.0 | CC3 Kata packaged/GA (experimental today — see Confinement Classes), Cilium toFQDNs, hash-chained audit + signed action receipts, separation-of-duty on control plane, conformance suite across both targets | Planned |
| Binary | Role |
|---|---|
wardynd |
Control plane: REST API, embedded web UI, policy engine, approval FSM, token broker, audit ingest. Postgres only. |
wardyn-runner |
Data plane: docker/ driver implementing internal/runner.Runner (k8s driver [v0.5 — planned], not yet built). |
wardyn-proxy |
Per-workspace L2 egress sidecar: default-deny domain allowlist, method rules, first-use approval, decision logs, proxy-side credential injection. |
wardyn-rec |
Per-workspace PTY session recorder (execs asciinema; GPL subprocess, never linked). |
wardyn-tetragon-ingest |
Host-scoped eBPF/Tetragon ground-truth ingest sidecar: tails Tetragon's JSON export, correlates each kernel.* event to a run via the wardyn.run-id container label, and POSTs to /api/v1/internal/groundtruth. Opt-in (groundtruth profile). |
wardyn-git-helper |
In-sandbox git credential helper: brokers a short-lived, repo-scoped token from the control plane to stdout only (never disk or env). |
wardyn-scan |
In-sandbox workspace scanner: clone-and-scan a source, upload raw ScanFacts to the control plane (profile derivation happens server-side). |
wardyn-verify |
In-sandbox verify runner: executes the workspace's operator-approved setup commands (install/build/test/lint) in the built devcontainer image under confinement (default-deny egress + live approvals) and reports the result. It does NOT replay a recorded PTY session — see "Recorded profiles → governed reruns" below for that. |
wardyn |
CLI: wardyn run, wardyn approve, wardyn audit, wardyn policy, wardyn setup wall|vault. |
Apache-2.0. Contributor sign-off via DCO (Signed-off-by). No enterprise/
directory — every control is in the open. Nothing here is a free tier of a
paid product — there is no paid product. Every control described above
(identity, egress, audit, approvals) is in this repo under Apache-2.0, not
gated behind a commercial license. CNCF Sandbox is the governance target.
Contributions welcome. See CONTRIBUTING.md.
Wardyn is a working name, pending trademark search.


