Skip to content

v0.20.2

Choose a tag to compare

@github-actions github-actions released this 25 May 20:02
· 41 commits to main since this release
67950ce

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 + SelectMode for uninstallable seccomp (#392, closes #390) — Follow-up to #388. The per-backend ✓/- marks were correct, but the domain score, the active backend label, and the reported security mode still overstated protection where the NEW_LISTENER install fails (Daytona/EBUSY): COMMAND CONTROL 25/25 with active backend: seccomp-execve while seccomp-execve showed -. SelectMode now gates full on SeccompInstallable (a real install) rather than kernel user-notify support; the ptrace Command Control backend reflects actual enforcement (Ptrace && PtraceEnabled) with an actionable detail; and commandActive is derived by priority — so on an uninstallable host the domain honestly reports 0/25 with no active backend and the overall score drops. ValidateStrictMode likewise requires installable seccomp for strict full, so an explicit mode: full + strict: true fails fast at startup instead of booting non-enforcing. The ptrace capability stays visible in the flat CAPABILITIES section.

  • Detect seccomp from a real NEW_LISTENER install probe (#389, closes #388) — agentsh detect reported 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 into seccomp_user_notify (installable here) vs seccomp_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 -c script it couldn't fully parse (rule=shellc-opaque-script, exit 126) even when runtime execve interception was active and would police every inner exec — breaking orchestrators (e.g. Daytona) that run every command as bash -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 inner execve calls are policed at depth by CheckExecve (EACCES); with no interception active, behavior is byte-for-byte unchanged (hard-deny preserved). Derivable denies (sh -c "shutdown now") and shellc-wrapper-bypass are untouched.

  • sandbox.seccomp.shellc.opaque knob for opaque shell-c handling (#386, closes #378) — Makes the opaque-shell-c posture explicit and operator-controlled: deny | enforce (default) | allow. enforce keeps the interception-aware behavior from #375 (run under per-exec policing when interception is active, deny otherwise); allow runs opaque scripts without a pre-deny even when interception is off (with a warning); deny always hard-denies. Lets operators on execve-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, so env_policy allow/deny isolation never ran there — only on the server-spawned exec path. New opt-in sandbox.wrap_env_policy.enabled plumbs the resolved policy through WrapInitResponse and applies a subtractive, fail-open allow/deny (+ built-in default-secret-deny) filter client-side. Default-off (zero behavior change until enabled) and mixed-version safe. block_iteration and max_bytes/max_keys are intentionally not carried on this path (a BuildEnv overflow error would fail-open to the full unfiltered env, defeating the filter).

  • Apply sandbox.env_inject on the wrap-init / kernel-install path (#380, closes #374) — On the client-spawned wrap path the /wrap-init response carried only seccomp markers and never delivered sandbox.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+). New internal/envinject.Apply (override/dedup semantics mirroring the exec path) is wired through WrapInitResponse.EnvInject and applied by both the shim (assembleWrapperEnv) and the CLI wrap (seccomp + ptrace branches), with internal AGENTSH_* markers kept authoritative.

  • Don't blanket-deny commands from a symlinked cwd under symlink_escape=deny (#385, closes part 2 of #377) — With policies.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 against file_rules instead of unconditionally denied, so legitimate work in a symlinked working directory proceeds while genuine escapes still deny.

  • Allow command -v/-V introspection in shell-c pre-check (#384, closes part 1 of #377) — bash -c 'command -v ls' was denied as shellc-wrapper-bypass (exit 126). command -v/-V NAME are read-only introspection that never execute NAME, so they now fall through to the operator's allow sh/bash rule. command -p NAME (which does execute) and bare command NAME (which derives to NAME) keep their conservative behavior.

  • WAIT_KILLABLE_RECV behavioral probe + operator override (#371, closes #369) — Replaces the uname-based WAIT_KILLABLE_RECV guess 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 a sandbox.seccomp.wait_killable tri-state override (auto / force-on / force-off) as the escape hatch, and surfaces wait_killable_source on every per-exec seccomp: filter loaded line. Fail-safe to off on probe error.

Config validation

  • Enforce sandbox + signing schema invariants in config validate (#383, closes #376) — agentsh config validate reported ok for configs the server then rejected at startup (e.g. the sandbox.ptrace + unix_sockets execve-only constraint, or signing enforce/warn without 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 the config.Load path 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 repo canyonroad/wtp-protos (Apache-2.0; ships .proto + committed Go and Rust artifacts for future polyglot consumers). agentsh deletes its in-tree proto/canyonroad/** and depends on the published Go module — a pure import-path swap across 61 sites (wtpv1, ocsfpb identifiers 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; UNSPECIFIED stays 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 surfaces goaway_code=GOAWAY_CODE_PROTOCOL_ERROR for triage. v0.4 watchtower remains compatible.

  • ServerHeartbeat.generation hard cutover (#352) — Adds uint32 generation to the ServerHeartbeat wire frame and removes the FIFO-order substitution workaround in the recv multiplexer; state_live/state_replaying and inflight.Release now read the wire gen directly. Validator rejects generation == 0 (no v0.4.x compat — no prior server emits ServerHeartbeat).

Store reliability (Windows)

  • Retry Windows sidecar MoveFileEx on transient access errors (#382) — Deflakes TestFlushLoop_PeriodicSync, which intermittently failed on Windows CI with rename sidecar: Access is denied. audit.WriteSidecar's atomic replace used a single MoveFileEx with no retry, so a brief handle collision (CI poller, antivirus, search indexer) failed the audit flush. New platform-neutral retryReplace helper (bounded ~180ms) wraps the Windows replace and classifies ERROR_ACCESS_DENIED / ERROR_SHARING_VIOLATION as transient; Unix os.Rename is 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 the NEW_LISTENER filter can't install, the reported mode is landlock (or lower) instead of full, WarnDegraded fires, and Security.Strict requiring full fails fast at startup rather than per-command later (#392, #389).
  • Opaque bash -c/sh -c scripts run under per-exec policing when interception is active instead of being blanket-denied; tune with the new sandbox.seccomp.shellc.opaque knob (deny | enforce (default) | allow) (#381, #386).
  • sandbox.env_inject is now delivered on the shim/CLI wrap path (previously silently dropped since 0.19.1) (#380).
  • command -v/-V and 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 into env_policy allow/deny filtering (#387).
  • sandbox.seccomp.wait_killable (new, tri-state, default auto) overrides the behavioral WAIT_KILLABLE_RECV probe (#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, ubuntu2204
  • build-macos-app (signed + notarized)
  • publish-homebrew-cask
  • update-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