Compare commits

...

4 Commits

Author SHA1 Message Date
Charles Cunningham
3f70d55fae share experimental thread item filtering
Move generic thread-item experimental filtering into app-server-protocol so transport can reuse typed helpers instead of carrying guardian-specific logic.

Co-authored-by: Codex <noreply@openai.com>
2026-03-07 11:45:41 -08:00
viyatb-oai
b7b1de7892 test(codex-rs): fix bazel snapshot coverage 2026-03-07 11:19:16 -08:00
viyatb-oai
eea6b2d2df fix(core): stabilize guardian test coverage 2026-03-07 11:19:16 -08:00
Charles Cunningham
b89a794304 register experimental app-server enum variants
Co-authored-by: Codex <noreply@openai.com>
2026-03-07 06:33:18 -08:00
18 changed files with 677 additions and 477 deletions

View File

@@ -956,40 +956,6 @@
],
"title": "ChatgptLoginAccountParams",
"type": "object"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.",
"properties": {
"accessToken": {
"description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.",
"type": "string"
},
"chatgptAccountId": {
"description": "Workspace/account identifier supplied by the client.",
"type": "string"
},
"chatgptPlanType": {
"description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.",
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"chatgptAuthTokens"
],
"title": "ChatgptAuthTokensLoginAccountParamsType",
"type": "string"
}
},
"required": [
"accessToken",
"chatgptAccountId",
"type"
],
"title": "ChatgptAuthTokensLoginAccountParams",
"type": "object"
}
]
},

View File

@@ -3901,106 +3901,6 @@
"title": "FuzzyFileSearch/sessionCompletedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/started"
],
"title": "Thread/realtime/startedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeStartedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/startedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/itemAdded"
],
"title": "Thread/realtime/itemAddedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeItemAddedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/itemAddedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/outputAudio/delta"
],
"title": "Thread/realtime/outputAudio/deltaNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/outputAudio/deltaNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/error"
],
"title": "Thread/realtime/errorNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeErrorNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/errorNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/closed"
],
"title": "Thread/realtime/closedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeClosedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/closedNotification",
"type": "object"
},
{
"description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.",
"properties": {

View File

@@ -7538,106 +7538,6 @@
"title": "FuzzyFileSearch/sessionCompletedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/started"
],
"title": "Thread/realtime/startedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadRealtimeStartedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/startedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/itemAdded"
],
"title": "Thread/realtime/itemAddedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadRealtimeItemAddedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/itemAddedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/outputAudio/delta"
],
"title": "Thread/realtime/outputAudio/deltaNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadRealtimeOutputAudioDeltaNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/outputAudio/deltaNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/error"
],
"title": "Thread/realtime/errorNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadRealtimeErrorNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/errorNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/closed"
],
"title": "Thread/realtime/closedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadRealtimeClosedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/closedNotification",
"type": "object"
},
{
"description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.",
"properties": {
@@ -11362,40 +11262,6 @@
],
"title": "Chatgptv2::LoginAccountParams",
"type": "object"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.",
"properties": {
"accessToken": {
"description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.",
"type": "string"
},
"chatgptAccountId": {
"description": "Workspace/account identifier supplied by the client.",
"type": "string"
},
"chatgptPlanType": {
"description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.",
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"chatgptAuthTokens"
],
"title": "ChatgptAuthTokensv2::LoginAccountParamsType",
"type": "string"
}
},
"required": [
"accessToken",
"chatgptAccountId",
"type"
],
"title": "ChatgptAuthTokensv2::LoginAccountParams",
"type": "object"
}
],
"title": "LoginAccountParams"

View File

