mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Add managed new-thread model settings (#29683)
## Why Admins need persistent defaults for the model, reasoning effort, and service tier shown when the Desktop App creates a new thread. These are initialization defaults rather than runtime constraints: the App should use them to initialize its draft while still allowing a user to make an explicit selection. The app-server therefore needs to expose the managed values before thread creation without changing `thread/start` behavior for other clients. ## What changed - Parse `model`, `model_reasoning_effort`, and `service_tier` from `[models.new_thread]` in `requirements.toml`. - Compose the `models` requirements through the existing requirements-layer precedence rules. - Expose the resolved values through `configRequirements/read` as `requirements.models.newThread`. - Add the corresponding app-server protocol types and regenerate the JSON and TypeScript schema fixtures. - Document the new `configRequirements/read` fields in the app-server README. ## Scope This PR is data plumbing only. It does not apply these values during `thread/start` and does not change thread creation for existing app-server clients, resumed or forked sessions, internal or subagent sessions, `codex exec`, or the TUI. A companion Desktop App change owns draft initialization, sends the effective settings for ordinary and prewarmed starts, and preserves explicit user changes. ## Validation - Requirements deserialization coverage for `[models.new_thread]` - Requirements-layer precedence coverage - App-server API mapping coverage - `configRequirements/read` integration coverage - Regenerated app-server JSON and TypeScript schema fixtures
This commit is contained in:
committed by
GitHub
Unverified
parent
f91334380e
commit
d9cf931d0e
+52
@@ -8414,6 +8414,16 @@
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"models": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ModelsRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -12834,6 +12844,21 @@
|
||||
"title": "ModelVerificationNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ModelsRequirements": {
|
||||
"properties": {
|
||||
"newThread": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/NewThreadModelDefaults"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
@@ -12981,6 +13006,33 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NewThreadModelDefaults": {
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelReasoningEffort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
|
||||
+52
@@ -4654,6 +4654,16 @@
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"models": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ModelsRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -9238,6 +9248,21 @@
|
||||
"title": "ModelVerificationNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ModelsRequirements": {
|
||||
"properties": {
|
||||
"newThread": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NewThreadModelDefaults"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
@@ -9385,6 +9410,33 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NewThreadModelDefaults": {
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelReasoningEffort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
|
||||
+57
@@ -169,6 +169,16 @@
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"models": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ModelsRequirements"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -362,6 +372,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModelsRequirements": {
|
||||
"properties": {
|
||||
"newThread": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NewThreadModelDefaults"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkDomainPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
@@ -484,6 +509,38 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NewThreadModelDefaults": {
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelReasoningEffort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ResidencyRequirement": {
|
||||
"enum": [
|
||||
"us"
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
|
||||
import type { ModelsRequirements } from "./ModelsRequirements";
|
||||
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
|
||||
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, models: ModelsRequirements | null};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { NewThreadModelDefaults } from "./NewThreadModelDefaults";
|
||||
|
||||
export type ModelsRequirements = { newThread: NewThreadModelDefaults | null, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
|
||||
export type NewThreadModelDefaults = { model: string | null, modelReasoningEffort: ReasoningEffort | null, serviceTier: string | null, };
|
||||
@@ -267,6 +267,7 @@ export type { ModelServiceTier } from "./ModelServiceTier";
|
||||
export type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
|
||||
export type { ModelVerification } from "./ModelVerification";
|
||||
export type { ModelVerificationNotification } from "./ModelVerificationNotification";
|
||||
export type { ModelsRequirements } from "./ModelsRequirements";
|
||||
export type { NetworkAccess } from "./NetworkAccess";
|
||||
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
|
||||
@@ -275,6 +276,7 @@ export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
|
||||
export type { NetworkRequirements } from "./NetworkRequirements";
|
||||
export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission";
|
||||
export type { NewThreadModelDefaults } from "./NewThreadModelDefaults";
|
||||
export type { NonSteerableTurnKind } from "./NonSteerableTurnKind";
|
||||
export type { OverriddenMetadata } from "./OverriddenMetadata";
|
||||
export type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
|
||||
@@ -390,6 +390,23 @@ pub struct ConfigRequirements {
|
||||
pub enforce_residency: Option<ResidencyRequirement>,
|
||||
#[experimental("configRequirements/read.network")]
|
||||
pub network: Option<NetworkRequirements>,
|
||||
pub models: Option<ModelsRequirements>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ModelsRequirements {
|
||||
pub new_thread: Option<NewThreadModelDefaults>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct NewThreadModelDefaults {
|
||||
pub model: Option<String>,
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub service_tier: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -1753,6 +1753,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
||||
hooks: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
models: None,
|
||||
});
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.granular"));
|
||||
|
||||
@@ -242,7 +242,7 @@ Example with notification opt-out:
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Callers may pass `source` to identify the product that initiated the import; omitted or `null` means unspecified. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
|
||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
|
||||
### Example: Start or resume a thread
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::ManagedHooksRequirements;
|
||||
use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse;
|
||||
use codex_app_server_protocol::ModelsRequirements;
|
||||
use codex_app_server_protocol::NetworkDomainPermission;
|
||||
use codex_app_server_protocol::NetworkRequirements;
|
||||
use codex_app_server_protocol::NetworkUnixSocketPermission;
|
||||
use codex_app_server_protocol::NewThreadModelDefaults;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
@@ -375,6 +377,13 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
.enforce_residency
|
||||
.map(map_residency_requirement_to_api),
|
||||
network: requirements.network.map(map_network_requirements_to_api),
|
||||
models: requirements.models.map(|models| ModelsRequirements {
|
||||
new_thread: models.new_thread.map(|new_thread| NewThreadModelDefaults {
|
||||
model: new_thread.model,
|
||||
model_reasoning_effort: new_thread.model_reasoning_effort,
|
||||
service_tier: new_thread.service_tier,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,7 +577,10 @@ mod tests {
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_config::ComputerUseRequirementsToml;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::ModelsRequirementsToml;
|
||||
use codex_config::NewThreadModelDefaultsToml;
|
||||
use codex_config::WindowsRequirementsToml;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -628,6 +640,31 @@ mod tests {
|
||||
assert_eq!(mapped.allow_remote_control, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_new_thread_model_defaults() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
models: Some(ModelsRequirementsToml {
|
||||
new_thread: Some(NewThreadModelDefaultsToml {
|
||||
model: Some("gpt-managed".to_string()),
|
||||
model_reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
service_tier: Some("fast".to_string()),
|
||||
}),
|
||||
}),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
|
||||
let defaults = mapped
|
||||
.models
|
||||
.and_then(|models| models.new_thread)
|
||||
.expect("new-thread defaults");
|
||||
assert_eq!(defaults.model.as_deref(), Some("gpt-managed"));
|
||||
assert_eq!(
|
||||
defaults.model_reasoning_effort,
|
||||
Some(ReasoningEffort::Medium)
|
||||
);
|
||||
assert_eq!(defaults.service_tier.as_deref(), Some("fast"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_computer_use_requirements() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
|
||||
@@ -75,6 +75,43 @@ async fn config_requirements_read_includes_allow_remote_control() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_requirements_read_includes_new_thread_model_defaults() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("requirements.toml"),
|
||||
r#"
|
||||
[models.new_thread]
|
||||
model = "gpt-managed"
|
||||
model_reasoning_effort = "medium"
|
||||
service_tier = "fast"
|
||||
"#,
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_config_requirements_read_request().await?;
|
||||
let response = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: ConfigRequirementsReadResponse = to_response(response)?;
|
||||
|
||||
let defaults = response
|
||||
.requirements
|
||||
.and_then(|requirements| requirements.models)
|
||||
.and_then(|models| models.new_thread)
|
||||
.expect("managed new-thread defaults");
|
||||
assert_eq!(defaults.model.as_deref(), Some("gpt-managed"));
|
||||
assert_eq!(
|
||||
defaults.model_reasoning_effort,
|
||||
Some(ReasoningEffort::Medium)
|
||||
);
|
||||
assert_eq!(defaults.service_tier.as_deref(), Some("fast"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_returns_effective_and_layers() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -2,6 +2,7 @@ use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
@@ -876,9 +877,36 @@ pub struct ConfigRequirementsToml {
|
||||
#[serde(rename = "experimental_network")]
|
||||
pub network: Option<NetworkRequirementsToml>,
|
||||
pub permissions: Option<PermissionsRequirementsToml>,
|
||||
pub models: Option<ModelsRequirementsToml>,
|
||||
pub guardian_policy_config: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct ModelsRequirementsToml {
|
||||
pub new_thread: Option<NewThreadModelDefaultsToml>,
|
||||
}
|
||||
|
||||
impl ModelsRequirementsToml {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.new_thread
|
||||
.as_ref()
|
||||
.is_none_or(NewThreadModelDefaultsToml::is_empty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NewThreadModelDefaultsToml {
|
||||
pub model: Option<String>,
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub service_tier: Option<String>,
|
||||
}
|
||||
|
||||
impl NewThreadModelDefaultsToml {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.model.is_none() && self.model_reasoning_effort.is_none() && self.service_tier.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct RemoteSandboxConfigToml {
|
||||
pub hostname_patterns: Vec<String>,
|
||||
@@ -930,6 +958,7 @@ pub struct ConfigRequirementsWithSources {
|
||||
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
|
||||
pub network: Option<Sourced<NetworkRequirementsToml>>,
|
||||
pub permissions: Option<Sourced<PermissionsRequirementsToml>>,
|
||||
pub models: Option<Sourced<ModelsRequirementsToml>>,
|
||||
pub guardian_policy_config: Option<Sourced<String>>,
|
||||
}
|
||||
|
||||
@@ -974,6 +1003,7 @@ impl ConfigRequirementsWithSources {
|
||||
enforce_residency: _,
|
||||
network: _,
|
||||
permissions: _,
|
||||
models: _,
|
||||
guardian_policy_config: _,
|
||||
} = &other;
|
||||
|
||||
@@ -1010,6 +1040,7 @@ impl ConfigRequirementsWithSources {
|
||||
enforce_residency,
|
||||
network,
|
||||
permissions,
|
||||
models,
|
||||
guardian_policy_config,
|
||||
}
|
||||
);
|
||||
@@ -1046,6 +1077,7 @@ impl ConfigRequirementsWithSources {
|
||||
enforce_residency,
|
||||
network,
|
||||
permissions,
|
||||
models,
|
||||
guardian_policy_config,
|
||||
} = self;
|
||||
ConfigRequirementsToml {
|
||||
@@ -1071,6 +1103,7 @@ impl ConfigRequirementsWithSources {
|
||||
enforce_residency: enforce_residency.map(|sourced| sourced.value),
|
||||
network: network.map(|sourced| sourced.value),
|
||||
permissions: permissions.map(|sourced| sourced.value),
|
||||
models: models.map(|sourced| sourced.value),
|
||||
guardian_policy_config: guardian_policy_config.map(|sourced| sourced.value),
|
||||
}
|
||||
}
|
||||
@@ -1183,6 +1216,10 @@ impl ConfigRequirementsToml {
|
||||
&& self.enforce_residency.is_none()
|
||||
&& self.network.is_none()
|
||||
&& self.permissions.is_none()
|
||||
&& self
|
||||
.models
|
||||
.as_ref()
|
||||
.is_none_or(ModelsRequirementsToml::is_empty)
|
||||
&& self
|
||||
.guardian_policy_config
|
||||
.as_deref()
|
||||
@@ -1214,8 +1251,9 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
|
||||
fn try_from(toml: ConfigRequirementsWithSources) -> Result<Self, Self::Error> {
|
||||
// Profile catalog selection remains on ConfigRequirementsToml for
|
||||
// config loading and requirements API projection. The normalized
|
||||
// constraints below only need the compiled PermissionProfile envelope.
|
||||
// config loading and requirements API projection. Managed new-thread
|
||||
// defaults also remain there because they are initialization values,
|
||||
// not runtime constraints.
|
||||
let ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_approvals_reviewers,
|
||||
@@ -1238,6 +1276,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
enforce_residency,
|
||||
network,
|
||||
permissions,
|
||||
models: _,
|
||||
guardian_policy_config,
|
||||
} = toml;
|
||||
|
||||
@@ -1635,6 +1674,7 @@ mod tests {
|
||||
enforce_residency,
|
||||
network,
|
||||
permissions,
|
||||
models,
|
||||
guardian_policy_config,
|
||||
} = toml;
|
||||
ConfigRequirementsWithSources {
|
||||
@@ -1670,6 +1710,7 @@ mod tests {
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
permissions: permissions.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
models: models.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
guardian_policy_config: guardian_policy_config
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
}
|
||||
@@ -1822,6 +1863,31 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_new_thread_model_defaults() -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
[models.new_thread]
|
||||
model = "managed-model"
|
||||
model_reasoning_effort = "medium"
|
||||
service_tier = "fast"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
requirements.models,
|
||||
Some(ModelsRequirementsToml {
|
||||
new_thread: Some(NewThreadModelDefaultsToml {
|
||||
model: Some("managed-model".to_string()),
|
||||
model_reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
service_tier: Some("fast".to_string()),
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert!(!requirements.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_unset_fields_copies_every_field_and_sets_sources() {
|
||||
let mut target = ConfigRequirementsWithSources::default();
|
||||
@@ -1844,6 +1910,13 @@ mod tests {
|
||||
let computer_use = ComputerUseRequirementsToml {
|
||||
allow_locked_computer_use: Some(false),
|
||||
};
|
||||
let models = ModelsRequirementsToml {
|
||||
new_thread: Some(NewThreadModelDefaultsToml {
|
||||
model: Some("managed-model".to_string()),
|
||||
model_reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
service_tier: Some("fast".to_string()),
|
||||
}),
|
||||
};
|
||||
let enforce_residency = ResidencyRequirement::Us;
|
||||
let enforce_source = source.clone();
|
||||
let guardian_policy_config = "Use the company-managed guardian policy.".to_string();
|
||||
@@ -1873,6 +1946,7 @@ mod tests {
|
||||
enforce_residency: Some(enforce_residency),
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: Some(models.clone()),
|
||||
guardian_policy_config: Some(guardian_policy_config.clone()),
|
||||
};
|
||||
|
||||
@@ -1923,6 +1997,7 @@ mod tests {
|
||||
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: Some(Sourced::new(models, source.clone())),
|
||||
guardian_policy_config: Some(Sourced::new(guardian_policy_config, source)),
|
||||
}
|
||||
);
|
||||
@@ -1970,6 +2045,7 @@ mod tests {
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: None,
|
||||
guardian_policy_config: None,
|
||||
}
|
||||
);
|
||||
@@ -2025,6 +2101,7 @@ mod tests {
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: None,
|
||||
guardian_policy_config: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -67,12 +67,14 @@ pub use config_requirements::FilesystemDenyReadPattern;
|
||||
pub use config_requirements::MarketplaceAllowedSourceKind;
|
||||
pub use config_requirements::MarketplaceAllowedSourceToml;
|
||||
pub use config_requirements::MarketplaceRequirementsToml;
|
||||
pub use config_requirements::ModelsRequirementsToml;
|
||||
pub use config_requirements::NetworkConstraints;
|
||||
pub use config_requirements::NetworkDomainPermissionToml;
|
||||
pub use config_requirements::NetworkDomainPermissionsToml;
|
||||
pub use config_requirements::NetworkRequirementsToml;
|
||||
pub use config_requirements::NetworkUnixSocketPermissionToml;
|
||||
pub use config_requirements::NetworkUnixSocketPermissionsToml;
|
||||
pub use config_requirements::NewThreadModelDefaultsToml;
|
||||
pub use config_requirements::PluginRequirementsToml;
|
||||
pub use config_requirements::RemoteSandboxConfigToml;
|
||||
pub use config_requirements::RequirementSource;
|
||||
|
||||
@@ -228,6 +228,7 @@ fn populate_merged_regular_fields_with_sources(
|
||||
enforce_residency,
|
||||
network,
|
||||
permissions,
|
||||
models,
|
||||
guardian_policy_config,
|
||||
} = requirements;
|
||||
|
||||
@@ -256,6 +257,7 @@ fn populate_merged_regular_fields_with_sources(
|
||||
set_sourced!(enforce_residency, &["enforce_residency"]);
|
||||
set_sourced!(network, &["experimental_network"]);
|
||||
set_sourced!(permissions, &["permissions"]);
|
||||
set_sourced!(models, &["models"]);
|
||||
|
||||
if let Some(guardian_policy_config) =
|
||||
guardian_policy_config.filter(|value| !value.trim().is_empty())
|
||||
|
||||
@@ -109,6 +109,46 @@ allow_remote_control = false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_thread_model_defaults_use_toml_priority() {
|
||||
let composed = compose(vec![
|
||||
layer(
|
||||
"req_low",
|
||||
"Low",
|
||||
r#"
|
||||
[models.new_thread]
|
||||
model = "low-priority-model"
|
||||
model_reasoning_effort = "low"
|
||||
service_tier = "flex"
|
||||
"#,
|
||||
),
|
||||
layer(
|
||||
"req_high",
|
||||
"High",
|
||||
r#"
|
||||
[models.new_thread]
|
||||
model = "high-priority-model"
|
||||
model_reasoning_effort = "high"
|
||||
service_tier = "fast"
|
||||
"#,
|
||||
),
|
||||
])
|
||||
.expect("compose requirements")
|
||||
.expect("requirements present");
|
||||
|
||||
assert_eq!(
|
||||
composed,
|
||||
expected_requirements(
|
||||
r#"
|
||||
[models.new_thread]
|
||||
model = "high-priority-model"
|
||||
model_reasoning_effort = "high"
|
||||
service_tier = "fast"
|
||||
"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_strategy_applies_to_non_cloud_layers() {
|
||||
let mdm_source = RequirementSource::MdmManagedPreferences {
|
||||
|
||||
@@ -8867,6 +8867,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: None,
|
||||
guardian_policy_config: None,
|
||||
};
|
||||
let requirement_source = codex_config::RequirementSource::Unknown;
|
||||
|
||||
@@ -791,6 +791,7 @@ mod tests {
|
||||
enforce_residency: Some(ResidencyRequirement::Us),
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: None,
|
||||
};
|
||||
|
||||
let user_file = if cfg!(windows) {
|
||||
@@ -1149,6 +1150,7 @@ approval_policy = "never"
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
permissions: None,
|
||||
models: None,
|
||||
};
|
||||
|
||||
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
|
||||
Reference in New Issue
Block a user