diff --git a/quest/m0/agent-test-env.md b/quest/m0/agent-test-env.md index f059253e..6dddd1e6 100644 --- a/quest/m0/agent-test-env.md +++ b/quest/m0/agent-test-env.md @@ -1,10 +1,12 @@ -# [XS] Keep agent tests independent of the caller's agent +# [XS] Keep tests independent of the caller's environment ## Goal -`just test` passes inside a Claude Code or Codex session. `test/agent.test.ts` -clears the caller's agent variables (`CLAUDE_CONFIG_DIR`, `ANTHROPIC_*`, -`CODEX_*`) before it compares the environment it launches with. +`just test` passes inside a Claude Code or Codex session and under any user +git configuration. `test/agent.test.ts` clears the caller's agent variables +(`CLAUDE_CONFIG_DIR`, `QUEST_WORKTREE_HOME`, `ANTHROPIC_*`, `CODEX_*`) before +it compares the environment it launches with, and every git fixture turns off +commit signing. ## Plan @@ -12,3 +14,15 @@ Decided 2026-10-28 (user, /quest-plan of the follow-ups from a 25-PR /quest-comp before `just test` would pass, because the launched agent's expected environment picked it up. Decide whether the launch itself should strip any of these variables or only the test should. Name that choice in the PR. + +Decided 2026-10-30 (user, /quest-plan of the follow-ups from +[moq-dev/quest#292](https://github.com/moq-dev/quest/pull/292)): + +- Signing: + - ✅ Fold it into this quest + - Drop it +- `test/review.test.ts` commits its fixtures without `commit.gpgsign=false`, + unlike `move`, `snapshot`, `landed`, and `start`. Under a global SSH + signing config it fails with `cannot run ssh-keygen`, since the fixture's + PATH lacks it. Turn signing off there, and check the other fixtures for the + same gap. diff --git a/quest/m0/host-land/README.md b/quest/m0/host-land/README.md index 4ce65fc3..290f3fda 100644 --- a/quest/m0/host-land/README.md +++ b/quest/m0/host-land/README.md @@ -69,6 +69,13 @@ can hand off instead. fixes live wrong ejections until the host gate replaces that code; close [moq-dev/quest#277](https://github.com/moq-dev/quest/pull/277), since an ejection now removes `quest:merge` and the fixer adds it back. +- Security (user, 2026-10-30): the gate runs as a dedicated user under a + hardened unit. PR code and Codex run in the runner's sandbox with no + credentials. moq-bot tokens are minted per push and scoped to this + repository. Codex reaches its model through the host's CLIProxyAPI, so the + Codex login stays outside the sandbox. A maintainer's approval of the + pinned head covers evaluating its devShell, but the fixer may not touch + files that run outside the sandbox. - This epic ranks first in m0. It replaces the hook-started kix fixer that [moq-dev/quest#276](https://github.com/moq-dev/quest/pull/276) proposes (`kix/eject-fix`). @@ -80,6 +87,6 @@ can hand off instead. ## Required -- [moq-bot's App key](/quest/m0/host-land/bot-key.md) - condition: the maintainer places the key on the gate host +- [Gate host setup](/quest/m0/host-land/bot-key.md) - a dedicated user holding moq-bot's key and a CLIProxyAPI client key, unreachable from the sandbox - [Host daemon](/quest/m0/host-land/daemon.md) - `quest land serve` lands batches and ejects without bisecting - [Codex fixer](/quest/m0/host-land/fixer.md) - ejected batches get one mechanical fix attempt and healthy PRs are requeued diff --git a/quest/m0/host-land/bot-key.md b/quest/m0/host-land/bot-key.md index a53dba9e..352e5c59 100644 --- a/quest/m0/host-land/bot-key.md +++ b/quest/m0/host-land/bot-key.md @@ -1,11 +1,15 @@ -# [XS] moq-bot's App key on the gate host +# [S] The gate host's user, moq-bot key, and Codex access ## Goal -The maintainer's gate host can mint moq-bot installation tokens for -moq-dev/quest from moq-bot's App private key, kept readable only by the user -that runs the gate. Check it by minting a token there and reading the -repository with it. This waits on the maintainer placing the key. +The maintainer's gate host has a dedicated unprivileged system user that runs +the gate under a hardened systemd unit. Only that user can read moq-bot's App +private key, and nothing the gate sandboxes can read it or the maintainer's +Codex login; sandboxed Codex holds only the gate's revocable CLIProxyAPI +client key. Check it by minting a moq-bot token as that user and reading the +repository with it, and by failing to read the bot key from another user and +from inside the sandbox. Placing the bot key and issuing the client key +wait on the maintainer. ## Plan @@ -14,6 +18,24 @@ which `main`'s ruleset lets bypass the `quest land` check. Today moq-bot's token comes only from `token.moq.dev` through land.yml's GitHub ID token, which a host cannot present. -- Accept a key file path or a 1Password `op://` reference, as - `QUEST_API_KEYS` does. -- Never expose the key or a token to the sandboxed check. +- Isolation (user, 2026-10-30): + - ✅ A dedicated user plus the sandbox + - A microVM per batch + - A separate machine +- Store the keys as files the gate user alone can read (mode 0400), or as + 1Password `op://` references as `QUEST_API_KEYS` does. Never put the bot key + in a sandboxed process's reach. +- Harden the unit: `NoNewPrivileges`, `ProtectSystem=strict`, + `ProtectHome=yes` apart from the gate's own state directory, `PrivateTmp`, + and no other users' files. The gate still needs unprivileged user + namespaces for bubblewrap (see + [AppArmor](/quest/m0/kix/sandbox/apparmor.md) on Ubuntu). +- Codex reaches its model through the host's CLIProxyAPI service + (`cliproxyapi.service`, `127.0.0.1:8317`), which holds the maintainer's + Codex login in systemd credentials and bills the Codex plan's credits + (user, 2026-10-30). Proxy over a key in the sandbox: + - Capped API key, no git creds + - ✅ Key via proxy, through the CLIProxyAPI already running here +- The gate gets its own CLIProxyAPI client key, so it can be revoked + alone. The sandbox can see that client key; it reaches only the proxy, and + the login itself stays outside. diff --git a/quest/m0/host-land/daemon.md b/quest/m0/host-land/daemon.md index d2d85962..db970d91 100644 --- a/quest/m0/host-land/daemon.md +++ b/quest/m0/host-land/daemon.md @@ -19,9 +19,22 @@ Decided 2026-10-30 (user, /quest-plan); see the floor(log₂ n) for n queued PRs. A PR that conflicts with the chain is skipped and stays queued. Only the base's own conflict ejects it. - **Check.** Run the repository's check (`just check` and `just test` here) - on the tip through `quest gate`, in a sandbox holding no GitHub token, - `gh` config, SSH agent, or the bot key. Reuse the runner's sandbox where it - fits. + on the tip through `quest gate`, in the runner's sandbox + (`src/runner/sandbox.ts`): no home directories, GitHub token, `gh` config, + SSH agent, or keys, and network only to package registries. +- **Trust** (user, 2026-10-30). A maintainer approved the pinned head, so + the gate evaluates its devShell even when it has non-maintainer commits. + The pin is what makes that safe, so never check a head other than the + pinned one. + - Base devShell, untrusted flake changes handed back + - Refuse untrusted heads until the VM ships + - Wait for the VM + - ✅ Fine as is: a maintainer already approved the merge +- **Credentials.** Run as the gate's own user + ([setup](/quest/m0/host-land/bot-key.md)). Mint a moq-bot installation + token for each push, scoped to this repository with contents and + pull-request write, adding workflows write only when the batch touches + `.github/workflows`. - **Eject.** On a failing tip, eject every PR in the batch: disable auto-merge, remove `quest:merge`, and comment with the reason, keeping the hidden marker `quest status` reads. Keep the resolve-time ejections @@ -39,4 +52,4 @@ Decided 2026-10-30 (user, /quest-plan); see the ## Required -- [moq-bot's App key](/quest/m0/host-land/bot-key.md) - the host can push as moq-bot +- [Gate host setup](/quest/m0/host-land/bot-key.md) - the gate's user can push as moq-bot and reach Codex diff --git a/quest/m0/host-land/fixer.md b/quest/m0/host-land/fixer.md index 17b8e7d3..cf2942b7 100644 --- a/quest/m0/host-land/fixer.md +++ b/quest/m0/host-land/fixer.md @@ -28,8 +28,16 @@ Decided 2026-10-30 (user, /quest-plan); see the maintainer's last pick. - A flaky failure the fixer requeues becomes a suggested follow-up in its comment. -- Codex runs with the maintainer's own Codex login and a write credential - for pushing to the PR branch and requeueing, never the bot key. +- **Credentials** (user, 2026-10-30). Codex runs in the gate's sandbox with + no git or GitHub credential and network only to the host's CLIProxyAPI, + through the gate's client key ([setup](/quest/m0/host-land/bot-key.md)). + It commits locally. The daemon checks the commits against the rules here, + pushes them as moq-bot, and requeues. +- **Off-limits paths.** A maintainer's approval covers the pinned head, so + the gate may evaluate that head's devShell. The fixer's commits come after + the approval, so they may not touch what runs outside the sandbox: + `flake.nix`, `flake.lock`, `nix/`, `.envrc`, or `.github/`. A fix that + needs them is handed back, and the daemon refuses to push one that does. ## Required