Merge a879dd4203 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-02-11 09:36:35 -08:00
committed by GitHub
8 changed files with 1962 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
use crate::config_loader::ConfigRequirementsToml;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use futures::future::Shared;
use std::fmt;
use std::future::Future;
#[derive(Clone)]
pub struct CloudRequirementsLoader {
// TODO(gt): This should return a Result once we can fail-closed.
fut: Shared<BoxFuture<'static, Option<ConfigRequirementsToml>>>,
}
impl CloudRequirementsLoader {
pub fn new<F>(fut: F) -> Self
where
F: Future<Output = Option<ConfigRequirementsToml>> + Send + 'static,
{
Self {
fut: fut.boxed().shared(),
}
}
pub async fn get(&self) -> Option<ConfigRequirementsToml> {
self.fut.clone().await
}
}
impl fmt::Debug for CloudRequirementsLoader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CloudRequirementsLoader").finish()
}
}
impl Default for CloudRequirementsLoader {
fn default() -> Self {
Self::new(async { None })
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
#[tokio::test]
async fn shared_future_runs_once() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let loader = CloudRequirementsLoader::new(async move {
counter_clone.fetch_add(1, Ordering::SeqCst);
Some(ConfigRequirementsToml::default())
});
let (first, second) = tokio::join!(loader.get(), loader.get());
assert_eq!(first, second);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
}

View File

@@ -0,0 +1,36 @@
#[path = "cloud_requirements.rs"]
pub mod cloud_requirements;
#[path = "config_requirements.rs"]
pub mod config_requirements;
#[path = "fingerprint.rs"]
pub mod fingerprint;
#[path = "merge.rs"]
pub mod merge;
#[path = "overrides.rs"]
pub mod overrides;
#[path = "requirements_exec_policy.rs"]
pub mod requirements_exec_policy;
#[path = "state.rs"]
pub mod state;
pub use cloud_requirements::CloudRequirementsLoader;
pub use config_requirements::ConfigRequirements;
pub use config_requirements::ConfigRequirementsToml;
pub use config_requirements::ConfigRequirementsWithSources;
pub use config_requirements::ConstrainedWithSource;
pub use config_requirements::McpServerIdentity;
pub use config_requirements::McpServerRequirement;
pub use config_requirements::NetworkConstraints;
pub use config_requirements::NetworkRequirementsToml;
pub use config_requirements::RequirementSource;
pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::Sourced;
pub use config_requirements::WebSearchModeRequirement;
pub use fingerprint::version_for_toml;
pub use merge::merge_toml_values;
pub use overrides::build_cli_overrides_layer;
pub use state::ConfigLayerEntry;
pub use state::ConfigLayerStack;
pub use state::ConfigLayerStackOrdering;
pub use state::LoaderOverrides;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
use codex_app_server_protocol::ConfigLayerMetadata;
use serde_json::Value as JsonValue;
use sha2::Digest;
use sha2::Sha256;
use std::collections::HashMap;
use toml::Value as TomlValue;
pub(super) fn record_origins(
value: &TomlValue,
meta: &ConfigLayerMetadata,
path: &mut Vec<String>,
origins: &mut HashMap<String, ConfigLayerMetadata>,
) {
match value {
TomlValue::Table(table) => {
for (key, val) in table {
path.push(key.clone());
record_origins(val, meta, path, origins);
path.pop();
}
}
TomlValue::Array(items) => {
for (idx, item) in (0_i32..).zip(items.iter()) {
path.push(idx.to_string());
record_origins(item, meta, path, origins);
path.pop();
}
}
_ => {
if !path.is_empty() {
origins.insert(path.join("."), meta.clone());
}
}
}
}
pub fn version_for_toml(value: &TomlValue) -> String {
let json = serde_json::to_value(value).unwrap_or(JsonValue::Null);
let canonical = canonical_json(&json);
let serialized = serde_json::to_vec(&canonical).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(serialized);
let hash = hasher.finalize();
let hex = hash
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
format!("sha256:{hex}")
}
fn canonical_json(value: &JsonValue) -> JsonValue {
match value {
JsonValue::Object(map) => {
let mut sorted = serde_json::Map::new();
let mut keys = map.keys().cloned().collect::<Vec<_>>();
keys.sort();
for key in keys {
if let Some(val) = map.get(&key) {
sorted.insert(key, canonical_json(val));
}
}
JsonValue::Object(sorted)
}
JsonValue::Array(items) => JsonValue::Array(items.iter().map(canonical_json).collect()),
other => other.clone(),
}
}

View File

@@ -0,0 +1,18 @@
use toml::Value as TomlValue;
/// Merge config `overlay` into `base`, giving `overlay` precedence.
pub fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) {
if let TomlValue::Table(overlay_table) = overlay
&& let TomlValue::Table(base_table) = base
{
for (key, value) in overlay_table {
if let Some(existing) = base_table.get_mut(key) {
merge_toml_values(existing, value);
} else {
base_table.insert(key.clone(), value.clone());
}
}
} else {
*base = overlay.clone();
}
}

