[0.160] Backport maintenance-line catalog and security reminder updates (#49763)

Bring the frozen 0.160 candidate into parity with the fixes delivered on
the 0.159 maintenance line, without advancing it to main.

Includes clean cherry-picks of #49318 (bundled model catalog), #49339
(Bedrock catalogs), and #49715 (optional account-security setup
reminders). Both Windows console fixes are already present in the target
and are not duplicated.

All three stable patch IDs match upstream. The catalog changes are
applied incrementally to the frozen candidate. This line includes the
upstream catalog-dedup snapshot update; the 0.159 backport omitted that
snapshot because its test was absent there. Server-side banner
eligibility and rollout remain unchanged.

Validation: clean application on
917ab70825 and whitespace checks passed.
Exact-backport CI is pending. Local Bazel validation is currently
blocked before compilation by the environment's V8 download
certificate-chain error.

This prepares a frozen alpha hotfix candidate, not a stable 0.160
release.

---------

Co-authored-by: Celia Chen <celia@openai.com>
Co-authored-by: dennyku <dennyku@openai.com>
This commit is contained in:
andrewgu-oai
2026-09-30 16:06:27 -07:00
committed by GitHub
co-authored by Celia Chen dennyku
parent 917ab70825
commit cb7799623b
52 changed files with 1171 additions and 113 deletions
@@ -115,7 +115,7 @@ model = "gpt-5.4-mini"
})
.await?;
assert_eq!(response.model, "openai.gpt-6-sol");
assert_eq!(response.model, "openai.gpt-6.1-sol");
Ok(())
}
@@ -245,6 +245,7 @@ async fn thread_start_provider_model_fallback_uses_bedrock_static_catalog() -> R
)
.await?;
for model in [
"openai.gpt-6.1-sol",
"openai.gpt-6-sol",
"openai.gpt-6-luna",
"openai.gpt-5.6-sol",
@@ -273,7 +274,7 @@ async fn thread_start_provider_model_fallback_uses_bedrock_static_catalog() -> R
supported_with_fallback.model,
unsupported_without_fallback.model,
],
vec!["openai.gpt-6-sol", "openai.gpt-5.5", "gpt-5.4-mini"]
vec!["openai.gpt-6.1-sol", "openai.gpt-5.5", "gpt-5.4-mini"]
);
Ok(())
}
@@ -292,6 +293,8 @@ async fn thread_start_bedrock_runtime_prefers_global_cross_region_models() -> Re
.await?;
for model in [
"global.openai.gpt-6.1-sol",
"us.openai.gpt-6.1-sol",
"global.openai.gpt-6-sol",
"us.openai.gpt-6-sol",
"global.openai.gpt-6-luna",
@@ -311,7 +314,7 @@ async fn thread_start_bedrock_runtime_prefers_global_cross_region_models() -> Re
/*allow_provider_model_fallback*/ true,
)
.await?;
assert_eq!(response.model, "global.openai.gpt-6-sol");
assert_eq!(response.model, "global.openai.gpt-6.1-sol");
Ok(())
}
@@ -6,7 +6,7 @@ Scenario: Astra starts in one workspace, switches for the next step of the activ
## Window 1
-- request 1 (turn) --
00:additional_tools/developer (3; hash=b1e101cdca5088a0):
00:additional_tools/developer (3; hash=a54e67018899e049):
- namespace/functions; hash=dead25325ac79c67
- custom/exec
- function/wait
@@ -14,7 +14,7 @@ Scenario: Astra starts in one workspace, switches for the next step of the activ
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -7,7 +7,7 @@ Scenario: Astra uses the catalog question schema to ask who a launch update is f
## Window 1
-- request 1 (request) --
00:additional_tools/developer (3; hash=9922060cb05f2122):
00:additional_tools/developer (3; hash=ae153e78f6722d2b):
- namespace/functions; hash=f1de44f63c977f3d
- custom/exec
- function/wait
@@ -15,7 +15,7 @@ Scenario: Astra uses the catalog question schema to ask who a launch update is f
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -6,7 +6,7 @@ Scenario: Astra receives host and handler timings on completed and failed code-m
## Window 1
-- request 1 (turn) --
00:additional_tools/developer (3; hash=32be5470c6e39932):
00:additional_tools/developer (3; hash=a1bfbfab4176467b):
- namespace/functions; hash=e7db0a45c4ec3157
- custom/exec
- function/wait
@@ -14,7 +14,7 @@ Scenario: Astra receives host and handler timings on completed and failed code-m
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -16,7 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (request) --
00:additional_tools/developer (3; hash=61ac4d92c828cbf9):
00:additional_tools/developer (3; hash=e33e09a2c1478914):
- namespace/functions; hash=99a630cff25563aa
- custom/exec
- function/wait
@@ -24,7 +24,7 @@ Settings:
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -6,7 +6,7 @@ Scenario: Astra receives new user input after its unfinished response is interru
## Window 1
-- request 1 (request) --
00:additional_tools/developer (3; hash=32be5470c6e39932):
00:additional_tools/developer (3; hash=a1bfbfab4176467b):
- namespace/functions; hash=e7db0a45c4ec3157
- custom/exec
- function/wait
@@ -14,7 +14,7 @@ Scenario: Astra receives new user input after its unfinished response is interru
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -6,7 +6,7 @@ Scenario: Astra continues after new user input yields a running code cell, then
## Window 1
-- request 1 (turn) --
00:additional_tools/developer (3; hash=07af97f334a32933):
00:additional_tools/developer (3; hash=b035de0e31ab087e):
- namespace/functions; hash=a0b6a7ae1ecf9f28
- custom/exec
- function/wait
@@ -14,7 +14,7 @@ Scenario: Astra continues after new user input yields a running code cell, then
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -16,7 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:additional_tools/developer (3; hash=32be5470c6e39932):
00:additional_tools/developer (3; hash=a1bfbfab4176467b):
- namespace/functions; hash=e7db0a45c4ec3157
- custom/exec
- function/wait
@@ -24,7 +24,7 @@ Settings:
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -187,7 +187,7 @@ Settings:
## Window 2 (after request 3: input diverged at item 02)
Settings: same as window 1
-- request 4 (turn) --
00:additional_tools/developer (3; hash=32be5470c6e39932):
00:additional_tools/developer (3; hash=a1bfbfab4176467b):
- namespace/functions; hash=e7db0a45c4ec3157
- custom/exec
- function/wait
@@ -195,7 +195,7 @@ Settings: same as window 1
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -16,7 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:additional_tools/developer (3; hash=32be5470c6e39932):
00:additional_tools/developer (3; hash=a1bfbfab4176467b):
- namespace/functions; hash=e7db0a45c4ec3157
- custom/exec
- function/wait
@@ -24,7 +24,7 @@ Settings:
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -110,7 +110,7 @@ Settings:
## Window 2 (after request 2: input diverged at item 00)
Settings: same as window 1
-- request 3 (turn) --
00:additional_tools/developer (3; hash=4327e247f74ba810):
00:additional_tools/developer (3; hash=7810769c6e12d379):
- namespace/functions; hash=8c17932a39355f2d
- custom/exec
- function/wait
@@ -118,7 +118,7 @@ Settings: same as window 1
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -16,7 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:additional_tools/developer (3; hash=4327e247f74ba810):
00:additional_tools/developer (3; hash=7810769c6e12d379):
- namespace/functions; hash=8c17932a39355f2d
- custom/exec
- function/wait
@@ -24,7 +24,7 @@ Settings:
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -32,14 +32,14 @@ Settings:
stream: true
text: {"verbosity":"low"}
tool_choice: "auto"
tools (7; hash=884bfb8fc9050f65):
tools (7; hash=7a45a45148e7ea6a):
- custom/exec: Catalog exec instructions. Default yield: 1234 ms. `image(imageUrlOrItem: string | { image_url: s...; hash=a49b91fd82908e18
- function/wait: Catalog wait instructions: resume the yielded cell.; args=[cell_id, max_tokens, terminate, yield_time_ms]; hash=67bc0f04da3505b3
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
- function/request_user_input_async: Ask the user one or more questions during ongoing work. Use this tool only to request missing inf...; args=[questions]; hash=d6d74f33db330162
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -16,8 +16,8 @@ Settings:
stream: true
text: {"verbosity":"low"}
tool_choice: "auto"
tools (4; hash=78e3b4e156c8704f):
- custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=6382c54728ef9e96
tools (4; hash=ee397215c4055252):
- custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=485f26f17d4a2899
- function/wait: Waits on a yielded `exec` cell and returns new output or completion. - Use `wait` only after `exe...; args=[cell_id, max_tokens, terminate, yield_time_ms]; hash=6692d8f6c55be489
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
- web_search; hash=e214d4db1bc49e89
@@ -16,8 +16,8 @@ Settings:
stream: true
text: {"verbosity":"low"}
tool_choice: "auto"
tools (4; hash=da1f22cdbb122728):
- custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=bd4bc63f8dac64cb
tools (4; hash=045ecf3b2d3b6af5):
- custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=e9cdd499ac568220
- function/wait: Waits on a yielded `exec` cell and returns new output or completion. - Use `wait` only after `exe...; args=[cell_id, max_tokens, terminate, yield_time_ms]; hash=6692d8f6c55be489
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
- web_search; hash=e214d4db1bc49e89
@@ -48,8 +48,8 @@ Settings:
## Window 2 (after request 2)
Settings: relative to window 1
model: "prefixes-cleared"
tools (4; hash=e51735744389fb0e):
~ custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=1ef6e3872d1dfe3f
tools (4; hash=dec6fdcb7bf3fec7):
~ custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=39e1eedcbc703718
-- request 3 (turn) --
00:message/developer:
<PERMISSIONS_INSTRUCTIONS>
@@ -32,14 +32,14 @@ Settings:
stream: true
text: {"verbosity":"low"}
tool_choice: "auto"
tools (7; hash=de6aceb7b0185d08):
tools (7; hash=4a2648ce3a65d327):
- custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=50ea9b7f60e145b5
- function/wait: Waits on a yielded `exec` cell and returns new output or completion. - Use `wait` only after `exe...; args=[cell_id, max_tokens, terminate, yield_time_ms]; hash=6692d8f6c55be489
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
- function/request_user_input_async: Ask the user one or more questions during ongoing work. Use this tool only to request missing inf...; args=[questions]; hash=d6d74f33db330162
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -16,7 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:additional_tools/developer (3; hash=48937f38e1083468):
00:additional_tools/developer (3; hash=78c6350cb8fdad81):
- namespace/functions; hash=e7db0a45c4ec3157
- custom/exec
- function/wait
@@ -24,7 +24,7 @@ Settings:
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=e7005987945a10f0
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=170baa4086bfe3cb
- function/followup_task
- function/interrupt_agent
- function/list_agents
@@ -16,7 +16,7 @@ Settings:
stream: true
text: {"verbosity":"low"}
tool_choice: "auto"
tools (11; hash=1974ec5490da160d):
tools (11; hash=14a76ba72d609d9e):
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
@@ -25,7 +25,7 @@ Settings:
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=b109e235997460a9
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
- function/close_agent
- function/resume_agent
- function/send_input
@@ -16,7 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:additional_tools/developer (3; hash=c803bbbd445f67cc):
00:additional_tools/developer (3; hash=853a5788ad239c35):
- namespace/functions; hash=48b2ce07cd26a39b
- custom/exec
- function/wait
@@ -24,7 +24,7 @@ Settings:
- function/request_user_input_async
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
- function/sleep
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=fd994b11525da73c
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=b263e3c84ad07c37
- function/followup_task
- function/interrupt_agent
- function/list_agents
+1
View File
@@ -81,6 +81,7 @@ const AMAZON_BEDROCK_RUNTIME_PROVIDER_NAME: &str = "Amazon Bedrock Runtime";
pub const AMAZON_BEDROCK_RUNTIME_PROVIDER_ID: &str = "amazon-bedrock-runtime";
pub const AMAZON_BEDROCK_GPT_5_5_MODEL_ID: &str = "openai.gpt-5.5";
pub const AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID: &str = "openai.gpt-5.6-sol";
pub const AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID: &str = "openai.gpt-6.1-sol";
pub const AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID: &str = "openai.gpt-6-sol";
pub const AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID: &str = "openai.gpt-6-luna";
pub const AMAZON_BEDROCK_GPT_6_ASTRA_MODEL_ID: &str = "openai.gpt-6-astra";
@@ -2,6 +2,7 @@ use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_5_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_ASTRA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID;
@@ -17,6 +18,7 @@ const GPT_5_BEDROCK_CONTEXT_WINDOW: i64 = 272_000;
const GPT_5_6_SOL_OPENAI_MODEL_ID: &str = "gpt-5.6-sol";
const GPT_5_6_TERRA_OPENAI_MODEL_ID: &str = "gpt-5.6-terra";
const GPT_5_6_LUNA_OPENAI_MODEL_ID: &str = "gpt-5.6-luna";
const GPT_6_1_SOL_OPENAI_MODEL_ID: &str = "gpt-6.1-sol";
const GPT_6_SOL_OPENAI_MODEL_ID: &str = "gpt-6-sol";
const GPT_6_LUNA_OPENAI_MODEL_ID: &str = "gpt-6-luna";
const GPT_6_ASTRA_OPENAI_MODEL_ID: &str = "gpt-6-astra";
@@ -26,9 +28,9 @@ pub(crate) fn static_model_catalog() -> ModelsResponse {
normalize_bedrock_catalog(ModelsResponse {
models: vec![
bedrock_model(
bundled_openai_model(GPT_6_SOL_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID,
"GPT-6 Sol",
bundled_openai_model(GPT_6_1_SOL_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID,
"GPT-6.1 Sol",
/*priority*/ 0,
),
bedrock_model(
@@ -37,35 +39,41 @@ pub(crate) fn static_model_catalog() -> ModelsResponse {
"GPT-6-Astra",
/*priority*/ 1,
),
bedrock_model(
bundled_openai_model(GPT_6_SOL_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID,
"GPT-6 Sol",
/*priority*/ 2,
),
bedrock_model(
bundled_openai_model(GPT_6_LUNA_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID,
"GPT-6 Luna",
/*priority*/ 2,
/*priority*/ 3,
),
bedrock_model(
bundled_openai_model(GPT_5_6_SOL_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID,
"GPT-5.6 Sol",
/*priority*/ 3,
/*priority*/ 4,
),
bedrock_model(
bundled_openai_model(GPT_5_6_TERRA_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID,
"GPT-5.6 Terra",
/*priority*/ 4,
/*priority*/ 5,
),
bedrock_model(
bundled_openai_model(GPT_5_6_LUNA_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID,
"GPT-5.6 Luna",
/*priority*/ 5,
/*priority*/ 6,
),
gpt_5_bedrock_model(
GPT_5_5_OPENAI_MODEL_ID,
AMAZON_BEDROCK_GPT_5_5_MODEL_ID,
"GPT-5.5",
/*priority*/ 6,
/*priority*/ 7,
),
],
})
@@ -161,8 +169,9 @@ mod tests {
.map(|model| model.slug.as_str())
.collect::<Vec<_>>(),
vec![
AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID,
AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID,
AMAZON_BEDROCK_GPT_6_ASTRA_MODEL_ID,
AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID,
AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID,
AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID,
AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID,
@@ -189,7 +198,7 @@ mod tests {
.collect::<Vec<_>>(),
vec![
(
AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID,
AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID,
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
Some(872_000),
WebSearchToolType::Text,
@@ -200,6 +209,12 @@ mod tests {
Some(872_000),
WebSearchToolType::Text,
),
(
AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID,
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
Some(872_000),
WebSearchToolType::Text,
),
(
AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID,
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
@@ -277,35 +292,41 @@ mod tests {
let catalog = static_model_catalog();
for (mut expected, slug, display_name, priority) in [
(
bundled_openai_model(GPT_6_1_SOL_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID,
"GPT-6.1 Sol",
0,
),
(
bundled_openai_model(GPT_6_SOL_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID,
"GPT-6 Sol",
0,
2,
),
(
bundled_openai_model(GPT_6_LUNA_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID,
"GPT-6 Luna",
2,
3,
),
(
bundled_openai_model(GPT_5_6_SOL_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID,
"GPT-5.6 Sol",
3,
4,
),
(
bundled_openai_model(GPT_5_6_TERRA_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID,
"GPT-5.6 Terra",
4,
5,
),
(
bundled_openai_model(GPT_5_6_LUNA_OPENAI_MODEL_ID),
AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID,
"GPT-5.6 Luna",
5,
6,
),
(
bundled_openai_model(GPT_6_ASTRA_OPENAI_MODEL_ID),
@@ -1,6 +1,7 @@
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_ASTRA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID;
@@ -21,6 +22,7 @@ pub(super) fn static_runtime_model_catalog() -> ModelsResponse {
AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID
| AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID
| AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID
| AMAZON_BEDROCK_GPT_6_1_SOL_MODEL_ID
| AMAZON_BEDROCK_GPT_6_ASTRA_MODEL_ID
| AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID
| AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID
@@ -18,25 +18,27 @@ fn runtime_catalog_includes_supported_cross_region_models_in_priority_order() {
))
.collect::<Vec<_>>(),
vec![
("global.openai.gpt-6-sol", "GPT-6 Sol (Global)", 0),
("global.openai.gpt-6.1-sol", "GPT-6.1 Sol (Global)", 0),
("global.openai.gpt-6-astra", "GPT-6-Astra (Global)", 1),
("global.openai.gpt-6-luna", "GPT-6 Luna (Global)", 2),
("global.openai.gpt-5.6-sol", "GPT-5.6 Sol (Global)", 3),
("global.openai.gpt-5.6-terra", "GPT-5.6 Terra (Global)", 4),
("global.openai.gpt-5.6-luna", "GPT-5.6 Luna (Global)", 5),
("us.openai.gpt-6-sol", "GPT-6 Sol (US cross-region)", 6),
("us.openai.gpt-6-astra", "GPT-6-Astra (US cross-region)", 7),
("us.openai.gpt-6-luna", "GPT-6 Luna (US cross-region)", 8),
("us.openai.gpt-5.6-sol", "GPT-5.6 Sol (US cross-region)", 9),
("global.openai.gpt-6-sol", "GPT-6 Sol (Global)", 2),
("global.openai.gpt-6-luna", "GPT-6 Luna (Global)", 3),
("global.openai.gpt-5.6-sol", "GPT-5.6 Sol (Global)", 4),
("global.openai.gpt-5.6-terra", "GPT-5.6 Terra (Global)", 5),
("global.openai.gpt-5.6-luna", "GPT-5.6 Luna (Global)", 6),
("us.openai.gpt-6.1-sol", "GPT-6.1 Sol (US cross-region)", 7),
("us.openai.gpt-6-astra", "GPT-6-Astra (US cross-region)", 8),
("us.openai.gpt-6-sol", "GPT-6 Sol (US cross-region)", 9),
("us.openai.gpt-6-luna", "GPT-6 Luna (US cross-region)", 10),
("us.openai.gpt-5.6-sol", "GPT-5.6 Sol (US cross-region)", 11),
(
"us.openai.gpt-5.6-terra",
"GPT-5.6 Terra (US cross-region)",
10
12
),
(
"us.openai.gpt-5.6-luna",
"GPT-5.6 Luna (US cross-region)",
11
13
),
]
);
@@ -59,7 +61,7 @@ fn runtime_catalog_disables_web_search_without_overriding_review_models() {
.collect::<Vec<_>>(),
vec![
(
"global.openai.gpt-6-sol",
"global.openai.gpt-6.1-sol",
None,
false,
Some(MultiAgentVersion::V1),
@@ -70,6 +72,12 @@ fn runtime_catalog_disables_web_search_without_overriding_review_models() {
false,
Some(MultiAgentVersion::V1),
),
(
"global.openai.gpt-6-sol",
None,
false,
Some(MultiAgentVersion::V1),
),
(
"global.openai.gpt-6-luna",
None,
@@ -95,7 +103,7 @@ fn runtime_catalog_disables_web_search_without_overriding_review_models() {
Some(MultiAgentVersion::V1),
),
(
"us.openai.gpt-6-sol",
"us.openai.gpt-6.1-sol",
None,
false,
Some(MultiAgentVersion::V1),
@@ -106,6 +114,12 @@ fn runtime_catalog_disables_web_search_without_overriding_review_models() {
false,
Some(MultiAgentVersion::V1),
),
(
"us.openai.gpt-6-sol",
None,
false,
Some(MultiAgentVersion::V1),
),
(
"us.openai.gpt-6-luna",
None,
+6 -3
View File
@@ -1267,6 +1267,7 @@ printf '%s\n' '{"AccessKeyId":"exported","SecretAccessKey":"secret"}'
.await;
assert_eq!(uncached_catalog, catalog);
for slug in [
"openai.gpt-6.1-sol",
"openai.gpt-6-sol",
"openai.gpt-6-luna",
"openai.gpt-5.6-sol",
@@ -1297,8 +1298,9 @@ printf '%s\n' '{"AccessKeyId":"exported","SecretAccessKey":"secret"}'
assert_eq!(
models,
vec![
("openai.gpt-6-sol", "GPT-6 Sol"),
("openai.gpt-6.1-sol", "GPT-6.1 Sol"),
("openai.gpt-6-astra", "GPT-6-Astra"),
("openai.gpt-6-sol", "GPT-6 Sol"),
("openai.gpt-6-luna", "GPT-6 Luna"),
("openai.gpt-5.6-sol", "GPT-5.6 Sol"),
("openai.gpt-5.6-terra", "GPT-5.6 Terra"),
@@ -1319,8 +1321,9 @@ printf '%s\n' '{"AccessKeyId":"exported","SecretAccessKey":"secret"}'
.map(|preset| preset.model.as_str())
.collect::<Vec<_>>(),
vec![
"openai.gpt-6-sol",
"openai.gpt-6.1-sol",
"openai.gpt-6-astra",
"openai.gpt-6-sol",
"openai.gpt-6-luna",
"openai.gpt-5.6-sol",
"openai.gpt-5.6-terra",
@@ -1334,7 +1337,7 @@ printf '%s\n' '{"AccessKeyId":"exported","SecretAccessKey":"secret"}'
.find(|preset| preset.is_default)
.expect("Bedrock catalog should have a default model");
assert_eq!(default_model.model, "openai.gpt-6-sol");
assert_eq!(default_model.model, "openai.gpt-6.1-sol");
}
#[tokio::test]
File diff suppressed because one or more lines are too long
@@ -307,6 +307,7 @@ impl App {
self.agents_overview.usage_disabled = false;
self.repaint_agents_overview();
self.chat_widget.cyber_policy_notice = Default::default();
self.chat_widget.invalidate_security_setup();
if let Some(crate::pager_overlay::Overlay::Analytics(view)) = &mut self.overlay {
view.refresh();
}
@@ -341,6 +342,12 @@ impl App {
has_codex_backend_auth,
);
if self.chat_widget.has_chatgpt_account() {
crate::security_setup::prefetch(
&self.config,
app_server_client,
self.app_event_tx.clone(),
self.chat_widget.security_setup_request_id,
);
crate::daybreak::prefetch_notice(
&self.config,
app_server_client,
+6
View File
@@ -1237,6 +1237,12 @@ impl App {
elicitation_target: None,
});
}
AppEvent::SecuritySetupLoaded { request_id, identity, notice } => {
tracing::debug!(current = request_id == self.chat_widget.security_setup_request_id, "handling security setup notice");
if request_id == self.chat_widget.security_setup_request_id {
self.chat_widget.show_security_setup(identity, notice);
}
}
AppEvent::OpenUrlInBrowser { url } => {
self.open_url_in_browser(url);
}
+6
View File
@@ -479,6 +479,12 @@ impl App {
matches!(bootstrap.auth_mode, Some(TelemetryAuthMode::Chatgpt)),
);
if self.chat_widget.has_chatgpt_account() {
crate::security_setup::prefetch(
&self.config,
app_server,
self.app_event_tx.clone(),
self.chat_widget.security_setup_request_id,
);
crate::daybreak::prefetch_notice(
&self.config,
app_server,
@@ -519,6 +519,7 @@ impl App {
AppServerTarget::LocalDaemon { .. }
));
chat_widget.inherit_backend_banner_state(&mut self.chat_widget);
chat_widget.inherit_security_setup(&mut self.chat_widget);
for (thread_id, entry) in self.agent_navigation.ordered_threads() {
chat_widget.set_collab_agent_metadata(
thread_id,
+6
View File
@@ -1019,6 +1019,12 @@ See the Codex keymap documentation for supported actions and examples."
// already has data and available reset credits can be surfaced, without
// delaying the initial frame render.
if requires_openai_auth && has_chatgpt_account {
crate::security_setup::prefetch(
&app.config,
&app_server,
app.app_event_tx.clone(),
app.chat_widget.security_setup_request_id,
);
crate::daybreak::prefetch_notice(
&app.config,
&app_server,
+2
View File
@@ -4,6 +4,8 @@
mod daybreak_tests;
#[path = "tests/math_interruption_tests.rs"]
mod math_interruption_tests;
#[path = "tests/security_setup_tests.rs"]
mod security_setup_tests;
#[path = "tests/advanced_reasoning_tests.rs"]
mod advanced_reasoning_tests;
@@ -0,0 +1,242 @@
//! Exercises the authenticated reminder transport against a local HTTP fixture.
use super::disconnect::serve_reconnect_requests;
use super::*;
use crate::app_server_session::ThreadParamsMode;
use crate::security_setup::Identity;
use app_test_support::ChatGptAuthFixture;
use app_test_support::write_chatgpt_auth;
use codex_app_server_client::AppServerEvent;
use codex_app_server_protocol::AccountUpdatedNotification;
use codex_app_server_protocol::AuthMode;
use codex_login::AuthCredentialsStoreMode;
use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::net::TcpListener;
#[tokio::test]
async fn security_setup_fetch_with_default_features_uses_authenticated_codex_endpoint() -> Result<()>
{
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
let backend = wiremock::MockServer::start().await;
app.config.chatgpt_base_url = backend.uri();
app.config.cli_auth_credentials_store_mode = AuthCredentialsStoreMode::File;
std::fs::write(
app.config.codex_home.join("config.toml"),
format!("chatgpt_base_url = {:?}\n", backend.uri()),
)?;
write_chatgpt_auth(
&app.config.codex_home,
ChatGptAuthFixture::new("test-token")
.account_id("account")
.chatgpt_user_id("user"),
AuthCredentialsStoreMode::File,
)
.expect("write synthetic auth");
app_test_support::mount_workspace_routing(&backend).await;
let mut server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
wiremock::Mock::given(wiremock::matchers::path("/wham/security-setup"))
.and(wiremock::matchers::header(
"authorization",
"Bearer test-token",
))
.and(wiremock::matchers::header("ChatGPT-Account-ID", "account"))
.and(wiremock::matchers::header(
"User-Agent",
codex_login::default_client::get_codex_user_agent(),
))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(json!({
"notice": {
"title": "Keep using Daybreak mode", "description": "Set up security.",
"action": {"label": "Set up security", "url": "https://chatgpt.com/cyber"}
}
})))
.expect(3)
.mount(&backend)
.await;
let request_id = app.chat_widget.security_setup_request_id;
let mut tui = crate::tui::test_support::make_test_tui()?;
let init = app.chatwidget_init_for_forked_or_resumed_thread(
&mut tui,
app.config.clone(),
/*initial_user_message*/ None,
);
app.replace_chat_widget(ChatWidget::new_with_app_event(init));
assert_eq!(app.chat_widget.security_setup_request_id, request_id);
crate::security_setup::prefetch(&app.config, &server, app.app_event_tx.clone(), request_id);
let notice = tokio::time::timeout(Duration::from_secs(5), async {
loop {
if let AppEvent::SecuritySetupLoaded {
request_id: actual,
identity,
notice,
} = events.recv().await.unwrap()
{
assert_eq!(actual, request_id);
assert_eq!(
identity,
Identity {
account: "account".into(),
user: "user".into()
}
);
break notice;
}
}
})
.await?;
assert!(notice.valid());
let identity = Identity {
account: "account".into(),
user: "user".into(),
};
app.handle_event(
&mut tui,
&mut server,
AppEvent::SecuritySetupLoaded {
request_id,
identity: identity.clone(),
notice: notice.clone(),
},
)
.await?;
assert!(render_bottom_popup(&app.chat_widget, /*width*/ 70).contains("Set up security"));
app.chat_widget
.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 70).contains("Set up security"));
// Reconnect can emit AccountUpdated for the same identity more than once.
for _ in 0..2 {
let stale_request_id = app.chat_widget.security_setup_request_id;
app.handle_app_server_event(
&server,
AppServerEvent::ServerNotification(Box::new(ServerNotification::AccountUpdated(
AccountUpdatedNotification {
auth_mode: Some(AuthMode::Chatgpt),
plan_type: None,
},
))),
)
.await;
let request_id = app.chat_widget.security_setup_request_id;
assert_ne!(request_id, stale_request_id);
app.handle_event(
&mut tui,
&mut server,
AppEvent::SecuritySetupLoaded {
request_id: stale_request_id,
identity: Identity {
account: "stale-account".into(),
..identity.clone()
},
notice: notice.clone(),
},
)
.await?;
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 70).contains("Set up security"));
let init = app.chatwidget_init_for_forked_or_resumed_thread(
&mut tui,
app.config.clone(),
/*initial_user_message*/ None,
);
app.replace_chat_widget(ChatWidget::new_with_app_event(init));
let event = tokio::time::timeout(Duration::from_secs(5), async {
loop {
let event = events.recv().await.unwrap();
if let AppEvent::SecuritySetupLoaded {
request_id: actual,
identity: actual_identity,
..
} = &event
{
assert_eq!(*actual, request_id);
assert_eq!(actual_identity, &identity);
break event;
}
}
})
.await?;
app.handle_event(&mut tui, &mut server, event).await?;
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 70).contains("Set up security"));
}
backend.verify().await;
server.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn security_setup_skips_fetch_when_server_auth_does_not_match_saved_login() -> Result<()> {
for (auth_method, auth_token) in [
(
Some(AuthMode::ChatgptAuthTokens),
Some("other-account-token"),
),
(Some(AuthMode::ChatgptAuthTokens), Some("saved-token")),
(Some(AuthMode::Chatgpt), Some("other-account-token")),
(Some(AuthMode::Chatgpt), None),
(None, None),
] {
let (mut app, _events, _ops) = make_test_app_with_channels().await;
let backend = wiremock::MockServer::start().await;
app.config.chatgpt_base_url = backend.uri();
app.config.cli_auth_credentials_store_mode = AuthCredentialsStoreMode::File;
write_chatgpt_auth(
&app.config.codex_home,
ChatGptAuthFixture::new("saved-token")
.account_id("saved-account")
.chatgpt_user_id("saved-user"),
AuthCredentialsStoreMode::File,
)
.expect("write synthetic auth");
wiremock::Mock::given(wiremock::matchers::path("/wham/security-setup"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(json!({
"notice": {
"title": "Keep using Daybreak mode", "description": "Set up security.",
"action": {"label": "Set up security", "url": "https://chatgpt.com/cyber"}
}
})))
.expect(0)
.mount(&backend)
.await;
let listener = TcpListener::bind("127.0.0.1:0").await?;
let endpoint = crate::resolve_remote_addr(&format!("ws://{}", listener.local_addr()?))?;
let daemon = tokio::spawn(async move {
let (stream, _) = listener.accept().await?;
serve_reconnect_requests(tokio_tungstenite::accept_async(stream).await?, |request| {
assert_eq!(request.method, "getAuthStatus");
assert_eq!(
request.params,
Some(json!({"includeToken": true, "refreshToken": false}))
);
std::future::ready(Some(json!({"result": {
"authMethod": auth_method, "authToken": auth_token,
"requiresOpenaiAuth": true
}})))
})
.await
});
let server = AppServerSession::new(
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Embedded,
);
let (tx, mut events) = mpsc::unbounded_channel();
crate::security_setup::prefetch(
&app.config,
&server,
AppEventSender::new(tx),
app.chat_widget.security_setup_request_id,
);
// The fetch owns the only sender, so channel closure proves it completed.
assert!(
tokio::time::timeout(Duration::from_secs(5), events.recv())
.await?
.is_none()
);
backend.verify().await;
server.shutdown().await?;
let methods = daemon.await??;
assert!(methods.iter().any(|method| method == "getAuthStatus"));
}
Ok(())
}
@@ -14,7 +14,7 @@ attached:
› draft during startup
GPT-6-Astra default · /tmp/project
GPT-6.1-Sol default · /tmp/project
owned=true
startup:
@@ -27,4 +27,4 @@ attached:
› draft during startup
GPT-6-Astra default · /tmp/project
GPT-6.1-Sol default · /tmp/project
@@ -398,7 +398,7 @@ async fn fresh_startup_reads_destination_and_cleared_model_uses_catalog() -> Res
insta::assert_snapshot!(rendered, @r"
› Ask Codex to do anything
GPT-6-Astra high · <PROJECT>
GPT-6.1-Sol high · <PROJECT>
");
}
let expected_cwd = if override_cwd {
+5
View File
@@ -278,6 +278,11 @@ pub(crate) struct AgentsOverviewThreadRefresh {
#[allow(clippy::large_enum_variant)]
#[derive(Debug, IntoStaticStr)]
pub(crate) enum AppEvent {
SecuritySetupLoaded {
request_id: uuid::Uuid,
identity: crate::security_setup::Identity,
notice: crate::security_setup::Notice,
},
OpenDaemonMenu,
ConfirmDaemonUpdate(crate::update_action::DaemonUpdateSource),
RunDaemonUpdate(crate::update_action::DaemonUpdateSource),
@@ -159,6 +159,16 @@ impl From<ActionableBanner> for SelectionViewParams {
}
impl BottomPane {
pub(crate) fn transfer_inline_banner_from(&mut self, previous: &mut Self) {
self.inline_banner = previous.inline_banner.take();
if let Some(banner) = &mut self.inline_banner
&& let InlineBannerContent::Actions(view) = &mut banner.content
{
view.app_event_tx = self.app_event_tx.clone();
}
self.request_redraw();
}
pub(crate) fn show_actionable_banner(&mut self, banner: ActionableBanner) {
self.show_selection_view(banner.into());
}
@@ -304,7 +304,7 @@ pub(crate) struct ListSelectionView {
state: ScrollState,
completion: Option<ViewCompletion>,
pub(super) dismiss_after_child_accept: bool,
app_event_tx: AppEventSender,
pub(super) app_event_tx: AppEventSender,
is_searchable: bool,
search_query: String,
search_placeholder: Option<String>,
+5
View File
@@ -336,6 +336,7 @@ mod backend_banners;
mod compaction;
mod luna_reserve_model;
mod luna_reserve_return;
mod security_setup;
pub(crate) use backend_banners::AutomaticModelSwitchReason;
mod protocol;
mod protocol_requests;
@@ -574,6 +575,10 @@ pub(crate) struct ChatWidget {
clock_format: crate::clock_format::ClockFormat,
usage_notice_state: usage_notice::UsageNoticeState,
backend_banner_state: backend_banners::BackendBannerState,
pub(crate) security_setup_request_id: uuid::Uuid,
security_setup_presented: bool,
security_setup_identity: Option<crate::security_setup::Identity>,
security_setup_dismissed: bool,
automatic_model_switch_state: backend_banners::AutomaticModelSwitchState,
backend_banner_notice_model: Option<String>,
// Remember the account's Reserve entry notice across chats and transient banner refreshes.
@@ -376,9 +376,11 @@ impl ChatWidget {
}
fn observe_backend_banner_view(&mut self) {
let (shown, dismissed) = self.bottom_pane.inline_banner_lifecycle();
self.backend_banner_state.shown |= shown;
self.backend_banner_state.dismissed |= dismissed;
if self.backend_banner_state.presented.is_some() {
let (shown, dismissed) = self.bottom_pane.inline_banner_lifecycle();
self.backend_banner_state.shown |= shown;
self.backend_banner_state.dismissed |= dismissed;
}
self.backend_banner_state.dismissed |= self
.backend_banner_state
.picker_dismissed
@@ -441,6 +443,7 @@ impl ChatWidget {
}
self.bottom_pane
.dismiss_view_by_id(LUNA_RESERVE_RECOVERY_VIEW_ID);
self.clear_security_setup_banner();
match (is_reserve, content) {
(true, Some(content)) => {
self.bottom_pane.set_inline_banner(/*banner*/ None);
@@ -510,6 +513,7 @@ impl ChatWidget {
}
pub(crate) fn clear_backend_banner(&mut self) {
self.clear_security_setup_banner();
self.backend_banner_state = BackendBannerState::default();
self.backend_banner_notice_model = None;
self.bottom_pane
@@ -164,6 +164,10 @@ impl ChatWidget {
clock_format: crate::clock_format::ClockFormat::system(),
usage_notice_state: usage_notice::UsageNoticeState::default(),
backend_banner_state: backend_banners::BackendBannerState::default(),
security_setup_request_id: uuid::Uuid::new_v4(),
security_setup_presented: false,
security_setup_identity: None,
security_setup_dismissed: false,
automatic_model_switch_state: backend_banners::AutomaticModelSwitchState::default(),
backend_banner_notice_model: None,
luna_reserve_notice_account_id: None,
@@ -0,0 +1,80 @@
//! Presents the optional setup reminder without displacing account usage controls.
//! Dismissal survives banner replacement and account notifications until a different identity is verified.
use super::ChatWidget;
use crate::app_event::AppEvent;
use crate::bottom_pane::ActionableBanner;
use crate::bottom_pane::SelectionItem;
use crate::security_setup::Identity;
use crate::security_setup::Notice;
impl ChatWidget {
pub(crate) fn inherit_security_setup(&mut self, previous: &mut Self) {
// Startup and thread navigation replace the widget while the request is in flight.
// Preserve the occurrence, including its rendered/dismissed state.
self.security_setup_request_id = previous.security_setup_request_id;
self.security_setup_identity = previous.security_setup_identity.clone();
self.security_setup_dismissed = previous.security_setup_dismissed
|| (previous.security_setup_presented
&& previous.bottom_pane.inline_banner_lifecycle().1);
if previous.security_setup_presented && !self.has_applicable_backend_banner() {
self.bottom_pane
.transfer_inline_banner_from(&mut previous.bottom_pane);
self.security_setup_presented = true;
previous.security_setup_presented = false;
}
}
pub(crate) fn invalidate_security_setup(&mut self) {
// AccountUpdated can announce the same account after reconnect. Hide stale content
// and reject pending results, but retain dismissal until we verify the identity.
self.security_setup_request_id = uuid::Uuid::new_v4();
self.clear_security_setup_banner();
}
pub(super) fn clear_security_setup_banner(&mut self) {
// Capture this notice's dismissal before the shared slot is cleared or replaced.
// Once another banner owns the slot, its lifecycle must not affect this notice.
if self.security_setup_presented {
self.security_setup_dismissed |= self.bottom_pane.inline_banner_lifecycle().1;
self.bottom_pane.set_inline_banner(/*banner*/ None);
self.security_setup_presented = false;
}
}
pub(crate) fn show_security_setup(&mut self, identity: Identity, notice: Notice) {
if !notice.valid() {
return;
}
if self.security_setup_identity.as_ref() != Some(&identity) {
self.invalidate_security_setup();
self.security_setup_identity = Some(identity);
self.security_setup_dismissed = false;
}
// Reconnect fetches must preserve the current occurrence, including Esc dismissal.
if self.security_setup_presented || self.security_setup_dismissed {
return;
}
tracing::debug!(
has_backend_banner = self.has_applicable_backend_banner(),
"presenting security setup notice"
);
if self.has_applicable_backend_banner() {
return;
}
self.security_setup_presented = true;
self.bottom_pane.set_inline_banner(Some(ActionableBanner {
title: notice.title,
description: notice.description,
actions: vec![SelectionItem {
name: notice.action.label,
actions: vec![Box::new(move |tx| {
tx.send(AppEvent::OpenUrlInBrowser {
url: notice.action.url.clone(),
})
})],
..Default::default()
}],
..Default::default()
}));
}
}
@@ -5,12 +5,13 @@ expression: "render_bottom_popup(&chat, 100)"
Select Model and Effort
› 1. GPT-6 Sol (current) Workhorse model for coding and everyday work.
2. GPT-6-Astra Frontier intelligence for the most demanding work.
3. GPT-6 Luna Fast and affordable model for easier tasks.
4. GPT-5.6 Sol Older coding model for complex work.
5. GPT-5.6 Terra Older balanced model for straightforward work.
6. GPT-5.6 Luna Older fast and efficient model.
7. GPT-5.5 Legacy coding model.
› 1. GPT-6.1 Sol (current) Latest workhorse model for coding and everyday work.
2. GPT-6-Astra Frontier intelligence for the most demanding work.
3. GPT-6 Sol Previous generation workhorse model.
4. GPT-6 Luna Fast and affordable model for easier tasks.
5. GPT-5.6 Sol Older generation workhorse model.
6. GPT-5.6 Terra Older balanced model for straightforward work.
7. GPT-5.6 Luna Older fast and efficient model.
8. GPT-5.5 Legacy coding model.
enter select · esc back
@@ -5,13 +5,13 @@ expression: "render_bottom_popup(&chat, 100)"
Select Model and Effort
› 1. GPT-6 Sol (Global) (current) Workhorse model for coding and everyday work.
› 1. GPT-6.1 Sol (Global) (current) Latest workhorse model for coding and everyday work.
2. GPT-6-Astra (Global) Frontier intelligence for the most demanding work.
3. GPT-6 Luna (Global) Fast and affordable model for easier tasks.
4. GPT-5.6 Sol (Global) Older coding model for complex work.
5. GPT-5.6 Terra (Global) Older balanced model for straightforward work.
6. GPT-5.6 Luna (Global) Older fast and efficient model.
7. GPT-6 Sol (US cross-region) Workhorse model for coding and everyday work.
8. GPT-6-Astra (US cross-region) Frontier intelligence for the most demanding work.
3. GPT-6 Sol (Global) Previous generation workhorse model.
4. GPT-6 Luna (Global) Fast and affordable model for easier tasks.
5. GPT-5.6 Sol (Global) Older generation workhorse model.
6. GPT-5.6 Terra (Global) Older balanced model for straightforward work.
7. GPT-5.6 Luna (Global) Older fast and efficient model.
8. GPT-6.1 Sol (US cross-region) Latest workhorse model for coding and everyday work.
↓
enter select · esc back
@@ -5,12 +5,13 @@ expression: popup
Select Model and Effort
1. GPT-6-Astra (default) Frontier intelligence for the most demanding work.
2. GPT-6-Sol Workhorse model for coding and everyday work.
3. GPT-6-Luna Fast and affordable model for easier tasks.
4. GPT-5.6-Sol Older coding model for complex work.
5. GPT-5.6-Terra Older balanced model for straightforward work.
6. GPT-5.6-Luna Older fast and efficient model.
› 7. GPT-5.5 (current) Legacy coding model.
1. GPT-6.1-Sol (default) Latest workhorse model for coding and everyday work.
2. GPT-6-Astra Frontier intelligence for the most demanding work.
3. GPT-6-Sol Previous generation workhorse model.
4. GPT-6-Luna Fast and affordable model for easier tasks.
5. GPT-5.6-Sol Older generation workhorse model.
6. GPT-5.6-Terra Older balanced model for straightforward work.
7. GPT-5.6-Luna Older fast and efficient model.
› 8. GPT-5.5 (current) Legacy coding model.
enter select · esc back
+3
View File
@@ -321,3 +321,6 @@ mod list_spacing_tests;
#[path = "tests/question_notifications_tests.rs"]
mod question_notifications_tests;
#[path = "tests/security_setup_tests.rs"]
mod security_setup_tests;
@@ -13,13 +13,13 @@ async fn bedrock_astra_model_and_reasoning_pickers() {
(
"mantle",
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),
"openai.gpt-6-sol",
"openai.gpt-6.1-sol",
"openai.gpt-6-astra",
),
(
"runtime",
ModelProviderInfo::create_amazon_bedrock_runtime_provider(/*aws*/ None),
"global.openai.gpt-6-sol",
"global.openai.gpt-6.1-sol",
"global.openai.gpt-6-astra",
),
] {
@@ -30,6 +30,13 @@ async fn bedrock_astra_model_and_reasoning_pickers() {
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
)
.await;
assert_eq!(
presets
.iter()
.find(|preset| preset.is_default)
.map(|preset| preset.model.as_str()),
Some(default_model),
);
let astra = presets
.iter()
.find(|model| model.model == astra_model)
@@ -0,0 +1,190 @@
use super::*;
use crate::security_setup::Identity;
use crate::security_setup::Notice;
use serde_json::json;
fn setup_notice() -> Notice {
serde_json::from_value(json!({
"title":"Keep using Daybreak mode",
"description":"Set up Advanced Account Security with a hardware security key by October 1. Already Persona-verified? Add your key before October 15 to skip re-verification.",
"action":{"label":"Set up security","url":"https://chatgpt.com/cyber"}
}))
.unwrap()
}
fn setup_identity() -> Identity {
Identity {
account: "account".into(),
user: "user".into(),
}
}
#[tokio::test]
async fn security_setup_preserves_draft_action_and_dismissal_across_widgets() {
let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
chat.show_security_setup(setup_identity(), setup_notice());
let rendered = normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 70));
insta::assert_snapshot!("security_setup_banner", rendered);
let (mut replacement, mut events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
chat.app_event_tx
.voice_only
.store(/*val*/ true, std::sync::atomic::Ordering::Relaxed);
replacement.inherit_security_setup(&mut chat);
chat = replacement;
assert!(render_bottom_popup(&chat, /*width*/ 70).contains("Set up security"));
chat.handle_key_event(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));
assert!(
matches!(events.try_recv(), Ok(AppEvent::OpenUrlInBrowser { url }) if url == "https://chatgpt.com/cyber")
);
assert!(render_bottom_popup(&chat, /*width*/ 70).contains("Set up security"));
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(!render_bottom_popup(&chat, /*width*/ 70).contains("Set up security"));
let (mut replacement, _events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
replacement.inherit_security_setup(&mut chat);
replacement.show_security_setup(setup_identity(), setup_notice());
assert_eq!(
replacement.bottom_pane.inline_banner_lifecycle(),
(true, true)
);
assert!(!render_bottom_popup(&replacement, /*width*/ 70).contains("Set up security"));
replacement.invalidate_security_setup();
replacement.show_security_setup(setup_identity(), setup_notice());
assert!(!render_bottom_popup(&replacement, /*width*/ 70).contains("Set up security"));
// A verified change of account starts a new notice occurrence.
replacement.invalidate_security_setup();
replacement.show_security_setup(
Identity {
account: "other-account".into(),
..setup_identity()
},
setup_notice(),
);
assert!(render_bottom_popup(&replacement, /*width*/ 70).contains("Set up security"));
}
#[tokio::test]
async fn security_setup_dismissal_survives_usage_banner_replacement() {
for dismiss_security in [true, false] {
let (mut chat, _events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
chat.show_security_setup(setup_identity(), setup_notice());
assert!(render_bottom_popup(&chat, /*width*/ 70).contains("Set up security"));
if dismiss_security {
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
}
let response = codex_app_server_protocol::GetAccountRateLimitsResponse {
ordinary_usage_allowed: None,
account_id: Some("account".into()),
rate_limit_upsell: Some(json!({
"banner_type": "selected_model_limit", "model_slug": "test-model-a",
"title": "Selected model usage exhausted", "description": "Switch models.",
"presentation": "dismissible", "ctas": [],
})),
rate_limits: snapshot(/*percent*/ 100.0),
rate_limits_by_limit_id: None,
rate_limit_reset_credits: None,
};
chat.update_backend_banner(&response);
assert!(
render_bottom_popup(&chat, /*width*/ 70).contains("Selected model usage exhausted")
);
if !dismiss_security {
// Dismissing usage must not dismiss the security reminder instead.
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
}
// Account refresh must not read the replacement banner's dismissal state.
chat.invalidate_security_setup();
chat.clear_backend_banner();
chat.show_security_setup(setup_identity(), setup_notice());
let rendered = normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 70));
assert_eq!(rendered.contains("Set up security"), !dismiss_security);
if dismiss_security {
insta::assert_snapshot!("security_setup_dismissed_after_usage", rendered);
}
let (mut replacement, _events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
replacement.inherit_security_setup(&mut chat);
replacement.invalidate_security_setup();
replacement.show_security_setup(setup_identity(), setup_notice());
assert_eq!(
render_bottom_popup(&replacement, /*width*/ 70).contains("Set up security"),
!dismiss_security
);
// A fresh CLI run has no inherited dismissal and reminds the same eligible user.
let (mut fresh, _events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
fresh.show_security_setup(setup_identity(), setup_notice());
assert!(render_bottom_popup(&fresh, /*width*/ 70).contains("Set up security"));
}
}
#[tokio::test]
async fn security_setup_after_cutoff_wraps_in_narrow_terminal() {
let (mut chat, _events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
let mut notice = setup_notice();
notice.title = "Set up security for Daybreak mode".into();
notice.description = "Set up Advanced Account Security with a hardware security key. Already Persona-verified? Add your key before October 15 to skip re-verification. You can keep using Codex while you finish setup.".into();
chat.show_security_setup(setup_identity(), notice);
insta::assert_snapshot!(
"security_setup_after_cutoff",
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 48))
);
}
#[tokio::test]
async fn security_setup_after_persona_grace_wraps_in_narrow_terminal() {
let (mut chat, _events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
let mut notice = setup_notice();
notice.title = "Set up security for Daybreak mode".into();
notice.description = "Set up Advanced Account Security with a hardware security key. Verify each new hardware security key with Persona. You can keep using Codex while you finish setup.".into();
chat.show_security_setup(setup_identity(), notice);
insta::assert_snapshot!(
"security_setup_after_persona_grace",
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 48))
);
}
#[tokio::test]
async fn security_setup_respects_applicable_backend_banner() {
for state in ["visible", "dismissed", "another-model"] {
let (mut chat, _events, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
let response = codex_app_server_protocol::GetAccountRateLimitsResponse {
ordinary_usage_allowed: None,
account_id: Some("workspace-a".into()),
rate_limit_upsell: Some(json!({
"banner_type": "selected_model_limit", "model_slug": "test-model-a",
"title": "Selected model usage exhausted", "description": "Switch models.",
"presentation": "dismissible", "ctas": [],
})),
rate_limits: snapshot(/*percent*/ 25.0),
rate_limits_by_limit_id: None,
rate_limit_reset_credits: None,
};
chat.update_backend_banner(&response);
render_bottom_popup(&chat, /*width*/ 70);
match state {
"dismissed" => {
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
}
"another-model" => chat.set_model("test-model-b"),
"visible" => {}
_ => unreachable!(),
}
chat.show_security_setup(setup_identity(), setup_notice());
assert_eq!(
render_bottom_popup(&chat, /*width*/ 70).contains("Set up security"),
state != "visible"
);
if state == "another-model" {
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
chat.set_model("test-model-a");
assert!(
render_bottom_popup(&chat, /*width*/ 70).contains("Selected model usage exhausted")
);
}
}
}
@@ -0,0 +1,20 @@
---
source: tui/src/chatwidget/tests/security_setup_tests.rs
assertion_line: 43
expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 48))"
---
Set up security for Daybreak mode
Set up Advanced Account Security with a
hardware security key. Already Persona-
verified? Add your key before October 15 to
skip re-verification. You can keep using
Codex while you finish setup.
› 1. Set up security
Press a number to choose · esc to dismiss ·
type to continue
› Ask Codex to do anything
test-model-a default · /tmp/project
@@ -0,0 +1,19 @@
---
source: tui/src/chatwidget/tests/security_setup_tests.rs
assertion_line: 56
expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 48))"
---
Set up security for Daybreak mode
Set up Advanced Account Security with a
hardware security key. Verify each new
hardware security key with Persona. You can
keep using Codex while you finish setup.
› 1. Set up security
Press a number to choose · esc to dismiss ·
type to continue
› Ask Codex to do anything
test-model-a default · /tmp/project
@@ -0,0 +1,17 @@
---
source: tui/src/chatwidget/tests/security_setup_tests.rs
assertion_line: 19
expression: rendered
---
Keep using Daybreak mode
Set up Advanced Account Security with a hardware security key by
October 1. Already Persona-verified? Add your key before October
15 to skip re-verification.
› 1. Set up security
Press a number to choose · esc to dismiss · type to continue
› Ask Codex to do anything
test-model-a default · /tmp/project
@@ -0,0 +1,7 @@
---
source: tui/src/chatwidget/tests/security_setup_tests.rs
expression: rendered
---
› Ask Codex to do anything
test-model-a default · /tmp/project
+1
View File
@@ -131,6 +131,7 @@ mod experimental_features;
mod markdown_copy;
mod permission_discovery;
mod pets;
mod security_setup;
mod worktree_browser;
pub use custom_terminal::Terminal;
mod assistant_directives;
+164
View File
@@ -0,0 +1,164 @@
//! Optional account-security reminders for the local, ChatGPT-authenticated CLI.
//! The server owns eligibility, rollout and cutoff copy. Reads never block a turn;
//! no reminder analytics are sent. Unknown or unavailable state produces no notice.
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::app_server_session::AppServerSession;
use crate::legacy_core::config::Config;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::GetAuthStatusParams;
use codex_app_server_protocol::GetAuthStatusResponse;
use codex_app_server_protocol::RequestId;
use codex_http_client::ClientRouteClass;
use codex_http_client::RouteAwareClientPool;
use codex_login::CodexAuth;
use serde::Deserialize;
use std::time::Duration;
use uuid::Uuid;
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct Notice {
pub title: String,
pub description: String,
pub action: Action,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct Action {
pub label: String,
pub url: String,
}
#[derive(Deserialize)]
struct Response {
notice: Option<Notice>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Identity {
pub account: String,
pub user: String,
}
impl Identity {
fn from_auth(auth: &CodexAuth) -> Option<Self> {
Some(Self {
account: auth.get_account_id()?,
user: auth.get_chatgpt_user_id()?,
})
}
}
impl Notice {
pub(crate) fn valid(&self) -> bool {
!self.title.trim().is_empty()
&& self.title.len() <= 256
&& self.description.len() <= 2048
&& !self.title.chars().any(char::is_control)
&& !self.description.chars().any(char::is_control)
&& !self.action.label.trim().is_empty()
&& self.action.label.len() <= 128
&& !self.action.label.chars().any(char::is_control)
&& url::Url::parse(&self.action.url).is_ok_and(|url| {
url.scheme() == "https"
&& url.host_str() == Some("chatgpt.com")
&& url.username().is_empty()
&& url.password().is_none()
&& url.port().is_none()
})
}
}
pub(crate) fn prefetch(
config: &Config,
server: &AppServerSession,
tx: AppEventSender,
request_id: Uuid,
) {
if config.model_provider_id != "openai" || server.uses_remote_workspace() {
return;
}
let config = config.clone();
let request_handle = server.request_handle();
tokio::spawn(async move {
let result = tokio::time::timeout(Duration::from_secs(3), async {
// Saved credentials must match the connected server, which may use external auth.
let status: GetAuthStatusResponse = request_handle
.request_typed(ClientRequest::GetAuthStatus {
request_id: RequestId::String(Uuid::new_v4().to_string()),
params: GetAuthStatusParams {
include_token: Some(true),
refresh_token: Some(false),
},
})
.await
.ok()?;
let auth = config
.auth_config()
.load_auth(/*enable_codex_api_key_env*/ false)
.await
.ok()
.flatten()?;
if !matches!(auth, CodexAuth::Chatgpt(_)) || auth.is_fedramp_account() {
return None;
}
let saved_token = auth.get_token().ok()?;
if status.auth_method != Some(AuthMode::Chatgpt)
|| status.auth_token.as_deref() != Some(saved_token.as_str())
{
return None;
}
let identity = Identity::from_auth(&auth)?;
let client = RouteAwareClientPool::new_without_redirects(
config.http_client_factory(),
ClientRouteClass::Api,
);
let response = client
.get(format!(
"{}/wham/security-setup",
config.chatgpt_base_url.trim_end_matches('/')
))
.headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers())
.header(
"User-Agent",
codex_login::default_client::get_codex_user_agent(),
)
.send()
.await
.ok()?;
if !response.status().is_success() {
return None;
}
let notice = response.json::<Response>().await.ok()?.notice?;
tracing::debug!(valid = notice.valid(), "received security setup notice");
if !notice.valid() {
return None;
}
let current = config
.auth_config()
.load_auth(/*enable_codex_api_key_env*/ false)
.await
.ok()
.flatten()?;
if Identity::from_auth(&current).as_ref() != Some(&identity) {
tracing::debug!("security setup identity changed during fetch");
return None;
}
Some((identity, notice))
})
.await
.ok()
.flatten();
if let Some((identity, notice)) = result {
tracing::debug!("security setup notice ready");
tx.send(AppEvent::SecuritySetupLoaded {
request_id,
identity,
notice,
});
}
});
}
#[cfg(test)]
#[path = "security_setup_tests.rs"]
mod tests;
+29
View File
@@ -0,0 +1,29 @@
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn security_setup_rejects_unsafe_actions_and_terminal_controls() {
let value = json!({"title":"Keep using Daybreak mode","description":"Set up security.",
"action":{"label":"Set up security","url":"https://chatgpt.com/cyber"}});
let notice: Notice = serde_json::from_value(value.clone()).unwrap();
assert!(notice.valid());
for url in [
"http://chatgpt.com/cyber",
"https://chatgpt.com.evil.test/cyber",
"https://evil.test/",
"https://user@chatgpt.com/cyber",
"https://chatgpt.com:8080/cyber",
"javascript:alert(1)",
] {
let mut invalid = value.clone();
invalid["action"]["url"] = json!(url);
assert!(
!serde_json::from_value::<Notice>(invalid).unwrap().valid(),
"{url}"
);
}
let mut invalid = notice;
invalid.title = "untrusted\u{1b}[31m".into();
assert_eq!(invalid.valid(), false);
}