Skip to content

feat(codexrunner): carry the Codex rollout in durable checkpoints - #179

Merged
Desperado merged 1 commit into
mainfrom
claude/artifact-review-ku6fdn
Aug 27, 2026
Merged

feat(codexrunner): carry the Codex rollout in durable checkpoints#179
Desperado merged 1 commit into
mainfrom
claude/artifact-review-ku6fdn

Conversation

@Desperado

Copy link
Copy Markdown
Contributor

Implements Ask A of the Cross-Sandbox Continuity handoff: let a checkpoint carry the rollout, not just its id.

The problem

Checkpoint held a thread ID and a model. That is enough to resume on the box that produced it, because codex exec resume resolves a thread from the local Codex session store — and enough for nothing at all on a replacement sandbox, where that store is empty and the thread ID names a rollout no process can reach.

What changed

Checkpoint gains RolloutPath, the local rollout backing its thread, so a host can upload it and restore it into a rebuilt sandbox before resuming. The package locates the file and stops there: it never reads, ships, or logs a rollout, and it learns nothing about artifact storage. Options.Rollouts selects the locator; CodexHomeLocator resolves $CODEX_HOME, then $HOME/.codex, and matches sessions/<yyyy>/<mm>/<dd>/rollout-*-<thread_id>.jsonl.

Adding the field is not sufficient on its own. Two sites rebuilt Checkpoint as a literal and would have dropped it silently:

  • Runner.handleEvent (runner.go:324) emitted the sink checkpoint from the thread ID and model, so Result never saw the rollout it had just located.
  • Continuity.Run (continuity.go:51) adopted the post-turn checkpoint from Result's thread ID and model, zeroing the rollout on every turn after the first.

Both now carry it, and Result grew the mirroring field so a caller adopting a Result as durable state keeps what the sink already received.

Continuity.Restore validates the rollout the way it already validates the thread ID and the model:

Condition Result
Empty RolloutPath Accepted — a same-sandbox checkpoint stays valid
Relative path ErrInvalidRolloutPath, not installed
Absent on this box ErrRolloutUnavailable, not installed

ErrRolloutUnavailable is the sandbox handover made loud. Callers recover by reporting the reason and restoring with RolloutPath cleared, which starts a fresh thread rather than resuming into an empty one that looks live.

Scope

Digest verification stays with the host: the checkpoint struct records no hash, and the contract's rollout_ref carries sha256 alongside the reference. Asks B (rehydrate $CODEX_HOME before resuming) and C (snapshot the working tree) land on the worker and are untouched here.

Testing

go build ./... && go vet ./... && go test ./... all pass.

New codexrunner/rollout_test.go covers the locator (exact-thread match, unknown thread, non-canonical thread ID, absent store, $CODEX_HOME), the checkpoint sink receiving the path at thread.started, a turn succeeding when no rollout is locatable, retention across a resumed turn, all three Restore outcomes, and the documented fresh-thread recovery.

Both regression tests were mutation-checked: reverting the two literal sites and the Restore validation fails TestContinuityRetainsRolloutAcrossTurns and TestContinuityRestoreValidatesRollout respectively, and nothing else.


Generated by Claude Code

A checkpoint held only a thread ID and a model. That is enough to resume on
the box that produced it, because "codex exec resume" resolves a thread from
the local Codex session store — and enough for nothing at all on a
replacement sandbox, where that store is empty and the thread ID names a
rollout no process can reach.

Checkpoint now carries RolloutPath, the local rollout backing its thread, so
a host can upload it and restore it into a rebuilt sandbox before resuming.
The package locates the file and stops there: it never reads, ships, or logs
a rollout, and it learns nothing about artifact storage. Options.Rollouts
selects the locator; CodexHomeLocator resolves $CODEX_HOME, then $HOME/.codex.

Adding the field is not sufficient on its own — two sites rebuilt Checkpoint
as a literal and would have dropped it:

- Runner.handleEvent emitted the sink checkpoint from the thread ID and model,
  so Result never saw the rollout it had just located.
- Continuity.Run adopted the post-turn checkpoint from Result's thread ID and
  model, zeroing the rollout on every turn after the first.

Both now carry it, and Result grew the mirroring field so a caller adopting a
Result as durable state keeps what the sink already received.

Continuity.Restore validates the rollout the way it already validates the
thread ID and the model: a relative path is refused as ErrInvalidRolloutPath,
and a rollout absent from this box as ErrRolloutUnavailable, without
installing the checkpoint. That second error is the sandbox handover made
loud — callers recover by reporting the reason and restoring with RolloutPath
cleared, which starts a fresh thread instead of resuming into an empty one
that looks live.

Digest verification stays with the host: the checkpoint struct records no
hash, and the contract's rollout_ref carries sha256 alongside the reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014apoNDacRcYKGdxtLaauu1
@sigilix

sigilix Bot commented Aug 27, 2026

Copy link
Copy Markdown

Sigilix Overview

Effort: 3/5 (medium)