View File

@@ -0,0 +1,55 @@
use toml::Value as TomlValue;
pub(crate) fn default_empty_table() -> TomlValue {
TomlValue::Table(Default::default())
}
pub fn build_cli_overrides_layer(cli_overrides: &[(String, TomlValue)]) -> TomlValue {
let mut root = default_empty_table();
for (path, value) in cli_overrides {
apply_toml_override(&mut root, path, value.clone());
}
root
}
/// Apply a single dotted-path override onto a TOML value.
fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
use toml::value::Table;
let mut current = root;
let mut segments_iter = path.split('.').peekable();
while let Some(segment) = segments_iter.next() {
let is_last = segments_iter.peek().is_none();
if is_last {
match current {
TomlValue::Table(table) => {
table.insert(segment.to_string(), value);
}
_ => {
let mut table = Table::new();
table.insert(segment.to_string(), value);
*current = TomlValue::Table(table);
}
}
return;
}
match current {
TomlValue::Table(table) => {
current = table
.entry(segment.to_string())
.or_insert_with(|| TomlValue::Table(Table::new()));
}
_ => {
*current = TomlValue::Table(Table::new());
if let TomlValue::Table(tbl) = current {
current = tbl
.entry(segment.to_string())
.or_insert_with(|| TomlValue::Table(Table::new()));
}
}
}
}
}

View File

