@steipete/oracle 0.14.1 → 0.15.1
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/bin/oracle-cli.js +16 -6
- package/dist/bin/oracle.js +569 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +416 -0
- package/dist/docs-site/browser-mode.html +594 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +462 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +399 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +388 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +323 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/src/browser/actions/assistantResponse.js +40 -31
- package/dist/src/browser/actions/attachments.js +28 -1
- package/dist/src/browser/actions/deepResearch.js +344 -128
- package/dist/src/browser/actions/modelSelection.js +75 -9
- package/dist/src/browser/actions/promptComposer.js +71 -14
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/actions/thinkingTime.js +65 -20
- package/dist/src/browser/artifacts.js +193 -14
- package/dist/src/browser/chatgptFiles.js +674 -91
- package/dist/src/browser/chromeCookies.js +312 -0
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/constants.js +5 -0
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +112 -41
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/browser/windowsCookies.js +219 -0
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/browserConfig.js +17 -1
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +41 -17
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +45 -43
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/vendor/oracle-notifier/README.md +26 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
|
|
4
4
|
import { sessionStore } from "../sessionStore.js";
|
|
5
|
+
import { resolveOverriddenApiModel } from "./modelResolver.js";
|
|
5
6
|
import { findOscProgressSequences, OSC_PROGRESS_PREFIX } from "osc-progress";
|
|
6
7
|
function forwardOscProgress(chunk, shouldForward) {
|
|
7
8
|
if (!shouldForward || !chunk.includes(OSC_PROGRESS_PREFIX)) {
|
|
@@ -81,7 +82,9 @@ function startModelExecution({ sessionMeta, runOptions, model, cwd, store, runOr
|
|
|
81
82
|
});
|
|
82
83
|
const result = await runOracleImpl({
|
|
83
84
|
...perModelOptions,
|
|
84
|
-
|
|
85
|
+
// Respect a user-config apiModel override for this known model; falls back
|
|
86
|
+
// to the canonical model id (unchanged behavior) when no override applies.
|
|
87
|
+
effectiveModelId: resolveOverriddenApiModel(model, runOptions.modelOverrides) ?? model,
|
|
85
88
|
// Drop per-model preamble; the aggregate runner prints the shared header and tips once.
|
|
86
89
|
suppressHeader: true,
|
|
87
90
|
suppressAnswerHeader: true,
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -25,7 +25,7 @@ import { createMarkdownStreamer } from "markdansi";
|
|
|
25
25
|
import { executeBackgroundResponse } from "./background.js";
|
|
26
26
|
import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from "./runUtils.js";
|
|
27
27
|
import { estimateUsdCost } from "tokentally";
|
|
28
|
-
import { isOpenRouterBaseUrl, isProModel, resolveModelConfig } from "./modelResolver.js";
|
|
28
|
+
import { isOpenRouterBaseUrl, isProModel, resolveModelConfig, resolveOverriddenApiModel, } from "./modelResolver.js";
|
|
29
29
|
import { validateProviderRouting } from "./providerRouting.js";
|
|
30
30
|
import { formatRouteTargetForLog, resolveProviderRoute, } from "./providerRoutePlan.js";
|
|
31
31
|
const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
|
|
@@ -133,6 +133,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
133
133
|
const modelConfig = await resolveModelConfig(options.model, {
|
|
134
134
|
baseUrl,
|
|
135
135
|
openRouterApiKey: resolverOpenRouterApiKey,
|
|
136
|
+
modelOverrides: options.modelOverrides,
|
|
136
137
|
});
|
|
137
138
|
const isLongRunningModel = isProTierModel;
|
|
138
139
|
const supportsBackground = modelConfig.supportsBackground !== false;
|
|
@@ -194,6 +195,8 @@ export async function runOracle(options, deps = {}) {
|
|
|
194
195
|
// Track the concrete model id we dispatch to (especially for Gemini preview aliases)
|
|
195
196
|
const effectiveModelId = azureDeploymentName ??
|
|
196
197
|
options.effectiveModelId ??
|
|
198
|
+
// A user-config apiModel override (known models only) wins over Gemini alias remapping.
|
|
199
|
+
resolveOverriddenApiModel(options.model, options.modelOverrides) ??
|
|
197
200
|
(options.model.startsWith("gemini")
|
|
198
201
|
? resolveGeminiModelId(options.model)
|
|
199
202
|
: (modelConfig.apiModel ?? modelConfig.model));
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { Transform } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
2
5
|
import path from "node:path";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
|
7
|
+
import { appendArtifacts, computeFileSha256, resolveSessionArtifactsDir, resolveUniqueArtifactPath, sanitizeArtifactFilename, sanitizeArtifactMimeType, validateArtifactFile, } from "../browser/artifacts.js";
|
|
8
|
+
import { MAX_REMOTE_ARTIFACT_BYTES, } from "./types.js";
|
|
4
9
|
import { parseHostPort } from "../bridge/connection.js";
|
|
5
10
|
export function createRemoteBrowserExecutor({ host, token }) {
|
|
6
11
|
// Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
|
|
@@ -25,6 +30,18 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
25
30
|
const body = Buffer.from(JSON.stringify(payload));
|
|
26
31
|
const { hostname, port } = parseHost(host);
|
|
27
32
|
return new Promise((resolve, reject) => {
|
|
33
|
+
const transferredFiles = [];
|
|
34
|
+
const transferFailures = [];
|
|
35
|
+
const transferPromises = [];
|
|
36
|
+
let artifactTransferQueue = Promise.resolve();
|
|
37
|
+
let settled = false;
|
|
38
|
+
let resolved = null;
|
|
39
|
+
const fail = (error) => {
|
|
40
|
+
if (settled)
|
|
41
|
+
return;
|
|
42
|
+
settled = true;
|
|
43
|
+
reject(error);
|
|
44
|
+
};
|
|
28
45
|
const req = http.request({
|
|
29
46
|
hostname,
|
|
30
47
|
port,
|
|
@@ -38,13 +55,12 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
38
55
|
}, (res) => {
|
|
39
56
|
if (res.statusCode !== 200) {
|
|
40
57
|
collectError(res)
|
|
41
|
-
.then((message) =>
|
|
42
|
-
.catch(
|
|
58
|
+
.then((message) => fail(new Error(message)))
|
|
59
|
+
.catch(fail);
|
|
43
60
|
return;
|
|
44
61
|
}
|
|
45
62
|
res.setEncoding("utf8");
|
|
46
63
|
let buffer = "";
|
|
47
|
-
let resolved = null;
|
|
48
64
|
res.on("data", (chunk) => {
|
|
49
65
|
buffer += chunk;
|
|
50
66
|
let newlineIndex = buffer.indexOf("\n");
|
|
@@ -52,22 +68,51 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
52
68
|
const line = buffer.slice(0, newlineIndex).trim();
|
|
53
69
|
buffer = buffer.slice(newlineIndex + 1);
|
|
54
70
|
if (line.length > 0) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
const transferPromise = handleEvent({
|
|
72
|
+
line,
|
|
73
|
+
options,
|
|
74
|
+
hostname,
|
|
75
|
+
port,
|
|
76
|
+
token,
|
|
77
|
+
onResult: (result) => {
|
|
78
|
+
resolved = result;
|
|
79
|
+
},
|
|
80
|
+
onArtifact: (artifact) => {
|
|
81
|
+
transferredFiles.push(artifact);
|
|
82
|
+
},
|
|
83
|
+
onArtifactFailure: (message) => {
|
|
84
|
+
transferFailures.push(message);
|
|
85
|
+
},
|
|
86
|
+
enqueueArtifactTransfer: (transfer) => {
|
|
87
|
+
const queued = artifactTransferQueue.then(transfer);
|
|
88
|
+
artifactTransferQueue = queued.catch(() => undefined);
|
|
89
|
+
return queued;
|
|
90
|
+
},
|
|
91
|
+
onError: fail,
|
|
92
|
+
});
|
|
93
|
+
if (transferPromise) {
|
|
94
|
+
transferPromises.push(transferPromise);
|
|
95
|
+
}
|
|
58
96
|
}
|
|
59
97
|
newlineIndex = buffer.indexOf("\n");
|
|
60
98
|
}
|
|
61
99
|
});
|
|
62
100
|
res.on("end", () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
void (async () => {
|
|
102
|
+
await Promise.allSettled(transferPromises);
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
if (!resolved) {
|
|
106
|
+
fail(new Error("Remote browser run completed without a result."));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
settled = true;
|
|
110
|
+
resolve(mergeTransferredArtifacts(resolved, transferredFiles, transferFailures));
|
|
111
|
+
})().catch(fail);
|
|
68
112
|
});
|
|
113
|
+
res.on("error", fail);
|
|
69
114
|
});
|
|
70
|
-
req.on("error",
|
|
115
|
+
req.on("error", fail);
|
|
71
116
|
req.write(body);
|
|
72
117
|
req.end();
|
|
73
118
|
});
|
|
@@ -95,26 +140,212 @@ function parseHost(input) {
|
|
|
95
140
|
throw new Error(`Invalid remote host: ${input} (${error instanceof Error ? error.message : String(error)})`);
|
|
96
141
|
}
|
|
97
142
|
}
|
|
98
|
-
function handleEvent(
|
|
143
|
+
function handleEvent(params) {
|
|
99
144
|
let event;
|
|
100
145
|
try {
|
|
101
|
-
event = JSON.parse(line);
|
|
146
|
+
event = JSON.parse(params.line);
|
|
102
147
|
}
|
|
103
148
|
catch (error) {
|
|
104
|
-
onError(new Error(`Failed to parse remote event: ${error instanceof Error ? error.message : String(error)}`));
|
|
105
|
-
return;
|
|
149
|
+
params.onError(new Error(`Failed to parse remote event: ${error instanceof Error ? error.message : String(error)}`));
|
|
150
|
+
return null;
|
|
106
151
|
}
|
|
107
152
|
if (event.type === "log") {
|
|
108
|
-
options.log?.(event.message);
|
|
109
|
-
return;
|
|
153
|
+
params.options.log?.(event.message);
|
|
154
|
+
return null;
|
|
110
155
|
}
|
|
111
156
|
if (event.type === "error") {
|
|
112
|
-
onError(new Error(event.message));
|
|
113
|
-
return;
|
|
157
|
+
params.onError(new Error(event.message));
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (event.type === "artifact-progress") {
|
|
161
|
+
if (params.options.verbose) {
|
|
162
|
+
params.options.log?.(`[browser] Artifact ${event.artifactId} ${event.phase}${event.receivedBytes !== undefined && event.totalBytes !== undefined
|
|
163
|
+
? ` ${event.receivedBytes}/${event.totalBytes} bytes`
|
|
164
|
+
: ""}`);
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (event.type === "artifact-ready") {
|
|
169
|
+
const displayFilename = sanitizeArtifactFilename(String(event.artifact?.filename ?? ""), "artifact.bin");
|
|
170
|
+
const transfer = params.enqueueArtifactTransfer(() => transferRemoteArtifact({
|
|
171
|
+
hostname: params.hostname,
|
|
172
|
+
port: params.port,
|
|
173
|
+
token: params.token,
|
|
174
|
+
descriptor: event.artifact,
|
|
175
|
+
sessionId: params.options.sessionId,
|
|
176
|
+
log: params.options.log,
|
|
177
|
+
})
|
|
178
|
+
.then((artifact) => {
|
|
179
|
+
params.onArtifact(artifact);
|
|
180
|
+
})
|
|
181
|
+
.catch((error) => {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
const fallback = `Oracle captured the browser text response, but bridge artifact transfer failed for ${displayFilename}. Open the ChatGPT browser on the bridge host, download the ZIP/file shown in the current response, and copy it to a cloud-readable path. Reason: ${message}`;
|
|
184
|
+
params.options.log?.(`[browser] ${fallback}`);
|
|
185
|
+
params.onArtifactFailure(fallback);
|
|
186
|
+
}));
|
|
187
|
+
return transfer;
|
|
114
188
|
}
|
|
115
189
|
if (event.type === "result") {
|
|
116
|
-
onResult(event.result);
|
|
190
|
+
params.onResult(event.result);
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
async function transferRemoteArtifact(params) {
|
|
195
|
+
validateRemoteArtifactDescriptor(params.descriptor);
|
|
196
|
+
const sessionId = params.sessionId ?? params.descriptor.runId;
|
|
197
|
+
const artifactsDir = resolveSessionArtifactsDir(sessionId);
|
|
198
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
199
|
+
const filename = sanitizeArtifactFilename(params.descriptor.filename, `artifact-${params.descriptor.artifactId}.bin`);
|
|
200
|
+
const finalPath = await resolveUniqueArtifactPath(path.join(artifactsDir, filename));
|
|
201
|
+
const partPath = `${finalPath}.part-${params.descriptor.artifactId}`;
|
|
202
|
+
const artifactPath = `/runs/${encodeURIComponent(params.descriptor.runId)}/artifacts/${encodeURIComponent(params.descriptor.artifactId)}`;
|
|
203
|
+
params.log?.(`[browser] Transferring artifact ${filename} from bridge host...`);
|
|
204
|
+
await downloadArtifactToFile({
|
|
205
|
+
hostname: params.hostname,
|
|
206
|
+
port: params.port,
|
|
207
|
+
path: artifactPath,
|
|
208
|
+
token: params.token,
|
|
209
|
+
targetPath: partPath,
|
|
210
|
+
descriptor: params.descriptor,
|
|
211
|
+
}).catch(async (error) => {
|
|
212
|
+
await rm(partPath, { force: true }).catch(() => undefined);
|
|
213
|
+
throw error;
|
|
214
|
+
});
|
|
215
|
+
const fileStat = await stat(partPath);
|
|
216
|
+
if (fileStat.size !== params.descriptor.byteSize) {
|
|
217
|
+
await rm(partPath, { force: true }).catch(() => undefined);
|
|
218
|
+
throw new Error(`size mismatch (${fileStat.size} != ${params.descriptor.byteSize})`);
|
|
219
|
+
}
|
|
220
|
+
const sha256 = await computeFileSha256(partPath);
|
|
221
|
+
if (sha256 !== params.descriptor.sha256) {
|
|
222
|
+
await rm(partPath, { force: true }).catch(() => undefined);
|
|
223
|
+
throw new Error("sha256 mismatch");
|
|
224
|
+
}
|
|
225
|
+
const validation = await validateArtifactFile({
|
|
226
|
+
path: partPath,
|
|
227
|
+
filename,
|
|
228
|
+
mimeType: sanitizeArtifactMimeType(params.descriptor.mimeType),
|
|
229
|
+
});
|
|
230
|
+
if (!validation.ok) {
|
|
231
|
+
await rm(partPath, { force: true }).catch(() => undefined);
|
|
232
|
+
throw new Error(`${validation.type} validation failed: ${validation.error ?? "invalid"}`);
|
|
233
|
+
}
|
|
234
|
+
await rename(partPath, finalPath);
|
|
235
|
+
params.log?.(`[browser] Transferred artifact to ${finalPath}`);
|
|
236
|
+
const publishedFilename = path.basename(finalPath);
|
|
237
|
+
return {
|
|
238
|
+
kind: "file",
|
|
239
|
+
path: finalPath,
|
|
240
|
+
label: publishedFilename,
|
|
241
|
+
mimeType: sanitizeArtifactMimeType(params.descriptor.mimeType),
|
|
242
|
+
sizeBytes: fileStat.size,
|
|
243
|
+
sourceUrl: "bridge-artifact",
|
|
244
|
+
sha256,
|
|
245
|
+
validation,
|
|
246
|
+
transfer: { status: "completed", bytes: fileStat.size },
|
|
247
|
+
origin: { mode: "bridge" },
|
|
248
|
+
url: "bridge-artifact",
|
|
249
|
+
finalUrl: "bridge-artifact",
|
|
250
|
+
filename: publishedFilename,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function downloadArtifactToFile(params) {
|
|
254
|
+
await new Promise((resolve, reject) => {
|
|
255
|
+
const req = http.request({
|
|
256
|
+
hostname: params.hostname,
|
|
257
|
+
port: params.port,
|
|
258
|
+
path: params.path,
|
|
259
|
+
method: "GET",
|
|
260
|
+
headers: params.token ? { authorization: `Bearer ${params.token}` } : undefined,
|
|
261
|
+
}, (res) => {
|
|
262
|
+
if (res.statusCode !== 200) {
|
|
263
|
+
collectError(res)
|
|
264
|
+
.then((message) => reject(new Error(message)))
|
|
265
|
+
.catch(reject);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const headerSha = String(res.headers["x-oracle-artifact-sha256"] ?? "");
|
|
269
|
+
if (headerSha && headerSha !== params.descriptor.sha256) {
|
|
270
|
+
res.resume();
|
|
271
|
+
reject(new Error("artifact sha256 header mismatch"));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const contentLengthHeader = res.headers["content-length"];
|
|
275
|
+
const contentLength = typeof contentLengthHeader === "string" ? Number(contentLengthHeader) : undefined;
|
|
276
|
+
if (contentLength !== undefined &&
|
|
277
|
+
(!Number.isSafeInteger(contentLength) ||
|
|
278
|
+
contentLength <= 0 ||
|
|
279
|
+
contentLength > MAX_REMOTE_ARTIFACT_BYTES ||
|
|
280
|
+
contentLength !== params.descriptor.byteSize)) {
|
|
281
|
+
res.resume();
|
|
282
|
+
reject(new Error("artifact content-length mismatch"));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const output = createWriteStream(params.targetPath, { flags: "wx" });
|
|
286
|
+
let receivedBytes = 0;
|
|
287
|
+
const limiter = new Transform({
|
|
288
|
+
transform(chunk, _encoding, callback) {
|
|
289
|
+
receivedBytes += chunk.length;
|
|
290
|
+
if (receivedBytes > params.descriptor.byteSize ||
|
|
291
|
+
receivedBytes > MAX_REMOTE_ARTIFACT_BYTES) {
|
|
292
|
+
callback(new Error("artifact exceeded declared size"));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
callback(null, chunk);
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
void pipeline(res, limiter, output).then(() => resolve(), reject);
|
|
299
|
+
});
|
|
300
|
+
req.on("error", reject);
|
|
301
|
+
req.end();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function validateRemoteArtifactDescriptor(descriptor) {
|
|
305
|
+
if (!descriptor ||
|
|
306
|
+
typeof descriptor !== "object" ||
|
|
307
|
+
descriptor.kind !== "file" ||
|
|
308
|
+
typeof descriptor.runId !== "string" ||
|
|
309
|
+
!/^[a-zA-Z0-9_-]{1,128}$/.test(descriptor.runId) ||
|
|
310
|
+
typeof descriptor.artifactId !== "string" ||
|
|
311
|
+
!/^[a-zA-Z0-9_-]{1,128}$/.test(descriptor.artifactId) ||
|
|
312
|
+
typeof descriptor.filename !== "string" ||
|
|
313
|
+
!Number.isSafeInteger(descriptor.byteSize) ||
|
|
314
|
+
descriptor.byteSize <= 0 ||
|
|
315
|
+
descriptor.byteSize > MAX_REMOTE_ARTIFACT_BYTES ||
|
|
316
|
+
typeof descriptor.sha256 !== "string" ||
|
|
317
|
+
!/^[a-f0-9]{64}$/.test(descriptor.sha256)) {
|
|
318
|
+
throw new Error("invalid bridge artifact descriptor");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function mergeTransferredArtifacts(result, transferredFiles, transferFailures) {
|
|
322
|
+
const artifacts = appendArtifacts(result.artifacts, transferredFiles);
|
|
323
|
+
const savedFiles = appendSavedFiles(result.savedFiles, transferredFiles);
|
|
324
|
+
const warnings = [
|
|
325
|
+
...(result.warnings ?? []),
|
|
326
|
+
...transferFailures.map((message) => ({
|
|
327
|
+
code: "remote-artifact-transfer-failed",
|
|
328
|
+
severity: "warning",
|
|
329
|
+
message,
|
|
330
|
+
})),
|
|
331
|
+
];
|
|
332
|
+
return {
|
|
333
|
+
...result,
|
|
334
|
+
artifacts,
|
|
335
|
+
savedFiles,
|
|
336
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function appendSavedFiles(existing, additions) {
|
|
340
|
+
const merged = new Map();
|
|
341
|
+
for (const artifact of existing ?? []) {
|
|
342
|
+
merged.set(artifact.path, artifact);
|
|
343
|
+
}
|
|
344
|
+
for (const artifact of additions) {
|
|
345
|
+
merged.set(artifact.path, artifact);
|
|
117
346
|
}
|
|
347
|
+
const values = Array.from(merged.values());
|
|
348
|
+
return values.length > 0 ? values : undefined;
|
|
118
349
|
}
|
|
119
350
|
function collectError(res) {
|
|
120
351
|
return new Promise((resolve, reject) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import { parseHostPort } from "../bridge/connection.js";
|
|
4
|
+
import { MAX_REMOTE_ARTIFACT_BYTES } from "./types.js";
|
|
4
5
|
export async function checkTcpConnection(host, timeoutMs = 2000) {
|
|
5
6
|
const { hostname, port } = parseHostPort(host);
|
|
6
7
|
return await new Promise((resolve) => {
|
|
@@ -47,11 +48,13 @@ export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
|
|
|
47
48
|
const ok = response.json.ok === true;
|
|
48
49
|
const version = response.json.version;
|
|
49
50
|
const uptimeSeconds = response.json.uptimeSeconds;
|
|
51
|
+
const capabilities = parseCapabilities(response.json.capabilities);
|
|
50
52
|
return {
|
|
51
53
|
ok,
|
|
52
54
|
statusCode: response.statusCode,
|
|
53
55
|
version: typeof version === "string" ? version : undefined,
|
|
54
56
|
uptimeSeconds: typeof uptimeSeconds === "number" ? uptimeSeconds : undefined,
|
|
57
|
+
capabilities,
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
if (response.statusCode === 404) {
|
|
@@ -68,6 +71,30 @@ export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
|
|
|
68
71
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
69
72
|
}
|
|
70
73
|
}
|
|
74
|
+
function parseCapabilities(value) {
|
|
75
|
+
if (!value || typeof value !== "object") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const raw = value;
|
|
79
|
+
if (raw.artifactTransfer !== true) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const artifactProtocolVersion = raw.artifactProtocolVersion;
|
|
83
|
+
const maxArtifactBytes = raw.maxArtifactBytes;
|
|
84
|
+
if (typeof artifactProtocolVersion !== "number" ||
|
|
85
|
+
!Number.isSafeInteger(artifactProtocolVersion) ||
|
|
86
|
+
artifactProtocolVersion <= 0 ||
|
|
87
|
+
typeof maxArtifactBytes !== "number" ||
|
|
88
|
+
!Number.isSafeInteger(maxArtifactBytes) ||
|
|
89
|
+
maxArtifactBytes <= 0) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
artifactTransfer: true,
|
|
94
|
+
artifactProtocolVersion,
|
|
95
|
+
maxArtifactBytes: Math.min(maxArtifactBytes, MAX_REMOTE_ARTIFACT_BYTES),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
71
98
|
function extractErrorMessage(json, bodyText) {
|
|
72
99
|
if (json && typeof json === "object") {
|
|
73
100
|
const err = json.error;
|