iflytek/astron-agent · error · OutboundPolicyError

Outbound address is unsafe

Error message

Outbound address is unsafe

What it means

OutboundPolicyError raised in validate_address when the target IP falls into one of the hardcoded _NEVER_CONNECT_NETWORKS (unspecified, loopback, link-local, multicast, reserved, documentation ranges like 192.0.2.0/24, NAT64 64:ff9b::/96, 6to4 2002::/16, etc.). Unlike the blocklist error, this deny set is built into the library and cannot be configured away. It fires after the literal-whitelist exception check, so even whitelisted literals cannot reach these ranges.

Solutions

  1. Replace loopback/unspecified/documentation literal IPs with the real routable address of the target service.
  2. For genuinely intended private endpoints (RFC1918, site-local), register the exact endpoint in PRIVATE_ENDPOINT_ALLOW_LIST — but note loopback, link-local, multicast and reserved ranges are still rejected unconditionally.
  3. If DNS resolves to a never-connect address (e.g. 169.254.x), fix the DNS/service-discovery record.
  4. If you truly need one of these ranges (e.g. tests), run against a non-never-connect private address instead; the check is unconditional and not configurable.

Example fix

// before
url = "http://127.0.0.1:8080/api"
// after
url = "http://192.168.10.5:8080/api"  # plus PRIVATE_ENDPOINT_ALLOW_LIST=http://192.168.10.5:8080/api
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress

_NEVER = ["0.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "224.0.0.0/4", "240.0.0.0/4",
          "192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24", "2001:db8::/32", "2002::/16"]

def is_never_connect(host: str) -> bool:
    try:
        addr = ipaddress.ip_address(host)
    except ValueError:
        return False
    return any(addr in ipaddress.ip_network(n) for n in _NEVER)

# before the call: if is_never_connect("127.0.0.1"): raise ValueError("use a real endpoint")

Type guard

def is_safe_literal_host(host: str) -> bool:
    try:
        addr = ipaddress.ip_address(host)
    except ValueError:
        return True  # hostname, resolved later
    return not (addr.is_loopback or addr.is_unspecified or addr.is_link_local or addr.is_multicast or addr.is_reserved)

Try / catch

try:
    policy.validate_url(url)
except OutboundPolicyError as exc:
    if "unsafe" in str(exc):
        raise ValueError(f"{url} targets a reserved/loopback network; use a routable endpoint") from exc
    raise

Prevention

When it happens

Trigger: validate_url/create_socket_factory given a literal IP in a never-connect range (e.g. http://127.0.0.1, http://0.0.0.0, http://[::1], http://203.0.113.5), or a hostname whose DNS resolution in socket_factory returns such an address; also via 64:ff9b::/96 or 2002::/16 NAT/6to4 embedded addresses.

Common situations: Developers pointing tools at localhost or a docker-internal IP without adding it to PRIVATE_ENDPOINT_ALLOW_LIST (note: private-endpoint allow-listing does NOT bypass never-connect ranges for loopback — loopback is rejected unconditionally); using documentation/example IPs (203.0.113.x, 2001:db8::) copied from docs; DNS returning link-local (169.254.x) addresses during infrastructure issues.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12). Data as JSON: /api/errors/a132e6b7272b7267. Report an issue: GitHub.

Appendix: source

Thrown at core/plugin/link/infra/tool_exector/ssrf_guard.py:120

            and _endpoint(parsed) in self.allowed_private_endpoints
        )

    def validate_address(
        self,
        address: IpAddress,
        *,
        allow_private_endpoint: bool,
        allow_literal_exception: bool,
    ) -> None:
        """Validate the exact IP address that aiohttp is about to connect to."""
        if _matches_any(address, self.blocked_networks):
            raise OutboundPolicyError("Outbound address is blocked")
        if allow_literal_exception and _matches_any(
            address, self.allowed_literal_networks
        ):
            return
        if _is_never_connect_address(address):
            raise OutboundPolicyError("Outbound address is unsafe")
        if allow_private_endpoint:
            return
        if not _canonical_address(address).is_global:
            raise OutboundPolicyError("Outbound address is not globally routable")

    def is_domain_blocked(self, hostname: str) -> bool:
        """Match configured domains on label boundaries, including subdomains."""
        for rule in self.blocked_domains:
            if hostname == rule or hostname.endswith("." + rule):
                return True
        return False


def ensure_same_origin(base_url: str, candidate_url: str) -> None:
    """Reject path or authentication data that changes scheme, host, or port."""
    if _origin(base_url) != _origin(candidate_url):
        raise OutboundPolicyError("Tool path must not change the endpoint origin")

View on GitHub (pinned to 5e758547a8)