@@ -0,0 +1,236 @@
use codex_execpolicy::Decision;
use codex_execpolicy::Policy;
use codex_execpolicy::rule::PatternToken;
use codex_execpolicy::rule::PrefixPattern;
use codex_execpolicy::rule::PrefixRule;
use codex_execpolicy::rule::RuleRef;
use multimap::MultiMap;
use serde::Deserialize;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct RequirementsExecPolicy {
policy: Policy,
}
impl RequirementsExecPolicy {
pub fn new(policy: Policy) -> Self {
Self { policy }
}
}
impl PartialEq for RequirementsExecPolicy {
fn eq(&self, other: &Self) -> bool {
policy_fingerprint(&self.policy) == policy_fingerprint(&other.policy)
}
}
impl Eq for RequirementsExecPolicy {}
impl AsRef<Policy> for RequirementsExecPolicy {
fn as_ref(&self) -> &Policy {
&self.policy
}
}
fn policy_fingerprint(policy: &Policy) -> Vec<String> {
let mut entries = Vec::new();
for (program, rules) in policy.rules().iter_all() {
for rule in rules {
entries.push(format!("{program}:{rule:?}"));
}
}
entries.sort();
entries
}
/// TOML representation of `[rules]` within `requirements.toml`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RequirementsExecPolicyToml {
pub prefix_rules: Vec<RequirementsExecPolicyPrefixRuleToml>,
}
/// A TOML representation of the `prefix_rule(...)` Starlark builtin.
///
/// This mirrors the builtin defined in `execpolicy/src/parser.rs`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RequirementsExecPolicyPrefixRuleToml {
pub pattern: Vec<RequirementsExecPolicyPatternTokenToml>,
pub decision: Option<RequirementsExecPolicyDecisionToml>,
pub justification: Option<String>,
}
/// TOML-friendly representation of a pattern token.
///
/// Starlark supports either a string token or a list of alternative tokens at
/// each position, but TOML arrays cannot mix strings and arrays. Using an
/// array of tables sidesteps that restriction.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RequirementsExecPolicyPatternTokenToml {
pub token: Option<String>,
pub any_of: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RequirementsExecPolicyDecisionToml {
Allow,
Prompt,
Forbidden,
}
impl RequirementsExecPolicyDecisionToml {
fn as_decision(self) -> Decision {
match self {
Self::Allow => Decision::Allow,
Self::Prompt => Decision::Prompt,
Self::Forbidden => Decision::Forbidden,
}
}
}
#[derive(Debug, Error)]
pub enum RequirementsExecPolicyParseError {
#[error("rules prefix_rules cannot be empty")]
EmptyPrefixRules,
#[error("rules prefix_rule at index {rule_index} has an empty pattern")]
EmptyPattern { rule_index: usize },
#[error(
"rules prefix_rule at index {rule_index} has an invalid pattern token at index {token_index}: {reason}"
)]
InvalidPatternToken {
rule_index: usize,
token_index: usize,
reason: String,
},
#[error("rules prefix_rule at index {rule_index} has an empty justification")]
EmptyJustification { rule_index: usize },
#[error("rules prefix_rule at index {rule_index} is missing a decision")]
MissingDecision { rule_index: usize },
#[error(
"rules prefix_rule at index {rule_index} has decision 'allow', which is not permitted in requirements.toml: Codex merges these rules with other config and uses the most restrictive result (use 'prompt' or 'forbidden')"
)]
AllowDecisionNotAllowed { rule_index: usize },
}
impl RequirementsExecPolicyToml {
/// Convert requirements TOML rules into the internal `.rules`
/// representation used by `codex-execpolicy`.
pub fn to_policy(&self) -> Result<Policy, RequirementsExecPolicyParseError> {
if self.prefix_rules.is_empty() {
return Err(RequirementsExecPolicyParseError::EmptyPrefixRules);
}
let mut rules_by_program: MultiMap<String, RuleRef> = MultiMap::new();
for (rule_index, rule) in self.prefix_rules.iter().enumerate() {
if let Some(justification) = &rule.justification
&& justification.trim().is_empty()
{
return Err(RequirementsExecPolicyParseError::EmptyJustification { rule_index });
}
if rule.pattern.is_empty() {
return Err(RequirementsExecPolicyParseError::EmptyPattern { rule_index });
}
let pattern_tokens = rule
.pattern
.iter()
.enumerate()
.map(|(token_index, token)| parse_pattern_token(token, rule_index, token_index))
.collect::<Result<Vec<_>, _>>()?;
let decision = match rule.decision {
Some(RequirementsExecPolicyDecisionToml::Allow) => {
return Err(RequirementsExecPolicyParseError::AllowDecisionNotAllowed {
rule_index,
});
}
Some(decision) => decision.as_decision(),
None => {
return Err(RequirementsExecPolicyParseError::MissingDecision { rule_index });
}
};
let justification = rule.justification.clone();
let (first_token, remaining_tokens) = pattern_tokens
.split_first()
.ok_or(RequirementsExecPolicyParseError::EmptyPattern { rule_index })?;
let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into();
for head in first_token.alternatives() {
let rule: RuleRef = Arc::new(PrefixRule {
pattern: PrefixPattern {
first: Arc::from(head.as_str()),
rest: rest.clone(),
},
decision,
justification: justification.clone(),
});
rules_by_program.insert(head.clone(), rule);
}
}
Ok(Policy::new(rules_by_program))
}
pub(crate) fn to_requirements_policy(
&self,
) -> Result<RequirementsExecPolicy, RequirementsExecPolicyParseError> {
self.to_policy().map(RequirementsExecPolicy::new)
}
}
fn parse_pattern_token(
token: &RequirementsExecPolicyPatternTokenToml,
rule_index: usize,
token_index: usize,
) -> Result<PatternToken, RequirementsExecPolicyParseError> {
match (&token.token, &token.any_of) {
(Some(single), None) => {
if single.trim().is_empty() {
return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "token cannot be empty".to_string(),
});
}
Ok(PatternToken::Single(single.clone()))
}
(None, Some(alternatives)) => {
if alternatives.is_empty() {
return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "any_of cannot be empty".to_string(),
});
}
if alternatives.iter().any(|alt| alt.trim().is_empty()) {
return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "any_of cannot include empty tokens".to_string(),
});
}
Ok(PatternToken::Alts(alternatives.clone()))
}
(Some(_), Some(_)) => Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "set either token or any_of, not both".to_string(),
}),
(None, None) => Err(RequirementsExecPolicyParseError::InvalidPatternToken {
rule_index,
token_index,
reason: "set either token or any_of".to_string(),
}),
}
}