@@ -7613,40 +7613,6 @@
],
"title": "Chatgptv2::LoginAccountParams",
"type": "object"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.",
"properties": {
"accessToken": {
"description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.",
"type": "string"
},
"chatgptAccountId": {
"description": "Workspace/account identifier supplied by the client.",
"type": "string"
},
"chatgptPlanType": {
"description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.",
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"chatgptAuthTokens"
],
"title": "ChatgptAuthTokensv2::LoginAccountParamsType",
"type": "string"
}
},
"required": [
"accessToken",
"chatgptAccountId",
"type"
],
"title": "ChatgptAuthTokensv2::LoginAccountParams",
"type": "object"
}
],
"title": "LoginAccountParams"
@@ -11505,106 +11471,6 @@
"title": "FuzzyFileSearch/sessionCompletedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/started"
],
"title": "Thread/realtime/startedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeStartedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/startedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/itemAdded"
],
"title": "Thread/realtime/itemAddedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeItemAddedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/itemAddedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/outputAudio/delta"
],
"title": "Thread/realtime/outputAudio/deltaNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/outputAudio/deltaNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/error"
],
"title": "Thread/realtime/errorNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeErrorNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/errorNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/realtime/closed"
],
"title": "Thread/realtime/closedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadRealtimeClosedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/realtime/closedNotification",
"type": "object"
},
{
"description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.",
"properties": {

View File

@@ -36,40 +36,6 @@
],
"title": "Chatgptv2::LoginAccountParams",
"type": "object"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.",
"properties": {
"accessToken": {
"description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.",
"type": "string"
},
"chatgptAccountId": {
"description": "Workspace/account identifier supplied by the client.",
"type": "string"
},
"chatgptPlanType": {
"description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.",
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"chatgptAuthTokens"
],
"title": "ChatgptAuthTokensv2::LoginAccountParamsType",
"type": "string"
}
},
"required": [
"accessToken",
"chatgptAccountId",
"type"
],
"title": "ChatgptAuthTokensv2::LoginAccountParams",
"type": "object"
}
],
"title": "LoginAccountParams"

View File

@@ -31,11 +31,6 @@ import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNo
import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification";
import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification";
import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification";
import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification";
import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification";
import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification";
import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification";
import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification";
import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification";
import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification";
import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification";
@@ -50,4 +45,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
/**
* Notification sent from the server to the client.
*/
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
export type ServerNotification ={ "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };

View File

@@ -2,20 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt" } | { "type": "chatgptAuthTokens",
/**
* Access token (JWT) supplied by the client.
* This token is used for backend API requests and email extraction.
*/
accessToken: string,
/**
* Workspace/account identifier supplied by the client.
*/
chatgptAccountId: string,
/**
* Optional plan type supplied by the client.
*
* When `null`, Codex attempts to derive the plan type from access-token
* claims. If unavailable, the plan defaults to `unknown`.
*/
chatgptPlanType?: string | null, };
export type LoginAccountParams ={ "type": "apiKey", apiKey: string, } | { "type": "chatgpt" };

View File

