Skip to content

test: unit tests can no longer write a developer's real agent or Orca settings - #23979

Merged
brennanb2025 merged 4 commits into
mainfrom
brennanb2025/tests-never-write-real-agent-home
Sep 30, 2026
Merged

brennanb2025 merged 4 commits into
mainfrom
brennanb2025/tests-never-write-real-agent-home

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Running Orca's unit tests on a developer machine quietly edited that developer's real Codex settings file. Two fake folders from test fixtures, /tmp/worktree-a and /repos/workspace-1, ended up marked "trusted" in the real ~/.codex/config.toml. This PR makes that impossible to do silently: a test that tries to write into a developer's real agent or Orca settings now fails, and the tests that did so now use throwaway folders.

What Changed

The problem. On 2026-09-29 two project-trust entries appeared in a developer's real ~/.codex/config.toml: [projects."/tmp/worktree-a"] and [projects."/repos/workspace-1"]. Both are fixture paths from unit tests. Orca writes a "trust this folder" entry for the agent it is about to start (#23744, #23937), and those writers looked up the home folder at write time with os.homedir(). Any test that reached a writer without replacing the home therefore wrote into the real one. The same writers also create ~/.cursor/projects/…, ~/.copilot/config.json, ~/.qoder/settings.json and ~/.gemini/antigravity-cli/settings.json.

User-facing change. None for people using Orca. For developers: running the unit tests can no longer change your real Codex, Claude, Cursor, Copilot, Qoder, Gemini or Orca settings, and a test that tries fails with the file path and the call stack that tried.