Quality gates

  • ✅ PR title follows convention
  • ✅ PR description is complete
  • ℹ️ PR is linked to an issue — No Closes #N / Closes SIG-N keyword found in PR body or commit messages.

Summary — latest push

Extends the Checkpoint and Result structs to carry RolloutPath, the local Codex rollout file backing a thread, so sessions moved to a replacement sandbox can resume with their transcript instead of a dangling thread ID. Two sites that previously rebuilt Checkpoint as a literal from just the thread ID and model now propagate the rollout, and Continuity.Restore validates the path—rejecting relative paths or missing files with explicit errors to prevent silent resumption into an empty thread.

Important files

File Score Notes Next step
codexrunner/rollout.go 5/5 Introduces the RolloutLocator interface, CodexHomeLocator implementation, and validateRolloutPath which enforces absolute-path and file-existence checks for durable checkpoints. Add a test verifying CodexHomeLocator rejects or safely handles glob metacharacters in threadID if validThreadID ever relaxes its UUID format constraints.
codexrunner/rollout_test.go 4/5 Covers the locator, checkpoint sink receiving the path at thread.started, unlocatable rollouts, retention across resumed turns, Restore validation outcomes, and the fresh-thread recovery path. Add a test for CodexHomeLocator when multiple rollout files match the same thread ID (e.g., different timestamps) to confirm the glob selection behavior is deterministic.
codexrunner/runner.go 4/5 Wires RolloutLocator into Runner, adds RolloutPath to Checkpoint and Result, and ensures handleEvent propagates the located rollout to both the result and the checkpoint sink. Verify that locateRollout is called exactly once per thread.started event and cannot race if multiple such events arrive in a single stream.
codexrunner/continuity.go 4/5 Updates Continuity.Run to carry RolloutPath from Result into the durable checkpoint, and adds validateRolloutPath validation to Restore to refuse missing or relative rollout paths. Confirm that Restore intentionally allows an empty RolloutPath on a checkpoint that also carries a ThreadID, ensuring same-sandbox resumption remains valid without a rollout.
codexrunner/doc.go 1/5 Documents the package's stance on rollout handling: it locates but never reads, uploads, or logs rollout contents. Consider cross-linking to the RolloutLocator interface documentation from the package-level doc for discoverability.

Confidence: 4/5

The change is well-scoped with comprehensive test coverage including mutation-checked regression tests, and the validation logic correctly prevents silent data loss on sandbox handover.

  • Verify validThreadID strictly enforces canonical UUIDs so the rollout-*-{threadID}.jsonl glob pattern in CodexHomeLocator.LocateRollout cannot be expanded maliciously.
  • Check that Continuity.Restore intentionally permits a non-empty ThreadID with an empty RolloutPath, allowing same-sandbox resumption without a locatable rollout file.
  • Confirm locateRollout at runner.go:409 handles the r == nil case defensively, though Runner zero-values should be constructed via New.
  • Ensure CodexHomeLocator.LocateRollout returns a deterministic path when the glob matches multiple rollout files for the same thread ID (e.g., different timestamps).

Suggested labels: feature


Posted · 69a5f5d · 0 findings — View review
Dismiss @sigilix dismiss <reason> (not-a-bug | bad-anchor | already-covered | too-minor | wrong-context) · Re-run /sigilix review
Sigilix · 0 of 50 reviews used in past 5h

@sigilix sigilix Bot added the enhancement New feature or request label Aug 27, 2026

@qualitymaxapp qualitymaxapp Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

QualityMax Review — canonical overview updated; inline findings are attached to this review.

😸 1 finding(s) auto-dismissed by the Finding Verifier
  • llm-path-traversal codexrunner/rollout_test.go:10 — false positive: The code is in a test file using hardcoded test constants, not actual user-controlled input, so it poses no production risk.

Auto-dismissals are advisory. If a dismissal looks wrong, treat the finding as blocking and request a verifier review.

"os"
"path/filepath"
"testing"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 · insecure-configuration · MODEL-ONLY

File permissions 0o700 for directory creation are overly permissive (owner read/write/execute), which could expose sensitive session data

Evidence: codexrunner/rollout_test.go:9
Impact / next step: Use more restrictive permissions like 0o750 (owner rwx, group rx) or 0o700 only if absolutely necessary. Consider the principle of least privilege for file permissions.

Detailed reasoning
  • Root cause: File permissions 0o700 for directory creation are overly permissive (owner read/write/execute), which could expose sensitive session data
  • Threat model: Review callers that can reach codexrunner/rollout_test.go:9.
  • Existing protection: Reported by llm; confidence: medium.
  • Alternatives considered: Preserve the current interface; prefer the smallest safe change.
  • Severity calibration: P3 based on the scanner severity.
Prompt to fix with AI
Fix this insecure-configuration finding in codexrunner/rollout_test.go:9. Evidence: File permissions 0o700 for directory creation are overly permissive (owner read/write/execute), which could expose sensitive session data Requested outcome: Use more restrictive permissions like 0o750 (owner rwx, group rx) or 0o700 only if absolutely necessary. Consider the principle of least privilege for file permissions.. Preserve existing behavior and add focused coverage.

