Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,26 @@ jobs:
run: cargo test --workspace

- name: Install network test tools
run: sudo apt-get update && sudo apt-get install -y iproute2 iptables iputils-ping util-linux openssh-client openssh-server
run: sudo apt-get update && sudo apt-get install -y iproute2 iptables iputils-ping util-linux openssh-client openssh-server python3-dbus

- name: Release preflight regression tests
run: python3 tests/test-preflight-release.py

- name: Integration probe regression tests
run: python3 tests/test-integration-probes.py

- name: Codex account wrapper regression tests
run: python3 tests/test-codex-account.py

- name: Codex permission defaults regression tests
run: python3 tests/test-codex-permissions.py

- name: Codex SSH session provisioning regression tests
run: python3 tests/test-codex-session.py

- name: Codex shared keyring readiness tests
run: /usr/bin/python3 tests/test-codex-keyring.py

- name: Integration test — proxy reverse forwarding
run: ./tests/integration-proxy-forward.sh

Expand Down
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ The `commands/` submodules own the domains: `lifecycle.rs` (up/start/shell/
exec/stop/destroy/status/list/resize/commit/restore), `quickstart.rs`,
`devcontainer.rs`, `profiles.rs` (+ images), `agent.rs` (`coop agent update`),
`model.rs` (`coop model`), `proxy.rs` (`coop proxy`), `github.rs`,
`codex.rs` (shared guest keyring installation and `coop codex-unlock`),
`admin.rs` (init/validate/uninstall),
and `json.rs` (machine-readable `--json` output types). `commands/mod.rs`
re-exports the dispatch surface and holds cross-domain helpers
Expand Down
134 changes: 89 additions & 45 deletions docs/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,87 @@ coop codex [instance-name] [-- extra-args...]

This SSHes into the guest and runs the `codex` CLI. By default coop passes `--dangerously-bypass-approvals-and-sandbox`, so Codex runs without its sandbox or approval prompts — parity with how `coop claude` runs unrestricted. The VM is the isolation boundary, so Codex's own sandbox is redundant; it also does not work in the guest, which lacks a functioning bubblewrap, so leaving it enabled makes every shell command Codex runs fail.

When `[codex] auth = "chatgpt"` is enabled, `coop codex` launches a small
guest wrapper (`/usr/local/bin/codex-account`) that starts a D-Bus session,
unlocks GNOME Keyring, and then runs the real Codex binary. In keyring mode
each launch gets a fresh private D-Bus session, so the wrapper asks for the
guest keyring password every time before Codex starts.

The in-guest `codex-yolo` shortcut routes through the same wrapper, so it works
in either auth mode. Running the bare `codex` binary from `coop shell` does
not: it has no D-Bus session, and `keyring` credential storage has no
`auth.json` fallback, so Codex will not find its credentials. Inside the guest,
run `codex-account` (or `codex-yolo`) instead of `codex`. The wrapper is a
transparent passthrough unless the guest `~/.codex/config.toml` asks for
keyring storage, so it is safe to use in either mode. It gates on the guest
file rather than on coop's `auth` setting, which is what keeps `codex-yolo`
working from inside the guest; coop keeps that file in step when you switch
modes, rewriting it on the next `coop start` to drop the keyring setting.

To keep Codex's sandbox and approval prompts for a single session, pass `--ask`. coop then launches `codex` with no bypass flag, so Codex applies its normal defaults:
With `[codex] auth = "chatgpt"`, the guest wrapper (`codex-account`) verifies
and unlocks the guest user's shared GNOME Keyring service before launching
Codex. The systemd user bus and keyring survive closing the terminal; a VM
restart locks the keyring again. Nested launches reuse the unlocked service.

