(
policy: &mut SandboxPolicy,
rule_name: &str,
incoming_rule: &NetworkPolicyRule,
warnings: &mut Vec<PolicyMergeWarning>,
)
| 379 | } |
| 380 | |
| 381 | fn add_rule( |
| 382 | policy: &mut SandboxPolicy, |
| 383 | rule_name: &str, |
| 384 | incoming_rule: &NetworkPolicyRule, |
| 385 | warnings: &mut Vec<PolicyMergeWarning>, |
| 386 | ) -> Result<(), PolicyMergeError> { |
| 387 | if rule_name.trim().is_empty() { |
| 388 | return Err(PolicyMergeError::MissingRuleNameForAddRule); |
| 389 | } |
| 390 | |
| 391 | let mut incoming_rule = incoming_rule.clone(); |
| 392 | normalize_rule(&mut incoming_rule); |
| 393 | if incoming_rule.name.is_empty() { |
| 394 | incoming_rule.name = rule_name.to_string(); |
| 395 | } |
| 396 | |
| 397 | // Endpoint-overlap fallback: when a chunk arrives with a new rule_name |
| 398 | // that doesn't already exist, fold it into a same-host/port rule if one |
| 399 | // is present. This is intentional for user-authored policies (incremental |
| 400 | // refinements live under one rule name). |
| 401 | // |
| 402 | // Provider-injected rules (`_provider_*` — see `compose.rs::provider_rule_name`) |
| 403 | // are deliberately EXCLUDED from this fallback. Provider profiles supply a |
| 404 | // baseline layer that should stay separate from agent/user contributions; |
| 405 | // merging an agent's narrow proposal into a provider's broad rule would |
| 406 | // (a) expand the provider rule's `access` shorthand into wildcard |
| 407 | // `path: "**"` rules at the prover's input, masking the agent's narrow |
| 408 | // scope behind the existing broad coverage, and (b) silently widen the |
| 409 | // provider rule's binary list. The agent's contribution is kept on its |
| 410 | // own rule key, the prover sees the actual narrow proposal, and the |
| 411 | // reviewer gets honest signal about what's being added. |
| 412 | let target_key = if policy.network_policies.contains_key(rule_name) { |
| 413 | Some(rule_name.to_string()) |
| 414 | } else { |
| 415 | let mut keys: Vec<_> = policy.network_policies.keys().cloned().collect(); |
| 416 | keys.sort(); |
| 417 | keys.into_iter() |
| 418 | .filter(|k| !is_provider_rule_name(k)) |
| 419 | .find(|key| { |
| 420 | policy |
| 421 | .network_policies |
| 422 | .get(key) |
| 423 | .is_some_and(|existing_rule| { |
| 424 | rules_share_endpoint(existing_rule, &incoming_rule) |
| 425 | }) |
| 426 | }) |
| 427 | }; |
| 428 | |
| 429 | if let Some(key) = target_key { |
| 430 | let existing_rule = policy |
| 431 | .network_policies |
| 432 | .get_mut(&key) |
| 433 | .expect("existing rule must be present"); |
| 434 | merge_rules(existing_rule, &incoming_rule, warnings)?; |
| 435 | } else { |
| 436 | policy |
| 437 | .network_policies |
| 438 | .insert(rule_name.to_string(), incoming_rule); |
no test coverage detected