fix(pi): certify pinned native inspection semantics

This commit is contained in:
SaladDay
2026-08-02 15:24:48 +00:00
parent 15c735f040
commit 2a0a8125eb
12 changed files with 942 additions and 120 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ test-hooks = []
tauri-build = { version = "2.4.0", features = [] }
[dependencies]
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
jsonc-parser = { version = "0.33", features = ["cst", "serde_json"] }
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
+56 -34
View File
@@ -148,22 +148,18 @@ struct ArchitectureVisitor<'a> {
internal_edges: BTreeSet<String>,
}
fn pi_config_source_module(path: &str) -> Option<&'static str> {
["raw_schema", "composer", "gateway", "native", "model"]
.into_iter()
.find(|module| {
let root = format!("pi_config/{module}");
path.ends_with(&format!("{root}.rs")) || path.contains(&format!("{root}/"))
})
}
impl ArchitectureVisitor<'_> {
fn record_dependency(&mut self, segments: &[String]) {
let source_module = if self.path.ends_with("pi_config/raw_schema.rs") {
Some("raw_schema")
} else if self.path.ends_with("pi_config/composer.rs") {
Some("composer")
} else if self.path.ends_with("pi_config/gateway.rs") {
Some("gateway")
} else if self.path.ends_with("pi_config/native.rs") {
Some("native")
} else if self.path.ends_with("pi_config/model.rs") {
Some("model")
} else {
None
};
let Some(source_module) = source_module else {
let Some(source_module) = pi_config_source_module(self.path) else {
return;
};
let qualified_internal_path = segments.len() > 1
@@ -398,6 +394,27 @@ fn rust_sources(root: &Path) -> Vec<PathBuf> {
output
}
fn scan_production_tree(
manifest_dir: &Path,
source_root: &Path,
) -> (Vec<Violation>, BTreeSet<String>) {
let mut violations = Vec::new();
let mut edges = BTreeSet::new();
for path in rust_sources(source_root) {
let relative = path
.strip_prefix(manifest_dir)
.expect("source is under manifest")
.to_string_lossy()
.replace('\\', "/");
let source = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
let (mut source_violations, source_edges) = scan_source(&relative, &source);
violations.append(&mut source_violations);
edges.extend(source_edges);
}
(violations, edges)
}
fn type_leaf(ty: &Type) -> String {
match ty {
Type::Path(path) => path
@@ -480,26 +497,7 @@ fn provider_write_api_snapshot(source: &str) -> serde_json::Value {
fn architecture_scanner_accepts_production_tree_and_matches_machine_snapshots() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let source_root = manifest_dir.join("src");
let mut violations = Vec::new();
let mut edges = BTreeSet::new();
for path in rust_sources(&source_root) {
let relative = path
.strip_prefix(manifest_dir)
.expect("source is under manifest")
.to_string_lossy()
.replace('\\', "/");
if relative.ends_with("architecture_tests.rs")
|| relative.ends_with("/tests.rs")
|| relative.contains("/tests/")
{
continue;
}
let source = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
let (mut source_violations, source_edges) = scan_source(&relative, &source);
violations.append(&mut source_violations);
edges.extend(source_edges);
}
let (violations, edges) = scan_production_tree(manifest_dir, &source_root);
assert!(
violations.is_empty(),
"architecture violations:\n{}",
@@ -648,4 +646,28 @@ fn architecture_scanner_negative_fixtures_prove_each_guard_fires() {
violations.is_empty(),
"a predicate requiring test must stay excluded: {violations:?}"
);
let temp = tempfile::tempdir().expect("temp architecture tree");
let nested = temp.path().join("src/pi_config/composer/tests/mod.rs");
fs::create_dir_all(nested.parent().expect("nested parent")).expect("create nested tree");
fs::write(
&nested,
r#"
use super::super::gateway::PiGatewayApiFamily;
fn production_escape_attempt() {
save_provider();
}
"#,
)
.expect("write production nested-module fixture");
let (violations, _) = scan_production_tree(temp.path(), &temp.path().join("src"));
for expected_kind in ["cross_layer_import", "forbidden_provider_symbol"] {
assert!(
violations
.iter()
.any(|violation| violation.kind == expected_kind),
"production code under tests/ must inherit composer ownership and trigger \
{expected_kind}: {violations:?}"
);
}
}
+50 -36
View File
@@ -6,7 +6,10 @@
#![allow(dead_code)]
use super::raw_schema::{PiRawApiId, PiRawValidProvider};
use super::{
merge_pi_compat,
raw_schema::{PiRawApiId, PiRawValidProvider},
};
use serde_json::{json, Map, Value};
use std::collections::{BTreeMap, HashSet};
@@ -302,7 +305,7 @@ pub(super) fn compose_explicit_custom_catalog(
headers: BTreeMap::new(),
provider_headers: provider_header_entries.clone(),
model_headers: Vec::new(),
compat: merge_compat(provider_compat.clone(), definition.get("compat").cloned()),
compat: merge_pi_compat(provider_compat.clone(), definition.get("compat").cloned()),
api_key: api_key.clone(),
oauth: oauth.clone(),
auth_header,
@@ -398,7 +401,7 @@ pub(super) fn compose_explicit_custom_catalog(
model.max_tokens = max_tokens.clone();
}
model.compat =
merge_compat(model.compat.clone(), model_override.get("compat").cloned());
merge_pi_compat(model.compat.clone(), model_override.get("compat").cloned());
model.override_extra = unknown_fields(model_override, OVERRIDE_FIELDS);
}
}
@@ -454,39 +457,6 @@ fn merge_cost(base: &Value, overlay: &Map<String, Value>) -> Value {
Value::Object(merged)
}
fn merge_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
let Some(overlay) = overlay else {
return base;
};
let mut merged = base
.as_ref()
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let Some(overlay_object) = overlay.as_object() else {
return Some(overlay);
};
for (key, value) in overlay_object {
if matches!(
key.as_str(),
"openRouterRouting" | "vercelGatewayRouting" | "chatTemplateKwargs"
) {
let base_nested = merged.get(key).and_then(Value::as_object);
let override_nested = value.as_object();
if base_nested.is_some() || override_nested.is_some() {
let mut nested = base_nested.cloned().unwrap_or_default();
if let Some(override_nested) = override_nested {
nested.extend(override_nested.clone());
}
merged.insert(key.clone(), Value::Object(nested));
continue;
}
}
merged.insert(key.clone(), value.clone());
}
Some(Value::Object(merged))
}
fn header_entries(value: Option<&Value>, base_pointer: &str) -> Vec<PiComposedHeader> {
value
.and_then(Value::as_object)
@@ -816,6 +786,50 @@ mod tests {
);
}
#[test]
fn compat_spread_matches_pinned_composer_request_capture() {
let value = json!({
"api": "openai-responses",
"baseUrl": "https://compat.example/v1",
"apiKey": "literal",
"compat": {
"openRouterRouting": ["first", "second"],
"chatTemplateKwargs": "ab",
"baseOnly": true
},
"models": [{
"id": "m",
"compat": {"supportsStore": true}
}],
"modelOverrides": {
"m": {
"compat": {
"openRouterRouting": null,
"chatTemplateKwargs": {"named": true},
"overlayOnly": true
}
}
}
});
let raw = evaluate_provider_value(&value);
let composed = compose_explicit_custom_catalog(
"compat-spread",
raw.valid_provider.as_ref().expect("raw-valid"),
);
assert_eq!(
composed.models[0].compat,
Some(json!({
"openRouterRouting": {"0": "first", "1": "second"},
"chatTemplateKwargs": {"0": "a", "1": "b", "named": true},
"baseOnly": true,
"supportsStore": true,
"overlayOnly": true
})),
"captured by scripts/pi-transport-capture.mjs at the pinned Pi commit"
);
}
#[test]
fn header_layers_retain_runtime_precedence_and_source_pointers() {
let value = json!({
+23
View File
@@ -288,6 +288,29 @@ mod tests {
assert!(entry.raw_source.contains("\"model//literal\""));
}
#[test]
fn parses_javascript_overflow_without_hiding_sibling_entries() {
let document = parse_models_source(
Path::new("models.json"),
r#"{
"providers": {
"healthy": {"models": [{"id": "healthy"}]},
"overflow": {"models": [{"id": "m", "contextWindow": 1e400}]}
}
}"#,
)
.expect("JSON.parse accepts the numeric token before TypeBox validation");
assert!(document.providers().contains_key("healthy"));
let overflow = &document.providers()["overflow"].value["models"][0]["contextWindow"];
assert!(
overflow
.as_number()
.is_some_and(|number| number.as_f64().is_none()),
"the raw evaluator must still be able to distinguish non-finite JavaScript Number"
);
}
#[test]
fn rejects_json_extensions_that_pinned_pi_rejects() {
let temp = tempfile::tempdir().expect("tempdir");
+33 -3
View File
@@ -122,6 +122,7 @@ pub(super) enum PiGatewayCapability {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PiGatewayReasonCode {
UnsupportedFamily,
UnsupportedCredentialKind,
InvalidEndpoint,
MissingCredential,
InvalidHeaderName,
@@ -242,11 +243,27 @@ impl CandidateHeaderPlan {
}
};
let credential = model.api_key.as_ref();
if credential.is_none() {
reasons.push(PiGatewayReason {
match credential {
None => reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::MissingCredential,
json_pointer: "/apiKey".to_string(),
});
}),
Some(credential) if !is_deferred(credential) => {
if parse_transport_header_value(credential).is_none() {
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::InvalidHeaderValue,
json_pointer: "/apiKey".to_string(),
});
} else if family == PiGatewayApiFamily::AnthropicMessages
&& is_anthropic_oauth_credential(credential)
{
reasons.push(PiGatewayReason {
code: PiGatewayReasonCode::UnsupportedCredentialKind,
json_pointer: "/apiKey".to_string(),
});
}
}
Some(_) => {}
};
let mut protocol_identity_predictable = true;
@@ -292,6 +309,14 @@ impl CandidateHeaderPlan {
let mut headers = HeaderMap::new();
let mut protocol_headers = HeaderMap::new();
let credential = self.credential.materialize(resolver, "/apiKey")?;
if self.family == PiGatewayApiFamily::AnthropicMessages
&& credential.to_str().is_ok_and(is_anthropic_oauth_credential)
{
return Err(PiGatewayReason {
code: PiGatewayReasonCode::UnsupportedCredentialKind,
json_pointer: "/apiKey".to_string(),
});
}
let bearer_credential = credential.clone();
let (auth_name, auth_value) = match self.family {
PiGatewayApiFamily::AnthropicMessages => {
@@ -486,6 +511,11 @@ fn is_deferred(value: &str) -> bool {
value.starts_with('!') || value.contains('$')
}
fn is_anthropic_oauth_credential(value: &str) -> bool {
// Pinned Pi's Anthropic adapter uses `includes`, not a prefix test.
value.contains("sk-ant-oat")
}
fn parse_transport_header_value(value: &str) -> Option<HeaderValue> {
if !value.bytes().all(|byte| matches!(byte, 0x20..=0x7e)) {
return None;
+112
View File
@@ -4,6 +4,8 @@
//! Pi's shared files and from the proxy data plane. Callers must use the
//! typed model resolver rather than reimplementing provider/model inheritance.
use serde_json::{Map, Value};
mod composer;
mod document;
mod gateway;
@@ -12,3 +14,113 @@ pub(crate) mod native;
#[cfg(test)]
mod native_inspection_certification;
mod raw_schema;
const PI_COMPAT_NESTED_SPREAD_KEYS: [&str; 3] = [
"openRouterRouting",
"vercelGatewayRouting",
"chatTemplateKwargs",
];
/// Mirror pinned Pi's `mergeCompat` JavaScript object-spread semantics.
///
/// Arrays expose numeric enumerable properties, strings expose character
/// properties, objects expose their own fields, and the remaining JSON
/// primitives expose none. Existing key positions are retained when an
/// overlay replaces their values, matching object spread.
fn merge_pi_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
let Some(overlay) = overlay else {
return base;
};
if !javascript_truthy(&overlay) {
return base;
}
let mut merged = javascript_object_spread(base.as_ref());
merged.extend(javascript_object_spread(Some(&overlay)));
for key in PI_COMPAT_NESTED_SPREAD_KEYS {
let base_value = javascript_property(base.as_ref(), key);
let overlay_value = javascript_property(Some(&overlay), key);
if base_value.is_some_and(javascript_is_object)
|| overlay_value.is_some_and(javascript_is_object)
{
let mut nested = javascript_object_spread(base_value);
nested.extend(javascript_object_spread(overlay_value));
merged.insert(key.to_string(), Value::Object(nested));
}
}
Some(Value::Object(merged))
}
fn javascript_truthy(value: &Value) -> bool {
match value {
Value::Null | Value::Bool(false) => false,
Value::Number(value) => value.as_f64().is_none_or(|value| value != 0.0),
Value::String(value) => !value.is_empty(),
Value::Bool(true) | Value::Array(_) | Value::Object(_) => true,
}
}
fn javascript_is_object(value: &Value) -> bool {
matches!(value, Value::Array(_) | Value::Object(_))
}
fn javascript_property<'a>(value: Option<&'a Value>, key: &str) -> Option<&'a Value> {
value.and_then(Value::as_object)?.get(key)
}
fn javascript_object_spread(value: Option<&Value>) -> Map<String, Value> {
match value {
Some(Value::Object(object)) => object.clone(),
Some(Value::Array(values)) => values
.iter()
.enumerate()
.map(|(index, value)| (index.to_string(), value.clone()))
.collect(),
Some(Value::String(value)) => value
.chars()
.enumerate()
.map(|(index, value)| (index.to_string(), Value::String(value.to_string())))
.collect(),
Some(Value::Null | Value::Bool(_) | Value::Number(_)) | None => Map::new(),
}
}
#[cfg(test)]
mod compat_spread_tests {
use super::*;
use serde_json::json;
#[test]
fn compat_nested_values_follow_javascript_object_spread() {
let merged = merge_pi_compat(
Some(json!({
"openRouterRouting": ["first", "second"],
"chatTemplateKwargs": "ab",
"baseOnly": true
})),
Some(json!({
"openRouterRouting": null,
"chatTemplateKwargs": {"named": true},
"overlayOnly": true
})),
)
.expect("truthy overlay produces an object");
assert_eq!(
merged,
json!({
"openRouterRouting": {"0": "first", "1": "second"},
"chatTemplateKwargs": {"0": "a", "1": "b", "named": true},
"baseOnly": true,
"overlayOnly": true
})
);
}
#[test]
fn compat_falsy_overlay_returns_base_without_spreading() {
let base = Some(json!({"openRouterRouting": ["kept"]}));
assert_eq!(merge_pi_compat(base.clone(), Some(Value::Null)), base);
}
}
+106 -30
View File
@@ -7,6 +7,7 @@
#![allow(dead_code)]
use super::merge_pi_compat;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -144,8 +145,8 @@ pub(crate) struct PiModelCostTier {
pub(crate) struct PiModelCost {
#[serde(flatten)]
pub rates: PiModelCostRates,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tiers: Vec<PiModelCostTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tiers: Option<Vec<PiModelCostTier>>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
@@ -404,6 +405,7 @@ pub(crate) enum PiReasonCode {
InvalidThinkingLevel,
CompositionFailed,
GatewayCredentialUnavailable,
UnsupportedCredentialKind,
UnsupportedGatewayFamily,
InvalidHeaderName,
InvalidHeaderValue,
@@ -550,8 +552,8 @@ fn effective_pi_model_unchecked(
.map(|entry| apply_cost_override(base_cost.clone(), entry))
.unwrap_or(base_cost);
let compat = merge_compat(
merge_compat(provider.compat.clone(), model.compat.clone()),
let compat = merge_pi_compat(
merge_pi_compat(provider.compat.clone(), model.compat.clone()),
model_override.and_then(|entry| entry.compat.clone()),
);
@@ -620,37 +622,12 @@ fn apply_cost_override(mut base: PiModelCost, model_override: &PiModelCostOverri
cache_write: model_override.cache_write.unwrap_or(base.rates.cache_write),
};
if let Some(tiers) = &model_override.tiers {
base.tiers = tiers.clone();
base.tiers = Some(tiers.clone());
}
base.extra.extend(model_override.extra.clone());
base
}
fn merge_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
match (base, overlay) {
(None, None) => None,
(value, None) | (None, value) => value,
(Some(Value::Object(mut base)), Some(Value::Object(overlay))) => {
for (key, value) in overlay {
if matches!(
key.as_str(),
"openRouterRouting" | "vercelGatewayRouting" | "chatTemplateKwargs"
) {
if let Some(Value::Object(base_nested)) = base.get_mut(&key) {
if let Value::Object(overlay_nested) = &value {
base_nested.extend(overlay_nested.clone());
continue;
}
}
}
base.insert(key, value);
}
Some(Value::Object(base))
}
(Some(_), Some(overlay)) => Some(overlay),
}
}
fn validate_optional_text(value: Option<&str>, field: &'static str) -> Result<(), PiConfigError> {
if value.is_some_and(str::is_empty) {
return Err(PiConfigError::EmptyOptionalField { field });
@@ -891,6 +868,46 @@ mod tests {
);
}
#[test]
fn effective_compat_uses_the_pinned_composer_spread_result() {
let config: PiManagedProviderConfig = serde_json::from_value(json!({
"api": "openai-responses",
"baseUrl": "https://compat.example/v1",
"compat": {
"openRouterRouting": ["first", "second"],
"chatTemplateKwargs": "ab",
"baseOnly": true
},
"models": [{
"id": "m",
"compat": {"supportsStore": true}
}],
"modelOverrides": {
"m": {
"compat": {
"openRouterRouting": null,
"chatTemplateKwargs": {"named": true},
"overlayOnly": true
}
}
}
}))
.expect("deserialize pinned compat vector");
assert_eq!(
effective_pi_model(&config, "m")
.expect("effective compat")
.compat,
Some(json!({
"openRouterRouting": {"0": "first", "1": "second"},
"chatTemplateKwargs": {"0": "a", "1": "b", "named": true},
"baseOnly": true,
"supportsStore": true,
"overlayOnly": true
}))
);
}
#[test]
fn fractional_pi_numbers_survive_managed_round_trip() {
let mut fractional = model("fractional");
@@ -1001,6 +1018,51 @@ mod tests {
);
}
#[test]
fn empty_cost_tiers_remain_distinct_from_absent_at_managed_and_effective_boundaries() {
let config: PiManagedProviderConfig = serde_json::from_value(json!({
"api": "anthropic-messages",
"baseUrl": "https://cost.example",
"models": [
{
"id": "empty",
"cost": {
"input": 1.0,
"output": 2.0,
"cacheRead": 0.5,
"cacheWrite": 0.25,
"tiers": []
}
},
{
"id": "absent",
"cost": {
"input": 1.0,
"output": 2.0,
"cacheRead": 0.5,
"cacheWrite": 0.25
}
}
]
}))
.expect("deserialize explicit and absent tiers");
let managed = serde_json::to_value(&config).expect("serialize managed config");
assert_eq!(managed.pointer("/models/0/cost/tiers"), Some(&json!([])));
assert_eq!(managed.pointer("/models/1/cost/tiers"), None);
let empty_effective = serde_json::to_value(
effective_pi_model(&config, "empty").expect("effective empty tiers"),
)
.expect("serialize effective empty tiers");
let absent_effective = serde_json::to_value(
effective_pi_model(&config, "absent").expect("effective absent tiers"),
)
.expect("serialize effective absent tiers");
assert_eq!(empty_effective.pointer("/cost/tiers"), Some(&json!([])));
assert_eq!(absent_effective.pointer("/cost/tiers"), None);
}
#[test]
fn diagnostic_reason_serialization_is_structured() {
let reason = PiDiagnosticReason::new(
@@ -1016,5 +1078,19 @@ mod tests {
"jsonPointer": "/models/0/api"
})
);
let credential_reason = PiDiagnosticReason::new(
PiDiagnosticLayer::Gateway,
PiReasonCode::UnsupportedCredentialKind,
Some("/apiKey".into()),
);
assert_eq!(
serde_json::to_value(credential_reason).expect("serialize"),
json!({
"layer": "gateway",
"code": "unsupported_credential_kind",
"jsonPointer": "/apiKey"
})
);
}
}
+3
View File
@@ -604,6 +604,9 @@ fn map_gateway_reasons(gateway: &PiGatewayAssessment) -> Vec<PiDiagnosticReason>
PiGatewayReasonCode::UnsupportedFamily => {
PiReasonCode::UnsupportedGatewayFamily
}
PiGatewayReasonCode::UnsupportedCredentialKind => {
PiReasonCode::UnsupportedCredentialKind
}
PiGatewayReasonCode::InvalidEndpoint => PiReasonCode::InvalidEndpoint,
PiGatewayReasonCode::MissingCredential => {
PiReasonCode::GatewayCredentialUnavailable
@@ -11,12 +11,13 @@
//! 盲审前置条件而非充分条件。既有测试不得弱化——以测试总数只增不减 + 盲审
//! 核对为准,不设扫描器。
//!
//! ## 条裁决及其上游证据
//! ## 条裁决及其上游证据
//! C1【无损性】pinned schema 对 `thinkingLevelMap` 只约束 7 个标准键
//! (string|null;oracle 实证 `low: 2` 非法),额外键无约束(oracle 实证
//! `future: {nested:true}` 合法);`cost`/tier 同样接受未来键。managed 与
//! **effective 边界**(`effective_pi_model` 是 projection/routing/failover
//! 的共同入口)都必须无损,`{}` 与缺席必须保持可区分
//! 的共同入口)都必须无损,**空容器与缺席必须保持可区分**(`{}` 之于
//! thinkingLevelMap、`[]` 之于 cost.tiers 同理)。
//! 据此取代两个既有测试中把收窄固化为断言的部分:
//! `managed_narrowing_rejects_duplicates_or_unknown_thinking_keys` 与
//! `unknown_thinking_shape_is_lossless_for_composer_and_narrowed_separately`
@@ -41,22 +42,64 @@
//! `!command` / 展开 `${ENV}`,再使用结果;**从不按 HTTP 头规则校验原始
//! 表达式**(命令输出 trim,环境模板不 trim,解析结果亦不做头合法性校验)。
//! 因此原始表达式含头非法字符、而解析结果合法的配置必须被接受;头合法性
//! 校验只能发生在物化之后(这是网关自身的传输约束,保留)。字面量值仍
//! 原样校验。
//! 校验只能发生在物化之后(这是网关自身的传输约束,保留)。**字面量值仍
//! 判定期校验,且该规则对 credential 与 header 一视同仁**——判定期说
//! "可代理"而每次物化必然失败,是判定层与执行层自相矛盾。
//!
//! C5【凭证种类,2026-08-02 新增,**已 request-capture 实证**】pinned
//! Anthropic 传输层以 `apiKey.includes("sk-ant-oat")`(子串,非前缀)判定
//! OAuth,命中则以 `Authorization: Bearer` 发送、**不发 x-api-key**,并附
//! `anthropic-beta: claude-code-20250219,oauth-2025-04-20,...`;**models.json
//! 里的字面量 apiKey 同样会走该分支**;该判定**只在 Anthropic 族**,同形
//! token 在 OpenAI 族仍按普通 Bearer 发送。因此网关不得把这类凭证当普通
//! x-api-key 代理:字面量命中即判定期 DirectOnly 并给结构化理由(不得是
//! MissingCredential);deferred 凭证判定期不可知,则**物化期解析出命中值
//! 时必须失败**,绝不发出错误的认证形态。
//! **完整 OAuth 传输(Bearer + oauth beta 值)不在前置 C 范围**——按
//! docs/pi-support-restructure-zh.md §1,gateway 数据面属主工程,且需要
//! 先补 request-capture oracle。本工程只保证判定诚实、不发错凭证。
//! C6【entry 隔离,2026-08-02 新增】pinned Pi 逐 entry 做 TypeBox 判定,
//! 单个 entry 的取值错误(如 `contextWindow: 1e400`)只令该 entry 非法;
//! 整文件解析失败会让合法的兄弟 entry 被连坐隐藏,违反四层判定"每个
//! entry 独立"的核心设计。
//!
//! ## 实现方义务(不在本文件断言,交盲审核查)
//! O1 `compat` 需复现 JavaScript object-spread 对嵌套值(尤其数组)的语义;
//! O2 架构扫描器:cfg 布尔语义(`cfg(not(test))` 的生产代码必须被扫描)、
//! 不得按 `tests/` 路径整体跳过文件、嵌套模块须继承父层归属。
//!
//! ## 预期红绿(2026-08-02 复审修订基线)
//! 轮实现已使 C1/C2/C3 四项转绿。本次修订新增两项:
//! `certify_header_only_credentials_stay_direct_only` **应绿**——它把"无
//! apiKey 即降级"钉为契约(上游证据见 C2,首轮盲审曾按缺陷提报,现裁定
//! 实现正确、契约缺失);`certify_deferred_header_values_are_validated_after_resolution`
//! **应红**(C4:现实现对原始表达式做头校验)。
//! 即:应红 1、应绿 8。偏离(非清单红、应红变绿、编译失败)即上报
//! 前两轮实现已使 C1–C4 全部转绿(9 项)。本轮据盲审 finding 新增四项,
//! 全部**应红**:`certify_oauth_credentials_are_never_proxied_as_api_key`(C5)、
//! `certify_literal_credentials_are_validated_at_plan_time`(C4 扩展:
//! 判定期说可代理、物化必失败的自相矛盾)、
//! `certify_empty_containers_stay_distinct_from_absent`(C1 扩展:cost.tiers
//! `[]` 被抹成缺席)、`certify_one_bad_entry_does_not_hide_its_siblings`(C6)
//! 即:应红 4、应绿 9。偏离(非清单红、应红变绿、编译失败)即上报。
//!
//! ## 本轮裁决的两点澄清
//! 1. 盲审以"C2 同族复发"停止,**裁定不成立**:C2 管的是认证头的分类与取值
//! 次序,C5 是凭证种类识别,且完整 OAuth 传输按范围表属主工程。停止解除。
//! 2. 盲审对"网关强制 apiKey"的上一轮 High 已于前轮裁定为实现正确(见 C2)。
//!
//! ## 上游实证(request-capture,2026-08-02)
//! `scripts/pi-transport-capture.mjs` 以本地抓包端点作 baseUrl,用 pinned Pi
//! 的 adapter 真发请求,实测矩阵(据此 C2/C5 不再是"读源码推断"):
//! - anthropic 普通 key → `x-api-key: <key>`;
//! - anthropic `sk-ant-oat...` → `authorization: Bearer <token>` +
//! `anthropic-beta: claude-code-20250219,oauth-2025-04-20,...`,**无 x-api-key**;
//! - anthropic apiKey + 显式 `x-api-key` → 发**显式值**(显式覆盖合成);
//! - anthropic apiKey + 显式 `authorization` → 两者**并存**
//! (`authorization` 取显式值,`x-api-key` 取合成值);
//! - openai responses/completions + 显式 `authorization` → 发**显式值**;
//! - openai + `sk-ant-oat` 形状 token → 仍是普通 `Bearer`,无 OAuth 特殊处理;
//! - openai completions + 显式 `x-api-key` → 与合成 `authorization` **并存**。
//!
//! ## 残余
//! 显式优先只对 Anthropic/OpenAI 两族有 SDK 证据(Google 两值并存的优先级、
//! 大小写变体未断言);transport oracle 只执行 resolver,不执行 adapter/SDK
//! 头合并,主工程触碰数据面须先补 request-capture oracle;命令输出 trim 与
//! 环境模板不 trim 的差异属数据面语义,本只读面不断言;其余按盲审 finding 处理。
//! Google 两值并存的优先级、头名大小写变体未实测;命令输出 trim 与环境模板
//! 不 trim 的差异属数据面语义,本只读面不断言;完整 OAuth 传输实现按范围表
//! 归主工程(harness 已就位,可直接扩为受冻结的 transport oracle);
//! 其余按盲审 finding 处理。
use super::composer::compose_explicit_custom_catalog;
use super::gateway::{assess_composition, PiGatewayCapability, PiGatewayReasonCode};
@@ -64,7 +107,7 @@ use super::model::{
effective_pi_model, validate_pi_managed_provider, PiConfigError, PiManagedAssessment,
PiManagedProviderConfig, PiManagementStatus, PiRawNativeValidity,
};
use super::native::inspect_pi_native_entry;
use super::native::{inspect_pi_native_catalog, inspect_pi_native_entry};
use super::raw_schema::evaluate_provider_value;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
@@ -598,3 +641,213 @@ fn certify_deferred_header_values_are_validated_after_resolution() {
"a literal header value outside visible ASCII must still be rejected"
);
}
// ---------------------------------------------------------------------------
// 应红(C5):OAuth 凭证绝不能按 x-api-key 代理
// ---------------------------------------------------------------------------
#[test]
fn certify_oauth_credentials_are_never_proxied_as_api_key() {
// 字面量命中:判定期即可知,必须 DirectOnly 并给出结构化理由——
// 而不是宣称可代理再发出错误的认证形态。
let literal = composed_catalog(json!({
"api": "anthropic-messages",
"baseUrl": "https://anthropic.example",
"apiKey": "sk-ant-oat01-example-token",
"models": [{"id": "m"}]
}));
let gateway = assess_composition(&literal);
assert_eq!(
gateway.capability,
PiGatewayCapability::DirectOnly,
"pinned Pi sends an sk-ant-oat credential as an OAuth Bearer with oauth beta \
headers; proxying it as x-api-key would send the wrong auth form"
);
assert!(
!gateway.reasons.is_empty(),
"the downgrade must carry a structured reason"
);
assert!(
!has_gateway_reason(&gateway, PiGatewayReasonCode::MissingCredential),
"the credential is present; MissingCredential would misreport the cause"
);
// deferred 凭证:判定期不可知,允许 Proxyable;但物化解析出命中值时必须
// 失败,绝不发出错误的认证形态。
let deferred = composed_catalog(json!({
"api": "anthropic-messages",
"baseUrl": "https://anthropic.example",
"apiKey": "!load-token",
"models": [{"id": "m"}]
}));
let gateway = assess_composition(&deferred);
assert_eq!(
gateway.capability,
PiGatewayCapability::Proxyable,
"a deferred credential's kind is unknowable at plan time"
);
assert!(
gateway.plans[0]
.materialize(&|_: &str| Some("sk-ant-oat01-resolved".to_string()))
.is_err(),
"materialising a resolved OAuth credential must fail rather than send it as \
a plain api key"
);
// 防过度收窄:普通 Anthropic key 不受影响;非 Anthropic 族不适用该判定
// (pinned 的 includes 检查只在 Anthropic 传输层)。
for (api, key) in [
("anthropic-messages", "sk-ant-api03-plain"),
("openai-responses", "sk-ant-oat01-not-anthropic"),
] {
let plain = composed_catalog(json!({
"api": api,
"baseUrl": "https://plain.example/v1",
"apiKey": key,
"models": [{"id": "m"}]
}));
assert_eq!(
assess_composition(&plain).capability,
PiGatewayCapability::Proxyable,
"{api}: the OAuth rule must not over-reach"
);
}
}
// ---------------------------------------------------------------------------
// 应红(C4 扩展):字面量凭证必须在判定期校验
// ---------------------------------------------------------------------------
#[test]
fn certify_literal_credentials_are_validated_at_plan_time() {
// 判定期宣称"可代理"、而每次物化必然失败,是判定层与执行层自相矛盾。
let illegal = composed_catalog(json!({
"api": "openai-responses",
"baseUrl": "https://openai.example/v1",
"apiKey": "café",
"models": [{"id": "m"}]
}));
let gateway = assess_composition(&illegal);
assert!(
gateway.plans.is_empty() || gateway.plans[0].materialize(&|_: &str| None).is_err(),
"sanity: this literal credential can never materialise"
);
assert_eq!(
gateway.capability,
PiGatewayCapability::DirectOnly,
"a literal credential that can never materialise must not be judged proxyable"
);
assert!(
!gateway.reasons.is_empty(),
"the downgrade must carry a structured reason"
);
// 对称约束:deferred 凭证仍不得因原始表达式在判定期被拒(C4)。
let deferred = composed_catalog(json!({
"api": "openai-responses",
"baseUrl": "https://openai.example/v1",
"apiKey": "!echo café",
"models": [{"id": "m"}]
}));
assert_eq!(
assess_composition(&deferred).capability,
PiGatewayCapability::Proxyable,
"a deferred credential must not be validated as a header value before it is \
resolved"
);
}
// ---------------------------------------------------------------------------
// 应红(C1 扩展):空容器与缺席必须可区分
// ---------------------------------------------------------------------------
#[test]
fn certify_empty_containers_stay_distinct_from_absent() {
let base_rates = json!({
"input": 1.0, "output": 2.0, "cacheRead": 0.5, "cacheWrite": 0.25
});
let mut with_empty = base_rates.as_object().expect("rates").clone();
with_empty.insert("tiers".into(), json!([]));
let catalog = json!({
"providers": {
"tiers": {
"api": "anthropic-messages",
"baseUrl": "https://tiers.example",
"apiKey": "literal",
"models": [
{"id": "empty-tiers", "cost": Value::Object(with_empty)},
{"id": "absent-tiers", "cost": base_rates.clone()}
]
}
}
});
let (_temp, path) = write_catalog(&catalog);
let managed = inspect_pi_native_entry(&path, "tiers", &BTreeMap::new())
.expect("inspect")
.expect("entry present")
.managed_config
.expect("managed config");
let round_trip = serde_json::to_value(&managed).expect("serialize managed config");
assert_eq!(
round_trip.pointer("/models/0/cost/tiers"),
Some(&json!([])),
"an explicitly empty tiers list must survive as an empty list"
);
assert_eq!(
round_trip.pointer("/models/1/cost/tiers"),
None,
"an absent tiers list must stay absent"
);
}
// ---------------------------------------------------------------------------
// 应红(C6):单个 entry 的错误不得连坐兄弟 entry
// ---------------------------------------------------------------------------
#[test]
fn certify_one_bad_entry_does_not_hide_its_siblings() {
// pinned Pi 逐 entry 判定:`contextWindow: 1e400` 只令该 entry 非法。
// 整文件解析失败会让合法条目一并消失,破坏"每个 entry 独立"的判定设计。
let source = r#"{
"providers": {
"healthy": {
"api": "anthropic-messages",
"baseUrl": "https://healthy.example",
"apiKey": "literal",
"models": [{"id": "m"}]
},
"overflow": {
"api": "anthropic-messages",
"baseUrl": "https://overflow.example",
"apiKey": "literal",
"models": [{"id": "m", "contextWindow": 1e400}]
}
}
}"#;
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("models.json");
fs::write(&path, source).expect("write");
let diagnostics = inspect_pi_native_catalog(&path, &BTreeMap::new())
.expect("one malformed entry must not fail the whole catalog");
assert_eq!(diagnostics.len(), 2, "both entries must still be reported");
let healthy = diagnostics
.iter()
.find(|diagnostic| diagnostic.provider_key == "healthy")
.expect("healthy entry present");
assert_eq!(
healthy.raw_validity,
PiRawNativeValidity::Valid,
"a legal sibling must not be hidden by a malformed entry"
);
assert_eq!(healthy.management_status, PiManagementStatus::Importable);
let overflow = diagnostics
.iter()
.find(|diagnostic| diagnostic.provider_key == "overflow")
.expect("overflow entry present");
assert_ne!(
overflow.raw_validity,
PiRawNativeValidity::Valid,
"the out-of-range contextWindow entry itself must not be judged valid"
);
}
+7 -1
View File
@@ -662,7 +662,13 @@ fn evaluate_schema(schema: &Value, instance: &Value, pointer: &str) -> SchemaOut
"object" => instance.is_object(),
"array" => instance.is_array(),
"string" => instance.is_string(),
"number" => instance.is_number(),
// TypeBox Number follows JavaScript Number semantics and rejects
// NaN/Infinity. With serde_json arbitrary precision, an overflow
// token remains a Number but has no finite f64 representation.
"number" => instance
.as_number()
.and_then(serde_json::Number::as_f64)
.is_some(),
"boolean" => instance.is_boolean(),
"null" => instance.is_null(),
_ => return unsupported(pointer),