Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,12 @@ export function ensureStrictCatalogFields(
if (typeof entry.supports_reasoning_summaries !== "boolean") entry.supports_reasoning_summaries = false;
if (typeof entry.default_reasoning_summary !== "string") entry.default_reasoning_summary = "none";
if (typeof entry.support_verbosity !== "boolean") entry.support_verbosity = true;
if (typeof entry.default_verbosity !== "string") entry.default_verbosity = "low";
// A row that has declared it does NOT support verbosity must not also ship a default for the
// control it just disowned: Codex seeds its picker from `default_verbosity`, so leaving the
// strict-fields fallback in place re-creates the dead toggle the explicit opt-out removed.
// Scoped to an explicit `false`, so rows that never declare a capability keep the default.
if (entry.support_verbosity === false) delete entry.default_verbosity;
else if (typeof entry.default_verbosity !== "string") entry.default_verbosity = "low";
if (typeof entry.apply_patch_tool_type !== "string") entry.apply_patch_tool_type = "freeform";
if (!entry.truncation_policy || typeof entry.truncation_policy !== "object" || Array.isArray(entry.truncation_policy)) {
entry.truncation_policy = { mode: "tokens", limit: 10000 };
Expand Down
53 changes: 53 additions & 0 deletions src/lib/destination-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@ function classifyIpv4(hostname: string): DestinationAssessment {
return { kind: "public", detail: "public IP" };
}

/**
* Expand an IPv6 literal into its eight hextets, or null when it is not one this can parse.
* `firstIpv6Hextet` below only needs the leading group; prefix matching needs the whole address,
* and `::` compression plus the RFC 4291 trailing dotted-quad form both have to be handled.
*/
function ipv6Hextets(hostname: string): number[] | null {
let text = hostname;
const dotted = text.match(/(\d{1,3}(?:\.\d{1,3}){3})$/);
if (dotted?.index !== undefined) {
const octets = dotted[1].split(".").map(Number);
if (octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) return null;
text = text.slice(0, dotted.index)
+ ((octets[0]! << 8) | octets[1]!).toString(16)
+ ":"
+ ((octets[2]! << 8) | octets[3]!).toString(16);
}
const halves = text.split("::");
if (halves.length > 2) return null;
const parseGroups = (part: string): number[] | null => {
if (!part) return [];
const out: number[] = [];
for (const piece of part.split(":")) {
if (!/^[0-9a-f]{1,4}$/i.test(piece)) return null;
out.push(Number.parseInt(piece, 16));
}
return out;
};
const head = parseGroups(halves[0] ?? "");
const tail = halves.length === 2 ? parseGroups(halves[1] ?? "") : [];
if (!head || !tail) return null;
if (halves.length === 1) return head.length === 8 ? head : null;
const fill = 8 - head.length - tail.length;
if (fill < 1) return null;
return [...head, ...Array<number>(fill).fill(0), ...tail];
}

/** RFC 6052 §2.1 well-known NAT64 prefix, 64:ff9b::/96, as its six leading hextets. */
const NAT64_WELL_KNOWN_PREFIX = [0x64, 0xff9b, 0, 0, 0, 0] as const;

function firstIpv6Hextet(hostname: string): number | null {
const head = hostname.split(":")[0];
if (!head) return 0;
Expand All @@ -89,6 +128,20 @@ function classifyIpv6(hostname: string): DestinationAssessment {
const ipv4 = `${(hi >> 8) & 255}.${hi & 255}.${(lo >> 8) & 255}.${lo & 255}`;
return classifyIpv4(ipv4);
}
// NAT64 (RFC 6052): on an IPv6-only/DNS64 network every IPv4-only peer is synthesized into
// 64:ff9b::<ipv4>, whose leading hextet (0x64) is below the 2000::/3 global-unicast window and
// so fell through to "non-global address". That rejected ordinary public destinations for any
// user behind NAT64 — two tests already worked around it with `allowPrivateNetwork: true`.
// Classify the EMBEDDED IPv4 instead, exactly as the ::ffff: forms above do, so a wrapped
// 127.0.0.1 or 10/8 stays blocked rather than becoming an SSRF bypass. Only the well-known
// prefix is decoded; RFC 8215's 64:ff9b:1::/48 is reserved for local-use translation and keeps
// its non-global treatment.
const hextets = ipv6Hextets(hostname);
if (hextets && NAT64_WELL_KNOWN_PREFIX.every((group, index) => hextets[index] === group)) {
const hi = hextets[6]!;
const lo = hextets[7]!;
return classifyIpv4(`${(hi >> 8) & 255}.${hi & 255}.${(lo >> 8) & 255}.${lo & 255}`);
}
if (hostname === "::1") return { kind: "loopback", detail: "loopback address" };
if (hostname === "::") return { kind: "unspecified", detail: "unspecified address" };
const hextet = firstIpv6Hextet(hostname);
Expand Down
90 changes: 90 additions & 0 deletions tests/catalog-verbosity-default.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { afterEach, describe, expect, test } from "bun:test";
import { buildCatalogEntries, gatherRoutedModels as gatherRoutedModelsDirect, upstreamNativeEntry } from "../src/codex/catalog";
import { withStubbedProviderFetch } from "./helpers/catalog-provider-fetch";
import { resetCatalogRuntimeStateForTests, resetOpenAiApiCatalogWarningStateForTests } from "../src/codex/catalog";
import { clearModelCache } from "../src/codex/model-cache";

const gatherRoutedModels: typeof gatherRoutedModelsDirect = (config, options) =>
gatherRoutedModelsDirect(withStubbedProviderFetch(config), options);

afterEach(() => {
globalThis.fetch = originalFetch;
clearModelCache();
resetOpenAiApiCatalogWarningStateForTests();
resetCatalogRuntimeStateForTests();
});

const originalFetch = globalThis.fetch;

/**
* A serialized row that declares `support_verbosity: false` must not also carry a
* `default_verbosity`. Codex seeds its picker from `default_verbosity`, so leaving the
* strict-fields fallback in place re-creates the dead toggle that the explicit opt-out
* (#2578 architecture) removed. All field names here are the SERIALIZED Codex spellings —
* `supports_verbosity` does not exist in this format, which is exactly how an earlier
* assertion passed while every routed row advertised the control.
*/
describe("catalog — default_verbosity is dropped when verbosity is unsupported", () => {
test("GREEN: an opted-out routed row carries no verbosity default", async () => {
const models = await gatherRoutedModels({
providers: {
xai: {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "oauth",
liveModels: false,
models: ["grok-4.6"],
},
},
});
const entries = buildCatalogEntries(null, [], models);
const xai = entries.find(e => e.slug === "xai/grok-4.6");
expect(xai?.support_verbosity).toBe(false);
expect(xai?.default_verbosity).toBeUndefined();
});

test("GREEN: a Kiro opted-out row carries no verbosity default either", async () => {
const models = await gatherRoutedModels({
providers: {
kiro: {
adapter: "kiro",
baseUrl: "https://runtime.us-east-1.kiro.dev",
authMode: "oauth",
liveModels: false,
models: ["gpt-5.6-sol"],
},
},
});
const entries = buildCatalogEntries(null, [], models);
const kiro = entries.find(e => e.slug === "kiro/gpt-5.6-sol");
expect(kiro?.support_verbosity).toBe(false);
expect(kiro?.default_verbosity).toBeUndefined();
});

test("CONTROL: rows that never declare a capability keep the permissive default", async () => {
const models = await gatherRoutedModels({
providers: {
plain: {
adapter: "openai-responses",
baseUrl: "https://plain.example.test/v1",
authMode: "key",
liveModels: false,
models: ["plain-model"],
},
},
});
const entries = buildCatalogEntries(null, [], models);
const plain = entries.find(e => e.slug === "plain/plain-model");
expect(plain?.support_verbosity).toBe(true);
expect(plain?.default_verbosity).toBe("low");
});

test("CONTROL: a native OpenAI row keeps verbosity and its default", () => {
const template = upstreamNativeEntry("gpt-5.6-sol");
expect(template).not.toBeNull();
const entries = buildCatalogEntries(template, ["gpt-5.6-sol"], []);
const native = entries.find(e => e.slug === "gpt-5.6-sol");
expect(native?.support_verbosity).toBe(true);
expect(native?.default_verbosity).toBe("low");
});
});
43 changes: 43 additions & 0 deletions tests/destination-policy-resolved.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,46 @@ describe("resolvePublicAddresses — caller-specific diagnostics", () => {
)).rejects.toThrow("benchmark address (198.19.7.9)");
});
});

