-
Notifications
You must be signed in to change notification settings - Fork 1
Fix all open Codex recovery-capture findings on PR #4 #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
BleedingDev
merged 7 commits into
fix/recovery-review-followups
from
fix/codex-recovery-capture
Sep 22, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
709bb24
test: reproduce open recovery capture review findings
BleedingDev ea20aa7
fix: isolate recovery capture secrets and handle future frame scopes
BleedingDev 5f1a0f4
fix: narrow locator element handles at the recovery boundary
BleedingDev 50889f2
perf: share protected frame traversal within each recovery snapshot
BleedingDev b3cc23e
test: bound recovery frame lookups and retain absent-control semantics
BleedingDev 2861cd2
Merge PR #4's concurrent copy of the recovery fixes
BleedingDev 1438b9c
fix: carry recovery sensitivity across environment dispatch
BleedingDev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import * as fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { fixture } from './support/chrome.mjs'; | ||
| import { Cdp } from '../dist/cdp.js'; | ||
| import { createRun, atomic } from '../dist/journal.js'; | ||
| import { attachPage, pageJob } from '../dist/page-worker.js'; | ||
|
|
||
| const key = 'FE2E_RERENDER_SECRET', secret = 'synthetic-private-123'; | ||
| const formatted = [...secret].join(' '), previous = process.env[key]; | ||
| process.env[key] = secret; | ||
| const f = await fixture(), raw = await Cdp.open(f.session), passed = []; | ||
| const worker = fileURLToPath(new URL('../worker', import.meta.url)); | ||
| function assertPrivate(journal) { | ||
| for (const entry of fs.readdirSync(journal.dir, { recursive: true })) { | ||
| const file = path.join(journal.dir, entry); | ||
| if (fs.statSync(file).isFile()) { | ||
| const text = fs.readFileSync(file, 'utf8'); | ||
| assert.equal(text.includes(secret), false, entry); | ||
| assert.equal(text.includes(formatted), false, entry); | ||
| } | ||
| } | ||
| } | ||
| function checkJevHandoff(journal) { | ||
| // A fresh Python adapter must honor the durable Node suspension even without | ||
| // the original environment. Any capture evaluation would fail this test. | ||
| const env = { ...process.env }; delete env[key]; | ||
| execFileSync('python3', ['-c', `import sys | ||
| sys.path.insert(0, sys.argv[1]) | ||
| from jev_runner import Journal, capture | ||
| class Page: | ||
| def evaluate(self, expression): | ||
| raise AssertionError('Suspended Jev recovery must not evaluate the page') | ||
| capture(Page(), Journal(sys.argv[2], sys.argv[3])) | ||
| `, worker, f.directory, journal.runId], { env, timeout: 10_000 }); | ||
| } | ||
| async function scenario(name, variant, reconstruct, check) { | ||
| const { targetId } = await raw.send('Target.createTarget', { url: 'about:blank' }); | ||
| const connection = await attachPage(f.session, targetId); | ||
| try { | ||
| const page = connection.page; | ||
| await page.goto(f.url, { waitUntil: 'load' }); | ||
| await page.setContent(`<input id="public" class="active-input" value="safe-before"><input id="token"><input id="safe" value="safe-before"> | ||
| <script>document.querySelector('#token').oninput=event=>{ | ||
| const node=event.target,value=[...node.value].join(' '); | ||
| if(${JSON.stringify(variant)}==='mirror') document.querySelector('#public').value=value; | ||
| else { | ||
| document.querySelector('#public').classList.remove('active-input'); | ||
| const next=${JSON.stringify(variant)}==='replace'?node.cloneNode(true):node; | ||
| next.id='renamed';next.classList.add('active-input');next.value=value; | ||
| if(next!==node)node.replaceWith(next); | ||
| } | ||
| };</script>`); | ||
| const step = { kind: 'fill', selector: '#token', valueFromEnv: key }; | ||
| const task = { url: f.url, goal: name, steps: [step], recovery: { | ||
| fields: [{ key: 'public', selector: '.active-input' }, { key: 'safe', selector: '#safe' }], | ||
| ...(reconstruct ? { reconstruct: [{ kind: 'navigate', value: f.url, safeToRepeat: true }, { ...step, safeToRepeat: true }] } : {}), | ||
| } }; | ||
| const { journal } = createRun(f.directory, task, false, f.profile); | ||
| journal.append('target', { targetId, browserId: f.session.browserId }); | ||
| atomic(path.join(f.directory, 'targets', f.session.namespace, `${targetId}.json`), { targetId, browserId: f.session.browserId }); | ||
| const invoke = (operation = 'capture', extra = {}) => pageJob({ | ||
| root: f.directory, runId: journal.runId, session: f.session, targetId, | ||
| operation, steps: [step], timeoutMs: 20_000, ...extra, | ||
| }, new AbortController().signal); | ||
| await invoke(); | ||
| await check({ page, journal, step, invoke }); | ||
| assertPrivate(journal); checkJevHandoff(journal); | ||
| passed.push(name); | ||
| } finally { | ||
| await connection.browser.close(); await raw.send('Target.closeTarget', { targetId }); | ||
| } | ||
| } | ||
| try { | ||
| for (const variant of ['retag', 'replace', 'mirror']) for (const reconstruct of [false, true]) { | ||
| await scenario(`Formatted ${variant} value is not retained; reconstruct=${reconstruct}`, variant, reconstruct, | ||
| async ({ page, journal, invoke }) => { | ||
| await invoke('steps', reconstruct ? { saveProgress: false } : {}); | ||
| assert.equal(await page.locator('.active-input').inputValue(), formatted); | ||
| const retained = journal.recoveryFields(); | ||
| assert.equal(retained.public.value, 'safe-before'); | ||
| assert.equal(retained.public.source, 'observed'); | ||
| const events = journal.events(); | ||
| const boundary = events.find(e => e.type === 'recovery.capture_suspended'); | ||
| assert.ok(boundary); | ||
| assert.ok(boundary.seq < events.find(e => e.type === 'action.start').seq); | ||
| // Reattachment, a later capture and loss of the env value cannot undo suspension. | ||
| delete process.env[key]; | ||
| try { await invoke(); await invoke(); } finally { process.env[key] = secret; } | ||
| assert.deepEqual(journal.recoveryFields(), retained); | ||
| assert.equal(journal.events().filter(e => e.type === 'recovery.capture_suspended').length, 1); | ||
| }); | ||
| } | ||
| await scenario('Explicit public inputs remain recoverable after secret dispatch', 'mirror', false, | ||
| async ({ journal, step, invoke }) => { | ||
| await invoke('steps', { steps: [step, { kind: 'fill', selector: '#safe', value: 'user-provided-public' }] }); | ||
| assert.equal(journal.recoveryFields().safe.value, 'user-provided-public'); | ||
| assert.equal(journal.recoveryFields().safe.source, 'intent'); | ||
| assert.equal(journal.recoveryFields().public.value, 'safe-before'); | ||
| }); | ||
| await scenario('Legacy dispatch without provenance suspends capture on resume', 'mirror', false, | ||
| async ({ page, journal, invoke }) => { | ||
| journal.append('action.start', { id: 'legacy', kind: 'fill', selector: '#token', step: 0, engine: 'playwright' }); | ||
| await page.locator('#token').fill(secret); | ||
| journal.append('action.end', { id: 'legacy' }); | ||
| delete process.env[key]; | ||
| try { await invoke(); } finally { process.env[key] = secret; } | ||
| assert.equal(journal.recoveryFields().public.value, 'safe-before'); | ||
| assert.ok(journal.events().some(e => e.type === 'recovery.capture_suspended')); | ||
| }); | ||
| } finally { | ||
| await raw.close(); await f.cleanup(); | ||
| if (previous === undefined) delete process.env[key]; else process.env[key] = previous; | ||
| } | ||
| console.log(JSON.stringify({ suite: 'environment retention across dispatch and handoff', passed, paidModelCalls: 0 })); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| """A fresh managed-Jev process must honor the shared capture boundary.""" | ||
| import json | ||
| from pathlib import Path | ||
| import sys | ||
| import tempfile | ||
| import time | ||
| import unittest | ||
| from unittest.mock import Mock | ||
|
|
||
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | ||
| from jev_runner import Journal, capture | ||
|
|
||
|
|
||
| class RecoveryBoundaryTests(unittest.TestCase): | ||
| def setUp(self): | ||
| self.temp = tempfile.TemporaryDirectory() | ||
| self.addCleanup(self.temp.cleanup) | ||
| self.run_id = 'r_' + 'a' * 32 | ||
| self.directory = Path(self.temp.name) / 'runs' / self.run_id | ||
| self.directory.mkdir(parents=True) | ||
| self.task = { | ||
| 'steps': [{'kind': 'fill', 'selector': '#token', 'valueFromEnv': 'SYNTHETIC_INPUT'}], | ||
| 'recovery': {'fields': [{'key': 'public', 'selector': '#public'}]}, | ||
| } | ||
| self.write_meta() | ||
| (self.directory / 'events.jsonl').write_text('') | ||
| self.journal = Journal(self.temp.name, self.run_id) | ||
| self.page = Mock() | ||
| self.page.evaluate.return_value = {'value': 'public', 'matches': False} | ||
|
|
||
| def write_meta(self): | ||
| (self.directory / 'task.json').write_text(json.dumps({ | ||
| 'version': 2, 'expiresAt': time.time() * 1000 + 60_000, 'task': self.task, | ||
| })) | ||
|
|
||
| def test_suspension_survives_new_journal_and_skips_model_generated_intents(self): | ||
| self.journal.append('recovery.capture_suspended', reason='environment boundary') | ||
| fresh = Journal(self.temp.name, self.run_id) | ||
| capture(self.page, fresh) | ||
| capture(self.page, fresh, {'kind': 'fill', 'node': 1}, 'model-generated') | ||
| self.page.evaluate.assert_not_called() | ||
| self.assertFalse((self.directory / 'recovery.json').exists()) | ||
|
|
||
| def test_legacy_playwright_receipt_is_not_assumed_public(self): | ||
| self.journal.append('action.start', engine='playwright', id='old') | ||
| capture(self.page, self.journal) | ||
| capture(self.page, Journal(self.temp.name, self.run_id)) | ||
| self.page.evaluate.assert_not_called() | ||
| self.assertEqual(sum(e['type'] == 'recovery.capture_suspended' for e in self.journal.events()), 1) | ||
|
|
||
| def test_guarded_public_dispatch_does_not_disable_capture(self): | ||
| self.journal.append('action.start', engine='playwright', id='public', recoveryGuarded=True) | ||
| capture(self.page, self.journal) | ||
| self.page.evaluate.assert_called_once() | ||
| self.assertEqual(json.loads((self.directory / 'recovery.json').read_text())['public']['value'], 'public') | ||
|
|
||
| def test_legacy_run_without_environment_steps_still_captures(self): | ||
| self.task['steps'] = [{'kind': 'click', 'selector': '#open'}] | ||
| self.write_meta() | ||
| self.journal.append('action.start', engine='playwright', id='old-public') | ||
| capture(self.page, Journal(self.temp.name, self.run_id)) | ||
| self.page.evaluate.assert_called_once() | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When resuming a run created by the previous implementation, an unguarded environment-backed fill may already have completed its post-action capture and written a transformed or mirrored secret into
recovery.json. This branch merely setssave = false, so that untrusted value remains persisted and can later be consumed throughvalueFromRecovery; detecting a legacy boundary must quarantine or remove the existing recovery payload rather than only preventing future captures.AGENTS.md reference: AGENTS.md:L9-L9
Useful? React with 👍 / 👎.