fix(egress): a backend is defined in two maps, and the canary only had one
First canary run failed: phase failed, nothing delivered, and the streamed log said exactly why — "Failed to authenticate. API Error: 403 api.anthropic.com is not on the egress allow-list". Not a 2.1.226 regression. `canary-claude` was added to the server's credential map and not to the node's `provider_hosts`, so the VM booted with a valid subscription token and a door that only opened onto the forge. The fail-closed branch was working correctly: a backend nobody taught that function about reaches no model API, deliberately, so it cannot silently borrow another provider's door. Both maps now name it, each pointing at the other, with a test asserting the canary reaches the same provider as `claude` AND that unknown backends still resolve to nothing. Worth noting what made this a five-second diagnosis instead of an afternoon: the live log streaming built earlier today. The failure was a 403 inside a microVM that no longer exists, and its reason was sitting in the run's checkpoint.
This commit is contained in:
@@ -62,7 +62,15 @@ const COMMON_ALLOW: &[&str] = &["git.redclaw.dev"];
|
|||||||
/// proxy's log of which host it dialled does.
|
/// proxy's log of which host it dialled does.
|
||||||
fn provider_hosts(backend: Option<&str>) -> &'static [&'static str] {
|
fn provider_hosts(backend: Option<&str>) -> &'static [&'static str] {
|
||||||
match backend {
|
match backend {
|
||||||
None | Some("") | Some("default") | Some("claude") => {
|
// `canary-claude` is the same provider, from a candidate CLI image —
|
||||||
|
// see `mission_runtime::microvm_credential_for`, which must grant it the
|
||||||
|
// same credential. A backend is defined in TWO maps: the credential one
|
||||||
|
// on the server and this one on the node. Adding it to only the first is
|
||||||
|
// exactly what happened here: the mission launched, the VM booted, the
|
||||||
|
// agent ran, and the turn died on
|
||||||
|
// "403 api.anthropic.com is not on the egress allow-list" — which is the
|
||||||
|
// fail-closed branch below working correctly.
|
||||||
|
None | Some("") | Some("default") | Some("claude") | Some("canary-claude") => {
|
||||||
&["api.anthropic.com", ".anthropic.com"]
|
&["api.anthropic.com", ".anthropic.com"]
|
||||||
}
|
}
|
||||||
Some("glm") => &["api.z.ai"],
|
Some("glm") => &["api.z.ai"],
|
||||||
@@ -348,6 +356,26 @@ pub fn start(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
/// A backend is defined in TWO places — the server's credential map and this
|
||||||
|
/// egress map — and granting it one without the other produces a mission
|
||||||
|
/// that launches, boots, runs, and dies on a 403 from our own proxy.
|
||||||
|
///
|
||||||
|
/// Measured exactly that way: `canary-claude` was credentialed on the server
|
||||||
|
/// and unknown here, and the turn failed with
|
||||||
|
/// "api.anthropic.com is not on the egress allow-list".
|
||||||
|
#[test]
|
||||||
|
fn the_canary_backend_reaches_the_same_provider_as_claude() {
|
||||||
|
assert_eq!(
|
||||||
|
provider_hosts(Some("canary-claude")),
|
||||||
|
provider_hosts(Some("claude")),
|
||||||
|
"a canary of the Claude image must reach Anthropic, or it tests nothing"
|
||||||
|
);
|
||||||
|
// And the fail-closed branch must still hold for anything unknown: this
|
||||||
|
// is what stops a new backend silently borrowing another provider's door.
|
||||||
|
assert!(provider_hosts(Some("canary-something-else")).is_empty());
|
||||||
|
assert!(provider_hosts(Some("definitely-not-built")).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn allow() -> Vec<String> {
|
fn allow() -> Vec<String> {
|
||||||
|
|||||||
@@ -1009,6 +1009,15 @@ case "${1:-all}" in
|
|||||||
noop)
|
noop)
|
||||||
run_scenario noop "$NOOP_BODY" assert_noop
|
run_scenario noop "$NOOP_BODY" assert_noop
|
||||||
;;
|
;;
|
||||||
|
canary)
|
||||||
|
# The microvm scenario, run against the CANDIDATE CLI image instead of the
|
||||||
|
# one every mission uses. Same assertions — kernel, delegation, gate, judge,
|
||||||
|
# single writer — because the question is whether the new version still
|
||||||
|
# satisfies what the current one does.
|
||||||
|
run_scenario canary \
|
||||||
|
"$(echo "$MICROVM_BODY" | sed 's/"backend":"claude"/"backend":"canary-claude"/' | tr -d '\n')" \
|
||||||
|
assert_microvm
|
||||||
|
;;
|
||||||
microvm)
|
microvm)
|
||||||
run_scenario microvm "$(echo "$MICROVM_BODY" | tr -d '\n')" assert_microvm
|
run_scenario microvm "$(echo "$MICROVM_BODY" | tr -d '\n')" assert_microvm
|
||||||
scenario_microvm_unavailable_backend
|
scenario_microvm_unavailable_backend
|
||||||
@@ -1051,7 +1060,7 @@ case "${1:-all}" in
|
|||||||
scenario_roster
|
scenario_roster
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
die "unknown scenario: $1 (selftest|uids|chain|multirole|noop|microvm|gatecap|research-only|benchmark|security|refactor|composed|roster|all)"
|
die "unknown scenario: $1 (selftest|uids|chain|multirole|noop|microvm|canary|gatecap|research-only|benchmark|security|refactor|composed|roster|all)"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user