QualityMax · proof: MODEL-ONLY · served model: mistral-large-latest · requested: gemini-3.1-pro-preview


func writeRollout(t *testing.T, home, threadID string) string {
t.Helper()
directory := filepath.Join(home, "sessions", "2026", "08", "27")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 · insecure-configuration · MODEL-ONLY

File permissions 0o600 for rollout file creation are correct but should be documented as a security requirement

Evidence: codexrunner/rollout_test.go:13
Impact / next step: While 0o600 is appropriate (owner read/write), add comments explaining the security rationale for these permissions to prevent future changes that might weaken security.

Detailed reasoning
  • Root cause: File permissions 0o600 for rollout file creation are correct but should be documented as a security requirement
  • Threat model: Review callers that can reach codexrunner/rollout_test.go:13.
  • Existing protection: Reported by llm; confidence: medium.
  • Alternatives considered: Preserve the current interface; prefer the smallest safe change.
  • Severity calibration: P3 based on the scanner severity.
Prompt to fix with AI
Fix this insecure-configuration finding in codexrunner/rollout_test.go:13. Evidence: File permissions 0o600 for rollout file creation are correct but should be documented as a security requirement Requested outcome: While 0o600 is appropriate (owner read/write), add comments explaining the security rationale for these permissions to prevent future changes that might weaken security.. Preserve existing behavior and add focused coverage.

QualityMax · proof: MODEL-ONLY · served model: mistral-large-latest · requested: gemini-3.1-pro-preview

@qualitymaxapp

qualitymaxapp Bot commented Aug 27, 2026

Copy link
Copy Markdown

QualityMax Review

Verdict: COMMENT · Confidence: evidence-backed scan

Files eligible: 6 · Files reviewed: 6 · Files with findings: 1 · Findings: 2 · Inline cards: 2

Priority findings

priority location finding
P3 codexrunner/rollout_test.go:9 File permissions 0o700 for directory creation are overly permissive (owner read/write/execute), which could expose sensitive session data
P3 codexrunner/rollout_test.go:13 File permissions 0o600 for rollout file creation are correct but should be documented as a security requirement

Review gates

gate status
AI diff review completed · eligible 5, reviewed 4 · LLM · served mistral-large-latest · requested gemini-3.1-flash-lite
SAST completed · eligible 6, reviewed 6 · hybrid · served mistral-large-latest · requested gemini-3.1-pro-preview
Overall review evidence non-blocking findings remain
Inline evidence posted

Important files

file risk note next step
codexrunner/rollout_test.go P3 File permissions 0o700 for directory creation are overly permissive (owner read/write/execute), which could expose sensitive session data Inspect the inline card and apply the smallest safe fix.

Change diagram — Flow

flowchart TD
    A[Continuity.Run] -->|thread.started| B[Checkpoint Update]
    B --> C[RolloutLocator.LocateRollout]
    C -->|path| D[Checkpoint.RolloutPath]
    D --> E[Continuity.Restore]
    E --> F{validateRolloutPath}
    F -->|valid| G[Install Checkpoint]
    F -->|invalid| H[ErrRolloutUnavailable]
    H --> I[Clear RolloutPath]
    I --> J[Start Fresh Thread]
Loading

Review lifecycle

Use the inline cards to inspect evidence and suggested remediation. Re-run the QualityMax review after pushing a fix; unchanged cards are identified by their stable finding marker. Dismiss with a reason through the existing QualityMax/GitHub review feedback flow. 0 prior card(s) are stale/resolved on this head. @qmax Q&A is tracked separately.

Proof legend: VERIFIED independently judged patch · REPRODUCED verified finding · GROUNDED deterministic evidence · MODEL-ONLY model judgment.

QualityMax project results are available in the configured project.

Receipt · commit 69a5f5d3bf84ec9246d79a44de8dbaaeac13ca09 · run 2026-08-27T12:05:33+00:00 · model served mistral-large-latest · model requested gemini-3.1-pro-preview, gemini-3.1-flash-lite · model review substantive — 593 model output tokens · model source repository ai_review_preferences.preferred_model · re-review 1 · proof counts {'MODEL-ONLY': 2}

@qualitymaxapp

qualitymaxapp Bot commented Aug 27, 2026

Copy link
Copy Markdown

⚠️ QualityMax Pipeline

Gate Result
🔍 AI diff review ✅ Clean · mistral-large-latest · completed · 5 eligible / 4 reviewed · mistral-large-latest
🔍 SAST completed · 6 eligible / 6 reviewed · mistral-large-latest
🔍 Canonical PR review delivery completed · 0 eligible / 0 reviewed · exact-head review #5040525808 and overview #5438808909 confirmed
🧪 Repo Tests ✅ 675/675 passed (go)
🤖 AI Tests ⚠️ 51/56 passed

Powered by QualityMax — AI-Powered Test Automation

@Desperado
Desperado merged commit 36dfd00 into main Aug 27, 2026
7 checks passed
@Desperado
Desperado deleted the claude/artifact-review-ku6fdn branch August 27, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants