Skip to content

v0.6.0

Latest

Choose a tag to compare

@github-actions github-actions released this 09 Sep 07:03
· 3 commits to main since this release
Immutable release. Only release title and notes can be modified.
63e4ff3

Upgrading from v0.5.4

  • Rerun the installer to install both coop
    and the new coop-proxy companion in the same directory. The v0.5.4 updater
    replaces only coop, so its first upgrade does not install the companion
    required for proxy mode. Preserve your INSTALL_DIR if you used a custom
    installation directory.
  • The published v0.5.4 Linux ARM64 binary reports
    coop 0.5.4-dev (8e24729+dirty) and refuses self-update as a development
    build. Use the installer to upgrade it.
  • Restart all running Linux VMs after upgrading to apply guest-to-guest
    network isolation. Every VM on the shared bridge needs the restart; a
    pre-upgrade VM leaves peers reachable. macOS is unaffected.
  • Save guest-only work before restore --reprovision: it replaces the guest
    disk, including any guest keyring and cached account login. Rebuilding an
    image alone does not update existing VM disks.

New features

  • Codex ChatGPT account auth — New [codex] auth = "chatgpt" mode for
    account/workspace access without OpenAI API billing. coop installs Linux
    Secret Service support in the guest image, writes Codex's
    cli_auth_credentials_store = "keyring" setting, launches Codex through a
    D-Bus/GNOME Keyring wrapper (/usr/local/bin/codex-account, which the
    in-guest codex-yolo shortcut also uses and which execs Codex unchanged when
    the mode is off), suppresses every OPENAI_API_KEY forwarding path, and stops
    copying host auth.json into the guest. Sign in with coop codex -- login --device-auth; login and logout now run without the sandbox-bypass flag,
    which is meaningless on subcommands that never start an agent session.
    Existing images must be rebuilt with coop setup --rebuild before using this
    mode. A restart reuses the old guest disk, so an existing VM also needs
    coop restore <vm> --image <image> --reprovision (or a destroy and
    recreate) to pick up the new guest packages.

  • coop restore --reprovision — start over without re-typing anything
    (#432) — Replaces a clobbered or bloated guest filesystem with a fresh copy of
    the instance's image, then provisions it as a first boot: /workspace is
    restored from the source the instance recorded — re-synced, re-cloned or
    re-mounted — agents are re-bootstrapped, and plugins, marketplaces and MCP
    servers are reinstalled. The instance is left running, so no follow-up
    coop start is needed. It keeps its name, index, IP, image, disk size, port
    forwards and guest env — including a devcontainer's containerEnv and
    forwardPorts — so those flags do not have to be remembered. GitHub PATs and
    provider credentials live in the host-side secret store and are untouched.
    Extra --extra-mount directories, --exclude-git and a devcontainer's
    postStartCommand are not replayed, because coop does not persist them.

    This is what coop restore + coop start could not do: a restart skips the
    workspace sync and the plugin install on the assumption that both survived on
    the guest disk — which holds for a coop commit checkpoint, but not after the
    disk is replaced with a base image. Plain restore + start remains the
    checkpoint rollback; --reprovision is the base-image case.

    --image becomes optional under --reprovision, defaulting to the image the
    instance already records; it stays required otherwise. -y skips the
    confirmation and is required off a TTY. -y, --no-agents and --no-prompt
    are rejected without --reprovision, since none of them mean anything to a
    plain disk swap. Unlike a plain restore, which requires a stopped instance,
    --reprovision also accepts a running one and stops it itself. coop restore
    without --reprovision is unchanged.

  • Credential-injecting proxy — keep the model API keys out of the guest
    (#411) — New opt-in [proxy] config. When set, coop runs a small host-side
    reverse proxy (coop-proxy, a new binary shipped in the same tarball) — one
    process per (VM, provider) — for the lifetime of a remote-mode VM: the guest
    is pointed at it and holds only a per-instance capability token, while the
    real credential stays on the host and is injected onto requests upstream.

    • Claude Code ([proxy.anthropic]) is pointed at the proxy via
      ANTHROPIC_BASE_URL; supports an API key (x-api-key) or a Claude
      setup-token (Authorization: Bearer).
    • Codex ([proxy.openai]) is pointed at the proxy via a
      [model_providers.coop_local] block (Responses API) with the capability
      token as the provider bearer; OpenAI API keys inject as
      Authorization: Bearer. In proxy mode Codex's ~/.codex/auth.json is no
      longer staged onto the guest disk; Codex subscription is out of scope — use
      an API key.

    The raw ANTHROPIC_API_KEY / OPENAI_API_KEY is no longer forwarded into the
    guest, so a prompt-injected or rogue agent cannot read a usable key.
    Resolution fails closed — a bad credential aborts the boot rather than booting
    without injection. coop model <vm> local takes precedence and tears the
    proxy down. The proxy binds host loopback and is reverse-tunnelled (ssh -R)
    into the guest, so it works on both backends (Firecracker and Lima). GitHub
    is a tracked follow-up. coop update keeps coop and coop-proxy in
    lockstep.

  • The credential proxy is jailed (#411) — the host-side coop-proxy
    process runs confined so a proxy exploit cannot write files or execute
    programs, and — on a new enough kernel — cannot reach any host beyond the
    upstream :443 and DNS :53. On Linux it self-applies Landlock before
    serving, tiered by kernel capability: filesystem-write and program-exec are
    always denied (kernel ≥5.13) and fail-closed — the VM start aborts rather
    than running the proxy without that floor; TCP egress is port-scoped only on
    kernel ≥6.7, and on kernels 5.13–6.6 (which have no Landlock network rules)
    that scoping is dropped and egress is left open. On macOS the launcher wraps
    it in sandbox-exec with a Seatbelt profile. The jail is port-scoped, not
    host-scoped (upstream identity is enforced by the proxy's TLS verification),
    and does not restrict UDP on Linux; see
    docs/trust-model.md.

  • Per-VM credential overrides + coop proxy status (#411) — A single VM can
    use a different credential than the [proxy.<provider>] default — for
    per-project billing, scope, or revocation — via coop proxy setup [--openai] --vm <name>. The override is stored in that instance's state
    (<inst.dir>/proxy.json), not a growing config table, and its secret is
    namespaced separately. Resolution is override → default → off. coop proxy status [--vm <name>] shows what each VM resolves to, with credentials
    redacted.

  • coop proxy setup — store a provider credential like a GitHub PAT (#411)
    — Mirrors the coop github PAT wizard: paste a credential, pick a secret
    backend (macOS Keychain / Linux secret-service / 1Password / 0600 file), and
    coop stores it and writes the cmd: reference into [proxy.<provider>] — the
    credential is never plaintext in the config. Anthropic is the default;
    --openai configures Codex. The secret store is namespaced per service, so
    proxy secrets live under their own directory rather than among the GitHub PATs.

  • coop editor — VS Code or Zedcoop vscode is now coop editor
    (the old name remains as an alias). --editor takes code or zed;
    when omitted, coop tries VS Code first, then Zed. Zed connects with
    zed ssh://coop-<name>/<path>, reusing the same ~/.ssh/config alias
    block that VS Code's Remote-SSH uses.

  • coop up --new-instance — a second instance for the same projectup
    normally reuses the instance recorded for a project directory or --git-repo
    URL. --new-instance skips that lookup and creates a sibling instance
    instead, so two agents can work from one source tree. It requires --name,
    since the project-derived name is already taken. Once a project has siblings,
    coop up reports the ambiguity rather than picking one, so address them by
    name (coop start <name>, coop shell <name>).

Fixes

  • Firecracker guests can no longer reach each other by IP — Instances on the
    same host share the br0 bridge and a single subnet, so any guest could reach
    any other guest's SSH and forwarded ports. coop now marks every TAP as an
    isolated bridge port and inserts a FORWARD -i br0 -o br0 -j DROP rule; both are
    needed, since the bridge flag alone leaves a guest able to route around it via
    the host's bridge address. Guest→host and guest→internet are unaffected. A
    host that cannot apply the flag fails the VM start instead of booting an
    unisolated guest; this needs Linux ≥ 4.18 and iproute2 ≥ 4.19. Restart any running VMs after
    upgrading
    — isolation applies per start, and because the kernel requires
    both ports to be isolated, one pre-upgrade VM leaves the whole bridge
    unisolated. This blocks reachability, not impersonation; see
    docs/trust-model.md for the residuals. macOS is
    unaffected — Lima gives each guest its own user-mode NAT.

  • Guest transports fail instead of hanging when a VM stops responding — A
    paused VM, a wedged sshd, or a lost TAP device left ssh, scp, and rsync
    calls blocked on a dead socket with no deadline, so lifecycle commands,
    coop exec, and coop push/pull hung until interrupted. Every transport
    now derives from one option list that sets BatchMode, a connect timeout,
    and a liveness probe, so a guest whose sshd stops answering fails after ~90s
    — the bound interactive sessions already had.

  • The guest hostname resolves, so sudo stops warning — Instance creation
    renamed the Firecracker guest to claude-<name> in /etc/hostname but left
    the image's 127.0.1.1 claude-vm entry in /etc/hosts, so every sudo in
    the guest printed sudo: unable to resolve host claude-<name> before running.
    Both files are now written together at create and restore, and the guest
    hostname is clamped to fit the kernel's 64-byte hostname limit so long
    instance names still get a resolvable name. No image rebuild is needed — the patch is
    per-instance, and the image's own entry is what gets overwritten — but
    patch_guest_network runs only on create and restore, so an existing VM
    keeps the stale entry until coop restore <vm> --image <image> or a destroy
    and recreate.

  • Fail closed on an unmanaged CODEX_HOME in ChatGPT auth mode (#441) —
    The guest wrapper now refuses an explicitly set CODEX_HOME when coop's
    managed ~/.codex/config.toml selects keyring storage. This prevents codex login from silently writing a plaintext refresh token to the alternate
    directory, including a workspace path that syncs back to the host.

  • Install Codex's complete runtime package (#442) — Recent Codex releases
    use a companion codex-code-mode-host executable, but coop installed only
    the raw codex binary, causing Code Mode to fail closed at startup. Image
    builds and coop agent update --codex now verify and install the upstream
    package with its host and runtime resources intact, root-owned and behind a
    shared current-release link.

  • install.sh and coop update verify provenance without a GitHub
    credential
    (#421) — Verification ran gh attestation verify --repo trailofbits/coop, which reads the Sigstore bundle from the attestations
    API. gh refuses to run that command unless it is logged in, and then
    attaches its stored token, so a token with no SSO session for the
    trailofbits org got HTTP 403 and the install was refused; the same path
    failed outright for anyone with gh installed but not authenticated.
    Releases now publish the provenance bundle as an attestations.jsonl asset,
    and both clients fetch it with an unauthenticated request and verify against
    it with --bundle, which makes no attestations-API call and needs no
    credential. Releases published before the asset existed are still verified
    through the API path, unchanged.