v0.20.2
What's new since v0.20.1
A sandbox-hardening release focused on hosted runtimes where the seccomp user-notify listener can't be installed (Daytona, E2B, Firecracker-class). agentsh detect now tells the truth about what will actually enforce, the shell-shim stops over-denying legitimate scripts under runtime execve interception, the wrap/kernel-install path finally delivers env_inject (and optionally env_policy), and config validate catches sandbox/signing mistakes before deploy. The WTP contract also moves out into a standalone public repo, and two wire-schema items land (GoawayCode.PROTOCOL_ERROR, ServerHeartbeat.generation).
Sandboxing & detection — seccomp, ptrace, wrap
-
Honest
detect+SelectModefor uninstallable seccomp (#392, closes #390) — Follow-up to #388. The per-backend✓/-marks were correct, but the domain score, theactive backendlabel, and the reported security mode still overstated protection where theNEW_LISTENERinstall fails (Daytona/EBUSY):COMMAND CONTROL 25/25withactive backend: seccomp-execvewhileseccomp-execveshowed-.SelectModenow gatesfullonSeccompInstallable(a real install) rather than kernel user-notify support; the ptrace Command Control backend reflects actual enforcement (Ptrace && PtraceEnabled) with an actionable detail; andcommandActiveis derived by priority — so on an uninstallable host the domain honestly reports0/25with no active backend and the overall score drops.ValidateStrictModelikewise requires installable seccomp for strictfull, so an explicitmode: full+strict: truefails fast at startup instead of booting non-enforcing. The ptrace capability stays visible in the flatCAPABILITIESsection. -
Detect seccomp from a real
NEW_LISTENERinstall probe (#389, closes #388) —agentsh detectreported seccomp-execve / user-notify as available based on read-only kernel probes, a false positive where the listener install actually fails (e.g. Daytona, where the container runtime already holds the one allowed user-notify listener →EBUSY). Adds a fail-safe behavioral install-probe (SeccompInstallable) — a re-exec'd child attempts the real filter install and reports the errno — and splits the flat capability intoseccomp_user_notify(installable here) vsseccomp_user_notify_kernel(kernel-supported, read-only probe). -
Make opaque shell-c pre-deny interception-aware (#381, closes #375) — The shell-shim pre-check denied any
bash -c/sh -cscript it couldn't fully parse (rule=shellc-opaque-script, exit 126) even when runtimeexecveinterception was active and would police every inner exec — breaking orchestrators (e.g. Daytona) that run every command asbash -c "<script>". A regression from v0.18.0. When seccomp-execve(with unix sockets) or ptrace is active, the opaque script now runs and its innerexecvecalls are policed at depth byCheckExecve(EACCES); with no interception active, behavior is byte-for-byte unchanged (hard-deny preserved). Derivable denies (sh -c "shutdown now") andshellc-wrapper-bypassare untouched. -
sandbox.seccomp.shellc.opaqueknob for opaque shell-c handling (#386, closes #378) — Makes the opaque-shell-c posture explicit and operator-controlled:deny|enforce(default) |allow.enforcekeeps the interception-aware behavior from #375 (run under per-exec policing when interception is active, deny otherwise);allowruns opaque scripts without a pre-deny even when interception is off (with a warning);denyalways hard-denies. Lets operators onexecve-policed runtimes opt into running unparseable scripts deliberately. -
Enforce
env_policy(allow/deny) on the wrap path (opt-in) (#387, closes #379) — Part 2 of #374. The client-spawned wrap path (shell shim /agentsh wrap) inherited the launcher's full environment, soenv_policyallow/deny isolation never ran there — only on the server-spawned exec path. New opt-insandbox.wrap_env_policy.enabledplumbs the resolved policy throughWrapInitResponseand applies a subtractive, fail-openallow/deny(+ built-in default-secret-deny) filter client-side. Default-off (zero behavior change until enabled) and mixed-version safe.block_iterationandmax_bytes/max_keysare intentionally not carried on this path (aBuildEnvoverflow error would fail-open to the full unfiltered env, defeating the filter). -
Apply
sandbox.env_injecton the wrap-init / kernel-install path (#380, closes #374) — On the client-spawned wrap path the/wrap-initresponse carried only seccomp markers and never deliveredsandbox.env_inject, so injected vars (e.g.BASH_ENV→ startup hardening) silently never reached commands. Regressed when the shim moved to kernel-install enforcement (0.19.1+). Newinternal/envinject.Apply(override/dedup semantics mirroring the exec path) is wired throughWrapInitResponse.EnvInjectand applied by both the shim (assembleWrapperEnv) and the CLI wrap (seccomp + ptrace branches), with internalAGENTSH_*markers kept authoritative. -
Don't blanket-deny commands from a symlinked cwd under
symlink_escape=deny(#385, closes part 2 of #377) — Withpolicies.symlink_escape: deny, running any command from a cwd that is itself a symlink escaping the workspace mount was blanket-denied. The cwd is now evaluated as a subtree againstfile_rulesinstead of unconditionally denied, so legitimate work in a symlinked working directory proceeds while genuine escapes still deny. -
Allow
command -v/-Vintrospection in shell-c pre-check (#384, closes part 1 of #377) —bash -c 'command -v ls'was denied asshellc-wrapper-bypass(exit 126).command -v/-V NAMEare read-only introspection that never execute NAME, so they now fall through to the operator'sallow sh/bashrule.command -p NAME(which does execute) and barecommand NAME(which derives to NAME) keep their conservative behavior. -
WAIT_KILLABLE_RECVbehavioral probe + operator override (#371, closes #369) — Replaces the uname-basedWAIT_KILLABLE_RECVguess with a server-side behavioral probe that installs the worst-case production filter composition, services notifications, and classifies child exit before deciding whether to use the flag. Adds asandbox.seccomp.wait_killabletri-state override (auto / force-on / force-off) as the escape hatch, and surfaceswait_killable_sourceon every per-execseccomp: filter loadedline. Fail-safe to off on probe error.
Config validation
- Enforce sandbox + signing schema invariants in
config validate(#383, closes #376) —agentsh config validatereportedokfor configs the server then rejected at startup (e.g. thesandbox.ptrace+unix_socketsexecve-only constraint, or signingenforce/warnwithout a trust store), so misconfiguration surfaced as a generic "connection refused" via the shim. The two cross-field validators (Sandbox.Validate(),Policies.Signing.Validate()) are now wired into theconfig.Loadpath with the same messages used at startup. Scope is pure config-schema invariants — host/environment checks stay at startup — and the validators short-circuit on default/disabled state, so no previously-valid config is newly rejected.
Watchtower / WTP
-
Consume the WTP contract from
canyonroad/wtp-protos(#373) — Extracts the Watchtower Protocol protobuf contract out of agentsh into the new standalone public repocanyonroad/wtp-protos(Apache-2.0; ships.proto+ committed Go and Rust artifacts for future polyglot consumers). agentsh deletes its in-treeproto/canyonroad/**and depends on the published Go module — a pure import-path swap across 61 sites (wtpv1,ocsfpbidentifiers unchanged).proto/agentsh/v1(pty) is untouched. -
GoawayCode.PROTOCOL_ERROR = 6(#372, Stage 1 of #353) — Adds the canonical wire code for server-detected protocol-invariant violations;UNSPECIFIEDstays as the v0.4-legacy catch-all. Label-only: agent behavior is unchanged (reconnect-with-backoff on every Goaway), but the structured WARN log now surfacesgoaway_code=GOAWAY_CODE_PROTOCOL_ERRORfor triage. v0.4 watchtower remains compatible. -
ServerHeartbeat.generationhard cutover (#352) — Addsuint32 generationto theServerHeartbeatwire frame and removes the FIFO-order substitution workaround in the recv multiplexer;state_live/state_replayingandinflight.Releasenow read the wiregendirectly. Validator rejectsgeneration == 0(no v0.4.x compat — no prior server emitsServerHeartbeat).
Store reliability (Windows)
- Retry Windows sidecar
MoveFileExon transient access errors (#382) — DeflakesTestFlushLoop_PeriodicSync, which intermittently failed on Windows CI withrename sidecar: Access is denied.audit.WriteSidecar's atomic replace used a singleMoveFileExwith no retry, so a brief handle collision (CI poller, antivirus, search indexer) failed the audit flush. New platform-neutralretryReplacehelper (bounded ~180ms) wraps the Windows replace and classifiesERROR_ACCESS_DENIED/ERROR_SHARING_VIOLATIONas transient; Unixos.Renameis unchanged. A genuine permanent error still surfaces after the bounded retry. Production-robustness gap, not just test noise.
Operator-visible behavior changes
detect/ startup mode now reflect seccomp installability — on a host where theNEW_LISTENERfilter can't install, the reported mode islandlock(or lower) instead offull,WarnDegradedfires, andSecurity.Strictrequiringfullfails fast at startup rather than per-command later (#392, #389).- Opaque
bash -c/sh -cscripts run under per-exec policing when interception is active instead of being blanket-denied; tune with the newsandbox.seccomp.shellc.opaqueknob (deny|enforce(default) |allow) (#381, #386). sandbox.env_injectis now delivered on the shim/CLI wrap path (previously silently dropped since 0.19.1) (#380).command -v/-Vand commands run from a symlinked cwd are no longer over-denied by the shell-shim pre-check /symlink_escape=deny(#384, #385).sandbox.wrap_env_policy.enabled(new, default off) opts the wrap path intoenv_policyallow/deny filtering (#387).sandbox.seccomp.wait_killable(new, tri-state, default auto) overrides the behavioralWAIT_KILLABLE_RECVprobe (#371).
Validation
The final v0.20.2 Release workflow ran the full matrix and all jobs passed:
goreleaser(binaries, archives, GitHub release)alpine-build(amd64 + arm64)docker-test× 8: alpine, archlinux, debian, debian-trixie, fedora, rockylinux10, ubuntu, ubuntu2204build-macos-app(signed + notarized)publish-homebrew-caskupdate-checksums
v0.20.2-rc1 shipped at e11a32a0 (through #388) for a Daytona integration soak; v0.20.2 is tagged at 67950cea, adding the #390 detect-honesty fix (#392) on top after rc1 testing surfaced the scoring/active-backend overstatement.
Full changelog: v0.20.1...v0.20.2