@forg3t/sdk 0.1.3 → 0.1.4
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/README.md +407 -63
- package/dist/index.d.mts +528 -12
- package/dist/index.d.ts +528 -12
- package/dist/index.js +1105 -34
- package/dist/index.mjs +1077 -40
- package/package.json +54 -49
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,17 +17,42 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
AccessLevel: () => AccessLevel,
|
|
34
|
+
DEFAULT_WHITEBOX_QUALITY_GATE_POLICY: () => DEFAULT_WHITEBOX_QUALITY_GATE_POLICY,
|
|
35
|
+
ENTERPRISE_STRICT_QUALITY_GATE_POLICY: () => ENTERPRISE_STRICT_QUALITY_GATE_POLICY,
|
|
23
36
|
Forg3tApiConnectionError: () => Forg3tApiConnectionError,
|
|
24
37
|
Forg3tAuthenticationError: () => Forg3tAuthenticationError,
|
|
25
38
|
Forg3tClient: () => Forg3tClient,
|
|
26
39
|
Forg3tError: () => Forg3tError,
|
|
27
40
|
Forg3tNotFoundError: () => Forg3tNotFoundError,
|
|
28
|
-
|
|
41
|
+
Forg3tNotImplementedError: () => Forg3tNotImplementedError,
|
|
42
|
+
Forg3tRateLimitError: () => Forg3tRateLimitError,
|
|
43
|
+
HttpTrainingAdapter: () => HttpTrainingAdapter,
|
|
44
|
+
IntegrationMode: () => IntegrationMode,
|
|
45
|
+
IntegrationType: () => IntegrationType,
|
|
46
|
+
LocalCommandTrainingAdapter: () => LocalCommandTrainingAdapter,
|
|
47
|
+
WHITEBOX_QUALITY_GATE_PRESETS: () => WHITEBOX_QUALITY_GATE_PRESETS,
|
|
48
|
+
WHITEBOX_QUALITY_GATE_PRESET_EXPLANATIONS: () => WHITEBOX_QUALITY_GATE_PRESET_EXPLANATIONS,
|
|
49
|
+
WhiteboxWorker: () => WhiteboxWorker,
|
|
50
|
+
assertDeploymentCompatibility: () => assertDeploymentCompatibility,
|
|
51
|
+
buildModelUnlearningPayload: () => buildModelUnlearningPayload,
|
|
52
|
+
buildWhiteboxWorkerOptions: () => buildWhiteboxWorkerOptions,
|
|
53
|
+
createNativeTrainingAdapter: () => createNativeTrainingAdapter,
|
|
54
|
+
evaluateWhiteboxQualityGate: () => evaluateWhiteboxQualityGate,
|
|
55
|
+
resolveWhiteboxQualityGatePolicy: () => resolveWhiteboxQualityGatePolicy
|
|
29
56
|
});
|
|
30
57
|
module.exports = __toCommonJS(index_exports);
|
|
31
58
|
|
|
@@ -64,18 +91,39 @@ var Forg3tApiConnectionError = class extends Forg3tError {
|
|
|
64
91
|
this.name = "Forg3tApiConnectionError";
|
|
65
92
|
}
|
|
66
93
|
};
|
|
94
|
+
var Forg3tNotImplementedError = class extends Forg3tError {
|
|
95
|
+
constructor(message, endpoint, requestId) {
|
|
96
|
+
super(
|
|
97
|
+
`Endpoint not implemented: ${endpoint || "unknown"}`,
|
|
98
|
+
501,
|
|
99
|
+
"NOT_IMPLEMENTED",
|
|
100
|
+
requestId,
|
|
101
|
+
{ endpoint }
|
|
102
|
+
);
|
|
103
|
+
this.name = "Forg3tNotImplementedError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
67
106
|
|
|
68
107
|
// src/transport.ts
|
|
69
108
|
var Transport = class {
|
|
70
109
|
constructor(config) {
|
|
71
|
-
|
|
110
|
+
const rawBaseUrl = config.baseUrl || typeof process !== "undefined" && process.env.FORG3T_API_URL || "";
|
|
111
|
+
this.baseUrl = String(rawBaseUrl).trim();
|
|
72
112
|
if (!this.baseUrl) {
|
|
73
113
|
throw new Error("Forg3tClient: apiUrl option or FORG3T_API_URL environment variable is required");
|
|
74
114
|
}
|
|
115
|
+
if (!/^https?:\/\//i.test(this.baseUrl)) {
|
|
116
|
+
throw new Error("Forg3tClient: apiUrl must be an absolute http(s) URL");
|
|
117
|
+
}
|
|
75
118
|
if (this.baseUrl.endsWith("/")) {
|
|
76
119
|
this.baseUrl = this.baseUrl.slice(0, -1);
|
|
77
120
|
}
|
|
78
|
-
|
|
121
|
+
const rawApiKey = config.apiKey || (typeof process !== "undefined" ? process.env.FORG3T_API_KEY : void 0);
|
|
122
|
+
const apiKey = typeof rawApiKey === "string" ? rawApiKey.trim() : rawApiKey;
|
|
123
|
+
this.apiKey = apiKey || void 0;
|
|
124
|
+
const rawBearerToken = config.bearerToken || (typeof process !== "undefined" ? process.env.FORG3T_BEARER_TOKEN : void 0);
|
|
125
|
+
const bearerToken = typeof rawBearerToken === "string" ? rawBearerToken.trim() : rawBearerToken;
|
|
126
|
+
this.bearerToken = bearerToken || void 0;
|
|
79
127
|
this.timeoutMs = config.timeoutMs || 3e4;
|
|
80
128
|
}
|
|
81
129
|
getBaseUrl() {
|
|
@@ -91,8 +139,11 @@ var Transport = class {
|
|
|
91
139
|
if (this.apiKey) {
|
|
92
140
|
headers["x-api-key"] = this.apiKey;
|
|
93
141
|
}
|
|
142
|
+
if (this.bearerToken && !headers.Authorization) {
|
|
143
|
+
headers.Authorization = `Bearer ${this.bearerToken}`;
|
|
144
|
+
}
|
|
94
145
|
if (requestId) {
|
|
95
|
-
headers["x-
|
|
146
|
+
headers["x-correlation-id"] = requestId;
|
|
96
147
|
}
|
|
97
148
|
const controller = new AbortController();
|
|
98
149
|
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
@@ -105,7 +156,7 @@ var Transport = class {
|
|
|
105
156
|
// Add the abort signal
|
|
106
157
|
});
|
|
107
158
|
clearTimeout(timeoutId);
|
|
108
|
-
const responseRequestId = response.headers.get("x-request-id") || requestId;
|
|
159
|
+
const responseRequestId = response.headers.get("x-correlation-id") || response.headers.get("x-request-id") || requestId;
|
|
109
160
|
if (!response.ok) {
|
|
110
161
|
let errorData = {};
|
|
111
162
|
try {
|
|
@@ -143,7 +194,8 @@ var Transport = class {
|
|
|
143
194
|
if (error instanceof Error && error.name === "AbortError") {
|
|
144
195
|
throw new Forg3tApiConnectionError(`Request timeout after ${this.timeoutMs}ms`);
|
|
145
196
|
}
|
|
146
|
-
|
|
197
|
+
const detail = error instanceof Error ? error.message : "Network error";
|
|
198
|
+
throw new Forg3tApiConnectionError(`Request failed for ${url}: ${detail}`);
|
|
147
199
|
}
|
|
148
200
|
}
|
|
149
201
|
async download(url, requestId) {
|
|
@@ -154,6 +206,9 @@ var Transport = class {
|
|
|
154
206
|
if (this.apiKey) {
|
|
155
207
|
headers["x-api-key"] = this.apiKey;
|
|
156
208
|
}
|
|
209
|
+
if (this.bearerToken && !headers.Authorization) {
|
|
210
|
+
headers.Authorization = `Bearer ${this.bearerToken}`;
|
|
211
|
+
}
|
|
157
212
|
if (requestId) {
|
|
158
213
|
headers["x-request-id"] = requestId;
|
|
159
214
|
}
|
|
@@ -202,7 +257,8 @@ var Transport = class {
|
|
|
202
257
|
if (error instanceof Error && error.name === "AbortError") {
|
|
203
258
|
throw new Forg3tApiConnectionError(`Download timeout after ${this.timeoutMs}ms`);
|
|
204
259
|
}
|
|
205
|
-
|
|
260
|
+
const detail = error instanceof Error ? error.message : "Network error";
|
|
261
|
+
throw new Forg3tApiConnectionError(`Download failed for ${url}: ${detail}`);
|
|
206
262
|
}
|
|
207
263
|
}
|
|
208
264
|
async requestRaw(path, options = {}, requestId) {
|
|
@@ -215,6 +271,9 @@ var Transport = class {
|
|
|
215
271
|
if (this.apiKey) {
|
|
216
272
|
headers["x-api-key"] = this.apiKey;
|
|
217
273
|
}
|
|
274
|
+
if (this.bearerToken && !headers.Authorization) {
|
|
275
|
+
headers.Authorization = `Bearer ${this.bearerToken}`;
|
|
276
|
+
}
|
|
218
277
|
if (requestId) {
|
|
219
278
|
headers["x-request-id"] = requestId;
|
|
220
279
|
}
|
|
@@ -238,7 +297,8 @@ var Transport = class {
|
|
|
238
297
|
if (error instanceof Error && error.name === "AbortError") {
|
|
239
298
|
throw new Forg3tApiConnectionError(`Request timeout after ${this.timeoutMs}ms`);
|
|
240
299
|
}
|
|
241
|
-
|
|
300
|
+
const detail = error instanceof Error ? error.message : "Network error";
|
|
301
|
+
throw new Forg3tApiConnectionError(`Raw request failed for ${url}: ${detail}`);
|
|
242
302
|
}
|
|
243
303
|
}
|
|
244
304
|
};
|
|
@@ -249,9 +309,31 @@ var Forg3tClient = class {
|
|
|
249
309
|
this.transport = new Transport({
|
|
250
310
|
baseUrl: options.apiUrl,
|
|
251
311
|
apiKey: options.apiKey,
|
|
312
|
+
bearerToken: options.bearerToken,
|
|
252
313
|
timeoutMs: options.timeoutMs
|
|
253
314
|
});
|
|
254
315
|
}
|
|
316
|
+
// --- Authentication ---
|
|
317
|
+
/**
|
|
318
|
+
* Get the current user context and their default project.
|
|
319
|
+
* Used by the Admin Dashboard for bootstrap and session verification.
|
|
320
|
+
*/
|
|
321
|
+
async getCurrentUser(requestId) {
|
|
322
|
+
return this.transport.request("/v1/me", { method: "GET" }, requestId);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Bootstraps a tenant for an authenticated dashboard user.
|
|
326
|
+
* Requires a bearer token from the user's session instead of an API key.
|
|
327
|
+
*/
|
|
328
|
+
async bootstrapTenant(data, requestId) {
|
|
329
|
+
if (!data?.tenantName || typeof data.tenantName !== "string" || !data.tenantName.trim()) {
|
|
330
|
+
throw new Error("bootstrapTenant requires a non-empty tenantName.");
|
|
331
|
+
}
|
|
332
|
+
return this.transport.request("/v1/bootstrap/tenant", {
|
|
333
|
+
method: "POST",
|
|
334
|
+
body: JSON.stringify(data)
|
|
335
|
+
}, requestId);
|
|
336
|
+
}
|
|
255
337
|
// --- Projects ---
|
|
256
338
|
async getProjectOverview(projectId, requestId) {
|
|
257
339
|
return this.transport.request(`/v1/projects/${projectId}/overview`, { method: "GET" }, requestId);
|
|
@@ -267,6 +349,42 @@ var Forg3tClient = class {
|
|
|
267
349
|
if (filters.cursor) params.append("cursor", filters.cursor);
|
|
268
350
|
return this.transport.request(`/v1/projects/${projectId}/audit?${params.toString()}`, { method: "GET" }, requestId);
|
|
269
351
|
}
|
|
352
|
+
// --- Integrations ---
|
|
353
|
+
async listIntegrations(projectId, requestId) {
|
|
354
|
+
return this.transport.request(`/v1/projects/${projectId}/integrations`, { method: "GET" }, requestId);
|
|
355
|
+
}
|
|
356
|
+
async createIntegration(projectId, data, requestId) {
|
|
357
|
+
return this.transport.request(`/v1/projects/${projectId}/integrations`, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
body: JSON.stringify(data)
|
|
360
|
+
}, requestId);
|
|
361
|
+
}
|
|
362
|
+
async testIntegration(projectId, integrationId, requestId) {
|
|
363
|
+
void projectId;
|
|
364
|
+
return this.transport.request(
|
|
365
|
+
`/v1/integrations/${integrationId}/test`,
|
|
366
|
+
{ method: "POST" },
|
|
367
|
+
requestId
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
// --- Unlearning Requests ---
|
|
371
|
+
async createUnlearningRequest(projectId, data, requestId) {
|
|
372
|
+
this.assertCreateUnlearningRequest(data);
|
|
373
|
+
return this.transport.request(`/v1/projects/${projectId}/unlearning-requests`, {
|
|
374
|
+
method: "POST",
|
|
375
|
+
body: JSON.stringify(data)
|
|
376
|
+
}, requestId);
|
|
377
|
+
}
|
|
378
|
+
async listUnlearningRequests(projectId, requestId) {
|
|
379
|
+
return this.transport.request(`/v1/projects/${projectId}/unlearning-requests`, { method: "GET" }, requestId);
|
|
380
|
+
}
|
|
381
|
+
async getUnlearningRequest(projectId, unlearningRequestId, requestId) {
|
|
382
|
+
return this.transport.request(
|
|
383
|
+
`/v1/projects/${projectId}/unlearning-requests/${unlearningRequestId}`,
|
|
384
|
+
{ method: "GET" },
|
|
385
|
+
requestId
|
|
386
|
+
);
|
|
387
|
+
}
|
|
270
388
|
// --- API Keys ---
|
|
271
389
|
async listApiKeys(projectId, requestId) {
|
|
272
390
|
return this.transport.request(`/v1/projects/${projectId}/api-keys`, { method: "GET" }, requestId);
|
|
@@ -285,15 +403,48 @@ var Forg3tClient = class {
|
|
|
285
403
|
}
|
|
286
404
|
// --- Jobs ---
|
|
287
405
|
async submitJob(projectId, data, requestId) {
|
|
406
|
+
const normalizedPayload = "payload" in data ? data.payload : {
|
|
407
|
+
claim: data.claim,
|
|
408
|
+
config: data.config
|
|
409
|
+
};
|
|
410
|
+
if (data.type === "MODEL_UNLEARN") {
|
|
411
|
+
this.assertModelUnlearnPreflight(normalizedPayload);
|
|
412
|
+
}
|
|
288
413
|
const res = await this.transport.request(`/v1/projects/${projectId}/jobs`, {
|
|
289
414
|
method: "POST",
|
|
290
|
-
body: JSON.stringify(
|
|
415
|
+
body: JSON.stringify({
|
|
416
|
+
type: data.type,
|
|
417
|
+
payload: normalizedPayload,
|
|
418
|
+
idempotencyKey: data.idempotencyKey
|
|
419
|
+
})
|
|
291
420
|
}, requestId);
|
|
292
421
|
return res;
|
|
293
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Creates a new job with deduplication support via an optional idempotency key.
|
|
425
|
+
* This wrapper cleanly exposes the core parameters for the production job queue.
|
|
426
|
+
*/
|
|
427
|
+
async createJob(projectId, type, claim, config, idempotencyKey, requestId) {
|
|
428
|
+
return this.submitJob(projectId, { type, claim, config, idempotencyKey }, requestId);
|
|
429
|
+
}
|
|
294
430
|
async getJob(projectId, jobId, requestId) {
|
|
295
431
|
return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}`, { method: "GET" }, requestId);
|
|
296
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Polls the job status continuously until a terminal state (succeeded, failed, canceled) is reached,
|
|
435
|
+
* or the specified timeout limits are exceeded.
|
|
436
|
+
*/
|
|
437
|
+
async pollJobStatus(projectId, jobId, timeoutMs = 3e5, pollIntervalMs = 2e3, requestId) {
|
|
438
|
+
const startTime = Date.now();
|
|
439
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
440
|
+
const job = await this.getJob(projectId, jobId, requestId);
|
|
441
|
+
if (job.status === "succeeded" || job.status === "failed" || job.status === "canceled") {
|
|
442
|
+
return job;
|
|
443
|
+
}
|
|
444
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
445
|
+
}
|
|
446
|
+
throw new Error(`pollJobStatus timed out after ${timeoutMs}ms waiting for job ${jobId} to reach a terminal state.`);
|
|
447
|
+
}
|
|
297
448
|
async listJobs(projectId, query, requestId) {
|
|
298
449
|
const params = new URLSearchParams();
|
|
299
450
|
if (query?.status) params.append("status", query.status);
|
|
@@ -307,20 +458,37 @@ var Forg3tClient = class {
|
|
|
307
458
|
body: JSON.stringify(data)
|
|
308
459
|
}, requestId);
|
|
309
460
|
}
|
|
310
|
-
async heartbeatJob(projectId, jobId, requestId) {
|
|
311
|
-
|
|
461
|
+
async heartbeatJob(projectId, jobId, optionsOrRequestId, requestId) {
|
|
462
|
+
const options = typeof optionsOrRequestId === "string" ? {} : optionsOrRequestId || {};
|
|
463
|
+
const resolvedRequestId = typeof optionsOrRequestId === "string" ? optionsOrRequestId : requestId;
|
|
464
|
+
return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/heartbeat`, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
body: JSON.stringify({
|
|
467
|
+
claimId: options.claimId
|
|
468
|
+
})
|
|
469
|
+
}, resolvedRequestId);
|
|
312
470
|
}
|
|
313
|
-
async completeJob(projectId, jobId, result, requestId) {
|
|
471
|
+
async completeJob(projectId, jobId, result, optionsOrRequestId, requestId) {
|
|
472
|
+
const options = typeof optionsOrRequestId === "string" ? {} : optionsOrRequestId || {};
|
|
473
|
+
const resolvedRequestId = typeof optionsOrRequestId === "string" ? optionsOrRequestId : requestId;
|
|
314
474
|
return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/complete`, {
|
|
315
475
|
method: "POST",
|
|
316
|
-
body: JSON.stringify(
|
|
317
|
-
|
|
476
|
+
body: JSON.stringify({
|
|
477
|
+
...result,
|
|
478
|
+
claimId: options.claimId || result.claimId
|
|
479
|
+
})
|
|
480
|
+
}, resolvedRequestId);
|
|
318
481
|
}
|
|
319
|
-
async failJob(projectId, jobId, error, requestId) {
|
|
482
|
+
async failJob(projectId, jobId, error, optionsOrRequestId, requestId) {
|
|
483
|
+
const options = typeof optionsOrRequestId === "string" ? {} : optionsOrRequestId || {};
|
|
484
|
+
const resolvedRequestId = typeof optionsOrRequestId === "string" ? optionsOrRequestId : requestId;
|
|
320
485
|
return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/fail`, {
|
|
321
486
|
method: "POST",
|
|
322
|
-
body: JSON.stringify(
|
|
323
|
-
|
|
487
|
+
body: JSON.stringify({
|
|
488
|
+
...error,
|
|
489
|
+
claimId: options.claimId || error.claimId
|
|
490
|
+
})
|
|
491
|
+
}, resolvedRequestId);
|
|
324
492
|
}
|
|
325
493
|
async listDeadJobs(projectId, requestId) {
|
|
326
494
|
return this.transport.request(`/v1/projects/${projectId}/jobs/dead`, { method: "GET" }, requestId);
|
|
@@ -330,7 +498,20 @@ var Forg3tClient = class {
|
|
|
330
498
|
}
|
|
331
499
|
// --- Evidence ---
|
|
332
500
|
async getEvidence(jobId, requestId) {
|
|
333
|
-
|
|
501
|
+
try {
|
|
502
|
+
return await this.transport.request(`/v1/jobs/${jobId}/evidence`, { method: "GET" }, requestId);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (error instanceof Forg3tNotFoundError) {
|
|
505
|
+
return this.transport.request(`/v1/jobs/evidence/${jobId}`, { method: "GET" }, requestId);
|
|
506
|
+
}
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async getEvidenceJson(jobId, requestId) {
|
|
511
|
+
return this.transport.request(`/v1/jobs/${jobId}/evidence.json`, { method: "GET" }, requestId);
|
|
512
|
+
}
|
|
513
|
+
async getArtifactDownloadUrl(projectId, jobId, filename, requestId) {
|
|
514
|
+
return this.transport.request(`/v1/projects/${projectId}/jobs/${jobId}/artifacts/${filename}/download`, { method: "GET" }, requestId);
|
|
334
515
|
}
|
|
335
516
|
async getEvidencePdf(jobId, options) {
|
|
336
517
|
const response = await this.transport.requestRaw(`/v1/jobs/${jobId}/evidence.pdf`, { method: "GET" });
|
|
@@ -354,26 +535,24 @@ var Forg3tClient = class {
|
|
|
354
535
|
return result;
|
|
355
536
|
}
|
|
356
537
|
async savePdfToFile(data, filePath) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const fs = require("fs");
|
|
360
|
-
const path = require("path");
|
|
361
|
-
const dir = path.dirname(filePath);
|
|
362
|
-
if (!fs.existsSync(dir)) {
|
|
363
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
364
|
-
}
|
|
365
|
-
fs.writeFileSync(filePath, Buffer.from(data));
|
|
366
|
-
} catch (error) {
|
|
367
|
-
console.error("Failed to save PDF to file:", error);
|
|
368
|
-
throw error;
|
|
369
|
-
}
|
|
370
|
-
} else {
|
|
538
|
+
const isNode = typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
539
|
+
if (!isNode) {
|
|
371
540
|
throw new Error("File saving is only supported in Node.js environment");
|
|
372
541
|
}
|
|
542
|
+
try {
|
|
543
|
+
const fs = await import("fs/promises");
|
|
544
|
+
const path = await import("path");
|
|
545
|
+
const dir = path.dirname(filePath);
|
|
546
|
+
await fs.mkdir(dir, { recursive: true });
|
|
547
|
+
await fs.writeFile(filePath, data);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error("Failed to save PDF to file:", error);
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
373
552
|
}
|
|
374
553
|
/**
|
|
375
554
|
* @deprecated The current Control Plane version does not support direct artifact downloads.
|
|
376
|
-
* Please use
|
|
555
|
+
* Please use getArtifactDownloadUrl(), getEvidencePdf(), or getEvidenceJson().
|
|
377
556
|
*/
|
|
378
557
|
async downloadEvidenceArtifact(_jobId, _requestId) {
|
|
379
558
|
throw new Error("Artifact download not supported by current API version. Use getEvidence() to retrieve content.");
|
|
@@ -444,13 +623,905 @@ var Forg3tClient = class {
|
|
|
444
623
|
body: JSON.stringify(result)
|
|
445
624
|
}, requestId);
|
|
446
625
|
}
|
|
626
|
+
assertCreateUnlearningRequest(data) {
|
|
627
|
+
if (!data.target?.value?.trim()) {
|
|
628
|
+
throw new Error("createUnlearningRequest: target.value is required.");
|
|
629
|
+
}
|
|
630
|
+
if (typeof data.target.aliases !== "undefined") {
|
|
631
|
+
if (!Array.isArray(data.target.aliases) || data.target.aliases.some((alias) => typeof alias !== "string" || !alias.trim())) {
|
|
632
|
+
throw new Error("createUnlearningRequest: target.aliases must be an array of non-empty strings when provided.");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (data.accessLevel === "layer_a_only") {
|
|
636
|
+
const target = data.execution?.target;
|
|
637
|
+
if (!target) {
|
|
638
|
+
throw new Error("createUnlearningRequest: execution.target is required for layer_a_only.");
|
|
639
|
+
}
|
|
640
|
+
if ((target.provider === "openai" || target.provider === "groq") && !target.model?.trim()) {
|
|
641
|
+
throw new Error(`createUnlearningRequest: execution.target.model is required when provider=${target.provider}.`);
|
|
642
|
+
}
|
|
643
|
+
if ((target.provider === "http_generic" || target.provider === "custom") && !target.endpoint?.trim()) {
|
|
644
|
+
throw new Error(`createUnlearningRequest: execution.target.endpoint is required when provider=${target.provider}.`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (data.accessLevel === "layer_a_and_b") {
|
|
648
|
+
if (!data.execution?.model?.uri?.trim()) {
|
|
649
|
+
throw new Error("createUnlearningRequest: execution.model.uri is required for layer_a_and_b.");
|
|
650
|
+
}
|
|
651
|
+
const targetTokenIds = data.execution?.plan?.hyperparameters?.target_token_ids;
|
|
652
|
+
const tokenizerUri = data.execution?.model?.metadata?.tokenizer_uri;
|
|
653
|
+
const localizationMode = String(
|
|
654
|
+
data.execution?.plan?.hyperparameters?.localization_mode || data.execution?.plan?.hyperparameters?.localizationMode || ""
|
|
655
|
+
).trim().toLowerCase();
|
|
656
|
+
const allowMissingLocalization = String(process.env.FORG3T_ALLOW_MODEL_UNLEARN_WITHOUT_LOCALIZATION_INPUT || "").toLowerCase() === "true";
|
|
657
|
+
const hasTokenIds = Array.isArray(targetTokenIds) && targetTokenIds.some((value) => Number.isInteger(value) && Number(value) >= 0);
|
|
658
|
+
const hasTokenizer = typeof tokenizerUri === "string" && tokenizerUri.trim().length > 0;
|
|
659
|
+
if (localizationMode === "explicit_token_ids" && !hasTokenIds) {
|
|
660
|
+
throw new Error("createUnlearningRequest: localization_mode=explicit_token_ids requires target token IDs.");
|
|
661
|
+
}
|
|
662
|
+
if (localizationMode === "tokenizer_auto" && !hasTokenizer) {
|
|
663
|
+
throw new Error("createUnlearningRequest: localization_mode=tokenizer_auto requires execution.model.metadata.tokenizer_uri.");
|
|
664
|
+
}
|
|
665
|
+
if (localizationMode === "hybrid") {
|
|
666
|
+
if (!hasTokenIds) {
|
|
667
|
+
throw new Error("createUnlearningRequest: localization_mode=hybrid requires target token IDs.");
|
|
668
|
+
}
|
|
669
|
+
if (!hasTokenizer) {
|
|
670
|
+
throw new Error("createUnlearningRequest: localization_mode=hybrid requires execution.model.metadata.tokenizer_uri.");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!allowMissingLocalization && !hasTokenIds && !hasTokenizer) {
|
|
674
|
+
throw new Error(
|
|
675
|
+
"createUnlearningRequest: layer_a_and_b requires localization input. Provide target token IDs (execution.plan.hyperparameters.target_token_ids) or tokenizer URI (execution.model.metadata.tokenizer_uri)."
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
assertModelUnlearnPreflight(payload) {
|
|
681
|
+
if (!payload || typeof payload !== "object") {
|
|
682
|
+
throw new Error("MODEL_UNLEARN preflight failed: payload must be an object.");
|
|
683
|
+
}
|
|
684
|
+
const asRecord = payload;
|
|
685
|
+
const config = asRecord.config && typeof asRecord.config === "object" && !Array.isArray(asRecord.config) ? asRecord.config : null;
|
|
686
|
+
if (!config) {
|
|
687
|
+
throw new Error("MODEL_UNLEARN preflight failed: payload.config is required.");
|
|
688
|
+
}
|
|
689
|
+
const targetConfig = config.target_config && typeof config.target_config === "object" && !Array.isArray(config.target_config) ? config.target_config : null;
|
|
690
|
+
const parameters = config.parameters && typeof config.parameters === "object" && !Array.isArray(config.parameters) ? config.parameters : null;
|
|
691
|
+
const modelFromTargetConfig = targetConfig?.model && typeof targetConfig.model === "object" && !Array.isArray(targetConfig.model) ? targetConfig.model : null;
|
|
692
|
+
const modelFromParameters = parameters?.model && typeof parameters.model === "object" && !Array.isArray(parameters.model) ? parameters.model : null;
|
|
693
|
+
const model = modelFromParameters || modelFromTargetConfig;
|
|
694
|
+
const modelUri = typeof model?.uri === "string" ? model.uri.trim() : "";
|
|
695
|
+
if (!modelUri) {
|
|
696
|
+
throw new Error("MODEL_UNLEARN preflight failed: config.target_config.model.uri (or config.parameters.model.uri) is required.");
|
|
697
|
+
}
|
|
698
|
+
const target = parameters?.target && typeof parameters.target === "object" && !Array.isArray(parameters.target) ? parameters.target : null;
|
|
699
|
+
const tokenIds = target?.tokenIds;
|
|
700
|
+
const hasTokenIds = Array.isArray(tokenIds) && tokenIds.some((value) => Number.isInteger(value) && Number(value) >= 0);
|
|
701
|
+
const tokenizerFromTargetConfig = targetConfig?.tokenizer && typeof targetConfig.tokenizer === "object" && !Array.isArray(targetConfig.tokenizer) ? targetConfig.tokenizer : null;
|
|
702
|
+
const tokenizerFromParameters = parameters?.tokenizer && typeof parameters.tokenizer === "object" && !Array.isArray(parameters.tokenizer) ? parameters.tokenizer : null;
|
|
703
|
+
const tokenizerFromModel = model?.tokenizer && typeof model.tokenizer === "object" && !Array.isArray(model.tokenizer) ? model.tokenizer : null;
|
|
704
|
+
const tokenizerCandidates = [
|
|
705
|
+
tokenizerFromTargetConfig?.uri,
|
|
706
|
+
tokenizerFromParameters?.uri,
|
|
707
|
+
parameters?.tokenizer_uri,
|
|
708
|
+
model?.tokenizer_uri,
|
|
709
|
+
tokenizerFromModel?.uri
|
|
710
|
+
];
|
|
711
|
+
const hasTokenizerUri = tokenizerCandidates.some((candidate) => typeof candidate === "string" && candidate.trim().length > 0);
|
|
712
|
+
const allowMissingLocalization = String(process.env.FORG3T_ALLOW_MODEL_UNLEARN_WITHOUT_LOCALIZATION_INPUT || "").toLowerCase() === "true";
|
|
713
|
+
if (!allowMissingLocalization && !hasTokenIds && !hasTokenizerUri) {
|
|
714
|
+
throw new Error(
|
|
715
|
+
"MODEL_UNLEARN preflight failed: missing localization input. Provide config.parameters.target.tokenIds or a tokenizer URI in config.target_config.tokenizer.uri / config.parameters.tokenizer.uri / config.parameters.tokenizer_uri / model.tokenizer_uri."
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// src/types.ts
|
|
722
|
+
var IntegrationType = /* @__PURE__ */ ((IntegrationType2) => {
|
|
723
|
+
IntegrationType2["SLACK"] = "slack";
|
|
724
|
+
IntegrationType2["CONFLUENCE"] = "confluence";
|
|
725
|
+
IntegrationType2["GOOGLE_DRIVE"] = "google_drive";
|
|
726
|
+
IntegrationType2["ZENDESK"] = "zendesk";
|
|
727
|
+
IntegrationType2["NOTION"] = "notion";
|
|
728
|
+
IntegrationType2["LANGCHAIN"] = "langchain";
|
|
729
|
+
IntegrationType2["OPENROUTER"] = "openrouter";
|
|
730
|
+
IntegrationType2["OPENAI"] = "openai";
|
|
731
|
+
IntegrationType2["CUSTOM"] = "custom";
|
|
732
|
+
return IntegrationType2;
|
|
733
|
+
})(IntegrationType || {});
|
|
734
|
+
var IntegrationMode = /* @__PURE__ */ ((IntegrationMode2) => {
|
|
735
|
+
IntegrationMode2["MODE_A_BLACKBOX"] = "mode_a_blackbox";
|
|
736
|
+
IntegrationMode2["MODE_B_WHITEBOX"] = "mode_b_whitebox";
|
|
737
|
+
return IntegrationMode2;
|
|
738
|
+
})(IntegrationMode || {});
|
|
739
|
+
var AccessLevel = /* @__PURE__ */ ((AccessLevel2) => {
|
|
740
|
+
AccessLevel2["LAYER_A_ONLY"] = "layer_a_only";
|
|
741
|
+
AccessLevel2["LAYER_A_AND_B"] = "layer_a_and_b";
|
|
742
|
+
return AccessLevel2;
|
|
743
|
+
})(AccessLevel || {});
|
|
744
|
+
|
|
745
|
+
// src/whitebox.ts
|
|
746
|
+
var crypto = __toESM(require("crypto"));
|
|
747
|
+
|
|
748
|
+
// src/quality-gate.ts
|
|
749
|
+
var WHITEBOX_QUALITY_GATE_PRESETS = {
|
|
750
|
+
balanced: {
|
|
751
|
+
requireEvaluation: false,
|
|
752
|
+
maxLeakScore: 0.05,
|
|
753
|
+
allowedVerdicts: ["PASS", "REVIEW"],
|
|
754
|
+
minChangedTensorCount: 1,
|
|
755
|
+
minChangedParamCount: 1,
|
|
756
|
+
requireModelHashTransition: true,
|
|
757
|
+
requireLocalization: false,
|
|
758
|
+
minLocalizationTokenCoverage: 0.5,
|
|
759
|
+
minLocalizedParamCount: 1,
|
|
760
|
+
minLocalizationConfidence: 0.6
|
|
761
|
+
},
|
|
762
|
+
strict: {
|
|
763
|
+
requireEvaluation: true,
|
|
764
|
+
maxLeakScore: 0.03,
|
|
765
|
+
allowedVerdicts: ["PASS"],
|
|
766
|
+
minChangedTensorCount: 1,
|
|
767
|
+
minChangedParamCount: 1,
|
|
768
|
+
requireModelHashTransition: true,
|
|
769
|
+
requireLocalization: true,
|
|
770
|
+
minLocalizationTokenCoverage: 0.5,
|
|
771
|
+
minLocalizedParamCount: 1,
|
|
772
|
+
minLocalizationConfidence: 0.6
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
var WHITEBOX_QUALITY_GATE_PRESET_EXPLANATIONS = {
|
|
776
|
+
balanced: "Balanced gate for faster rollout. Allows PASS/REVIEW and can run without evaluator output.",
|
|
777
|
+
strict: "Strict gate for production. Requires evaluator output, localization evidence, and PASS verdict only."
|
|
778
|
+
};
|
|
779
|
+
var DEFAULT_WHITEBOX_QUALITY_GATE_POLICY = {
|
|
780
|
+
requireEvaluation: false,
|
|
781
|
+
maxLeakScore: 0.05,
|
|
782
|
+
allowedVerdicts: ["PASS", "REVIEW"],
|
|
783
|
+
minChangedTensorCount: 1,
|
|
784
|
+
minChangedParamCount: 1,
|
|
785
|
+
requireModelHashTransition: true,
|
|
786
|
+
requireLocalization: false,
|
|
787
|
+
minLocalizationTokenCoverage: 0.5,
|
|
788
|
+
minLocalizedParamCount: 1,
|
|
789
|
+
minLocalizationConfidence: 0.6
|
|
790
|
+
};
|
|
791
|
+
var resolveWhiteboxQualityGatePolicy = (preset = "balanced", overrides) => {
|
|
792
|
+
return {
|
|
793
|
+
...WHITEBOX_QUALITY_GATE_PRESETS[preset],
|
|
794
|
+
...overrides || {}
|
|
795
|
+
};
|
|
796
|
+
};
|
|
797
|
+
var evaluateWhiteboxQualityGate = (unlearning, evaluation, policyOverrides) => {
|
|
798
|
+
const policy = {
|
|
799
|
+
...resolveWhiteboxQualityGatePolicy("balanced"),
|
|
800
|
+
...policyOverrides || {}
|
|
801
|
+
};
|
|
802
|
+
const reasons = [];
|
|
803
|
+
const changedTensors = Number(unlearning.changedTensorCount || 0);
|
|
804
|
+
const changedParams = Number(unlearning.changedParamCount || 0);
|
|
805
|
+
const minChangedTensors = Number(policy.minChangedTensorCount || 0);
|
|
806
|
+
const minChangedParams = Number(policy.minChangedParamCount || 0);
|
|
807
|
+
if (changedTensors < minChangedTensors) {
|
|
808
|
+
reasons.push(`Changed tensor count ${changedTensors} is below minimum ${minChangedTensors}.`);
|
|
809
|
+
}
|
|
810
|
+
if (minChangedParams > 0 && changedParams < minChangedParams) {
|
|
811
|
+
reasons.push(`Changed parameter count ${changedParams} is below minimum ${minChangedParams}.`);
|
|
812
|
+
}
|
|
813
|
+
if (policy.requireModelHashTransition) {
|
|
814
|
+
const before = (unlearning.modelBeforeSha256 || "").trim();
|
|
815
|
+
const after = (unlearning.modelAfterSha256 || "").trim();
|
|
816
|
+
if (!before || !after) {
|
|
817
|
+
reasons.push("Model hash transition is required but before/after hash is missing.");
|
|
818
|
+
} else if (before === after) {
|
|
819
|
+
reasons.push("Model hash did not change after unlearning.");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (policy.requireEvaluation && !evaluation) {
|
|
823
|
+
reasons.push("Evaluation output is required but missing.");
|
|
824
|
+
}
|
|
825
|
+
if (policy.requireLocalization) {
|
|
826
|
+
const localization = unlearning.localization;
|
|
827
|
+
if (!localization) {
|
|
828
|
+
reasons.push("Localization output is required but missing.");
|
|
829
|
+
} else {
|
|
830
|
+
const tokenCoverage = Number(localization.tokenCoverage);
|
|
831
|
+
if (!Number.isFinite(tokenCoverage) || tokenCoverage < 0 || tokenCoverage > 1) {
|
|
832
|
+
reasons.push("Localization tokenCoverage must be a finite number in [0, 1].");
|
|
833
|
+
} else if (tokenCoverage < Number(policy.minLocalizationTokenCoverage)) {
|
|
834
|
+
reasons.push(
|
|
835
|
+
`Localization tokenCoverage ${tokenCoverage} is below minimum ${policy.minLocalizationTokenCoverage}.`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const localizedParamCount = Number(localization.localizedParamCount || 0);
|
|
839
|
+
if (localizedParamCount < Number(policy.minLocalizedParamCount)) {
|
|
840
|
+
reasons.push(
|
|
841
|
+
`Localization localizedParamCount ${localizedParamCount} is below minimum ${policy.minLocalizedParamCount}.`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const confidence = Number(localization.confidence);
|
|
845
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
846
|
+
reasons.push("Localization confidence must be a finite number in [0, 1].");
|
|
847
|
+
} else if (confidence < Number(policy.minLocalizationConfidence)) {
|
|
848
|
+
reasons.push(
|
|
849
|
+
`Localization confidence ${confidence} is below minimum ${policy.minLocalizationConfidence}.`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (evaluation) {
|
|
855
|
+
const maxLeak = Number(policy.maxLeakScore);
|
|
856
|
+
if (!Number.isNaN(maxLeak) && evaluation.leakScore > maxLeak) {
|
|
857
|
+
reasons.push(`Leak score ${evaluation.leakScore} is above max ${maxLeak}.`);
|
|
858
|
+
}
|
|
859
|
+
const allowedVerdicts = policy.allowedVerdicts || [];
|
|
860
|
+
if (allowedVerdicts.length > 0 && !allowedVerdicts.includes(evaluation.finalVerdict)) {
|
|
861
|
+
reasons.push(`Final verdict ${evaluation.finalVerdict} is not allowed by policy.`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
pass: reasons.length === 0,
|
|
866
|
+
reasons,
|
|
867
|
+
policy
|
|
868
|
+
};
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
// src/whitebox.ts
|
|
872
|
+
var buildModelUnlearningPayload = (input) => {
|
|
873
|
+
return {
|
|
874
|
+
claim: input.claim,
|
|
875
|
+
config: {
|
|
876
|
+
target_adapter: input.targetAdapter || "CUSTOM_WHITEBOX_BACKEND",
|
|
877
|
+
target_config: {
|
|
878
|
+
model: input.model
|
|
879
|
+
},
|
|
880
|
+
sensitivity: input.sensitivity || "high",
|
|
881
|
+
parameters: {
|
|
882
|
+
target: input.target,
|
|
883
|
+
plan: input.plan,
|
|
884
|
+
...input.extraParameters
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
};
|
|
889
|
+
var defaultLogger = {
|
|
890
|
+
info(message, meta) {
|
|
891
|
+
console.log(`[whitebox-worker] ${message}`, meta || {});
|
|
892
|
+
},
|
|
893
|
+
warn(message, meta) {
|
|
894
|
+
console.warn(`[whitebox-worker] ${message}`, meta || {});
|
|
895
|
+
},
|
|
896
|
+
error(message, meta) {
|
|
897
|
+
console.error(`[whitebox-worker] ${message}`, meta || {});
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
var safeErrorMessage = (error) => {
|
|
901
|
+
if (error instanceof Error) {
|
|
902
|
+
return error.message.slice(0, 500);
|
|
903
|
+
}
|
|
904
|
+
return String(error).slice(0, 500);
|
|
905
|
+
};
|
|
906
|
+
var toJobError = (error) => {
|
|
907
|
+
const message = safeErrorMessage(error);
|
|
908
|
+
const isValidationFailure = message.startsWith("VALIDATION_ERROR:") || message.startsWith("QUALITY_GATE_FAILED:") || message.startsWith("ADAPTER_OUTPUT_INVALID:");
|
|
909
|
+
return {
|
|
910
|
+
error: {
|
|
911
|
+
code: isValidationFailure ? "VALIDATION_ERROR" : "WORKER_ERROR",
|
|
912
|
+
message
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
};
|
|
916
|
+
var toClaim = (job) => {
|
|
917
|
+
const claim = job.payload?.claim;
|
|
918
|
+
if (!claim) {
|
|
919
|
+
throw new Error("VALIDATION_ERROR: job payload.claim is required.");
|
|
920
|
+
}
|
|
921
|
+
if (typeof claim.claim_payload !== "string" || !claim.claim_payload.trim()) {
|
|
922
|
+
throw new Error("VALIDATION_ERROR: job payload.claim.claim_payload must be a non-empty string.");
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
claim_type: claim.claim_type || "CONCEPT",
|
|
926
|
+
claim_payload: claim.claim_payload,
|
|
927
|
+
scope: claim.scope || "global",
|
|
928
|
+
assertion: claim.assertion || "must_not_be_recalled"
|
|
929
|
+
};
|
|
930
|
+
};
|
|
931
|
+
var toConfig = (job) => {
|
|
932
|
+
const config = job.payload?.config;
|
|
933
|
+
if (!config) {
|
|
934
|
+
throw new Error("VALIDATION_ERROR: job payload.config is required.");
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
target_adapter: config.target_adapter || "CUSTOM_WHITEBOX_BACKEND",
|
|
938
|
+
target_config: config.target_config || {},
|
|
939
|
+
sensitivity: config.sensitivity || "high",
|
|
940
|
+
parameters: config.parameters || {}
|
|
941
|
+
};
|
|
942
|
+
};
|
|
943
|
+
var resolveModel = (config) => {
|
|
944
|
+
const modelFromTargetConfig = config.target_config?.model;
|
|
945
|
+
const modelFromParameters = config.parameters?.model;
|
|
946
|
+
const model = modelFromParameters || modelFromTargetConfig;
|
|
947
|
+
if (!model || !model.uri) {
|
|
948
|
+
throw new Error("MODEL_UNLEARN job missing model URI in config.target_config.model or config.parameters.model");
|
|
949
|
+
}
|
|
950
|
+
return model;
|
|
951
|
+
};
|
|
952
|
+
var resolveTarget = (claim, config) => {
|
|
953
|
+
const targetFromParameters = config.parameters?.target;
|
|
954
|
+
if (targetFromParameters) {
|
|
955
|
+
return targetFromParameters;
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
text: claim.claim_payload,
|
|
959
|
+
selector: {
|
|
960
|
+
claim_type: claim.claim_type,
|
|
961
|
+
scope: claim.scope
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
};
|
|
965
|
+
var resolvePlan = (config) => {
|
|
966
|
+
const planFromParameters = config.parameters?.plan;
|
|
967
|
+
if (planFromParameters?.method) {
|
|
968
|
+
return planFromParameters;
|
|
969
|
+
}
|
|
970
|
+
const fallbackMethod = typeof config.parameters?.method === "string" ? config.parameters.method : "gradient_surgery";
|
|
971
|
+
const fallbackHyperparams = config.parameters?.hyperparameters || {};
|
|
972
|
+
return {
|
|
973
|
+
method: fallbackMethod,
|
|
974
|
+
hyperparameters: fallbackHyperparams
|
|
975
|
+
};
|
|
976
|
+
};
|
|
977
|
+
var isNonArrayRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
978
|
+
var resolveQualityGatePresetCandidate = (value) => {
|
|
979
|
+
if (typeof value !== "string") {
|
|
980
|
+
return void 0;
|
|
981
|
+
}
|
|
982
|
+
const normalized = value.trim().toLowerCase();
|
|
983
|
+
if (normalized === "strict" || normalized === "balanced") {
|
|
984
|
+
return normalized;
|
|
985
|
+
}
|
|
986
|
+
return void 0;
|
|
987
|
+
};
|
|
988
|
+
var resolvePerJobQualityGatePolicy = (workerPolicy, config, plan) => {
|
|
989
|
+
const configParams = isNonArrayRecord(config.parameters) ? config.parameters : {};
|
|
990
|
+
const planHyperparameters = isNonArrayRecord(plan.hyperparameters) ? plan.hyperparameters : {};
|
|
991
|
+
const preset = resolveQualityGatePresetCandidate(
|
|
992
|
+
planHyperparameters.quality_gate_preset || planHyperparameters.qualityGatePreset || configParams.quality_gate_preset || configParams.qualityGatePreset
|
|
993
|
+
);
|
|
994
|
+
const overrideFromPlan = isNonArrayRecord(planHyperparameters.quality_gate_policy) ? planHyperparameters.quality_gate_policy : isNonArrayRecord(planHyperparameters.qualityGatePolicy) ? planHyperparameters.qualityGatePolicy : {};
|
|
995
|
+
const overrideFromConfig = isNonArrayRecord(configParams.quality_gate_policy) ? configParams.quality_gate_policy : isNonArrayRecord(configParams.qualityGatePolicy) ? configParams.qualityGatePolicy : {};
|
|
996
|
+
const hasOverrides = Object.keys(overrideFromPlan).length > 0 || Object.keys(overrideFromConfig).length > 0;
|
|
997
|
+
if (!preset && !hasOverrides) {
|
|
998
|
+
return {
|
|
999
|
+
policy: workerPolicy,
|
|
1000
|
+
preset: null
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const presetPolicy = preset ? resolveWhiteboxQualityGatePolicy(preset) : workerPolicy;
|
|
1004
|
+
return {
|
|
1005
|
+
policy: {
|
|
1006
|
+
...presetPolicy,
|
|
1007
|
+
...overrideFromConfig,
|
|
1008
|
+
...overrideFromPlan
|
|
1009
|
+
},
|
|
1010
|
+
preset: preset || null
|
|
1011
|
+
};
|
|
1012
|
+
};
|
|
1013
|
+
var assertUnlearningOutput = (value) => {
|
|
1014
|
+
const updatedModel = value?.updatedModel;
|
|
1015
|
+
if (!updatedModel || typeof updatedModel.uri !== "string" || !updatedModel.uri.trim()) {
|
|
1016
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: updatedModel.uri must be a non-empty string.");
|
|
1017
|
+
}
|
|
1018
|
+
if (!Number.isFinite(value.changedTensorCount) || value.changedTensorCount < 0) {
|
|
1019
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: changedTensorCount must be a finite number >= 0.");
|
|
1020
|
+
}
|
|
1021
|
+
if (typeof value.changedParamCount !== "undefined" && (!Number.isFinite(value.changedParamCount) || value.changedParamCount < 0)) {
|
|
1022
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: changedParamCount must be a finite number >= 0 when provided.");
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof value.metrics !== "undefined" && !isNonArrayRecord(value.metrics)) {
|
|
1025
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: metrics must be an object when provided.");
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof value.artifacts !== "undefined") {
|
|
1028
|
+
if (!Array.isArray(value.artifacts)) {
|
|
1029
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: artifacts must be an array when provided.");
|
|
1030
|
+
}
|
|
1031
|
+
for (const artifact of value.artifacts) {
|
|
1032
|
+
if (!artifact || typeof artifact.name !== "string" || typeof artifact.uri !== "string") {
|
|
1033
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: each artifact requires string name and uri.");
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
var assertEvaluationOutput = (value) => {
|
|
1039
|
+
if (!value || !["PASS", "FAIL", "REVIEW"].includes(value.finalVerdict)) {
|
|
1040
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: evaluation.finalVerdict must be PASS | FAIL | REVIEW.");
|
|
1041
|
+
}
|
|
1042
|
+
if (!Number.isFinite(value.leakScore) || value.leakScore < 0) {
|
|
1043
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: evaluation.leakScore must be a finite number >= 0.");
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof value.details !== "undefined" && !isNonArrayRecord(value.details)) {
|
|
1046
|
+
throw new Error("ADAPTER_OUTPUT_INVALID: evaluation.details must be an object when provided.");
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
var sha256Hex = (value) => {
|
|
1050
|
+
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
1051
|
+
};
|
|
1052
|
+
var buildResultClaim = (claim, mode) => {
|
|
1053
|
+
if (mode === "raw") {
|
|
1054
|
+
return claim;
|
|
1055
|
+
}
|
|
1056
|
+
const base = {
|
|
1057
|
+
claim_type: claim.claim_type,
|
|
1058
|
+
scope: claim.scope,
|
|
1059
|
+
assertion: claim.assertion
|
|
1060
|
+
};
|
|
1061
|
+
if (mode === "omit") {
|
|
1062
|
+
return base;
|
|
1063
|
+
}
|
|
1064
|
+
const payload = typeof claim.claim_payload === "string" ? claim.claim_payload : "";
|
|
1065
|
+
return {
|
|
1066
|
+
...base,
|
|
1067
|
+
payload_hash: sha256Hex(payload),
|
|
1068
|
+
claim_payload_sha256: sha256Hex(payload),
|
|
1069
|
+
claim_payload_length: payload.length
|
|
1070
|
+
};
|
|
1071
|
+
};
|
|
1072
|
+
var WhiteboxWorker = class {
|
|
1073
|
+
constructor(client, adapter, options) {
|
|
1074
|
+
this.client = client;
|
|
1075
|
+
this.adapter = adapter;
|
|
1076
|
+
this.options = options;
|
|
1077
|
+
this.running = false;
|
|
1078
|
+
this.loopPromise = null;
|
|
1079
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 2e3;
|
|
1080
|
+
this.claimBatchSize = options.claimBatchSize ?? 1;
|
|
1081
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 2e4;
|
|
1082
|
+
this.supportedJobTypes = options.supportedJobTypes ?? ["MODEL_UNLEARN"];
|
|
1083
|
+
this.qualityGatePolicy = {
|
|
1084
|
+
...DEFAULT_WHITEBOX_QUALITY_GATE_POLICY,
|
|
1085
|
+
...options.qualityGatePolicy || {}
|
|
1086
|
+
};
|
|
1087
|
+
this.qualityGateOnFailure = options.qualityGateOnFailure || "fail_job";
|
|
1088
|
+
this.resultClaimMode = options.resultClaimMode || "hash";
|
|
1089
|
+
this.logger = options.logger ?? defaultLogger;
|
|
1090
|
+
}
|
|
1091
|
+
async start() {
|
|
1092
|
+
if (this.running) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
this.running = true;
|
|
1096
|
+
this.logger.info("worker started", {
|
|
1097
|
+
projectId: this.options.projectId,
|
|
1098
|
+
adapter: this.adapter.name,
|
|
1099
|
+
claimBatchSize: this.claimBatchSize
|
|
1100
|
+
});
|
|
1101
|
+
this.loopPromise = this.runLoop();
|
|
1102
|
+
}
|
|
1103
|
+
async stop() {
|
|
1104
|
+
this.running = false;
|
|
1105
|
+
if (this.loopPromise) {
|
|
1106
|
+
await this.loopPromise;
|
|
1107
|
+
this.loopPromise = null;
|
|
1108
|
+
}
|
|
1109
|
+
this.logger.info("worker stopped", { projectId: this.options.projectId });
|
|
1110
|
+
}
|
|
1111
|
+
async runOnce() {
|
|
1112
|
+
const jobs = await this.client.claimJobs(this.options.projectId, {
|
|
1113
|
+
limit: this.claimBatchSize,
|
|
1114
|
+
types: this.supportedJobTypes
|
|
1115
|
+
});
|
|
1116
|
+
if (!jobs.length) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
for (const job of jobs) {
|
|
1120
|
+
await this.processClaimedJob(job);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async runLoop() {
|
|
1124
|
+
while (this.running) {
|
|
1125
|
+
try {
|
|
1126
|
+
await this.runOnce();
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
this.logger.error("worker loop error", {
|
|
1129
|
+
projectId: this.options.projectId,
|
|
1130
|
+
error: safeErrorMessage(error)
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async processClaimedJob(job) {
|
|
1137
|
+
const claimId = typeof job.lastClaimId === "string" ? job.lastClaimId : void 0;
|
|
1138
|
+
if (!this.supportedJobTypes.includes(job.type)) {
|
|
1139
|
+
this.logger.warn("unsupported job type claimed", {
|
|
1140
|
+
jobId: job.id,
|
|
1141
|
+
type: job.type
|
|
1142
|
+
});
|
|
1143
|
+
await this.client.failJob(this.options.projectId, job.id, {
|
|
1144
|
+
error: {
|
|
1145
|
+
code: "VALIDATION_ERROR",
|
|
1146
|
+
message: `Unsupported job type: ${job.type}`
|
|
1147
|
+
}
|
|
1148
|
+
}, { claimId });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const context = {
|
|
1152
|
+
projectId: this.options.projectId,
|
|
1153
|
+
jobId: job.id,
|
|
1154
|
+
correlationId: claimId
|
|
1155
|
+
};
|
|
1156
|
+
let heartbeatTimer = null;
|
|
1157
|
+
const heartbeat = async () => {
|
|
1158
|
+
try {
|
|
1159
|
+
await this.client.heartbeatJob(this.options.projectId, job.id, { claimId });
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
this.logger.warn("heartbeat failed", {
|
|
1162
|
+
jobId: job.id,
|
|
1163
|
+
error: safeErrorMessage(error)
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
try {
|
|
1168
|
+
const claim = toClaim(job);
|
|
1169
|
+
const config = toConfig(job);
|
|
1170
|
+
const model = resolveModel(config);
|
|
1171
|
+
const target = resolveTarget(claim, config);
|
|
1172
|
+
const plan = resolvePlan(config);
|
|
1173
|
+
const perJobQualityGate = resolvePerJobQualityGatePolicy(this.qualityGatePolicy, config, plan);
|
|
1174
|
+
heartbeatTimer = setInterval(heartbeat, this.heartbeatIntervalMs);
|
|
1175
|
+
await heartbeat();
|
|
1176
|
+
const unlearningOutput = await this.adapter.runUnlearning(
|
|
1177
|
+
{ model, target, plan, claim, config },
|
|
1178
|
+
context
|
|
1179
|
+
);
|
|
1180
|
+
assertUnlearningOutput(unlearningOutput);
|
|
1181
|
+
let evaluationOutput;
|
|
1182
|
+
if (this.adapter.runEvaluation) {
|
|
1183
|
+
evaluationOutput = await this.adapter.runEvaluation(
|
|
1184
|
+
{
|
|
1185
|
+
model: unlearningOutput.updatedModel,
|
|
1186
|
+
baselineModel: model,
|
|
1187
|
+
claim,
|
|
1188
|
+
config,
|
|
1189
|
+
probes: config.parameters?.probes || []
|
|
1190
|
+
},
|
|
1191
|
+
context
|
|
1192
|
+
);
|
|
1193
|
+
assertEvaluationOutput(evaluationOutput);
|
|
1194
|
+
}
|
|
1195
|
+
const qualityGate = evaluateWhiteboxQualityGate(
|
|
1196
|
+
unlearningOutput,
|
|
1197
|
+
evaluationOutput,
|
|
1198
|
+
perJobQualityGate.policy
|
|
1199
|
+
);
|
|
1200
|
+
if (!qualityGate.pass && this.qualityGateOnFailure === "fail_job") {
|
|
1201
|
+
throw new Error(`QUALITY_GATE_FAILED: ${qualityGate.reasons.join(" | ")}`);
|
|
1202
|
+
}
|
|
1203
|
+
const finalVerdict = qualityGate.pass ? evaluationOutput?.finalVerdict || "REVIEW" : "REVIEW";
|
|
1204
|
+
const result = {
|
|
1205
|
+
result: {
|
|
1206
|
+
worker: {
|
|
1207
|
+
adapter: this.adapter.name,
|
|
1208
|
+
mode: "whitebox"
|
|
1209
|
+
},
|
|
1210
|
+
claim: buildResultClaim(claim, this.resultClaimMode),
|
|
1211
|
+
plan,
|
|
1212
|
+
unlearning: unlearningOutput,
|
|
1213
|
+
evaluation: evaluationOutput || null,
|
|
1214
|
+
quality_gate: qualityGate,
|
|
1215
|
+
quality_gate_preset: perJobQualityGate.preset,
|
|
1216
|
+
final_verdict: finalVerdict,
|
|
1217
|
+
report_hash: unlearningOutput.modelAfterSha256 || null,
|
|
1218
|
+
privacy: {
|
|
1219
|
+
claim_mode: this.resultClaimMode
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
await this.client.completeJob(this.options.projectId, job.id, result, { claimId });
|
|
1224
|
+
this.logger.info("job completed", {
|
|
1225
|
+
jobId: job.id,
|
|
1226
|
+
type: job.type,
|
|
1227
|
+
adapter: this.adapter.name
|
|
1228
|
+
});
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
this.logger.error("job failed", {
|
|
1231
|
+
jobId: job.id,
|
|
1232
|
+
type: job.type,
|
|
1233
|
+
error: safeErrorMessage(error)
|
|
1234
|
+
});
|
|
1235
|
+
await this.client.failJob(this.options.projectId, job.id, toJobError(error), { claimId });
|
|
1236
|
+
} finally {
|
|
1237
|
+
if (heartbeatTimer) {
|
|
1238
|
+
clearInterval(heartbeatTimer);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/native-adapters.ts
|
|
1245
|
+
var import_node_child_process = require("child_process");
|
|
1246
|
+
var DEFAULT_ADAPTER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
1247
|
+
var LocalCommandTrainingAdapter = class {
|
|
1248
|
+
constructor(options) {
|
|
1249
|
+
this.options = options;
|
|
1250
|
+
this.name = options.name || "local-command-adapter";
|
|
1251
|
+
}
|
|
1252
|
+
async runUnlearning(input, context) {
|
|
1253
|
+
const spec = resolveCommandSpec(this.options.unlearning, input, context);
|
|
1254
|
+
const output = await runJsonCommand(
|
|
1255
|
+
spec,
|
|
1256
|
+
{
|
|
1257
|
+
mode: "unlearning",
|
|
1258
|
+
input,
|
|
1259
|
+
context
|
|
1260
|
+
},
|
|
1261
|
+
this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
|
|
1262
|
+
);
|
|
1263
|
+
return output;
|
|
1264
|
+
}
|
|
1265
|
+
async runEvaluation(input, context) {
|
|
1266
|
+
if (!this.options.evaluation) {
|
|
1267
|
+
throw new Error("LocalCommandTrainingAdapter evaluation command is not configured.");
|
|
1268
|
+
}
|
|
1269
|
+
const spec = resolveCommandSpec(this.options.evaluation, input, context);
|
|
1270
|
+
const output = await runJsonCommand(
|
|
1271
|
+
spec,
|
|
1272
|
+
{
|
|
1273
|
+
mode: "evaluation",
|
|
1274
|
+
input,
|
|
1275
|
+
context
|
|
1276
|
+
},
|
|
1277
|
+
this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
|
|
1278
|
+
);
|
|
1279
|
+
return output;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
var HttpTrainingAdapter = class {
|
|
1283
|
+
constructor(options) {
|
|
1284
|
+
this.options = options;
|
|
1285
|
+
this.name = options.name || "http-training-adapter";
|
|
1286
|
+
}
|
|
1287
|
+
async runUnlearning(input, context) {
|
|
1288
|
+
const output = await postJson(
|
|
1289
|
+
this.options.unlearningUrl,
|
|
1290
|
+
{
|
|
1291
|
+
mode: "unlearning",
|
|
1292
|
+
input,
|
|
1293
|
+
context
|
|
1294
|
+
},
|
|
1295
|
+
this.options.headers,
|
|
1296
|
+
this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
|
|
1297
|
+
);
|
|
1298
|
+
return output;
|
|
1299
|
+
}
|
|
1300
|
+
async runEvaluation(input, context) {
|
|
1301
|
+
if (!this.options.evaluationUrl) {
|
|
1302
|
+
throw new Error("HttpTrainingAdapter evaluationUrl is not configured.");
|
|
1303
|
+
}
|
|
1304
|
+
const output = await postJson(
|
|
1305
|
+
this.options.evaluationUrl,
|
|
1306
|
+
{
|
|
1307
|
+
mode: "evaluation",
|
|
1308
|
+
input,
|
|
1309
|
+
context
|
|
1310
|
+
},
|
|
1311
|
+
this.options.headers,
|
|
1312
|
+
this.options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS
|
|
1313
|
+
);
|
|
1314
|
+
return output;
|
|
1315
|
+
}
|
|
447
1316
|
};
|
|
1317
|
+
var createNativeTrainingAdapter = (config) => {
|
|
1318
|
+
if (config.provider === "local_command") {
|
|
1319
|
+
const { provider: _provider2, ...rest2 } = config;
|
|
1320
|
+
return new LocalCommandTrainingAdapter(rest2);
|
|
1321
|
+
}
|
|
1322
|
+
const { provider: _provider, ...rest } = config;
|
|
1323
|
+
return new HttpTrainingAdapter(rest);
|
|
1324
|
+
};
|
|
1325
|
+
function resolveCommandSpec(resolver, input, context) {
|
|
1326
|
+
const resolved = typeof resolver === "function" ? resolver(input, context) : resolver;
|
|
1327
|
+
if (!resolved?.command) {
|
|
1328
|
+
throw new Error("Adapter command is missing.");
|
|
1329
|
+
}
|
|
1330
|
+
return resolved;
|
|
1331
|
+
}
|
|
1332
|
+
async function runJsonCommand(spec, payload, defaultTimeoutMs) {
|
|
1333
|
+
const timeoutMs = spec.timeoutMs ?? defaultTimeoutMs;
|
|
1334
|
+
return new Promise((resolve, reject) => {
|
|
1335
|
+
const child = (0, import_node_child_process.spawn)(spec.command, spec.args || [], {
|
|
1336
|
+
cwd: spec.cwd,
|
|
1337
|
+
env: { ...process.env, ...spec.env || {} },
|
|
1338
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1339
|
+
});
|
|
1340
|
+
let stdout = "";
|
|
1341
|
+
let stderr = "";
|
|
1342
|
+
let timedOut = false;
|
|
1343
|
+
let hardKillTimer = null;
|
|
1344
|
+
const timer = setTimeout(() => {
|
|
1345
|
+
timedOut = true;
|
|
1346
|
+
child.kill("SIGTERM");
|
|
1347
|
+
hardKillTimer = setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
1348
|
+
}, timeoutMs);
|
|
1349
|
+
child.stdout.on("data", (chunk) => {
|
|
1350
|
+
stdout += chunk.toString();
|
|
1351
|
+
});
|
|
1352
|
+
child.stderr.on("data", (chunk) => {
|
|
1353
|
+
stderr += chunk.toString();
|
|
1354
|
+
});
|
|
1355
|
+
child.on("error", (error) => {
|
|
1356
|
+
clearTimeout(timer);
|
|
1357
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
1358
|
+
reject(error);
|
|
1359
|
+
});
|
|
1360
|
+
child.on("close", (code) => {
|
|
1361
|
+
clearTimeout(timer);
|
|
1362
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
1363
|
+
if (timedOut) {
|
|
1364
|
+
reject(new Error(`Adapter command timed out after ${timeoutMs}ms: ${spec.command}`));
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (code !== 0) {
|
|
1368
|
+
reject(
|
|
1369
|
+
new Error(
|
|
1370
|
+
`Adapter command failed (${code}): ${spec.command} ${(spec.args || []).join(" ")}; stderr=${stderr.trim()}`
|
|
1371
|
+
)
|
|
1372
|
+
);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const trimmed = stdout.trim();
|
|
1376
|
+
if (!trimmed) {
|
|
1377
|
+
reject(new Error(`Adapter command returned empty stdout: ${spec.command}`));
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
try {
|
|
1381
|
+
resolve(JSON.parse(trimmed));
|
|
1382
|
+
} catch {
|
|
1383
|
+
reject(new Error(`Adapter command output is not valid JSON: ${trimmed.slice(0, 800)}`));
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
if (spec.stdinMode !== "none") {
|
|
1387
|
+
try {
|
|
1388
|
+
child.stdin.write(JSON.stringify(payload));
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
clearTimeout(timer);
|
|
1391
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
1392
|
+
reject(error);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
child.stdin.end();
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
async function postJson(url, body, headers, timeoutMs) {
|
|
1400
|
+
const controller = new AbortController();
|
|
1401
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1402
|
+
try {
|
|
1403
|
+
const response = await fetch(url, {
|
|
1404
|
+
method: "POST",
|
|
1405
|
+
headers: {
|
|
1406
|
+
"content-type": "application/json",
|
|
1407
|
+
...headers || {}
|
|
1408
|
+
},
|
|
1409
|
+
body: JSON.stringify(body),
|
|
1410
|
+
signal: controller.signal
|
|
1411
|
+
});
|
|
1412
|
+
const text = await response.text();
|
|
1413
|
+
if (!response.ok) {
|
|
1414
|
+
throw new Error(`HTTP adapter request failed (${response.status}): ${text.slice(0, 800)}`);
|
|
1415
|
+
}
|
|
1416
|
+
if (!text.trim()) {
|
|
1417
|
+
throw new Error(`HTTP adapter returned empty response: ${url}`);
|
|
1418
|
+
}
|
|
1419
|
+
try {
|
|
1420
|
+
return JSON.parse(text);
|
|
1421
|
+
} catch {
|
|
1422
|
+
throw new Error(`HTTP adapter returned non-JSON response: ${text.slice(0, 800)}`);
|
|
1423
|
+
}
|
|
1424
|
+
} finally {
|
|
1425
|
+
clearTimeout(timeout);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/deployment-profiles.ts
|
|
1430
|
+
var ENTERPRISE_STRICT_QUALITY_GATE_POLICY = {
|
|
1431
|
+
...WHITEBOX_QUALITY_GATE_PRESETS.strict
|
|
1432
|
+
};
|
|
1433
|
+
function assertDeploymentCompatibility(options) {
|
|
1434
|
+
const normalizedHost = parseHost(options.apiUrl);
|
|
1435
|
+
if (!normalizedHost) {
|
|
1436
|
+
throw new Error(`Invalid FORG3T_API_URL: ${options.apiUrl}`);
|
|
1437
|
+
}
|
|
1438
|
+
if (options.profile !== "customer_airgapped") {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (options.adapterMode === "http" && !options.allowHttpInAirgapped) {
|
|
1442
|
+
throw new Error(
|
|
1443
|
+
"Air-gapped profile rejects adapterMode=http by default. Use local_command or explicitly set allowHttpInAirgapped=true for trusted internal gateways."
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
if (!isHostAllowedForAirgapped(normalizedHost, options.allowedApiHosts || [])) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
`Air-gapped profile requires a private/control-plane host. Received ${normalizedHost}. Set allowedApiHosts for private DNS hosts if needed.`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function buildWhiteboxWorkerOptions(profile, baseOptions) {
|
|
1453
|
+
const strictMode = profile === "customer_airgapped" || profile === "customer_online";
|
|
1454
|
+
const defaultPolicy = strictMode ? ENTERPRISE_STRICT_QUALITY_GATE_POLICY : DEFAULT_WHITEBOX_QUALITY_GATE_POLICY;
|
|
1455
|
+
const defaultSupportedTypes = ["MODEL_UNLEARN"];
|
|
1456
|
+
return {
|
|
1457
|
+
projectId: baseOptions.projectId,
|
|
1458
|
+
pollIntervalMs: baseOptions.pollIntervalMs ?? 2e3,
|
|
1459
|
+
claimBatchSize: baseOptions.claimBatchSize ?? 1,
|
|
1460
|
+
heartbeatIntervalMs: baseOptions.heartbeatIntervalMs ?? 2e4,
|
|
1461
|
+
supportedJobTypes: baseOptions.supportedJobTypes ?? defaultSupportedTypes,
|
|
1462
|
+
qualityGatePolicy: baseOptions.qualityGatePolicy ?? defaultPolicy,
|
|
1463
|
+
qualityGateOnFailure: baseOptions.qualityGateOnFailure ?? "fail_job",
|
|
1464
|
+
resultClaimMode: baseOptions.resultClaimMode ?? "hash",
|
|
1465
|
+
logger: baseOptions.logger
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
function parseHost(apiUrl) {
|
|
1469
|
+
try {
|
|
1470
|
+
const parsed = new URL(apiUrl);
|
|
1471
|
+
return parsed.hostname.toLowerCase();
|
|
1472
|
+
} catch {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
function isHostAllowedForAirgapped(host, allowedApiHosts) {
|
|
1477
|
+
if (isPrivateHost(host)) {
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
const allowSet = new Set(allowedApiHosts.map((item) => item.trim().toLowerCase()).filter(Boolean));
|
|
1481
|
+
return allowSet.has(host);
|
|
1482
|
+
}
|
|
1483
|
+
function isPrivateHost(host) {
|
|
1484
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
if (host.endsWith(".local") || host.endsWith(".internal") || host.endsWith(".lan")) {
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
const parts = host.split(".");
|
|
1491
|
+
if (parts.length !== 4 || parts.some((part) => Number.isNaN(Number(part)))) {
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
const octets = parts.map((part) => Number(part));
|
|
1495
|
+
const [a, b] = octets;
|
|
1496
|
+
if (a === 10) return true;
|
|
1497
|
+
if (a === 127) return true;
|
|
1498
|
+
if (a === 192 && b === 168) return true;
|
|
1499
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
448
1502
|
// Annotate the CommonJS export names for ESM import in node:
|
|
449
1503
|
0 && (module.exports = {
|
|
1504
|
+
AccessLevel,
|
|
1505
|
+
DEFAULT_WHITEBOX_QUALITY_GATE_POLICY,
|
|
1506
|
+
ENTERPRISE_STRICT_QUALITY_GATE_POLICY,
|
|
450
1507
|
Forg3tApiConnectionError,
|
|
451
1508
|
Forg3tAuthenticationError,
|
|
452
1509
|
Forg3tClient,
|
|
453
1510
|
Forg3tError,
|
|
454
1511
|
Forg3tNotFoundError,
|
|
455
|
-
|
|
1512
|
+
Forg3tNotImplementedError,
|
|
1513
|
+
Forg3tRateLimitError,
|
|
1514
|
+
HttpTrainingAdapter,
|
|
1515
|
+
IntegrationMode,
|
|
1516
|
+
IntegrationType,
|
|
1517
|
+
LocalCommandTrainingAdapter,
|
|
1518
|
+
WHITEBOX_QUALITY_GATE_PRESETS,
|
|
1519
|
+
WHITEBOX_QUALITY_GATE_PRESET_EXPLANATIONS,
|
|
1520
|
+
WhiteboxWorker,
|
|
1521
|
+
assertDeploymentCompatibility,
|
|
1522
|
+
buildModelUnlearningPayload,
|
|
1523
|
+
buildWhiteboxWorkerOptions,
|
|
1524
|
+
createNativeTrainingAdapter,
|
|
1525
|
+
evaluateWhiteboxQualityGate,
|
|
1526
|
+
resolveWhiteboxQualityGatePolicy
|
|
456
1527
|
});
|