From 446324234672e64f5e9c6904cec17744e9740d01 Mon Sep 17 00:00:00 2001 From: docwilde Date: Thu, 12 Nov 2026 22:08:09 -0700 Subject: [PATCH 1/2] fix(codex): pin and verify features.context_management=false With features.context_management=true, a model that supports experimental context and a Plus/Pro ChatGPT plan, Codex enables TokenBudget at session start. The CLI override features.token_budget=false does not stop that, so token-budget resets were gated by PreCompact but no longer disabled as documented. Pass features.context_management=false with the other protected overrides and refuse startup unless config/read reports it false. The key exists, defaulting to false, in 0.163.0, 0.164.0 and 0.165.0, so retained Docker providers accept it. The compiled proof configs and verify_code_mode's config/read assertion cover the real provider. A new context-management refusal mode (and a missing-key mode) joins authoritative_version_and_hook_hash_refuse_before_thread_creation. Co-Authored-By: Claude Opus 5.5 --- rust/doxa-daemon/tests/codex_legacy_protection.rs | 2 +- rust/doxa-daemon/tests/fixtures/codex_appserver.py | 2 +- rust/doxa-daemon/tests/fixtures/codex_compaction_failure.py | 1 + rust/doxa-daemon/tests/fixtures/codex_live_auto.py | 2 +- rust/doxa-daemon/tests/fixtures/codex_peer_permissions.py | 2 +- rust/doxa-daemon/tests/fixtures/router_cli.py | 2 +- rust/doxa-daemon/tests/process.rs | 4 ++-- rust/doxa-engines/README.md | 2 +- rust/doxa-engines/src/codex_appserver.rs | 6 ++++++ rust/doxa-engines/src/codex_compact.rs | 2 +- rust/doxa-engines/tests/codex_protected.rs | 2 +- rust/doxa-engines/tests/fixtures/codex_protected_fixture.rs | 5 +++-- scripts/codex-protected/README.md | 5 ++++- scripts/codex-protected/verify_automatic.py | 2 +- scripts/codex-protected/verify_code_mode.py | 3 ++- 15 files changed, 27 insertions(+), 15 deletions(-) diff --git a/rust/doxa-daemon/tests/codex_legacy_protection.rs b/rust/doxa-daemon/tests/codex_legacy_protection.rs index 4abd6e6f..f9648c91 100644 --- a/rust/doxa-daemon/tests/codex_legacy_protection.rs +++ b/rust/doxa-daemon/tests/codex_legacy_protection.rs @@ -173,7 +173,7 @@ query=read();assert query['method']=='initialize' reply(query,{'userAgent':'doxa_codex_rs/__DOXA_SUPPORTED_CODEX_VERSION__ (doxa-precompact-fail-closed-v1; fixture)'}) assert read()['method']=='initialized' args=sys.argv[1:];overrides=[args[i+1] for i,value in enumerate(args[:-1]) if value=='-c'] -for flag in ['features.token_budget=false','features.async_compaction=false','agents.enabled=false','features.multi_agent_v2=false']: +for flag in ['features.token_budget=false','features.context_management=false','features.async_compaction=false','agents.enabled=false','features.multi_agent_v2=false']: assert flag in overrides, flag def merge(target, incoming): for key,value in incoming.items(): diff --git a/rust/doxa-daemon/tests/fixtures/codex_appserver.py b/rust/doxa-daemon/tests/fixtures/codex_appserver.py index 6892d9de..934199c6 100644 --- a/rust/doxa-daemon/tests/fixtures/codex_appserver.py +++ b/rust/doxa-daemon/tests/fixtures/codex_appserver.py @@ -64,7 +64,7 @@ def reply(request, value): def fixture_config(): overrides = [sys.argv[i+1] for i, item in enumerate(sys.argv[:-1]) if item == '-c'] - for flag in ('features.token_budget=false', 'features.async_compaction=false', + for flag in ('features.token_budget=false', 'features.context_management=false', 'features.async_compaction=false', 'agents.enabled=false', 'features.multi_agent_v2=false'): assert flag in overrides, flag def merge(target, incoming): diff --git a/rust/doxa-daemon/tests/fixtures/codex_compaction_failure.py b/rust/doxa-daemon/tests/fixtures/codex_compaction_failure.py index 18093890..c74e1888 100644 --- a/rust/doxa-daemon/tests/fixtures/codex_compaction_failure.py +++ b/rust/doxa-daemon/tests/fixtures/codex_compaction_failure.py @@ -44,6 +44,7 @@ def merge(target, incoming): if item == "-c": merge(config, tomllib.loads(sys.argv[i + 1])) assert config["features"]["token_budget"] is False +assert config["features"]["context_management"] is False assert config["features"]["async_compaction"] is False assert config["features"]["multi_agent_v2"] is False assert config["agents"]["enabled"] is False diff --git a/rust/doxa-daemon/tests/fixtures/codex_live_auto.py b/rust/doxa-daemon/tests/fixtures/codex_live_auto.py index e0881295..f6eb2781 100644 --- a/rust/doxa-daemon/tests/fixtures/codex_live_auto.py +++ b/rust/doxa-daemon/tests/fixtures/codex_live_auto.py @@ -29,7 +29,7 @@ def complete(): def fixture_config(): overrides = [sys.argv[i+1] for i, item in enumerate(sys.argv[:-1]) if item == '-c'] - for flag in ('features.token_budget=false', 'features.async_compaction=false', + for flag in ('features.token_budget=false', 'features.context_management=false', 'features.async_compaction=false', 'agents.enabled=false', 'features.multi_agent_v2=false'): assert flag in overrides, flag def merge(target, incoming): diff --git a/rust/doxa-daemon/tests/fixtures/codex_peer_permissions.py b/rust/doxa-daemon/tests/fixtures/codex_peer_permissions.py index 18a7eeb9..908ad9e1 100644 --- a/rust/doxa-daemon/tests/fixtures/codex_peer_permissions.py +++ b/rust/doxa-daemon/tests/fixtures/codex_peer_permissions.py @@ -25,7 +25,7 @@ def tool(): def fixture_config(): overrides = [sys.argv[i+1] for i, item in enumerate(sys.argv[:-1]) if item == '-c'] - for flag in ('features.token_budget=false', 'features.async_compaction=false', + for flag in ('features.token_budget=false', 'features.context_management=false', 'features.async_compaction=false', 'agents.enabled=false', 'features.multi_agent_v2=false'): assert flag in overrides, flag def merge(target, incoming): diff --git a/rust/doxa-daemon/tests/fixtures/router_cli.py b/rust/doxa-daemon/tests/fixtures/router_cli.py index 579dd7f9..b14f6c54 100644 --- a/rust/doxa-daemon/tests/fixtures/router_cli.py +++ b/rust/doxa-daemon/tests/fixtures/router_cli.py @@ -23,7 +23,7 @@ def reply(row, value): def fixture_config(): overrides = [sys.argv[i+1] for i, item in enumerate(sys.argv[:-1]) if item == '-c'] - for flag in ('features.token_budget=false', 'features.async_compaction=false', + for flag in ('features.token_budget=false', 'features.context_management=false', 'features.async_compaction=false', 'agents.enabled=false', 'features.multi_agent_v2=false'): assert flag in overrides, flag def merge(target, incoming): diff --git a/rust/doxa-daemon/tests/process.rs b/rust/doxa-daemon/tests/process.rs index 4b3aa448..a9dd16bb 100644 --- a/rust/doxa-daemon/tests/process.rs +++ b/rust/doxa-daemon/tests/process.rs @@ -3963,7 +3963,7 @@ import json, sys, tomllib def fixture_config(): overrides = [sys.argv[i+1] for i, item in enumerate(sys.argv[:-1]) if item == '-c'] - for flag in ('features.token_budget=false', 'features.async_compaction=false', + for flag in ('features.token_budget=false', 'features.context_management=false', 'features.async_compaction=false', 'agents.enabled=false', 'features.multi_agent_v2=false'): assert flag in overrides, flag def merge(target, incoming): @@ -5038,7 +5038,7 @@ source.parent.mkdir(parents=True,exist_ok=True) if not source.exists(): source.write_text(json.dumps({'type':'session_meta','payload':{'id':'thread-1'}})+'\n') def fixture_config(): overrides = [sys.argv[i+1] for i, item in enumerate(sys.argv[:-1]) if item == '-c'] - for flag in ('features.token_budget=false', 'features.async_compaction=false', + for flag in ('features.token_budget=false', 'features.context_management=false', 'features.async_compaction=false', 'agents.enabled=false', 'features.multi_agent_v2=false'): assert flag in overrides, flag def merge(target, incoming): diff --git a/rust/doxa-engines/README.md b/rust/doxa-engines/README.md index 25c3787d..cebbe174 100644 --- a/rust/doxa-engines/README.md +++ b/rust/doxa-engines/README.md @@ -22,7 +22,7 @@ Assistant text and reasoning are bounded and scrubbed as complete messages. The ## Compaction protection -Protected sessions require DOXA's private **Codex 0.165.0** app server with contract `doxa-precompact-fail-closed-v1`. Before thread startup, DOXA checks the build identity, trusted synchronous `PreCompact` hook hash, and disabled token-budget and asynchronous compaction flags. The protected payload also refuses speculative background compaction internally, regardless of client feature overrides. Stock or unsupported app servers fail before creating or resuming a protected thread. +Protected sessions require DOXA's private **Codex 0.165.0** app server with contract `doxa-precompact-fail-closed-v1`. Before thread startup, DOXA checks the build identity, trusted synchronous `PreCompact` hook hash, and disabled token-budget, context-management and asynchronous compaction flags. The protected payload also refuses speculative background compaction internally, regardless of client feature overrides. Stock or unsupported app servers fail before creating or resuming a protected thread. The hook binds the actual provider thread and owned rollout, prepares a scrubbed snapshot, and waits for the native LORE reviewer. Manual `/compact` independently reviews the same bound source and rechecks its identity and digest before submission. Missing, failed, timed-out, changed, asynchronous, or duplicate review blocks replacement; a worker exit code without an exact receipt is insufficient. DOXA also waits for matching hook and compaction events after submission. It does not overwrite global Codex settings or trust unrelated user hooks. diff --git a/rust/doxa-engines/src/codex_appserver.rs b/rust/doxa-engines/src/codex_appserver.rs index 682a7fba..9bcd07a2 100644 --- a/rust/doxa-engines/src/codex_appserver.rs +++ b/rust/doxa-engines/src/codex_appserver.rs @@ -392,6 +392,12 @@ impl AppServerDriver { if config["config"]["features"]["token_budget"] != false { return Err(AppServerError::Protocol("Codex unhooked token-budget reset path is not disabled")); } + // Experimental context management re-enables TokenBudget at + // session start for eligible accounts, regardless of the + // token_budget override. Require it explicitly off as well. + if config["config"]["features"]["context_management"] != false { + return Err(AppServerError::Protocol("Codex context management can re-enable the token-budget reset path")); + } if config["config"]["features"]["async_compaction"] != false { return Err(AppServerError::Protocol("Codex unreviewed asynchronous compaction path is not disabled")); } diff --git a/rust/doxa-engines/src/codex_compact.rs b/rust/doxa-engines/src/codex_compact.rs index 87044603..cd41adac 100644 --- a/rust/doxa-engines/src/codex_compact.rs +++ b/rust/doxa-engines/src/codex_compact.rs @@ -200,7 +200,7 @@ impl CompactGate { let group = format!("{{matcher={},hooks=[{{type=\"command\",command={},timeout={},async=false}}]}}", string(MATCHER), string(&self.command), HOOK_TIMEOUT); let state = format!("{}={{enabled=true,trusted_hash={}}}", string(HOOK_KEY), string(&self.hash)); - vec!["features.codex_hooks=true".into(), "features.token_budget=false".into(), "features.async_compaction=false".into(), format!("hooks={{PreCompact=[{group}],state={{{state}}}}}")] + vec!["features.codex_hooks=true".into(), "features.token_budget=false".into(), "features.context_management=false".into(), "features.async_compaction=false".into(), format!("hooks={{PreCompact=[{group}],state={{{state}}}}}")] } /// Call `hooks/list` after initialize and BEFORE starting/resuming a thread. /// Failure means this provider must not claim protected compaction. diff --git a/rust/doxa-engines/tests/codex_protected.rs b/rust/doxa-engines/tests/codex_protected.rs index f7ffa619..d579674e 100644 --- a/rust/doxa-engines/tests/codex_protected.rs +++ b/rust/doxa-engines/tests/codex_protected.rs @@ -9,7 +9,7 @@ include!("fixtures/codex_protected_fixture.rs"); #[tokio::test] async fn authoritative_version_and_hook_hash_refuse_before_thread_creation() { - for mode in ["version", "stock", "hash", "token-budget", "async-compaction", "native-agents", "native-agents-v2", "missing-agent-policy"] { + for mode in ["version", "stock", "hash", "token-budget", "context-management", "missing-context-management", "async-compaction", "native-agents", "native-agents-v2", "missing-agent-policy"] { let (dir, options, gate) = fixture(mode); assert!(AppServerDriver::spawn_protected(options, str::to_owned, false, gate).await.is_err()); assert!(!dir.path().join(format!("unsafe-after-{mode}")).exists()); diff --git a/rust/doxa-engines/tests/fixtures/codex_protected_fixture.rs b/rust/doxa-engines/tests/fixtures/codex_protected_fixture.rs index fcfa1905..a5940e5e 100644 --- a/rust/doxa-engines/tests/fixtures/codex_protected_fixture.rs +++ b/rust/doxa-engines/tests/fixtures/codex_protected_fixture.rs @@ -46,11 +46,12 @@ hooks=next(tomllib.loads(x)['hooks'] for x in overrides if x.startswith('hooks=' key=next(iter(hooks['state'])); command=hooks['PreCompact'][0]['hooks'][0]['command'] query=read(); assert query['method']=='config/read' assert 'features.token_budget=false' in overrides +assert 'features.context_management=false' in overrides assert 'features.async_compaction=false' in overrides assert 'agents.enabled=false' in overrides assert 'features.multi_agent_v2=false' in overrides -send({'id':query['id'],'result':{'config':{'agents':{'enabled':None if mode=='missing-agent-policy' else mode=='native-agents'},'features':{'token_budget':mode=='token-budget','async_compaction':mode=='async-compaction','multi_agent_v2':None if mode=='missing-agent-policy' else mode=='native-agents-v2'}},'origins':{},'layers':None}}) -if mode in ('token-budget','async-compaction','native-agents','native-agents-v2','missing-agent-policy'): +send({'id':query['id'],'result':{'config':{'agents':{'enabled':None if mode=='missing-agent-policy' else mode=='native-agents'},'features':{'token_budget':mode=='token-budget','context_management':None if mode=='missing-context-management' else mode=='context-management','async_compaction':mode=='async-compaction','multi_agent_v2':None if mode=='missing-agent-policy' else mode=='native-agents-v2'}},'origins':{},'layers':None}}) +if mode in ('token-budget','context-management','missing-context-management','async-compaction','native-agents','native-agents-v2','missing-agent-policy'): if sys.stdin.readline(): Path('unsafe-after-'+mode).write_text('request') sys.exit(0) query=read(); assert query['method']=='hooks/list' diff --git a/scripts/codex-protected/README.md b/scripts/codex-protected/README.md index d59bef87..9ff49604 100644 --- a/scripts/codex-protected/README.md +++ b/scripts/codex-protected/README.md @@ -19,7 +19,10 @@ that foreground review. This path is deliberately disabled in the protected payload: the scheduler refuses it even with its feature enabled, and the direct background execution entry point returns `TurnAborted` before a request. DOXA also overrides and verifies `features.async_compaction=false`. Token-budget -reset remains disabled. Background compaction may only be enabled after a +reset remains disabled: DOXA overrides and verifies `features.token_budget=false` +and `features.context_management=false`, because experimental context +management would otherwise re-enable the token budget at session start for +eligible accounts. Background compaction may only be enabled after a separate reviewed pre-inference authorization design and compiled proof. The private `doxaAuto` live update only tightens on-request approval to never. diff --git a/scripts/codex-protected/verify_automatic.py b/scripts/codex-protected/verify_automatic.py index 025de6ae..3397c650 100755 --- a/scripts/codex-protected/verify_automatic.py +++ b/scripts/codex-protected/verify_automatic.py @@ -134,7 +134,7 @@ def do_POST(self): ['model_context_window=100000', 'model_auto_compact_token_limit=100']) config = '\n'.join(['model="gpt-5.5"', 'model_provider="doxa_fixture"', *compact_config, - 'model_post_turn_compact_threshold_percent=0', '[agents]', 'enabled=false', '[features]', 'codex_hooks=true', 'token_budget=false', 'async_compaction=false', 'multi_agent_v2=false', + 'model_post_turn_compact_threshold_percent=0', '[agents]', 'enabled=false', '[features]', 'codex_hooks=true', 'token_budget=false', 'context_management=false', 'async_compaction=false', 'multi_agent_v2=false', '[model_providers.doxa_fixture]', 'name="DOXA loopback fixture"', f'base_url="http://127.0.0.1:{server.server_port}/v1"', 'wire_api="responses"', 'requires_openai_auth=false', 'request_max_retries=0', 'stream_max_retries=0', 'supports_websockets=false']) diff --git a/scripts/codex-protected/verify_code_mode.py b/scripts/codex-protected/verify_code_mode.py index 170ff4d6..0eb29190 100644 --- a/scripts/codex-protected/verify_code_mode.py +++ b/scripts/codex-protected/verify_code_mode.py @@ -424,7 +424,7 @@ def configure(root, peer): home.mkdir(mode=0o700) config = '\n'.join(['model="gpt-6-sol"', 'model_provider="doxa_fixture"', 'model_auto_compact_token_limit=100000000', 'model_post_turn_compact_threshold_percent=0', - '[agents]', 'enabled=false', '[features]', 'codex_hooks=true', 'token_budget=false', 'async_compaction=false', 'multi_agent_v2=false', 'code_mode_only=true', + '[agents]', 'enabled=false', '[features]', 'codex_hooks=true', 'token_budget=false', 'context_management=false', 'async_compaction=false', 'multi_agent_v2=false', 'code_mode_only=true', '[model_providers.doxa_fixture]', 'name="Owned code mode proof"', f'base_url="http://127.0.0.1:{peer.server.server_port}/v1"', 'wire_api="responses"', 'requires_openai_auth=false', 'request_max_retries=0', 'stream_max_retries=0', 'supports_websockets=false']) @@ -468,6 +468,7 @@ def provider_turn(server, root, peer, token, launcher=None): rpc.send({"method": "initialized"}) config = rpc.request("config/read", {"cwd": str(root / "workspace"), "includeLayers": False}) assert config["config"]["features"]["token_budget"] is False + assert config["config"]["features"]["context_management"] is False assert config["config"]["features"]["async_compaction"] is False assert config["config"]["agents"]["enabled"] is False assert config["config"]["features"]["multi_agent_v2"] is False From 709617489bccaa50b0fa57a4b1c41fb372220556 Mon Sep 17 00:00:00 2001 From: docwilde Date: Thu, 12 Nov 2026 22:36:22 -0700 Subject: [PATCH 2/2] =?UTF-8?q?release:=202.0.0-beta.77=20=E2=80=94=20prot?= =?UTF-8?q?ected=20Codex=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the two weak spots recorded in the 0.165.0 review. A stale command approval answered with an execpolicy amendment after live auto tightened the turn to never was denied, but its prefix allow rule had already been written to the in-memory policy and the rules file. Later matching commands could then skip the sandbox. persist_execpolicy_amendment, the only writer, now holds the active_turn lock that publishes doxaAuto and refuses once the turn has switched. New regression doxa_auto_refuses_an_execpolicy_amendment_answered_after_the_switch; it fails with the guard removed. Provider patch 376dc699 replaces 128293eb on the same 0.165.0 source. 128293eb stays in the installer migration registry and PREVIOUS_PATCHES. Retained contracts remain version-keyed, so Docker sessions saved with the beta.76 provider resume with beta.76; 0.163.0 and 0.164.0 sessions are unaffected. Review record: reviews/0.165.0-r2.json. Validation: installer build from the official tag with the exact patch; 9 core and 1 hook guard tests; automatic-compaction proofs in both windows (9 denial modes, 0 replacements; allow control 1); code-mode proof, which now asserts context_management=false from the real provider, and its missing-helper control; doxa-engines, daemon Codex and process suites; updater/installer Python suites. Compiled gates ran locally with 8 build jobs instead of CI's 1-job, 8 GiB scope. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 + Cargo.lock | 42 ++--- rust/doxa-claude/Cargo.toml | 2 +- rust/doxa-codegraph/Cargo.toml | 2 +- rust/doxa-daemon/Cargo.toml | 2 +- rust/doxa-engines/Cargo.toml | 2 +- .../src/bin/doxa-codex-protected.rs | 2 +- rust/doxa-engines/src/codex_compact.rs | 2 +- rust/doxa-engines/tests/codex_launcher.rs | 2 +- rust/doxa-fleet/Cargo.toml | 2 +- rust/doxa-hub/Cargo.toml | 2 +- rust/doxa-input/Cargo.toml | 2 +- rust/doxa-isolation/Cargo.toml | 2 +- rust/doxa-lore/Cargo.toml | 2 +- rust/doxa-mcp/Cargo.toml | 2 +- rust/doxa-peers/Cargo.toml | 2 +- rust/doxa-protocol/Cargo.toml | 2 +- rust/doxa-remote-wire/Cargo.toml | 2 +- rust/doxa-remote/Cargo.toml | 2 +- rust/doxa-router/Cargo.toml | 2 +- rust/doxa-runtime/Cargo.toml | 2 +- rust/doxa-state/Cargo.toml | 2 +- rust/doxa-transcript/Cargo.toml | 2 +- rust/doxa-tui/Cargo.toml | 2 +- rust/doxa-vendors/Cargo.toml | 2 +- rust/doxa-worktrees/Cargo.toml | 2 +- scripts/build-session-image.py | 2 +- scripts/codex-protected/README.md | 9 +- scripts/codex-protected/pin.json | 2 +- scripts/codex-protected/precompact.patch | 145 ++++++++++++++++-- .../codex-protected/reviews/0.165.0-r2.json | 32 ++++ scripts/install_codex_protected.py | 9 +- 32 files changed, 230 insertions(+), 67 deletions(-) create mode 100644 scripts/codex-protected/reviews/0.165.0-r2.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e622b66..073cccb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ them (`v0.1.0` … `v1.19.0`); the 1.x ranges below are derived from that history, not written from memory. Rust alpha and beta tags identify previews; the stable 2.0 release has not been cut. +## 2.0.0-beta.77 — 2026-11-12 + +**Protected Codex hardening** + +- Fix **`persist_execpolicy_amendment`** writing a durable allow rule from a stale approval answered after live auto tightened the turn to never. The rule is now committed under the same lock that publishes the switch, and refused once it has happened. New regression: `doxa_auto_refuses_an_execpolicy_amendment_answered_after_the_switch`. +- Pin and verify **`features.context_management=false`**. Experimental context management could re-enable the token budget at session start despite `features.token_budget=false`; startup now refuses unless both are off. +- Provider patch `376dc699` replaces `128293eb` on the same Codex 0.165.0 source. Docker sessions saved with the beta.76 provider resume only with beta.76; earlier 0.163.0 and 0.164.0 sessions are unaffected. + ## 2.0.0-beta.76 — 2026-11-12 **Protected Codex 0.165.0** diff --git a/Cargo.lock b/Cargo.lock index 38b9a1c5..db71a9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,7 +453,7 @@ dependencies = [ [[package]] name = "doxa-claude" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-input", "doxa-isolation", @@ -465,7 +465,7 @@ dependencies = [ [[package]] name = "doxa-codegraph" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "libc", "proc-macro2", @@ -481,7 +481,7 @@ dependencies = [ [[package]] name = "doxa-daemon" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-claude", "doxa-engines", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "doxa-engines" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-input", "doxa-isolation", @@ -529,7 +529,7 @@ dependencies = [ [[package]] name = "doxa-fleet" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-isolation", "doxa-protocol", @@ -547,7 +547,7 @@ dependencies = [ [[package]] name = "doxa-hub" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "base64 0.22.1", "bytes", @@ -571,7 +571,7 @@ dependencies = [ [[package]] name = "doxa-input" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "base64 0.22.1", "image", @@ -583,7 +583,7 @@ dependencies = [ [[package]] name = "doxa-isolation" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-router", "errno", @@ -597,7 +597,7 @@ dependencies = [ [[package]] name = "doxa-lore" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-codegraph", "doxa-state", @@ -611,7 +611,7 @@ dependencies = [ [[package]] name = "doxa-mcp" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-vendors", "libc", @@ -624,7 +624,7 @@ dependencies = [ [[package]] name = "doxa-peers" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "bytes", "doxa-isolation", @@ -646,7 +646,7 @@ dependencies = [ [[package]] name = "doxa-protocol" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "serde", "serde_json", @@ -654,7 +654,7 @@ dependencies = [ [[package]] name = "doxa-remote" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "bytes", "doxa-lore", @@ -676,7 +676,7 @@ dependencies = [ [[package]] name = "doxa-remote-wire" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "aes-gcm", "base64 0.22.1", @@ -688,7 +688,7 @@ dependencies = [ [[package]] name = "doxa-router" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-vendors", "libc", @@ -702,7 +702,7 @@ dependencies = [ [[package]] name = "doxa-runtime" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-input", "doxa-protocol", @@ -713,7 +713,7 @@ dependencies = [ [[package]] name = "doxa-state" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "libc", "serde", @@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "doxa-transcript" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-vendors", "libc", @@ -737,7 +737,7 @@ dependencies = [ [[package]] name = "doxa-tui" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "crossterm", "doxa-claude", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "doxa-vendors" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "futures-util", "libc", @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "doxa-worktrees" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" dependencies = [ "doxa-isolation", "doxa-state", diff --git a/rust/doxa-claude/Cargo.toml b/rust/doxa-claude/Cargo.toml index 9178fdbb..f3db5a56 100644 --- a/rust/doxa-claude/Cargo.toml +++ b/rust/doxa-claude/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-claude" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-codegraph/Cargo.toml b/rust/doxa-codegraph/Cargo.toml index 27d331b6..f5e01f55 100644 --- a/rust/doxa-codegraph/Cargo.toml +++ b/rust/doxa-codegraph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-codegraph" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-daemon/Cargo.toml b/rust/doxa-daemon/Cargo.toml index 73d73c4e..89bd44a4 100644 --- a/rust/doxa-daemon/Cargo.toml +++ b/rust/doxa-daemon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-daemon" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-engines/Cargo.toml b/rust/doxa-engines/Cargo.toml index aaa3e168..bca9a0f2 100644 --- a/rust/doxa-engines/Cargo.toml +++ b/rust/doxa-engines/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-engines" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-engines/src/bin/doxa-codex-protected.rs b/rust/doxa-engines/src/bin/doxa-codex-protected.rs index 41ff77b6..35db6116 100644 --- a/rust/doxa-engines/src/bin/doxa-codex-protected.rs +++ b/rust/doxa-engines/src/bin/doxa-codex-protected.rs @@ -20,7 +20,7 @@ use std::{ const SOURCE: &str = "360319d8dee62d87e2d59fc0c21805d79519cfed"; const CONTRACT: &str = "doxa-precompact-fail-closed-v1"; -const PATCH: &str = "128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484"; +const PATCH: &str = "376dc69901a97e62d19c6bf32377c4d464e93192340aa582d6992f2613f8ca46"; const MAX_BINARY: u64 = 1024 * 1024 * 1024; #[derive(Deserialize)] diff --git a/rust/doxa-engines/src/codex_compact.rs b/rust/doxa-engines/src/codex_compact.rs index cd41adac..90f42ae8 100644 --- a/rust/doxa-engines/src/codex_compact.rs +++ b/rust/doxa-engines/src/codex_compact.rs @@ -12,7 +12,7 @@ use std::os::unix::fs::PermissionsExt; pub const SUPPORTED_VERSION: &str = "0.165.0"; pub const PROTECTED_AGENT_PREFIX: &str = "doxa_codex_rs/0.165.0 (doxa-precompact-fail-closed-v1; "; pub const SUPPORTED_SOURCE: &str = "360319d8dee62d87e2d59fc0c21805d79519cfed"; -pub const SUPPORTED_PATCH: &str = "128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484"; +pub const SUPPORTED_PATCH: &str = "376dc69901a97e62d19c6bf32377c4d464e93192340aa582d6992f2613f8ca46"; #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] diff --git a/rust/doxa-engines/tests/codex_launcher.rs b/rust/doxa-engines/tests/codex_launcher.rs index b9524830..1ee96791 100644 --- a/rust/doxa-engines/tests/codex_launcher.rs +++ b/rust/doxa-engines/tests/codex_launcher.rs @@ -2,7 +2,7 @@ use serde_json::json; use sha2::{Digest, Sha256}; use std::{fs, os::unix::fs::PermissionsExt, process::Command}; -const PATCH: &str = "128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484"; +const PATCH: &str = "376dc69901a97e62d19c6bf32377c4d464e93192340aa582d6992f2613f8ca46"; fn fixture() -> tempfile::TempDir { let dir = tempfile::tempdir().unwrap(); fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o700)).unwrap(); diff --git a/rust/doxa-fleet/Cargo.toml b/rust/doxa-fleet/Cargo.toml index 8663583c..c3cc08c8 100644 --- a/rust/doxa-fleet/Cargo.toml +++ b/rust/doxa-fleet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-fleet" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-hub/Cargo.toml b/rust/doxa-hub/Cargo.toml index 9544b1e4..9b32f7ce 100644 --- a/rust/doxa-hub/Cargo.toml +++ b/rust/doxa-hub/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-hub" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-input/Cargo.toml b/rust/doxa-input/Cargo.toml index 4412dcc3..5bb54764 100644 --- a/rust/doxa-input/Cargo.toml +++ b/rust/doxa-input/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-input" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-isolation/Cargo.toml b/rust/doxa-isolation/Cargo.toml index 574ac91c..2686b6db 100644 --- a/rust/doxa-isolation/Cargo.toml +++ b/rust/doxa-isolation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-isolation" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-lore/Cargo.toml b/rust/doxa-lore/Cargo.toml index 35e97411..82116214 100644 --- a/rust/doxa-lore/Cargo.toml +++ b/rust/doxa-lore/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-lore" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-mcp/Cargo.toml b/rust/doxa-mcp/Cargo.toml index 7163c394..c3de7a14 100644 --- a/rust/doxa-mcp/Cargo.toml +++ b/rust/doxa-mcp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-mcp" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-peers/Cargo.toml b/rust/doxa-peers/Cargo.toml index 52ccee48..21bf84f0 100644 --- a/rust/doxa-peers/Cargo.toml +++ b/rust/doxa-peers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-peers" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-protocol/Cargo.toml b/rust/doxa-protocol/Cargo.toml index eea897bb..0c208bab 100644 --- a/rust/doxa-protocol/Cargo.toml +++ b/rust/doxa-protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-protocol" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-remote-wire/Cargo.toml b/rust/doxa-remote-wire/Cargo.toml index 80caf527..cc4a0429 100644 --- a/rust/doxa-remote-wire/Cargo.toml +++ b/rust/doxa-remote-wire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-remote-wire" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-remote/Cargo.toml b/rust/doxa-remote/Cargo.toml index cdcc1ad3..0a145b79 100644 --- a/rust/doxa-remote/Cargo.toml +++ b/rust/doxa-remote/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-remote" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-router/Cargo.toml b/rust/doxa-router/Cargo.toml index 9b1e55fb..7e127e55 100644 --- a/rust/doxa-router/Cargo.toml +++ b/rust/doxa-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-router" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-runtime/Cargo.toml b/rust/doxa-runtime/Cargo.toml index 792acdc4..06ab218e 100644 --- a/rust/doxa-runtime/Cargo.toml +++ b/rust/doxa-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-runtime" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-state/Cargo.toml b/rust/doxa-state/Cargo.toml index d07d396f..23da6131 100644 --- a/rust/doxa-state/Cargo.toml +++ b/rust/doxa-state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-state" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-transcript/Cargo.toml b/rust/doxa-transcript/Cargo.toml index a4d13c29..a8129aff 100644 --- a/rust/doxa-transcript/Cargo.toml +++ b/rust/doxa-transcript/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-transcript" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-tui/Cargo.toml b/rust/doxa-tui/Cargo.toml index 35dc59a0..2b9eac71 100644 --- a/rust/doxa-tui/Cargo.toml +++ b/rust/doxa-tui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-tui" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-vendors/Cargo.toml b/rust/doxa-vendors/Cargo.toml index 83325066..f957db1f 100644 --- a/rust/doxa-vendors/Cargo.toml +++ b/rust/doxa-vendors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-vendors" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/rust/doxa-worktrees/Cargo.toml b/rust/doxa-worktrees/Cargo.toml index c2673d0e..e1336586 100644 --- a/rust/doxa-worktrees/Cargo.toml +++ b/rust/doxa-worktrees/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "doxa-worktrees" -version = "2.0.0-beta.76" +version = "2.0.0-beta.77" edition = "2021" publish = false license = "AGPL-3.0-only" diff --git a/scripts/build-session-image.py b/scripts/build-session-image.py index 7cae6caf..5095b3d0 100644 --- a/scripts/build-session-image.py +++ b/scripts/build-session-image.py @@ -27,7 +27,7 @@ } CONTRACT = "doxa-precompact-fail-closed-v1" SOURCE = "360319d8dee62d87e2d59fc0c21805d79519cfed" -PATCH = "128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484" +PATCH = "376dc69901a97e62d19c6bf32377c4d464e93192340aa582d6992f2613f8ca46" MAX_BINARY = 1024 * 1024 * 1024 APT_SNAPSHOT = "20260901T000000Z" APT_PACKAGES = {"bash", "ca-certificates", "git", "python3"} diff --git a/scripts/codex-protected/README.md b/scripts/codex-protected/README.md index 9ff49604..6c1cc2fe 100644 --- a/scripts/codex-protected/README.md +++ b/scripts/codex-protected/README.md @@ -51,7 +51,7 @@ python3 scripts/codex-protected/verify_code_mode.py --server PRIVATE_SERVER --sc ``` List each Rust filter first and require a nonzero inventory: the current source -has eight core guard tests and one hook guard test. The originating-turn, +has nine core guard tests and one hook guard test. The originating-turn, environment-keyed permission-grant and strict Guardian review baselines must also remain passing. These focused checks do not replace the complete upstream test suite. @@ -76,6 +76,13 @@ can stop compaction but never satisfy the required session-flags identity. No new caller replaces history or starts compaction inference outside the gate. The review record is `reviews/0.165.0.json`. +Revision `376dc699` (`reviews/0.165.0-r2.json`) keeps the same 0.165.0 source. +A stale command approval answered after the live-auto switch can no longer +persist an execpolicy amendment: the durable allow rule is written under the +same `active_turn` lock that publishes the switch, and refused once it has. +Docker sessions saved with the earlier `128293eb` revision resume with +beta.76. + The 0.164.0 port preserved structured approval source and cancellation metadata when a live-auto switch refuses a stale result. It also made remote execution routing use the live step policy. Current payloads use Rust 1.99.1 diff --git a/scripts/codex-protected/pin.json b/scripts/codex-protected/pin.json index a5a83549..ddbaa517 100644 --- a/scripts/codex-protected/pin.json +++ b/scripts/codex-protected/pin.json @@ -1,6 +1,6 @@ { "contract": "doxa-precompact-fail-closed-v1", - "patch_sha256": "128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484", + "patch_sha256": "376dc69901a97e62d19c6bf32377c4d464e93192340aa582d6992f2613f8ca46", "provider": "codex-0.165.0-precompact-v1", "schema": 1, "source_commit": "360319d8dee62d87e2d59fc0c21805d79519cfed", diff --git a/scripts/codex-protected/precompact.patch b/scripts/codex-protected/precompact.patch index 5ee17c48..984516b7 100644 --- a/scripts/codex-protected/precompact.patch +++ b/scripts/codex-protected/precompact.patch @@ -1928,10 +1928,45 @@ index e4a0481..d622696 100644 shutdown(&sess).await; } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs -index aba4971..93cec01 100644 +index aba4971..16c8f6c 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs -@@ -3034,6 +3034,11 @@ impl Session { +@@ -2870,6 +2870,10 @@ impl Session { + + /// Adds an execpolicy amendment to both the in-memory and on-disk policies so future + /// commands can use the newly approved prefix. ++ #[expect( ++ clippy::await_holding_invalid_type, ++ reason = "durable allow rules must be committed atomically with live-auto publication" ++ )] + pub(crate) async fn persist_execpolicy_amendment( + &self, + amendment: &ExecPolicyAmendment, +@@ -2882,10 +2886,23 @@ impl Session { + .codex_home() + .clone(); + ++ // DOXA live auto: an amendment is a durable allow rule that later ++ // commands may use to skip the sandbox. A stale approval answered ++ // after the turn tightened to never must not write one. Commit at the ++ // same serialized boundary that publishes the tightening. ++ let active = self.active_turn.lock().await; ++ if active ++ .as_ref() ++ .and_then(|turn| turn.task.as_ref()) ++ .is_some_and(|task| task.turn_context.doxa_switched_to_auto()) ++ { ++ return Ok(()); ++ } + self.services + .exec_policy + .append_amendment_and_update(&codex_home, amendment) + .await?; ++ drop(active); + + Ok(()) + } +@@ -3034,6 +3051,11 @@ impl Session { available_decisions: Option>, plugin_attribution_override: Option, ) -> ReviewDecision { @@ -1943,7 +1978,7 @@ index aba4971..93cec01 100644 let _elicitation = self.services.elicitations.register(); // command-level approvals use `call_id`. // `approval_id` identifies subcommand callbacks and stdin writes. -@@ -3105,7 +3110,14 @@ impl Session { +@@ -3105,7 +3127,14 @@ impl Session { parsed_cmd, }); self.send_event(turn_context, event).await; @@ -1959,7 +1994,7 @@ index aba4971..93cec01 100644 } #[expect( -@@ -3120,6 +3132,11 @@ impl Session { +@@ -3120,6 +3149,11 @@ impl Session { reason: Option, grant_root: Option, ) -> ReviewDecision { @@ -1971,7 +2006,7 @@ index aba4971..93cec01 100644 let _elicitation = self.services.elicitations.register(); // Add the tx_approve callback to the map before sending the request. let (tx_approve, rx_approve) = oneshot::channel(); -@@ -3147,7 +3164,14 @@ impl Session { +@@ -3147,7 +3181,14 @@ impl Session { grant_root, }); self.send_event(turn_context, event).await; @@ -1987,7 +2022,7 @@ index aba4971..93cec01 100644 } #[expect( -@@ -3163,7 +3187,7 @@ impl Session { +@@ -3163,7 +3204,7 @@ impl Session { cancellation_token: CancellationToken, ) -> Option { let turn_context = &step_context.turn; @@ -1996,7 +2031,7 @@ index aba4971..93cec01 100644 let Some(environment) = step_context .environments .turn_environments() -@@ -3231,6 +3255,13 @@ impl Session { +@@ -3231,6 +3272,13 @@ impl Session { ) => decision, }; if let Some(decision) = decision { @@ -2010,7 +2045,7 @@ index aba4971..93cec01 100644 let (permissions, scope) = match decision { ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } -@@ -3269,11 +3300,13 @@ impl Session { +@@ -3269,11 +3317,13 @@ impl Session { response, &context, ); @@ -2029,7 +2064,7 @@ index aba4971..93cec01 100644 return Some(response); } } -@@ -3282,6 +3315,15 @@ impl Session { +@@ -3282,6 +3332,15 @@ impl Session { let (tx_response, rx_response) = oneshot::channel(); let prev_entry = { let mut active = self.active_turn.lock().await; @@ -2045,7 +2080,7 @@ index aba4971..93cec01 100644 match active.as_mut() { Some(at) => { let mut ts = at.turn_state.lock().await; -@@ -3426,6 +3468,15 @@ impl Session { +@@ -3426,6 +3485,15 @@ impl Session { }; match entry { Some(entry) => { @@ -2061,7 +2096,7 @@ index aba4971..93cec01 100644 let sandbox_context = entry .environment .sandbox_context(/*additional_permissions*/ None); -@@ -3434,11 +3485,13 @@ impl Session { +@@ -3434,11 +3502,13 @@ impl Session { response, &sandbox_context.policy_context(), ); @@ -2080,7 +2115,7 @@ index aba4971..93cec01 100644 entry.tx_response.send(response).ok(); } None => { -@@ -3476,14 +3529,24 @@ impl Session { +@@ -3476,14 +3546,24 @@ impl Session { } } @@ -2109,7 +2144,7 @@ index aba4971..93cec01 100644 } match response.scope { PermissionGrantScope::Turn => { -@@ -3501,6 +3564,7 @@ impl Session { +@@ -3501,6 +3581,7 @@ impl Session { ); } } @@ -2209,7 +2244,7 @@ index 48fe180..3aee52b 100644 // Recheck the latest rules before applying the update. diff --git a/codex-rs/core/src/session/step_activation_tests.rs b/codex-rs/core/src/session/step_activation_tests.rs -index a61493a..a881b22 100644 +index a61493a..be54251 100644 --- a/codex-rs/core/src/session/step_activation_tests.rs +++ b/codex-rs/core/src/session/step_activation_tests.rs @@ -416,6 +416,13 @@ struct ActivationFixture { @@ -2247,7 +2282,7 @@ index a61493a..a881b22 100644 let prepared = session .new_turn_with_default_settings("step-activation-turn".to_string(), Default::default()) .await; -@@ -467,6 +481,389 @@ async fn activation_fixture(models: Vec) -> ActivationFixture { +@@ -467,6 +481,469 @@ async fn activation_fixture(models: Vec) -> ActivationFixture { } } @@ -2413,6 +2448,86 @@ index a61493a..a881b22 100644 +} + +#[tokio::test] ++async fn doxa_auto_refuses_an_execpolicy_amendment_answered_after_the_switch() { ++ use codex_protocol::approvals::ExecPolicyAmendment; ++ use codex_protocol::protocol::ReviewDecision; ++ ++ let amend = |command: &str| ReviewDecision::ApprovedExecpolicyAmendment { ++ proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![command.to_string()]), ++ }; ++ // Positive control: without the switch the same answer writes a durable rule, ++ // so the refusal below is not vacuous. ++ let control = activation_fixture_with_model_switching(activation_models(), false).await; ++ let control_home = control.session.codex_home().await; ++ std::fs::create_dir_all(&control_home).expect("create codex home"); ++ let control_rules = crate::exec_policy::default_policy_path(control_home.as_path()); ++ crate::session::handlers::exec_approval( ++ &control.session, ++ "control-exec".into(), ++ Some(control.turn.sub_id.clone()), ++ amend("doxa-control-prefix"), ++ ) ++ .await; ++ assert!( ++ std::fs::read_to_string(&control_rules) ++ .unwrap_or_default() ++ .contains("doxa-control-prefix") ++ ); ++ control.session.abort_all_tasks(TurnAbortReason::Replaced).await; ++ ++ let ActivationFixture { session, turn, .. } = ++ activation_fixture_with_model_switching(activation_models(), false).await; ++ let home = session.codex_home().await; ++ std::fs::create_dir_all(&home).expect("create codex home"); ++ let rules = crate::exec_policy::default_policy_path(home.as_path()); ++ let before = std::fs::read_to_string(&rules).ok(); ++ #[allow(deprecated)] ++ let cwd = turn.cwd.clone(); ++ let approval = session.request_command_approval( ++ &turn, ++ codex_protocol::approvals::ExecApprovalKind::Command, ++ crate::guardian::GuardianReviewContext::from(Arc::clone(&turn)).model_context(), ++ "pending-exec".into(), ++ /*approval_id*/ None, ++ /*environment_id*/ None, ++ vec!["doxa-switched-prefix".into()], ++ cwd.into(), ++ /*reason*/ None, ++ /*network_approval_context*/ None, ++ Some(ExecPolicyAmendment::new(vec!["doxa-switched-prefix".into()])), ++ /*additional_permissions*/ None, ++ /*available_decisions*/ None, ++ /*plugin_attribution_override*/ None, ++ ); ++ tokio::pin!(approval); ++ assert!(futures::poll!(&mut approval).is_pending()); ++ assert_eq!( ++ session ++ .apply_turn_settings( ++ &turn.sub_id, ++ TurnSettingsUpdate { ++ doxa_auto: true, ++ ..Default::default() ++ } ++ ) ++ .await, ++ TurnSettingsUpdateOutcome::Applied ++ ); ++ crate::session::handlers::exec_approval( ++ &session, ++ "pending-exec".into(), ++ Some(turn.sub_id.clone()), ++ amend("doxa-switched-prefix"), ++ ) ++ .await; ++ assert!(matches!(approval.await, ReviewDecision::Denied { .. })); ++ // The on-disk write precedes the in-memory policy update, so an unchanged ++ // rules file shows the amendment was never committed. ++ assert_eq!(std::fs::read_to_string(&rules).ok(), before); ++ session.abort_all_tasks(TurnAbortReason::Replaced).await; ++} ++ ++#[tokio::test] +async fn doxa_auto_structured_approval_preserves_review_metadata_when_denied() { + use crate::guardian::GuardianReviewContext; + use crate::tools::ApprovalContext; diff --git a/scripts/codex-protected/reviews/0.165.0-r2.json b/scripts/codex-protected/reviews/0.165.0-r2.json new file mode 100644 index 00000000..dc8ddeb8 --- /dev/null +++ b/scripts/codex-protected/reviews/0.165.0-r2.json @@ -0,0 +1,32 @@ +{ + "schema": 1, + "review_kind": "manual protected contract revision on unchanged upstream source; no automatic approval exception", + "before": { + "schema": 1, + "version": "0.165.0", + "tag": "rust-v0.165.0", + "source_commit": "360319d8dee62d87e2d59fc0c21805d79519cfed", + "patch_sha256": "128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484", + "contract": "doxa-precompact-fail-closed-v1", + "provider": "codex-0.165.0-precompact-v1" + }, + "after": { + "contract": "doxa-precompact-fail-closed-v1", + "patch_sha256": "376dc69901a97e62d19c6bf32377c4d464e93192340aa582d6992f2613f8ca46", + "provider": "codex-0.165.0-precompact-v1", + "schema": 1, + "source_commit": "360319d8dee62d87e2d59fc0c21805d79519cfed", + "tag": "rust-v0.165.0", + "version": "0.165.0" + }, + "patch_delta": "Adds only the live-auto guard in Session::persist_execpolicy_amendment (core/src/session/mod.rs) and its regression test (core/src/session/step_activation_tests.rs). All other hunks are byte-identical to 128293eb.", + "resolves": { + "execpolicy_amendment_after_switch": "persist_execpolicy_amendment, the only writer of execpolicy amendments, now holds the active_turn lock that apply_turn_settings uses to publish doxaAuto, and refuses to write once the active turn has switched. A stale exec approval with AcceptWithExecpolicyAmendment answered after the switch is still denied by request_command_approval and no longer leaves a durable allow rule in memory or on disk. The exec-policy update semaphore is a leaf lock, so holding active_turn across it cannot deadlock.", + "context_management_token_budget": "Host side, no patch change: DOXA passes features.context_management=false with the other protected overrides and refuses startup unless config/read reports it false. apply_experimental_context returns before enabling TokenBudget when ContextManagement is off. apply_model_defaults already honours the explicit features.token_budget override. The key exists, defaulting to false, in 0.163.0, 0.164.0 and 0.165.0." + }, + "compatibility": "Retained contracts stay keyed by version, so the replaced 128293eb patch is not added to retained.json; it remains in the installer migration registry and as PREVIOUS_PATCHES for rollback. Docker sessions saved with the beta.76 provider are refused on resume with their data retained, and resume with beta.76.", + "required_new_regression": "doxa_auto_refuses_an_execpolicy_amendment_answered_after_the_switch", + "regression_negative_control": "With the guard disabled the regression fails at its unchanged-rules-file assertion: the approval is denied but the rule is written.", + "compiled_verification": "Separate hash-bound build and compiled guard proof required; this static review record is never an acceptance receipt.", + "automatic_sensitive_path_gate_changed": false +} diff --git a/scripts/install_codex_protected.py b/scripts/install_codex_protected.py index ff0bfb61..c7bda8b9 100755 --- a/scripts/install_codex_protected.py +++ b/scripts/install_codex_protected.py @@ -24,15 +24,16 @@ CONTRACT = "doxa-precompact-fail-closed-v1" PROVIDER = "codex-0.165.0-precompact-v1" PATCH = Path(__file__).resolve().parent / "codex-protected/precompact.patch" -PATCH_SHA256 = "128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484" +PATCH_SHA256 = "376dc69901a97e62d19c6bf32377c4d464e93192340aa582d6992f2613f8ca46" LEGACY_PATCH_SHA256 = "d6c8a41c0370c12dcace10d6babe13de7852f0095fed7b46289b38e7a6cd0f4b" -PREVIOUS_SOURCE = '6aaac694803004a63c6f393d153c96d22521a6b9' -PREVIOUS_PROVIDER = 'codex-0.164.0-precompact-v1' -PREVIOUS_PATCHES = {'f8759872787029e5bf7064fe5be7166c5367b1db09b4b60362ea625117658924'} +PREVIOUS_SOURCE = '360319d8dee62d87e2d59fc0c21805d79519cfed' +PREVIOUS_PROVIDER = 'codex-0.165.0-precompact-v1' +PREVIOUS_PATCHES = {'128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484'} # Installer migration/rollback trust only. The native launcher stays current-only. # Never infer identities from an active receipt or discard older reviewed entries. MAX_REVIEWED_INSTALLATIONS = 256 REVIEWED_INSTALLATIONS = ( + ('360319d8dee62d87e2d59fc0c21805d79519cfed', '128293eb2e10aebdff916ab1d8fbce30fa10eba2dfac01cc501f9c1591034484', 'codex-0.165.0-precompact-v1'), ('6aaac694803004a63c6f393d153c96d22521a6b9', 'f8759872787029e5bf7064fe5be7166c5367b1db09b4b60362ea625117658924', 'codex-0.164.0-precompact-v1'), ('83086188ae0614bd882818ff1aa23f3194962012', '3a7b8c09b1f95bc523611202005aa71e6ba14d23f26a3833249487385e013e9c', 'codex-0.163.0-precompact-v1'), ('b412ff32c417f855c2b2d1581b77058eed87c84b', '23bccbb08344fc70d1fd8a482b19814ca7f8edff07d478091b8c703e37a7ec6d', 'codex-0.156.1-precompact-v1'),