Skip to content

Commit 3ebe0bd

Browse files
committed
fix(providers): harden discovery contract cleanup
1 parent 0666b41 commit 3ebe0bd

4 files changed

Lines changed: 44 additions & 42 deletions

File tree

src/providers/model-discovery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export function providerModelDiscoverySpecError(spec: ProviderModelDiscoverySpec
9090
if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//") || path.includes("?") || path.includes("#")) {
9191
return "discovery path must be a query-free relative/origin path";
9292
}
93+
if (path.split("/").includes("..")) return "discovery path must not contain parent-directory segments";
9394
}
9495
const queryEntries = Object.entries(spec.query ?? {});
9596
if (queryEntries.length > 32) return "discovery query may contain at most 32 entries";
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { clearModelCache } from "../../src/codex/model-cache";
2+
import { PROVIDER_REGISTRY, type ProviderModelDiscoverySpec } from "../../src/providers/registry";
3+
4+
interface RegistryDiscoveryOverrides {
5+
preserveCustomDestination?: boolean;
6+
}
7+
8+
/** Temporarily override registry-owned discovery policy and always clear its cached catalog. */
9+
export async function withRegistryDiscovery<T>(
10+
providerId: string,
11+
spec: ProviderModelDiscoverySpec,
12+
run: () => Promise<T> | T,
13+
overrides: RegistryDiscoveryOverrides = {},
14+
): Promise<T> {
15+
const entry = PROVIDER_REGISTRY.find(row => row.id === providerId);
16+
if (!entry) throw new Error(`missing ${providerId} registry entry`);
17+
const originalDiscovery = entry.modelDiscovery;
18+
const originalPreserveCustomDestination = entry.preserveCustomDestination;
19+
entry.modelDiscovery = spec;
20+
if (overrides.preserveCustomDestination !== undefined) {
21+
entry.preserveCustomDestination = overrides.preserveCustomDestination;
22+
}
23+
try {
24+
return await run();
25+
} finally {
26+
if (originalDiscovery === undefined) delete entry.modelDiscovery;
27+
else entry.modelDiscovery = originalDiscovery;
28+
if (originalPreserveCustomDestination === undefined) delete entry.preserveCustomDestination;
29+
else entry.preserveCustomDestination = originalPreserveCustomDestination;
30+
clearModelCache(providerId);
31+
}
32+
}

tests/provider-connection-test.test.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { join } from "node:path";
44
import { tmpdir } from "node:os";
55
import { handleManagementAPI } from "../src/server/management-api";
66
import { saveConfig } from "../src/config";
7-
import { PROVIDER_REGISTRY } from "../src/providers/registry";
87
import type { OcxConfig } from "../src/types";
8+
import { withRegistryDiscovery } from "./helpers/provider-registry-discovery";
99

1010
const TEST_DIR = join(tmpdir(), "ocx-conn-test");
1111
const previousHome = process.env.OPENCODEX_HOME;
@@ -178,13 +178,9 @@ describe("POST /api/providers/test (WP040 connectivity probe)", () => {
178178
});
179179

180180
test("reports only eligible deduplicated models from a registry discovery contract", async () => {
181-
const entry = PROVIDER_REGISTRY.find(row => row.id === "together");
182-
if (!entry) throw new Error("missing together registry entry");
183-
const original = entry.modelDiscovery;
184-
entry.modelDiscovery = {
181+
await withRegistryDiscovery("together", {
185182
filter: { anyOf: [{ path: ["type"], equalsAny: ["chat"] }] },
186-
};
187-
try {
183+
}, async () => {
188184
globalThis.fetch = (async () => Response.json({
189185
data: [
190186
{ id: "chat-model", type: "chat" },
@@ -202,10 +198,7 @@ describe("POST /api/providers/test (WP040 connectivity probe)", () => {
202198
const { body } = await probe(config, "together");
203199
expect(body.ok).toBe(true);
204200
expect(body.models).toBe(1);
205-
} finally {
206-
if (original === undefined) delete entry.modelDiscovery;
207-
else entry.modelDiscovery = original;
208-
}
201+
});
209202
});
210203

211204
test("Google's models-array response shape is accepted (x-goog-api-key path)", async () => {

tests/provider-model-discovery-contract.test.ts

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { PROVIDER_REGISTRY, type ProviderModelDiscoverySpec } from "../src/provi
1717
import { routeModel } from "../src/router";
1818
import type { OcxConfig, OcxProviderConfig } from "../src/types";
1919
import { withStubbedProviderFetch } from "./helpers/catalog-provider-fetch";
20+
import { withRegistryDiscovery } from "./helpers/provider-registry-discovery";
2021

2122
const FIXTURE = readFileSync(join(import.meta.dir, "fixtures/provider-model-discovery.json"), "utf8");
2223
const originalFetch = globalThis.fetch;
@@ -36,37 +37,7 @@ async function withTogetherDiscovery<T>(
3637
spec: ProviderModelDiscoverySpec,
3738
run: () => Promise<T> | T,
3839
): Promise<T> {
39-
const entry = togetherEntry();
40-
const original = entry.modelDiscovery;
41-
const originalPreserveCustomDestination = entry.preserveCustomDestination;
42-
entry.modelDiscovery = spec;
43-
entry.preserveCustomDestination = true;
44-
try {
45-
return await run();
46-
} finally {
47-
if (original === undefined) delete entry.modelDiscovery;
48-
else entry.modelDiscovery = original;
49-
if (originalPreserveCustomDestination === undefined) delete entry.preserveCustomDestination;
50-
else entry.preserveCustomDestination = originalPreserveCustomDestination;
51-
clearModelCache("together");
52-
}
53-
}
54-
55-
async function withRegistryDiscovery<T>(
56-
providerId: string,
57-
spec: ProviderModelDiscoverySpec,
58-
run: () => Promise<T> | T,
59-
): Promise<T> {
60-
const entry = PROVIDER_REGISTRY.find(row => row.id === providerId);
61-
if (!entry) throw new Error(`missing ${providerId} registry entry`);
62-
const original = entry.modelDiscovery;
63-
entry.modelDiscovery = spec;
64-
try {
65-
return await run();
66-
} finally {
67-
if (original === undefined) delete entry.modelDiscovery;
68-
else entry.modelDiscovery = original;
69-
}
40+
return withRegistryDiscovery("together", spec, run, { preserveCustomDestination: true });
7041
}
7142

7243
function togetherConfig(overrides: Partial<OcxProviderConfig> = {}): OcxConfig {
@@ -96,6 +67,11 @@ describe("registry-owned provider model discovery", () => {
9667
.toContain("https");
9768
expect(providerModelDiscoverySpecError({ path: "models?unbounded=true" }))
9869
.toContain("query-free");
70+
expect(providerModelDiscoverySpecError({ path: "../../internal/models" }))
71+
.toContain("parent-directory");
72+
expect(providerModelDiscoverySpecError({ path: "models/../internal" }))
73+
.toContain("parent-directory");
74+
expect(providerModelDiscoverySpecError({ path: "models/model..variant" })).toBeNull();
9975
expect(providerModelDiscoverySpecError({
10076
url: "https://api.example.test/models",
10177
path: "models",

0 commit comments

Comments
 (0)