use std::collections::HashMap;
use cpex_core::extensions::container::{chain_extends, OwnedExtensions};
use cpex_core::extensions::delegation::DelegationExtension;
use cpex_core::extensions::guarded::Guarded;
use cpex_core::extensions::http::HttpExtension;
use cpex_core::extensions::security::SecurityExtension;
use cpex_core::extensions::Extensions;
use serde_json::Value;
use tracing::warn;
use crate::error::HostError;
pub const EXTENSIONS_FIELD: &str = "extensions";
pub const MODIFIED_EXTENSIONS_FIELD: &str = "modified_extensions";
const SENSITIVE_HEADERS: &[&str] = &["authorization", "cookie", "set-cookie", "x-api-key"];
fn is_sensitive(name: &str) -> bool {
SENSITIVE_HEADERS
.iter()
.any(|s| name.eq_ignore_ascii_case(s))
}
fn strip_sensitive(headers: &HashMap<String, String>) -> HashMap<String, String> {
headers
.iter()
.filter(|(name, _)| !is_sensitive(name))
.map(|(name, value)| (name.clone(), value.clone()))
.collect()
}
fn sanitize_http(http: &HttpExtension) -> HttpExtension {
HttpExtension {
request_headers: strip_sensitive(&http.request_headers),
response_headers: strip_sensitive(&http.response_headers),
method: http.method.clone(),
path: http.path.clone(),
host: http.host.clone(),
scheme: http.scheme.clone(),
}
}
fn returned_http(returned: &HttpExtension, inbound: Option<&HttpExtension>) -> HttpExtension {
HttpExtension {
request_headers: strip_sensitive(&returned.request_headers),
response_headers: strip_sensitive(&returned.response_headers),
method: inbound.and_then(|h| h.method.clone()),
path: inbound.and_then(|h| h.path.clone()),
host: inbound.and_then(|h| h.host.clone()),
scheme: inbound.and_then(|h| h.scheme.clone()),
}
}
pub fn attach_extensions(task: &mut Value, extensions: &Extensions) -> Result<(), HostError> {
let wire = to_wire(extensions)?;
if wire.as_object().is_none_or(serde_json::Map::is_empty) {
return Ok(());
}
let object = task.as_object_mut().ok_or_else(|| HostError::Protocol {
message: "task must be a JSON object to carry extensions".into(),
})?;
object.insert(EXTENSIONS_FIELD.to_string(), wire);
Ok(())
}
fn to_wire(extensions: &Extensions) -> Result<Value, HostError> {
let mut map = serde_json::Map::new();
let mut put = |key: &str, value: Result<Value, serde_json::Error>| -> Result<(), HostError> {
let value = value.map_err(|e| HostError::Protocol {
message: format!("could not serialize the '{key}' extension for the worker: {e}"),
})?;
map.insert(key.to_string(), value);
Ok(())
};
if let Some(request) = &extensions.request {
put("request", serde_json::to_value(request))?;
}
if let Some(agent) = &extensions.agent {
put("agent", serde_json::to_value(agent))?;
}
if let Some(http) = &extensions.http {
put("http", serde_json::to_value(sanitize_http(http)))?;
}
if let Some(security) = &extensions.security {
put("security", serde_json::to_value(security))?;
}
if let Some(delegation) = &extensions.delegation {
put("delegation", serde_json::to_value(delegation))?;
}
if let Some(mcp) = &extensions.mcp {
put("mcp", serde_json::to_value(mcp))?;
}
if let Some(completion) = &extensions.completion {
put("completion", serde_json::to_value(completion))?;
}
if let Some(provenance) = &extensions.provenance {
put("provenance", serde_json::to_value(provenance))?;
}
if let Some(llm) = &extensions.llm {
put("llm", serde_json::to_value(llm))?;
}
if let Some(framework) = &extensions.framework {
put("framework", serde_json::to_value(framework))?;
}
if let Some(meta) = &extensions.meta {
put("meta", serde_json::to_value(meta))?;
}
if let Some(custom) = &extensions.custom {
put("custom", serde_json::to_value(custom))?;
}
Ok(Value::Object(map))
}
pub fn parse_returned_extensions(
response: &Value,
inbound: &Extensions,
) -> Result<Option<OwnedExtensions>, HostError> {
let Some(field) = response.get(MODIFIED_EXTENSIONS_FIELD) else {
return Ok(None);
};
owned_from_returned_slot(field, inbound)
}
pub fn owned_from_returned_slot(
field: &Value,
inbound: &Extensions,
) -> Result<Option<OwnedExtensions>, HostError> {
if field.is_null() {
return Ok(None);
}
let object = field.as_object().ok_or_else(|| HostError::Protocol {
message: format!(
"the worker returned a '{MODIFIED_EXTENSIONS_FIELD}' field that is not a JSON object"
),
})?;
let mut owned = inbound.cow_copy();
let mut applied = false;
fn slot<T: serde::de::DeserializeOwned>(
object: &serde_json::Map<String, Value>,
key: &str,
) -> Result<Option<T>, HostError> {
match object.get(key) {
None | Some(Value::Null) => Ok(None),
Some(value) => serde_json::from_value(value.clone())
.map(Some)
.map_err(|e| HostError::Protocol {
message: format!(
"could not deserialize the '{key}' extension returned by the worker: {e}"
),
}),
}
}
if let Some(http) = slot::<HttpExtension>(object, "http")? {
if inbound.http_write_token.is_some() {
owned.http = Some(Guarded::new(returned_http(&http, inbound.http.as_deref())));
applied = true;
}
}
if let Some(security) = slot::<SecurityExtension>(object, "security")? {
if inbound.labels_write_token.is_some() {
let mut merged = inbound
.security
.as_deref()
.cloned()
.unwrap_or_else(SecurityExtension::default);
for label in security.labels.iter() {
merged.add_label(label.clone());
}
owned.security = Some(merged);
applied = true;
}
}
if let Some(delegation) = slot::<DelegationExtension>(object, "delegation")? {
if inbound.delegation_write_token.is_some() {
let inbound_chain = inbound
.delegation
.as_deref()
.map(|d| d.chain.as_slice())
.unwrap_or_default();
if chain_extends(inbound_chain, &delegation.chain) {
owned.delegation = Some(delegation);
applied = true;
} else {
warn!(
inbound_hops = inbound_chain.len(),
returned_hops = delegation.chain.len(),
"dropping a returned delegation chain that does not extend the \
one the plugin was given — an append-only chain cannot be \
shortened or rewritten"
);
}
}
}
if let Some(custom) = slot::<HashMap<String, Value>>(object, "custom")? {
owned.custom = Some(custom);
applied = true;
}
if !applied {
return Ok(None);
}
Ok(Some(owned))
}
#[cfg(test)]
fn same_arc<T>(a: &Option<std::sync::Arc<T>>, b: &Option<std::sync::Arc<T>>) -> bool {
match (a, b) {
(Some(a), Some(b)) => std::sync::Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::sync::Arc;
use cpex_core::extensions::agent::AgentExtension;
use cpex_core::extensions::monotonic::MonotonicSet;
use super::*;
fn http_with_headers() -> HttpExtension {
let mut http = HttpExtension::default();
http.set_request_header("Authorization", "Bearer secret-token");
http.set_request_header("Cookie", "session=abc");
http.set_request_header("X-API-Key", "key-123");
http.set_request_header("X-Request-Id", "req-1");
http.set_response_header("Set-Cookie", "s=1");
http.set_response_header("Authorization", "Bearer echoed");
http.set_response_header("Content-Type", "application/json");
http
}
fn security_with_labels(labels: &[&str]) -> SecurityExtension {
let set: HashSet<String> = labels.iter().map(|s| s.to_string()).collect();
SecurityExtension {
labels: MonotonicSet::from_set(set),
..Default::default()
}
}
fn populated() -> Extensions {
Extensions {
agent: Some(Arc::new(AgentExtension::default())),
http: Some(Arc::new(http_with_headers())),
security: Some(Arc::new(security_with_labels(&["PII"]))),
..Default::default()
}
}
#[test]
fn present_slots_land_on_the_wire() {
let mut task = serde_json::json!({"task_type": "run"});
attach_extensions(&mut task, &populated()).expect("serializing a populated view");
let wire = task
.get(EXTENSIONS_FIELD)
.expect("the field is attached")
.as_object()
.expect("the field is an object");
assert!(wire.contains_key("agent"), "agent slot present");
assert!(wire.contains_key("http"), "http slot present");
assert!(wire.contains_key("security"), "security slot present");
}
fn assert_absent(headers: &serde_json::Map<String, Value>, names: &[&str]) {
for name in names {
assert!(
!headers.keys().any(|k| k.eq_ignore_ascii_case(name)),
"header '{name}' must not cross the process boundary; got {:?}",
headers.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn sensitive_request_headers_are_stripped_but_others_survive() {
let mut task = serde_json::json!({});
attach_extensions(&mut task, &populated()).expect("serializing");
let headers = task[EXTENSIONS_FIELD]["http"]["request_headers"]
.as_object()
.expect("request_headers is an object")
.clone();
assert_absent(&headers, &["Authorization", "Cookie", "X-API-Key"]);
assert_eq!(
headers.get("X-Request-Id").and_then(Value::as_str),
Some("req-1"),
"a non-sensitive header is preserved verbatim"
);
}
#[test]
fn sensitive_response_headers_are_stripped_too() {
let mut task = serde_json::json!({});
attach_extensions(&mut task, &populated()).expect("serializing");
let headers = task[EXTENSIONS_FIELD]["http"]["response_headers"]
.as_object()
.expect("response_headers is an object")
.clone();
assert_absent(&headers, &["Set-Cookie", "Authorization"]);
assert_eq!(
headers.get("Content-Type").and_then(Value::as_str),
Some("application/json"),
);
}
#[test]
fn set_cookie_is_stripped_in_both_directions_and_under_any_casing() {
let mut http = HttpExtension::default();
http.set_response_header("set-cookie", "session=lower");
http.set_response_header("SET-COOKIE", "session=upper");
http.set_request_header("Set-Cookie", "session=req");
let sanitized = sanitize_http(&http);
assert!(
sanitized.response_headers.is_empty(),
"no casing of set-cookie survives outbound: {:?}",
sanitized.response_headers
);
assert!(sanitized.request_headers.is_empty());
let returned = returned_http(&http, None);
assert!(
returned.response_headers.is_empty() && returned.request_headers.is_empty(),
"nor inbound on the return path"
);
}
#[test]
fn lowercase_sensitive_headers_are_stripped() {
let mut http = HttpExtension::default();
http.set_request_header("authorization", "Bearer sneaky");
http.set_request_header("x-api-key", "k");
let extensions = Extensions {
http: Some(Arc::new(http)),
..Default::default()
};
let mut task = serde_json::json!({});
attach_extensions(&mut task, &extensions).expect("serializing");
let headers = task[EXTENSIONS_FIELD]["http"]["request_headers"]
.as_object()
.expect("request_headers is an object");
assert!(
headers.is_empty(),
"lower-cased sensitive headers must be stripped too, got {headers:?}"
);
}
#[test]
fn a_filtered_out_slot_is_absent_from_the_wire() {
let extensions = Extensions {
security: Some(Arc::new(security_with_labels(&["PII"]))),
..Default::default()
};
let mut task = serde_json::json!({});
attach_extensions(&mut task, &extensions).expect("serializing");
let wire = task[EXTENSIONS_FIELD].as_object().expect("an object");
assert!(
!wire.contains_key("agent"),
"a slot the capabilities excluded stays excluded"
);
assert!(wire.contains_key("security"));
}
#[test]
fn raw_credentials_never_appear_on_the_wire() {
use cpex_core::extensions::raw_credentials::{
RawCredentialsExtension, RawInboundToken, TokenKind, TokenRole,
};
let mut inbound_tokens = HashMap::new();
inbound_tokens.insert(
TokenRole::User,
RawInboundToken::new("super-secret", "Authorization", TokenKind::Jwt),
);
let extensions = Extensions {
raw_credentials: Some(Arc::new(RawCredentialsExtension {
inbound_tokens,
..Default::default()
})),
security: Some(Arc::new(security_with_labels(&["PII"]))),
..Default::default()
};
let mut task = serde_json::json!({});
attach_extensions(&mut task, &extensions).expect("serializing");
let wire = task[EXTENSIONS_FIELD].as_object().expect("an object");
assert!(
!wire.contains_key("raw_credentials"),
"the credential slot belongs to the credential DTO, not this channel"
);
let serialized = serde_json::to_string(&task).expect("re-serializing the task");
assert!(
!serialized.contains("super-secret"),
"no token bytes anywhere in the task JSON"
);
}
#[test]
fn empty_extensions_attach_no_field() {
let mut task = serde_json::json!({"task_type": "run"});
attach_extensions(&mut task, &Extensions::default()).expect("serializing an empty view");
assert!(
task.get(EXTENSIONS_FIELD).is_none(),
"a view with no visible slots leaves the task shape unchanged"
);
}
#[test]
fn an_absent_field_means_no_change() {
let response = serde_json::json!({"payload": {}});
let parsed = parse_returned_extensions(&response, &populated()).expect("parsing");
assert!(
parsed.is_none(),
"an omitted field is the documented no-change signal"
);
}
#[test]
fn an_explicit_null_means_no_change() {
let response = serde_json::json!({"modified_extensions": null});
let parsed = parse_returned_extensions(&response, &populated()).expect("parsing");
assert!(parsed.is_none(), "null reads the same as omitted");
}
#[test]
fn a_non_object_field_is_a_protocol_error() {
let response = serde_json::json!({"modified_extensions": "nope"});
let err = parse_returned_extensions(&response, &populated());
assert!(err.is_err(), "a non-object field cannot be interpreted");
}
#[test]
fn immutable_slots_keep_their_inbound_arcs() {
let inbound = populated();
let response = serde_json::json!({
"modified_extensions": {"custom": {"k": "v"}}
});
let owned = parse_returned_extensions(&response, &inbound)
.expect("parsing")
.expect("a returned field yields modifications");
assert!(
same_arc(&inbound.agent, &owned.agent),
"the agent slot must be the very same Arc the executor issued"
);
assert!(inbound.validate_immutable(&owned), "the merge accepts it");
}
#[test]
fn a_forged_immutable_slot_is_dropped_not_merged() {
let inbound = populated();
let response = serde_json::json!({
"modified_extensions": {
"agent": {"agent_id": "forged"},
"custom": {"ok": true}
}
});
let owned = parse_returned_extensions(&response, &inbound)
.expect("parsing")
.expect("modifications present");
assert!(
same_arc(&inbound.agent, &owned.agent),
"the forged agent edit is dropped, not merged"
);
assert!(
inbound.validate_immutable(&owned),
"so the rest of the return still merges cleanly"
);
assert!(
owned.custom.is_some(),
"the legitimate custom edit survives"
);
}
#[test]
fn a_custom_change_is_taken_as_is() {
let response = serde_json::json!({
"modified_extensions": {"custom": {"verdict": "clean", "score": 3}}
});
let owned = parse_returned_extensions(&response, &populated())
.expect("parsing")
.expect("modifications present");
let custom = owned.custom.expect("custom present");
assert_eq!(custom.get("verdict").and_then(Value::as_str), Some("clean"));
assert_eq!(custom.get("score").and_then(Value::as_i64), Some(3));
}
#[test]
fn a_label_append_without_the_token_is_dropped() {
let inbound = populated(); assert!(
inbound.labels_write_token.is_none(),
"no append_labels capability was granted"
);
let response = serde_json::json!({
"modified_extensions": {"security": {"labels": ["PII", "SCANNED"]}}
});
let parsed = parse_returned_extensions(&response, &inbound).expect("parsing");
assert!(
parsed.is_none(),
"an unauthorized label append yields no modification, so the \
pipeline's labels are untouched"
);
}
#[test]
fn a_label_removal_without_the_token_cannot_strip_labels() {
let inbound = populated(); let response = serde_json::json!({
"modified_extensions": {"security": {"labels": []}}
});
let parsed = parse_returned_extensions(&response, &inbound).expect("parsing");
assert!(
parsed.is_none(),
"the removal never reaches the merge, so PII cannot be stripped"
);
}
#[test]
fn an_http_change_needs_the_write_token() {
let inbound = populated();
assert!(inbound.http_write_token.is_none());
let response = serde_json::json!({
"modified_extensions": {"http": {"request_headers": {"X-Added": "1"}}}
});
let parsed = parse_returned_extensions(&response, &inbound).expect("parsing");
assert!(
parsed.is_none(),
"an http edit without the capability is dropped entirely"
);
}
#[test]
fn a_delegation_change_without_the_token_is_dropped() {
let inbound = populated();
assert!(inbound.delegation_write_token.is_none());
let response = serde_json::json!({
"modified_extensions": {"delegation": {"hops": []}}
});
let parsed = parse_returned_extensions(&response, &inbound).expect("parsing");
assert!(
parsed.is_none(),
"the wire cannot add a delegation slot without the capability"
);
}
#[test]
fn an_unauthorized_gated_write_does_not_suppress_a_legitimate_custom_write() {
let inbound = populated();
let response = serde_json::json!({
"modified_extensions": {
"security": {"labels": ["PII", "SCANNED"]},
"custom": {"verdict": "clean"}
}
});
let owned = parse_returned_extensions(&response, &inbound)
.expect("parsing")
.expect("the custom write is a real modification");
assert_eq!(
owned.custom.as_ref().expect("custom present")["verdict"],
"clean"
);
let security = owned.security.as_ref().expect("the inbound slot stands");
assert!(
!security.labels.contains(&"SCANNED".to_string()),
"the unauthorized label append is still dropped"
);
}
#[test]
fn a_custom_write_does_not_carry_a_filtered_security_view_into_the_merge() {
let inbound = Extensions {
security: Some(Arc::new(security_with_labels(&[]))),
..Default::default()
};
assert!(inbound.labels_write_token.is_none());
let response = serde_json::json!({
"modified_extensions": {"custom": {"verdict": "clean"}}
});
let owned = parse_returned_extensions(&response, &inbound)
.expect("parsing")
.expect("the custom write is a real modification");
let mut canonical = Extensions {
security: Some(Arc::new(security_with_labels(&["PII", "HIPAA"]))),
..Default::default()
};
canonical.merge_owned(owned);
let merged = canonical.security.as_ref().expect("slot present");
assert!(
merged.labels.contains(&"PII".to_string()),
"a capability-less custom write must not wipe canonical labels"
);
assert!(merged.labels.contains(&"HIPAA".to_string()));
assert_eq!(
canonical.custom.as_ref().expect("custom merged")["verdict"],
"clean",
"and the legitimate custom write still lands"
);
}
#[test]
fn the_returned_http_slot_preserves_the_inbound_request_line() {
let mut inbound_http = HttpExtension::default();
inbound_http.method = Some("POST".into());
inbound_http.path = Some("/api/v1/transfer".into());
inbound_http.host = Some("bank.internal".into());
inbound_http.scheme = Some("https".into());
let returned_absent = HttpExtension {
request_headers: [("X-Scanned".to_string(), "1".to_string())].into(),
..Default::default()
};
let merged = returned_http(&returned_absent, Some(&inbound_http));
assert_eq!(merged.method.as_deref(), Some("POST"));
assert_eq!(merged.path.as_deref(), Some("/api/v1/transfer"));
assert_eq!(merged.host.as_deref(), Some("bank.internal"));
assert_eq!(merged.scheme.as_deref(), Some("https"));
assert_eq!(
merged.request_headers.get("X-Scanned").map(String::as_str),
Some("1")
);
let returned_hostile = HttpExtension {
method: Some("GET".into()),
path: Some("/healthz".into()),
host: Some("evil.example".into()),
scheme: Some("http".into()),
..Default::default()
};
let merged = returned_http(&returned_hostile, Some(&inbound_http));
assert_eq!(
merged.host.as_deref(),
Some("bank.internal"),
"the worker cannot re-point the validated authority"
);
assert_eq!(merged.method.as_deref(), Some("POST"));
assert_eq!(merged.path.as_deref(), Some("/api/v1/transfer"));
assert_eq!(merged.scheme.as_deref(), Some("https"));
}
#[test]
fn a_returned_delegation_chain_is_validated_not_accepted_blind() {
use cpex_core::extensions::delegation::DelegationHop;
let hop = |id: &str, scopes: Vec<&str>| DelegationHop {
subject_id: id.into(),
scopes_granted: scopes.into_iter().map(str::to_string).collect(),
..Default::default()
};
let canonical = vec![
hop("user-1", vec!["read_hr"]),
hop("svc-a", vec!["read_hr"]),
];
assert!(
chain_extends(&canonical, &canonical),
"an unchanged chain is a valid (empty) append"
);
let mut appended = canonical.clone();
appended.push(hop("svc-b", vec!["read_hr"]));
assert!(
chain_extends(&canonical, &appended),
"a real append is valid"
);
assert!(
!chain_extends(&canonical, &canonical[..1]),
"truncation drops the hop that recorded a narrowing — not an append"
);
let mut widened = canonical.clone();
widened[0].scopes_granted = vec!["admin".into()];
assert!(
!chain_extends(&canonical, &widened),
"widening an existing hop's scopes is not an append"
);
let mut reseated = canonical.clone();
reseated[1].subject_id = "attacker".into();
assert!(
!chain_extends(&canonical, &reseated),
"rewriting an existing hop's subject is not an append"
);
}
#[test]
fn malformed_slot_json_is_an_error_not_a_silent_drop() {
let response = serde_json::json!({
"modified_extensions": {"security": "not-an-object"}
});
let err = parse_returned_extensions(&response, &populated());
assert!(
err.is_err(),
"unparseable slot JSON must surface, not read as no-change"
);
}
}