mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
d45cd26248
## Summary - Adapts the moved `codex-cloud-config` crate from the legacy cloud requirements endpoint to the new config bundle endpoint. - Switches runtime consumers from `CloudRequirementsLoader` to `CloudConfigBundleLoader` so one shared bundle supplies cloud-delivered config and requirements. - Removes the legacy cloud requirements domain loader path. ## Details This intentionally keeps `codex-cloud-config` monolithic for review lineage: the previous PR establishes the crate move, and this PR shows the behavior change against that moved implementation. A follow-up PR splits the module back into focused files. The new bundle path preserves the important cloud requirements loader semantics where intended: account-scoped signed cache, 30 minute TTL, 5 minute refresh cadence, retry/backoff, auth recovery, and fail-closed startup loading. The cached payload changes from a single requirements TOML string to the backend-delivered bundle, and validation rejects malformed config or requirements fragments before cache write/use.
215 lines
6.5 KiB
Rust
215 lines
6.5 KiB
Rust
//! Cloud config bundle domain model and shared in-memory loader.
|
|
//!
|
|
//! The backend bundle groups cloud-delivered config and requirements fragments
|
|
//! by source bucket. `CloudConfigBundleLayers` converts those raw buckets into
|
|
//! layer entries while preserving each bucket's insertion semantics.
|
|
|
|
use crate::CloudConfigFragment;
|
|
use crate::ConfigLayerEntry;
|
|
use crate::RequirementSource;
|
|
use crate::RequirementsLayerEntry;
|
|
use crate::cloud_config_layers::CloudConfigLayerError;
|
|
use crate::cloud_config_layers::cloud_config_layers_from_fragments_strict;
|
|
use crate::cloud_config_layers_from_fragments;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use futures::future::BoxFuture;
|
|
use futures::future::FutureExt;
|
|
use futures::future::Shared;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::fmt;
|
|
use std::future::Future;
|
|
use thiserror::Error;
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
|
pub struct CloudConfigBundle {
|
|
pub config_toml: CloudConfigTomlBundle,
|
|
pub requirements_toml: CloudRequirementsTomlBundle,
|
|
}
|
|
|
|
impl CloudConfigBundle {
|
|
pub fn is_empty(&self) -> bool {
|
|
let CloudConfigBundle {
|
|
config_toml,
|
|
requirements_toml,
|
|
} = self;
|
|
let CloudConfigTomlBundle {
|
|
enterprise_managed: config_enterprise_managed,
|
|
} = config_toml;
|
|
let CloudRequirementsTomlBundle {
|
|
enterprise_managed: requirements_enterprise_managed,
|
|
} = requirements_toml;
|
|
|
|
config_enterprise_managed.is_empty() && requirements_enterprise_managed.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
|
pub struct CloudConfigTomlBundle {
|
|
pub enterprise_managed: Vec<CloudConfigFragment>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
|
pub struct CloudRequirementsTomlBundle {
|
|
pub enterprise_managed: Vec<CloudRequirementsFragment>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
pub struct CloudRequirementsFragment {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub contents: String,
|
|
}
|
|
|
|
/// Cloud config bundle converted into semantic layer buckets.
|
|
///
|
|
/// This is not a final config stack. Callers still decide where each bucket is
|
|
/// inserted relative to local/system/user layers.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CloudConfigBundleLayers {
|
|
/// Enterprise-managed config layers in `ConfigLayerStack` order.
|
|
pub enterprise_managed_config: Vec<ConfigLayerEntry>,
|
|
/// Enterprise-managed requirements layers in requirements layer merge order.
|
|
pub enterprise_managed_requirements: Vec<RequirementsLayerEntry>,
|
|
}
|
|
|
|
impl CloudConfigBundleLayers {
|
|
pub fn from_bundle(
|
|
bundle: CloudConfigBundle,
|
|
base_dir: &AbsolutePathBuf,
|
|
) -> Result<Self, CloudConfigLayerError> {
|
|
Self::from_bundle_impl(bundle, base_dir, /*strict_config*/ false)
|
|
}
|
|
|
|
pub fn from_bundle_strict_config(
|
|
bundle: CloudConfigBundle,
|
|
base_dir: &AbsolutePathBuf,
|
|
) -> Result<Self, CloudConfigLayerError> {
|
|
Self::from_bundle_impl(bundle, base_dir, /*strict_config*/ true)
|
|
}
|
|
|
|
fn from_bundle_impl(
|
|
bundle: CloudConfigBundle,
|
|
base_dir: &AbsolutePathBuf,
|
|
strict_config: bool,
|
|
) -> Result<Self, CloudConfigLayerError> {
|
|
// Keep this destructuring exhaustive so adding a new bundle bucket forces
|
|
// an explicit choice about how it becomes layer data.
|
|
let CloudConfigBundle {
|
|
config_toml:
|
|
CloudConfigTomlBundle {
|
|
enterprise_managed: config_enterprise_managed,
|
|
},
|
|
requirements_toml:
|
|
CloudRequirementsTomlBundle {
|
|
enterprise_managed: requirements_enterprise_managed,
|
|
},
|
|
} = bundle;
|
|
|
|
let enterprise_managed_config = if strict_config {
|
|
cloud_config_layers_from_fragments_strict(config_enterprise_managed, base_dir)?
|
|
} else {
|
|
cloud_config_layers_from_fragments(config_enterprise_managed, base_dir)?
|
|
};
|
|
|
|
let mut enterprise_managed_requirements = requirements_enterprise_managed
|
|
.into_iter()
|
|
.map(|fragment| {
|
|
RequirementsLayerEntry::from_toml(
|
|
RequirementSource::EnterpriseManaged {
|
|
id: fragment.id,
|
|
name: fragment.name,
|
|
},
|
|
fragment.contents,
|
|
)
|
|
.with_base_dir(base_dir.clone())
|
|
})
|
|
.collect::<Vec<_>>();
|
|
// Bundle fragments arrive highest-priority first, while requirements
|
|
// layers are merged lowest-priority to highest-priority.
|
|
enterprise_managed_requirements.reverse();
|
|
|
|
Ok(Self {
|
|
enterprise_managed_config,
|
|
enterprise_managed_requirements,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum CloudConfigBundleLoadErrorCode {
|
|
Auth,
|
|
Timeout,
|
|
RequestFailed,
|
|
InvalidBundle,
|
|
Internal,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Error, PartialEq)]
|
|
#[error("{message}")]
|
|
pub struct CloudConfigBundleLoadError {
|
|
code: CloudConfigBundleLoadErrorCode,
|
|
message: String,
|
|
status_code: Option<u16>,
|
|
}
|
|
|
|
impl CloudConfigBundleLoadError {
|
|
pub fn new(
|
|
code: CloudConfigBundleLoadErrorCode,
|
|
status_code: Option<u16>,
|
|
message: impl Into<String>,
|
|
) -> Self {
|
|
Self {
|
|
code,
|
|
message: message.into(),
|
|
status_code,
|
|
}
|
|
}
|
|
|
|
pub fn code(&self) -> CloudConfigBundleLoadErrorCode {
|
|
self.code
|
|
}
|
|
|
|
pub fn status_code(&self) -> Option<u16> {
|
|
self.status_code
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CloudConfigBundleLoader {
|
|
fut: Shared<BoxFuture<'static, Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError>>>,
|
|
}
|
|
|
|
impl CloudConfigBundleLoader {
|
|
pub fn new<F>(fut: F) -> Self
|
|
where
|
|
F: Future<Output = Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError>>
|
|
+ Send
|
|
+ 'static,
|
|
{
|
|
Self {
|
|
fut: fut.boxed().shared(),
|
|
}
|
|
}
|
|
|
|
pub async fn get(&self) -> Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError> {
|
|
self.fut.clone().await
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for CloudConfigBundleLoader {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("CloudConfigBundleLoader").finish()
|
|
}
|
|
}
|
|
|
|
impl Default for CloudConfigBundleLoader {
|
|
fn default() -> Self {
|
|
Self::new(async { Ok(None) })
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "cloud_config_bundle_tests.rs"]
|
|
mod tests;
|