The wrapper supplies `-c 'cli_auth_credentials_store="keyring"'` to keep the
terminal app-server independent of the desktop app-server (#481). Both read
one guest credential store. Caller arguments retain their precedence,
including an explicitly selected remote app-server. API-key mode passes through.

Use `codex-account` or `codex-yolo` inside the guest for automatic readiness
checks. After unlock, ordinary SSH sessions can also run bare `codex` against
the standard user bus. The wrapper gates on the managed guest configuration;
coop updates that configuration when you change authentication modes and start
the VM again.

### Desktop over SSH

Desktop authentication is implemented for validation with GNOME Keyring 46.1
and native Codex 0.154.0. Actual desktop UI connection and real-account OAuth
validation remain release gates; see the [implementation record](design/issue-480-desktop-auth-implementation.md).

```bash
coop ssh-config my-project
ssh coop-my-project codex --version
coop codex-unlock my-project
```

Then enable the SSH host in the desktop app's Settings → Connections, select
`/workspace`, and complete Codex login inside the guest. Desktop sign-in, SSH
access, the keyring password and guest account login are separate steps. Close
the unlock terminal and reconnect to verify that credentials remain available.

On an existing VM, the first `codex-unlock` installs shared service support in
place. Stop and start that VM before running unlock again. This retires old
private keyring daemons and updaters together. Installation preserves the Codex
home and encrypted credentials. Multiple running keyring daemons must be
resolved before migration so their cached credential histories are not silently
selected at reboot. Conflicting keyring files, unsupported formats
and plaintext stores require explicit migration; coop never selects a history
or replaces them automatically. Back up the files inside the guest before
resolving conflicts or reauthenticating.

`codex-unlock` also upgrades guests missing the SSH session support required
by older Firecracker images, then requests the same stop/start cycle. See
[Firecracker SSH user sessions](platform-notes.md#firecracker-ssh-user-sessions)
for the PAM and persistent-linger details.

After a keyring crash or locked-to-unlocked transition, rerun `codex-unlock`
and reconnect the desktop. Recovery retires the desktop server through Codex's
native `daemon stop`; the desktop owns its next startup and updater. Closing or
locking the keyring cannot erase credentials already cached in running clients.
Concurrent OAuth refresh and logout across clients require real-account testing.
Desktop SSH does not use coop's API-key/proxy secret forwarding.

#### Desktop execution permissions

Select **Full access** in the desktop thread's permissions control when using
the VM as the isolation boundary. An explicit desktop selection overrides
guest defaults, including when continuing an existing thread. Auto mode uses
the Linux workspace sandbox and may fail to initialize on guests without
working bubblewrap/user-namespace support. Changing authentication or unlocking
the keyring does not change a thread's permissions.

Image provisioning and agent bootstrap install `/etc/codex/config.toml` when
that file does not already exist, in both authentication modes:

```toml
approval_policy = "never"
default_permissions = ":danger-full-access"
```

These are system defaults for Codex 0.154.0, below user/project configuration
and explicit thread selections. Existing system configuration is preserved.
After upgrading an existing VM, restart it with agent bootstrap enabled to
install the defaults, then reconnect the desktop. `--no-agents` skips this
installation on existing images. The defaults do not disable separate app/MCP
approval policies or organization requirements.

To restore workspace sandboxing and on-request approval prompts for a single
session, pass `--ask`. coop explicitly overrides the unrestricted guest defaults:

```bash
coop codex --ask
Expand Down Expand Up @@ -111,14 +174,13 @@ agent session, so there is nothing to sandbox — and no `--ask` is needed.)
A fresh VM has no keyring, so the first prompt is *choosing* a password, not
entering one. The wrapper says so and asks for confirmation. That password
encrypts the Codex account credentials at rest inside the guest and is
requested again on later launches; it is unrelated to your ChatGPT or host
requested again after a VM restart or keyring lock; it is unrelated to your ChatGPT or host
credentials. Because it is per-guest, `coop destroy` discards it along with the
cached login.

The prompt needs a terminal. `coop codex` provides one. Anything that runs
the wrapper without one — invoking `codex-account` yourself through
`coop exec`, or a `post_start` script — fails with a clear message rather than
hanging.
Unlocking needs a terminal, supplied by `coop codex` and `coop codex-unlock`.
Noninteractive launches work after successful unlock in the current service
generation; otherwise they fail with guidance to run an interactive unlock.

Security and billing guardrails in this mode:

Expand All @@ -141,28 +203,10 @@ own state in it — installed marketplaces and plugins, and the
`[projects.*]` workspace-trust records — is read back and preserved across the
rewrite, so you are not re-approving workspace trust after each restart.

Images built before this support existed need a rebuild:

```bash
coop setup --rebuild
```

A rebuild only changes the golden image. An existing VM keeps its own guest
disk across `coop stop` / `coop start`, so it will not pick up the new guest
packages. Swap the rebuilt image in without losing the instance:

```bash
coop restore my-project --image default --reprovision
```

[`--reprovision`](commands.md#--reprovision) keeps the instance's name, index,
IP, and workspace association, accepts a running instance, and leaves it
running. It provisions the replaced disk as a first boot, so `/workspace` is
restored and the agent plugins are reinstalled — a plain `restore` here would
leave both empty, because the base image carries neither. Both reprovisioning
and destroying/recreating replace the guest disk. Save
guest-only work first (for example with `coop pull`); the replacement also
discards any guest keyring and cached account login.
For existing guests, use `coop codex-unlock <vm>` to install shared keyring
support in place, then stop and start the VM. To include support in future
VMs, rebuild the golden image with `coop setup --rebuild`. Rebuilding an image
does not change an existing guest disk.

### GitHub auth

Expand Down
22 changes: 21 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ coop ca my-project -- --cwd /workspace

### `codex`

Launch Codex inside the VM. By default coop passes `--dangerously-bypass-approvals-and-sandbox`, so Codex runs without its sandbox or approval prompts — parity with `coop claude`. The VM is the isolation boundary, and Codex's own Linux sandbox does not work in the guest (no functioning bubblewrap), so leaving it enabled makes every shell command Codex runs fail. Use `--ask` to keep Codex's sandbox and approval prompts for that session. With `[codex] auth = "chatgpt"`, `coop codex` launches through the guest keyring wrapper. The `login` and `logout` subcommands are always launched without the bypass flag: they never start an agent session, so there is nothing to sandbox.
Launch Codex inside the VM. By default coop passes `--dangerously-bypass-approvals-and-sandbox`, so Codex runs without its sandbox or approval prompts — parity with `coop claude`. The VM is the isolation boundary, and Codex's own Linux sandbox does not work in the guest (no functioning bubblewrap), so leaving it enabled makes every shell command Codex runs fail. Use `--ask` to explicitly restore workspace sandboxing and on-request approvals for that session, overriding the guest's full-access defaults. Caller arguments can override those settings. For desktop permission selection and system defaults, see [Codex integration](codex-integration.md#desktop-execution-permissions). With `[codex] auth = "chatgpt"`, `coop codex` launches through the guest keyring wrapper. The `login` and `logout` subcommands are always launched without the bypass flag: they never start an agent session, so there is nothing to sandbox.

```
coop codex [NAME] [FLAGS] [ARGS...]
Expand All @@ -359,6 +359,26 @@ coop codex my-project -- --model gpt-5
coop codex my-project -- login --device-auth
```

### `codex-unlock`

```text
coop codex-unlock [NAME]
```

Unlock the guest's shared encrypted keyring for ChatGPT account authentication.
Requires `[codex] auth = "chatgpt"` and a running VM with the managed guest
configuration. First use confirms a nonempty password; later unlocks reuse the
same store. The keyring stays unlocked after SSH logout and locks at VM restart.

Existing guests receive support files and packages in place on the first call;
stop and start the VM before retrying. Invalid, plaintext, unsupported or
conflicting stores are preserved and refused. Successful recovery retires the
native desktop server's cached authentication; reconnect the desktop afterward.
See [desktop SSH setup](codex-integration.md#desktop-over-ssh) for validation
status and the account login steps.

`coop codex unlock` still launches Codex in a VM named `unlock`.

### `exec`

Run a command in the VM and print its output. No PTY is allocated and stdin is not forwarded; use `shell` for interactive work.
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,8 @@ config means no proxy — credentials are forwarded into the guest exactly as
before.

Every golden image installs the Secret Service packages this mode needs
(`dbus-user-session`, `gnome-keyring`, `libsecret-tools`) regardless of the
(`dbus-user-session`, `gnome-keyring`, `libpam-gnome-keyring`,
`libpam0g-dev`, `libpam-systemd`, `python3`, `python3-dbus`, `libsecret-tools`) regardless of the
`auth` setting, because the image is built once and reused across configs —
gating them would let a later `auth = "chatgpt"` edit meet an image that cannot
serve it.
Expand Down
Loading