Mechanism.

  1. A test guard (config/scripts/vitest-real-agent-home-write-guard.ts, loaded as a vitest setup file) wraps the Node file-system calls that change disk: write, append, mkdir, mkdtemp, rename, copy, symlink, link, rm, rmdir, unlink, truncate, open-for-write and write streams, in their callback, sync and promise forms. A target under the real ~/.codex, ~/.claude, ~/.claude.json (and its temp siblings), ~/.orca, ~/.cursor, ~/.copilot, ~/.gemini, ~/.qoder or Orca's default user-data folder is refused. Reads are untouched.
  2. The real home comes from os.userInfo().homedir, which changing HOME does not move, so a test that points HOME elsewhere cannot hide the real one.
  3. The trust writers swallow their errors so a launch never fails over trust. The guard therefore also records each refusal and fails the test afterwards, even when the writer caught the error.
  4. The guard stands down only while an opted-in real-agent suite's own switch is set: ORCA_REAL_CLAUDE_SUPERVISED_STOP, ORCA_CODEX_CONTRACT_BINARY, ORCA_CODEX_NO_DAEMON_CONTRACT_BINARY, ORCA_CODEX_TRUST_CONTRACT_BINARY and ORCA_REPRO_CODEX_BINARY.
  5. A terminal inside Orca exports paths into the live app: ORCA_USER_DATA_PATH, CODEX_HOME, ORCA_CODEX_HOME, CLAUDE_CONFIG_DIR and ORCA_CODEX_LAUNCH_PREFLIGHT. The guard protects the first four and then unsets all five for the test process, so a local run behaves like CI. Before this, running src/main/pty/codex-shell-no-daemon.test.ts from an Orca terminal made its child shells run the installed Orca CLI's agent hooks prepare-codex against the live app's user data. The rule: a test run started inside an Orca terminal never targets the live app. This follows the repo's own vitest-caller-identity-env-setup.ts, which already clears an Orca terminal's caller identity for tests the same way. The opted-in real-agent suites keep these variables while their switch is set.
  6. The trust writers now take the home as an argument instead of calling os.homedir(). The home is worked out once per launch from the launch environment (HOME, or USERPROFILE on Windows, else this machine's home) by launchedAgentHome, the rule the SSH relay already used inline. The local Codex config list uses the same home. SSH hosts still work out the home on the relay from the relay's own spawn environment. WSL launches are unchanged: every writer except Claude's still skips them.
  7. The tests the guard caught now use a temp home or temp user-data folder: the adoption replay test (its pre-trust now lands in a temp HOME), the Codex session-resume and WSL hook tests (Orca's managed Codex home under user data), and the Claude Agent Teams tests (their tmux shim under ~/.orca).

Why

A second guard beside each writer would only cover the writers someone remembered. Refusing the write at the file-system layer covers every present and future writer, including ones that swallow errors. Passing the home in, rather than mocking os.homedir() in each test, makes the writer's target explicit. It also gives the local and SSH paths one rule for "the home the agent will read". Unsetting the inherited Orca paths removes the cause of the local-only leaks instead of patching each test.

What the guard does not cover: child processes a test starts, since each has its own file-system module. Three tests still create folders under HOME that are outside the protected list: src/relay/pty-handler-*.test.ts and src/relay/terminal-history.test.ts (~/.orca-relay/shell-ready, ~/.orca-remote/terminal-history) and src/relay/omp-fresh-launch-environment.test.ts (~/.orca-relay/omp-managed-status-extension). These are the relay's own cache folders, not agent or app settings, and are left for a follow-up.

Linked Issue

N/A (found while working on agent pre-trust; no issue filed).

Visual Proof

N/A: this changes tests and where trust files are written, not any screen.

Testing

  • Guard self-test (config/scripts/vitest-real-agent-home-write-guard.test.ts, 8 cases): sync, promise and swallowed writes under the real home are refused; a forced rm of a missing folder, which would otherwise succeed silently, is refused; the account home stays protected with HOME pointed elsewhere; a sibling like ~/.codex-… is left alone; the guard stands down under a real-agent switch; reads and temp-home writes work. Every probe path sits under a missing folder or is a no-op removal, so nothing is created even with the guard off.
  • Ablation: with the guard's check forced off, 5 of the self-test cases fail, including the forced rm case, which then returns without throwing. Nothing was created in the real home. With the check restored, all pass.
  • Broad guarded run with HOME set to a scratch folder: 1,696 test files (every unit test matching spawn, pty, runtime, trust, relay, codex, agent hooks and the per-agent writers), 17,347 tests. Before the fixes the guard caught 15 files; after them it catches none. The one remaining failure, src/main/providers/local-pty-provider-spawn-session.test.ts (it cannot load a linux-arm64 native module on macOS), also fails the same way on code without this change.
  • Oxlint and the anti-slop lint pass on every changed file.
  • Tested on macOS only; the guard's Windows paths (USERPROFILE, AppData/Roaming/orca) and case-insensitive comparison are covered by CI.

…ent reads

The Cursor, Copilot, Qoder and Antigravity writers and the local Codex config list
resolved ~ with os.homedir() at write time, so any test that reached them wrote into
the developer's real ~/.codex, ~/.cursor, ~/.copilot or ~/.gemini. Each writer now
takes the home, derived once from the launch env (HOME, or USERPROFILE on Windows,
else this host's home) by launchedAgentHome, which the SSH relay already used.
The structured Codex adoption replay pre-trusted /repos/workspace-1 in the real
~/.codex/config.toml; it now runs with a temp HOME and userData. The Codex
session-resume and WSL hook tests created Orca's managed Codex home under the live
userData, and the Claude Agent Teams tests wrote their tmux shim into ~/.orca; each
now runs against a temp userData or HOME.
A vitest setup file wraps the node:fs mutating calls and refuses a target under the
account's real ~/.codex, ~/.claude(.json), ~/.orca, ~/.cursor, ~/.copilot, ~/.gemini,
~/.qoder or Orca userData, found through os.userInfo() so a test that swaps HOME
cannot hide it. The refusal is recorded and rethrown after the test, since trust
writers swallow errors. Reads are untouched. It stands down only while an opted-in
real-agent suite's own switch is set. It also unsets what an Orca terminal exports
toward the live app (userData, Codex and Claude homes, and the Codex launch preflight
CLI, which a shell test would otherwise run), so a local run matches CI.
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Medium risk] Test infrastructure prevents unit tests from writing to developer home directories.

The PR is not ready to merge because the guard can still permit silent writes to real settings in the identified environments.

Findings

  1. P1 Contract switch disables all protection ▶
  2. P1 Redirected Orca settings remain unprotected ▶

Summary

This PR adds a Vitest filesystem guard against writes to real agent and Orca settings, makes trust writers use an explicit launch home, and moves affected tests to temporary homes. The guard has two remaining gaps: a contract switch can disable it for unrelated tests, and redirected default Orca user-data paths are not protected.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Vitest setup] --> B[Capture protected roots]
  A --> C[Wrap filesystem writes]
  C --> D{Real-agent switch set?}
  D -->|Yes| E[Write proceeds]
  D -->|No| F{Target under captured root?}
  F -->|Yes| G[Refuse and record violation]
  F -->|No| E
  G --> H[Fail test after execution]
Loading

Reviews (1) · Last reviewed commit: "test: type the guarded fs call from its ..."


function assertOutsideRealHome(state: GuardState, operation: string, target: unknown): void {
const path = targetPath(target)
if (path === null || realAgentSuiteOptedIn()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Contract switch disables all protection

If a developer exports any real-agent contract switch before running the full unit suite, this check disables the guard for every test in that process, not just the contract test. An unrelated test can then silently change the developer’s real agent settings. Limit the exemption to the intended contract run.

Comment on lines +67 to +69
join(home, 'Library', 'Application Support', 'orca'),
join(home, '.config', 'orca'),
join(home, 'AppData', 'Roaming', 'orca')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Redirected Orca settings remain unprotected

If XDG_CONFIG_HOME on Linux or APPDATA on Windows points outside the home directory, Orca uses that location for its user data, but these protected roots still assume a location under home. With ORCA_USER_DATA_PATH unset, a unit test can therefore write to the developer’s live Orca settings without the guard refusing it.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change adds a Vitest guard that blocks filesystem writes to protected agent-home and inherited-state paths. Workspace trust flows now pass the launched agent’s home to trust writers and Codex config-path lookup. Tests add temporary home and user-data paths, explicit home arguments, and guard coverage.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to f1a64

The new test safety net has two gaps: a symlink, or a recursive delete of a parent directory, can still reach real settings. Production behavior is unaffected, so the change is mergeable, but these gaps should be closed soon.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f1a64

The change makes trust-file ownership explicit and reduces accidental modification of real settings during tests. No introduced security defect was established, but complete launch-path consistency remains unverified, and the test safeguard is not a filesystem sandbox.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed sensitive sinks are persistent trust settings under the selected local or relay home, plus the managed local Codex config. Writes execute with the existing host process's filesystem permissions; explicit home routing changes destination selection rather than granting additional operating-system privileges.

Trust Boundaries and Controls

  • observed — The test guard captures account and environment homes plus absolute inherited state locations before removing selected inherited state and live-launch variables. It intercepts enumerated path-taking filesystem mutation APIs, including sync, promise, stream and write-open variants.
  • observed — The safeguard is process-local and uses lexical path-prefix matching, not filesystem identity resolution. Child processes are explicitly outside it, symlink aliases are not resolved, and named real-process test switches intentionally disable enforcement. These limits qualify the protection claim; they do not demonstrate increased exposure over the previously unguarded tests.

Resilience and Maintainability Implications

  • observed — Blocked writes are recorded before throwing. afterEach and afterAll drain those violations and fail the suite, preserving visibility when best-effort trust writers swallow the original exception.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 22 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: preventing unit tests from writing to real agent and Orca settings.
Description check ✅ Passed The description is complete and directly matches the changes. It explains the problem, mechanism, rationale, scope limits, testing, and platform coverage. The linked issue is marked N/A, which is acce…
Full details: Docstring Coverage

Explanation

Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 22 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 73f64332-95a3-43f1-b728-16b0f711cc69

📥 Commits

Reviewing files that changed from the base of the PR and between fb67d5d and f1a6418.

📒 Files selected for processing (23)
  • config/scripts/bun-profile-change-scope.mjs
  • config/scripts/vitest-real-agent-home-write-guard.test.ts
  • config/scripts/vitest-real-agent-home-write-guard.ts
  • config/tsconfig.node.json
  • config/vitest.config.ts
  • src/main/agent-trust-presets.test.ts
  • src/main/agent-trust-presets.ts
  • src/main/agent-workspace-trust.test.ts
  • src/main/agent-workspace-trust.ts
  • src/main/codex/codex-ai-vault-session-resume.test.ts
  • src/main/codex/codex-home-paths.ts
  • src/main/codex/codex-legacy-session-resume.test.ts
  • src/main/codex/codex-trust-config-concurrent-launch.test.ts
  • src/main/codex/hook-service-wsl-runtime.test.ts
  • src/main/execution-host-workspace-trust.test.ts
  • src/main/execution-host-workspace-trust.ts
  • src/main/qoder/hook-service.test.ts
  • src/main/qoder/workspace-trust.ts
  • src/main/runtime/claude-agent-teams-shim-env.test.ts
  • src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-04.spec.ts
  • src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts
  • src/relay/agent-workspace-trust-spawn.test.ts
  • src/relay/agent-workspace-trust-spawn.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

const foldCase = process.platform === 'darwin' || process.platform === 'win32'

function normalize(path: string): string {
const resolved = resolve(path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve symlink destinations before allowing writes.

A temporary fixture can contain a symlink to the real ~/.codex/config.toml. copyFileSync(source, fixtureLink) passes this lexical check, but Node follows the destination symlink and overwrites the real config. No violation is recorded. (nodejs.org)

For operations that follow symlinks, compare canonical targets against canonical protected roots. For new files, resolve the nearest existing parent before checking containment. Add a regression using temporary protected roots.

return
}
const normalized = normalize(path) + sep
const root = state.roots.find((candidate) => normalized.startsWith(candidate))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Block recursive removal of protected roots through their parents.

With the guard enabled, rmSync(userInfo().homedir, { recursive: true, force: true }) passes this check because the home is not inside any protected root. Node then recursively removes the home, including the protected settings directories. (nodejs.org)

For recursive removal, reject targets that contain a protected root as well as targets inside one. Keep this check operation-specific so unrelated writes under the home remain allowed. Add a regression using a temporary parent and protected child.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor suggestion inline.

Reviewed changes

  • Trust writers take the launch home, not os.homedir() at write time. WorkspaceTrustHost gains agentHome, computed once per launch by launchedAgentHome (win32 → USERPROFILE, else HOME, else homedir()); markCursorWorkspaceTrusted, markCopilotFolderTrusted, markAntigravityWorkspaceTrusted, markQoderWorkspaceTrusted, and getLocalCodexTrustConfigFiles(agentHome) now receive it. The local and relay paths share the rule, so a launch whose env names a different home writes trust where that agent will read it. Relay wire shape is unchanged.
  • New vitest guard refuses writes to the real agent/Orca home. config/scripts/vitest-real-agent-home-write-guard.ts wraps mutating fs/fs.promises calls and throws on targets under ~/.{codex,claude,orca,cursor,copilot,gemini,qoder}, ~/.claude.json*, and the default Orca userData folders. It uses userInfo().homedir so swapping HOME can't hide the account home, records refusals so a writer that swallows the error still fails the test, and stands down only under the opted-in real-agent suite switches. It also unsets the Orca paths an Orca terminal exports into the test process.
  • Tests that reached the real home now use temp locations. Adoption replay, Codex session-resume, WSL hooks, Claude Agent Teams shim, and the runtime readiness fragment; the trust unit tests pass an explicit home through the new signatures.

Verified this run: pnpm tc:node clean; guard self-test (8 tests), 11 changed test files (184 tests), and the orca-runtime.test.ts aggregator (1329 tests) all pass.

ℹ️ The guard only covers the process it runs in

The guarantee is narrower than it reads: only writes made through the test process's own node:fs module are checked. A test that spawns a child (a shell, an agent CLI, a shim) can still write the real home, which is why the PR's own ~/.orca-relay/~/.orca-remote cache writes remain open. Worth keeping the follow-up tracked so the boundary doesn't get mistaken for full coverage.

Technical details
# Guard coverage boundary

## Affected sites
- `config/scripts/vitest-real-agent-home-write-guard.ts:8-12` — JSDoc already states child processes are outside the guard.
- `src/relay/pty-handler-*.test.ts`, `src/relay/terminal-history.test.ts`, `src/relay/omp-fresh-launch-environment.test.ts` — still create `~/.orca-relay/...` and `~/.orca-remote/...` under the real home; those prefixes are not in `AGENT_HOME_ENTRIES` or the Orca folder list.

## Required outcome
- The guard's stated scope and the suite's actual guarantee stay in sync: either the relay cache prefixes are protected (or redirected) or the limitation stays documented where reviewers will see it.

## Open questions for the human
- Is the relay follow-up tracked in an issue? If not, a short note in the PR description would anchor it.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

const folders = realHomes().flatMap((home) => [
...AGENT_HOME_ENTRIES.map((entry) => join(home, entry)),
join(home, 'Library', 'Application Support', 'orca'),
join(home, '.config', 'orca'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Orca userData roots are hardcoded to the default layouts (~/.config/orca, ~/Library/Application Support/orca, ~/AppData/Roaming/orca), but production getOrcaUserDataPath() honors XDG_CONFIG_HOME on Linux and APPDATA on Windows. On a machine that overrides either, the real userData directory is not a protected root, so a test could still write the developer's real Orca settings — the exact failure this guard is meant to close.

Technical details
# Missing XDG_CONFIG_HOME / APPDATA-derived Orca roots

## Affected sites
- `config/scripts/vitest-real-agent-home-write-guard.ts:65-70` — only the default `orca` folders are added.
- `src/main/codex/codex-home-paths.ts:62-75` — `getOrcaUserDataPath()` uses `process.env.XDG_CONFIG_HOME` (linux) and `process.env.APPDATA` (win32) before the home default.

## Required outcome
- The guard protects whichever directory Orca actually uses for userData when those env vars are set, mirroring `getOrcaUserDataPath()`.

## Suggested approach (optional)
- Add roots derived from `process.env.XDG_CONFIG_HOME` and `process.env.APPDATA` (when absolute) alongside the per-home defaults.

@brennanb2025
brennanb2025 merged commit 5b3366f into main Sep 30, 2026
51 checks passed
LaPaGaYo pushed a commit to LaPaGaYo/Axiom that referenced this pull request Oct 20, 2026
… home

Semantic port of stablyai/orca 5b3366f (stablyai#23979), parts (b) and (c): the vitest setup guard
config/scripts/vitest-real-agent-home-write-guard.ts (+ test) registered first in config/vitest.config.ts
and config/tsconfig.node.json, protecting ~/.codex, ~/.claude(.json), ~/.orca, ~/.axiom, ~/.cursor,
~/.copilot, ~/.gemini, ~/.qoder, ~/.config/orca, ~/.config/axiom and the Orca userData; plus temp
HOME / userData for the tests that wrote the real home. Part (a) (trust writers taking the launched
agent's home) is deferred to the trust-subsystem sync: upstream moved those writers into files this
tree does not have. Content prepared by the UPSTREAM-1 attempt-1 Worker (launcher commit 63e71074ea);
the Codex sandbox cannot write .git, so the verifier created the commits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6WthNG1VZrbuiAXy8bpY7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant