Skip to content
Merged
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
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion quest/m0/kix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,5 @@ fixes what it finds:

- [Operator setup](/quest/m0/kix/operator-setup.md) - pilot installations and a verified test PR on moq-dev/moq and moq-dev/quest
- [Private repositories](/quest/m0/kix/private.md) - moq-dev/moq.pro enrolls, visible only to signed-in users who can read it
- [Job sandbox](/quest/m0/kix/sandbox/README.md) - head approval for kix-pushed heads, and opencode sandboxed
- [Runner account pool](/quest/m0/kix/cliproxy.md) - the runner routes every Claude and Codex job and review through its own CLIProxyAPI pool
- [Web execution](/quest/m0/kix/execution.md) - spawn and iterate through the UI, ending in contribution PRs
4 changes: 0 additions & 4 deletions quest/m0/kix/execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,3 @@ Earlier guidance:
- Cover the real prompt loop in integration tests, including simultaneous
starts, a stale PR head, a blocked decision, an exhausted account, and a
runner going offline mid-job.

## Required

- [Job sandbox](/quest/m0/kix/sandbox/README.md) - iterate and merge jobs on approved kix-pushed heads, with opencode sandboxed
98 changes: 0 additions & 98 deletions quest/m0/kix/sandbox/README.md

This file was deleted.

80 changes: 0 additions & 80 deletions quest/m0/kix/sandbox/opencode-data.md

This file was deleted.

87 changes: 83 additions & 4 deletions quest/m1/kix/sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,89 @@ which lifts the owner's approval of another maintainer's answer.

## Plan

