From a7b87de50e96c55f6298ca7cbb63d61381f97c6b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 17:23:33 -0700 Subject: [PATCH 1/2] quest: plan follow-ups from the 2026-10-24 merge wave Split the job sandbox into an epic of six children, and add quests for CI flakes, the review policy, landing rules, wait/status accuracy, merge holds, context paging, and four m1 follow-ups. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 2 + quest/m0/ci-flakes.md | 36 +++++++++++ quest/m0/helpers/README.md | 2 + quest/m0/helpers/landing.md | 41 +++++++++++++ quest/m0/helpers/wait-accuracy.md | 35 +++++++++++ quest/m0/kix/README.md | 12 +++- quest/m0/kix/context-pages.md | 20 +++++++ quest/m0/kix/execution.md | 2 +- quest/m0/kix/merge-holds.md | 25 ++++++++ .../m0/kix/{sandbox.md => sandbox/README.md} | 60 ++++++++----------- quest/m0/kix/sandbox/apparmor.md | 19 ++++++ quest/m0/kix/sandbox/claude-pin.md | 19 ++++++ quest/m0/kix/sandbox/macos.md | 21 +++++++ quest/m0/kix/sandbox/opencode.md | 25 ++++++++ quest/m0/kix/sandbox/trust.md | 23 +++++++ quest/m0/kix/sandbox/vm.md | 20 +++++++ quest/m0/review-policy.md | 26 ++++++++ quest/m1/README.md | 4 ++ quest/m1/cloud/funding.md | 2 +- quest/m1/donor-isolation.md | 2 +- quest/m1/inbox-polish.md | 26 ++++++++ quest/m1/merge-jobs.md | 24 ++++++++ quest/m1/resolve-tuning.md | 20 +++++++ quest/m1/worker-dev.md | 26 ++++++++ 24 files changed, 452 insertions(+), 40 deletions(-) create mode 100644 quest/m0/ci-flakes.md create mode 100644 quest/m0/helpers/landing.md create mode 100644 quest/m0/helpers/wait-accuracy.md create mode 100644 quest/m0/kix/context-pages.md create mode 100644 quest/m0/kix/merge-holds.md rename quest/m0/kix/{sandbox.md => sandbox/README.md} (57%) create mode 100644 quest/m0/kix/sandbox/apparmor.md create mode 100644 quest/m0/kix/sandbox/claude-pin.md create mode 100644 quest/m0/kix/sandbox/macos.md create mode 100644 quest/m0/kix/sandbox/opencode.md create mode 100644 quest/m0/kix/sandbox/trust.md create mode 100644 quest/m0/kix/sandbox/vm.md create mode 100644 quest/m0/review-policy.md create mode 100644 quest/m1/inbox-polish.md create mode 100644 quest/m1/merge-jobs.md create mode 100644 quest/m1/resolve-tuning.md create mode 100644 quest/m1/worker-dev.md diff --git a/quest/m0/README.md b/quest/m0/README.md index 63ec49b0..3f8be31e 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -35,6 +35,8 @@ ownership-manifest design: ## Required +- [CI flakes](/quest/m0/ci-flakes.md) - native Windows tests and Release's macOS checks fail only on real breakage +- [Review policy](/quest/m0/review-policy.md) - CONTRIBUTING.md says whose review counts - [Gate bench](/quest/m0/gate-bench.md) - measure headroom admission on an idle host - [Agent lifecycle integration](/quest/m0/worktree-agents.md) - reclaim completed and suspended checkouts through their harness - [Agent workflow helpers](/quest/m0/helpers/README.md) - CLI commands for CI waits, PR status, claims, quest moves, and serialized merges diff --git a/quest/m0/ci-flakes.md b/quest/m0/ci-flakes.md new file mode 100644 index 00000000..5cfc8b17 --- /dev/null +++ b/quest/m0/ci-flakes.md @@ -0,0 +1,36 @@ +# [M] Fail CI checks only on real breakage + +## Goal + +The native Windows test jobs and Release's macOS checks fail only when the +code under test is broken: no admission-count, timeout, or runner-queue +failure on a change that does not touch the code it exercises. + +## Plan + +Decided 2026-10-24 (user) from the spawn and merge wave that landed +[kixelated/quest#202](https://github.com/kixelated/quest/pull/202) through +[#219](https://github.com/kixelated/quest/pull/219), where native Windows +unit tests caused most failed Check runs, including on `main` (82a9566): + +- **Gate headroom test (user).** `test/gate.test.ts` "concurrent processes + never admit past the headroom" races 12 callers for 4 slots before a + deadline of `SETTLE_MS/2` and asserts exactly 4 were admitted. Slow + `windows-11-arm` and `windows-2022` runners, and loaded local hosts, admit + 2 or 3; the gate never admitted more than its headroom. Assert that + invariant (never more than the headroom, in the output and the ledger) and + that every caller is eventually admitted, rather than an exact count by a + fixed deadline. Rejected: treating under-admission on a slow host as a gate + bug. +- **Timeouts.** Find why `test/move.test.ts` ("refuses a claimed quest", + "reports mentions it does not edit"), several `test/start.test.ts` tests, + and `test/landed.test.ts` exceed vitest's 5 s on native Windows, and fix + that cause. No raised timeouts, retries, or sleeps. +- **Release's macOS queue (user).** Release's macOS `check` jobs can wait + out a GitHub macOS capacity notice and show as a failed check on a PR, as + the `lifecycle (macos-14)` job did before + [kixelated/quest#218](https://github.com/kixelated/quest/pull/218). Apply + the same decision: keep macOS covered on pushes to `main` and where it + guards release artifacts, without failing PRs on a runner that never + arrived. +- Reproduce each on its native runner before fixing, as #218 did. diff --git a/quest/m0/helpers/README.md b/quest/m0/helpers/README.md index 7fe550ce..9ba42114 100644 --- a/quest/m0/helpers/README.md +++ b/quest/m0/helpers/README.md @@ -57,6 +57,8 @@ commands from Sep-Oct. Session counts in each child come from that window. ## Required +- [Landing rules](/quest/m0/helpers/landing.md) - land only heads that contain the current base, ask on a missing review, one design checkpoint +- [Wait and status accuracy](/quest/m0/helpers/wait-accuracy.md) - wake on edited bot comments, honest `checks:passed`, review rounds in `quest status` - [Status holds](/quest/m0/helpers/status-holds.md) - `quest status` shows a PR's `quest:*` hold - [Headless agents](/quest/m0/helpers/headless-agents.md) - `/quest-spawn api` launches, resumes, and stops billed agents through one command auto mode allows diff --git a/quest/m0/helpers/landing.md b/quest/m0/helpers/landing.md new file mode 100644 index 00000000..018f047f --- /dev/null +++ b/quest/m0/helpers/landing.md @@ -0,0 +1,41 @@ +# [S] Land only heads tested against the current base + +## Goal + +A PR lands only on a head that contains the base branch as it is at landing +time, and the merge skill stops for the user, rather than waiting again, when +a required review of a changed head does not arrive. The merge skill, iterate, +and complete share one definition of the design checkpoint. + +## Plan + +Decided 2026-10-24 (user) from the merge wave that landed +[kixelated/quest#205](https://github.com/kixelated/quest/pull/205) through +[#219](https://github.com/kixelated/quest/pull/219). The user asked for these +skill and guide edits through this plan. + +- **Refuse a stale head (user).** GitHub reported + [kixelated/quest#213](https://github.com/kixelated/quest/pull/213) clean, + but landing it would have broken a test #205 had just added. `quest merge + --land ` refuses when the head does not contain the base branch's + current tip, naming both. The merge skill then merges the base (never + rebasing), reruns the repository's checks, and lands the new head; a + conflict-free base merge still needs no new review. Rejected: skill prose + alone, and GitHub's "require branches to be up to date" protection. + GitHub merge queues need an organization-owned repository, which + kixelated/quest is not. +- **Ask on a missing review.** `assets/skills/merge.md` says to rerun + `quest wait` with `--since` after a timeout. When the wait is for a + required review of a changed head, ask the user instead, as the guide's + Reviews section already says + ([kixelated/quest#206](https://github.com/kixelated/quest/pull/206)). +- **One checkpoint.** Move the every-fourth-round design checkpoint that + [kixelated/quest#207](https://github.com/kixelated/quest/pull/207) wrote + into both `merge` and `iterate` into the guide's Reviews section, and point + `merge`, `iterate`, and `complete` at it. +- Test a stale head refused, the same head landing after a base merge, and a + head the base already contains. + +## Related + +- [Wait and status accuracy](/quest/m0/helpers/wait-accuracy.md) - the review-round count the checkpoint reads diff --git a/quest/m0/helpers/wait-accuracy.md b/quest/m0/helpers/wait-accuracy.md new file mode 100644 index 00000000..b5abeab2 --- /dev/null +++ b/quest/m0/helpers/wait-accuracy.md @@ -0,0 +1,35 @@ +# [S] Report reviews and checks accurately in wait and status + +## Goal + +`quest wait` wakes when a bot edits its review-status comment and never +reports a failed check under `checks:passed`, and `quest status` prints how +many review rounds a PR has had since its last `Design change:` comment. + +## Plan + +Decided 2026-10-24 (user) from the merge wave that landed +[kixelated/quest#205](https://github.com/kixelated/quest/pull/205) through +[#219](https://github.com/kixelated/quest/pull/219): + +- **Edited bot comments.** Codex reports a finished or stuck review by + editing its summary comment. `quest wait` did not wake on those edits, so + agents on [#213](https://github.com/kixelated/quest/pull/213) and + [#219](https://github.com/kixelated/quest/pull/219) read the comment by + hand after each wait. Report an edit as an event. +- **Honest `checks:passed`.** On + [#207](https://github.com/kixelated/quest/pull/207), `quest wait` printed + `checks:passed native_Windows_(windows-11-arm)` six seconds after that + optional job failed: the event covers the required checks but is labelled + with the last check to finish. Name what passed, and report a failed + optional check separately. +- **Review rounds.** `quest status` prints the number of pushed heads that + drew reviews since the last `Design change:` comment, which the merge + skill's design checkpoint counts today with `gh pr view --json`. +- Dropped (user): removing the `quest:merge` label after a PR merges; holds + only count on open PRs, so the leftover label is cosmetic. + +## Related + +- [Landing rules](/quest/m0/helpers/landing.md) - the checkpoint that reads the round count +- [Status holds](/quest/m0/helpers/status-holds.md) - also extends `quest status` diff --git a/quest/m0/kix/README.md b/quest/m0/kix/README.md index 44f7d2f5..982a0c65 100644 --- a/quest/m0/kix/README.md +++ b/quest/m0/kix/README.md @@ -19,7 +19,8 @@ connect a Linux and a macOS runner, one with two accounts, receive an issue noti answer planning prompts, publish a draft quest PR, merge through GitHub's rules, start a ready quest, follow its summaries, answer an implementation question from a push notification, take its runner offline so the job is -reassigned to the other, and receive its contribution PR notification. Exercise duplicate events, +reassigned to the other, receive its contribution PR notification, and pick +merge on it while it is not yet clean, so a merge job fixes and lands it. Exercise duplicate events, interrupted sessions, and an exhausted account with stubbed agents, starting from the seeded local server's fake GitHub and seed (`cloudflare/dev/`), before a live rehearsal on the operator's runner. @@ -114,12 +115,19 @@ fixes what it finds: `account_rate_limit` retry, and ChatGPT's `usage_limit_reached` error with `resets_at`. If ChatGPT plans fall short through opencode, plan the Codex adapter the 2026-10-15 decision allows. +- A merge job's publication refuses a merge commit that changes + `.github/workflows` relative to the head it updates, which + [kixelated/quest#209](https://github.com/kixelated/quest/pull/209) assumes + is the comparison GitHub makes. Try it with a real PR that edits a + workflow (decided 2026-10-24, user). ## Required +- [Context pages](/quest/m0/kix/context-pages.md) - a job's `context` read returns every comment, review, and check of a busy PR +- [Merge holds](/quest/m0/kix/merge-holds.md) - ended merge jobs never leave a `quest:merge` hold, through one shared hold rule - [Deploy kix.quest](/quest/m0/kix/deploy.md) - the operator registers the App and deploys the Worker with GitHub sign-in at kix.quest - [Operator setup](/quest/m0/kix/operator-setup.md) - fork namespace, pilot installations, and a verified test PR - [Per-project forks](/quest/m0/kix/forks.md) - each project publishes through its own fork, recorded by repository ID -- [Job sandbox](/quest/m0/kix/sandbox.md) - untrusted PR devShells built in a VM on Linux runners, maintainer PR heads trusted, opencode sandboxed, and macOS verified +- [Job sandbox](/quest/m0/kix/sandbox/README.md) - untrusted PR devShells built in a VM on Linux runners, maintainer PR heads trusted, opencode sandboxed, and macOS verified - [Web execution](/quest/m0/kix/execution.md) - spawn and iterate through the UI, ending in contribution PRs - [Onboarding guide](/quest/m0/kix/guide.md) - a complete maintainer path, from runner setup through a reviewed PR diff --git a/quest/m0/kix/context-pages.md b/quest/m0/kix/context-pages.md new file mode 100644 index 00000000..ad9fd4b6 --- /dev/null +++ b/quest/m0/kix/context-pages.md @@ -0,0 +1,20 @@ +# [S] Read every page of a PR's context + +## Goal + +The runner's `context` tool returns all of a pull request's comments, +reviews, review-thread comments, and check contexts, however busy the PR is. + +## Plan + +Decided 2026-10-24 (user) as a follow-up of +[kixelated/quest#210](https://github.com/kixelated/quest/pull/210), which +paged review threads in `cloudflare/src/github/service.ts`: + +- The same query still caps the first 50 comments in each thread, the last + 100 PR comments, the last 50 reviews, and 100 check contexts, so a busy PR + silently drops the rest. Follow each connection's `pageInfo` cursor, and + raise an error on a missing or repeated cursor instead of looping, as #210 + does. +- Test each list past its page size with the seeded fake GitHub + (`cloudflare/dev/github.ts`). diff --git a/quest/m0/kix/execution.md b/quest/m0/kix/execution.md index 3ca0dab5..77c9654a 100644 --- a/quest/m0/kix/execution.md +++ b/quest/m0/kix/execution.md @@ -68,4 +68,4 @@ Earlier guidance: ## Required -- [Job sandbox](/quest/m0/kix/sandbox.md) - safe iteration on PRs with contributors' commits +- [Job sandbox](/quest/m0/kix/sandbox/README.md) - safe iteration on PRs with contributors' commits diff --git a/quest/m0/kix/merge-holds.md b/quest/m0/kix/merge-holds.md new file mode 100644 index 00000000..75574394 --- /dev/null +++ b/quest/m0/kix/merge-holds.md @@ -0,0 +1,25 @@ +# [S] Release merge holds reliably + +## Goal + +No PR keeps a `quest:merge` hold for a merge job that has ended, whatever path +ended it, and the CLI and the Worker read and apply `quest:*` holds through +one shared definition. + +## Plan + +Decided 2026-10-24 (user) as a follow-up of +[kixelated/quest#209](https://github.com/kixelated/quest/pull/209), whose +reviews found a missed release path in each of four rounds (a stopped attempt +that never reported, dispatch before the hold was recorded, a cancel racing +the label): + +- **Sweep.** A scheduled pass releases the hold of every merge job that has + ended without a landing in progress, as a backstop for any release path the + job's own lifecycle misses. +- **One definition.** `src/cli/hold.ts` and the Worker each decide which + labels are holds and when a hold is spent. Move that rule into the core + (`quest/core`) and have both call it, as `landed()` did for merged claims + in [kixelated/quest#217](https://github.com/kixelated/quest/pull/217). +- Test a hold left by each path #209 fixed, released by the sweep, and a + live job's hold left alone. diff --git a/quest/m0/kix/sandbox.md b/quest/m0/kix/sandbox/README.md similarity index 57% rename from quest/m0/kix/sandbox.md rename to quest/m0/kix/sandbox/README.md index 1ed340e6..8d5176e5 100644 --- a/quest/m0/kix/sandbox.md +++ b/quest/m0/kix/sandbox/README.md @@ -1,4 +1,4 @@ -# [L] Run untrusted PR heads on sandboxed runners +# Run untrusted PR heads on sandboxed runners ## Goal @@ -21,26 +21,19 @@ Decided 2026-10-15, from Codex's review of kixelated/quest#121: - OS sandbox, not a VM per job: bubblewrap on Linux and Seatbelt on macOS. Claude Code's docs say this does not replace a container or VM; the README documents that residual risk. -- Job coordination marks every `iterate` job untrusted and routes it only to - Linux runners that register with `vm: true`. Classify PR heads whose commits - all come from maintainers or kix jobs as trusted here. -- Untrusted heads: evaluate and build the PR's own devShell in a per-job - KVM microVM on a Linux runner, then bring the closure into the host store - and run the job's commands in the OS sandbox. Verify how to import unsigned - paths without making the runner trust arbitrary builders, and never - publish them to the shared cache. The runner refuses untrusted jobs until - then. -- macOS runners take trusted revisions only, since a Linux VM cannot build a - darwin devShell. An untrusted-head job waits with "needs a Linux runner" - while none is online. Cloud runners are Linux-only. +- Untrusted heads build their devShell in a per-job VM on a Linux runner + before their commands run in the OS sandbox. macOS runners take trusted + revisions only, since a Linux VM cannot build a darwin devShell. An + untrusted-head job waits with "needs a Linux runner" while none is online. + Cloud runners are Linux-only. - Decided 2026-10-22 on [kixelated/quest#188](https://github.com/kixelated/quest/pull/188), from Codex's security review: a job resumes with an answer from a maintainer other than the runner's owner (the job's requester) only after the owner approves it in the inbox, since the agent would act on that text with the owner's machine. kix holds the job and the runner refuses an answer - without `approvedAt`. The sandbox lifts this gate: remove the approval step - from kix, the inbox, and the runner. + without `approvedAt`. The sandbox lifts this gate once it is verified on + macOS. Shipped in [kixelated/quest#212](https://github.com/kixelated/quest/pull/212) (`src/runner/sandbox.ts`), and verified live on Linux with Claude Code @@ -65,26 +58,23 @@ Shipped in [kixelated/quest#212](https://github.com/kixelated/quest/pull/212) cannot run their checks without installing dependencies. An untrusted revision's commands reach no network host. The answer-approval gate stays until the sandbox is verified on macOS. - -Remaining work: - -- opencode has no native sandbox, and its jobs' commands still run unconfined - on the host (in the revision's devShell, with the attempt's private HOME). - Decided 2026-10-15: run `opencode serve` under Anthropic's sandbox-runtime - with the runner's `Sandbox` policy (`src/runner/sandbox.ts`). The runner - reaches the server on localhost, and opencode needs its providers' hosts, - so check both under sandbox-runtime's network isolation. Keep the account's - login out of what the commands can read: verify how much of its XDG home the - wrapped process exposes, and deny reads of every other account home. - Commands also inherit the per-turn `OPENCODE_SERVER_PASSWORD`, and the kix - tools' bearer token in `OPENCODE_CONFIG_CONTENT`; keep both out of reach. - -- On macOS, sandboxed commands cannot reach the account's Keychain item - (`security find-generic-password`), `/private/var/folders`, or another - home, and the realpath'd `/private/tmp` directory works as `TMPDIR`. -- On Ubuntu 23.10 and later, unprivileged user namespaces need an AppArmor - profile for the runner; document one and fail fast when the sandbox cannot - start, instead of failing each job. +- Decided 2026-10-24 (user) on the same PR: opencode jobs keep running + their commands unconfined, as before the sandbox, until + [opencode sandboxing](/quest/m0/kix/sandbox/opencode.md) ships. The README + says so. +- Decided 2026-10-24 (user) in `/quest-plan`: split the remaining work into + the children below, ranked so the earliest unblock the most. Dropped + (user): read-only registry access, which Claude Code's sandbox cannot + filter by request today, and a cancellable `Workspace.prepare` fetch. - Sources: [Claude Code sandboxing](https://code.claude.com/docs/en/sandboxing) and [sandbox-runtime](https://www.npmjs.com/package/@anthropic-ai/sandbox-runtime). + +## Required + +- [Claude Code pin](/quest/m0/kix/sandbox/claude-pin.md) - the runner's pinned Claude Code moves to the version the sandbox docs describe +- [Trusted PR heads](/quest/m0/kix/sandbox/trust.md) - PR heads whose commits all come from maintainers or kix jobs run on any runner +- [opencode sandboxing](/quest/m0/kix/sandbox/opencode.md) - opencode jobs' commands run in the same sandbox as Claude Code's +- [macOS sandbox](/quest/m0/kix/sandbox/macos.md) - the boundary verified under Seatbelt, then the answer-approval gate removed +- [Ubuntu AppArmor](/quest/m0/kix/sandbox/apparmor.md) - the sandbox starts on stock Ubuntu, or the runner refuses at startup +- [Untrusted devShells](/quest/m0/kix/sandbox/vm.md) - untrusted PR heads build their devShell in a per-job VM diff --git a/quest/m0/kix/sandbox/apparmor.md b/quest/m0/kix/sandbox/apparmor.md new file mode 100644 index 00000000..908dea34 --- /dev/null +++ b/quest/m0/kix/sandbox/apparmor.md @@ -0,0 +1,19 @@ +# [S] Start the sandbox on stock Ubuntu + +## Goal + +A runner started from an ordinary terminal on Ubuntu 23.10 or later runs +sandboxed jobs, or refuses to start with an explanation, instead of failing +each job. + +## Plan + +Found on [kixelated/quest#212](https://github.com/kixelated/quest/pull/212): +Ubuntu's AppArmor restriction on unprivileged user namespaces made the system +`/usr/bin/bwrap` fail. The Nix-pinned bwrap worked only because that process +tree ran under the Claude desktop app's profile, which allows them. + +- Document an AppArmor profile for the runner's pinned bwrap. +- At startup, when a Claude Code account needs the sandbox, run a trivial + sandboxed command and refuse with the profile's install hint if it fails. +- Test on a stock Ubuntu image, from a normal login shell. diff --git a/quest/m0/kix/sandbox/claude-pin.md b/quest/m0/kix/sandbox/claude-pin.md new file mode 100644 index 00000000..774ca1b8 --- /dev/null +++ b/quest/m0/kix/sandbox/claude-pin.md @@ -0,0 +1,19 @@ +# [XS] Bump the runner's pinned Claude Code + +## Goal + +The runner's Nix closure pins Claude Code 2.1.305 or later instead of +2.1.291, and the sandbox's behaviour the README documents matches the pinned +version. + +## Plan + +Decided 2026-10-24 (user) as a follow-up of +[kixelated/quest#212](https://github.com/kixelated/quest/pull/212), whose live +boundary test passed on both 2.1.291 and 2.1.305: + +- Bump the pin in `flake.nix` (`QUEST_RUNNER_CLAUDE`) and rerun the runner + tests. Check the sandbox settings the runner passes (`dontAsk`, + `--setting-sources user`, the MCP tool timeout) against the new version's + docs and changelog. +- Ranked first in the epic: the later children build on the pinned version. diff --git a/quest/m0/kix/sandbox/macos.md b/quest/m0/kix/sandbox/macos.md new file mode 100644 index 00000000..4333b3d8 --- /dev/null +++ b/quest/m0/kix/sandbox/macos.md @@ -0,0 +1,21 @@ +# [M] Verify the sandbox on macOS, then lift the approval gate + +## Goal + +The job boundary holds on a macOS runner under Seatbelt, and once it does, a +job resumes with another maintainer's answer without the runner owner's +approval: the approval step is gone from kix, the inbox, and the runner. + +## Plan + +- Rerun the hostile-repository boundary test from + [kixelated/quest#212](https://github.com/kixelated/quest/pull/212) on + macOS. Sandboxed commands must not reach the account's Keychain item + (`security find-generic-password`), `/private/var/folders`, or another + home, and the realpath'd `/private/tmp` directory must work as `TMPDIR`. +- Decided 2026-10-24 (user) on #212: the answer-approval gate stays until this + verification passes. Then remove `approvedAt` handling from the runner, the + approval decision from the inbox, and the hold from kix, and update the + "until the job sandbox ships" comments in `cloudflare/README.md`, + `cloudflare/src/jobs/routes.ts`, and `store.ts`. +- If the boundary does not hold, record what leaks here and keep the gate. diff --git a/quest/m0/kix/sandbox/opencode.md b/quest/m0/kix/sandbox/opencode.md new file mode 100644 index 00000000..fe913fbd --- /dev/null +++ b/quest/m0/kix/sandbox/opencode.md @@ -0,0 +1,25 @@ +# [M] Sandbox opencode jobs' commands + +## Goal + +opencode jobs' commands run inside the same boundary as Claude Code's: writes +only to the checkout and the attempt's private directory, network only to +the policy's hosts, and no read access to account logins or other homes. + +## Plan + +Decided 2026-10-15: opencode has no native sandbox, so run `opencode serve` +under Anthropic's sandbox-runtime with the runner's `Sandbox` policy +(`src/runner/sandbox.ts`). Today its commands run unconfined on the host, in +the revision's devShell with the attempt's private HOME. + +- The runner reaches the server on localhost, and opencode needs its + providers' hosts, so check both under sandbox-runtime's network isolation. +- Keep the account's login out of what the commands can read: verify how + much of its XDG home the wrapped process exposes, and deny reads of every + other account home. +- Commands inherit the per-turn `OPENCODE_SERVER_PASSWORD`, and the kix + tools' bearer token in `OPENCODE_CONFIG_CONTENT`; keep both out of reach. +- Rerun the hostile-repository boundary test from + [kixelated/quest#212](https://github.com/kixelated/quest/pull/212) against + an opencode account, and update the README's residual-risk note. diff --git a/quest/m0/kix/sandbox/trust.md b/quest/m0/kix/sandbox/trust.md new file mode 100644 index 00000000..7b72ecb5 --- /dev/null +++ b/quest/m0/kix/sandbox/trust.md @@ -0,0 +1,23 @@ +# [M] Run maintainers' PR heads on any runner + +## Goal + +kix classifies a PR head whose commits all come from the project's +maintainers or kix jobs as trusted, so its iterate and merge jobs run on any +runner, in the same sandbox as other trusted revisions. Any other PR head is +untrusted and routes only to Linux runners that register with `vm: true`. + +## Plan + +- Decided 2026-10-15 (from Codex's review of kixelated/quest#121): job + coordination marks every `iterate` job untrusted and routes it only to + Linux runners with `vm: true`, unless every commit on the head comes from a + maintainer or a kix job. +- Classify by commit authorship as GitHub reports it, checked against the + project's current maintainers when the job is created and again when it is + assigned, so a head that gains a contributor's commit is reclassified. +- The runner keeps refusing untrusted jobs until + [untrusted devShells](/quest/m0/kix/sandbox/vm.md) ship, so until then an + untrusted head's job waits with "needs a Linux runner". +- Test a maintainer-only head, a head with one contributor commit, a head + that gains one after the job is queued, and a revoked maintainer's commit. diff --git a/quest/m0/kix/sandbox/vm.md b/quest/m0/kix/sandbox/vm.md new file mode 100644 index 00000000..83dafdff --- /dev/null +++ b/quest/m0/kix/sandbox/vm.md @@ -0,0 +1,20 @@ +# [L] Build untrusted PR devShells in a VM + +## Goal + +A Linux runner registered with `vm: true` accepts untrusted PR heads: it +evaluates and builds the PR's own devShell in a per-job KVM microVM, brings +the closure into the host store, and runs the job's commands in the OS +sandbox with no network. + +## Plan + +- Decided 2026-10-15: a VM per untrusted build, not per job; the commands + still run in the OS sandbox afterwards. +- Verify how to import unsigned store paths without making the runner trust + arbitrary builders, and never publish them to the + [shared cache](/quest/m1/automation/cache.md). +- The runner refuses untrusted jobs until this ships; + [trusted PR heads](/quest/m0/kix/sandbox/trust.md) route them here. +- Test a PR whose flake or shellHook tries to run on the host, and one whose + build tries to reach the network. diff --git a/quest/m0/review-policy.md b/quest/m0/review-policy.md new file mode 100644 index 00000000..7ca1e5d1 --- /dev/null +++ b/quest/m0/review-policy.md @@ -0,0 +1,26 @@ +# [XS] Write down whose review counts + +## Goal + +`CONTRIBUTING.md` states which review satisfies the guide's required review +of a PR head in this repository, so merge agents stop deciding it from +precedent. + +## Plan + +Decided 2026-10-24 (user) in `/quest-plan`, after a merge wave in which agents +counted Codex reactions and status comments as reviews and landed Dependabot +bumps with none. The user asked for this `CONTRIBUTING.md` edit through this +plan. + +- **Posted reviews only (user).** A Codex review counts when Codex posts a + GitHub review, or a 👍 reaction, on the exact head being landed, with no + unresolved findings. A status comment that says a review completed does + not count; with nothing posted, the agent asks the maintainer, as + [kixelated/quest#209](https://github.com/kixelated/quest/pull/209) did. + Rejected: counting completed status comments, and requiring the + maintainer's review after a hand-resolved conflict. +- Dependabot PRs that only bump patch versions or a lockfile need no review. +- The guide's narrower exemptions for changes since the last review (a + conflict-free base merge, or a reviewer's suggested patch applied + unchanged) still apply. diff --git a/quest/m1/README.md b/quest/m1/README.md index a9e7775d..801364ba 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -15,5 +15,9 @@ additional login providers. This milestone does not gate the first release. - [Donor isolation](/quest/m1/donor-isolation.md) - stronger boundaries for untrusted shared jobs - [ChatGPT hosted runs](/quest/m1/chatgpt-hosted.md) - conditional on provider approval and current terms - [ChatGPT approval](/quest/m1/chatgpt-waitlist.md) - resolve hosted-use approval before implementation +- [Merge-job extensions](/quest/m1/merge-jobs.md) - submodule pointers, continuing from a moved head, and webhook wakeups +- [Live inbox](/quest/m1/inbox-polish.md) - live agent cards, active quests on the log, runner choice for every job, expiring offers +- [Worker dev setup](/quest/m1/worker-dev.md) - per-server cookies, seeded raw output, one fake GitHub, each test run alone +- [Merge driver tuning](/quest/m1/resolve-tuning.md) - measure `quest resolve` on moq's history and match git's markers - [Batched inbox reads](/quest/m1/inbox-reads.md) - a fixed number of D1 queries per project on each inbox load - [Incremental history](/quest/m1/history-pass.md) - each history pass reads only what changed since the last diff --git a/quest/m1/cloud/funding.md b/quest/m1/cloud/funding.md index 08c029e0..cd37ce20 100644 --- a/quest/m1/cloud/funding.md +++ b/quest/m1/cloud/funding.md @@ -53,7 +53,7 @@ cloud runners; donor allocations follow working cloud runners separately. ## Required -- [Job sandbox](/quest/m0/kix/sandbox.md) - the boundary keeping provider credentials outside job commands +- [Job sandbox](/quest/m0/kix/sandbox/README.md) - the boundary keeping provider credentials outside job commands ## Related diff --git a/quest/m1/donor-isolation.md b/quest/m1/donor-isolation.md index 91a574b3..c3bc1d25 100644 --- a/quest/m1/donor-isolation.md +++ b/quest/m1/donor-isolation.md @@ -7,7 +7,7 @@ with constrained network and filesystem access, without exposing host secrets. ## Plan -- m0 [sandboxes](/quest/m0/kix/sandbox.md) job commands at the OS level and +- m0 [sandboxes](/quest/m0/kix/sandbox/README.md) job commands at the OS level and builds untrusted devShells in a VM. This quest adds a stronger whole-job boundary for shared runners running other people's projects. Nix devShells give reproducibility, not an isolation boundary for arbitrary commands. diff --git a/quest/m1/inbox-polish.md b/quest/m1/inbox-polish.md new file mode 100644 index 00000000..a81d77a8 --- /dev/null +++ b/quest/m1/inbox-polish.md @@ -0,0 +1,26 @@ +# [M] Keep the inbox live and current + +## Goal + +The inbox updates agent cards as summaries arrive, the quest log shows which +quests have active jobs, every job type offers the runner and account choice, +and old "plan these?" offers expire. + +## Plan + +Decided 2026-10-24 (user) as follow-ups of +[kixelated/quest#215](https://github.com/kixelated/quest/pull/215) and +[kixelated/quest#205](https://github.com/kixelated/quest/pull/205), ranked +after launch: + +- **Live cards.** Cards refresh only on page load. Push summaries from the + runner hub to open inbox pages, over the existing per-owner connection or + Server-Sent Events, keeping summaries escaped and raw output behind its + maintainer-only disclosure. +- **Active quests.** The quest log shows a quest as ready while a job is + queued or running on it; show the job's state instead, from the same card + data. +- **Runner choice everywhere.** The folded runner and account control exists + only on the new plan box; offer it when starting or iterating work too. +- **Offer expiry.** "Plan these?" offers are capped at the newest 50 per + project; expire them by age instead. diff --git a/quest/m1/merge-jobs.md b/quest/m1/merge-jobs.md new file mode 100644 index 00000000..218ec98d --- /dev/null +++ b/quest/m1/merge-jobs.md @@ -0,0 +1,24 @@ +# [M] Extend merge jobs + +## Goal + +Merge jobs handle a PR that changes submodule pointers, continue after +someone else moves the head, and wake on GitHub events instead of polling. + +## Plan + +Decided 2026-10-24 (user) as follow-ups of +[kixelated/quest#209](https://github.com/kixelated/quest/pull/209), ranked +after launch: + +- **Submodule pointers.** #209 refuses a merge publication whose snapshot + skipped a submodule change, since kix builds the merge commit's tree from + the base and would revert the PR's pointer. Carry the PR's existing + pointers into the merge tree instead, which means publishing submodule + pointers kix avoids today. +- **Moved heads.** A head moved by anyone but the job pauses it into a + decision that needs a new pick. Let the maintainer continue from the new + head in that decision. +- **Webhook wakeups.** The runner's `wait` tool polls every minute for up to + 55 minutes. Wake a waiting job from the `pull_request`, review, and check + webhooks kix already receives. diff --git a/quest/m1/resolve-tuning.md b/quest/m1/resolve-tuning.md new file mode 100644 index 00000000..9145d17f --- /dev/null +++ b/quest/m1/resolve-tuning.md @@ -0,0 +1,20 @@ +# [S] Measure and refine the quest merge driver + +## Goal + +The `quest resolve` merge driver's real effect is measured on moq-dev/moq's +merge history, and its unresolved conflict markers match git's own labels +and marker size. + +## Plan + +Decided 2026-10-24 (user) as follow-ups of +[kixelated/quest#219](https://github.com/kixelated/quest/pull/219), ranked +after launch: + +- **Replay.** Rerun the plan's replay of 280 merges of `main` into PR + branches in moq-dev/moq through the driver, and record how many of the 29 + quest-only conflicts it resolves and which shapes still conflict. +- **Marker labels.** Register the driver with git's `%L`, `%X`, and `%Y` and + pass them to `git merge-file`, so markers carry git's branch labels and + marker size. diff --git a/quest/m1/worker-dev.md b/quest/m1/worker-dev.md new file mode 100644 index 00000000..66bf9bb8 --- /dev/null +++ b/quest/m1/worker-dev.md @@ -0,0 +1,26 @@ +# [S] Tighten the Worker's local and test setup + +## Goal + +Parallel agents can run seeded local servers without signing each other out, +the seed exercises every inbox section, and every Worker test uses the shared +fake GitHub and stays independent of the others. + +## Plan + +Decided 2026-10-24 (user) as follow-ups of +[kixelated/quest#213](https://github.com/kixelated/quest/pull/213), +[#208](https://github.com/kixelated/quest/pull/208), and +[#215](https://github.com/kixelated/quest/pull/215), ranked after launch: + +- **Session cookies.** Seeded servers on different localhost ports overwrite + each other's session cookie, causing false "Sign in required" errors. Give + each dev server its own cookie name or host. +- **Seeded raw output.** Seed raw output in `cloudflare/dev/seed.ts` so the + agent cards' "Raw output" disclosure has something to show. +- **Shared fake.** The `projects`, `inbox`, `jobs`, and `push` Worker tests + still mock `fetch` themselves; move them onto `cloudflare/dev/github.ts` + where it models their flows. +- **Each test alone.** #208's global reset clears storage but not + module-level state. Add a scheduled CI job that runs each Worker test case + in its own process, which is too slow for every PR. From 5abf4817e98bc882a30eeb692e7005266c165eea Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 17:31:15 -0700 Subject: [PATCH 2/2] quest: trust PR heads by authenticated provenance, not commit authorship Co-Authored-By: Claude Opus 5.5 --- quest/m0/kix/sandbox/trust.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/quest/m0/kix/sandbox/trust.md b/quest/m0/kix/sandbox/trust.md index 7b72ecb5..6cb880a3 100644 --- a/quest/m0/kix/sandbox/trust.md +++ b/quest/m0/kix/sandbox/trust.md @@ -13,11 +13,21 @@ untrusted and routes only to Linux runners that register with `vm: true`. coordination marks every `iterate` job untrusted and routes it only to Linux runners with `vm: true`, unless every commit on the head comes from a maintainer or a kix job. -- Classify by commit authorship as GitHub reports it, checked against the - project's current maintainers when the job is created and again when it is - assigned, so a head that gains a contributor's commit is reclassified. +- Classify by authenticated provenance, never by commit authorship: GitHub + links a commit to an account by its author email, which a contributor can + forge (from Codex's review of + [kixelated/quest#220](https://github.com/kixelated/quest/pull/220)). A + commit comes from a maintainer only when GitHub reports its signature + verified for that maintainer's account, and from a kix job only when kix + recorded that job pushing it. Any other commit, including a maintainer's + unsigned one, makes the head untrusted. +- Check against the project's current maintainers when the job is created + and again when it is assigned, so a head that gains a contributor's commit + is reclassified. - The runner keeps refusing untrusted jobs until [untrusted devShells](/quest/m0/kix/sandbox/vm.md) ship, so until then an untrusted head's job waits with "needs a Linux runner". - Test a maintainer-only head, a head with one contributor commit, a head - that gains one after the job is queued, and a revoked maintainer's commit. + that gains one after the job is queued, a revoked maintainer's commit, an + unsigned maintainer commit, and a contributor commit forging a + maintainer's author email.