feat(import): resolve references and schema composition

This commit is contained in:
2026-08-29 08:54:26 +03:00
parent 6c2a3712d8
commit 55209a9bbc
46 changed files with 4848 additions and 437 deletions
+16 -2
View File
@@ -1,8 +1,9 @@
use std::fmt;
use crate::{
AdminProcessConfig, CacheSettings, DatabaseSettings, McpProcessConfig, MetricsSettings,
MigratorConfig, ObservabilitySettings, OtlpSettings, OutboundSettings, RuntimeSettings,
AdminProcessConfig, CacheSettings, DatabaseSettings, ExternalReferenceSettings,
McpProcessConfig, MetricsSettings, MigratorConfig, ObservabilitySettings, OtlpSettings,
OutboundSettings, RuntimeSettings,
};
impl fmt::Debug for DatabaseSettings {
@@ -32,6 +33,18 @@ impl fmt::Debug for OutboundSettings {
.finish()
}
}
impl fmt::Debug for ExternalReferenceSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExternalReferenceSettings")
.field("allowed_url_prefix_count", &self.allowed_url_prefixes.len())
.field("max_depth", &self.max_depth)
.field("max_documents", &self.max_documents)
.field("max_fetch_bytes", &self.max_fetch_bytes)
.field("fetch_timeout_ms", &self.fetch_timeout_ms)
.field("max_expanded_nodes", &self.max_expanded_nodes)
.finish()
}
}
impl fmt::Debug for RuntimeSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RuntimeSettings")
@@ -93,6 +106,7 @@ impl fmt::Debug for AdminProcessConfig {
f.debug_struct("AdminProcessConfig")
.field("database", &self.database)
.field("runtime", &self.runtime)
.field("external_references", &self.external_references)
.field("observability", &self.observability)
.field("storage_root", &"configured")
.field("session_secret", &self.session_secret)
+3 -2
View File
@@ -15,8 +15,9 @@ pub use diagnostic::{ConfigError, Diagnostic, DiagnosticCode};
pub use migrator::{MigratorConfig, parse_migrator};
pub use process::{
AdminProcessConfig, CacheBackend, CacheSettings, DatabaseSettings, DeprecationRecord,
EffectiveConfig, McpProcessConfig, MetricsSettings, ObservabilitySettings, OtlpSettings,
OutboundSettings, PoolSettings, ProcessKind, RateLimitSettings, RuntimeSettings, parse_process,
EffectiveConfig, ExternalReferenceSettings, McpProcessConfig, MetricsSettings,
ObservabilitySettings, OtlpSettings, OutboundSettings, PoolSettings, ProcessKind,
RateLimitSettings, RuntimeSettings, parse_process,
};
pub use schema::{
FieldMode, FieldSpec, ProcessScope, Sensitivity, deployment_field_registry, field_registry,
+22 -6
View File
@@ -12,7 +12,9 @@ use std::{
path::PathBuf,
};
use url::Url;
mod external_references;
mod list_parsers;
pub use external_references::ExternalReferenceSettings;
const MAX_ENV_VALUE_BYTES: usize = 8_192;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcessKind {
@@ -127,6 +129,7 @@ pub struct ObservabilitySettings {
pub struct AdminProcessConfig {
pub database: DatabaseSettings,
pub runtime: RuntimeSettings,
pub external_references: ExternalReferenceSettings,
pub observability: ObservabilitySettings,
pub bind_addr: SocketAddr,
pub storage_root: PathBuf,
@@ -154,8 +157,8 @@ pub struct McpProcessConfig {
#[derive(Clone)]
enum Projection {
Admin(AdminProcessConfig),
Mcp(McpProcessConfig),
Admin(Box<AdminProcessConfig>),
Mcp(Box<McpProcessConfig>),
}
#[derive(Clone)]
@@ -790,9 +793,10 @@ pub fn parse_process(
{
parser.push(DiagnosticCode::UnsafeCombination, "admin.exposure.tls");
}
Projection::Admin(AdminProcessConfig {
Projection::Admin(Box::new(AdminProcessConfig {
database,
runtime,
external_references: external_references::parse(&mut parser),
observability,
bind_addr,
storage_root: parser.absolute_path("CRANK_STORAGE_ROOT", "/var/lib/crank/storage"),
@@ -811,7 +815,7 @@ pub fn parse_process(
bootstrap_display_name: parser
.string("CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME", Some("Crank Owner")),
demo_seed: parser.boolean("CRANK_DEMO_SEED"),
})
}))
}
ProcessKind::McpServer => {
let rps = parser.number("CRANK_MCP_RATE_LIMIT_RPS") as u32;
@@ -819,7 +823,7 @@ pub fn parse_process(
if burst < rps {
parser.push(DiagnosticCode::UnsafeCombination, "mcp.rate_limit.burst");
}
Projection::Mcp(McpProcessConfig {
Projection::Mcp(Box::new(McpProcessConfig {
database,
runtime,
observability,
@@ -829,7 +833,7 @@ pub fn parse_process(
requests_per_second: rps,
burst,
},
})
}))
}
};
@@ -867,6 +871,18 @@ fn fingerprint_parts(kind: ProcessKind, projection: &Projection) -> Vec<String>
format!("session_ttl={}", config.session_ttl_hours),
format!("trusted_proxies={:?}", config.trusted_proxy_ips),
format!("demo={}", config.demo_seed),
format!(
"external_reference_prefixes={}",
config.external_references.allowed_url_prefixes.join(",")
),
format!(
"external_reference_limits={}:{}:{}:{}:{}",
config.external_references.max_depth,
config.external_references.max_documents,
config.external_references.max_fetch_bytes,
config.external_references.fetch_timeout_ms,
config.external_references.max_expanded_nodes,
),
"storage=path-configured".to_owned(),
format!("session_secret={}", config.session_secret.is_configured()),
format!("pepper={}", config.password_pepper.is_configured()),
@@ -0,0 +1,24 @@
#[derive(Clone, Eq, PartialEq)]
pub struct ExternalReferenceSettings {
/// Canonical HTTP(S) URL prefixes which opt an operator into remote `$ref` fetches.
/// An empty list is a deliberate default-deny switch.
pub allowed_url_prefixes: Vec<String>,
pub max_depth: usize,
pub max_documents: usize,
pub max_fetch_bytes: usize,
pub fetch_timeout_ms: u64,
pub max_expanded_nodes: usize,
}
pub(super) fn parse(parser: &mut super::Parser<'_>) -> ExternalReferenceSettings {
ExternalReferenceSettings {
allowed_url_prefixes: parser
.url_prefix_list("CRANK_IMPORT_EXTERNAL_REFERENCE_ALLOWED_URL_PREFIXES"),
max_depth: parser.number("CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_DEPTH") as usize,
max_documents: parser.number("CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_DOCUMENTS") as usize,
max_fetch_bytes: parser.number("CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_FETCH_BYTES") as usize,
fetch_timeout_ms: parser.number("CRANK_IMPORT_EXTERNAL_REFERENCE_FETCH_TIMEOUT_MS"),
max_expanded_nodes: parser.number("CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_EXPANDED_NODES")
as usize,
}
}
@@ -4,6 +4,7 @@ use crate::{
DiagnosticCode,
validation::{parse_host_list, parse_ip_list},
};
use url::Url;
impl super::Parser<'_> {
pub(super) fn host_list(&mut self, name: &'static str) -> Vec<String> {
@@ -33,4 +34,41 @@ impl super::Parser<'_> {
}
parsed.items
}
pub(super) fn url_prefix_list(&mut self, name: &'static str) -> Vec<String> {
let Some(raw) = self.optional(name) else {
return Vec::new();
};
let mut prefixes = Vec::new();
let mut invalid = false;
for item in raw.split(',') {
let item = item.trim();
let Ok(url) = Url::parse(item) else {
invalid = true;
continue;
};
if !matches!(url.scheme(), "http" | "https")
|| url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
{
invalid = true;
continue;
}
let canonical = url.to_string();
if !prefixes.contains(&canonical) {
prefixes.push(canonical);
}
}
if invalid {
self.push(DiagnosticCode::InvalidType, name);
}
if prefixes.len() > 64 {
self.push(DiagnosticCode::OutOfRange, name);
prefixes.truncate(64);
}
prefixes
}
}
+73 -1
View File
@@ -78,7 +78,7 @@ macro_rules! f {
};
}
static FIELDS: [FieldSpec; 59] = [
static FIELDS: [FieldSpec; 65] = [
FieldSpec {
compatibility: Some("legacy URL form"),
rules: &[
@@ -339,6 +339,78 @@ static FIELDS: [FieldSpec; 59] = [
Some(67108864),
Public
),
FieldSpec {
rules: &[
"empty list disables external OpenAPI reference fetching",
"each prefix must be canonical HTTP(S) without userinfo, query, or fragment",
],
..f!(
"import.external_references.allowed_url_prefixes",
"CRANK_IMPORT_EXTERNAL_REFERENCE_ALLOWED_URL_PREFIXES",
AdminApi,
"url_prefix_list",
None,
Some(""),
None,
None,
Internal
)
},
f!(
"import.external_references.max_depth",
"CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_DEPTH",
AdminApi,
"u32",
Some("edges"),
Some("8"),
Some(1),
Some(32),
Public
),
f!(
"import.external_references.max_documents",
"CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_DOCUMENTS",
AdminApi,
"u32",
Some("documents"),
Some("32"),
Some(1),
Some(32),
Public
),
f!(
"import.external_references.max_fetch_bytes",
"CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_FETCH_BYTES",
AdminApi,
"u64",
Some("bytes"),
Some("262144"),
Some(1),
Some(4194304),
Public
),
f!(
"import.external_references.fetch_timeout_ms",
"CRANK_IMPORT_EXTERNAL_REFERENCE_FETCH_TIMEOUT_MS",
AdminApi,
"u64",
Some("milliseconds"),
Some("10000"),
Some(1),
Some(300000),
Public
),
f!(
"import.external_references.max_expanded_nodes",
"CRANK_IMPORT_EXTERNAL_REFERENCE_MAX_EXPANDED_NODES",
AdminApi,
"u32",
Some("nodes"),
Some("10000"),
Some(1),
Some(100000),
Public
),
f!(
"observability.environment",
"CRANK_ENVIRONMENT",