@voce-engine/core 0.1.0-rc.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/LICENSE +1 -0
- package/README.md +9 -0
- package/dist/canonical.d.ts +4 -0
- package/dist/canonical.js +30 -0
- package/dist/evidence.d.ts +31 -0
- package/dist/evidence.js +1053 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +608 -0
- package/dist/m4.d.ts +157 -0
- package/dist/m4.js +2250 -0
- package/dist/m5.d.ts +183 -0
- package/dist/m5.js +2433 -0
- package/dist/m6.d.ts +182 -0
- package/dist/m6.js +1183 -0
- package/package.json +50 -0
package/dist/m6.js
ADDED
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { ARTIFACT_REPLAY_RESULT_SCHEMA_VERSION, COMPARISON_ENTRY_SCHEMA_VERSION, COMPARISON_REPORT_SCHEMA_VERSION, EVALUATION_REPORT_SCHEMA_VERSION, HUMAN_ACCEPTANCE_ANNOTATION_SCHEMA_VERSION, HUMAN_ACCEPTANCE_DECISION_SCHEMA_VERSION, PROVIDER_ERROR_SCHEMA_VERSION, PROVIDER_REQUEST_ENVELOPE_SCHEMA_VERSION, PROVIDER_RESPONSE_ENVELOPE_SCHEMA_VERSION, PROVIDER_SUBMISSION_LOOKUP_SCHEMA_VERSION, REPORT_ARTIFACT_SCHEMA_VERSION, SEMANTIC_REVIEW_FINDING_SCHEMA_VERSION, SEMANTIC_REVIEW_REPORT_SCHEMA_VERSION, SEMANTIC_REVIEW_REQUEST_SCHEMA_VERSION, STATIC_TRACE_REPORT_MODEL_SCHEMA_VERSION, STRUCTURAL_VALIDATION_FINDING_SCHEMA_VERSION, STRUCTURAL_VALIDATION_INPUT_SCHEMA_VERSION, STRUCTURAL_VALIDATION_REPORT_SCHEMA_VERSION, } from '@voce-engine/contracts';
|
|
5
|
+
import { canonicalize, sha256 } from './canonical.js';
|
|
6
|
+
import { computeRemoteCallAuthorizationHash, dispatchPreflight } from './m4.js';
|
|
7
|
+
export const M6_RUNTIME_VERSION = 'voce.adapters-evaluation-runtime/v1alpha1';
|
|
8
|
+
export const STATIC_REPORT_VERSION = 'voce.static-trace-report/v1alpha1';
|
|
9
|
+
export const FIXED_M6_TIME = '2026-01-01T00:00:00.000Z';
|
|
10
|
+
const HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
11
|
+
const SAFE_URL_PATTERN = /^https?:\/\//i;
|
|
12
|
+
const SECRET_PATTERN = /(authorization\s*:\s*bearer\s+|bearer\s+|api[-_ ]?key\s*[:=]\s*|secret\s*[:=]\s*|sk-[A-Za-z0-9_-]+|x-[A-Za-z0-9-]+-key\s*[:=]\s*)[^\s<>&"']+/gi;
|
|
13
|
+
const DATA_URI_PATTERN = /data:[^,;\s]+(?:;[^,\s]+)*,[^\s<>&"']+/gi;
|
|
14
|
+
const SIGNED_URL_PATTERN = /https?:\/\/[^\s<>&"']+[?&](?:signature|sig|token|expires|credential|security[-_]?token|x[-_]tos[-_](?:signature|credential|security[-_]?token)|x[-_]amz-[^=]+)=[^\s<>&"']+/gi;
|
|
15
|
+
const DATA_URI_DETECTION_PATTERN = /^data:[^,;\s]+(?:;[^,\s]+)*,/i;
|
|
16
|
+
const BASE64_LIKE_PATTERN = /^[A-Za-z0-9+/]{80,}={0,2}$/;
|
|
17
|
+
const ABSOLUTE_PATH_PATTERN = /(?:[A-Za-z]:\\|\\\\|\/Users\/|\/home\/|\/tmp\/)[^\s<>&"']+/g;
|
|
18
|
+
export class ProviderTransportError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
safeDetails;
|
|
21
|
+
constructor(code, message, safeDetails) {
|
|
22
|
+
super(`${code}: ${message}`);
|
|
23
|
+
this.name = 'ProviderTransportError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.safeDetails = safeDetails;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function compareCodeUnits(left, right) {
|
|
29
|
+
const length = Math.min(left.length, right.length);
|
|
30
|
+
for (let index = 0; index < length; index += 1) {
|
|
31
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
32
|
+
if (difference !== 0)
|
|
33
|
+
return difference;
|
|
34
|
+
}
|
|
35
|
+
return left.length - right.length;
|
|
36
|
+
}
|
|
37
|
+
function jsonReady(value) {
|
|
38
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
39
|
+
return value;
|
|
40
|
+
if (typeof value === 'number') {
|
|
41
|
+
if (!Number.isFinite(value))
|
|
42
|
+
throw new ProviderTransportError('INPUT_INVALID', 'Numeric input is not finite.');
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value))
|
|
46
|
+
return value.map((item) => jsonReady(item === undefined ? null : item));
|
|
47
|
+
if (value && typeof value === 'object') {
|
|
48
|
+
if (value instanceof Uint8Array)
|
|
49
|
+
return Array.from(value);
|
|
50
|
+
const object = {};
|
|
51
|
+
for (const [key, item] of Object.entries(value)) {
|
|
52
|
+
if (item !== undefined)
|
|
53
|
+
object[key] = jsonReady(item);
|
|
54
|
+
}
|
|
55
|
+
return object;
|
|
56
|
+
}
|
|
57
|
+
throw new ProviderTransportError('INPUT_INVALID', 'Input is not JSON-compatible.');
|
|
58
|
+
}
|
|
59
|
+
function clone(value) {
|
|
60
|
+
return JSON.parse(JSON.stringify(jsonReady(value)));
|
|
61
|
+
}
|
|
62
|
+
function sortedStrings(values) {
|
|
63
|
+
return [...new Set(values ?? [])].sort(compareCodeUnits);
|
|
64
|
+
}
|
|
65
|
+
function sortedBy(values, key) {
|
|
66
|
+
return values.map((value) => clone(value)).sort((left, right) => compareCodeUnits(key(left), key(right)) || compareCodeUnits(canonicalize(jsonReady(left)), canonicalize(jsonReady(right))));
|
|
67
|
+
}
|
|
68
|
+
function isHash(value) {
|
|
69
|
+
return typeof value === 'string' && HASH_PATTERN.test(value);
|
|
70
|
+
}
|
|
71
|
+
function hashId(prefix, value) {
|
|
72
|
+
return `${prefix}-${sha256(jsonReady(value)).slice('sha256:'.length, 'sha256:'.length + 24)}`;
|
|
73
|
+
}
|
|
74
|
+
function without(value, field) {
|
|
75
|
+
const result = jsonReady(value);
|
|
76
|
+
if (result === null || typeof result !== 'object' || Array.isArray(result))
|
|
77
|
+
return {};
|
|
78
|
+
delete result[field];
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function binarySha256(bytes) {
|
|
82
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
83
|
+
}
|
|
84
|
+
export function computeArtifactBytesHash(bytes) { return binarySha256(bytes); }
|
|
85
|
+
function safeMessage(message) {
|
|
86
|
+
return message.replace(SECRET_PATTERN, '[REDACTED]').replace(DATA_URI_PATTERN, '[REDACTED_DATA]').replace(SIGNED_URL_PATTERN, '[REDACTED_URL]').replace(ABSOLUTE_PATH_PATTERN, '[REDACTED_PATH]');
|
|
87
|
+
}
|
|
88
|
+
function safeJson(value) {
|
|
89
|
+
if (typeof value === 'string') {
|
|
90
|
+
if (BASE64_LIKE_PATTERN.test(value))
|
|
91
|
+
return '[REDACTED_BASE64]';
|
|
92
|
+
return safeMessage(value);
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(value))
|
|
95
|
+
return value.map((item) => safeJson(item));
|
|
96
|
+
if (value && typeof value === 'object') {
|
|
97
|
+
const object = {};
|
|
98
|
+
for (const [key, item] of Object.entries(value)) {
|
|
99
|
+
if (/authorization|credential|secret|api.?key|base64|b64_json|signed.?url|local.?path|security.?token|x[-_]?tos|x[-_]?amz|signature/i.test(key))
|
|
100
|
+
object[key] = '[REDACTED]';
|
|
101
|
+
else
|
|
102
|
+
object[key] = safeJson(item);
|
|
103
|
+
}
|
|
104
|
+
return object;
|
|
105
|
+
}
|
|
106
|
+
return (value ?? null);
|
|
107
|
+
}
|
|
108
|
+
function sensitiveHashProjection(value, kind) {
|
|
109
|
+
return { kind, valueHash: sha256(value), length: value.length };
|
|
110
|
+
}
|
|
111
|
+
function hashJson(value, key) {
|
|
112
|
+
if (typeof value === 'string') {
|
|
113
|
+
if (key && /authorization|credential|secret|api.?key|base64|b64_json|signed.?url|security.?token|x[-_]?tos|x[-_]?amz|signature/i.test(key))
|
|
114
|
+
return sensitiveHashProjection(value, 'sensitive-string');
|
|
115
|
+
if (DATA_URI_DETECTION_PATTERN.test(value))
|
|
116
|
+
return sensitiveHashProjection(value, 'data-uri');
|
|
117
|
+
SIGNED_URL_PATTERN.lastIndex = 0;
|
|
118
|
+
if (SIGNED_URL_PATTERN.test(value))
|
|
119
|
+
return sensitiveHashProjection(value, 'signed-url');
|
|
120
|
+
if (BASE64_LIKE_PATTERN.test(value))
|
|
121
|
+
return sensitiveHashProjection(value, 'base64-like');
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
125
|
+
return value;
|
|
126
|
+
if (Array.isArray(value))
|
|
127
|
+
return value.map((item) => hashJson(item, key));
|
|
128
|
+
if (value && typeof value === 'object') {
|
|
129
|
+
if (value instanceof Uint8Array)
|
|
130
|
+
return sensitiveHashProjection(Buffer.from(value).toString('base64'), 'bytes');
|
|
131
|
+
const object = {};
|
|
132
|
+
for (const [childKey, item] of Object.entries(value))
|
|
133
|
+
object[childKey] = hashJson(item, childKey);
|
|
134
|
+
return object;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function assertHash(value, code = 'HASH_INVALID') {
|
|
139
|
+
if (!isHash(value))
|
|
140
|
+
throw new ProviderTransportError(code, 'A supplied content hash is invalid.');
|
|
141
|
+
}
|
|
142
|
+
function normalizeProviderRequestProjection(request) {
|
|
143
|
+
return jsonReady({
|
|
144
|
+
schemaVersion: PROVIDER_REQUEST_ENVELOPE_SCHEMA_VERSION,
|
|
145
|
+
id: request.id,
|
|
146
|
+
adapterId: request.adapterId,
|
|
147
|
+
adapterDigest: request.adapterDigest,
|
|
148
|
+
profileId: request.profileId,
|
|
149
|
+
profileDigest: request.profileDigest,
|
|
150
|
+
...(request.modelId === undefined ? {} : { modelId: request.modelId }),
|
|
151
|
+
...(request.modelVersion === undefined ? {} : { modelVersion: request.modelVersion }),
|
|
152
|
+
stepId: request.stepId,
|
|
153
|
+
destination: request.destination,
|
|
154
|
+
...(request.region === undefined ? {} : { region: request.region }),
|
|
155
|
+
purpose: request.purpose,
|
|
156
|
+
inputHash: request.inputHash,
|
|
157
|
+
inputArtifactHashes: sortedStrings(request.inputArtifactHashes),
|
|
158
|
+
dataCategories: sortedStrings(request.dataCategories),
|
|
159
|
+
maximumCalls: request.maximumCalls,
|
|
160
|
+
maximumRetries: request.maximumRetries,
|
|
161
|
+
timeoutMs: request.timeoutMs,
|
|
162
|
+
...(request.maximumBytes === undefined ? {} : { maximumBytes: request.maximumBytes }),
|
|
163
|
+
...(request.maximumCost === undefined ? {} : { maximumCost: request.maximumCost }),
|
|
164
|
+
idempotencyKey: request.idempotencyKey,
|
|
165
|
+
payload: hashJson(request.payload),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
export function computeProviderRequestEnvelopeHash(request) {
|
|
169
|
+
return sha256(normalizeProviderRequestProjection(request));
|
|
170
|
+
}
|
|
171
|
+
function normalizeProviderResponseProjection(response) {
|
|
172
|
+
return jsonReady({
|
|
173
|
+
schemaVersion: PROVIDER_RESPONSE_ENVELOPE_SCHEMA_VERSION,
|
|
174
|
+
requestHash: response.requestHash,
|
|
175
|
+
status: response.status,
|
|
176
|
+
...(response.providerRequestId === undefined ? {} : { providerRequestId: response.providerRequestId }),
|
|
177
|
+
...(response.body === undefined ? {} : { body: hashJson(response.body) }),
|
|
178
|
+
outputArtifactIds: sortedStrings(response.outputArtifactIds),
|
|
179
|
+
...(response.error === undefined ? {} : { error: normalizeProviderErrorProjection(response.error) }),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export function computeProviderResponseEnvelopeHash(response) {
|
|
183
|
+
return sha256(normalizeProviderResponseProjection(response));
|
|
184
|
+
}
|
|
185
|
+
function normalizeProviderErrorProjection(error) {
|
|
186
|
+
return jsonReady({
|
|
187
|
+
schemaVersion: PROVIDER_ERROR_SCHEMA_VERSION,
|
|
188
|
+
code: error.code,
|
|
189
|
+
message: safeMessage(error.message),
|
|
190
|
+
retryable: error.retryable,
|
|
191
|
+
submissionUnknown: error.submissionUnknown,
|
|
192
|
+
...(error.safeDetails === undefined ? {} : { safeDetails: hashJson(error.safeDetails) }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
export function computeProviderErrorHash(error) {
|
|
196
|
+
return sha256(normalizeProviderErrorProjection(error));
|
|
197
|
+
}
|
|
198
|
+
function normalizeLookupProjection(request) {
|
|
199
|
+
return jsonReady({
|
|
200
|
+
schemaVersion: PROVIDER_SUBMISSION_LOOKUP_SCHEMA_VERSION,
|
|
201
|
+
requestId: request.requestId,
|
|
202
|
+
adapterId: request.adapterId,
|
|
203
|
+
adapterDigest: request.adapterDigest,
|
|
204
|
+
profileId: request.profileId,
|
|
205
|
+
profileDigest: request.profileDigest,
|
|
206
|
+
...(request.modelId === undefined ? {} : { modelId: request.modelId }),
|
|
207
|
+
...(request.modelVersion === undefined ? {} : { modelVersion: request.modelVersion }),
|
|
208
|
+
destination: request.destination,
|
|
209
|
+
...(request.region === undefined ? {} : { region: request.region }),
|
|
210
|
+
stepId: request.stepId,
|
|
211
|
+
purpose: request.purpose,
|
|
212
|
+
...(request.providerRequestId === undefined ? {} : { providerRequestId: request.providerRequestId }),
|
|
213
|
+
requestHash: request.requestHash,
|
|
214
|
+
idempotencyKey: request.idempotencyKey,
|
|
215
|
+
inputHash: request.inputHash,
|
|
216
|
+
inputArtifactHashes: sortedStrings(request.inputArtifactHashes),
|
|
217
|
+
dataCategories: sortedStrings(request.dataCategories),
|
|
218
|
+
maximumCalls: request.maximumCalls,
|
|
219
|
+
maximumRetries: request.maximumRetries,
|
|
220
|
+
timeoutMs: request.timeoutMs,
|
|
221
|
+
...(request.maximumBytes === undefined ? {} : { maximumBytes: request.maximumBytes }),
|
|
222
|
+
...(request.maximumCost === undefined ? {} : { maximumCost: request.maximumCost }),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export function computeProviderSubmissionLookupHash(request) {
|
|
226
|
+
return sha256(normalizeLookupProjection(request));
|
|
227
|
+
}
|
|
228
|
+
function errorResponse(requestHash, code, message, retryable = false, submissionUnknown = false) {
|
|
229
|
+
const errorBase = {
|
|
230
|
+
schemaVersion: PROVIDER_ERROR_SCHEMA_VERSION,
|
|
231
|
+
code,
|
|
232
|
+
message: safeMessage(message),
|
|
233
|
+
retryable,
|
|
234
|
+
submissionUnknown,
|
|
235
|
+
};
|
|
236
|
+
const error = { ...errorBase, errorHash: computeProviderErrorHash(errorBase) };
|
|
237
|
+
const base = { schemaVersion: PROVIDER_RESPONSE_ENVELOPE_SCHEMA_VERSION, requestHash, status: submissionUnknown ? 'submission_unknown' : 'failed', outputArtifactIds: [], error };
|
|
238
|
+
return { ...base, responseHash: computeProviderResponseEnvelopeHash(base) };
|
|
239
|
+
}
|
|
240
|
+
export class DisabledProviderTransport {
|
|
241
|
+
id = 'voce.disabled-transport';
|
|
242
|
+
mode = 'offline';
|
|
243
|
+
async send(request, context) {
|
|
244
|
+
assertRemoteCallAuthorization(request, context);
|
|
245
|
+
return errorResponse(request.requestHash, 'PROVIDER_TRANSPORT_DISABLED', 'Provider transport is disabled in offline mode.');
|
|
246
|
+
}
|
|
247
|
+
async lookup(request, context) {
|
|
248
|
+
assertRemoteCallAuthorization(request, context);
|
|
249
|
+
return errorResponse(request.requestHash, 'PROVIDER_TRANSPORT_DISABLED', 'Provider transport is disabled in offline mode.');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function assertRemoteCallAuthorization(request, context) {
|
|
253
|
+
const authorization = context.authorization;
|
|
254
|
+
if (!context.credential || !context.credential.ref || !context.credential.value)
|
|
255
|
+
throw new ProviderTransportError('ADAPTER_CREDENTIAL_MISSING', 'Host credential injection is missing.');
|
|
256
|
+
if (!authorization || computeRemoteCallAuthorizationHash(authorization) !== authorization.authorizationHash)
|
|
257
|
+
throw new ProviderTransportError('REMOTE_CALL_AUTHORIZATION_INVALID', 'Remote call authorization is invalid.');
|
|
258
|
+
const isRequest = 'payload' in request;
|
|
259
|
+
if (isRequest && computeProviderRequestEnvelopeHash(request) !== request.requestHash)
|
|
260
|
+
throw new ProviderTransportError('PROVIDER_REQUEST_HASH_MISMATCH', 'Provider request hash is invalid.');
|
|
261
|
+
if (!isRequest && computeProviderSubmissionLookupHash(request) !== request.lookupHash)
|
|
262
|
+
throw new ProviderTransportError('PROVIDER_LOOKUP_HASH_MISMATCH', 'Submission lookup hash is invalid.');
|
|
263
|
+
const reasons = [];
|
|
264
|
+
if (authorization.stepId !== request.stepId)
|
|
265
|
+
reasons.push('stepId');
|
|
266
|
+
if (authorization.purpose !== request.purpose)
|
|
267
|
+
reasons.push('purpose');
|
|
268
|
+
if (authorization.adapterId !== request.adapterId)
|
|
269
|
+
reasons.push('adapterId');
|
|
270
|
+
if (authorization.adapterDigest !== request.adapterDigest)
|
|
271
|
+
reasons.push('adapterDigest');
|
|
272
|
+
if (authorization.profileDigest !== request.profileDigest)
|
|
273
|
+
reasons.push('profileDigest');
|
|
274
|
+
if (authorization.modelId !== request.modelId)
|
|
275
|
+
reasons.push('modelId');
|
|
276
|
+
if (authorization.modelVersion !== request.modelVersion)
|
|
277
|
+
reasons.push('modelVersion');
|
|
278
|
+
if (authorization.destination !== request.destination || authorization.region !== request.region)
|
|
279
|
+
reasons.push('destination');
|
|
280
|
+
if (authorization.inputHash !== request.inputHash)
|
|
281
|
+
reasons.push('inputHash');
|
|
282
|
+
if (authorization.idempotencyKey !== request.idempotencyKey)
|
|
283
|
+
reasons.push('idempotencyKey');
|
|
284
|
+
if (authorization.maximumCalls !== request.maximumCalls || authorization.maximumRetries !== request.maximumRetries || authorization.timeoutMs !== request.timeoutMs)
|
|
285
|
+
reasons.push('budget');
|
|
286
|
+
if (authorization.maximumBytes !== request.maximumBytes || authorization.maximumCost !== request.maximumCost)
|
|
287
|
+
reasons.push('limits');
|
|
288
|
+
if (canonicalize(jsonReady(sortedStrings(authorization.permittedArtifactHashes))) !== canonicalize(jsonReady(sortedStrings(request.inputArtifactHashes))))
|
|
289
|
+
reasons.push('artifactHashes');
|
|
290
|
+
if ('dataCategories' in request && canonicalize(jsonReady(sortedStrings(authorization.dataCategories))) !== canonicalize(jsonReady(sortedStrings(request.dataCategories))))
|
|
291
|
+
reasons.push('dataCategories');
|
|
292
|
+
if (!isRequest && request.requestId.length === 0)
|
|
293
|
+
reasons.push('requestId');
|
|
294
|
+
if (reasons.length)
|
|
295
|
+
throw new ProviderTransportError('REMOTE_CALL_AUTHORIZATION_SCOPE_MISMATCH', 'Remote call authorization scope does not match the provider envelope.', { fields: reasons });
|
|
296
|
+
const preflight = dispatchPreflight(authorization, {
|
|
297
|
+
kind: 'remote_call',
|
|
298
|
+
caseId: authorization.caseId,
|
|
299
|
+
caseRevision: authorization.caseRevision,
|
|
300
|
+
contextHash: authorization.contextHash,
|
|
301
|
+
stepId: authorization.stepId,
|
|
302
|
+
purpose: authorization.purpose,
|
|
303
|
+
inputHash: authorization.inputHash,
|
|
304
|
+
inputManifestHash: authorization.inputManifestHash,
|
|
305
|
+
modelId: authorization.modelId,
|
|
306
|
+
modelVersion: authorization.modelVersion,
|
|
307
|
+
permittedArtifactHashes: authorization.permittedArtifactHashes,
|
|
308
|
+
permittedScopeIds: authorization.permittedScopeIds,
|
|
309
|
+
constraintIds: authorization.constraintIds,
|
|
310
|
+
adapterId: authorization.adapterId,
|
|
311
|
+
adapterDigest: authorization.adapterDigest,
|
|
312
|
+
profileDigest: authorization.profileDigest,
|
|
313
|
+
destination: authorization.destination,
|
|
314
|
+
region: authorization.region,
|
|
315
|
+
dataCategories: authorization.dataCategories,
|
|
316
|
+
maximumCalls: authorization.maximumCalls,
|
|
317
|
+
maximumRetries: authorization.maximumRetries,
|
|
318
|
+
maximumBytes: authorization.maximumBytes,
|
|
319
|
+
timeoutMs: authorization.timeoutMs,
|
|
320
|
+
maximumCost: authorization.maximumCost,
|
|
321
|
+
currency: authorization.currency,
|
|
322
|
+
idempotencyKey: authorization.idempotencyKey,
|
|
323
|
+
});
|
|
324
|
+
if (preflight.status !== 'authorized')
|
|
325
|
+
throw new ProviderTransportError('REMOTE_CALL_NOT_AUTHORIZED', 'Remote call preflight was blocked.');
|
|
326
|
+
}
|
|
327
|
+
function providerBindingMatches(request, lookup) {
|
|
328
|
+
return request.id === lookup.requestId
|
|
329
|
+
&& request.adapterId === lookup.adapterId
|
|
330
|
+
&& request.adapterDigest === lookup.adapterDigest
|
|
331
|
+
&& request.profileId === lookup.profileId
|
|
332
|
+
&& request.profileDigest === lookup.profileDigest
|
|
333
|
+
&& request.modelId === lookup.modelId
|
|
334
|
+
&& request.modelVersion === lookup.modelVersion
|
|
335
|
+
&& request.destination === lookup.destination
|
|
336
|
+
&& request.region === lookup.region
|
|
337
|
+
&& request.stepId === lookup.stepId
|
|
338
|
+
&& request.purpose === lookup.purpose
|
|
339
|
+
&& request.requestHash === lookup.requestHash
|
|
340
|
+
&& request.idempotencyKey === lookup.idempotencyKey
|
|
341
|
+
&& request.inputHash === lookup.inputHash
|
|
342
|
+
&& canonicalize(jsonReady(sortedStrings(request.inputArtifactHashes))) === canonicalize(jsonReady(sortedStrings(lookup.inputArtifactHashes)))
|
|
343
|
+
&& canonicalize(jsonReady(sortedStrings(request.dataCategories))) === canonicalize(jsonReady(sortedStrings(lookup.dataCategories)))
|
|
344
|
+
&& request.maximumCalls === lookup.maximumCalls
|
|
345
|
+
&& request.maximumRetries === lookup.maximumRetries
|
|
346
|
+
&& request.timeoutMs === lookup.timeoutMs
|
|
347
|
+
&& request.maximumBytes === lookup.maximumBytes
|
|
348
|
+
&& request.maximumCost === lookup.maximumCost;
|
|
349
|
+
}
|
|
350
|
+
export function createProviderSubmissionLookup(request, providerRequestId) {
|
|
351
|
+
const base = {
|
|
352
|
+
schemaVersion: PROVIDER_SUBMISSION_LOOKUP_SCHEMA_VERSION,
|
|
353
|
+
requestId: request.id,
|
|
354
|
+
adapterId: request.adapterId,
|
|
355
|
+
adapterDigest: request.adapterDigest,
|
|
356
|
+
profileId: request.profileId,
|
|
357
|
+
profileDigest: request.profileDigest,
|
|
358
|
+
...(request.modelId === undefined ? {} : { modelId: request.modelId }),
|
|
359
|
+
...(request.modelVersion === undefined ? {} : { modelVersion: request.modelVersion }),
|
|
360
|
+
destination: request.destination,
|
|
361
|
+
...(request.region === undefined ? {} : { region: request.region }),
|
|
362
|
+
stepId: request.stepId,
|
|
363
|
+
purpose: request.purpose,
|
|
364
|
+
...(providerRequestId === undefined ? {} : { providerRequestId }),
|
|
365
|
+
requestHash: request.requestHash,
|
|
366
|
+
idempotencyKey: request.idempotencyKey,
|
|
367
|
+
inputHash: request.inputHash,
|
|
368
|
+
inputArtifactHashes: sortedStrings(request.inputArtifactHashes),
|
|
369
|
+
dataCategories: sortedStrings(request.dataCategories),
|
|
370
|
+
maximumCalls: request.maximumCalls,
|
|
371
|
+
maximumRetries: request.maximumRetries,
|
|
372
|
+
timeoutMs: request.timeoutMs,
|
|
373
|
+
...(request.maximumBytes === undefined ? {} : { maximumBytes: request.maximumBytes }),
|
|
374
|
+
...(request.maximumCost === undefined ? {} : { maximumCost: request.maximumCost }),
|
|
375
|
+
};
|
|
376
|
+
return clone({ ...base, lookupHash: computeProviderSubmissionLookupHash(base) });
|
|
377
|
+
}
|
|
378
|
+
function safeResponse(response) {
|
|
379
|
+
if (computeProviderResponseEnvelopeHash(response) !== response.responseHash)
|
|
380
|
+
throw new ProviderTransportError('PROVIDER_RESPONSE_HASH_MISMATCH', 'Provider response hash is invalid.');
|
|
381
|
+
if (!isHash(response.requestHash))
|
|
382
|
+
throw new ProviderTransportError('PROVIDER_RESPONSE_INVALID', 'Provider response request hash is invalid.');
|
|
383
|
+
if (response.error && computeProviderErrorHash(response.error) !== response.error.errorHash)
|
|
384
|
+
throw new ProviderTransportError('PROVIDER_ERROR_HASH_MISMATCH', 'Provider error hash is invalid.');
|
|
385
|
+
return clone(response);
|
|
386
|
+
}
|
|
387
|
+
function publicResponseReceipt(response) {
|
|
388
|
+
const safeError = response.error === undefined ? undefined : (() => {
|
|
389
|
+
const errorBase = {
|
|
390
|
+
schemaVersion: PROVIDER_ERROR_SCHEMA_VERSION,
|
|
391
|
+
code: response.error.code,
|
|
392
|
+
message: safeMessage(response.error.message),
|
|
393
|
+
retryable: response.error.retryable,
|
|
394
|
+
submissionUnknown: response.error.submissionUnknown,
|
|
395
|
+
...(response.error.safeDetails === undefined ? {} : { safeDetails: safeJson(response.error.safeDetails) }),
|
|
396
|
+
};
|
|
397
|
+
return { ...errorBase, errorHash: computeProviderErrorHash(errorBase) };
|
|
398
|
+
})();
|
|
399
|
+
const base = {
|
|
400
|
+
schemaVersion: PROVIDER_RESPONSE_ENVELOPE_SCHEMA_VERSION,
|
|
401
|
+
requestHash: response.requestHash,
|
|
402
|
+
status: response.status,
|
|
403
|
+
...(response.providerRequestId === undefined ? {} : { providerRequestId: response.providerRequestId }),
|
|
404
|
+
outputArtifactIds: sortedStrings(response.outputArtifactIds),
|
|
405
|
+
...(safeError === undefined ? {} : { error: safeError }),
|
|
406
|
+
};
|
|
407
|
+
return clone({ ...base, responseHash: computeProviderResponseEnvelopeHash(base) });
|
|
408
|
+
}
|
|
409
|
+
export class RecordingMockTransport {
|
|
410
|
+
id = 'voce.recording-mock-transport';
|
|
411
|
+
mode = 'offline';
|
|
412
|
+
calls = [];
|
|
413
|
+
lookupCalls = [];
|
|
414
|
+
responses;
|
|
415
|
+
lookups;
|
|
416
|
+
sentRequests = new Map();
|
|
417
|
+
sendCounts = new Map();
|
|
418
|
+
lookupCounts = new Map();
|
|
419
|
+
constructor(options = {}) {
|
|
420
|
+
if (Array.isArray(options)) {
|
|
421
|
+
this.responses = options.map((item) => clone(item));
|
|
422
|
+
this.lookups = [];
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
this.responses = (options.responses ?? []).map((item) => clone(item));
|
|
426
|
+
this.lookups = (options.lookups ?? []).map((item) => clone(item));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
enqueue(response) { this.responses.push(clone(response)); }
|
|
430
|
+
enqueueLookup(response) { this.lookups.push(clone(response)); }
|
|
431
|
+
consume(kind, authorization) {
|
|
432
|
+
const key = `${authorization.id}:${authorization.idempotencyKey}`;
|
|
433
|
+
const counts = kind === 'send' ? this.sendCounts : this.lookupCounts;
|
|
434
|
+
const count = counts.get(key) ?? 0;
|
|
435
|
+
if (count >= authorization.maximumCalls)
|
|
436
|
+
throw new ProviderTransportError('REMOTE_CALL_BUDGET_EXHAUSTED', `Remote ${kind} budget has been exhausted.`);
|
|
437
|
+
counts.set(key, count + 1);
|
|
438
|
+
}
|
|
439
|
+
async send(request, context) {
|
|
440
|
+
assertRemoteCallAuthorization(request, context);
|
|
441
|
+
this.consume('send', context.authorization);
|
|
442
|
+
const identity = `${context.authorization.id}:${context.authorization.idempotencyKey}`;
|
|
443
|
+
const prior = this.sentRequests.get(identity);
|
|
444
|
+
if (prior && !providerBindingMatches(prior, createProviderSubmissionLookup(request)))
|
|
445
|
+
throw new ProviderTransportError('REMOTE_CALL_IDENTITY_REUSED', 'Idempotency identity cannot be reused for a different provider request.');
|
|
446
|
+
this.sentRequests.set(identity, clone(request));
|
|
447
|
+
this.calls.push({ requestId: request.id, requestHash: request.requestHash, adapterId: request.adapterId, profileDigest: request.profileDigest, destination: request.destination, ...(request.region === undefined ? {} : { region: request.region }), inputHash: request.inputHash, idempotencyKey: request.idempotencyKey });
|
|
448
|
+
const response = this.responses.shift() ?? errorResponse(request.requestHash, 'MOCK_RESPONSE_MISSING', 'Recording mock response was not registered.');
|
|
449
|
+
const normalized = response.requestHash === request.requestHash ? response : { ...response, requestHash: request.requestHash, responseHash: '' };
|
|
450
|
+
if (!normalized.responseHash)
|
|
451
|
+
normalized.responseHash = computeProviderResponseEnvelopeHash(normalized);
|
|
452
|
+
return safeResponse(normalized);
|
|
453
|
+
}
|
|
454
|
+
async lookup(request, context) {
|
|
455
|
+
assertRemoteCallAuthorization(request, context);
|
|
456
|
+
this.consume('lookup', context.authorization);
|
|
457
|
+
const identity = `${context.authorization.id}:${context.authorization.idempotencyKey}`;
|
|
458
|
+
const original = this.sentRequests.get(identity);
|
|
459
|
+
if (!original || !providerBindingMatches(original, request))
|
|
460
|
+
throw new ProviderTransportError('PROVIDER_LOOKUP_BINDING_MISMATCH', 'Submission lookup is not bound to a prior provider request.');
|
|
461
|
+
this.lookupCalls.push({ requestId: request.providerRequestId ?? request.lookupHash, requestHash: request.requestHash, adapterId: request.adapterId, profileDigest: request.profileDigest, destination: request.destination, ...(request.region === undefined ? {} : { region: request.region }), inputHash: request.inputHash, idempotencyKey: request.idempotencyKey });
|
|
462
|
+
const response = this.lookups.shift() ?? errorResponse(request.requestHash, 'MOCK_LOOKUP_RESPONSE_MISSING', 'Recording lookup response was not registered.');
|
|
463
|
+
const normalized = response.requestHash === request.requestHash ? response : { ...response, requestHash: request.requestHash, responseHash: '' };
|
|
464
|
+
if (!normalized.responseHash)
|
|
465
|
+
normalized.responseHash = computeProviderResponseEnvelopeHash(normalized);
|
|
466
|
+
return safeResponse(normalized);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function validateEndpointProfile(endpoint, endpointProfile) {
|
|
470
|
+
if (!endpointProfile)
|
|
471
|
+
return;
|
|
472
|
+
let hostname;
|
|
473
|
+
try {
|
|
474
|
+
hostname = new URL(endpoint).hostname.toLowerCase();
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
throw new ProviderTransportError('ADAPTER_ENDPOINT_INVALID', 'Provider endpoint is not a valid URL.');
|
|
478
|
+
}
|
|
479
|
+
const expected = endpointProfile === 'domestic' ? 'volces.com' : 'bytepluses.com';
|
|
480
|
+
if (!(hostname === expected || hostname.endsWith(`.${expected}`)))
|
|
481
|
+
throw new ProviderTransportError('ADAPTER_ENDPOINT_PROFILE_MISMATCH', 'Provider endpoint host does not match the configured endpoint profile.');
|
|
482
|
+
}
|
|
483
|
+
function validateAdapterConfig(config) {
|
|
484
|
+
if (!config.endpoint || !SAFE_URL_PATTERN.test(config.endpoint))
|
|
485
|
+
throw new ProviderTransportError('ADAPTER_ENDPOINT_MISSING', 'Provider endpoint is missing or invalid.');
|
|
486
|
+
validateEndpointProfile(config.endpoint, config.endpointProfile);
|
|
487
|
+
if (!config.credentialRef)
|
|
488
|
+
throw new ProviderTransportError('ADAPTER_CREDENTIAL_MISSING', 'Provider credential reference is missing.');
|
|
489
|
+
if (!config.model)
|
|
490
|
+
throw new ProviderTransportError('ADAPTER_MODEL_MISSING', 'Provider model is missing.');
|
|
491
|
+
if (!config.modelVersion)
|
|
492
|
+
throw new ProviderTransportError('ADAPTER_MODEL_VERSION_MISSING', 'Provider model version is missing.');
|
|
493
|
+
if (!config.adapter?.id || !isHash(config.adapter.digest))
|
|
494
|
+
throw new ProviderTransportError('ADAPTER_VERSION_INVALID', 'Adapter version or digest is invalid.');
|
|
495
|
+
if (!config.profile?.id || !isHash(config.profile.digest))
|
|
496
|
+
throw new ProviderTransportError('ADAPTER_PROFILE_INVALID', 'Provider profile or digest is invalid.');
|
|
497
|
+
if (!config.destination)
|
|
498
|
+
throw new ProviderTransportError('ADAPTER_DESTINATION_MISSING', 'Provider destination is missing.');
|
|
499
|
+
if (config.destination !== config.endpoint)
|
|
500
|
+
throw new ProviderTransportError('ADAPTER_DESTINATION_ENDPOINT_MISMATCH', 'Provider destination must be the exact configured endpoint used by the transport.');
|
|
501
|
+
}
|
|
502
|
+
function imageInputs(value) {
|
|
503
|
+
if (value === undefined)
|
|
504
|
+
return [];
|
|
505
|
+
return Array.isArray(value) ? value : [value];
|
|
506
|
+
}
|
|
507
|
+
function imageBytes(value, references) {
|
|
508
|
+
if (value instanceof Uint8Array)
|
|
509
|
+
return value;
|
|
510
|
+
if (typeof value === 'string') {
|
|
511
|
+
const match = value.match(/^data:([^;,]+);base64,(.+)$/i);
|
|
512
|
+
if (!match)
|
|
513
|
+
return undefined;
|
|
514
|
+
try {
|
|
515
|
+
return Uint8Array.from(Buffer.from(match[2], 'base64'));
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const reference = references.find((item) => item.artifact.id === value.id);
|
|
522
|
+
return reference?.bytes;
|
|
523
|
+
}
|
|
524
|
+
function mediaTypeOf(value, references) {
|
|
525
|
+
if (value instanceof Uint8Array) {
|
|
526
|
+
const header = parseImageHeader(value);
|
|
527
|
+
return header.format === 'png' ? 'image/png' : header.format === 'jpeg' ? 'image/jpeg' : header.format === 'webp' ? 'image/webp' : undefined;
|
|
528
|
+
}
|
|
529
|
+
if (typeof value === 'object' && !(value instanceof Uint8Array))
|
|
530
|
+
return value.mediaType;
|
|
531
|
+
if (typeof value === 'string')
|
|
532
|
+
return value.match(/^data:([^;,]+)/i)?.[1];
|
|
533
|
+
return references.find((item) => item.bytes === value)?.artifact.mediaType;
|
|
534
|
+
}
|
|
535
|
+
function toProviderImage(value, references = []) {
|
|
536
|
+
if (typeof value === 'string') {
|
|
537
|
+
if (SAFE_URL_PATTERN.test(value))
|
|
538
|
+
return value;
|
|
539
|
+
if (DATA_URI_DETECTION_PATTERN.test(value))
|
|
540
|
+
return value;
|
|
541
|
+
throw new ProviderTransportError('SEEDREAM_IMAGE_REFERENCE_INVALID', 'Seedream image string must be an http(s) URL or image data URI.');
|
|
542
|
+
}
|
|
543
|
+
if (value instanceof Uint8Array) {
|
|
544
|
+
const mediaType = mediaTypeOf(value, []);
|
|
545
|
+
if (!mediaType || !['image/png', 'image/jpeg'].includes(mediaType))
|
|
546
|
+
throw new ProviderTransportError('SEEDREAM_IMAGE_MEDIA_TYPE_UNKNOWN', 'Seedream image bytes do not have a supported PNG or JPEG signature.');
|
|
547
|
+
return `data:${mediaType};base64,${Buffer.from(value).toString('base64')}`;
|
|
548
|
+
}
|
|
549
|
+
const bytes = imageBytes(value, references);
|
|
550
|
+
if (!bytes)
|
|
551
|
+
throw new ProviderTransportError('SEEDREAM_ARTIFACT_UNRESOLVED', 'Seedream ArtifactHandle must be resolved before request construction.');
|
|
552
|
+
assertHash(value.contentHash, 'SEEDREAM_ARTIFACT_HASH_INVALID');
|
|
553
|
+
if (binarySha256(bytes) !== value.contentHash)
|
|
554
|
+
throw new ProviderTransportError('ARTIFACT_HASH_MISMATCH', 'Seedream reference bytes do not match the ArtifactHandle content hash.');
|
|
555
|
+
const mediaType = mediaTypeOf(bytes, []);
|
|
556
|
+
if (!mediaType || mediaType !== value.mediaType || !['image/png', 'image/jpeg'].includes(mediaType))
|
|
557
|
+
throw new ProviderTransportError('SEEDREAM_ARTIFACT_MEDIA_TYPE_MISMATCH', 'Seedream ArtifactHandle bytes do not have a supported matching media type.');
|
|
558
|
+
return `data:${mediaType};base64,${Buffer.from(bytes).toString('base64')}`;
|
|
559
|
+
}
|
|
560
|
+
function pngHeader(bytes) {
|
|
561
|
+
if (bytes.length < 33 || ![137, 80, 78, 71, 13, 10, 26, 10].every((value, index) => bytes[index] === value))
|
|
562
|
+
return undefined;
|
|
563
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
564
|
+
if (view.getUint32(12) !== 0x49484452)
|
|
565
|
+
return undefined;
|
|
566
|
+
const colorType = bytes[25];
|
|
567
|
+
let alpha = colorType === 4 || colorType === 6;
|
|
568
|
+
let offset = 8;
|
|
569
|
+
while (offset + 12 <= bytes.length) {
|
|
570
|
+
const length = view.getUint32(offset);
|
|
571
|
+
if (offset + length + 12 > bytes.length)
|
|
572
|
+
break;
|
|
573
|
+
const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
|
|
574
|
+
if (type === 'tRNS')
|
|
575
|
+
alpha = true;
|
|
576
|
+
offset += length + 12;
|
|
577
|
+
if (type === 'IEND')
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
return { width: view.getUint32(16), height: view.getUint32(20), alpha };
|
|
581
|
+
}
|
|
582
|
+
function seedreamAllowedKeys() {
|
|
583
|
+
return new Set(['prompt', 'image', 'n', 'output_format', 'size', 'watermark', 'referenceArtifacts']);
|
|
584
|
+
}
|
|
585
|
+
function validateSeedreamInput(input) {
|
|
586
|
+
if (!input || typeof input.prompt !== 'string' || !input.prompt.trim())
|
|
587
|
+
throw new ProviderTransportError('SEEDREAM_PROMPT_MISSING', 'Seedream prompt is missing.');
|
|
588
|
+
for (const key of Object.keys(input))
|
|
589
|
+
if (!seedreamAllowedKeys().has(key))
|
|
590
|
+
throw new ProviderTransportError('SEEDREAM_FIELD_UNSUPPORTED', 'Seedream request contains an unsupported field.');
|
|
591
|
+
if ('sequential_image_generation' in input)
|
|
592
|
+
throw new ProviderTransportError('SEEDREAM_SEQUENTIAL_IMAGE_GENERATION_FORBIDDEN', 'Sequential image generation is not allowed.');
|
|
593
|
+
if (input.n !== undefined && input.n !== 1)
|
|
594
|
+
throw new ProviderTransportError('SEEDREAM_CARDINALITY_INVALID', 'Seedream requires n=1.');
|
|
595
|
+
if (input.output_format !== undefined && input.output_format !== 'png' && input.output_format !== 'jpeg')
|
|
596
|
+
throw new ProviderTransportError('SEEDREAM_OUTPUT_FORMAT_UNSUPPORTED', 'Seedream output_format must be png or jpeg.');
|
|
597
|
+
if (input.watermark !== undefined && typeof input.watermark !== 'boolean')
|
|
598
|
+
throw new ProviderTransportError('SEEDREAM_WATERMARK_INVALID', 'Seedream watermark must be a boolean.');
|
|
599
|
+
const images = imageInputs(input.image);
|
|
600
|
+
if (images.length > 10)
|
|
601
|
+
throw new ProviderTransportError('SEEDREAM_REFERENCE_LIMIT_EXCEEDED', 'Seedream accepts at most ten reference images.');
|
|
602
|
+
for (const image of images) {
|
|
603
|
+
if (typeof image === 'string' && !SAFE_URL_PATTERN.test(image) && !DATA_URI_DETECTION_PATTERN.test(image))
|
|
604
|
+
throw new ProviderTransportError('SEEDREAM_IMAGE_REFERENCE_INVALID', 'Seedream image string must be an http(s) URL or image data URI.');
|
|
605
|
+
if (image instanceof Uint8Array && !mediaTypeOf(image, []))
|
|
606
|
+
throw new ProviderTransportError('SEEDREAM_IMAGE_MEDIA_TYPE_UNKNOWN', 'Seedream image bytes do not have a recognized media signature.');
|
|
607
|
+
}
|
|
608
|
+
return images;
|
|
609
|
+
}
|
|
610
|
+
export function validateSeedreamConfig(config) { validateAdapterConfig(config); }
|
|
611
|
+
async function resolveSeedreamInput(input, config) {
|
|
612
|
+
const references = (input.referenceArtifacts ?? []).map((item) => ({ artifact: clone(item.artifact), ...(item.bytes === undefined ? {} : { bytes: new Uint8Array(item.bytes) }) }));
|
|
613
|
+
const images = imageInputs(input.image);
|
|
614
|
+
const resolved = await Promise.all(images.map(async (image) => {
|
|
615
|
+
if (!(image && typeof image === 'object' && !(image instanceof Uint8Array) && 'contentHash' in image))
|
|
616
|
+
return image;
|
|
617
|
+
const reference = references.find((item) => item.artifact.id === image.id);
|
|
618
|
+
const bytes = reference?.bytes ?? (config.resolver ? await config.resolver(image) : config.assetSink.resolve ? await config.assetSink.resolve(image) : undefined);
|
|
619
|
+
if (!bytes)
|
|
620
|
+
throw new ProviderTransportError('ARTIFACT_UNAVAILABLE', 'Seedream reference artifact could not be resolved.');
|
|
621
|
+
assertHash(image.contentHash, 'SEEDREAM_ARTIFACT_HASH_INVALID');
|
|
622
|
+
if (binarySha256(bytes) !== image.contentHash)
|
|
623
|
+
throw new ProviderTransportError('ARTIFACT_HASH_MISMATCH', 'Seedream reference bytes do not match the ArtifactHandle content hash.');
|
|
624
|
+
const actualMediaType = mediaTypeOf(bytes, []);
|
|
625
|
+
if (!actualMediaType || actualMediaType !== image.mediaType)
|
|
626
|
+
throw new ProviderTransportError('SEEDREAM_ARTIFACT_MEDIA_TYPE_MISMATCH', 'Seedream reference bytes do not match the ArtifactHandle media type.');
|
|
627
|
+
const existing = references.find((item) => item.artifact.id === image.id);
|
|
628
|
+
if (existing)
|
|
629
|
+
existing.bytes = new Uint8Array(bytes);
|
|
630
|
+
else
|
|
631
|
+
references.push({ artifact: clone(image), bytes: new Uint8Array(bytes) });
|
|
632
|
+
return clone(image);
|
|
633
|
+
}));
|
|
634
|
+
return { ...input, ...(input.image === undefined ? {} : { image: Array.isArray(input.image) ? resolved : resolved[0] }), referenceArtifacts: references };
|
|
635
|
+
}
|
|
636
|
+
export function buildSeedreamRequest(input, config, authorization) {
|
|
637
|
+
validateAdapterConfig(config);
|
|
638
|
+
const images = validateSeedreamInput(input);
|
|
639
|
+
const payload = { model: config.model, prompt: input.prompt, n: 1 };
|
|
640
|
+
if (images.length === 1)
|
|
641
|
+
payload.image = toProviderImage(images[0], input.referenceArtifacts ?? []);
|
|
642
|
+
if (images.length > 1)
|
|
643
|
+
payload.image = images.map((image) => toProviderImage(image, input.referenceArtifacts ?? []));
|
|
644
|
+
if (input.output_format !== undefined)
|
|
645
|
+
payload.output_format = input.output_format;
|
|
646
|
+
if (input.size !== undefined)
|
|
647
|
+
payload.size = input.size;
|
|
648
|
+
if (input.watermark !== undefined)
|
|
649
|
+
payload.watermark = input.watermark;
|
|
650
|
+
const artifactHashes = sortedStrings([
|
|
651
|
+
...images.filter((value) => Boolean(value && typeof value === 'object' && !(value instanceof Uint8Array) && 'contentHash' in value)).map((value) => value.contentHash),
|
|
652
|
+
...(input.referenceArtifacts ?? []).filter((item) => images.some((value) => typeof value === 'object' && !(value instanceof Uint8Array) && value.id === item.artifact.id)).map((item) => item.artifact.contentHash),
|
|
653
|
+
]);
|
|
654
|
+
artifactHashes.forEach((hash) => assertHash(hash, 'SEEDREAM_ARTIFACT_HASH_INVALID'));
|
|
655
|
+
const inputHash = authorization?.inputHash ?? sha256(jsonReady({ adapter: config.adapter, profile: config.profile, model: config.model, input: jsonReady({ ...input, referenceArtifacts: (input.referenceArtifacts ?? []).map((item) => item.artifact) }) }));
|
|
656
|
+
const base = {
|
|
657
|
+
schemaVersion: PROVIDER_REQUEST_ENVELOPE_SCHEMA_VERSION,
|
|
658
|
+
id: hashId('seedream-request', { inputHash, idempotencyKey: authorization?.idempotencyKey ?? 'unbound' }),
|
|
659
|
+
adapterId: config.adapter.id,
|
|
660
|
+
adapterDigest: config.adapter.digest,
|
|
661
|
+
profileId: config.profile.id,
|
|
662
|
+
profileDigest: config.profile.digest,
|
|
663
|
+
modelId: config.model,
|
|
664
|
+
modelVersion: config.modelVersion,
|
|
665
|
+
stepId: authorization?.stepId ?? 'unbound-seedream-step',
|
|
666
|
+
destination: config.destination,
|
|
667
|
+
...(config.region === undefined ? {} : { region: config.region }),
|
|
668
|
+
purpose: 'generation',
|
|
669
|
+
inputHash,
|
|
670
|
+
inputArtifactHashes: sortedStrings(artifactHashes),
|
|
671
|
+
dataCategories: ['prompt', ...(artifactHashes.length ? ['reference_image'] : [])],
|
|
672
|
+
maximumCalls: authorization?.maximumCalls ?? 1,
|
|
673
|
+
maximumRetries: authorization?.maximumRetries ?? 0,
|
|
674
|
+
timeoutMs: authorization?.timeoutMs ?? 60_000,
|
|
675
|
+
...(authorization?.maximumBytes === undefined ? {} : { maximumBytes: authorization.maximumBytes }),
|
|
676
|
+
...(authorization?.maximumCost === undefined ? {} : { maximumCost: authorization.maximumCost }),
|
|
677
|
+
idempotencyKey: authorization?.idempotencyKey ?? hashId('seedream-idempotency', { inputHash }),
|
|
678
|
+
payload,
|
|
679
|
+
};
|
|
680
|
+
return clone({ ...base, requestHash: computeProviderRequestEnvelopeHash(base) });
|
|
681
|
+
}
|
|
682
|
+
function responseItems(body) {
|
|
683
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
684
|
+
return [];
|
|
685
|
+
const object = body;
|
|
686
|
+
const candidate = Array.isArray(object.data) ? object.data : Array.isArray(object.output) ? object.output : [object];
|
|
687
|
+
return candidate.filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item))).map((item) => ({
|
|
688
|
+
...(typeof item.url === 'string' ? { url: item.url } : {}),
|
|
689
|
+
...(typeof item.b64_json === 'string' ? { b64_json: item.b64_json } : {}),
|
|
690
|
+
...(typeof item.base64 === 'string' ? { base64: item.base64 } : {}),
|
|
691
|
+
...(typeof item.mediaType === 'string' ? { mediaType: item.mediaType } : {}),
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
async function persistProviderItem(item, sink, role) {
|
|
695
|
+
if (item.url) {
|
|
696
|
+
if (!sink.putRemote)
|
|
697
|
+
throw new ProviderTransportError('ARTIFACT_PERSISTENCE_FAILURE', 'Provider output URL was not saved by the host asset sink.');
|
|
698
|
+
const artifact = await sink.putRemote({ url: item.url, mediaType: item.mediaType ?? 'image/png', role });
|
|
699
|
+
if (!artifact)
|
|
700
|
+
throw new ProviderTransportError('ARTIFACT_PERSISTENCE_FAILURE', 'Provider output URL could not be saved by the host asset sink.');
|
|
701
|
+
assertHash(artifact.contentHash, 'ARTIFACT_HANDLE_HASH_INVALID');
|
|
702
|
+
return clone(artifact);
|
|
703
|
+
}
|
|
704
|
+
const encoded = item.b64_json ?? item.base64;
|
|
705
|
+
if (!encoded)
|
|
706
|
+
throw new ProviderTransportError('PROVIDER_OUTPUT_MISSING', 'Provider response did not contain a persistable image.');
|
|
707
|
+
let bytes;
|
|
708
|
+
try {
|
|
709
|
+
bytes = Uint8Array.from(Buffer.from(encoded, 'base64'));
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
throw new ProviderTransportError('PROVIDER_OUTPUT_INVALID', 'Provider image payload could not be decoded.');
|
|
713
|
+
}
|
|
714
|
+
if (!bytes.length)
|
|
715
|
+
throw new ProviderTransportError('PROVIDER_OUTPUT_INVALID', 'Provider image payload is empty.');
|
|
716
|
+
const detected = mediaTypeOf(bytes, []);
|
|
717
|
+
if (!detected)
|
|
718
|
+
throw new ProviderTransportError('PROVIDER_OUTPUT_INVALID', 'Provider image payload has an unsupported media signature.');
|
|
719
|
+
if (item.mediaType !== undefined && item.mediaType !== detected)
|
|
720
|
+
throw new ProviderTransportError('PROVIDER_OUTPUT_MEDIA_TYPE_MISMATCH', 'Provider image media type does not match its bytes.');
|
|
721
|
+
const mediaType = item.mediaType ?? detected;
|
|
722
|
+
const artifact = await sink.put({ bytes, mediaType, role, sourceHash: binarySha256(bytes) });
|
|
723
|
+
assertHash(artifact.contentHash, 'ARTIFACT_HANDLE_HASH_INVALID');
|
|
724
|
+
return clone(artifact);
|
|
725
|
+
}
|
|
726
|
+
export class SeedreamAdapter {
|
|
727
|
+
config;
|
|
728
|
+
id;
|
|
729
|
+
version;
|
|
730
|
+
digest;
|
|
731
|
+
profileDigest;
|
|
732
|
+
offline;
|
|
733
|
+
transport;
|
|
734
|
+
constructor(config) {
|
|
735
|
+
this.config = config;
|
|
736
|
+
validateAdapterConfig(config);
|
|
737
|
+
this.id = config.adapter.id;
|
|
738
|
+
this.version = clone(config.adapter);
|
|
739
|
+
this.digest = config.adapter.digest;
|
|
740
|
+
this.profileDigest = config.profile.digest;
|
|
741
|
+
this.offline = (config.transport ?? new DisabledProviderTransport()).mode === 'offline';
|
|
742
|
+
this.transport = config.transport ?? new DisabledProviderTransport();
|
|
743
|
+
}
|
|
744
|
+
buildRequest(input, authorization) { return buildSeedreamRequest(input, this.config, authorization); }
|
|
745
|
+
async generate(input, authorization, context = {}) {
|
|
746
|
+
let requestHash = authorization.inputHash;
|
|
747
|
+
try {
|
|
748
|
+
const preparedInput = await resolveSeedreamInput(input, this.config);
|
|
749
|
+
const request = this.buildRequest(preparedInput, authorization);
|
|
750
|
+
requestHash = request.requestHash;
|
|
751
|
+
if (!context.credential || context.credential.ref !== this.config.credentialRef || !context.credential.value)
|
|
752
|
+
throw new ProviderTransportError('ADAPTER_CREDENTIAL_MISSING', 'Host credential injection is missing or does not match the configured reference.');
|
|
753
|
+
const transportContext = { ...context, authorization };
|
|
754
|
+
const response = safeResponse(await this.transport.send(request, transportContext));
|
|
755
|
+
if (response.requestHash !== request.requestHash)
|
|
756
|
+
throw new ProviderTransportError('PROVIDER_RESPONSE_REQUEST_MISMATCH', 'Provider response did not match the request.');
|
|
757
|
+
if (response.status === 'submission_unknown' || response.status === 'processing')
|
|
758
|
+
return { status: 'submission_unknown', artifacts: [], response: publicResponseReceipt(response), lookup: createProviderSubmissionLookup(request, response.providerRequestId), failureCode: 'REMOTE_SUBMISSION_UNKNOWN' };
|
|
759
|
+
if (response.status === 'failed')
|
|
760
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(response), failureCode: response.error?.code ?? 'PROVIDER_FAILED' };
|
|
761
|
+
const artifacts = [];
|
|
762
|
+
for (const item of responseItems(response.body))
|
|
763
|
+
artifacts.push(await persistProviderItem(item, this.config.assetSink, 'generated-image'));
|
|
764
|
+
if (artifacts.length !== 1)
|
|
765
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(response), failureCode: 'PROVIDER_OUTPUT_CARDINALITY_INVALID' };
|
|
766
|
+
return { status: 'succeeded', artifacts, response: publicResponseReceipt(response) };
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (error instanceof ProviderTransportError)
|
|
770
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(errorResponse(requestHash, error.code, error.message)), failureCode: error.code };
|
|
771
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(errorResponse(requestHash, 'PROVIDER_FAILED', 'Provider adapter failed safely.')), failureCode: 'PROVIDER_FAILED' };
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async lookup(lookup, authorization, context = {}) {
|
|
775
|
+
if (lookup.adapterId !== this.id || lookup.adapterDigest !== this.digest || lookup.profileId !== this.config.profile.id || lookup.profileDigest !== this.profileDigest || lookup.purpose !== 'generation' || lookup.modelId !== this.config.model || lookup.modelVersion !== this.config.modelVersion)
|
|
776
|
+
throw new ProviderTransportError('PROVIDER_LOOKUP_SCOPE_MISMATCH', 'Submission lookup identity does not match the adapter.');
|
|
777
|
+
if (!context.credential || context.credential.ref !== this.config.credentialRef || !context.credential.value)
|
|
778
|
+
throw new ProviderTransportError('ADAPTER_CREDENTIAL_MISSING', 'Host credential injection is missing or does not match the configured reference.');
|
|
779
|
+
try {
|
|
780
|
+
const response = safeResponse(await this.transport.lookup(lookup, { ...context, authorization }));
|
|
781
|
+
if (response.requestHash !== lookup.requestHash)
|
|
782
|
+
throw new ProviderTransportError('PROVIDER_LOOKUP_REQUEST_MISMATCH', 'Submission lookup response did not match the original request.');
|
|
783
|
+
if (response.status === 'submission_unknown' || response.status === 'processing')
|
|
784
|
+
return { status: 'submission_unknown', artifacts: [], response: publicResponseReceipt(response), lookup: clone(lookup), failureCode: 'REMOTE_SUBMISSION_UNKNOWN' };
|
|
785
|
+
if (response.status === 'failed')
|
|
786
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(response), lookup: clone(lookup), failureCode: response.error?.code ?? 'PROVIDER_FAILED' };
|
|
787
|
+
const artifacts = [];
|
|
788
|
+
for (const item of responseItems(response.body))
|
|
789
|
+
artifacts.push(await persistProviderItem(item, this.config.assetSink, 'generated-image'));
|
|
790
|
+
if (artifacts.length !== 1)
|
|
791
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(response), lookup: clone(lookup), failureCode: 'PROVIDER_OUTPUT_CARDINALITY_INVALID' };
|
|
792
|
+
return { status: 'succeeded', artifacts, response: publicResponseReceipt(response), lookup: clone(lookup) };
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
if (error instanceof ProviderTransportError)
|
|
796
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(errorResponse(lookup.requestHash, error.code, error.message)), lookup: clone(lookup), failureCode: error.code };
|
|
797
|
+
return { status: 'failed', artifacts: [], response: publicResponseReceipt(errorResponse(lookup.requestHash, 'PROVIDER_FAILED', 'Provider lookup failed safely.')), lookup: clone(lookup), failureCode: 'PROVIDER_FAILED' };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
export function createSeedreamAdapter(config) { return new SeedreamAdapter(config); }
|
|
802
|
+
function parseJpeg(bytes) {
|
|
803
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8)
|
|
804
|
+
return { format: 'unknown' };
|
|
805
|
+
let offset = 2;
|
|
806
|
+
while (offset + 9 < bytes.length) {
|
|
807
|
+
if (bytes[offset] !== 0xff) {
|
|
808
|
+
offset += 1;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
const marker = bytes[offset + 1];
|
|
812
|
+
offset += 2;
|
|
813
|
+
if (marker === 0xd8 || marker === 0xd9)
|
|
814
|
+
continue;
|
|
815
|
+
if (offset + 2 > bytes.length)
|
|
816
|
+
break;
|
|
817
|
+
const length = (bytes[offset] << 8) | bytes[offset + 1];
|
|
818
|
+
if (length < 2 || offset + length > bytes.length)
|
|
819
|
+
break;
|
|
820
|
+
if ((marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) || (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)) {
|
|
821
|
+
return { format: 'jpeg', height: (bytes[offset + 3] << 8) | bytes[offset + 4], width: (bytes[offset + 5] << 8) | bytes[offset + 6], alpha: false };
|
|
822
|
+
}
|
|
823
|
+
offset += length;
|
|
824
|
+
}
|
|
825
|
+
return { format: 'jpeg', alpha: false };
|
|
826
|
+
}
|
|
827
|
+
function parseWebp(bytes) {
|
|
828
|
+
if (bytes.length < 16 || String.fromCharCode(...bytes.slice(0, 4)) !== 'RIFF' || String.fromCharCode(...bytes.slice(8, 12)) !== 'WEBP')
|
|
829
|
+
return { format: 'unknown' };
|
|
830
|
+
const kind = String.fromCharCode(...bytes.slice(12, 16));
|
|
831
|
+
if (kind === 'VP8X' && bytes.length >= 30)
|
|
832
|
+
return { format: 'webp', width: 1 + bytes[24] + (bytes[25] << 8) + (bytes[26] << 16), height: 1 + bytes[27] + (bytes[28] << 8) + (bytes[29] << 16), alpha: (bytes[20] & 0x10) !== 0 };
|
|
833
|
+
if (kind === 'VP8L' && bytes.length >= 25) {
|
|
834
|
+
const bits = bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24);
|
|
835
|
+
return { format: 'webp', width: 1 + (bits & 0x3fff), height: 1 + ((bits >>> 14) & 0x3fff), alpha: (bits & 0x10000000) !== 0 };
|
|
836
|
+
}
|
|
837
|
+
return { format: 'webp', alpha: false };
|
|
838
|
+
}
|
|
839
|
+
function parseImageHeader(bytes) {
|
|
840
|
+
const png = pngHeader(bytes);
|
|
841
|
+
if (png)
|
|
842
|
+
return { format: 'png', ...png };
|
|
843
|
+
const jpeg = parseJpeg(bytes);
|
|
844
|
+
if (jpeg.format !== 'unknown')
|
|
845
|
+
return jpeg;
|
|
846
|
+
return parseWebp(bytes);
|
|
847
|
+
}
|
|
848
|
+
function finding(inputHash, code, status, severity, artifactId, expected, actual, evidenceSummary) {
|
|
849
|
+
const base = {
|
|
850
|
+
schemaVersion: STRUCTURAL_VALIDATION_FINDING_SCHEMA_VERSION,
|
|
851
|
+
code,
|
|
852
|
+
status,
|
|
853
|
+
severity,
|
|
854
|
+
...(artifactId === undefined ? {} : { artifactId }),
|
|
855
|
+
...(expected === undefined ? {} : { expected }),
|
|
856
|
+
...(actual === undefined ? {} : { actual }),
|
|
857
|
+
evidenceSummary: safeMessage(evidenceSummary),
|
|
858
|
+
};
|
|
859
|
+
const withId = { ...base, id: hashId('structural-finding', { inputHash, code, artifactId }) };
|
|
860
|
+
const evidenceHash = sha256(withId);
|
|
861
|
+
return clone({ ...withId, evidenceHash });
|
|
862
|
+
}
|
|
863
|
+
function structuralInputProjection(input) {
|
|
864
|
+
return jsonReady({ schemaVersion: STRUCTURAL_VALIDATION_INPUT_SCHEMA_VERSION, id: input.id, artifacts: input.artifacts.map((item) => ({ artifact: item.artifact, ...(item.bytes === undefined ? {} : { bytesHash: binarySha256(item.bytes) }) })).sort((left, right) => compareCodeUnits(String(left.artifact.id), String(right.artifact.id))), outputContract: input.outputContract, ...(input.expectedCardinality === undefined ? {} : { expectedCardinality: input.expectedCardinality }), ...(input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes }) });
|
|
865
|
+
}
|
|
866
|
+
export function computeStructuralValidationInputHash(input) { return sha256(structuralInputProjection(input)); }
|
|
867
|
+
export function computeStructuralValidationFindingHash(findingValue) { return sha256(without(findingValue, 'evidenceHash')); }
|
|
868
|
+
export function computeStructuralValidationReportHash(report) { return sha256(without(report, 'reportHash')); }
|
|
869
|
+
function expectedMediaTypes(contract) { return sortedStrings(contract.mediaTypes); }
|
|
870
|
+
export function validateStructuralImage(input) {
|
|
871
|
+
const inputHash = computeStructuralValidationInputHash(input);
|
|
872
|
+
const findings = [];
|
|
873
|
+
const artifacts = input.artifacts.map((item) => ({ artifact: clone(item.artifact), ...(item.bytes === undefined ? {} : { bytes: new Uint8Array(item.bytes) }) }));
|
|
874
|
+
const cardinality = input.expectedCardinality ?? input.outputContract.cardinality;
|
|
875
|
+
if (artifacts.length < cardinality.min || artifacts.length > cardinality.max)
|
|
876
|
+
findings.push(finding(inputHash, 'CARDINALITY_INVALID', 'fail', 'critical', undefined, { min: cardinality.min, max: cardinality.max }, artifacts.length, 'Artifact count does not satisfy the output cardinality contract.'));
|
|
877
|
+
for (const item of artifacts) {
|
|
878
|
+
const artifact = item.artifact;
|
|
879
|
+
if (!isHash(artifact.contentHash))
|
|
880
|
+
findings.push(finding(inputHash, 'ARTIFACT_HASH_INVALID', 'fail', 'critical', artifact.id, 'sha256:<64 hex>', artifact.contentHash, 'Artifact content hash is invalid.'));
|
|
881
|
+
if (artifact.availability !== 'available')
|
|
882
|
+
findings.push(finding(inputHash, 'ARTIFACT_UNAVAILABLE', 'fail', 'critical', artifact.id, 'available', artifact.availability, 'Artifact is not available for structural validation.'));
|
|
883
|
+
if (item.bytes === undefined) {
|
|
884
|
+
findings.push(finding(inputHash, 'ARTIFACT_BYTES_UNAVAILABLE', 'unknown', 'warning', artifact.id, 'bytes', 'unavailable', 'Artifact bytes were not supplied; format and dimensions cannot be determined.'));
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (binarySha256(item.bytes) !== artifact.contentHash)
|
|
888
|
+
findings.push(finding(inputHash, 'ARTIFACT_HASH_MISMATCH', 'fail', 'critical', artifact.id, artifact.contentHash, binarySha256(item.bytes), 'Supplied bytes do not match the accepted artifact hash.'));
|
|
889
|
+
if (artifact.byteLength !== undefined && artifact.byteLength !== item.bytes.byteLength)
|
|
890
|
+
findings.push(finding(inputHash, 'BYTE_LENGTH_MISMATCH', 'fail', 'error', artifact.id, artifact.byteLength, item.bytes.byteLength, 'Artifact byte length does not match the supplied bytes.'));
|
|
891
|
+
if (input.maxBytes !== undefined && item.bytes.byteLength > input.maxBytes)
|
|
892
|
+
findings.push(finding(inputHash, 'MAX_BYTES_EXCEEDED', 'fail', 'error', artifact.id, input.maxBytes, item.bytes.byteLength, 'Artifact exceeds the validation byte limit.'));
|
|
893
|
+
if (input.outputContract.maxBytes !== undefined && item.bytes.byteLength > input.outputContract.maxBytes)
|
|
894
|
+
findings.push(finding(inputHash, 'OUTPUT_MAX_BYTES_EXCEEDED', 'fail', 'error', artifact.id, input.outputContract.maxBytes, item.bytes.byteLength, 'Artifact exceeds the output contract byte limit.'));
|
|
895
|
+
const header = parseImageHeader(item.bytes);
|
|
896
|
+
const actualMediaType = header.format === 'png' ? 'image/png' : header.format === 'jpeg' ? 'image/jpeg' : header.format === 'webp' ? 'image/webp' : 'unknown';
|
|
897
|
+
if (header.format === 'unknown')
|
|
898
|
+
findings.push(finding(inputHash, 'MEDIA_SIGNATURE_INVALID', 'fail', 'critical', artifact.id, expectedMediaTypes(input.outputContract), 'unknown', 'Image magic signature is not a supported PNG, JPEG, or WebP header.'));
|
|
899
|
+
if (header.format !== 'unknown' && artifact.mediaType !== actualMediaType)
|
|
900
|
+
findings.push(finding(inputHash, 'DECLARED_MEDIA_TYPE_SIGNATURE_MISMATCH', 'fail', 'critical', artifact.id, artifact.mediaType, actualMediaType, 'Declared artifact media type does not match the detected magic signature.'));
|
|
901
|
+
if (expectedMediaTypes(input.outputContract).length && !expectedMediaTypes(input.outputContract).includes(actualMediaType))
|
|
902
|
+
findings.push(finding(inputHash, 'MEDIA_TYPE_NOT_ALLOWED', 'fail', 'error', artifact.id, expectedMediaTypes(input.outputContract), actualMediaType, 'Detected image media type is outside the output contract allowlist.'));
|
|
903
|
+
if (header.width === undefined || header.height === undefined || header.width <= 0 || header.height <= 0)
|
|
904
|
+
findings.push(finding(inputHash, 'DIMENSIONS_UNKNOWN', 'unknown', 'warning', artifact.id, 'positive width and height', { width: header.width ?? null, height: header.height ?? null }, 'Image dimensions could not be reliably decoded from the basic header.'));
|
|
905
|
+
if (input.outputContract.dimensions && header.width !== undefined && header.height !== undefined && (header.width !== input.outputContract.dimensions.width || header.height !== input.outputContract.dimensions.height))
|
|
906
|
+
findings.push(finding(inputHash, 'DIMENSIONS_MISMATCH', 'fail', 'error', artifact.id, input.outputContract.dimensions, { width: header.width, height: header.height }, 'Image dimensions do not match the output contract.'));
|
|
907
|
+
if (input.outputContract.background === 'transparent' && !header.alpha)
|
|
908
|
+
findings.push(finding(inputHash, 'ALPHA_REQUIRED', 'fail', 'critical', artifact.id, true, header.alpha ?? false, 'Transparent output requires an Alpha channel.'));
|
|
909
|
+
if (input.outputContract.background === 'transparent' && header.alpha)
|
|
910
|
+
findings.push(finding(inputHash, 'BACKGROUND_VISUAL_TRANSPARENCY_UNKNOWN', 'unknown', 'warning', artifact.id, 'at least one transparent pixel', 'Alpha channel present; pixel transparency not decoded', 'A basic image header proves only that an Alpha channel exists; visual transparency requires review.'));
|
|
911
|
+
if (input.outputContract.allowAlpha === false && header.alpha)
|
|
912
|
+
findings.push(finding(inputHash, 'ALPHA_FORBIDDEN', 'fail', 'error', artifact.id, false, true, 'The output contract forbids Alpha.'));
|
|
913
|
+
if (input.outputContract.background === 'transparent' && header.format !== 'png')
|
|
914
|
+
findings.push(finding(inputHash, 'TRANSPARENT_FORMAT_INVALID', 'fail', 'critical', artifact.id, 'image/png', actualMediaType, 'Transparent output must be PNG.'));
|
|
915
|
+
if (input.outputContract.background === 'any' && header.alpha === undefined)
|
|
916
|
+
findings.push(finding(inputHash, 'BACKGROUND_VISUAL_TRANSPARENCY_UNKNOWN', 'unknown', 'warning', artifact.id, 'known', 'unknown', 'Structural bytes cannot determine whether the visual background is semantically transparent.'));
|
|
917
|
+
}
|
|
918
|
+
const hasFailure = findings.some((item) => item.status === 'fail');
|
|
919
|
+
const hasUnknown = findings.some((item) => item.status === 'unknown');
|
|
920
|
+
const base = {
|
|
921
|
+
schemaVersion: STRUCTURAL_VALIDATION_REPORT_SCHEMA_VERSION,
|
|
922
|
+
id: hashId('structural-report', inputHash),
|
|
923
|
+
inputHash,
|
|
924
|
+
status: hasFailure ? 'failed' : hasUnknown ? 'needs_review' : 'passed',
|
|
925
|
+
findings: sortedBy(findings, (item) => item.id),
|
|
926
|
+
artifactIds: sortedStrings(artifacts.map((item) => item.artifact.id)),
|
|
927
|
+
};
|
|
928
|
+
return clone({ ...base, reportHash: computeStructuralValidationReportHash(base) });
|
|
929
|
+
}
|
|
930
|
+
export const structuralValidate = validateStructuralImage;
|
|
931
|
+
export class StructuralImageValidator {
|
|
932
|
+
validate(input) { return validateStructuralImage(input); }
|
|
933
|
+
}
|
|
934
|
+
function semanticRequestProjection(request) {
|
|
935
|
+
return jsonReady({
|
|
936
|
+
schemaVersion: SEMANTIC_REVIEW_REQUEST_SCHEMA_VERSION,
|
|
937
|
+
id: request.id,
|
|
938
|
+
caseId: request.caseId,
|
|
939
|
+
caseRevision: request.caseRevision,
|
|
940
|
+
contextHash: request.contextHash,
|
|
941
|
+
inputHash: request.inputHash,
|
|
942
|
+
outputArtifacts: sortedBy(request.outputArtifacts, (item) => item.id),
|
|
943
|
+
criteria: sortedBy(request.criteria, (item) => item.id),
|
|
944
|
+
model: request.model,
|
|
945
|
+
adapter: request.adapter,
|
|
946
|
+
profile: request.profile,
|
|
947
|
+
authorizationId: request.authorizationId,
|
|
948
|
+
destination: request.destination,
|
|
949
|
+
...(request.region === undefined ? {} : { region: request.region }),
|
|
950
|
+
...(request.allowedEvidenceRegionIds === undefined ? {} : { allowedEvidenceRegionIds: sortedStrings(request.allowedEvidenceRegionIds) }),
|
|
951
|
+
dataCategories: sortedStrings(request.dataCategories),
|
|
952
|
+
budget: request.budget,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
export function computeSemanticReviewRequestHash(request) { return sha256(semanticRequestProjection(request)); }
|
|
956
|
+
export function computeSemanticReviewFindingHash(findingValue) { return sha256(without(findingValue, 'findingHash')); }
|
|
957
|
+
export function computeSemanticReviewReportHash(report) { return sha256(without(report, 'reportHash')); }
|
|
958
|
+
function assertSemanticRequest(request, authorization) {
|
|
959
|
+
if (request.schemaVersion !== SEMANTIC_REVIEW_REQUEST_SCHEMA_VERSION || computeSemanticReviewRequestHash(request) !== request.requestHash)
|
|
960
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_REQUEST_HASH_MISMATCH', 'Semantic review request hash is invalid.');
|
|
961
|
+
if (authorization.id !== request.authorizationId || authorization.purpose !== 'semantic_review' || authorization.inputHash !== request.inputHash || authorization.modelId !== request.model.id || authorization.modelVersion !== request.model.version || authorization.adapterId !== request.adapter.id || authorization.adapterDigest !== request.adapter.digest || authorization.profileDigest !== request.profile.digest || authorization.destination !== request.destination || authorization.region !== request.region || authorization.maximumCalls !== request.budget.maximumCalls || authorization.maximumRetries !== request.budget.maximumRetries || authorization.timeoutMs !== request.budget.timeoutMs || authorization.maximumBytes !== request.budget.maximumBytes || authorization.maximumCost !== request.budget.maximumCost || canonicalize(jsonReady(sortedStrings(authorization.permittedArtifactHashes))) !== canonicalize(jsonReady(sortedStrings(request.outputArtifacts.map((item) => item.contentHash)))) || canonicalize(jsonReady(sortedStrings(authorization.dataCategories))) !== canonicalize(jsonReady(sortedStrings(request.dataCategories))))
|
|
962
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_AUTHORIZATION_SCOPE_MISMATCH', 'Semantic review authorization does not match the request.');
|
|
963
|
+
assertHash(authorization.authorizationHash, 'REMOTE_CALL_AUTHORIZATION_INVALID');
|
|
964
|
+
if (computeRemoteCallAuthorizationHash(authorization) !== authorization.authorizationHash)
|
|
965
|
+
throw new ProviderTransportError('REMOTE_CALL_AUTHORIZATION_INVALID', 'Semantic review authorization hash is invalid.');
|
|
966
|
+
}
|
|
967
|
+
function assertSemanticReviewReport(request, report) {
|
|
968
|
+
if (!report || typeof report !== 'object' || report.schemaVersion !== SEMANTIC_REVIEW_REPORT_SCHEMA_VERSION || !Array.isArray(report.findings) || !report.model || !report.adapter || !report.profile || report.requestHash !== request.requestHash || computeSemanticReviewReportHash(report) !== report.reportHash)
|
|
969
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_REPORT_INVALID', 'Semantic reviewer returned a report with an invalid request or report hash.');
|
|
970
|
+
if (report.model.id !== request.model.id || report.model.version !== request.model.version || report.model.digest !== request.model.digest || report.adapter.id !== request.adapter.id || report.adapter.version !== request.adapter.version || report.adapter.digest !== request.adapter.digest || report.profile.id !== request.profile.id || report.profile.version !== request.profile.version || report.profile.digest !== request.profile.digest)
|
|
971
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_REPORT_BINDING_MISMATCH', 'Semantic reviewer report version binding does not match the request.');
|
|
972
|
+
const criteria = new Set(request.criteria.map((criterion) => criterion.id));
|
|
973
|
+
const artifacts = new Set(request.outputArtifacts.map((artifact) => artifact.id));
|
|
974
|
+
const regions = new Set(request.allowedEvidenceRegionIds ?? []);
|
|
975
|
+
for (const finding of report.findings) {
|
|
976
|
+
if (finding.schemaVersion !== SEMANTIC_REVIEW_FINDING_SCHEMA_VERSION || !finding.proposal || !criteria.has(finding.criterionId) || computeSemanticReviewFindingHash(finding) !== finding.findingHash)
|
|
977
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_FINDING_INVALID', 'Semantic reviewer returned an invalid finding proposal.');
|
|
978
|
+
if (finding.confidence !== undefined && (!Number.isFinite(finding.confidence) || finding.confidence < 0 || finding.confidence > 1))
|
|
979
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_CONFIDENCE_INVALID', 'Semantic reviewer confidence is outside the closed interval [0,1].');
|
|
980
|
+
if (finding.evidenceArtifactIds.some((id) => !artifacts.has(id)) || finding.evidenceRegionIds.some((id) => !regions.has(id)))
|
|
981
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_EVIDENCE_UNKNOWN', 'Semantic reviewer evidence references an unknown artifact or region.');
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
export class FixtureSemanticReviewer {
|
|
985
|
+
options;
|
|
986
|
+
id = 'voce.fixture-semantic-reviewer';
|
|
987
|
+
version = { id: this.id, version: '1.0.0', digest: sha256({ id: this.id, version: '1.0.0' }) };
|
|
988
|
+
constructor(options = {}) {
|
|
989
|
+
this.options = options;
|
|
990
|
+
}
|
|
991
|
+
async review(request, authorization) {
|
|
992
|
+
assertSemanticRequest(request, authorization);
|
|
993
|
+
const findings = (this.options.findings ?? request.criteria.map((criterion) => {
|
|
994
|
+
const base = { schemaVersion: SEMANTIC_REVIEW_FINDING_SCHEMA_VERSION, criterionId: criterion.id, code: 'FIXTURE_UNCERTAIN', status: 'uncertain', confidence: 0.5, explanation: 'Offline fixture semantic review is a proposal requiring human acceptance.', evidenceArtifactIds: request.outputArtifacts.map((item) => item.id), evidenceRegionIds: [], warnings: [], proposal: true };
|
|
995
|
+
const withId = { ...base, id: hashId('semantic-finding', base) };
|
|
996
|
+
return { ...withId, findingHash: computeSemanticReviewFindingHash(withId) };
|
|
997
|
+
})).map((item) => clone(item));
|
|
998
|
+
for (const item of findings) {
|
|
999
|
+
if (!item.proposal || computeSemanticReviewFindingHash(item) !== item.findingHash)
|
|
1000
|
+
throw new ProviderTransportError('SEMANTIC_REVIEW_FINDING_INVALID', 'Semantic finding is not a valid proposal.');
|
|
1001
|
+
}
|
|
1002
|
+
const base = { schemaVersion: SEMANTIC_REVIEW_REPORT_SCHEMA_VERSION, id: hashId('semantic-report', request.requestHash), requestHash: request.requestHash, status: 'proposal', model: clone(request.model), adapter: clone(request.adapter), profile: clone(request.profile), findings: sortedBy(findings, (item) => item.id), warnings: sortedStrings(this.options.warnings), receiptIds: [hashId('semantic-receipt', { requestHash: request.requestHash, authorizationId: authorization.id })] };
|
|
1003
|
+
return clone({ ...base, reportHash: computeSemanticReviewReportHash(base) });
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
export async function executeSemanticReview(reviewer, request, authorization) {
|
|
1007
|
+
assertSemanticRequest(request, authorization);
|
|
1008
|
+
const report = await reviewer.review(request, authorization);
|
|
1009
|
+
assertSemanticReviewReport(request, report);
|
|
1010
|
+
const receiptId = report.receiptIds[0] ?? hashId('semantic-receipt', request.requestHash);
|
|
1011
|
+
const state = report.status === 'proposal' ? 'succeeded' : report.status;
|
|
1012
|
+
const receiptBase = { schemaVersion: 'voce.step-receipt/v1alpha1', id: receiptId, runId: hashId('semantic-run', request.requestHash), stepId: request.id, state, eventIds: [hashId('semantic-event', { requestHash: request.requestHash, status: report.status })], firstSequence: 1, lastSequence: 1, authorizationId: authorization.id, inputHash: request.inputHash, outputHashes: [report.reportHash], adapterId: request.adapter.id, adapterVersion: clone(request.adapter), profileDigest: request.profile.digest, destination: request.destination, dataCategories: sortedStrings(request.dataCategories), budgetId: request.budget.id, maximumCalls: request.budget.maximumCalls, maximumRetries: request.budget.maximumRetries, timeoutMs: request.budget.timeoutMs, attempts: 1, retriesUsed: 0, ...(report.status === 'proposal' ? {} : { failureCode: report.status === 'submission_unknown' ? 'SEMANTIC_REVIEW_SUBMISSION_UNKNOWN' : 'SEMANTIC_REVIEW_FAILED' }), cleanupStatus: 'not_required' };
|
|
1013
|
+
const receipt = clone({ ...receiptBase, receiptHash: sha256(receiptBase) });
|
|
1014
|
+
const remoteBase = { schemaVersion: 'voce.remote-call-run/v1alpha1', id: hashId('semantic-remote-run', request.requestHash), runId: receipt.runId, stepId: request.id, authorizationId: authorization.id, inputHash: request.inputHash, state: state, provider: reviewer.id, adapterId: request.adapter.id, profileDigest: request.profile.digest, destination: request.destination, budgetId: request.budget.id, maximumCalls: request.budget.maximumCalls, maximumRetries: request.budget.maximumRetries, timeoutMs: request.budget.timeoutMs, receiptId };
|
|
1015
|
+
return { report, remoteCallRun: clone({ ...remoteBase, runHash: sha256(remoteBase) }), receipt };
|
|
1016
|
+
}
|
|
1017
|
+
function humanAnnotationHash(annotation) { return sha256(without(annotation, 'annotationHash')); }
|
|
1018
|
+
export function computeHumanAcceptanceAnnotationHash(annotation) { return humanAnnotationHash(annotation); }
|
|
1019
|
+
export function computeHumanAcceptanceDecisionHash(decision) { return sha256(without(decision, 'decisionHash')); }
|
|
1020
|
+
export function createHumanAcceptanceDecision(input) {
|
|
1021
|
+
const annotations = sortedBy(input.annotations.map((annotation) => ({ ...annotation, schemaVersion: HUMAN_ACCEPTANCE_ANNOTATION_SCHEMA_VERSION, annotationHash: annotation.annotationHash || humanAnnotationHash(annotation) })), (item) => item.id);
|
|
1022
|
+
for (const annotation of annotations)
|
|
1023
|
+
if (humanAnnotationHash(annotation) !== annotation.annotationHash)
|
|
1024
|
+
throw new ProviderTransportError('HUMAN_ANNOTATION_HASH_MISMATCH', 'Human annotation hash is invalid.');
|
|
1025
|
+
const base = { ...clone(input), schemaVersion: HUMAN_ACCEPTANCE_DECISION_SCHEMA_VERSION, annotations };
|
|
1026
|
+
return clone({ ...base, decisionHash: computeHumanAcceptanceDecisionHash(base) });
|
|
1027
|
+
}
|
|
1028
|
+
function normalizeHumanAcceptance(value, runId) {
|
|
1029
|
+
if ('decisionHash' in value)
|
|
1030
|
+
return clone(value);
|
|
1031
|
+
const base = { schemaVersion: HUMAN_ACCEPTANCE_DECISION_SCHEMA_VERSION, id: value.id, runId, status: value.status, ...(value.reviewerId === undefined ? {} : { reviewerId: value.reviewerId }), ...(value.decidedAt === undefined ? {} : { decidedAt: value.decidedAt }), ...(value.reasonCode === undefined ? {} : { reasonCode: value.reasonCode }), annotations: [], artifactIds: value.artifactIds };
|
|
1032
|
+
return createHumanAcceptanceDecision(base);
|
|
1033
|
+
}
|
|
1034
|
+
function cleanupStatus(receipts) {
|
|
1035
|
+
if (!receipts.length)
|
|
1036
|
+
return { status: 'not_required', receiptIds: [], failureCodes: [] };
|
|
1037
|
+
const failed = receipts.some((item) => item.status === 'cleanup_failed');
|
|
1038
|
+
const pending = receipts.some((item) => item.status === 'pending');
|
|
1039
|
+
return { status: failed ? 'failed' : pending ? 'pending' : 'completed', receiptIds: sortedStrings(receipts.map((item) => item.id)), failureCodes: sortedStrings(receipts.flatMap((item) => item.failureCode ? [item.failureCode] : [])) };
|
|
1040
|
+
}
|
|
1041
|
+
export function computeEvaluationReportHash(report) { return sha256(without(report, 'reportHash')); }
|
|
1042
|
+
export function compileEvaluationReport(input) {
|
|
1043
|
+
for (const hash of Object.values(input.sourceHashes ?? {}))
|
|
1044
|
+
if (!isHash(hash))
|
|
1045
|
+
throw new ProviderTransportError('EVALUATION_SOURCE_HASH_INVALID', 'Evaluation source hash is invalid.');
|
|
1046
|
+
for (const artifact of input.artifacts ?? [])
|
|
1047
|
+
if (!isHash(artifact.contentHash))
|
|
1048
|
+
throw new ProviderTransportError('ARTIFACT_HANDLE_HASH_INVALID', 'Evaluation artifact hash is invalid.');
|
|
1049
|
+
if (input.structural && computeStructuralValidationReportHash(input.structural) !== input.structural.reportHash)
|
|
1050
|
+
throw new ProviderTransportError('STRUCTURAL_REPORT_HASH_MISMATCH', 'Structural validation report hash is invalid.');
|
|
1051
|
+
if (input.semanticProposal && computeSemanticReviewReportHash(input.semanticProposal) !== input.semanticProposal.reportHash)
|
|
1052
|
+
throw new ProviderTransportError('SEMANTIC_REPORT_HASH_MISMATCH', 'Semantic review report hash is invalid.');
|
|
1053
|
+
const human = input.humanAcceptance ? normalizeHumanAcceptance(input.humanAcceptance, input.run.id) : undefined;
|
|
1054
|
+
if (human && (human.annotations.some((annotation) => humanAnnotationHash(annotation) !== annotation.annotationHash) || computeHumanAcceptanceDecisionHash(human) !== human.decisionHash))
|
|
1055
|
+
throw new ProviderTransportError('HUMAN_DECISION_HASH_MISMATCH', 'Human acceptance decision hash is invalid.');
|
|
1056
|
+
const cleanup = cleanupStatus(input.cleanup ?? []);
|
|
1057
|
+
const replay = input.replay ?? { mode: 'none', status: 'not_requested', artifactIds: [] };
|
|
1058
|
+
const technicalStatus = input.run.technicalOutcome === 'succeeded' ? 'passed' : input.run.technicalOutcome === 'failed' ? 'failed' : input.run.technicalOutcome === 'pending' ? 'pending' : 'needs_review';
|
|
1059
|
+
const status = input.run.technicalOutcome === 'failed' || input.structural?.status === 'failed' ? 'failed' : input.semanticProposal?.status === 'submission_unknown' || human?.status === 'pending' || human?.status === 'declined' || cleanup.status === 'failed' ? 'needs_review' : input.structural?.status === 'needs_review' ? 'partial' : 'complete';
|
|
1060
|
+
const base = { schemaVersion: EVALUATION_REPORT_SCHEMA_VERSION, id: hashId('evaluation-report', { runId: input.run.id, structural: input.structural?.reportHash, semantic: input.semanticProposal?.reportHash, human: human?.decisionHash }), runId: input.run.id, technicalOutcome: input.run.technicalOutcome, technicalStatus, ...(input.structural === undefined ? {} : { structural: clone(input.structural) }), ...(input.semanticProposal === undefined ? {} : { semanticProposal: clone(input.semanticProposal) }), ...(human === undefined ? {} : { humanAcceptance: human }), cleanup, replay: clone(replay), artifactIds: sortedStrings((input.artifacts ?? []).map((item) => item.id)), sourceHashes: Object.fromEntries(Object.entries(input.sourceHashes ?? {}).sort((left, right) => compareCodeUnits(left[0], right[0]))), status, warnings: sortedStrings([...(input.run.state === 'needs_review' ? ['HUMAN_ACCEPTANCE_REQUIRED'] : []), ...(cleanup.failureCodes)]) };
|
|
1061
|
+
return clone({ ...base, reportHash: computeEvaluationReportHash(base) });
|
|
1062
|
+
}
|
|
1063
|
+
export const compileEvaluation = compileEvaluationReport;
|
|
1064
|
+
const VOLATILE_FIELDS = new Set(['at', 'createdAt', 'updatedAt', 'decidedAt', 'authorizedAt', 'expiresAt', 'runId', 'parentRunId', 'liveRerunOf', 'eventIds', 'eventCount']);
|
|
1065
|
+
const UNORDERED_COMPARISON_ARRAY_KEYS = new Set(['inputArtifactHashes', 'dataCategories', 'permittedArtifactHashes', 'permittedScopeIds', 'constraintIds', 'artifactIds', 'outputArtifactIds', 'receiptIds', 'warnings', 'destinations', 'eventIds', 'findings', 'annotations', 'receipts', 'cleanup', 'reconciliation']);
|
|
1066
|
+
function stableComparisonValue(value, key) {
|
|
1067
|
+
if (Array.isArray(value)) {
|
|
1068
|
+
const normalized = value.map((item) => stableComparisonValue(item, key));
|
|
1069
|
+
return UNORDERED_COMPARISON_ARRAY_KEYS.has(key ?? '') ? normalized.sort((left, right) => compareCodeUnits(canonicalize(left), canonicalize(right))) : normalized;
|
|
1070
|
+
}
|
|
1071
|
+
if (value && typeof value === 'object') {
|
|
1072
|
+
const object = {};
|
|
1073
|
+
for (const [childKey, item] of Object.entries(value))
|
|
1074
|
+
if (!VOLATILE_FIELDS.has(childKey))
|
|
1075
|
+
object[childKey] = stableComparisonValue(item, childKey);
|
|
1076
|
+
return object;
|
|
1077
|
+
}
|
|
1078
|
+
return safeJson(value);
|
|
1079
|
+
}
|
|
1080
|
+
function hashComparisonValue(value) { return value === undefined ? undefined : sha256(value); }
|
|
1081
|
+
function changedFields(before, after) {
|
|
1082
|
+
if (!before || !after || typeof before !== 'object' || typeof after !== 'object' || Array.isArray(before) || Array.isArray(after))
|
|
1083
|
+
return before === after ? [] : ['value'];
|
|
1084
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
1085
|
+
return [...keys].filter((key) => canonicalize(before[key] ?? null) !== canonicalize(after[key] ?? null)).sort(compareCodeUnits);
|
|
1086
|
+
}
|
|
1087
|
+
function recordsForSnapshot(snapshot) {
|
|
1088
|
+
if (snapshot === undefined)
|
|
1089
|
+
return [];
|
|
1090
|
+
if (Array.isArray(snapshot))
|
|
1091
|
+
return snapshot.map((item, index) => ({ key: typeof item === 'object' && item && 'id' in item ? String(item.id) : String(index), value: stableComparisonValue(item) }));
|
|
1092
|
+
if (snapshot && typeof snapshot === 'object') {
|
|
1093
|
+
const object = snapshot;
|
|
1094
|
+
const arrayKey = Object.keys(object).some((key) => key.endsWith('s')) && Array.isArray(object.items) ? object.items : undefined;
|
|
1095
|
+
if (arrayKey && Array.isArray(arrayKey))
|
|
1096
|
+
return recordsForSnapshot(arrayKey);
|
|
1097
|
+
return [{ key: typeof object.id === 'string' ? object.id : 'root', value: stableComparisonValue(object) }];
|
|
1098
|
+
}
|
|
1099
|
+
return [{ key: 'root', value: stableComparisonValue(snapshot) }];
|
|
1100
|
+
}
|
|
1101
|
+
export function compareSnapshots(input) {
|
|
1102
|
+
const entries = [];
|
|
1103
|
+
const categories = ['ontology', 'bindings', 'constraintIR', 'referencePlan', 'promptIR', 'promptCandidate', 'pipelinePlan', 'receipts', 'evaluation'];
|
|
1104
|
+
for (const category of categories) {
|
|
1105
|
+
const beforeRecords = new Map(recordsForSnapshot(input.before[category]).map((item) => [item.key, item.value]));
|
|
1106
|
+
const afterRecords = new Map(recordsForSnapshot(input.after[category]).map((item) => [item.key, item.value]));
|
|
1107
|
+
const keys = [...new Set([...beforeRecords.keys(), ...afterRecords.keys()])].sort(compareCodeUnits);
|
|
1108
|
+
for (const key of keys) {
|
|
1109
|
+
const before = beforeRecords.get(key);
|
|
1110
|
+
const after = afterRecords.get(key);
|
|
1111
|
+
const kind = before === undefined ? 'added' : after === undefined ? 'removed' : canonicalize(before) === canonicalize(after) ? 'unchanged' : 'changed';
|
|
1112
|
+
const base = { schemaVersion: COMPARISON_ENTRY_SCHEMA_VERSION, category, key, kind, ...(hashComparisonValue(before) === undefined ? {} : { beforeHash: hashComparisonValue(before) }), ...(hashComparisonValue(after) === undefined ? {} : { afterHash: hashComparisonValue(after) }), ...(before === undefined ? {} : { before }), ...(after === undefined ? {} : { after }), changedFields: changedFields(before, after), reasonCode: kind === 'unchanged' ? 'UNCHANGED_AFTER_VOLATILE_FIELDS_IGNORED' : 'SEMANTIC_FIELD_CHANGED' };
|
|
1113
|
+
entries.push({ ...base, id: hashId('comparison-entry', base) });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const summary = { added: entries.filter((item) => item.kind === 'added').length, removed: entries.filter((item) => item.kind === 'removed').length, changed: entries.filter((item) => item.kind === 'changed').length, unchanged: entries.filter((item) => item.kind === 'unchanged').length };
|
|
1117
|
+
const base = { schemaVersion: COMPARISON_REPORT_SCHEMA_VERSION, id: hashId('comparison-report', { caseId: input.caseId, beforeRevision: input.beforeRevision, afterRevision: input.afterRevision }), caseId: input.caseId, beforeRevision: input.beforeRevision, afterRevision: input.afterRevision, ignoredFields: [...VOLATILE_FIELDS].sort(compareCodeUnits), entries: sortedBy(entries, (item) => `${item.category}:${item.key}`), summary };
|
|
1118
|
+
return clone({ ...base, reportHash: sha256(base) });
|
|
1119
|
+
}
|
|
1120
|
+
export const compare = compareSnapshots;
|
|
1121
|
+
export function computeComparisonReportHash(report) { return sha256(without(report, 'reportHash')); }
|
|
1122
|
+
export function computeStaticTraceReportModelHash(model) { return sha256(without(model, 'modelHash')); }
|
|
1123
|
+
function assertTraceNestedHashes(input) {
|
|
1124
|
+
if (input.structural && computeStructuralValidationReportHash(input.structural) !== input.structural.reportHash)
|
|
1125
|
+
throw new ProviderTransportError('TRACE_STRUCTURAL_HASH_MISMATCH', 'Trace structural report hash is invalid.');
|
|
1126
|
+
if (input.semanticProposal && computeSemanticReviewReportHash(input.semanticProposal) !== input.semanticProposal.reportHash)
|
|
1127
|
+
throw new ProviderTransportError('TRACE_SEMANTIC_HASH_MISMATCH', 'Trace semantic report hash is invalid.');
|
|
1128
|
+
if (input.humanAcceptance && computeHumanAcceptanceDecisionHash(input.humanAcceptance) !== input.humanAcceptance.decisionHash)
|
|
1129
|
+
throw new ProviderTransportError('TRACE_HUMAN_HASH_MISMATCH', 'Trace human acceptance hash is invalid.');
|
|
1130
|
+
if (input.comparison && computeComparisonReportHash(input.comparison) !== input.comparison.reportHash)
|
|
1131
|
+
throw new ProviderTransportError('TRACE_COMPARISON_HASH_MISMATCH', 'Trace comparison report hash is invalid.');
|
|
1132
|
+
for (const artifact of input.artifacts ?? [])
|
|
1133
|
+
if (!isHash(artifact.contentHash))
|
|
1134
|
+
throw new ProviderTransportError('ARTIFACT_HANDLE_HASH_INVALID', 'Trace artifact content hash is invalid.');
|
|
1135
|
+
for (const [name, value] of [['structural', input.structural], ['semantic', input.semanticProposal], ['human', input.humanAcceptance], ['comparison', input.comparison]]) {
|
|
1136
|
+
if (value !== undefined && canonicalize(jsonReady(value)) !== canonicalize(jsonReady(safeJson(value))))
|
|
1137
|
+
throw new ProviderTransportError('TRACE_NESTED_SECRET_UNSAFE', `Trace ${name} contains a secret or unsafe reference.`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function escapeHtml(value) {
|
|
1141
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
1142
|
+
}
|
|
1143
|
+
function display(value) { return escapeHtml(safeMessage(typeof value === 'string' ? value : canonicalize(safeJson(value)))); }
|
|
1144
|
+
function traceStepRows(steps) { return sortedBy(steps, (item) => item.id).map((step) => `<tr><td>${display(step.id)}</td><td>${display(step.type)}</td><td>${display(step.state)}</td><td>${display(step.at ?? '')}</td><td>${display(step.adapterId ?? '')}</td><td>${display(step.destination ?? '')}</td><td>${display(step.receiptId ?? '')}</td><td>${display(step.failureCode ?? '')}</td></tr>`).join(''); }
|
|
1145
|
+
export function renderStaticTraceReport(model) {
|
|
1146
|
+
assertTraceNestedHashes(model);
|
|
1147
|
+
if (model.schemaVersion !== STATIC_TRACE_REPORT_MODEL_SCHEMA_VERSION || !isHash(model.modelHash) || computeStaticTraceReportModelHash(model) !== model.modelHash)
|
|
1148
|
+
throw new ProviderTransportError('STATIC_TRACE_MODEL_HASH_MISMATCH', 'Static trace report model hash is invalid.');
|
|
1149
|
+
for (const hash of [model.contextHash, model.constraintHash, model.referencePlanHash, model.pipelinePlanHash, model.promptHash].filter((value) => value !== undefined))
|
|
1150
|
+
if (!isHash(hash))
|
|
1151
|
+
throw new ProviderTransportError('STATIC_TRACE_HASH_INVALID', 'Static trace report contains an invalid bound hash.');
|
|
1152
|
+
for (const artifact of model.artifacts)
|
|
1153
|
+
if (!isHash(artifact.contentHash))
|
|
1154
|
+
throw new ProviderTransportError('ARTIFACT_HANDLE_HASH_INVALID', 'Static trace report contains an invalid artifact hash.');
|
|
1155
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>VOCE Trace Report</title><style>body{font-family:system-ui,sans-serif;color:#1f2937;background:#f8fafc;margin:0;padding:2rem}main{max-width:1200px;margin:auto;background:#fff;padding:2rem;border:1px solid #e5e7eb;border-radius:12px}h1,h2{color:#111827}table{border-collapse:collapse;width:100%;margin:1rem 0}th,td{border:1px solid #d1d5db;padding:.45rem;text-align:left;vertical-align:top;font-size:.9rem}th{background:#f3f4f6}.meta{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem 2rem}.safe{font-family:ui-monospace,monospace;overflow-wrap:anywhere}.warning{color:#92400e}</style></head><body><main><h1>VOCE Static Trace Report</h1><section class="meta"><div><strong>Case</strong><br>${display(model.caseId)}</div><div><strong>Revision</strong><br>${display(model.revision)}</div><div><strong>Context hash</strong><br><span class="safe">${display(model.contextHash)}</span></div><div><strong>Constraint hash</strong><br><span class="safe">${display(model.constraintHash ?? '')}</span></div><div><strong>Reference plan hash</strong><br><span class="safe">${display(model.referencePlanHash ?? '')}</span></div><div><strong>Pipeline plan hash</strong><br><span class="safe">${display(model.pipelinePlanHash ?? '')}</span></div><div><strong>Prompt hash</strong><br><span class="safe">${display(model.promptHash ?? '')}</span></div><div><strong>Model hash</strong><br><span class="safe">${display(model.modelHash)}</span></div></section><h2>Timeline</h2><table><thead><tr><th>Step</th><th>Type</th><th>State</th><th>Time</th><th>Adapter</th><th>Destination</th><th>Receipt</th><th>Failure</th></tr></thead><tbody>${traceStepRows(model.steps)}</tbody></table><h2>Budgets and destinations</h2><pre>${display({ budgets: model.budgets, destinations: model.destinations })}</pre><h2>Receipts and cleanup</h2><pre>${display({ receipts: model.receipts, cleanup: model.cleanup, reconciliation: model.reconciliation })}</pre><h2>Evaluation</h2><pre>${display({ structural: model.structural, semanticProposal: model.semanticProposal, humanAcceptance: model.humanAcceptance, artifacts: model.artifacts, comparison: model.comparison })}</pre><h2>Warnings</h2><p class="warning">${display(model.warnings.join('\n'))}</p></main></body></html>`;
|
|
1156
|
+
const base = { schemaVersion: REPORT_ARTIFACT_SCHEMA_VERSION, id: hashId('report-artifact', { modelHash: model.modelHash, version: STATIC_REPORT_VERSION }), mediaType: 'text/html', content: html, modelHash: model.modelHash };
|
|
1157
|
+
return clone({ ...base, contentHash: sha256({ content: html }) });
|
|
1158
|
+
}
|
|
1159
|
+
export const createStaticTraceReport = renderStaticTraceReport;
|
|
1160
|
+
export async function writeStaticTraceReport(model, outputPath) {
|
|
1161
|
+
const artifact = renderStaticTraceReport(model);
|
|
1162
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
1163
|
+
await writeFile(outputPath, artifact.content, 'utf8');
|
|
1164
|
+
return artifact;
|
|
1165
|
+
}
|
|
1166
|
+
export function traceModelFromExecution(input) {
|
|
1167
|
+
for (const hash of [input.run.contextHash, input.run.pipelinePlanHash, input.constraintHash, input.referencePlanHash, input.promptHash].filter((value) => value !== undefined))
|
|
1168
|
+
if (!isHash(hash))
|
|
1169
|
+
throw new ProviderTransportError('TRACE_HASH_INVALID', 'Trace execution contains an invalid bound hash.');
|
|
1170
|
+
assertTraceNestedHashes(input);
|
|
1171
|
+
const steps = input.steps ?? input.receipts.map((receipt) => ({ id: receipt.stepId, type: receipt.stepId, state: receipt.state, adapterId: receipt.adapterId, adapterVersion: receipt.adapterVersion, profileDigest: receipt.profileDigest, destination: receipt.destination, budgetId: receipt.budgetId, inputHash: receipt.inputHash, outputHashes: receipt.outputHashes, receiptId: receipt.id, ...(receipt.failureCode === undefined ? {} : { failureCode: receipt.failureCode }) }));
|
|
1172
|
+
const safeSteps = safeJson(steps);
|
|
1173
|
+
const safeReceipts = safeJson(input.receipts);
|
|
1174
|
+
const safeCleanup = safeJson(input.cleanup);
|
|
1175
|
+
const safeReconciliation = safeJson(input.reconciliation);
|
|
1176
|
+
const safeBudgets = safeJson(input.budgets ?? []);
|
|
1177
|
+
const safeDestinations = safeJson(input.destinations ?? []);
|
|
1178
|
+
const safeWarnings = safeJson(input.warnings ?? []);
|
|
1179
|
+
const safeArtifacts = safeJson(input.artifacts ?? []);
|
|
1180
|
+
const base = { schemaVersion: STATIC_TRACE_REPORT_MODEL_SCHEMA_VERSION, caseId: safeJson(input.run.caseId), revision: input.run.caseRevision, contextHash: input.run.contextHash, ...(input.constraintHash === undefined ? {} : { constraintHash: input.constraintHash }), ...(input.referencePlanHash === undefined ? {} : { referencePlanHash: input.referencePlanHash }), ...(input.run.pipelinePlanHash === undefined ? {} : { pipelinePlanHash: input.run.pipelinePlanHash }), ...(input.promptHash === undefined ? {} : { promptHash: input.promptHash }), steps: sortedBy(safeSteps, (item) => item.id), budgets: sortedBy(safeBudgets, (item) => item.id), destinations: sortedStrings(safeDestinations), receipts: sortedBy(safeReceipts, (item) => item.id), cleanup: sortedBy(safeCleanup, (item) => item.id), reconciliation: sortedBy(safeReconciliation, (item) => item.id), ...(input.structural === undefined ? {} : { structural: input.structural }), ...(input.semanticProposal === undefined ? {} : { semanticProposal: input.semanticProposal }), ...(input.humanAcceptance === undefined ? {} : { humanAcceptance: input.humanAcceptance }), artifacts: sortedBy(safeArtifacts, (item) => item.id), ...(input.comparison === undefined ? {} : { comparison: input.comparison }), warnings: sortedStrings(safeWarnings) };
|
|
1181
|
+
return clone({ ...base, modelHash: sha256(base) });
|
|
1182
|
+
}
|
|
1183
|
+
export { ARTIFACT_REPLAY_RESULT_SCHEMA_VERSION };
|