View File

@@ -0,0 +1,311 @@
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use super::fingerprint::record_origins;
use super::fingerprint::version_for_toml;
use super::merge::merge_toml_values;
use codex_app_server_protocol::ConfigLayer;
use codex_app_server_protocol::ConfigLayerMetadata;
use codex_app_server_protocol::ConfigLayerSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::path::PathBuf;
use toml::Value as TomlValue;
/// LoaderOverrides overrides managed configuration inputs (primarily for tests).
#[derive(Debug, Default, Clone)]
pub struct LoaderOverrides {
pub managed_config_path: Option<PathBuf>,
//TODO(gt): Add a macos_ prefix to this field and remove the target_os check.
#[cfg(target_os = "macos")]
pub managed_preferences_base64: Option<String>,
pub macos_managed_config_requirements_base64: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigLayerEntry {
pub name: ConfigLayerSource,
pub config: TomlValue,
pub raw_toml: Option<String>,
pub version: String,
pub disabled_reason: Option<String>,
}
impl ConfigLayerEntry {
pub fn new(name: ConfigLayerSource, config: TomlValue) -> Self {
let version = version_for_toml(&config);
Self {
name,
config,
raw_toml: None,
version,
disabled_reason: None,
}
}
pub fn new_with_raw_toml(name: ConfigLayerSource, config: TomlValue, raw_toml: String) -> Self {
let version = version_for_toml(&config);
Self {
name,
config,
raw_toml: Some(raw_toml),
version,
disabled_reason: None,
}
}
pub fn new_disabled(
name: ConfigLayerSource,
config: TomlValue,
disabled_reason: impl Into<String>,
) -> Self {
let version = version_for_toml(&config);
Self {
name,
config,
raw_toml: None,
version,
disabled_reason: Some(disabled_reason.into()),
}
}
pub fn is_disabled(&self) -> bool {
self.disabled_reason.is_some()
}
pub fn raw_toml(&self) -> Option<&str> {
self.raw_toml.as_deref()
}
pub fn metadata(&self) -> ConfigLayerMetadata {
ConfigLayerMetadata {
name: self.name.clone(),
version: self.version.clone(),
}
}
pub fn as_layer(&self) -> ConfigLayer {
ConfigLayer {
name: self.name.clone(),
version: self.version.clone(),
config: serde_json::to_value(&self.config).unwrap_or(JsonValue::Null),
disabled_reason: self.disabled_reason.clone(),
}
}
// Get the `.codex/` folder associated with this config layer, if any.
pub fn config_folder(&self) -> Option<AbsolutePathBuf> {
match &self.name {
ConfigLayerSource::Mdm { .. } => None,
ConfigLayerSource::System { file } => file.parent(),
ConfigLayerSource::User { file } => file.parent(),
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
ConfigLayerSource::SessionFlags => None,
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None,
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigLayerStackOrdering {
LowestPrecedenceFirst,
HighestPrecedenceFirst,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConfigLayerStack {
/// Layers are listed from lowest precedence (base) to highest (top), so
/// later entries in the Vec override earlier ones.
layers: Vec<ConfigLayerEntry>,
/// Index into [layers] of the user config layer, if any.
user_layer_index: Option<usize>,
/// Constraints that must be enforced when deriving a [Config] from the
/// layers.
requirements: ConfigRequirements,
/// Raw requirements data as loaded from requirements.toml/MDM/legacy
/// sources. This preserves the original allow-lists so they can be
/// surfaced via APIs.
requirements_toml: ConfigRequirementsToml,
}
impl ConfigLayerStack {
pub fn new(
layers: Vec<ConfigLayerEntry>,
requirements: ConfigRequirements,
requirements_toml: ConfigRequirementsToml,
) -> std::io::Result<Self> {
let user_layer_index = verify_layer_ordering(&layers)?;
Ok(Self {
layers,
user_layer_index,
requirements,
requirements_toml,
})
}
/// Returns the user config layer, if any.
pub fn get_user_layer(&self) -> Option<&ConfigLayerEntry> {
self.user_layer_index
.and_then(|index| self.layers.get(index))
}
pub fn requirements(&self) -> &ConfigRequirements {
&self.requirements
}
pub fn requirements_toml(&self) -> &ConfigRequirementsToml {
&self.requirements_toml
}
/// Creates a new [ConfigLayerStack] using the specified values to inject a
/// "user layer" into the stack. If such a layer already exists, it is
/// replaced; otherwise, it is inserted into the stack at the appropriate
/// position based on precedence rules.
pub fn with_user_config(&self, config_toml: &AbsolutePathBuf, user_config: TomlValue) -> Self {
let user_layer = ConfigLayerEntry::new(
ConfigLayerSource::User {
file: config_toml.clone(),
},
user_config,
);
let mut layers = self.layers.clone();
match self.user_layer_index {
Some(index) => {
layers[index] = user_layer;
Self {
layers,
user_layer_index: self.user_layer_index,
requirements: self.requirements.clone(),
requirements_toml: self.requirements_toml.clone(),
}
}
None => {
let user_layer_index = match layers
.iter()
.position(|layer| layer.name.precedence() > user_layer.name.precedence())
{
Some(index) => {
layers.insert(index, user_layer);
index
}
None => {
layers.push(user_layer);
layers.len() - 1
}
};
Self {
layers,
user_layer_index: Some(user_layer_index),
requirements: self.requirements.clone(),
requirements_toml: self.requirements_toml.clone(),
}
}
}
}
pub fn effective_config(&self) -> TomlValue {
let mut merged = TomlValue::Table(toml::map::Map::new());
for layer in self.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false) {
merge_toml_values(&mut merged, &layer.config);
}
merged
}
pub fn origins(&self) -> HashMap<String, ConfigLayerMetadata> {
let mut origins = HashMap::new();
let mut path = Vec::new();
for layer in self.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false) {
record_origins(&layer.config, &layer.metadata(), &mut path, &mut origins);
}
origins
}
/// Returns the highest-precedence to lowest-precedence layers, so
/// `ConfigLayerSource::SessionFlags` would be first, if present.
pub fn layers_high_to_low(&self) -> Vec<&ConfigLayerEntry> {
self.get_layers(ConfigLayerStackOrdering::HighestPrecedenceFirst, false)
}
/// Returns the highest-precedence to lowest-precedence layers, so
/// `ConfigLayerSource::SessionFlags` would be first, if present.
pub fn get_layers(
&self,
ordering: ConfigLayerStackOrdering,
include_disabled: bool,
) -> Vec<&ConfigLayerEntry> {
let mut layers: Vec<&ConfigLayerEntry> = self
.layers
.iter()
.filter(|layer| include_disabled || !layer.is_disabled())
.collect();
if ordering == ConfigLayerStackOrdering::HighestPrecedenceFirst {
layers.reverse();
}
layers
}
}
/// Ensures precedence ordering of config layers is correct. Returns the index
/// of the user config layer, if any (at most one should exist).
fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result<Option<usize>> {
if !layers.iter().map(|layer| &layer.name).is_sorted() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"config layers are not in correct precedence order",
));
}
// The previous check ensured `layers` is sorted by precedence, so now we
// further verify that:
// 1. There is at most one user config layer.
// 2. Project layers are ordered from root to cwd.
let mut user_layer_index: Option<usize> = None;
let mut previous_project_dot_codex_folder: Option<&AbsolutePathBuf> = None;
for (index, layer) in layers.iter().enumerate() {
if matches!(layer.name, ConfigLayerSource::User { .. }) {
if user_layer_index.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"multiple user config layers found",
));
}
user_layer_index = Some(index);
}
if let ConfigLayerSource::Project {
dot_codex_folder: current_project_dot_codex_folder,
} = &layer.name
{
if let Some(previous) = previous_project_dot_codex_folder {
let Some(parent) = previous.as_path().parent() else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"project layer has no parent directory",
));
};
if previous == current_project_dot_codex_folder
|| !current_project_dot_codex_folder
.as_path()
.ancestors()
.any(|ancestor| ancestor == parent)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"project layers are not ordered from root to cwd",
));
}
}
previous_project_dot_codex_folder = Some(current_project_dot_codex_folder);
}
}
Ok(user_layer_index)
}