Split from [the m0 sandbox epic](/quest/m0/kix/sandbox/README.md) on
2026-10-30 (user, /quest-plan): moq-dev's maintainers run their jobs on
trusted or approved heads, so untrusted heads stay refused until m1. The
design decisions for the whole sandbox stay in the m0 epic's Plan.
Split from the m0 sandbox epic on 2026-10-30 (user, /quest-plan):
moq-dev's maintainers run their jobs on trusted or approved heads, so
untrusted heads stay refused until m1. The m0 epic finished with
[moq-dev/quest#536](https://github.com/moq-dev/quest/pull/536); its design
decisions for the whole sandbox moved here.

Decided 2026-10-15, from Codex's review of moq-dev/quest#121:

- A Nix devShell is not an isolation boundary, and maintainers' machines
must not run contributor-controlled code unconfined. Sandbox all jobs, not
just untrusted ones: one execution path, which also contains prompt
injection from untrusted issue text.
- 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.
- 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
[moq-dev/quest#188](https://github.com/moq-dev/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 once it is verified on
macOS.

Shipped in [moq-dev/quest#212](https://github.com/moq-dev/quest/pull/212)
(`src/runner/sandbox.ts`), and verified live on Linux with Claude Code
2.1.291 and 2.1.305:

- Claude Code's sandbox runs every command in `dontAsk` mode with
`--setting-sources user`, since a checkout's `.claude/settings.json` hooks
run outside the sandbox even in `-p` mode. The file tools edit and read
only the checkout, and WebFetch is off.
- The runner builds the devShell with `nix print-dev-env` from the job's
revision in the bare clone, never the edited checkout, and Claude Code
sources it through `CLAUDE_ENV_FILE` inside the sandbox, shellHook
included.
- Commands have no Nix daemon: on Linux the sandbox's seccomp filter
refuses Unix sockets, and allowing them all would expose every socket on
the filesystem, while a trusted Nix user could reach the daemon's
root-level options. `nix build` and `nix flake check` run in CI instead.
- Decided 2026-10-24 on
[moq-dev/quest#212](https://github.com/moq-dev/quest/pull/212): a
trusted revision's commands reach a fixed list of package registries
(npm, crates.io's index and downloads, PyPI), since most repositories
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.
- opencode jobs' commands run under sandbox-runtime, set as opencode's
shell, with the same policy
([moq-dev/quest#338](https://github.com/moq-dev/quest/pull/338)).
Its seccomp helper, which nixpkgs' sandbox-runtime leaves out, is built
from source (`nix/sandbox-runtime.nix`), and the runner refuses an opencode
account when srt lets commands make Unix sockets
([moq-dev/quest#465](https://github.com/moq-dev/quest/pull/465)).
Besides disabling project config, the runner keeps the agent from creating
a checkout's own `opencode.json` or `.opencode/` and refuses a checkout
that holds them. Before each turn it refuses to run if opencode loaded any
config but the runner's and the account's, since a machine's managed
`/etc/opencode` loads above the runner's and could replace the sandboxed
`shell`.
`test/live/runner-sandbox.test.ts` checks that boundary with the pinned
opencode and srt and a scripted model, and
`test/live/runner-sandbox-claude.test.ts` checks Claude Code's the same way,
its scripted model set as the account's endpoint in its settings. It runs in `just runner-sandbox`
and a GitHub-hosted workflow (`.github/workflows/sandbox.yml`), nightly
and on PRs that touch the flake, `nix/`, the dependencies, the runner, or
these tests, not in `just test` or the merge queue, whose container can't
mount `/proc` for bwrap, so a bump that weakens the boundary fails on its
own PR. Decided 2026-11-03 (user, /quest-complete), after the merge
queue dropped [moq-dev/quest#404](https://github.com/moq-dev/quest/pull/404):
its own CI, nightly, so landing never blocks on it.
- Decided 2026-10-24 (user) in `/quest-plan`: split the remaining work into
the m0 epic's children, since landed, 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

Expand Down
4 changes: 0 additions & 4 deletions quest/m2/cloud/funding.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ cloud runners; donor allocations follow working cloud runners separately.
[SDK cost tracking](https://code.claude.com/docs/en/agent-sdk/cost-tracking),
and [Console usage APIs](https://platform.claude.com/docs/en/manage-claude/usage-cost-api).

## Required

- [Job sandbox](/quest/m0/kix/sandbox/README.md) - the boundary keeping provider credentials outside job commands

## Related

- [Donor allocations](/quest/m2/cloud/donors.md) - add shared budgets after cloud runners work
4 changes: 2 additions & 2 deletions quest/m2/donor-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ with constrained network and filesystem access, without exposing host secrets.

## Plan

- 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
- m0 sandboxes job commands at the OS level and
[m1](/quest/m1/kix/sandbox/README.md) 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.
- Evaluate current microVM tooling and supported Linux/macOS hosts before
Expand Down
2 changes: 2 additions & 0 deletions src/runner/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface Launch {
cwd: string;
/** The account's isolated home, which holds its CLI login and sessions. */
home: string;
/** A directory for the CLI's files only this job may see, which commands cannot reach and which goes with the checkout. */
data: string;
session: string;
/** Resume `session` rather than starting it. */
resume: boolean;
Expand Down
5 changes: 3 additions & 2 deletions src/runner/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { randomBytes, randomUUID } from "node:crypto";
import { readFileSync, readdirSync, rmSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { join, resolve } from "node:path";

import { privateDirectory } from "../cli/agent";
import { type Owner, alive, currentOwner } from "../cli/supervise";
Expand Down Expand Up @@ -55,7 +55,8 @@ export class RunnerHome {
readonly dir: string;

constructor(dir: string) {
this.dir = privateDirectory(dir);
// Absolute, since the sandbox and opencode's OPENCODE_DB take the paths under it as they are.
this.dir = privateDirectory(resolve(dir));
}

login(): Login | undefined {
Expand Down
1 change: 1 addition & 0 deletions src/runner/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export class Attempt {
this.#agent = adapter.launch({
cwd: this.#workspace.dir,
home: ctx.home(account),
data: this.#workspace.data,
session: this.#session,
resume,
prompt: resume ? resumePrompt(job) : startPrompt(job, this.instructions, this.#workspace.dir),
Expand Down
Loading
Loading