/** * The egress half of the web red line: what an IP LITERAL actually points at. * * Why this exists: the domain allowlist answers "is this host on the list", and * `TM_WEBFETCH_ALLOWED_DOMAINS="*"` (a documented setting) answers it with yes * for every string — including `169.254.169.254`, the cloud instance-metadata * endpoint that hands out temporary credentials to anything that asks. Fetching * that reads a secret into the model context, the run store and the trajectory * in one call, and no user is being asked first. * * Three tiers, and the middle one is why this file changed: * - FORBIDDEN (metadata / link-local / reserved / multicast): no consent path * under any setting. * - LOOPBACK (127.0.0.0/8, ::1, localhost, *.localhost): allowed by default. * It only ever reaches a service the user started on their own machine, and * the browser doing the fetching is the user's own browser — there is no * third party to protect from it. On OpenCode 2.x a plugin cannot raise * the host's dialog, so "needs approval" here would mean "always refused", * which is exactly the defect this tier fixes (a local dev server was * unreachable through every governed tool). * - PRIVATE (RFC1918, ULA, CGNAT, fe80::/10): gated by TM_PRIVATE_SPACE * (default deny on v2, ask on v1) because it reaches OTHER machines — a * real lateral-movement surface, and "*" must not answer for it. * * Borrowed shape, own implementation: ZCode's `webfetch-egress-guard.ts` unwraps * IPv4-mapped IPv6 AND the DNS64/NAT64 well-known prefix before applying its * policy, because a policy that reads the outer address is a policy that can be * put on a diet by changing the notation. Zero dependency here (no ipaddr.js) — * a plugin should not add a runtime dep to check fourteen prefixes. * * Deliberate asymmetry: IPv4 link-local (169.254/16) is FORBIDDEN while IPv6 * fe80::/10 is only PRIVATE. The metadata service that makes 169.254 dangerous * is an IPv4 service; fe80 has no equivalent, and refusing it would break * nothing we care about — so the stricter rule applies where the payload is. */ export type EgressLevel = "public" | "loopback" | "private" | "forbidden"; export interface EgressVerdict { level: EgressLevel; /** which rule answered, in the words the agent will read back */ via?: string; } /** Hostname as the URL parser gives it: lowercased, possibly bracketed (IPv6), * possibly with the root dot. Case and the trailing dot must not change a * security verdict, so they go first. */ export declare function normalizeEgressHost(raw: unknown): string; /** Dotted quad, strictly — the URL layer has already collapsed the decimal / * hex / octal spellings, and anything still irregular is NOT treated as an IP * (a false "public" on garbage costs nothing; a false parse would be a bug). */ export declare function parseIpv4(text: string): number | null; /** Eight 16-bit groups, with `::` compression and an optional trailing IPv4. */ export declare function parseIpv6Groups(text: string): number[] | null; /** One host, one verdict. Public means "the domain allowlist decides"; loopback * means "the user's own machine — allowed by default"; private means "only * TM_PRIVATE_SPACE=allow decides"; forbidden means nothing decides. */ export declare function classifyHost(raw: unknown): EgressVerdict; //# sourceMappingURL=egress.d.ts.map