@artemiskit/core 0.4.2 → 0.5.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/CHANGELOG.md +16 -0
- package/dist/artifacts/manifest.d.ts +3 -1
- package/dist/artifacts/manifest.d.ts.map +1 -1
- package/dist/artifacts/types.d.ts +48 -0
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/index.js +186 -12
- package/dist/provenance/execution-provenance.d.ts +11 -0
- package/dist/provenance/execution-provenance.d.ts.map +1 -0
- package/dist/provenance/index.d.ts +2 -0
- package/dist/provenance/index.d.ts.map +1 -1
- package/dist/provenance/workload-identity.d.ts +12 -0
- package/dist/provenance/workload-identity.d.ts.map +1 -0
- package/dist/runner/executor.d.ts.map +1 -1
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/types.d.ts +2 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/artifacts/manifest.test.ts +67 -1
- package/src/artifacts/manifest.ts +9 -1
- package/src/artifacts/types.ts +139 -0
- package/src/provenance/execution-provenance.test.ts +59 -0
- package/src/provenance/execution-provenance.ts +53 -0
- package/src/provenance/index.ts +2 -0
- package/src/provenance/workload-identity.test.ts +69 -0
- package/src/provenance/workload-identity.ts +92 -0
- package/src/runner/executor.test.ts +10 -0
- package/src/runner/executor.ts +71 -9
- package/src/runner/release-validation.test.ts +41 -0
- package/src/runner/runner.ts +12 -0
- package/src/runner/types.ts +2 -0
|
@@ -55,7 +55,7 @@ describe('createRunManifest', () => {
|
|
|
55
55
|
endTime,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
expect(manifest.version).toBe('1.
|
|
58
|
+
expect(manifest.version).toBe('1.3');
|
|
59
59
|
expect(manifest.project).toBe('test-project');
|
|
60
60
|
expect(manifest.run_id).toBeTruthy();
|
|
61
61
|
expect(manifest.run_id.length).toBe(12);
|
|
@@ -163,6 +163,56 @@ describe('createRunManifest', () => {
|
|
|
163
163
|
expect(manifest.resolved_config?.source.model).toBe('config');
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
test('retains a validated workload identity when provided', () => {
|
|
167
|
+
const manifest = createRunManifest({
|
|
168
|
+
project: 'test-project',
|
|
169
|
+
config: { scenario: 'test-scenario', provider: 'openai' },
|
|
170
|
+
workloadIdentity: {
|
|
171
|
+
schema_version: '1',
|
|
172
|
+
workload: {
|
|
173
|
+
schema_version: '1',
|
|
174
|
+
algorithm: 'sha256',
|
|
175
|
+
digest: 'a'.repeat(64),
|
|
176
|
+
},
|
|
177
|
+
rubric: {
|
|
178
|
+
schema_version: '1',
|
|
179
|
+
algorithm: 'sha256',
|
|
180
|
+
digest: 'b'.repeat(64),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
cases: mockCases,
|
|
184
|
+
startTime: new Date(),
|
|
185
|
+
endTime: new Date(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(manifest.workload_identity?.workload.digest).toBe('a'.repeat(64));
|
|
189
|
+
expect(() => assertRunManifestIntegrity(manifest)).not.toThrow();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('retains target and evaluator execution provenance separately', () => {
|
|
193
|
+
const manifest = createRunManifest({
|
|
194
|
+
project: 'test-project',
|
|
195
|
+
config: { scenario: 'test-scenario', provider: 'openai' },
|
|
196
|
+
executionProvenance: {
|
|
197
|
+
schema_version: '1',
|
|
198
|
+
target: {
|
|
199
|
+
provider: 'openai',
|
|
200
|
+
requested_models: ['gpt-requested'],
|
|
201
|
+
observed_models: ['gpt-observed'],
|
|
202
|
+
generation: { temperature: 0, max_tokens: 100, seed: 42 },
|
|
203
|
+
},
|
|
204
|
+
evaluator: { models: ['judge-model'] },
|
|
205
|
+
},
|
|
206
|
+
cases: mockCases,
|
|
207
|
+
startTime: new Date(),
|
|
208
|
+
endTime: new Date(),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(manifest.execution_provenance?.target.observed_models).toEqual(['gpt-observed']);
|
|
212
|
+
expect(manifest.execution_provenance?.evaluator?.models).toEqual(['judge-model']);
|
|
213
|
+
expect(() => assertRunManifestIntegrity(manifest)).not.toThrow();
|
|
214
|
+
});
|
|
215
|
+
|
|
166
216
|
test('includes provenance information', () => {
|
|
167
217
|
const manifest = createRunManifest({
|
|
168
218
|
project: 'test-project',
|
|
@@ -273,5 +323,21 @@ describe('createRunManifest', () => {
|
|
|
273
323
|
],
|
|
274
324
|
};
|
|
275
325
|
expect(() => assertRunManifestIntegrity(malformed)).toThrow('invalid evidence validation');
|
|
326
|
+
|
|
327
|
+
expect(() =>
|
|
328
|
+
assertRunManifestIntegrity({ ...historical, workload_identity: { schema_version: '1' } })
|
|
329
|
+
).toThrow('malformed workload identity');
|
|
330
|
+
expect(() =>
|
|
331
|
+
assertRunManifestIntegrity({
|
|
332
|
+
...historical,
|
|
333
|
+
execution_provenance: { schema_version: '1', target: { provider: '' } },
|
|
334
|
+
})
|
|
335
|
+
).toThrow('malformed execution provenance');
|
|
336
|
+
expect(() =>
|
|
337
|
+
assertRunManifestIntegrity({
|
|
338
|
+
...historical,
|
|
339
|
+
cases: [{ ...historical.cases[0], target: { provider: '', observed_models: ['x'] } }],
|
|
340
|
+
})
|
|
341
|
+
).toThrow('malformed target evidence');
|
|
276
342
|
});
|
|
277
343
|
});
|
|
@@ -9,11 +9,13 @@ import { getGitInfo } from '../provenance/git';
|
|
|
9
9
|
import type {
|
|
10
10
|
CaseResult,
|
|
11
11
|
CostEstimateInfo,
|
|
12
|
+
ExecutionProvenance,
|
|
12
13
|
ManifestRedactionInfo,
|
|
13
14
|
ResolvedConfig,
|
|
14
15
|
RunConfig,
|
|
15
16
|
RunManifest,
|
|
16
17
|
RunMetrics,
|
|
18
|
+
WorkloadIdentity,
|
|
17
19
|
} from './types';
|
|
18
20
|
import { getCaseEvaluationStatus } from './types';
|
|
19
21
|
|
|
@@ -24,6 +26,8 @@ export function createRunManifest(options: {
|
|
|
24
26
|
project: string;
|
|
25
27
|
config: RunConfig;
|
|
26
28
|
resolvedConfig?: ResolvedConfig;
|
|
29
|
+
workloadIdentity?: WorkloadIdentity;
|
|
30
|
+
executionProvenance?: ExecutionProvenance;
|
|
27
31
|
cases: CaseResult[];
|
|
28
32
|
startTime: Date;
|
|
29
33
|
endTime: Date;
|
|
@@ -35,6 +39,8 @@ export function createRunManifest(options: {
|
|
|
35
39
|
project,
|
|
36
40
|
config,
|
|
37
41
|
resolvedConfig,
|
|
42
|
+
workloadIdentity,
|
|
43
|
+
executionProvenance,
|
|
38
44
|
cases,
|
|
39
45
|
startTime,
|
|
40
46
|
endTime,
|
|
@@ -50,7 +56,7 @@ export function createRunManifest(options: {
|
|
|
50
56
|
const environment = getEnvironmentInfo();
|
|
51
57
|
|
|
52
58
|
return {
|
|
53
|
-
version: '1.
|
|
59
|
+
version: '1.3',
|
|
54
60
|
run_id: nanoid(12),
|
|
55
61
|
project,
|
|
56
62
|
start_time: startTime.toISOString(),
|
|
@@ -58,6 +64,8 @@ export function createRunManifest(options: {
|
|
|
58
64
|
duration_ms: endTime.getTime() - startTime.getTime(),
|
|
59
65
|
config,
|
|
60
66
|
resolved_config: resolvedConfig,
|
|
67
|
+
workload_identity: workloadIdentity,
|
|
68
|
+
execution_provenance: executionProvenance,
|
|
61
69
|
metrics,
|
|
62
70
|
git,
|
|
63
71
|
provenance: {
|
package/src/artifacts/types.ts
CHANGED
|
@@ -42,6 +42,56 @@ export interface ManifestRedactionInfo {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Reproducible Evidence Types
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
/** A versioned SHA-256 digest of canonical, redacted assessment material. */
|
|
50
|
+
export interface ContentIdentity {
|
|
51
|
+
schema_version: '1';
|
|
52
|
+
algorithm: 'sha256';
|
|
53
|
+
digest: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Separates the scenario workload from the criteria used to judge it.
|
|
58
|
+
*
|
|
59
|
+
* The digests prove matching declared, sanitized inputs. They are not a
|
|
60
|
+
* signature or an attestation of provider behaviour.
|
|
61
|
+
*/
|
|
62
|
+
export interface WorkloadIdentity {
|
|
63
|
+
schema_version: '1';
|
|
64
|
+
workload: ContentIdentity;
|
|
65
|
+
rubric: ContentIdentity;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Bounded target identity captured from a case execution. */
|
|
69
|
+
export interface CaseTargetEvidence {
|
|
70
|
+
provider: string;
|
|
71
|
+
requested_model?: string;
|
|
72
|
+
/** Model identifiers returned by the target provider during this case. */
|
|
73
|
+
observed_models?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Requested and observed execution configuration for a complete run. */
|
|
77
|
+
export interface ExecutionProvenance {
|
|
78
|
+
schema_version: '1';
|
|
79
|
+
target: {
|
|
80
|
+
provider: string;
|
|
81
|
+
requested_models?: string[];
|
|
82
|
+
observed_models?: string[];
|
|
83
|
+
generation?: {
|
|
84
|
+
temperature?: number;
|
|
85
|
+
max_tokens?: number;
|
|
86
|
+
seed?: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
/** Judge/evaluator model identities, never combined with target identity. */
|
|
90
|
+
evaluator?: {
|
|
91
|
+
models?: string[];
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
45
95
|
// ============================================================================
|
|
46
96
|
// Case Result Types
|
|
47
97
|
// ============================================================================
|
|
@@ -101,6 +151,8 @@ export interface CaseResult {
|
|
|
101
151
|
error?: string;
|
|
102
152
|
/** Sanitized evaluator evidence; arbitrary evaluator details are never stored here. */
|
|
103
153
|
evidence?: CaseEvaluationEvidence;
|
|
154
|
+
/** Requested and observed target identity for this case. */
|
|
155
|
+
target?: CaseTargetEvidence;
|
|
104
156
|
/** Redaction information for this case */
|
|
105
157
|
redaction?: CaseRedactionInfo;
|
|
106
158
|
/** Ordered tool activity captured for an enabled tool loop. */
|
|
@@ -277,6 +329,10 @@ export interface RunManifest {
|
|
|
277
329
|
config: RunConfig;
|
|
278
330
|
/** Resolved configuration with full provider details and source tracking */
|
|
279
331
|
resolved_config?: ResolvedConfig;
|
|
332
|
+
/** Versioned identities for the declared workload and evaluation rubric. */
|
|
333
|
+
workload_identity?: WorkloadIdentity;
|
|
334
|
+
/** Requested and observed target/evaluator configuration for this run. */
|
|
335
|
+
execution_provenance?: ExecutionProvenance;
|
|
280
336
|
metrics: RunMetrics;
|
|
281
337
|
git: GitInfo;
|
|
282
338
|
provenance: ProvenanceInfo;
|
|
@@ -323,6 +379,13 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
|
|
|
323
379
|
throw new Error('Invalid run manifest: expected an object with a cases array');
|
|
324
380
|
}
|
|
325
381
|
|
|
382
|
+
if (manifest.workload_identity !== undefined) {
|
|
383
|
+
assertWorkloadIdentity(manifest.workload_identity);
|
|
384
|
+
}
|
|
385
|
+
if (manifest.execution_provenance !== undefined) {
|
|
386
|
+
assertExecutionProvenance(manifest.execution_provenance);
|
|
387
|
+
}
|
|
388
|
+
|
|
326
389
|
for (const [index, caseResult] of manifest.cases.entries()) {
|
|
327
390
|
if (!isRecord(caseResult)) {
|
|
328
391
|
throw new Error(`Invalid run manifest: case ${index} is not an object`);
|
|
@@ -341,9 +404,85 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
|
|
|
341
404
|
if (caseResult.evidence !== undefined) {
|
|
342
405
|
assertCaseEvaluationEvidence(caseResult.evidence, index);
|
|
343
406
|
}
|
|
407
|
+
if (caseResult.target !== undefined) {
|
|
408
|
+
assertCaseTargetEvidence(caseResult.target);
|
|
409
|
+
}
|
|
344
410
|
}
|
|
345
411
|
}
|
|
346
412
|
|
|
413
|
+
function assertCaseTargetEvidence(target: unknown): void {
|
|
414
|
+
if (
|
|
415
|
+
!isRecord(target) ||
|
|
416
|
+
typeof target.provider !== 'string' ||
|
|
417
|
+
target.provider.length === 0 ||
|
|
418
|
+
target.provider.length > 100 ||
|
|
419
|
+
(target.requested_model !== undefined &&
|
|
420
|
+
(typeof target.requested_model !== 'string' || target.requested_model.length > 200)) ||
|
|
421
|
+
!isBoundedStringList(target.observed_models)
|
|
422
|
+
) {
|
|
423
|
+
throw new Error('Invalid run manifest: malformed target evidence');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function assertExecutionProvenance(provenance: unknown): void {
|
|
428
|
+
if (!isRecord(provenance) || provenance.schema_version !== '1' || !isRecord(provenance.target)) {
|
|
429
|
+
throw new Error('Invalid run manifest: malformed execution provenance');
|
|
430
|
+
}
|
|
431
|
+
const target = provenance.target;
|
|
432
|
+
if (
|
|
433
|
+
typeof target.provider !== 'string' ||
|
|
434
|
+
target.provider.length === 0 ||
|
|
435
|
+
target.provider.length > 100 ||
|
|
436
|
+
!isBoundedStringList(target.requested_models) ||
|
|
437
|
+
!isBoundedStringList(target.observed_models) ||
|
|
438
|
+
(target.generation !== undefined && !isGenerationConfig(target.generation))
|
|
439
|
+
) {
|
|
440
|
+
throw new Error('Invalid run manifest: malformed execution provenance');
|
|
441
|
+
}
|
|
442
|
+
if (provenance.evaluator !== undefined) {
|
|
443
|
+
if (!isRecord(provenance.evaluator) || !isBoundedStringList(provenance.evaluator.models)) {
|
|
444
|
+
throw new Error('Invalid run manifest: malformed execution provenance');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isBoundedStringList(value: unknown): boolean {
|
|
450
|
+
return (
|
|
451
|
+
value === undefined ||
|
|
452
|
+
(Array.isArray(value) &&
|
|
453
|
+
value.length <= 100 &&
|
|
454
|
+
value.every((item) => typeof item === 'string' && item.length > 0 && item.length <= 200))
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isGenerationConfig(value: unknown): boolean {
|
|
459
|
+
if (!isRecord(value)) return false;
|
|
460
|
+
return [value.temperature, value.max_tokens, value.seed].every(
|
|
461
|
+
(item) => item === undefined || (typeof item === 'number' && Number.isFinite(item))
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function assertWorkloadIdentity(identity: unknown): void {
|
|
466
|
+
if (
|
|
467
|
+
!isRecord(identity) ||
|
|
468
|
+
identity.schema_version !== '1' ||
|
|
469
|
+
!isContentIdentity(identity.workload) ||
|
|
470
|
+
!isContentIdentity(identity.rubric)
|
|
471
|
+
) {
|
|
472
|
+
throw new Error('Invalid run manifest: malformed workload identity');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isContentIdentity(value: unknown): boolean {
|
|
477
|
+
return (
|
|
478
|
+
isRecord(value) &&
|
|
479
|
+
value.schema_version === '1' &&
|
|
480
|
+
value.algorithm === 'sha256' &&
|
|
481
|
+
typeof value.digest === 'string' &&
|
|
482
|
+
/^[a-f0-9]{64}$/.test(value.digest)
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
347
486
|
function assertCaseEvaluationEvidence(evidence: unknown, caseIndex: number): void {
|
|
348
487
|
if (
|
|
349
488
|
!isRecord(evidence) ||
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createExecutionProvenance } from './execution-provenance';
|
|
3
|
+
|
|
4
|
+
describe('createExecutionProvenance', () => {
|
|
5
|
+
test('keeps requested, observed target, and evaluator model identities separate', () => {
|
|
6
|
+
const provenance = createExecutionProvenance({
|
|
7
|
+
provider: 'openai',
|
|
8
|
+
requestedModel: 'gpt-requested',
|
|
9
|
+
temperature: 0.2,
|
|
10
|
+
maxTokens: 100,
|
|
11
|
+
seed: 7,
|
|
12
|
+
cases: [
|
|
13
|
+
{
|
|
14
|
+
id: 'one',
|
|
15
|
+
ok: true,
|
|
16
|
+
score: 1,
|
|
17
|
+
matcherType: 'exact',
|
|
18
|
+
latencyMs: 1,
|
|
19
|
+
tokens: { prompt: 1, completion: 1, total: 2 },
|
|
20
|
+
prompt: 'prompt',
|
|
21
|
+
response: 'response',
|
|
22
|
+
expected: { type: 'exact', value: 'response', caseSensitive: true },
|
|
23
|
+
tags: [],
|
|
24
|
+
target: {
|
|
25
|
+
provider: 'openai',
|
|
26
|
+
requested_model: 'gpt-requested',
|
|
27
|
+
observed_models: ['gpt-observed'],
|
|
28
|
+
},
|
|
29
|
+
evidence: { evaluator: 'llm_grader', model: 'judge-model' },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(provenance).toEqual({
|
|
35
|
+
schema_version: '1',
|
|
36
|
+
target: {
|
|
37
|
+
provider: 'openai',
|
|
38
|
+
requested_models: ['gpt-requested'],
|
|
39
|
+
observed_models: ['gpt-observed'],
|
|
40
|
+
generation: { temperature: 0.2, max_tokens: 100, seed: 7 },
|
|
41
|
+
},
|
|
42
|
+
evaluator: { models: ['judge-model'] },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('records an unavailable observed identity without inventing one', () => {
|
|
47
|
+
const provenance = createExecutionProvenance({
|
|
48
|
+
provider: 'custom',
|
|
49
|
+
requestedModel: 'requested-model',
|
|
50
|
+
cases: [],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(provenance.target).toEqual({
|
|
54
|
+
provider: 'custom',
|
|
55
|
+
requested_models: ['requested-model'],
|
|
56
|
+
});
|
|
57
|
+
expect(provenance.evaluator).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Bounded execution provenance derived from declared and observed run evidence. */
|
|
2
|
+
|
|
3
|
+
import type { CaseResult, ExecutionProvenance } from '../artifacts/types';
|
|
4
|
+
|
|
5
|
+
export function createExecutionProvenance(options: {
|
|
6
|
+
provider: string;
|
|
7
|
+
requestedModel?: string;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
seed?: number;
|
|
11
|
+
cases: CaseResult[];
|
|
12
|
+
}): ExecutionProvenance {
|
|
13
|
+
const requestedModels = uniqueStrings([
|
|
14
|
+
options.requestedModel,
|
|
15
|
+
...options.cases.map((caseResult) => caseResult.target?.requested_model),
|
|
16
|
+
]);
|
|
17
|
+
const observedModels = uniqueStrings(
|
|
18
|
+
options.cases.flatMap((caseResult) => caseResult.target?.observed_models ?? [])
|
|
19
|
+
);
|
|
20
|
+
const evaluatorModels = uniqueStrings(
|
|
21
|
+
options.cases.map((caseResult) => caseResult.evidence?.model)
|
|
22
|
+
);
|
|
23
|
+
const generation = omitUndefined({
|
|
24
|
+
temperature: options.temperature,
|
|
25
|
+
max_tokens: options.maxTokens,
|
|
26
|
+
seed: options.seed,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
schema_version: '1',
|
|
31
|
+
target: {
|
|
32
|
+
provider: boundedString(options.provider, 100) ?? 'unknown',
|
|
33
|
+
...(requestedModels.length ? { requested_models: requestedModels } : {}),
|
|
34
|
+
...(observedModels.length ? { observed_models: observedModels } : {}),
|
|
35
|
+
...(Object.keys(generation).length ? { generation } : {}),
|
|
36
|
+
},
|
|
37
|
+
...(evaluatorModels.length ? { evaluator: { models: evaluatorModels } } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function uniqueStrings(values: unknown[]): string[] {
|
|
42
|
+
return [...new Set(values.map((value) => boundedString(value, 200)).filter(Boolean))] as string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function boundedString(value: unknown, maxLength: number): string | undefined {
|
|
46
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, maxLength) : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function omitUndefined<T extends Record<string, number | undefined>>(value: T): Partial<T> {
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
Object.entries(value).filter(([, item]) => item !== undefined)
|
|
52
|
+
) as Partial<T>;
|
|
53
|
+
}
|
package/src/provenance/index.ts
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { validateScenario } from '../scenario';
|
|
3
|
+
import { createWorkloadIdentity } from './workload-identity';
|
|
4
|
+
|
|
5
|
+
const baseScenario = () =>
|
|
6
|
+
validateScenario({
|
|
7
|
+
name: 'Customer-service policy test',
|
|
8
|
+
version: '2026-09',
|
|
9
|
+
providerConfig: { apiKey: 'sk-live-very-secret-value' },
|
|
10
|
+
setup: {
|
|
11
|
+
systemPrompt: 'Use token=internal-secret only for this fixture.',
|
|
12
|
+
},
|
|
13
|
+
cases: [
|
|
14
|
+
{
|
|
15
|
+
id: 'refund-policy',
|
|
16
|
+
prompt: 'Can I receive a refund?',
|
|
17
|
+
expected: {
|
|
18
|
+
type: 'llm_grader',
|
|
19
|
+
rubric: 'Approve only eligible refunds.',
|
|
20
|
+
strict: true,
|
|
21
|
+
threshold: 0.8,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('createWorkloadIdentity', () => {
|
|
28
|
+
test('is deterministic for equivalent declared scenario material', () => {
|
|
29
|
+
const first = createWorkloadIdentity(baseScenario());
|
|
30
|
+
const second = createWorkloadIdentity(baseScenario());
|
|
31
|
+
|
|
32
|
+
expect(first).toEqual(second);
|
|
33
|
+
expect(first.workload.digest).toHaveLength(64);
|
|
34
|
+
expect(first.rubric.digest).toHaveLength(64);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('separates workload changes from rubric changes', () => {
|
|
38
|
+
const base = baseScenario();
|
|
39
|
+
const changedPrompt = structuredClone(base);
|
|
40
|
+
changedPrompt.cases[0].prompt = 'Can I receive a replacement?';
|
|
41
|
+
const changedRubric = structuredClone(base);
|
|
42
|
+
changedRubric.cases[0].expected = {
|
|
43
|
+
type: 'llm_grader',
|
|
44
|
+
rubric: 'Approve only eligible replacements.',
|
|
45
|
+
strict: true,
|
|
46
|
+
threshold: 0.8,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const baseIdentity = createWorkloadIdentity(base);
|
|
50
|
+
const promptIdentity = createWorkloadIdentity(changedPrompt);
|
|
51
|
+
const rubricIdentity = createWorkloadIdentity(changedRubric);
|
|
52
|
+
|
|
53
|
+
expect(promptIdentity.workload.digest).not.toBe(baseIdentity.workload.digest);
|
|
54
|
+
expect(promptIdentity.rubric.digest).toBe(baseIdentity.rubric.digest);
|
|
55
|
+
expect(rubricIdentity.workload.digest).toBe(baseIdentity.workload.digest);
|
|
56
|
+
expect(rubricIdentity.rubric.digest).not.toBe(baseIdentity.rubric.digest);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('excludes sensitive configuration and recognized secret text before hashing', () => {
|
|
60
|
+
const first = baseScenario();
|
|
61
|
+
first.cases[0].metadata = { author: 'ArtemisKit', authToken: 'fixture-token-one' };
|
|
62
|
+
const second = structuredClone(first);
|
|
63
|
+
second.providerConfig = { apiKey: 'sk-live-another-secret-value' };
|
|
64
|
+
second.setup = { systemPrompt: 'Use token=different-secret only for this fixture.' };
|
|
65
|
+
second.cases[0].metadata = { author: 'ArtemisKit', authToken: 'fixture-token-two' };
|
|
66
|
+
|
|
67
|
+
expect(createWorkloadIdentity(second)).toEqual(createWorkloadIdentity(first));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical, redacted workload identities for reproducible assessment evidence.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import type { WorkloadIdentity } from '../artifacts/types';
|
|
7
|
+
import { redactText, resolvePatterns } from '../redaction/redactor';
|
|
8
|
+
import { DEFAULT_REDACTION_PATTERNS } from '../redaction/types';
|
|
9
|
+
import type { Scenario } from '../scenario/schema';
|
|
10
|
+
|
|
11
|
+
const SENSITIVE_KEY = /(?:api[-_]?key|authorization|auth|credential|password|secret|token)$/i;
|
|
12
|
+
const REDACTED_SECRET = '[REDACTED_SECRET]';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create independent identities for the declared workload and its evaluation
|
|
16
|
+
* criteria. Input is sorted, redacted, and hashed locally; raw material is
|
|
17
|
+
* never retained in the identity artifact.
|
|
18
|
+
*/
|
|
19
|
+
export function createWorkloadIdentity(scenario: Scenario): WorkloadIdentity {
|
|
20
|
+
const patterns = resolvePatterns([
|
|
21
|
+
...DEFAULT_REDACTION_PATTERNS,
|
|
22
|
+
...(scenario.redaction?.patterns ?? []),
|
|
23
|
+
]).map(({ regex }) => regex);
|
|
24
|
+
|
|
25
|
+
const workload = {
|
|
26
|
+
name: scenario.name,
|
|
27
|
+
version: scenario.version,
|
|
28
|
+
description: scenario.description,
|
|
29
|
+
tags: scenario.tags,
|
|
30
|
+
variables: scenario.variables,
|
|
31
|
+
setup: scenario.setup,
|
|
32
|
+
cases: scenario.cases.map((testCase) => ({
|
|
33
|
+
id: testCase.id,
|
|
34
|
+
name: testCase.name,
|
|
35
|
+
description: testCase.description,
|
|
36
|
+
prompt: testCase.prompt,
|
|
37
|
+
tags: testCase.tags,
|
|
38
|
+
metadata: testCase.metadata,
|
|
39
|
+
timeout: testCase.timeout,
|
|
40
|
+
retries: testCase.retries,
|
|
41
|
+
variables: testCase.variables,
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const rubric = {
|
|
46
|
+
cases: scenario.cases.map((testCase) => ({
|
|
47
|
+
id: testCase.id,
|
|
48
|
+
expected: testCase.expected,
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
schema_version: '1',
|
|
54
|
+
workload: createContentIdentity(workload, patterns),
|
|
55
|
+
rubric: createContentIdentity(rubric, patterns),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createContentIdentity(value: unknown, patterns: RegExp[]) {
|
|
60
|
+
const canonical = JSON.stringify(canonicalize(value, patterns));
|
|
61
|
+
return {
|
|
62
|
+
schema_version: '1' as const,
|
|
63
|
+
algorithm: 'sha256' as const,
|
|
64
|
+
digest: createHash('sha256').update(canonical).digest('hex'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canonicalize(value: unknown, patterns: RegExp[], key?: string): unknown {
|
|
69
|
+
if (key && SENSITIVE_KEY.test(key)) return REDACTED_SECRET;
|
|
70
|
+
|
|
71
|
+
if (typeof value === 'string') {
|
|
72
|
+
return redactText(value, patterns, REDACTED_SECRET).text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
return value.map((item) => canonicalize(item, patterns));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value && typeof value === 'object') {
|
|
80
|
+
return Object.fromEntries(
|
|
81
|
+
Object.entries(value)
|
|
82
|
+
.filter(([, nestedValue]) => nestedValue !== undefined)
|
|
83
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
84
|
+
.map(([nestedKey, nestedValue]) => [
|
|
85
|
+
nestedKey,
|
|
86
|
+
canonicalize(nestedValue, patterns, nestedKey),
|
|
87
|
+
])
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
@@ -170,6 +170,11 @@ describe('executeCase tool loop', () => {
|
|
|
170
170
|
expect(result.error).toBe('TOOL_EXECUTOR_REQUIRED');
|
|
171
171
|
expect(result.latencyMs).toBe(4);
|
|
172
172
|
expect(result.tokens).toEqual({ prompt: 7, completion: 2, total: 9 });
|
|
173
|
+
expect(result.target).toEqual({
|
|
174
|
+
provider: 'ling',
|
|
175
|
+
requested_model: 'Ling-3.0-flash',
|
|
176
|
+
observed_models: ['Ling-3.0-flash'],
|
|
177
|
+
});
|
|
173
178
|
});
|
|
174
179
|
|
|
175
180
|
it('retains prior generation metrics when a later generation rejects', async () => {
|
|
@@ -201,6 +206,7 @@ describe('executeCase tool loop', () => {
|
|
|
201
206
|
steps: 1,
|
|
202
207
|
terminationReason: 'tool_error',
|
|
203
208
|
});
|
|
209
|
+
expect(result.target?.observed_models).toEqual(['Ling-3.0-flash']);
|
|
204
210
|
});
|
|
205
211
|
|
|
206
212
|
it('retains prior generation metrics when a later generation times out', async () => {
|
|
@@ -304,7 +310,11 @@ describe('executeCase measurement integrity', () => {
|
|
|
304
310
|
status: 'error',
|
|
305
311
|
response: '',
|
|
306
312
|
error: 'provider unavailable',
|
|
313
|
+
target: {
|
|
314
|
+
provider: 'test',
|
|
315
|
+
},
|
|
307
316
|
});
|
|
317
|
+
expect(result.target?.observed_models).toBeUndefined();
|
|
308
318
|
});
|
|
309
319
|
|
|
310
320
|
it('retains only the bounded evidence contract rather than evaluator details', async () => {
|