From e542b698b1f75a6c2be09b4907552512d93b9a14 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:06:19 -0700 Subject: [PATCH 1/2] quest: plan the kix trust audit's read-only CI, head approval, and land review Co-Authored-By: Claude Opus 5.5 --- quest/m0/kix/README.md | 7 ++++ quest/m0/kix/ci-read-only.md | 53 +++++++++++++++++++++++++++ quest/m0/kix/land-review.md | 33 +++++++++++++++++ quest/m0/kix/sandbox/README.md | 1 + quest/m0/kix/sandbox/head-approval.md | 34 +++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 quest/m0/kix/ci-read-only.md create mode 100644 quest/m0/kix/land-review.md create mode 100644 quest/m0/kix/sandbox/head-approval.md diff --git a/quest/m0/kix/README.md b/quest/m0/kix/README.md index 5679ed7b..c047ace3 100644 --- a/quest/m0/kix/README.md +++ b/quest/m0/kix/README.md @@ -73,6 +73,11 @@ operator-paid-hosting decisions: keeps GitHub credentials off runners. It pushes job branches to the enrolled repository itself, with no service forks (decided 2026-10-27, user, in [Upstream branches](/quest/m0/kix/upstream-branches.md)). +- **Read-only CI, signed trust** (decided 2026-10-28, user, trust audit). + Enrolled repositories keep pull-request CI read-only instead of kix + shielding CI behind forks or per-push gates. Host trust comes from + maintainer signatures or an explicit approval of the exact head, never from + kix having published a commit. Landing needs a review of the exact head. - **Ready means eligible.** After a maintainer opts in, ready quests on the default branch may be implemented. Draft plans are not eligible. Project pause stops new work. An explicit spawn decision can prioritize ready work @@ -123,10 +128,12 @@ fixes what it finds: - [Deploy kix.quest](/quest/m0/kix/deploy.md) - the operator registers the App and deploys the Worker with GitHub sign-in at kix.quest - [Upstream branches](/quest/m0/kix/upstream-branches.md) - jobs publish on the enrolled repository itself, with no service forks +- [Read-only CI](/quest/m0/kix/ci-read-only.md) - enrolled repositories keep pull-request CI read-only, checked at enrollment, with every trust boundary written down - [Operator setup](/quest/m0/kix/operator-setup.md) - pilot installations and a verified test PR on kixelated/quest and moq-dev/moq - [App sign-in](/quest/m0/kix/app-sign-in.md) - one GitHub screen signs in and installs the App, with no separate OAuth app - [Pick repositories](/quest/m0/kix/pick-repos.md) - after installing, the maintainer enables repositories from a list - [Setup PR](/quest/m0/kix/setup-pr.md) - enrolling a repository without Quest makes the App open its `quest init` pull request - [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 +- [Land review](/quest/m0/kix/land-review.md) - kix lands only heads with a review that counts on that exact head - [Onboarding guide](/quest/m0/kix/guide.md) - a complete maintainer path, from runner setup through a reviewed PR diff --git a/quest/m0/kix/ci-read-only.md b/quest/m0/kix/ci-read-only.md new file mode 100644 index 00000000..a2defe48 --- /dev/null +++ b/quest/m0/kix/ci-read-only.md @@ -0,0 +1,53 @@ +# [M] Require read-only pull-request CI + +## Goal + +Every kix trust boundary is written down, and enrolled repositories keep +pull-request CI read-only. kix publishes job branches on the repository +itself, where GitHub gives a branch's workflows the repository's secrets and +default token. So code no maintainer has reviewed must meet only a read-only +`GITHUB_TOKEN` and no write credentials. kix checks this when a repository is +enrolled and warns the maintainer about what to fix. `cloudflare/GITHUB.md` +states the trust rule for each stage of a job. + +## Plan + +Decided 2026-10-28 (user, /quest-plan trust audit started from +kixelated/quest#246's Codex Security P1): + +- **Rule over gates.** Read-only CI replaces a service fork or a per-push + review gate as the answer to agent-written code reaching CI. Fork PRs + already get no secrets; same-repository branches need the repository's own + settings to match. The guide states the rule for agents + ([kixelated/quest#263](https://github.com/kixelated/quest/pull/263)). +- **Check and warn, don't refuse.** At enrollment, and on the project's + settings page, kix reports each problem it finds: + - the default workflow token can write; + - Actions secrets at repository level; + - an environment with secrets but no protected-branch deployment policy. + + Jobs still run. Check which App permission reads secret and environment + metadata; adding one is an operator step in `cloudflare/GITHUB.md`. +- **Runner claims are the owner's attestation.** Jobs go only to the + requester's own runners, so a runner's self-declared `vm` flag and its + request to mark a PR ready are its owner's statements. kix doesn't verify + them. Under read-only CI, a runner marking its PR ready starts only harmless + CI. +- Rewrite `cloudflare/GITHUB.md`'s trust section as a stage-by-stage table: + intake, job creation, routing, devShell, agent run, publication, CI, review, + and land. For each stage, name what is trusted and why. Fix the + contradiction between `cloudflare/README.md` ("route only to Linux runners + that declare VM builds") and the root README (every runner refuses untrusted + jobs). +- `SETUP.md` recommends the same settings when a repository adopts Quest, + beside its advice to skip CI on drafts. +- The pilot repositories pass the check before contributors are invited: + kixelated/quest's `land` environment was limited to `main` on 2026-10-28, + and moq-dev/moq tracks its fix in + [read-only secrets](https://github.com/moq-dev/moq/blob/main/quest/m0/ci/read-only-secrets.md). + +## Related + +- [Operator setup](/quest/m0/kix/operator-setup.md) - pilot repositories should pass this check +- [Land review](/quest/m0/kix/land-review.md) - the land stage's trust rule +- [Head approval](/quest/m0/kix/sandbox/head-approval.md) - the devShell stage's trust rule for heads without signatures diff --git a/quest/m0/kix/land-review.md b/quest/m0/kix/land-review.md new file mode 100644 index 00000000..37864102 --- /dev/null +++ b/quest/m0/kix/land-review.md @@ -0,0 +1,33 @@ +# [M] Land only reviewed heads + +## Goal + +kix lands a PR only when the exact head being landed has a review that counts +under the repository's review policy. This covers both an inbox merge and a +merge job's `land` publication. A maintainer's earlier merge pick alone never +lands a head that a runner published or changed since the last review. When +landing waits, kix says why. + +## Plan + +Decided 2026-10-28 (user, /quest-plan trust audit): + +- Today a merge job lands a runner-published head on the maintainer's earlier + pick, bounded only by branch rules. kixelated/quest's `main` requires no + reviews, so nothing enforces the review policy at that point. +- Apply the guide's rule: a review counts only on the head being landed. The + only exemptions are a conflict-free base merge and a reviewer's patch applied + unchanged. Where a repository's policy names whose review counts (for + kixelated/quest, a Codex review or 👍 on the head, in `CONTRIBUTING.md`), + follow it. Otherwise require a review from someone other than the PR's + author and the App. +- Where GitHub branch protection already requires reviews, defer to it rather + than duplicating it. +- Test landing with a review of an older head, with a conflict-free base merge + since the review, with a hand-resolved conflict since the review, and with + only an AI comment from the author's account. + +## Related + +- [Web execution](/quest/m0/kix/execution.md) - merge jobs this rule gates +- [Require read-only pull-request CI](/quest/m0/kix/ci-read-only.md) - the trust model this completes diff --git a/quest/m0/kix/sandbox/README.md b/quest/m0/kix/sandbox/README.md index 8d5176e5..034c5b32 100644 --- a/quest/m0/kix/sandbox/README.md +++ b/quest/m0/kix/sandbox/README.md @@ -74,6 +74,7 @@ Shipped in [kixelated/quest#212](https://github.com/kixelated/quest/pull/212) - [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 +- [Head approval](/quest/m0/kix/sandbox/head-approval.md) - a maintainer approves an exact head, such as one kix pushed, for host runners - [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 diff --git a/quest/m0/kix/sandbox/head-approval.md b/quest/m0/kix/sandbox/head-approval.md new file mode 100644 index 00000000..e37edbf8 --- /dev/null +++ b/quest/m0/kix/sandbox/head-approval.md @@ -0,0 +1,34 @@ +# [M] Approve a PR head for host runners + +## Goal + +A maintainer can approve one exact PR head in kix, so its iterate and merge +jobs run on any runner even though not every commit carries a maintainer +signature. The common case is a head kix itself pushed. The approval covers +only that SHA. It lapses when the head moves, and kix rechecks the approver's +current maintain or admin role each time it relies on the approval. + +## Plan + +Decided 2026-10-28 (user, /quest-plan trust audit, from Codex Security's P1 on +[kixelated/quest#243](https://github.com/kixelated/quest/pull/243)): + +- **Publication is not trust.** kix publishes whatever the runner sends. A + prompt-injected agent or a compromised runner could therefore publish a + `flake.nix` that host Nix evaluates outside the sandbox. So "kix pushed it" + never grants host trust. [Trusted PR heads](/quest/m0/kix/sandbox/trust.md) + trusts only commits signed by current maintainers and drops the publication + ledger. +- This quest adds the explicit path. Show the approval as a decision item + linking GitHub's compare view of the commits beyond the default branch. + Record the approver, the SHA, and the time. Recheck the approval at job + creation and at assignment, as trust classification does. +- GitHub's PR approvals can't serve: the maintainer usually authors these + PRs, and can't approve their own. +- Test approval of a kix-pushed head, a head that moves after approval, an + approver who loses the role, and an approval from someone who isn't a + maintainer. + +## Required + +- [Trusted PR heads](/quest/m0/kix/sandbox/trust.md) - signature-only host trust, which this extends From d2c593ed2c31247bb65d8fb0613539b3f2a4f040 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:08:42 -0700 Subject: [PATCH 2/2] quest: defer land review only to current-head protection Addresses Codex's finding: branch protection that accepts an approval of an older head does not enforce the exact-head rule. Co-Authored-By: Claude Opus 5.5 --- quest/m0/kix/land-review.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/quest/m0/kix/land-review.md b/quest/m0/kix/land-review.md index 37864102..8b01ab83 100644 --- a/quest/m0/kix/land-review.md +++ b/quest/m0/kix/land-review.md @@ -21,11 +21,14 @@ Decided 2026-10-28 (user, /quest-plan trust audit): kixelated/quest, a Codex review or 👍 on the head, in `CONTRIBUTING.md`), follow it. Otherwise require a review from someone other than the PR's author and the App. -- Where GitHub branch protection already requires reviews, defer to it rather - than duplicating it. +- Defer to GitHub branch protection only where it enforces the same + current-head rule: it requires approval of the most recent push, or it + dismisses stale approvals. Protection that accepts an approval of an older + head does not count; keep kix's exact-head check there. - Test landing with a review of an older head, with a conflict-free base merge - since the review, with a hand-resolved conflict since the review, and with - only an AI comment from the author's account. + since the review, with a hand-resolved conflict since the review, with only + an AI comment from the author's account, and under protection that requires + reviews without dismissing stale ones. ## Related