mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
config requirements: improve requirement error messages (#8843)
**Before:**
```
Error loading configuration: value `Never` is not in the allowed set [OnRequest]
```
**After:**
```
Error loading configuration: invalid value for `approval_policy`: `Never` is not in the
allowed set [OnRequest] (set by MDM com.openai.codex:requirements_toml_base64)
```
Done by introducing a new struct `ConfigRequirementsWithSources` onto
which we `merge_unset_fields` now. Also introduces a pair of requirement
value and its `RequirementSource` (inspired by `ConfigLayerSource`):
```rust
pub struct Sourced<T> {
pub value: T,
pub source: RequirementSource,
}
```
This commit is contained in:
committed by
GitHub
Unverified
parent
484f6f4c26
commit
932a5a446f
@@ -1,25 +1,26 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::config_loader::RequirementSource;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error, PartialEq, Eq)]
|
#[derive(Debug, Error, PartialEq, Eq)]
|
||||||
pub enum ConstraintError {
|
pub enum ConstraintError {
|
||||||
#[error("value `{candidate}` is not in the allowed set {allowed}")]
|
#[error(
|
||||||
InvalidValue { candidate: String, allowed: String },
|
"invalid value for `{field_name}`: `{candidate}` is not in the allowed set {allowed} (set by {requirement_source})"
|
||||||
|
)]
|
||||||
|
InvalidValue {
|
||||||
|
field_name: &'static str,
|
||||||
|
candidate: String,
|
||||||
|
allowed: String,
|
||||||
|
requirement_source: RequirementSource,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("field `{field_name}` cannot be empty")]
|
#[error("field `{field_name}` cannot be empty")]
|
||||||
EmptyField { field_name: String },
|
EmptyField { field_name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConstraintError {
|
impl ConstraintError {
|
||||||
pub fn invalid_value(candidate: impl Into<String>, allowed: impl Into<String>) -> Self {
|
|
||||||
Self::InvalidValue {
|
|
||||||
candidate: candidate.into(),
|
|
||||||
allowed: allowed.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn empty_field(field_name: impl Into<String>) -> Self {
|
pub fn empty_field(field_name: impl Into<String>) -> Self {
|
||||||
Self::EmptyField {
|
Self::EmptyField {
|
||||||
field_name: field_name.into(),
|
field_name: field_name.into(),
|
||||||
@@ -63,24 +64,6 @@ impl<T: Send + Sync> Constrained<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allow_only(value: T) -> Self
|
|
||||||
where
|
|
||||||
T: PartialEq + Send + Sync + fmt::Debug + Clone + 'static,
|
|
||||||
{
|
|
||||||
#[expect(clippy::expect_used)]
|
|
||||||
Self::new(value.clone(), move |candidate| {
|
|
||||||
if *candidate == value {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ConstraintError::invalid_value(
|
|
||||||
format!("{candidate:?}"),
|
|
||||||
format!("{value:?}"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect("initial value should always be valid")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allow any value of T, using T's Default as the initial value.
|
/// Allow any value of T, using T's Default as the initial value.
|
||||||
pub fn allow_any_from_default() -> Self
|
pub fn allow_any_from_default() -> Self
|
||||||
where
|
where
|
||||||
@@ -89,22 +72,6 @@ impl<T: Send + Sync> Constrained<T> {
|
|||||||
Self::allow_any(T::default())
|
Self::allow_any(T::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allow_values(initial_value: T, allowed: Vec<T>) -> ConstraintResult<Self>
|
|
||||||
where
|
|
||||||
T: PartialEq + Send + Sync + fmt::Debug + 'static,
|
|
||||||
{
|
|
||||||
Self::new(initial_value, move |candidate| {
|
|
||||||
if allowed.contains(candidate) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ConstraintError::invalid_value(
|
|
||||||
format!("{candidate:?}"),
|
|
||||||
format!("{allowed:?}"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self) -> &T {
|
pub fn get(&self) -> &T {
|
||||||
&self.value
|
&self.value
|
||||||
}
|
}
|
||||||
@@ -154,6 +121,15 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
fn invalid_value(candidate: impl Into<String>, allowed: impl Into<String>) -> ConstraintError {
|
||||||
|
ConstraintError::InvalidValue {
|
||||||
|
field_name: "<unknown>",
|
||||||
|
candidate: candidate.into(),
|
||||||
|
allowed: allowed.into(),
|
||||||
|
requirement_source: RequirementSource::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn constrained_allow_any_accepts_any_value() {
|
fn constrained_allow_any_accepts_any_value() {
|
||||||
let mut constrained = Constrained::allow_any(5);
|
let mut constrained = Constrained::allow_any(5);
|
||||||
@@ -173,17 +149,11 @@ mod tests {
|
|||||||
if *value > 0 {
|
if *value > 0 {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ConstraintError::invalid_value(
|
Err(invalid_value(value.to_string(), "positive values"))
|
||||||
value.to_string(),
|
|
||||||
"positive values",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(result, Err(invalid_value("0", "positive values")));
|
||||||
result,
|
|
||||||
Err(ConstraintError::invalid_value("0", "positive values"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -192,10 +162,7 @@ mod tests {
|
|||||||
if *value > 0 {
|
if *value > 0 {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ConstraintError::invalid_value(
|
Err(invalid_value(value.to_string(), "positive values"))
|
||||||
value.to_string(),
|
|
||||||
"positive values",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect("initial value should be accepted");
|
.expect("initial value should be accepted");
|
||||||
@@ -203,7 +170,7 @@ mod tests {
|
|||||||
let err = constrained
|
let err = constrained
|
||||||
.set(-5)
|
.set(-5)
|
||||||
.expect_err("negative values should be rejected");
|
.expect_err("negative values should be rejected");
|
||||||
assert_eq!(err, ConstraintError::invalid_value("-5", "positive values"));
|
assert_eq!(err, invalid_value("-5", "positive values"));
|
||||||
assert_eq!(constrained.value(), 1);
|
assert_eq!(constrained.value(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,10 +180,7 @@ mod tests {
|
|||||||
if *value > 0 {
|
if *value > 0 {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ConstraintError::invalid_value(
|
Err(invalid_value(value.to_string(), "positive values"))
|
||||||
value.to_string(),
|
|
||||||
"positive values",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect("initial value should be accepted");
|
.expect("initial value should be accepted");
|
||||||
@@ -227,7 +191,7 @@ mod tests {
|
|||||||
let err = constrained
|
let err = constrained
|
||||||
.can_set(&-1)
|
.can_set(&-1)
|
||||||
.expect_err("can_set should reject negative value");
|
.expect_err("can_set should reject negative value");
|
||||||
assert_eq!(err, ConstraintError::invalid_value("-1", "positive values"));
|
assert_eq!(err, invalid_value("-1", "positive values"));
|
||||||
assert_eq!(constrained.value(), 1);
|
assert_eq!(constrained.value(), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,42 @@
|
|||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_protocol::protocol::SandboxPolicy;
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
use crate::config::Constrained;
|
use crate::config::Constrained;
|
||||||
use crate::config::ConstraintError;
|
use crate::config::ConstraintError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum RequirementSource {
|
||||||
|
Unknown,
|
||||||
|
MdmManagedPreferences { domain: String, key: String },
|
||||||
|
SystemRequirementsToml { file: AbsolutePathBuf },
|
||||||
|
LegacyManagedConfigTomlFromFile { file: AbsolutePathBuf },
|
||||||
|
LegacyManagedConfigTomlFromMdm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RequirementSource {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
RequirementSource::Unknown => write!(f, "<unspecified>"),
|
||||||
|
RequirementSource::MdmManagedPreferences { domain, key } => {
|
||||||
|
write!(f, "MDM {domain}:{key}")
|
||||||
|
}
|
||||||
|
RequirementSource::SystemRequirementsToml { file } => {
|
||||||
|
write!(f, "{}", file.as_path().display())
|
||||||
|
}
|
||||||
|
RequirementSource::LegacyManagedConfigTomlFromFile { file } => {
|
||||||
|
write!(f, "{}", file.as_path().display())
|
||||||
|
}
|
||||||
|
RequirementSource::LegacyManagedConfigTomlFromMdm => {
|
||||||
|
write!(f, "MDM managed_config.toml (legacy)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Normalized version of [`ConfigRequirementsToml`] after deserialization and
|
/// Normalized version of [`ConfigRequirementsToml`] after deserialization and
|
||||||
/// normalization.
|
/// normalization.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -30,6 +61,75 @@ pub struct ConfigRequirementsToml {
|
|||||||
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
|
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Value paired with the requirement source it came from, for better error
|
||||||
|
/// messages.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Sourced<T> {
|
||||||
|
pub value: T,
|
||||||
|
pub source: RequirementSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Sourced<T> {
|
||||||
|
pub fn new(value: T, source: RequirementSource) -> Self {
|
||||||
|
Self { value, source }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::ops::Deref for Sourced<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct ConfigRequirementsWithSources {
|
||||||
|
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
|
||||||
|
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigRequirementsWithSources {
|
||||||
|
pub fn merge_unset_fields(&mut self, source: RequirementSource, other: ConfigRequirementsToml) {
|
||||||
|
// For every field in `other` that is `Some`, if the corresponding field
|
||||||
|
// in `self` is `None`, copy the value from `other` into `self`.
|
||||||
|
macro_rules! fill_missing_take {
|
||||||
|
($base:expr, $other:expr, $source:expr, { $($field:ident),+ $(,)? }) => {
|
||||||
|
// Destructure without `..` so adding fields to `ConfigRequirementsToml`
|
||||||
|
// forces this merge logic to be updated.
|
||||||
|
let ConfigRequirementsToml { $($field: _,)+ } = &$other;
|
||||||
|
|
||||||
|
$(
|
||||||
|
if $base.$field.is_none()
|
||||||
|
&& let Some(value) = $other.$field.take()
|
||||||
|
{
|
||||||
|
$base.$field = Some(Sourced::new(value, $source.clone()));
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut other = other;
|
||||||
|
fill_missing_take!(
|
||||||
|
self,
|
||||||
|
other,
|
||||||
|
source,
|
||||||
|
{ allowed_approval_policies, allowed_sandbox_modes }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_toml(self) -> ConfigRequirementsToml {
|
||||||
|
let ConfigRequirementsWithSources {
|
||||||
|
allowed_approval_policies,
|
||||||
|
allowed_sandbox_modes,
|
||||||
|
} = self;
|
||||||
|
ConfigRequirementsToml {
|
||||||
|
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
|
||||||
|
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Currently, `external-sandbox` is not supported in config.toml, but it is
|
/// Currently, `external-sandbox` is not supported in config.toml, but it is
|
||||||
/// supported through programmatic use.
|
/// supported through programmatic use.
|
||||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -61,41 +161,38 @@ impl ConfigRequirementsToml {
|
|||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.allowed_approval_policies.is_none() && self.allowed_sandbox_modes.is_none()
|
self.allowed_approval_policies.is_none() && self.allowed_sandbox_modes.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For every field in `other` that is `Some`, if the corresponding field in
|
|
||||||
/// `self` is `None`, copy the value from `other` into `self`.
|
|
||||||
pub fn merge_unset_fields(&mut self, mut other: ConfigRequirementsToml) {
|
|
||||||
macro_rules! fill_missing_take {
|
|
||||||
($base:expr, $other:expr, { $($field:ident),+ $(,)? }) => {
|
|
||||||
$(
|
|
||||||
if $base.$field.is_none() {
|
|
||||||
if let Some(value) = $other.$field.take() {
|
|
||||||
$base.$field = Some(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)+
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fill_missing_take!(self, other, { allowed_approval_policies, allowed_sandbox_modes });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ConfigRequirementsToml> for ConfigRequirements {
|
impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||||
type Error = ConstraintError;
|
type Error = ConstraintError;
|
||||||
|
|
||||||
fn try_from(toml: ConfigRequirementsToml) -> Result<Self, Self::Error> {
|
fn try_from(toml: ConfigRequirementsWithSources) -> Result<Self, Self::Error> {
|
||||||
let ConfigRequirementsToml {
|
let ConfigRequirementsWithSources {
|
||||||
allowed_approval_policies,
|
allowed_approval_policies,
|
||||||
allowed_sandbox_modes,
|
allowed_sandbox_modes,
|
||||||
} = toml;
|
} = toml;
|
||||||
|
|
||||||
let approval_policy: Constrained<AskForApproval> = match allowed_approval_policies {
|
let approval_policy: Constrained<AskForApproval> = match allowed_approval_policies {
|
||||||
Some(policies) => {
|
Some(Sourced {
|
||||||
if let Some(first) = policies.first() {
|
value: policies,
|
||||||
Constrained::allow_values(*first, policies)?
|
source: requirement_source,
|
||||||
} else {
|
}) => {
|
||||||
|
let Some(initial_value) = policies.first().copied() else {
|
||||||
return Err(ConstraintError::empty_field("allowed_approval_policies"));
|
return Err(ConstraintError::empty_field("allowed_approval_policies"));
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Constrained::new(initial_value, move |candidate| {
|
||||||
|
if policies.contains(candidate) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ConstraintError::InvalidValue {
|
||||||
|
field_name: "approval_policy",
|
||||||
|
candidate: format!("{candidate:?}"),
|
||||||
|
allowed: format!("{policies:?}"),
|
||||||
|
requirement_source: requirement_source.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})?
|
||||||
}
|
}
|
||||||
None => Constrained::allow_any_from_default(),
|
None => Constrained::allow_any_from_default(),
|
||||||
};
|
};
|
||||||
@@ -109,12 +206,17 @@ impl TryFrom<ConfigRequirementsToml> for ConfigRequirements {
|
|||||||
// format to allow specifying those parameters.
|
// format to allow specifying those parameters.
|
||||||
let default_sandbox_policy = SandboxPolicy::ReadOnly;
|
let default_sandbox_policy = SandboxPolicy::ReadOnly;
|
||||||
let sandbox_policy: Constrained<SandboxPolicy> = match allowed_sandbox_modes {
|
let sandbox_policy: Constrained<SandboxPolicy> = match allowed_sandbox_modes {
|
||||||
Some(modes) => {
|
Some(Sourced {
|
||||||
|
value: modes,
|
||||||
|
source: requirement_source,
|
||||||
|
}) => {
|
||||||
if !modes.contains(&SandboxModeRequirement::ReadOnly) {
|
if !modes.contains(&SandboxModeRequirement::ReadOnly) {
|
||||||
return Err(ConstraintError::invalid_value(
|
return Err(ConstraintError::InvalidValue {
|
||||||
"allowed_sandbox_modes",
|
field_name: "allowed_sandbox_modes",
|
||||||
"must include 'read-only' to allow any SandboxPolicy",
|
candidate: format!("{modes:?}"),
|
||||||
));
|
allowed: "must include 'read-only' to allow any SandboxPolicy".to_string(),
|
||||||
|
requirement_source,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Constrained::new(default_sandbox_policy, move |candidate| {
|
Constrained::new(default_sandbox_policy, move |candidate| {
|
||||||
@@ -131,10 +233,12 @@ impl TryFrom<ConfigRequirementsToml> for ConfigRequirements {
|
|||||||
if modes.contains(&mode) {
|
if modes.contains(&mode) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ConstraintError::invalid_value(
|
Err(ConstraintError::InvalidValue {
|
||||||
format!("{candidate:?}"),
|
field_name: "sandbox_mode",
|
||||||
format!("{modes:?}"),
|
candidate: format!("{mode:?}"),
|
||||||
))
|
allowed: format!("{modes:?}"),
|
||||||
|
requirement_source: requirement_source.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
@@ -156,45 +260,168 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use toml::from_str;
|
use toml::from_str;
|
||||||
|
|
||||||
|
fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources {
|
||||||
|
let ConfigRequirementsToml {
|
||||||
|
allowed_approval_policies,
|
||||||
|
allowed_sandbox_modes,
|
||||||
|
} = toml;
|
||||||
|
ConfigRequirementsWithSources {
|
||||||
|
allowed_approval_policies: allowed_approval_policies
|
||||||
|
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
|
allowed_sandbox_modes: allowed_sandbox_modes
|
||||||
|
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_unset_fields_only_fills_missing_values() -> Result<()> {
|
fn merge_unset_fields_copies_every_field_and_sets_sources() {
|
||||||
|
let mut target = ConfigRequirementsWithSources::default();
|
||||||
|
let source = RequirementSource::LegacyManagedConfigTomlFromMdm;
|
||||||
|
|
||||||
|
let allowed_approval_policies = vec![AskForApproval::UnlessTrusted, AskForApproval::Never];
|
||||||
|
let allowed_sandbox_modes = vec![
|
||||||
|
SandboxModeRequirement::WorkspaceWrite,
|
||||||
|
SandboxModeRequirement::DangerFullAccess,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Intentionally constructed without `..Default::default()` so adding a new field to
|
||||||
|
// `ConfigRequirementsToml` forces this test to be updated.
|
||||||
|
let other = ConfigRequirementsToml {
|
||||||
|
allowed_approval_policies: Some(allowed_approval_policies.clone()),
|
||||||
|
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
target.merge_unset_fields(source.clone(), other);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
target,
|
||||||
|
ConfigRequirementsWithSources {
|
||||||
|
allowed_approval_policies: Some(Sourced::new(
|
||||||
|
allowed_approval_policies,
|
||||||
|
source.clone()
|
||||||
|
)),
|
||||||
|
allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_unset_fields_fills_missing_values() -> Result<()> {
|
||||||
let source: ConfigRequirementsToml = from_str(
|
let source: ConfigRequirementsToml = from_str(
|
||||||
r#"
|
r#"
|
||||||
allowed_approval_policies = ["on-request"]
|
allowed_approval_policies = ["on-request"]
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut empty_target: ConfigRequirementsToml = from_str(
|
let source_location = RequirementSource::MdmManagedPreferences {
|
||||||
r#"
|
domain: "com.codex".to_string(),
|
||||||
# intentionally left unset
|
key: "allowed_approval_policies".to_string(),
|
||||||
"#,
|
};
|
||||||
)?;
|
|
||||||
empty_target.merge_unset_fields(source.clone());
|
|
||||||
assert_eq!(
|
|
||||||
empty_target.allowed_approval_policies,
|
|
||||||
Some(vec![AskForApproval::OnRequest])
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut populated_target: ConfigRequirementsToml = from_str(
|
let mut empty_target = ConfigRequirementsWithSources::default();
|
||||||
|
empty_target.merge_unset_fields(source_location.clone(), source);
|
||||||
|
assert_eq!(
|
||||||
|
empty_target,
|
||||||
|
ConfigRequirementsWithSources {
|
||||||
|
allowed_approval_policies: Some(Sourced::new(
|
||||||
|
vec![AskForApproval::OnRequest],
|
||||||
|
source_location,
|
||||||
|
)),
|
||||||
|
allowed_sandbox_modes: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_unset_fields_does_not_overwrite_existing_values() -> Result<()> {
|
||||||
|
let existing_source = RequirementSource::LegacyManagedConfigTomlFromMdm;
|
||||||
|
let mut populated_target = ConfigRequirementsWithSources::default();
|
||||||
|
let populated_requirements: ConfigRequirementsToml = from_str(
|
||||||
r#"
|
r#"
|
||||||
allowed_approval_policies = ["never"]
|
allowed_approval_policies = ["never"]
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
populated_target.merge_unset_fields(source);
|
populated_target.merge_unset_fields(existing_source.clone(), populated_requirements);
|
||||||
|
|
||||||
|
let source: ConfigRequirementsToml = from_str(
|
||||||
|
r#"
|
||||||
|
allowed_approval_policies = ["on-request"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
let source_location = RequirementSource::MdmManagedPreferences {
|
||||||
|
domain: "com.codex".to_string(),
|
||||||
|
key: "allowed_approval_policies".to_string(),
|
||||||
|
};
|
||||||
|
populated_target.merge_unset_fields(source_location, source);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
populated_target.allowed_approval_policies,
|
populated_target,
|
||||||
Some(vec![AskForApproval::Never])
|
ConfigRequirementsWithSources {
|
||||||
|
allowed_approval_policies: Some(Sourced::new(
|
||||||
|
vec![AskForApproval::Never],
|
||||||
|
existing_source,
|
||||||
|
)),
|
||||||
|
allowed_sandbox_modes: None,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constraint_error_includes_requirement_source() -> Result<()> {
|
||||||
|
let source: ConfigRequirementsToml = from_str(
|
||||||
|
r#"
|
||||||
|
allowed_approval_policies = ["on-request"]
|
||||||
|
allowed_sandbox_modes = ["read-only"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let requirements_toml_file = if cfg!(windows) {
|
||||||
|
"C:\\etc\\codex\\requirements.toml"
|
||||||
|
} else {
|
||||||
|
"/etc/codex/requirements.toml"
|
||||||
|
};
|
||||||
|
let requirements_toml_file = AbsolutePathBuf::from_absolute_path(requirements_toml_file)?;
|
||||||
|
let source_location = RequirementSource::SystemRequirementsToml {
|
||||||
|
file: requirements_toml_file,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut target = ConfigRequirementsWithSources::default();
|
||||||
|
target.merge_unset_fields(source_location.clone(), source);
|
||||||
|
let requirements = ConfigRequirements::try_from(target)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
requirements.approval_policy.can_set(&AskForApproval::Never),
|
||||||
|
Err(ConstraintError::InvalidValue {
|
||||||
|
field_name: "approval_policy",
|
||||||
|
candidate: "Never".into(),
|
||||||
|
allowed: "[OnRequest]".into(),
|
||||||
|
requirement_source: source_location.clone(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
requirements
|
||||||
|
.sandbox_policy
|
||||||
|
.can_set(&SandboxPolicy::DangerFullAccess),
|
||||||
|
Err(ConstraintError::InvalidValue {
|
||||||
|
field_name: "sandbox_mode",
|
||||||
|
candidate: "DangerFullAccess".into(),
|
||||||
|
allowed: "[ReadOnly]".into(),
|
||||||
|
requirement_source: source_location,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_allowed_approval_policies() -> Result<()> {
|
fn deserialize_allowed_approval_policies() -> Result<()> {
|
||||||
let toml_str = r#"
|
let toml_str = r#"
|
||||||
allowed_approval_policies = ["untrusted", "on-request"]
|
allowed_approval_policies = ["untrusted", "on-request"]
|
||||||
"#;
|
"#;
|
||||||
let config: ConfigRequirementsToml = from_str(toml_str)?;
|
let config: ConfigRequirementsToml = from_str(toml_str)?;
|
||||||
let requirements = ConfigRequirements::try_from(config)?;
|
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
requirements.approval_policy.value(),
|
requirements.approval_policy.value(),
|
||||||
@@ -212,8 +439,10 @@ mod tests {
|
|||||||
.approval_policy
|
.approval_policy
|
||||||
.can_set(&AskForApproval::OnFailure),
|
.can_set(&AskForApproval::OnFailure),
|
||||||
Err(ConstraintError::InvalidValue {
|
Err(ConstraintError::InvalidValue {
|
||||||
|
field_name: "approval_policy",
|
||||||
candidate: "OnFailure".into(),
|
candidate: "OnFailure".into(),
|
||||||
allowed: "[UnlessTrusted, OnRequest]".into(),
|
allowed: "[UnlessTrusted, OnRequest]".into(),
|
||||||
|
requirement_source: RequirementSource::Unknown,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -225,8 +454,10 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
requirements.approval_policy.can_set(&AskForApproval::Never),
|
requirements.approval_policy.can_set(&AskForApproval::Never),
|
||||||
Err(ConstraintError::InvalidValue {
|
Err(ConstraintError::InvalidValue {
|
||||||
|
field_name: "approval_policy",
|
||||||
candidate: "Never".into(),
|
candidate: "Never".into(),
|
||||||
allowed: "[UnlessTrusted, OnRequest]".into(),
|
allowed: "[UnlessTrusted, OnRequest]".into(),
|
||||||
|
requirement_source: RequirementSource::Unknown,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -245,7 +476,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes = ["read-only", "workspace-write"]
|
allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||||
"#;
|
"#;
|
||||||
let config: ConfigRequirementsToml = from_str(toml_str)?;
|
let config: ConfigRequirementsToml = from_str(toml_str)?;
|
||||||
let requirements = ConfigRequirements::try_from(config)?;
|
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
|
||||||
|
|
||||||
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
|
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
|
||||||
assert!(
|
assert!(
|
||||||
@@ -270,8 +501,10 @@ mod tests {
|
|||||||
.sandbox_policy
|
.sandbox_policy
|
||||||
.can_set(&SandboxPolicy::DangerFullAccess),
|
.can_set(&SandboxPolicy::DangerFullAccess),
|
||||||
Err(ConstraintError::InvalidValue {
|
Err(ConstraintError::InvalidValue {
|
||||||
|
field_name: "sandbox_mode",
|
||||||
candidate: "DangerFullAccess".into(),
|
candidate: "DangerFullAccess".into(),
|
||||||
allowed: "[ReadOnly, WorkspaceWrite]".into(),
|
allowed: "[ReadOnly, WorkspaceWrite]".into(),
|
||||||
|
requirement_source: RequirementSource::Unknown,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -281,8 +514,10 @@ mod tests {
|
|||||||
network_access: NetworkAccess::Restricted,
|
network_access: NetworkAccess::Restricted,
|
||||||
}),
|
}),
|
||||||
Err(ConstraintError::InvalidValue {
|
Err(ConstraintError::InvalidValue {
|
||||||
candidate: "ExternalSandbox { network_access: Restricted }".into(),
|
field_name: "sandbox_mode",
|
||||||
|
candidate: "ExternalSandbox".into(),
|
||||||
allowed: "[ReadOnly, WorkspaceWrite]".into(),
|
allowed: "[ReadOnly, WorkspaceWrite]".into(),
|
||||||
|
requirement_source: RequirementSource::Unknown,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use super::config_requirements::ConfigRequirementsToml;
|
use super::config_requirements::ConfigRequirementsToml;
|
||||||
|
use super::config_requirements::ConfigRequirementsWithSources;
|
||||||
|
use super::config_requirements::RequirementSource;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::prelude::BASE64_STANDARD;
|
use base64::prelude::BASE64_STANDARD;
|
||||||
use core_foundation::base::TCFType;
|
use core_foundation::base::TCFType;
|
||||||
@@ -13,6 +15,13 @@ const MANAGED_PREFERENCES_APPLICATION_ID: &str = "com.openai.codex";
|
|||||||
const MANAGED_PREFERENCES_CONFIG_KEY: &str = "config_toml_base64";
|
const MANAGED_PREFERENCES_CONFIG_KEY: &str = "config_toml_base64";
|
||||||
const MANAGED_PREFERENCES_REQUIREMENTS_KEY: &str = "requirements_toml_base64";
|
const MANAGED_PREFERENCES_REQUIREMENTS_KEY: &str = "requirements_toml_base64";
|
||||||
|
|
||||||
|
pub(super) fn managed_preferences_requirements_source() -> RequirementSource {
|
||||||
|
RequirementSource::MdmManagedPreferences {
|
||||||
|
domain: MANAGED_PREFERENCES_APPLICATION_ID.to_string(),
|
||||||
|
key: MANAGED_PREFERENCES_REQUIREMENTS_KEY.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn load_managed_admin_config_layer(
|
pub(crate) async fn load_managed_admin_config_layer(
|
||||||
override_base64: Option<&str>,
|
override_base64: Option<&str>,
|
||||||
) -> io::Result<Option<TomlValue>> {
|
) -> io::Result<Option<TomlValue>> {
|
||||||
@@ -47,21 +56,26 @@ fn load_managed_admin_config() -> io::Result<Option<TomlValue>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn load_managed_admin_requirements_toml(
|
pub(crate) async fn load_managed_admin_requirements_toml(
|
||||||
target: &mut ConfigRequirementsToml,
|
target: &mut ConfigRequirementsWithSources,
|
||||||
override_base64: Option<&str>,
|
override_base64: Option<&str>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
if let Some(encoded) = override_base64 {
|
if let Some(encoded) = override_base64 {
|
||||||
let trimmed = encoded.trim();
|
let trimmed = encoded.trim();
|
||||||
if !trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
target.merge_unset_fields(parse_managed_requirements_base64(trimmed)?);
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target.merge_unset_fields(
|
||||||
|
managed_preferences_requirements_source(),
|
||||||
|
parse_managed_requirements_base64(trimmed)?,
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match task::spawn_blocking(load_managed_admin_requirements).await {
|
match task::spawn_blocking(load_managed_admin_requirements).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if let Some(requirements) = result? {
|
if let Some(requirements) = result? {
|
||||||
target.merge_unset_fields(requirements);
|
target.merge_unset_fields(managed_preferences_requirements_source(), requirements);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod tests;
|
|||||||
|
|
||||||
use crate::config::CONFIG_TOML_FILE;
|
use crate::config::CONFIG_TOML_FILE;
|
||||||
use crate::config::ConfigToml;
|
use crate::config::ConfigToml;
|
||||||
|
use crate::config_loader::config_requirements::ConfigRequirementsWithSources;
|
||||||
use crate::config_loader::layer_io::LoadedConfigLayers;
|
use crate::config_loader::layer_io::LoadedConfigLayers;
|
||||||
use codex_app_server_protocol::ConfigLayerSource;
|
use codex_app_server_protocol::ConfigLayerSource;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
@@ -25,6 +26,7 @@ use toml::Value as TomlValue;
|
|||||||
|
|
||||||
pub use config_requirements::ConfigRequirements;
|
pub use config_requirements::ConfigRequirements;
|
||||||
pub use config_requirements::ConfigRequirementsToml;
|
pub use config_requirements::ConfigRequirementsToml;
|
||||||
|
pub use config_requirements::RequirementSource;
|
||||||
pub use config_requirements::SandboxModeRequirement;
|
pub use config_requirements::SandboxModeRequirement;
|
||||||
pub use merge::merge_toml_values;
|
pub use merge::merge_toml_values;
|
||||||
pub use state::ConfigLayerEntry;
|
pub use state::ConfigLayerEntry;
|
||||||
@@ -77,7 +79,7 @@ pub async fn load_config_layers_state(
|
|||||||
cli_overrides: &[(String, TomlValue)],
|
cli_overrides: &[(String, TomlValue)],
|
||||||
overrides: LoaderOverrides,
|
overrides: LoaderOverrides,
|
||||||
) -> io::Result<ConfigLayerStack> {
|
) -> io::Result<ConfigLayerStack> {
|
||||||
let mut config_requirements_toml = ConfigRequirementsToml::default();
|
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
macos::load_managed_admin_requirements_toml(
|
macos::load_managed_admin_requirements_toml(
|
||||||
@@ -202,9 +204,11 @@ pub async fn load_config_layers_state(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let requirements_toml = config_requirements_toml.clone();
|
ConfigLayerStack::new(
|
||||||
let requirements = config_requirements_toml.try_into()?;
|
layers,
|
||||||
ConfigLayerStack::new(layers, requirements, requirements_toml)
|
config_requirements_toml.clone().try_into()?,
|
||||||
|
config_requirements_toml.into_toml(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to load a config.toml file from `config_toml`.
|
/// Attempts to load a config.toml file from `config_toml`.
|
||||||
@@ -256,9 +260,11 @@ async fn load_config_toml_for_required_layer(
|
|||||||
/// If available, apply requirements from `/etc/codex/requirements.toml` to
|
/// If available, apply requirements from `/etc/codex/requirements.toml` to
|
||||||
/// `config_requirements_toml` by filling in any unset fields.
|
/// `config_requirements_toml` by filling in any unset fields.
|
||||||
async fn load_requirements_toml(
|
async fn load_requirements_toml(
|
||||||
config_requirements_toml: &mut ConfigRequirementsToml,
|
config_requirements_toml: &mut ConfigRequirementsWithSources,
|
||||||
requirements_toml_file: impl AsRef<Path>,
|
requirements_toml_file: impl AsRef<Path>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
|
let requirements_toml_file =
|
||||||
|
AbsolutePathBuf::from_absolute_path(requirements_toml_file.as_ref())?;
|
||||||
match tokio::fs::read_to_string(&requirements_toml_file).await {
|
match tokio::fs::read_to_string(&requirements_toml_file).await {
|
||||||
Ok(contents) => {
|
Ok(contents) => {
|
||||||
let requirements_config: ConfigRequirementsToml =
|
let requirements_config: ConfigRequirementsToml =
|
||||||
@@ -271,7 +277,12 @@ async fn load_requirements_toml(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
config_requirements_toml.merge_unset_fields(requirements_config);
|
config_requirements_toml.merge_unset_fields(
|
||||||
|
RequirementSource::SystemRequirementsToml {
|
||||||
|
file: requirements_toml_file.clone(),
|
||||||
|
},
|
||||||
|
requirements_config,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind() != io::ErrorKind::NotFound {
|
if e.kind() != io::ErrorKind::NotFound {
|
||||||
@@ -290,7 +301,7 @@ async fn load_requirements_toml(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_requirements_from_legacy_scheme(
|
async fn load_requirements_from_legacy_scheme(
|
||||||
config_requirements_toml: &mut ConfigRequirementsToml,
|
config_requirements_toml: &mut ConfigRequirementsWithSources,
|
||||||
loaded_config_layers: LoadedConfigLayers,
|
loaded_config_layers: LoadedConfigLayers,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// In this implementation, earlier layers cannot be overwritten by later
|
// In this implementation, earlier layers cannot be overwritten by later
|
||||||
@@ -300,12 +311,16 @@ async fn load_requirements_from_legacy_scheme(
|
|||||||
managed_config,
|
managed_config,
|
||||||
managed_config_from_mdm,
|
managed_config_from_mdm,
|
||||||
} = loaded_config_layers;
|
} = loaded_config_layers;
|
||||||
for config in [
|
|
||||||
managed_config_from_mdm,
|
for (source, config) in managed_config_from_mdm
|
||||||
managed_config.map(|c| c.managed_config),
|
.map(|config| (RequirementSource::LegacyManagedConfigTomlFromMdm, config))
|
||||||
]
|
.into_iter()
|
||||||
.into_iter()
|
.chain(managed_config.map(|c| {
|
||||||
.flatten()
|
(
|
||||||
|
RequirementSource::LegacyManagedConfigTomlFromFile { file: c.file },
|
||||||
|
c.managed_config,
|
||||||
|
)
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
let legacy_config: LegacyManagedConfigToml =
|
let legacy_config: LegacyManagedConfigToml =
|
||||||
config.try_into().map_err(|err: toml::de::Error| {
|
config.try_into().map_err(|err: toml::de::Error| {
|
||||||
@@ -316,7 +331,7 @@ async fn load_requirements_from_legacy_scheme(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let new_requirements_toml = ConfigRequirementsToml::from(legacy_config);
|
let new_requirements_toml = ConfigRequirementsToml::from(legacy_config);
|
||||||
config_requirements_toml.merge_unset_fields(new_requirements_toml);
|
config_requirements_toml.merge_unset_fields(source, new_requirements_toml);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::config::ConfigBuilder;
|
|||||||
use crate::config::ConfigOverrides;
|
use crate::config::ConfigOverrides;
|
||||||
use crate::config_loader::ConfigLayerEntry;
|
use crate::config_loader::ConfigLayerEntry;
|
||||||
use crate::config_loader::ConfigRequirements;
|
use crate::config_loader::ConfigRequirements;
|
||||||
use crate::config_loader::config_requirements::ConfigRequirementsToml;
|
use crate::config_loader::config_requirements::ConfigRequirementsWithSources;
|
||||||
use crate::config_loader::fingerprint::version_for_toml;
|
use crate::config_loader::fingerprint::version_for_toml;
|
||||||
use crate::config_loader::load_requirements_toml;
|
use crate::config_loader::load_requirements_toml;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
@@ -315,11 +315,14 @@ allowed_approval_policies = ["never", "on-request"]
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut config_requirements_toml = ConfigRequirementsToml::default();
|
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||||
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
|
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config_requirements_toml.allowed_approval_policies,
|
config_requirements_toml
|
||||||
|
.allowed_approval_policies
|
||||||
|
.as_deref()
|
||||||
|
.cloned(),
|
||||||
Some(vec![AskForApproval::Never, AskForApproval::OnRequest])
|
Some(vec![AskForApproval::Never, AskForApproval::OnRequest])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use codex_core::config::Config;
|
|||||||
use codex_core::config::ConfigBuilder;
|
use codex_core::config::ConfigBuilder;
|
||||||
use codex_core::config::Constrained;
|
use codex_core::config::Constrained;
|
||||||
use codex_core::config::ConstraintError;
|
use codex_core::config::ConstraintError;
|
||||||
|
use codex_core::config_loader::RequirementSource;
|
||||||
use codex_core::features::Feature;
|
use codex_core::features::Feature;
|
||||||
use codex_core::models_manager::manager::ModelsManager;
|
use codex_core::models_manager::manager::ModelsManager;
|
||||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||||
@@ -85,6 +86,15 @@ async fn test_config() -> Config {
|
|||||||
.expect("config")
|
.expect("config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn invalid_value(candidate: impl Into<String>, allowed: impl Into<String>) -> ConstraintError {
|
||||||
|
ConstraintError::InvalidValue {
|
||||||
|
field_name: "<unknown>",
|
||||||
|
candidate: candidate.into(),
|
||||||
|
allowed: allowed.into(),
|
||||||
|
requirement_source: RequirementSource::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn snapshot(percent: f64) -> RateLimitSnapshot {
|
fn snapshot(percent: f64) -> RateLimitSnapshot {
|
||||||
RateLimitSnapshot {
|
RateLimitSnapshot {
|
||||||
primary: Some(RateLimitWindow {
|
primary: Some(RateLimitWindow {
|
||||||
@@ -2303,7 +2313,7 @@ async fn approvals_popup_shows_disabled_presets() {
|
|||||||
chat.config.approval_policy =
|
chat.config.approval_policy =
|
||||||
Constrained::new(AskForApproval::OnRequest, |candidate| match candidate {
|
Constrained::new(AskForApproval::OnRequest, |candidate| match candidate {
|
||||||
AskForApproval::OnRequest => Ok(()),
|
AskForApproval::OnRequest => Ok(()),
|
||||||
_ => Err(ConstraintError::invalid_value(
|
_ => Err(invalid_value(
|
||||||
candidate.to_string(),
|
candidate.to_string(),
|
||||||
"this message should be printed in the description",
|
"this message should be printed in the description",
|
||||||
)),
|
)),
|
||||||
@@ -2339,10 +2349,7 @@ async fn approvals_popup_navigation_skips_disabled() {
|
|||||||
chat.config.approval_policy =
|
chat.config.approval_policy =
|
||||||
Constrained::new(AskForApproval::OnRequest, |candidate| match candidate {
|
Constrained::new(AskForApproval::OnRequest, |candidate| match candidate {
|
||||||
AskForApproval::OnRequest => Ok(()),
|
AskForApproval::OnRequest => Ok(()),
|
||||||
_ => Err(ConstraintError::invalid_value(
|
_ => Err(invalid_value(candidate.to_string(), "[on-request]")),
|
||||||
candidate.to_string(),
|
|
||||||
"[on-request]",
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.expect("construct constrained approval policy");
|
.expect("construct constrained approval policy");
|
||||||
chat.open_approvals_popup();
|
chat.open_approvals_popup();
|
||||||
|
|||||||
Reference in New Issue
Block a user