@@ -23,6 +23,36 @@ pub fn experimental_fields() -> Vec<&'static ExperimentalField> {
inventory::iter::<ExperimentalField>.into_iter().collect()
}
/// Describes how an experimental enum variant appears on the wire.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExperimentalEnumVariantEncoding {
/// A plain string-literal union arm, such as `"chatgptAuthTokens"`.
StringLiteral,
/// A tagged object arm, such as `{ "type": "chatgptAuthTokens", ... }`.
TaggedObject { tag_name: &'static str },
/// An externally tagged object arm, such as `{ "reject": { ... } }`.
ExternallyTaggedObject,
}
/// Describes an experimental enum variant on a specific type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExperimentalEnumVariant {
pub type_name: &'static str,
pub serialized_name: &'static str,
pub reason: &'static str,
pub encoding: ExperimentalEnumVariantEncoding,
}
inventory::collect!(ExperimentalEnumVariant);
/// Returns all experimental enum variants registered across the protocol
/// types.
pub fn experimental_enum_variants() -> Vec<&'static ExperimentalEnumVariant> {
inventory::iter::<ExperimentalEnumVariant>
.into_iter()
.collect()
}
/// Constructs a consistent error message for experimental gating.
pub fn experimental_required_message(reason: &str) -> String {
format!("{reason} requires experimentalApi capability")
@@ -31,8 +61,11 @@ pub fn experimental_required_message(reason: &str) -> String {
#[cfg(test)]
mod tests {
use super::ExperimentalApi as ExperimentalApiTrait;
use super::ExperimentalEnumVariantEncoding;
use super::experimental_enum_variants;
use codex_experimental_api_macros::ExperimentalApi;
use pretty_assertions::assert_eq;
use serde::Serialize;
#[allow(dead_code)]
#[derive(ExperimentalApi)]
@@ -67,4 +100,31 @@ mod tests {
None
);
}
#[allow(dead_code)]
#[derive(ExperimentalApi, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
enum TaggedEnumVariantShapes {
Stable,
#[experimental("tagged/experimental")]
ExperimentalVariant,
}
#[test]
fn derive_registers_experimental_enum_variants_with_wire_shape_metadata() {
let variant = experimental_enum_variants()
.into_iter()
.find(|variant| variant.reason == "tagged/experimental")
.expect("tagged experimental variant should be registered");
assert_eq!(
*variant,
crate::experimental_api::ExperimentalEnumVariant {
type_name: "TaggedEnumVariantShapes",
serialized_name: "experimentalVariant",
reason: "tagged/experimental",
encoding: ExperimentalEnumVariantEncoding::TaggedObject { tag_name: "type" },
}
);
}
}

View File

@@ -2,6 +2,9 @@ use crate::ClientNotification;
use crate::ClientRequest;
use crate::ServerNotification;
use crate::ServerRequest;
use crate::experimental_api::ExperimentalEnumVariant;
use crate::experimental_api::ExperimentalEnumVariantEncoding;
use crate::experimental_api::experimental_enum_variants;
use crate::experimental_api::experimental_fields;
use crate::export_client_notification_schemas;
use crate::export_client_param_schemas;
@@ -220,6 +223,7 @@ pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) -
fn filter_experimental_ts(out_dir: &Path) -> Result<()> {
let registered_fields = experimental_fields();
let registered_enum_variants = experimental_enum_variants();
let experimental_method_types = experimental_method_types();
// Most generated TS files are filtered by schema processing, but
// `ClientRequest.ts` and any type with `#[experimental(...)]` fields need
@@ -227,6 +231,7 @@ fn filter_experimental_ts(out_dir: &Path) -> Result<()> {
// file-local unions/interfaces.
filter_client_request_ts(out_dir, EXPERIMENTAL_CLIENT_METHODS)?;
filter_experimental_type_fields_ts(out_dir, &registered_fields)?;
filter_experimental_enum_variants_ts(out_dir, &registered_enum_variants)?;
remove_generated_type_files(out_dir, &experimental_method_types, "ts")?;
Ok(())
}
@@ -327,15 +332,163 @@ fn filter_experimental_fields_in_ts_file(
Ok(())
}
fn filter_experimental_enum_variants_ts(
out_dir: &Path,
experimental_variants: &[&'static ExperimentalEnumVariant],
) -> Result<()> {
let mut variants_by_type_name: HashMap<String, Vec<&'static ExperimentalEnumVariant>> =
HashMap::new();
for variant in experimental_variants {
variants_by_type_name
.entry(variant.type_name.to_string())
.or_default()
.push(*variant);
}
for path in ts_files_in_recursive(out_dir)? {
let Some(type_name) = path.file_stem().and_then(|stem| stem.to_str()) else {
continue;
};
let Some(type_variants) = variants_by_type_name.get(type_name) else {
continue;
};
filter_experimental_enum_variants_in_ts_file(&path, type_variants)?;
}
Ok(())
}
fn filter_experimental_enum_variants_in_ts_file(
path: &Path,
experimental_variants: &[&ExperimentalEnumVariant],
) -> Result<()> {
let mut content =
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
let Some((prefix, body, suffix)) = split_type_alias(&content) else {
return Ok(());
};
let filtered_arms: Vec<String> = split_top_level(&body, '|')
.into_iter()
.filter(|arm| {
!experimental_variants
.iter()
.any(|variant| ts_union_arm_matches_experimental_enum_variant(arm, variant))
})
.collect();
let new_body = filtered_arms.join(" | ");
content = format!("{prefix}{new_body}{suffix}");
let import_usage_scope = split_type_alias(&content)
.map(|(_, body, _)| body)
.unwrap_or_else(|| new_body.clone());
content = prune_unused_type_imports(content, &import_usage_scope);
fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}
fn filter_experimental_schema(bundle: &mut Value) -> Result<()> {
let registered_fields = experimental_fields();
let registered_enum_variants = experimental_enum_variants();
filter_experimental_fields_in_root(bundle, &registered_fields);
filter_experimental_fields_in_definitions(bundle, &registered_fields);
filter_experimental_enum_variants_in_root(bundle, &registered_enum_variants);
filter_experimental_enum_variants_in_definitions(bundle, &registered_enum_variants);
prune_experimental_methods(bundle, EXPERIMENTAL_CLIENT_METHODS);
remove_experimental_method_type_definitions(bundle);
Ok(())
}
fn filter_experimental_enum_variants_in_root(
schema: &mut Value,
experimental_variants: &[&'static ExperimentalEnumVariant],
) {
let Some(title) = schema.get("title").and_then(Value::as_str) else {
return;
};
let title = title.to_string();
let matching_variants: Vec<&ExperimentalEnumVariant> = experimental_variants
.iter()
.copied()
.filter(|variant| title == variant.type_name)
.collect();
if matching_variants.is_empty() {
return;
}
remove_experimental_enum_variants_from_schema(schema, &matching_variants);
}
fn filter_experimental_enum_variants_in_definitions(
bundle: &mut Value,
experimental_variants: &[&'static ExperimentalEnumVariant],
) {
let Some(definitions) = bundle.get_mut("definitions").and_then(Value::as_object_mut) else {
return;
};
filter_experimental_enum_variants_in_definitions_map(definitions, experimental_variants);
}
fn filter_experimental_enum_variants_in_definitions_map(
definitions: &mut Map<String, Value>,
experimental_variants: &[&'static ExperimentalEnumVariant],
) {
for (def_name, def_schema) in definitions.iter_mut() {
if is_namespace_map(def_schema) {
if let Some(namespace_defs) = def_schema.as_object_mut() {
filter_experimental_enum_variants_in_definitions_map(
namespace_defs,
experimental_variants,
);
}
continue;
}
let matching_variants: Vec<&ExperimentalEnumVariant> = experimental_variants
.iter()
.copied()
.filter(|variant| definition_matches_type(def_name, variant.type_name))
.collect();
if matching_variants.is_empty() {
continue;
}
remove_experimental_enum_variants_from_schema(def_schema, &matching_variants);
}
}
fn remove_experimental_enum_variants_from_schema(
schema: &mut Value,
experimental_variants: &[&ExperimentalEnumVariant],
) {
match schema {
Value::Array(items) => {
items.retain(|item| {
!experimental_variants
.iter()
.any(|variant| json_schema_matches_experimental_enum_variant(item, variant))
});
for item in items {
remove_experimental_enum_variants_from_schema(item, experimental_variants);
}
}
Value::Object(map) => {
if let Some(enum_values) = map.get_mut("enum").and_then(Value::as_array_mut) {
enum_values.retain(|value| {
!experimental_variants.iter().any(|variant| {
matches!(
variant.encoding,
ExperimentalEnumVariantEncoding::StringLiteral
) && value.as_str() == Some(variant.serialized_name)
})
});
}
for child in map.values_mut() {
remove_experimental_enum_variants_from_schema(child, experimental_variants);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
fn filter_experimental_fields_in_root(
schema: &mut Value,
experimental_fields: &[&'static crate::experimental_api::ExperimentalField],
@@ -715,6 +868,105 @@ fn parse_property(input: &str) -> Option<(String, &str)> {
Some((name, input[colon_index + 1..].trim_start()))
}
fn ts_union_arm_matches_experimental_enum_variant(
arm: &str,
variant: &ExperimentalEnumVariant,
) -> bool {
let arm = strip_leading_block_comments(arm).trim();
match variant.encoding {
ExperimentalEnumVariantEncoding::StringLiteral => {
arm == format!("\"{}\"", variant.serialized_name)
}
ExperimentalEnumVariantEncoding::TaggedObject { tag_name } => {
ts_object_arm_has_string_property(arm, tag_name, variant.serialized_name)
}
ExperimentalEnumVariantEncoding::ExternallyTaggedObject => {
ts_object_arm_has_property(arm, variant.serialized_name)
}
}
}
fn ts_object_arm_has_string_property(arm: &str, property_name: &str, property_value: &str) -> bool {
let Some((open, close)) = find_top_level_brace_span(arm) else {
return false;
};
let inner = &arm[open + 1..close];
for field in split_top_level(inner, ',') {
let field = strip_leading_block_comments(field.as_str());
let Some((name, value)) = parse_property(field) else {
continue;
};
if name != property_name {
continue;
}
return parse_string_literal(strip_leading_block_comments(value).trim_start())
.is_some_and(|(value, _)| value == property_value);
}
false
}
fn ts_object_arm_has_property(arm: &str, property_name: &str) -> bool {
let Some((open, close)) = find_top_level_brace_span(arm) else {
return false;
};
let inner = &arm[open + 1..close];
split_top_level(inner, ',').into_iter().any(|field| {
let field = strip_leading_block_comments(field.as_str());
parse_property_name(field).is_some_and(|name| name == property_name)
})
}
fn json_schema_matches_experimental_enum_variant(
schema: &Value,
variant: &ExperimentalEnumVariant,
) -> bool {
match variant.encoding {
ExperimentalEnumVariantEncoding::StringLiteral => {
json_schema_matches_string_literal(schema, variant.serialized_name)
}
ExperimentalEnumVariantEncoding::TaggedObject { tag_name } => {
json_schema_has_tagged_variant(schema, tag_name, variant.serialized_name)
}
ExperimentalEnumVariantEncoding::ExternallyTaggedObject => {
json_schema_has_property(schema, variant.serialized_name)
}
}
}
fn json_schema_matches_string_literal(schema: &Value, literal: &str) -> bool {
let Value::Object(map) = schema else {
return false;
};
if map.get("const").and_then(Value::as_str) == Some(literal) {
return true;
}
map.get("enum")
.and_then(Value::as_array)
.is_some_and(|values| values.len() == 1 && values[0].as_str() == Some(literal))
}
fn json_schema_has_tagged_variant(schema: &Value, tag_name: &str, tag_value: &str) -> bool {
let Value::Object(map) = schema else {
return false;
};
let Some(properties) = map.get("properties").and_then(Value::as_object) else {
return false;
};
properties
.get(tag_name)
.is_some_and(|tag_schema| json_schema_matches_string_literal(tag_schema, tag_value))
}
fn json_schema_has_property(schema: &Value, property_name: &str) -> bool {
let Value::Object(map) = schema else {
return false;
};
let Some(properties) = map.get("properties").and_then(Value::as_object) else {
return false;
};
properties.contains_key(property_name)
}
fn strip_leading_block_comments(input: &str) -> &str {
let mut rest = input.trim_start();
loop {
@@ -1942,6 +2194,12 @@ mod tests {
let thread_start_ts =
fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.ts"))?;
assert_eq!(thread_start_ts.contains("mockExperimentalField"), false);
let login_account_params_ts =
fs::read_to_string(output_dir.join("v2").join("LoginAccountParams.ts"))?;
assert_eq!(
login_account_params_ts.contains("\"chatgptAuthTokens\""),
false
);
assert_eq!(
output_dir
.join("v2")
@@ -2197,6 +2455,12 @@ mod tests {
let thread_start_ts =
fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.ts"))?;
assert_eq!(thread_start_ts.contains("mockExperimentalField"), true);
let login_account_params_ts =
fs::read_to_string(output_dir.join("v2").join("LoginAccountParams.ts"))?;
assert_eq!(
login_account_params_ts.contains("\"chatgptAuthTokens\""),
true
);
let command_execution_request_approval_ts = fs::read_to_string(
output_dir
.join("v2")
@@ -2662,6 +2926,12 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k
let thread_start_json =
fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.json"))?;
assert_eq!(thread_start_json.contains("mockExperimentalField"), false);
let login_account_params_json =
fs::read_to_string(output_dir.join("v2").join("LoginAccountParams.json"))?;
assert_eq!(
login_account_params_json.contains("chatgptAuthTokens"),
false
);
let command_execution_request_approval_json =
fs::read_to_string(output_dir.join("CommandExecutionRequestApprovalParams.json"))?;
assert_eq!(

View File

@@ -1,8 +1,12 @@
use std::collections::HashSet;
use std::path::Path;
use std::sync::OnceLock;
use crate::JSONRPCNotification;
use crate::JSONRPCRequest;
use crate::RequestId;
use crate::experimental_api::ExperimentalEnumVariantEncoding;
use crate::experimental_api::experimental_enum_variants;
use crate::export::GeneratedSchema;
use crate::export::write_json_schema;
use crate::protocol::v1;
@@ -11,6 +15,7 @@ use codex_experimental_api_macros::ExperimentalApi;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use strum_macros::Display;
use ts_rs::TS;
@@ -639,6 +644,89 @@ macro_rules! client_notification_definitions {
};
}
pub fn filter_experimental_thread_items_in_server_notification(
notification: ServerNotification,
) -> Option<ServerNotification> {
match notification {
ServerNotification::ThreadStarted(mut notification) => {
notification.thread.strip_experimental_thread_items();
Some(ServerNotification::ThreadStarted(notification))
}
ServerNotification::ItemStarted(notification) if notification.item.is_experimental() => {
None
}
ServerNotification::ItemCompleted(notification) if notification.item.is_experimental() => {
None
}
ServerNotification::TurnStarted(mut notification) => {
notification.turn.strip_experimental_thread_items();
Some(ServerNotification::TurnStarted(notification))
}
ServerNotification::TurnCompleted(mut notification) => {
notification.turn.strip_experimental_thread_items();
Some(ServerNotification::TurnCompleted(notification))
}
_ => Some(notification),
}
}
/// Transport serializes app-server responses before per-connection filtering,
/// so stable clients need a last-mile scrub pass over the response JSON.
pub fn strip_experimental_thread_items_from_serialized_response(result: &mut Value) {
match result {
Value::Object(map) => {
for (key, value) in map {
if key == "items" {
if let Value::Array(items) = value {
items.retain(|item| !is_experimental_thread_item_value(item));
for item in items {
strip_experimental_thread_items_from_serialized_response(item);
}
}
} else {
strip_experimental_thread_items_from_serialized_response(value);
}
}
}
Value::Array(values) => {
for value in values {
strip_experimental_thread_items_from_serialized_response(value);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
fn is_experimental_thread_item_value(value: &Value) -> bool {
matches!(
value,
Value::Object(map)
if matches!(
map.get("type"),
Some(Value::String(kind))
if experimental_thread_item_variant_tags().contains(kind.as_str())
)
)
}
fn experimental_thread_item_variant_tags() -> &'static HashSet<&'static str> {
static TAGS: OnceLock<HashSet<&'static str>> = OnceLock::new();
TAGS.get_or_init(|| {
experimental_enum_variants()
.into_iter()
.filter_map(|variant| match variant {
crate::experimental_api::ExperimentalEnumVariant {
type_name: "ThreadItem",
serialized_name,
encoding: ExperimentalEnumVariantEncoding::TaggedObject { tag_name: "type" },
..
} => Some(*serialized_name),
_ => None,
})
.collect()
})
}
impl TryFrom<JSONRPCRequest> for ServerRequest {
type Error = serde_json::Error;

View File

@@ -3633,6 +3633,24 @@ impl ThreadItem {
| ThreadItem::ContextCompaction { id, .. } => id,
}
}
pub fn is_experimental(&self) -> bool {
crate::experimental_api::ExperimentalApi::experimental_reason(self).is_some()
}
}
impl Thread {
pub fn strip_experimental_thread_items(&mut self) {
for turn in &mut self.turns {
turn.strip_experimental_thread_items();
}
}
}
impl Turn {
pub fn strip_experimental_thread_items(&mut self) {
self.items.retain(|item| !item.is_experimental());
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

View File

@@ -7,6 +7,8 @@ use crate::outgoing_message::OutgoingMessage;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::filter_experimental_thread_items_in_server_notification;
use codex_app_server_protocol::strip_experimental_thread_items_from_serialized_response;
use futures::SinkExt;
use futures::StreamExt;
use owo_colors::OwoColorize;
@@ -584,7 +586,9 @@ async fn send_message_to_connection(
warn!("dropping message for disconnected connection: {connection_id:?}");
return false;
};
let message = filter_outgoing_message_for_connection(connection_state, message);
let Some(message) = filter_outgoing_message_for_connection(connection_state, message) else {
return false;
};
if should_skip_notification_for_connection(connection_state, &message) {
return false;
}
@@ -613,7 +617,7 @@ async fn send_message_to_connection(
fn filter_outgoing_message_for_connection(
connection_state: &OutboundConnectionState,
message: OutgoingMessage,
) -> OutgoingMessage {
) -> Option<OutgoingMessage> {
let experimental_api_enabled = connection_state
.experimental_api_enabled
.load(Ordering::Acquire);
@@ -625,12 +629,25 @@ fn filter_outgoing_message_for_connection(
if !experimental_api_enabled {
params.strip_experimental_fields();
}
OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval {
request_id,
params,
})
Some(OutgoingMessage::Request(
ServerRequest::CommandExecutionRequestApproval { request_id, params },
))
}
_ => message,
OutgoingMessage::AppServerNotification(notification) => {
if experimental_api_enabled {
Some(OutgoingMessage::AppServerNotification(notification))
} else {
filter_experimental_thread_items_in_server_notification(notification)
.map(OutgoingMessage::AppServerNotification)
}
}
OutgoingMessage::Response(mut response) => {
if !experimental_api_enabled {
strip_experimental_thread_items_from_serialized_response(&mut response.result);
}
Some(OutgoingMessage::Response(response))
}
_ => Some(message),
}
}

View File

@@ -13,6 +13,13 @@ use syn::LitStr;
use syn::Type;
use syn::parse_macro_input;
#[derive(Default)]
struct EnumSerdeConfig {
rename_all: Option<String>,
tag: Option<String>,
untagged: bool,
}
#[proc_macro_derive(ExperimentalApi, attributes(experimental))]
pub fn derive_experimental_api(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
@@ -141,7 +148,10 @@ fn derive_for_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream {
fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
let name = &input.ident;
let type_name_lit = LitStr::new(&name.to_string(), Span::call_site());
let serde_config = enum_serde_config(&input.attrs);
let mut match_arms = Vec::new();
let mut registrations = Vec::new();
for variant in &data.variants {
let variant_name = &variant.ident;
@@ -155,6 +165,14 @@ fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
match_arms.push(quote! {
#pattern => Some(#reason),
});
if let Some(registration) = experimental_enum_variant_registration(
&type_name_lit,
&serde_config,
variant,
&reason,
) {
registrations.push(registration);
}
} else {
match_arms.push(quote! {
#pattern => None,
@@ -163,6 +181,8 @@ fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
}
let expanded = quote! {
#(#registrations)*
impl crate::experimental_api::ExperimentalApi for #name {
fn experimental_reason(&self) -> Option<&'static str> {
match self {
@@ -181,12 +201,160 @@ fn experimental_reason(attrs: &[Attribute]) -> Option<LitStr> {
attr.parse_args::<LitStr>().ok()
}
fn enum_serde_config(attrs: &[Attribute]) -> EnumSerdeConfig {
let mut config = EnumSerdeConfig::default();
for attr in attrs.iter().filter(|attr| attr.path().is_ident("serde")) {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
config.rename_all = Some(meta.value()?.parse::<LitStr>()?.value());
} else if meta.path.is_ident("tag") {
config.tag = Some(meta.value()?.parse::<LitStr>()?.value());
} else if meta.path.is_ident("untagged") {
config.untagged = true;
}
Ok(())
});
}
config
}
fn variant_serialized_name(variant: &syn::Variant, rename_all: Option<&str>) -> String {
if let Some(rename) = serde_rename(&variant.attrs) {
return rename;
}
apply_rename_all(&variant.ident.to_string(), rename_all)
}
fn serde_rename(attrs: &[Attribute]) -> Option<String> {
let mut rename = None;
for attr in attrs.iter().filter(|attr| attr.path().is_ident("serde")) {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
rename = Some(meta.value()?.parse::<LitStr>()?.value());
}
Ok(())
});
}
rename
}
fn experimental_enum_variant_registration(
type_name_lit: &LitStr,
serde_config: &EnumSerdeConfig,
variant: &syn::Variant,
reason: &LitStr,
) -> Option<proc_macro2::TokenStream> {
if serde_config.untagged {
return None;
}
let serialized_name = variant_serialized_name(variant, serde_config.rename_all.as_deref());
let serialized_name_lit = LitStr::new(&serialized_name, Span::call_site());
let encoding = if let Some(tag_name) = serde_config.tag.as_deref() {
let tag_name_lit = LitStr::new(tag_name, Span::call_site());
quote! {
crate::experimental_api::ExperimentalEnumVariantEncoding::TaggedObject {
tag_name: #tag_name_lit,
}
}
} else if matches!(variant.fields, Fields::Unit) {
quote! {
crate::experimental_api::ExperimentalEnumVariantEncoding::StringLiteral
}
} else {
quote! {
crate::experimental_api::ExperimentalEnumVariantEncoding::ExternallyTaggedObject
}
};
Some(quote! {
::inventory::submit! {
crate::experimental_api::ExperimentalEnumVariant {
type_name: #type_name_lit,
serialized_name: #serialized_name_lit,
reason: #reason,
encoding: #encoding,
}
}
})
}
fn field_serialized_name(field: &Field) -> Option<String> {
let ident = field.ident.as_ref()?;
let name = ident.to_string();
Some(snake_to_camel(&name))
}
fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
let words = split_words(name);
match rename_all {
Some("camelCase") => {
let mut out = String::new();
for (index, word) in words.iter().enumerate() {
if index == 0 {
out.push_str(word);
} else {
out.push_str(&capitalize(word));
}
}
out
}
Some("snake_case") => words.join("_"),
Some("kebab-case") => words.join("-"),
Some("PascalCase") => words
.iter()
.map(|word| capitalize(word))
.collect::<Vec<_>>()
.join(""),
Some("SCREAMING_SNAKE_CASE") => words.join("_").to_ascii_uppercase(),
Some("UPPERCASE") => words.concat().to_ascii_uppercase(),
Some("lowercase") => words.concat(),
Some(_) | None => name.to_string(),
}
}
fn split_words(name: &str) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
let chars: Vec<char> = name.chars().collect();
for (index, ch) in chars.iter().copied().enumerate() {
if ch == '_' || ch == '-' {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
continue;
}
let previous = chars.get(index.wrapping_sub(1)).copied();
let next = chars.get(index + 1).copied();
let boundary_before = previous.is_some_and(|previous| {
(previous.is_ascii_lowercase() && ch.is_ascii_uppercase())
|| (previous.is_ascii_uppercase()
&& ch.is_ascii_uppercase()
&& next.is_some_and(|next| next.is_ascii_lowercase()))
});
if boundary_before && !current.is_empty() {
words.push(std::mem::take(&mut current));
}
current.push(ch.to_ascii_lowercase());
}
if !current.is_empty() {
words.push(current);
}
words
}
fn capitalize(word: &str) -> String {
let mut chars = word.chars();
let Some(first) = chars.next() else {
return String::new();
};
let mut out = String::new();
out.push(first.to_ascii_uppercase());
out.extend(chars);
out
}
fn snake_to_camel(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut upper = false;

View File

@@ -36,6 +36,9 @@ codex_rust_crate(
],
test_data_extra = [
"config.schema.json",
] + glob([
"src/**/snapshots/**",
]) + [
# This is a bit of a hack, but empirically, some of our integration tests
# are relying on the presence of this file as a repo root marker. When
# running tests locally, this "just works," but in remote execution,

View File

@@ -39,10 +39,15 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
.features
.enable(Feature::RequestPermissions)
.expect("test setup should allow enabling request permissions");
turn_context_raw
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("test setup should allow updating sandbox policy");
// This test is about request-permissions validation, not managed sandbox
// policy enforcement. Widen the derived sandbox policies directly so the
// command runs without depending on a platform sandbox binary.
turn_context_raw.file_system_sandbox_policy =
codex_protocol::permissions::FileSystemSandboxPolicy::from(
&SandboxPolicy::DangerFullAccess,
);
turn_context_raw.network_sandbox_policy =
codex_protocol::permissions::NetworkSandboxPolicy::from(&SandboxPolicy::DangerFullAccess);
let session = Arc::new(session);
let turn_context = Arc::new(turn_context_raw);

View File

@@ -664,12 +664,16 @@ fn truncate_guardian_action_value(value: Value) -> Value {
.map(truncate_guardian_action_value)
.collect::<Vec<_>>(),
),
Value::Object(values) => Value::Object(
values
.into_iter()
.map(|(key, value)| (key, truncate_guardian_action_value(value)))
.collect(),
),
Value::Object(values) => {
let mut entries = values.into_iter().collect::<Vec<_>>();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
Value::Object(
entries
.into_iter()
.map(|(key, value)| (key, truncate_guardian_action_value(value)))
.collect(),
)
}
other => other,
}
}

View File

@@ -0,0 +1,19 @@
---
source: tui/src/chatwidget/tests.rs
expression: popup
---
Experimental features
Toggle experimental features. Changes are saved to config.toml.
[ ] JavaScript REPL Enable a persistent Node-backed JavaScript REPL for interactive website debugging
and other inline JavaScript execution capabilities. Requires Node >= v22.22.0
installed.
[ ] Bubblewrap sandbox Try the new linux sandbox based on bubblewrap.
[ ] Multi-agents Ask Codex to spawn multiple agents to parallelize the work and win in efficiency.
[ ] Apps Use a connected ChatGPT App using "$". Install Apps via /apps command. Restart
Codex after enabling.
[ ] Guardian approvals Let a guardian subagent review `on-request` approval prompts instead of showing
them to you, including sandbox escapes and blocked network access.
[ ] Prevent sleep while running Keep your computer awake while Codex is running a thread.
Press space to select or enter to save for next conversation

View File

@@ -6949,7 +6949,12 @@ async fn experimental_popup_includes_guardian_approval() {
chat.open_experimental_popup();
let popup = render_bottom_popup(&chat, 120);
assert_snapshot!("experimental_popup_includes_guardian_approval", popup);
let snapshot_name = if cfg!(target_os = "linux") {
"experimental_popup_includes_guardian_approval_linux"
} else {
"experimental_popup_includes_guardian_approval"
};
assert_snapshot!(snapshot_name, popup);
}
#[tokio::test]