@wardby/cli 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coding/local-git.js +9 -1
- package/dist/coding/registry/pypi.d.ts +25 -1
- package/dist/coding/registry/pypi.js +140 -16
- package/dist/coding/registry/types.d.ts +17 -3
- package/dist/config/providers.d.ts +3 -2
- package/dist/core/http-runtime.js +13 -6
- package/dist/help-index.json +207 -15
- package/dist/mcp/auth/ownership.d.ts +1 -1
- package/dist/mcp/tools/agents.js +1 -1
- package/dist/providers/coding-proxy/mock-upstream.d.ts +33 -0
- package/dist/providers/coding-proxy/mock-upstream.js +116 -0
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +3 -0
- package/dist/providers/coding-proxy/registry/prisma-store.js +17 -0
- package/dist/providers/coding-proxy/registry/service.d.ts +4 -0
- package/dist/providers/coding-proxy/registry/service.js +20 -3
- package/dist/providers/coding-proxy/registry/store.d.ts +18 -1
- package/dist/providers/coding-proxy/registry/store.js +33 -0
- package/dist/providers/coding-proxy/runtime.js +12 -1
- package/dist/providers/coding-proxy/types.d.ts +2 -0
- package/dist/providers/jobs/kubernetes-isolation.d.ts +13 -0
- package/dist/providers/jobs/kubernetes-isolation.js +52 -5
- package/dist/providers/jobs/kubernetes.d.ts +20 -6
- package/dist/providers/jobs/kubernetes.js +43 -32
- package/dist/providers/llm/openai.js +0 -1
- package/dist/quickstart/coding-db.js +14 -2
- package/dist/quickstart/coding-doctor.d.ts +3 -0
- package/dist/quickstart/coding-doctor.js +3 -0
- package/dist/quickstart/coding-images.d.ts +9 -0
- package/dist/quickstart/coding-images.js +39 -0
- package/dist/quickstart/coding-seed.d.ts +13 -1
- package/dist/quickstart/coding-seed.js +25 -7
- package/dist/quickstart/coding.d.ts +2 -0
- package/dist/quickstart/coding.js +75 -3
- package/dist/quickstart/config.d.ts +2 -0
- package/dist/quickstart/config.js +4 -0
- package/dist/quickstart/images.d.ts +11 -0
- package/dist/quickstart/images.js +74 -12
- package/dist/quickstart/index.d.ts +0 -2
- package/dist/quickstart/index.js +10 -14
- package/dist/quickstart/manifests.d.ts +56 -0
- package/dist/quickstart/manifests.js +523 -0
- package/dist/quickstart/python-detect.d.ts +10 -0
- package/dist/quickstart/python-detect.js +32 -0
- package/dist/quickstart/repo-packages.d.ts +10 -0
- package/dist/quickstart/repo-packages.js +132 -0
- package/dist/quickstart/reviewer-prompt.d.ts +12 -0
- package/dist/quickstart/reviewer-prompt.js +84 -0
- package/dist/quickstart/scenarios.d.ts +19 -0
- package/dist/quickstart/scenarios.js +28 -0
- package/dist/quickstart/sync-reviewer-prompt.d.ts +1 -0
- package/dist/quickstart/sync-reviewer-prompt.js +10 -0
- package/dist/quickstart-images.json +1 -1
- package/dist/viewer/api-schema.d.ts +4 -4
- package/docs/coding-agent-setup.md +17 -0
- package/docs/coding-packages.md +101 -3
- package/docs/coding-worker-byo-images.md +47 -2
- package/docs/coding-worker-isolation.md +20 -9
- package/docs/getting-started.md +139 -11
- package/docs/knowledge.md +11 -0
- package/help/architecture-agent.md +136 -0
- package/help/build-worker-image.md +198 -0
- package/help/coding-packages.md +31 -2
- package/help/creating-agents.md +14 -0
- package/help/deploy-gke.md +29 -5
- package/help/deployment-targets.md +3 -3
- package/help/getting-started.md +19 -0
- package/help/local-repositories.md +36 -5
- package/package.json +3 -2
package/dist/coding/local-git.js
CHANGED
|
@@ -72,7 +72,15 @@ export async function localGitWrite(dir, args, options = {}) {
|
|
|
72
72
|
}
|
|
73
73
|
return await new Promise((resolve, reject) => {
|
|
74
74
|
const child = execFile("git", gitArgs(dir, args), { env: { ...cleanGitEnv(), ...extra }, maxBuffer: GIT_MAX_BUFFER, encoding: "utf8" }, (error, stdout) => (error ? reject(error) : resolve(stdout)));
|
|
75
|
-
|
|
75
|
+
// git may exit before reading stdin (an early error, or a command that never
|
|
76
|
+
// reads it); writing to the closed pipe then raises EPIPE on stdin. The
|
|
77
|
+
// callback above already reports git's own result, so a stdin error is not
|
|
78
|
+
// the outcome — but left unhandled it would be an uncaught exception.
|
|
79
|
+
child.stdin?.on("error", () => { });
|
|
80
|
+
if (options.input === undefined)
|
|
81
|
+
child.stdin?.end();
|
|
82
|
+
else
|
|
83
|
+
child.stdin?.end(options.input);
|
|
76
84
|
});
|
|
77
85
|
}
|
|
78
86
|
/** True for a plain branch/tag name that cannot read as an option or revision expression. */
|
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
import { type RegistryAdapter } from "./types.js";
|
|
1
|
+
import { type FileDependency, type RegistryAdapter } from "./types.js";
|
|
2
|
+
/** The most extras one allowlist entry or dependency line may name. */
|
|
3
|
+
export declare const MAX_EXTRAS = 32;
|
|
4
|
+
export declare function isPypiProjectName(name: string): boolean;
|
|
2
5
|
export declare function normalizePypiName(name: string): string;
|
|
6
|
+
/** Normalized (PEP 685), sorted, de-duplicated extras from the text inside
|
|
7
|
+
* `[...]`, or null when any is not a valid extra name (PEP 508: the same
|
|
8
|
+
* shape as a project name), the list is empty, or it names too many. */
|
|
9
|
+
export declare function parseExtras(list: string): string[] | null;
|
|
10
|
+
/**
|
|
11
|
+
* The dependencies a wheel's METADATA declares for an install of the package
|
|
12
|
+
* with `extras`: every line without an `extra == "…"` marker clause, plus the
|
|
13
|
+
* lines whose marker names one of `extras` (other marker clauses are ignored,
|
|
14
|
+
* so a requested extra's lines are included on every platform). A line gated
|
|
15
|
+
* on any other extra is skipped. Each dependency carries the extras its line
|
|
16
|
+
* asks of it (`uvicorn[standard]`); a line naming the package itself
|
|
17
|
+
* (`celery[redis]; extra == "all"`) adds those extras to this install
|
|
18
|
+
* instead. Names and extras are validated and normalized.
|
|
19
|
+
*
|
|
20
|
+
* Policy, not pip: a plain install (no `extras`) follows no extras at all,
|
|
21
|
+
* neither its own (a self-referencing line) nor its dependencies' — each
|
|
22
|
+
* dependency comes back bare, so extras are followed only along a chain that
|
|
23
|
+
* starts at an allowlist entry naming extras.
|
|
24
|
+
*/
|
|
25
|
+
export declare function requiredDists(metadataText: string, extras?: readonly string[]): FileDependency[];
|
|
26
|
+
/** Dependency names a METADATA declares for a plain install (no extras). */
|
|
3
27
|
export declare function requiresDist(metadataText: string): string[];
|
|
4
28
|
export declare const pypiAdapter: RegistryAdapter;
|
|
@@ -10,7 +10,10 @@ import { unzipSync, strFromU8 } from "fflate";
|
|
|
10
10
|
import { AllowlistEntryError, RegistryError, } from "./types.js";
|
|
11
11
|
const UPSTREAM = "https://pypi.org/simple/";
|
|
12
12
|
const SIMPLE_JSON = "application/vnd.pypi.simple.v1+json";
|
|
13
|
-
|
|
13
|
+
/** name, an optional `[extras]` group (checked separately), then the specifier. */
|
|
14
|
+
const ENTRY = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[([^\]]*)\])?\s*(.*)$/;
|
|
15
|
+
/** The most extras one allowlist entry or dependency line may name. */
|
|
16
|
+
export const MAX_EXTRAS = 32;
|
|
14
17
|
const WHEEL_METADATA_LIMIT = 64 * 1024 * 1024;
|
|
15
18
|
function uploadTime(file) {
|
|
16
19
|
const time = file["upload-time"] ? Date.parse(file["upload-time"]) : NaN;
|
|
@@ -19,6 +22,9 @@ function uploadTime(file) {
|
|
|
19
22
|
/** A valid PyPI project name (PEP 508): ASCII letters and digits, with
|
|
20
23
|
* `.`, `_` and `-` allowed only between them. */
|
|
21
24
|
const PROJECT_NAME = /^(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$/;
|
|
25
|
+
export function isPypiProjectName(name) {
|
|
26
|
+
return PROJECT_NAME.test(name);
|
|
27
|
+
}
|
|
22
28
|
function decoded(segment) {
|
|
23
29
|
try {
|
|
24
30
|
return decodeURIComponent(segment);
|
|
@@ -43,19 +49,132 @@ function versionOf(filename) {
|
|
|
43
49
|
const sdist = filename.match(/^.+?-(\d[^-]*)\.(?:tar\.gz|zip)$/);
|
|
44
50
|
return sdist ? sdist[1] : null;
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
/** Normalized (PEP 685), sorted, de-duplicated extras from the text inside
|
|
53
|
+
* `[...]`, or null when any is not a valid extra name (PEP 508: the same
|
|
54
|
+
* shape as a project name), the list is empty, or it names too many. */
|
|
55
|
+
export function parseExtras(list) {
|
|
56
|
+
const items = list.split(",").map((item) => item.trim());
|
|
57
|
+
if (items.length > MAX_EXTRAS || items.some((item) => !PROJECT_NAME.test(item)))
|
|
58
|
+
return null;
|
|
59
|
+
return [...new Set(items.map(normalizePypiName))].sort();
|
|
60
|
+
}
|
|
61
|
+
/** The string literals a marker compares `extra` to with `==` or `===`
|
|
62
|
+
* (either operand order), normalized; null when the marker never mentions
|
|
63
|
+
* `extra`. Tokenized, so a quoted value never counts as a clause. Fails
|
|
64
|
+
* closed: when `extra` appears in any shape this does not fully recognise
|
|
65
|
+
* (`extra in "a b"`, `extra.x`, a bare `extra`, an unbalanced quote), the
|
|
66
|
+
* line is gated on no extra at all, so it is never followed. */
|
|
67
|
+
function markerExtras(marker) {
|
|
68
|
+
if (!/\bextra\b/.test(marker))
|
|
69
|
+
return null;
|
|
70
|
+
const tokens = marker.match(/"[^"]*"|'[^']*'|===|==|!=|<=|>=|~=|[A-Za-z_][A-Za-z0-9_.]*|\S/g) ?? [];
|
|
71
|
+
const isLiteral = (token) => token !== undefined && token.length >= 2 && (token[0] === '"' || token[0] === "'");
|
|
72
|
+
const isEquals = (token) => token === "==" || token === "===";
|
|
73
|
+
// A lone quote (unterminated string) means the marker cannot be read.
|
|
74
|
+
if (tokens.some((token) => token === '"' || token === "'"))
|
|
75
|
+
return [];
|
|
76
|
+
const found = [];
|
|
77
|
+
let recognised = 0;
|
|
78
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
79
|
+
if (tokens[i] !== "extra")
|
|
80
|
+
continue;
|
|
81
|
+
let value = null;
|
|
82
|
+
if (isEquals(tokens[i + 1]) && isLiteral(tokens[i + 2]))
|
|
83
|
+
value = tokens[i + 2].slice(1, -1);
|
|
84
|
+
else if (isEquals(tokens[i - 1]) && isLiteral(tokens[i - 2]))
|
|
85
|
+
value = tokens[i - 2].slice(1, -1);
|
|
86
|
+
if (value === null)
|
|
87
|
+
return [];
|
|
88
|
+
recognised++;
|
|
89
|
+
if (PROJECT_NAME.test(value))
|
|
90
|
+
found.push(normalizePypiName(value));
|
|
91
|
+
}
|
|
92
|
+
// `extra` only inside quoted values: not a clause, but not one this reads either.
|
|
93
|
+
return recognised === 0 ? [] : found;
|
|
94
|
+
}
|
|
95
|
+
const REQUIREMENT = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[([^\]]*)\])?/;
|
|
96
|
+
// Splits each Requires-Dist value only at its first `;` (the marker), which is
|
|
97
|
+
// all wheel METADATA needs: no `name @ url` with a `;` in the URL, no comments
|
|
98
|
+
// or line continuations as in a requirements file.
|
|
99
|
+
function requirementLines(metadataText) {
|
|
100
|
+
let self = null;
|
|
101
|
+
const lines = [];
|
|
48
102
|
for (const line of metadataText.split(/\r?\n/)) {
|
|
103
|
+
// The headers end at the first truly empty line; the description
|
|
104
|
+
// follows. A whitespace-only line is a folded header's continuation.
|
|
105
|
+
if (line === "")
|
|
106
|
+
break;
|
|
107
|
+
if (line.startsWith("Name:")) {
|
|
108
|
+
const name = line.slice("Name:".length).trim();
|
|
109
|
+
if (self === null && PROJECT_NAME.test(name))
|
|
110
|
+
self = normalizePypiName(name);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
49
113
|
if (!line.startsWith("Requires-Dist:"))
|
|
50
114
|
continue;
|
|
51
115
|
const value = line.slice("Requires-Dist:".length).trim();
|
|
52
|
-
|
|
116
|
+
const match = value.match(REQUIREMENT);
|
|
117
|
+
if (!match || !PROJECT_NAME.test(match[1]))
|
|
118
|
+
continue;
|
|
119
|
+
const semicolon = value.indexOf(";", match[0].length);
|
|
120
|
+
lines.push({
|
|
121
|
+
name: normalizePypiName(match[1]),
|
|
122
|
+
// A malformed extras list still installs the package, just none of its extras.
|
|
123
|
+
extras: match[2] === undefined ? [] : (parseExtras(match[2]) ?? []),
|
|
124
|
+
gate: semicolon === -1 ? null : markerExtras(value.slice(semicolon + 1)),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { self, lines };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The dependencies a wheel's METADATA declares for an install of the package
|
|
131
|
+
* with `extras`: every line without an `extra == "…"` marker clause, plus the
|
|
132
|
+
* lines whose marker names one of `extras` (other marker clauses are ignored,
|
|
133
|
+
* so a requested extra's lines are included on every platform). A line gated
|
|
134
|
+
* on any other extra is skipped. Each dependency carries the extras its line
|
|
135
|
+
* asks of it (`uvicorn[standard]`); a line naming the package itself
|
|
136
|
+
* (`celery[redis]; extra == "all"`) adds those extras to this install
|
|
137
|
+
* instead. Names and extras are validated and normalized.
|
|
138
|
+
*
|
|
139
|
+
* Policy, not pip: a plain install (no `extras`) follows no extras at all,
|
|
140
|
+
* neither its own (a self-referencing line) nor its dependencies' — each
|
|
141
|
+
* dependency comes back bare, so extras are followed only along a chain that
|
|
142
|
+
* starts at an allowlist entry naming extras.
|
|
143
|
+
*/
|
|
144
|
+
export function requiredDists(metadataText, extras = []) {
|
|
145
|
+
const { self, lines } = requirementLines(metadataText);
|
|
146
|
+
const requested = new Set(extras.map(normalizePypiName));
|
|
147
|
+
const plain = requested.size === 0;
|
|
148
|
+
const included = (line) => line.gate === null || line.gate.some((extra) => requested.has(extra));
|
|
149
|
+
for (let grew = !plain; grew;) {
|
|
150
|
+
grew = false;
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
if (line.name !== self || !included(line))
|
|
153
|
+
continue;
|
|
154
|
+
for (const extra of line.extras) {
|
|
155
|
+
if (requested.size >= MAX_EXTRAS || requested.has(extra))
|
|
156
|
+
continue;
|
|
157
|
+
requested.add(extra);
|
|
158
|
+
grew = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const out = new Map();
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
if (line.name === self || !included(line))
|
|
53
165
|
continue;
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
|
|
166
|
+
const merged = out.get(line.name) ?? new Set();
|
|
167
|
+
if (!plain)
|
|
168
|
+
for (const extra of line.extras)
|
|
169
|
+
if (merged.size < MAX_EXTRAS)
|
|
170
|
+
merged.add(extra);
|
|
171
|
+
out.set(line.name, merged);
|
|
57
172
|
}
|
|
58
|
-
return [...
|
|
173
|
+
return [...out].map(([name, merged]) => ({ name, extras: [...merged].sort() }));
|
|
174
|
+
}
|
|
175
|
+
/** Dependency names a METADATA declares for a plain install (no extras). */
|
|
176
|
+
export function requiresDist(metadataText) {
|
|
177
|
+
return requiredDists(metadataText).map((dependency) => dependency.name);
|
|
59
178
|
}
|
|
60
179
|
export const pypiAdapter = {
|
|
61
180
|
id: "pypi",
|
|
@@ -68,12 +187,17 @@ export const pypiAdapter = {
|
|
|
68
187
|
if (!match)
|
|
69
188
|
throw new AllowlistEntryError(`"${raw}" is not a valid Python package entry`);
|
|
70
189
|
const name = normalizePypiName(match[1]);
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
190
|
+
let extras;
|
|
191
|
+
if (match[2] !== undefined) {
|
|
192
|
+
const parsed = parseExtras(match[2]);
|
|
193
|
+
if (!parsed)
|
|
194
|
+
throw new AllowlistEntryError(`"[${match[2]}]" in "${raw}" is not a valid list of extras (comma-separated names, at most ${MAX_EXTRAS})`);
|
|
195
|
+
extras = parsed;
|
|
196
|
+
}
|
|
197
|
+
const range = match[3].trim();
|
|
198
|
+
if (range && !validRange(range))
|
|
75
199
|
throw new AllowlistEntryError(`"${range}" is not a valid PEP 440 specifier`);
|
|
76
|
-
return { name, wildcard: false, range };
|
|
200
|
+
return { name, wildcard: false, ...(extras ? { extras } : {}), ...(range ? { range } : {}) };
|
|
77
201
|
},
|
|
78
202
|
normalizeName: normalizePypiName,
|
|
79
203
|
satisfies: (version, range) => pepSatisfies(version, range),
|
|
@@ -182,14 +306,14 @@ export const pypiAdapter = {
|
|
|
182
306
|
publishedAt: uploadTime(source),
|
|
183
307
|
};
|
|
184
308
|
},
|
|
185
|
-
async dependenciesFromFile(route, body) {
|
|
309
|
+
async dependenciesFromFile(route, body, extras) {
|
|
186
310
|
if (route.kind === "file-metadata")
|
|
187
|
-
return
|
|
311
|
+
return requiredDists(new TextDecoder().decode(body), extras);
|
|
188
312
|
if (!route.filename.endsWith(".whl") || body.byteLength > WHEEL_METADATA_LIMIT)
|
|
189
313
|
return [];
|
|
190
314
|
const entries = unzipSync(body, { filter: (file) => /\.dist-info\/METADATA$/.test(file.name) });
|
|
191
315
|
const metadata = Object.values(entries)[0];
|
|
192
|
-
return metadata ?
|
|
316
|
+
return metadata ? requiredDists(strFromU8(metadata), extras) : [];
|
|
193
317
|
},
|
|
194
318
|
workerConfig({ registryUrl, token, cacheDir }) {
|
|
195
319
|
const index = new URL("simple/", registryUrl);
|
|
@@ -17,6 +17,11 @@ export interface AllowlistEntry {
|
|
|
17
17
|
wildcard: boolean;
|
|
18
18
|
/** Optional version range in this ecosystem's syntax. */
|
|
19
19
|
range?: string;
|
|
20
|
+
/** PyPI only: the extras the entry names (`psycopg[binary]`), normalized
|
|
21
|
+
* (PEP 685), sorted and de-duplicated. Absent when it names none. Each
|
|
22
|
+
* one also allows the dependencies the package's own metadata declares
|
|
23
|
+
* under that extra, and nothing else. */
|
|
24
|
+
extras?: readonly string[];
|
|
20
25
|
}
|
|
21
26
|
export declare class AllowlistEntryError extends Error {
|
|
22
27
|
}
|
|
@@ -52,6 +57,12 @@ export interface DependencySpec {
|
|
|
52
57
|
name: string;
|
|
53
58
|
range: string;
|
|
54
59
|
}
|
|
60
|
+
/** One dependency read from a served file: its name and the extras the
|
|
61
|
+
* declaring line asks of it (`uvicorn[standard]`), both normalized. */
|
|
62
|
+
export interface FileDependency {
|
|
63
|
+
name: string;
|
|
64
|
+
extras: readonly string[];
|
|
65
|
+
}
|
|
55
66
|
export interface VersionInfo {
|
|
56
67
|
version: string;
|
|
57
68
|
/** Dependency names (normalized). Empty when discovered from files instead. */
|
|
@@ -271,10 +282,13 @@ export interface RegistryAdapter {
|
|
|
271
282
|
/** Map a download route to a file in metadata the proxy fetched itself.
|
|
272
283
|
* Returns null if the route names no known file. */
|
|
273
284
|
resolveDownload(route: DownloadRoute, meta: PackageMetadata): FileRef | null;
|
|
274
|
-
/** For ecosystems whose indexes omit dependencies (PyPI):
|
|
275
|
-
*
|
|
285
|
+
/** For ecosystems whose indexes omit dependencies (PyPI): dependencies
|
|
286
|
+
* read from a served file or its metadata file. `extras` are the extras
|
|
287
|
+
* the run allows for this package (from the allowlist or from a parent's
|
|
288
|
+
* dependency line): dependencies the file declares only under one of
|
|
289
|
+
* them are included too, and no others. Omitted when
|
|
276
290
|
* VersionInfo.dependencies is already complete (npm). */
|
|
277
|
-
dependenciesFromFile?(route: DownloadRoute | FileMetadataRoute, body: Uint8Array): Promise<
|
|
291
|
+
dependenciesFromFile?(route: DownloadRoute | FileMetadataRoute, body: Uint8Array, extras: readonly string[]): Promise<FileDependency[]>;
|
|
278
292
|
/** Map a file-metadata route (PEP 658) to the file it describes, from
|
|
279
293
|
* metadata the proxy fetched itself. Returns null if the route names no
|
|
280
294
|
* known file. */
|
|
@@ -144,8 +144,9 @@ export interface KubernetesJobConfig {
|
|
|
144
144
|
preflightTimeoutMs?: number;
|
|
145
145
|
readyTimeoutMs?: number;
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
148
|
-
* connects)
|
|
147
|
+
* Time budget for a single NetworkPolicy-enforcement probe (the keeper running two sequential
|
|
148
|
+
* connects); a streak's probes share one exec whose timeout the launcher derives from this.
|
|
149
|
+
* Defaults to the launcher's own 10 s, which a resource-constrained keeper (e.g. a
|
|
149
150
|
* laptop `kind` cluster's default 250m CPU / 128Mi limit) can exceed even though the probe
|
|
150
151
|
* itself is healthy — observed live: raising this to 60_000 was enough on `kind`. GKE Autopilot
|
|
151
152
|
* is unaffected by leaving this unset. The launcher derives its overall enforcement wall-clock
|
|
@@ -27,8 +27,14 @@
|
|
|
27
27
|
* client never did, so the connection is upgraded underneath a fetch
|
|
28
28
|
* implementation that never asked for it. That is why only real APIs broke: the
|
|
29
29
|
* GitHub App client (refusing every coding run before launch), the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Anthropic/Bedrock SDKs, and remote-JWKS verification in delegating auth mode.
|
|
31
|
+
* Plain h1 — every local test server — is unaffected. The OpenAI SDK was NOT
|
|
32
|
+
* among them at the time: openai@4 shipped its own node-fetch plus an
|
|
33
|
+
* agentkeepalive agent and never touched the global dispatcher. openai@5+
|
|
34
|
+
* (wardby pins 7.x) dropped that stack for the built-in `fetch`, so OpenAI
|
|
35
|
+
* traffic now rides this dispatcher too and depends on it staying h1-only —
|
|
36
|
+
* `providers/llm/openai.sdk-stream.test.ts` asserts the SDK's requests go
|
|
37
|
+
* through it.
|
|
32
38
|
*
|
|
33
39
|
* FIX. Keep the built-in `fetch` and give it a dispatcher that stays on the
|
|
34
40
|
* protocol it was written for: undici 8's own `Agent` with `allowH2: false`.
|
|
@@ -38,8 +44,9 @@
|
|
|
38
44
|
*
|
|
39
45
|
* WHY NOT replace the globals. Installing undici's `fetch`/`Headers`/`Response`/
|
|
40
46
|
* `Request`/`FormData` process-wide also works, but it swaps the classes under
|
|
41
|
-
* the MCP SDK, the OpenAI/Anthropic/Bedrock SDKs (all of which
|
|
42
|
-
* `Headers`/`Response`), `FormData`/`Blob`
|
|
47
|
+
* the MCP SDK, the OpenAI (7.x)/Anthropic/Bedrock SDKs (all of which use the
|
|
48
|
+
* built-in fetch and brand-check `Headers`/`Response`), `FormData`/`Blob`
|
|
49
|
+
* uploads, `AbortSignal` semantics and
|
|
43
50
|
* every cross-realm `instanceof` — real risk, for no gain over one dispatcher
|
|
44
51
|
* option. It would also move all control-plane egress to h2.
|
|
45
52
|
*
|
|
@@ -58,8 +65,8 @@
|
|
|
58
65
|
* silently restore the corruption — the price of fixing this with a dispatcher
|
|
59
66
|
* instead of owning the fetch stack. Nothing in the runtime image does that
|
|
60
67
|
* today: the only other `setGlobalDispatcher` call in the tree is
|
|
61
|
-
* `node-fetch-native` under the `prisma` CLI, which
|
|
62
|
-
* asserts is absent
|
|
68
|
+
* `node-fetch-native` under the `prisma` CLI, which the runtime stage of
|
|
69
|
+
* `deploy/Dockerfile` asserts is absent. (Prisma 7's client runtime and
|
|
63
70
|
* `@prisma/adapter-pg` contain none; Prisma 6's `runtime/binary.*` did.)
|
|
64
71
|
*
|
|
65
72
|
* Restoring Node's OWN dispatcher instead is not possible, though not for the
|