describe("providerDestinationConfigError — NAT64 well-known prefix (RFC 6052)", () => {
// On an IPv6-only/DNS64 network every IPv4-only peer is synthesized into 64:ff9b::<ipv4>.
// 0x64 sits below the 2000::/3 global-unicast window, so the wrapper alone read as
// "non-global address" and rejected ordinary public destinations for anyone behind NAT64.
test("a wrapped public IPv4 is accepted", () => {
for (const host of ["64:ff9b::d25:c62c", "64:ff9b::0fe0:7748", "64:ff9b::13.37.198.44"]) {
expect(providerDestinationConfigError("p", provider(`https://[${host}]/v1`))).toBeNull();
}
});

// The embedded address is what gets classified, so the decode cannot become an SSRF bypass.
test("a wrapped private, loopback, or link-local IPv4 stays blocked", () => {
const cases: [string, string][] = [
["64:ff9b::7f00:1", "loopback"],
["64:ff9b::a00:1", "private-network"],
["64:ff9b::c0a8:1", "private-network"],
["64:ff9b::ac10:1", "private-network"],
// 169.254.169.254 is the cloud metadata IP, so the wrapped form lands on the stronger
// metadata blocklist rather than the generic link-local rule.
["64:ff9b::a9fe:a9fe", "blocked metadata endpoint"],
["64:ff9b::127.0.0.1", "loopback"],
];
for (const [host, detail] of cases) {
expect(providerDestinationConfigError("p", provider(`https://[${host}]/v1`))).toContain(detail);
}
});

// RFC 8215 reserves 64:ff9b:1::/48 for local-use translation, which is not the well-known
// prefix and keeps its non-global treatment.
test("the RFC 8215 local-use prefix is not decoded", () => {
expect(providerDestinationConfigError("p", provider("https://[64:ff9b:1::d25:c62c]/v1")))
.toContain("non-global");
});

test("unrelated IPv6 classification is unchanged", () => {
expect(providerDestinationConfigError("p", provider("https://[2606:4700::6812:1250]/v1"))).toBeNull();
expect(providerDestinationConfigError("p", provider("https://[::1]/v1"))).toContain("loopback");
expect(providerDestinationConfigError("p", provider("https://[fd00::1]/v1"))).toContain("private-network");
expect(providerDestinationConfigError("p", provider("https://[fe80::1]/v1"))).toContain("link-local");
expect(providerDestinationConfigError("p", provider("https://[2001:db8::1]/v1"))).toContain("documentation");
});
});
Loading