nexus-ai-pro 2.4.0 → 2.5.0
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/API_STABILITY.md +85 -0
- package/CHANGELOG.md +147 -1
- package/README.md +8 -3
- package/SECURITY.md +78 -9
- package/dist/context-hub/hub.d.ts +53 -0
- package/dist/context-hub/hub.js +108 -3
- package/dist/context-hub/index.d.ts +2 -1
- package/dist/context-hub/index.js +2 -1
- package/dist/context-hub/signing.d.ts +95 -0
- package/dist/context-hub/signing.js +88 -0
- package/dist/doctor/index.d.ts +4 -2
- package/dist/doctor/index.js +29 -0
- package/dist/evaluate/threads.d.ts +187 -0
- package/dist/evaluate/threads.js +187 -0
- package/dist/operations/adapters.d.ts +2 -1
- package/dist/operations/runner.js +2 -0
- package/dist/operations/stats.js +11 -4
- package/dist/postgres/index.d.ts +2 -1
- package/dist/postgres/index.js +4 -0
- package/dist/postgres/operations.js +11 -4
- package/dist/postgres/store.d.ts +7 -0
- package/dist/postgres/store.js +21 -0
- package/dist/postgres/tenancy.d.ts +43 -0
- package/dist/postgres/tenancy.js +71 -0
- package/dist/server/deployments.d.ts +127 -12
- package/dist/server/deployments.js +402 -37
- package/dist/server/metrics.d.ts +2 -0
- package/dist/server/metrics.js +7 -0
- package/dist/server/runs.d.ts +9 -1
- package/dist/server/runs.js +46 -7
- package/dist/server/server.js +4 -1
- package/dist/server/state.d.ts +12 -2
- package/dist/server/state.js +15 -0
- package/dist/sqlite/operations.js +15 -4
- package/dist/sqlite/store.d.ts +7 -0
- package/dist/sqlite/store.js +28 -0
- package/dist/store/memory.d.ts +8 -0
- package/dist/store/memory.js +12 -1
- package/dist/store/redis.d.ts +13 -0
- package/dist/store/redis.js +38 -0
- package/dist/tracing/instrument.js +1 -0
- package/dist/tracing/tracer.d.ts +4 -0
- package/dist/tracing/tracer.js +6 -0
- package/dist/types/operations.d.ts +7 -0
- package/dist/types/server.d.ts +61 -2
- package/dist/types/store.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/context-hub/hub.d.ts +53 -0
- package/dist-cjs/context-hub/hub.js +110 -4
- package/dist-cjs/context-hub/index.d.ts +2 -1
- package/dist-cjs/context-hub/index.js +7 -1
- package/dist-cjs/context-hub/signing.d.ts +95 -0
- package/dist-cjs/context-hub/signing.js +96 -0
- package/dist-cjs/doctor/index.d.ts +4 -2
- package/dist-cjs/doctor/index.js +29 -0
- package/dist-cjs/evaluate/threads.d.ts +187 -0
- package/dist-cjs/evaluate/threads.js +198 -0
- package/dist-cjs/operations/adapters.d.ts +2 -1
- package/dist-cjs/operations/runner.js +2 -0
- package/dist-cjs/operations/stats.js +11 -4
- package/dist-cjs/postgres/index.d.ts +2 -1
- package/dist-cjs/postgres/index.js +8 -1
- package/dist-cjs/postgres/operations.js +11 -4
- package/dist-cjs/postgres/store.d.ts +7 -0
- package/dist-cjs/postgres/store.js +21 -0
- package/dist-cjs/postgres/tenancy.d.ts +43 -0
- package/dist-cjs/postgres/tenancy.js +110 -0
- package/dist-cjs/server/deployments.d.ts +127 -12
- package/dist-cjs/server/deployments.js +402 -37
- package/dist-cjs/server/metrics.d.ts +2 -0
- package/dist-cjs/server/metrics.js +8 -0
- package/dist-cjs/server/runs.d.ts +9 -1
- package/dist-cjs/server/runs.js +45 -6
- package/dist-cjs/server/server.js +4 -1
- package/dist-cjs/server/state.d.ts +12 -2
- package/dist-cjs/server/state.js +15 -0
- package/dist-cjs/sqlite/operations.js +15 -4
- package/dist-cjs/sqlite/store.d.ts +7 -0
- package/dist-cjs/sqlite/store.js +28 -0
- package/dist-cjs/store/memory.d.ts +8 -0
- package/dist-cjs/store/memory.js +12 -1
- package/dist-cjs/store/redis.d.ts +13 -0
- package/dist-cjs/store/redis.js +38 -0
- package/dist-cjs/tracing/instrument.js +1 -0
- package/dist-cjs/tracing/tracer.d.ts +4 -0
- package/dist-cjs/tracing/tracer.js +6 -0
- package/dist-cjs/types/operations.d.ts +7 -0
- package/dist-cjs/types/server.d.ts +61 -2
- package/dist-cjs/types/store.d.ts +9 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +27 -1
package/dist/context-hub/hub.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MemoryPromptStore } from '../prompts/memory.js';
|
|
2
2
|
import { canonicalJson } from '../prompts/version.js';
|
|
3
3
|
import { diffContexts } from './diff.js';
|
|
4
|
+
import { contextDigest, signDigest, verifiedBy, } from './signing.js';
|
|
4
5
|
export class ContextHubError extends Error {
|
|
5
6
|
code;
|
|
6
7
|
constructor(message, code) {
|
|
@@ -36,6 +37,16 @@ export class ContextPromotionError extends ContextHubError {
|
|
|
36
37
|
this.name = 'ContextPromotionError';
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
export class ContextSignatureError extends ContextHubError {
|
|
41
|
+
bundle;
|
|
42
|
+
version;
|
|
43
|
+
constructor(bundle, version, reason) {
|
|
44
|
+
super(`Context bundle "${bundle}" ${version}: ${reason}`, 'CONTEXT_SIGNATURE');
|
|
45
|
+
this.bundle = bundle;
|
|
46
|
+
this.version = version;
|
|
47
|
+
this.name = 'ContextSignatureError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
39
50
|
export class ContextDefinitionError extends ContextHubError {
|
|
40
51
|
constructor(message) {
|
|
41
52
|
super(message, 'CONTEXT_DEFINITION_ERROR');
|
|
@@ -63,12 +74,19 @@ export class ContextHub {
|
|
|
63
74
|
options;
|
|
64
75
|
store;
|
|
65
76
|
now;
|
|
77
|
+
trusted = new Map();
|
|
66
78
|
constructor(options = {}) {
|
|
67
79
|
this.options = options;
|
|
80
|
+
if (options.requireSignature && !options.keyring) {
|
|
81
|
+
throw new TypeError('requireSignature needs a keyring of the keys the hub trusts');
|
|
82
|
+
}
|
|
68
83
|
this.store = options.store ?? new MemoryPromptStore();
|
|
69
84
|
this.now = options.now ?? (() => new Date());
|
|
70
85
|
}
|
|
71
86
|
async commit(definition, options = {}) {
|
|
87
|
+
return this.commitSigned(definition, options, []);
|
|
88
|
+
}
|
|
89
|
+
async commitSigned(definition, options, inherited) {
|
|
72
90
|
validate(definition);
|
|
73
91
|
if (this.options.prompts) {
|
|
74
92
|
for (const [role, pin] of Object.entries(definition.prompts ?? {})) {
|
|
@@ -92,6 +110,13 @@ export class ContextHub {
|
|
|
92
110
|
...(options.author === undefined ? {} : { author: options.author }),
|
|
93
111
|
...(newest ? { parent: newest.version } : {}),
|
|
94
112
|
};
|
|
113
|
+
const signatures = [...inherited];
|
|
114
|
+
if (this.options.signer) {
|
|
115
|
+
const digest = await contextDigest(await this.signedContent(definition));
|
|
116
|
+
signatures.push(await signDigest(this.options.signer, digest, this.now()));
|
|
117
|
+
}
|
|
118
|
+
if (signatures.length > 0)
|
|
119
|
+
stored.signatures = signatures;
|
|
95
120
|
await this.store.saveVersion(toStored(stored));
|
|
96
121
|
await this.record({ name: definition.name, action: 'commit', version: id, by: options.author });
|
|
97
122
|
}
|
|
@@ -118,8 +143,19 @@ export class ContextHub {
|
|
|
118
143
|
throw new ContextNotFoundError(name, ref);
|
|
119
144
|
return found;
|
|
120
145
|
}
|
|
146
|
+
async verify(bundle) {
|
|
147
|
+
const key = `${bundle.name}@${bundle.version}`;
|
|
148
|
+
let pending = this.trusted.get(key);
|
|
149
|
+
if (!pending) {
|
|
150
|
+
pending = this.check(bundle, await this.signedContent(bundle));
|
|
151
|
+
this.trusted.set(key, pending);
|
|
152
|
+
pending.catch(() => this.trusted.delete(key));
|
|
153
|
+
}
|
|
154
|
+
return pending;
|
|
155
|
+
}
|
|
121
156
|
async resolve(name, ref = 'latest') {
|
|
122
157
|
const bundle = await this.get(name, ref);
|
|
158
|
+
await this.assertTrusted(bundle);
|
|
123
159
|
return {
|
|
124
160
|
bundle,
|
|
125
161
|
reference: {
|
|
@@ -133,6 +169,7 @@ export class ContextHub {
|
|
|
133
169
|
const registry = this.options.prompts;
|
|
134
170
|
if (!registry)
|
|
135
171
|
throw new ContextDefinitionError('Rendering a pinned prompt needs the hub to have a prompt registry');
|
|
172
|
+
await this.assertTrusted(bundle);
|
|
136
173
|
const pin = bundle.prompts?.[role];
|
|
137
174
|
if (!pin)
|
|
138
175
|
throw new ContextNotFoundError(bundle.name, `prompt role "${role}"`);
|
|
@@ -259,15 +296,52 @@ export class ContextHub {
|
|
|
259
296
|
throw new ContextDefinitionError(`Prompt ${prompt.name} was exported as ${version} but its content is ${committed.version}`);
|
|
260
297
|
}
|
|
261
298
|
}
|
|
262
|
-
const { version, createdAt: _createdAt, parent: _parent, message, author, ...definition } = exported.bundle;
|
|
299
|
+
const { version, createdAt: _createdAt, parent: _parent, message, author, signatures = [], ...definition } = exported.bundle;
|
|
263
300
|
if ((await contextVersion(definition)) !== version) {
|
|
264
301
|
throw new ContextDefinitionError(`Bundle ${definition.name} was exported as ${version} but its content has changed`);
|
|
265
302
|
}
|
|
266
|
-
|
|
303
|
+
const digest = await contextDigest(signedContentOf(definition, exportedPrompts(exported)));
|
|
304
|
+
const claims = signatures.filter((signature) => signature?.digest === digest);
|
|
305
|
+
if (this.options.requireSignature && this.options.keyring) {
|
|
306
|
+
const signed = await verifiedBy(this.options.keyring, digest, claims);
|
|
307
|
+
if (!signed) {
|
|
308
|
+
throw new ContextSignatureError(definition.name, version, signatures.length > 0
|
|
309
|
+
? 'no signature verifies against its content with a trusted key'
|
|
310
|
+
: 'it is not signed, and this hub requires a signature');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return this.commitSigned(definition, {
|
|
267
314
|
message: message ?? 'imported',
|
|
268
315
|
...((options.by ?? author) === undefined ? {} : { author: options.by ?? author }),
|
|
269
316
|
...(options.label === undefined ? {} : { label: options.label }),
|
|
270
|
-
});
|
|
317
|
+
}, claims);
|
|
318
|
+
}
|
|
319
|
+
async assertTrusted(bundle) {
|
|
320
|
+
if (!this.options.requireSignature)
|
|
321
|
+
return;
|
|
322
|
+
const verification = await this.verify(bundle);
|
|
323
|
+
if (!verification.trusted) {
|
|
324
|
+
throw new ContextSignatureError(bundle.name, bundle.version, bundle.signatures?.length
|
|
325
|
+
? 'no signature verifies against its content with a trusted key'
|
|
326
|
+
: 'it is not signed, and this hub requires a signature');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async check(bundle, content) {
|
|
330
|
+
const digest = await contextDigest(content);
|
|
331
|
+
const signature = this.options.keyring
|
|
332
|
+
? await verifiedBy(this.options.keyring, digest, bundle.signatures)
|
|
333
|
+
: undefined;
|
|
334
|
+
return { trusted: signature !== undefined, digest, ...(signature ? { signature } : {}) };
|
|
335
|
+
}
|
|
336
|
+
async signedContent(definition) {
|
|
337
|
+
const pins = Object.entries(definition.prompts ?? {});
|
|
338
|
+
if (pins.length > 0 && !this.options.prompts) {
|
|
339
|
+
throw new ContextDefinitionError(`Bundle "${definition.name}" pins prompts; signing or verifying it needs the hub to have a prompt registry`);
|
|
340
|
+
}
|
|
341
|
+
const prompts = [];
|
|
342
|
+
for (const [, pin] of pins)
|
|
343
|
+
prompts.push(await this.options.prompts.get(pin.name, pin.version));
|
|
344
|
+
return signedContentOf(definition, prompts);
|
|
271
345
|
}
|
|
272
346
|
async read(name, version) {
|
|
273
347
|
const stored = await this.store.getVersion(name, version);
|
|
@@ -304,6 +378,37 @@ function validate(definition) {
|
|
|
304
378
|
}
|
|
305
379
|
}
|
|
306
380
|
}
|
|
381
|
+
function signedContentOf(definition, prompts) {
|
|
382
|
+
const byPin = new Map(prompts.map((prompt) => [`${prompt.name}@${prompt.version}`, prompt]));
|
|
383
|
+
const pinned = {};
|
|
384
|
+
for (const [role, pin] of Object.entries(definition.prompts ?? {})) {
|
|
385
|
+
const prompt = byPin.get(`${pin.name}@${pin.version}`);
|
|
386
|
+
if (!prompt)
|
|
387
|
+
throw new ContextDefinitionError(`Bundle "${definition.name}" pins ${role} to ${pin.name}@${pin.version}, which is not at hand`);
|
|
388
|
+
pinned[role] = {
|
|
389
|
+
name: prompt.name,
|
|
390
|
+
messages: prompt.messages,
|
|
391
|
+
partials: prompt.partials,
|
|
392
|
+
config: prompt.config,
|
|
393
|
+
defaults: prompt.defaults,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
bundle: {
|
|
398
|
+
name: definition.name,
|
|
399
|
+
description: definition.description,
|
|
400
|
+
prompts: definition.prompts,
|
|
401
|
+
instructions: definition.instructions,
|
|
402
|
+
tools: definition.tools,
|
|
403
|
+
skills: definition.skills,
|
|
404
|
+
config: definition.config,
|
|
405
|
+
},
|
|
406
|
+
prompts: pinned,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function exportedPrompts(exported) {
|
|
410
|
+
return exported.prompts;
|
|
411
|
+
}
|
|
307
412
|
function toStored(bundle) {
|
|
308
413
|
return { ...bundle, messages: [], variables: [] };
|
|
309
414
|
}
|
|
@@ -4,8 +4,9 @@ import type { Dataset, DatasetExample, Evaluator, Experiment } from '../types/ev
|
|
|
4
4
|
import type { ToolDefinition } from '../types/messages.js';
|
|
5
5
|
import type { ContextBundle, ContextPromotionGate } from './hub.js';
|
|
6
6
|
export { type ContextChange, type ContextDiff, diffContexts, formatContextDiff } from './diff.js';
|
|
7
|
-
export { type ContextBundle, type ContextBundleDefinition, type ContextBundleExport, ContextConflictError, ContextDefinitionError, type ContextHistoryEntry, ContextHub, ContextHubError, type ContextHubOptions, type ContextLabel, ContextNotFoundError, type ContextPromotionContext, ContextPromotionError, type ContextPromotionGate, type ContextPromotionResult, type ContextPromptPin, type ContextReference, type ContextSkill, type ContextTool, contextVersion, } from './hub.js';
|
|
7
|
+
export { type ContextBundle, type ContextBundleDefinition, type ContextBundleExport, ContextConflictError, ContextDefinitionError, type ContextHistoryEntry, ContextHub, ContextHubError, type ContextHubOptions, type ContextLabel, ContextNotFoundError, type ContextPromotionContext, ContextPromotionError, type ContextPromotionGate, ContextSignatureError, type ContextVerification, type ContextPromotionResult, type ContextPromptPin, type ContextReference, type ContextSkill, type ContextTool, contextVersion, } from './hub.js';
|
|
8
8
|
export type { ExperimentGateOptions } from '../prompts/gates.js';
|
|
9
|
+
export { type ContextCryptoKey, type ContextJsonWebKey, type ContextKeyring, type ContextSignature, type ContextSignedContent, type ContextSigner, type ContextSigningKey, contextDigest, ed25519Keyring, ed25519Signer, generateContextKey, } from './signing.js';
|
|
9
10
|
/**
|
|
10
11
|
* A bundle's instructions joined into one system text, in the order the bundle lists them, or only
|
|
11
12
|
* the ones named in `include`, in that order.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { fingerprintOf } from '../evaluate/cache.js';
|
|
2
2
|
import { experimentVerdict } from '../prompts/gates.js';
|
|
3
3
|
export { diffContexts, formatContextDiff } from './diff.js';
|
|
4
|
-
export { ContextConflictError, ContextDefinitionError, ContextHub, ContextHubError, ContextNotFoundError, ContextPromotionError, contextVersion, } from './hub.js';
|
|
4
|
+
export { ContextConflictError, ContextDefinitionError, ContextHub, ContextHubError, ContextNotFoundError, ContextPromotionError, ContextSignatureError, contextVersion, } from './hub.js';
|
|
5
|
+
export { contextDigest, ed25519Keyring, ed25519Signer, generateContextKey, } from './signing.js';
|
|
5
6
|
export function contextInstructions(bundle, options = {}) {
|
|
6
7
|
const names = options.include ?? Object.keys(bundle.instructions ?? {});
|
|
7
8
|
return names
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { PromptDefinition } from '../types/prompts.js';
|
|
2
|
+
/** A JSON Web Key, as Web Crypto exports one. */
|
|
3
|
+
export interface ContextJsonWebKey {
|
|
4
|
+
/** The key type: `OKP` for Ed25519. */
|
|
5
|
+
kty?: string;
|
|
6
|
+
/** The curve: `Ed25519`. */
|
|
7
|
+
crv?: string;
|
|
8
|
+
/** The public key, in base64url. */
|
|
9
|
+
x?: string;
|
|
10
|
+
/** The private key, in base64url, on a private key only. */
|
|
11
|
+
d?: string;
|
|
12
|
+
/** Anything else the exporter wrote. */
|
|
13
|
+
[field: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** A Web Crypto key, as `crypto.subtle` returns one. Any runtime's `CryptoKey` is one. */
|
|
16
|
+
export interface ContextCryptoKey {
|
|
17
|
+
/** Its algorithm. */
|
|
18
|
+
readonly algorithm: unknown;
|
|
19
|
+
/** What it may do. */
|
|
20
|
+
readonly usages: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/** One signature on a bundle version. Stored with it, and carried by its export. */
|
|
23
|
+
export interface ContextSignature {
|
|
24
|
+
/** The id of the key that signed, which a keyring looks up. */
|
|
25
|
+
keyId: string;
|
|
26
|
+
/** Always `Ed25519`. */
|
|
27
|
+
algorithm: 'Ed25519';
|
|
28
|
+
/**
|
|
29
|
+
* What was signed: `sha256:` and the full SHA-256, in hex, of the bundle's content and the content
|
|
30
|
+
* of every prompt it pins. A bundle's version is only the first 12 hex digits, enough to name a
|
|
31
|
+
* version but not to stand behind a signature.
|
|
32
|
+
*/
|
|
33
|
+
digest: string;
|
|
34
|
+
/** The signature over the digest, in base64url. */
|
|
35
|
+
signature: string;
|
|
36
|
+
/** ISO-8601 time it was signed. */
|
|
37
|
+
signedAt: string;
|
|
38
|
+
}
|
|
39
|
+
/** Signs bundle digests with a private key, under a key id. */
|
|
40
|
+
export interface ContextSigner {
|
|
41
|
+
/** The key's id, recorded on each signature so a keyring finds the public key. */
|
|
42
|
+
keyId: string;
|
|
43
|
+
/** Signs bytes. */
|
|
44
|
+
sign(data: Uint8Array): Promise<Uint8Array> | Uint8Array;
|
|
45
|
+
}
|
|
46
|
+
/** Verifies signatures by key id. A signature by a key the keyring does not hold never verifies. */
|
|
47
|
+
export interface ContextKeyring {
|
|
48
|
+
/** Whether `signature` is `keyId`'s signature over `data`. */
|
|
49
|
+
verify(keyId: string, data: Uint8Array, signature: Uint8Array): Promise<boolean> | boolean;
|
|
50
|
+
}
|
|
51
|
+
/** A new signing key, with both halves as JSON Web Keys to keep in a secret store. */
|
|
52
|
+
export interface ContextSigningKey {
|
|
53
|
+
/** The key's id. */
|
|
54
|
+
keyId: string;
|
|
55
|
+
/** Signs with the new key. */
|
|
56
|
+
signer: ContextSigner;
|
|
57
|
+
/** The public half, for every keyring that should trust it. */
|
|
58
|
+
publicKey: ContextJsonWebKey;
|
|
59
|
+
/** The private half. Keep it where the signer runs, and nowhere else. */
|
|
60
|
+
privateKey: ContextJsonWebKey;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates an Ed25519 signing key through Web Crypto, so on any runtime. The key id defaults to the
|
|
64
|
+
* first 16 hex digits of the public key's SHA-256, which is stable for the key and names nothing
|
|
65
|
+
* else.
|
|
66
|
+
*/
|
|
67
|
+
export declare function generateContextKey(keyId?: string): Promise<ContextSigningKey>;
|
|
68
|
+
/** A signer from an Ed25519 private key, as a JSON Web Key or a Web Crypto key. */
|
|
69
|
+
export declare function ed25519Signer(keyId: string, privateKey: ContextJsonWebKey | ContextCryptoKey): Promise<ContextSigner>;
|
|
70
|
+
/** A keyring of trusted Ed25519 public keys, by key id. */
|
|
71
|
+
export declare function ed25519Keyring(keys: Record<string, ContextJsonWebKey | ContextCryptoKey>): Promise<ContextKeyring>;
|
|
72
|
+
/** What a bundle signature covers: the bundle's versioned content, and its pinned prompts' content. */
|
|
73
|
+
export interface ContextSignedContent {
|
|
74
|
+
/** The bundle's name and the content its version covers. */
|
|
75
|
+
bundle: {
|
|
76
|
+
name: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
prompts?: Record<string, {
|
|
79
|
+
name: string;
|
|
80
|
+
version: string;
|
|
81
|
+
}>;
|
|
82
|
+
instructions?: Record<string, string>;
|
|
83
|
+
tools?: unknown;
|
|
84
|
+
skills?: unknown;
|
|
85
|
+
config?: Record<string, unknown>;
|
|
86
|
+
};
|
|
87
|
+
/** Each pinned prompt's content, by role: what its own version covers. */
|
|
88
|
+
prompts: Record<string, Pick<PromptDefinition, 'name' | 'messages' | 'partials' | 'config' | 'defaults'>>;
|
|
89
|
+
}
|
|
90
|
+
/** The digest a signature covers, as `sha256:` and 64 hex digits. */
|
|
91
|
+
export declare function contextDigest(content: ContextSignedContent): Promise<string>;
|
|
92
|
+
/** Signs a digest. */
|
|
93
|
+
export declare function signDigest(signer: ContextSigner, digest: string, at: Date): Promise<ContextSignature>;
|
|
94
|
+
/** The first signature that is a trusted key's signature over exactly this digest, if any. */
|
|
95
|
+
export declare function verifiedBy(keyring: ContextKeyring, digest: string, signatures?: readonly ContextSignature[]): Promise<ContextSignature | undefined>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { canonicalJson } from '../prompts/version.js';
|
|
2
|
+
import { decodeBase64, encodeBase64 } from '../utils/base64.js';
|
|
3
|
+
const subtle = () => globalThis.crypto.subtle;
|
|
4
|
+
const ED25519 = { name: 'Ed25519' };
|
|
5
|
+
export async function generateContextKey(keyId) {
|
|
6
|
+
const pair = await subtle().generateKey(ED25519, true, ['sign', 'verify']);
|
|
7
|
+
const publicKey = await subtle().exportKey('jwk', pair.publicKey);
|
|
8
|
+
const privateKey = await subtle().exportKey('jwk', pair.privateKey);
|
|
9
|
+
const id = keyId ?? (await keyIdOf(publicKey));
|
|
10
|
+
return { keyId: id, signer: await ed25519Signer(id, pair.privateKey), publicKey, privateKey };
|
|
11
|
+
}
|
|
12
|
+
export async function ed25519Signer(keyId, privateKey) {
|
|
13
|
+
const key = isCryptoKey(privateKey)
|
|
14
|
+
? privateKey
|
|
15
|
+
: await subtle().importKey('jwk', privateKey, ED25519, false, ['sign']);
|
|
16
|
+
return { keyId, sign: async (data) => new Uint8Array(await subtle().sign(ED25519, key, data)) };
|
|
17
|
+
}
|
|
18
|
+
export async function ed25519Keyring(keys) {
|
|
19
|
+
const imported = new Map();
|
|
20
|
+
for (const [keyId, key] of Object.entries(keys)) {
|
|
21
|
+
imported.set(keyId, isCryptoKey(key) ? key : await subtle().importKey('jwk', key, ED25519, false, ['verify']));
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
async verify(keyId, data, signature) {
|
|
25
|
+
const key = imported.get(keyId);
|
|
26
|
+
if (!key)
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
return await subtle().verify(ED25519, key, signature, data);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function contextDigest(content) {
|
|
38
|
+
const text = canonicalJson({ format: 'nexus-context-signature', formatVersion: 1, ...content });
|
|
39
|
+
const digest = await subtle().digest('SHA-256', new TextEncoder().encode(text));
|
|
40
|
+
return `sha256:${hexOf(new Uint8Array(digest))}`;
|
|
41
|
+
}
|
|
42
|
+
export async function signDigest(signer, digest, at) {
|
|
43
|
+
const signature = await signer.sign(new TextEncoder().encode(digest));
|
|
44
|
+
return {
|
|
45
|
+
keyId: signer.keyId,
|
|
46
|
+
algorithm: 'Ed25519',
|
|
47
|
+
digest,
|
|
48
|
+
signature: toBase64Url(signature),
|
|
49
|
+
signedAt: at.toISOString(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function verifiedBy(keyring, digest, signatures = []) {
|
|
53
|
+
const data = new TextEncoder().encode(digest);
|
|
54
|
+
for (const signature of signatures) {
|
|
55
|
+
if (signature?.algorithm !== 'Ed25519' || signature.digest !== digest)
|
|
56
|
+
continue;
|
|
57
|
+
let bytes;
|
|
58
|
+
try {
|
|
59
|
+
bytes = fromBase64Url(signature.signature);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (await keyring.verify(signature.keyId, data, bytes))
|
|
65
|
+
return signature;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
async function keyIdOf(publicKey) {
|
|
70
|
+
const digest = await subtle().digest('SHA-256', new TextEncoder().encode(String(publicKey.x)));
|
|
71
|
+
return hexOf(new Uint8Array(digest).subarray(0, 8));
|
|
72
|
+
}
|
|
73
|
+
function hexOf(bytes) {
|
|
74
|
+
let hex = '';
|
|
75
|
+
for (const byte of bytes)
|
|
76
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
77
|
+
return hex;
|
|
78
|
+
}
|
|
79
|
+
function isCryptoKey(value) {
|
|
80
|
+
return typeof value.algorithm === 'object' && Array.isArray(value.usages);
|
|
81
|
+
}
|
|
82
|
+
function toBase64Url(bytes) {
|
|
83
|
+
return encodeBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
84
|
+
}
|
|
85
|
+
function fromBase64Url(text) {
|
|
86
|
+
const padded = text.replace(/-/g, '+').replace(/_/g, '/');
|
|
87
|
+
return decodeBase64(padded + '='.repeat((4 - (padded.length % 4)) % 4));
|
|
88
|
+
}
|
package/dist/doctor/index.d.ts
CHANGED
|
@@ -42,8 +42,10 @@ export interface DoctorDeployment {
|
|
|
42
42
|
replicas: number;
|
|
43
43
|
/**
|
|
44
44
|
* The stores and shared state it uses, by any name: a checkpointer, the operation store, the
|
|
45
|
-
* server's state, tenant usage, a rate-limit store, a circuit store, a cache. Each is
|
|
46
|
-
* it keeps its state in process memory, where each replica would see only its own
|
|
45
|
+
* server's state, tenant usage, a rate-limit store, a circuit store, a cache, a tracer. Each is
|
|
46
|
+
* reported when it keeps its state in process memory, where each replica would see only its own,
|
|
47
|
+
* and when it lacks an opt-in setting a deployment needs: a Redis operation store without its
|
|
48
|
+
* dispatch index, or a tracer writing to a persistent store without incremental tracing.
|
|
47
49
|
*/
|
|
48
50
|
stores?: Record<string, unknown>;
|
|
49
51
|
}
|
package/dist/doctor/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export async function diagnose(options = {}) {
|
|
|
54
54
|
await run('queue', 'Operation queue and leases', () => queueCheck(options.operations, now(), options.maxQueueAgeMs));
|
|
55
55
|
await run('health', 'Provider health', () => healthCheck(options.ai));
|
|
56
56
|
await run('topology', 'Settings that only work in one process', () => topologyCheck(options.deployment));
|
|
57
|
+
await run('settings', 'Opt-in settings a deployment needs', () => settingsCheck(options.deployment));
|
|
57
58
|
const failures = checks.filter((check) => check.status === 'fail').length;
|
|
58
59
|
const warnings = checks.filter((check) => check.status === 'warn').length;
|
|
59
60
|
return { checks, ok: failures === 0, failures, warnings };
|
|
@@ -235,3 +236,31 @@ function topologyCheck(deployment) {
|
|
|
235
236
|
hint: 'Use the Redis, Postgres, or SQLite adapter for each, or run one replica.',
|
|
236
237
|
};
|
|
237
238
|
}
|
|
239
|
+
function settingsCheck(deployment) {
|
|
240
|
+
const stores = Object.entries(deployment?.stores ?? {});
|
|
241
|
+
if (stores.length === 0)
|
|
242
|
+
return { status: 'skip', detail: 'No stores described.' };
|
|
243
|
+
const findings = [];
|
|
244
|
+
const hints = new Set();
|
|
245
|
+
for (const [name, value] of stores) {
|
|
246
|
+
const kind = kindOf(value);
|
|
247
|
+
if (kind === 'RedisOperationStore' && value.indexed === false) {
|
|
248
|
+
findings.push(`${name} is a RedisOperationStore without index: true, so finding queued work reads every record and a long queue slows every claim`);
|
|
249
|
+
hints.add('Give the Redis operation store index: true once every worker sharing its prefix runs 2.2 or later, then call reindex() once.');
|
|
250
|
+
}
|
|
251
|
+
if (kind === 'Tracer') {
|
|
252
|
+
const tracer = value;
|
|
253
|
+
const store = kindOf(tracer.store);
|
|
254
|
+
if (tracer.incremental === false && store && store !== 'MemoryTraceStore') {
|
|
255
|
+
findings.push(`${name} writes to a ${store} without incremental, so a trace is written when its root finishes and a replica that dies mid-run loses it`);
|
|
256
|
+
hints.add('Create the Tracer with incremental: true, and close what a dead process left with closeAbandonedRuns().');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (findings.length === 0)
|
|
261
|
+
return { status: 'ok', detail: 'No store lacks a setting a deployment needs.' };
|
|
262
|
+
return { status: 'warn', detail: `${findings.join('; ')}.`, hint: [...hints].join(' ') };
|
|
263
|
+
}
|
|
264
|
+
function kindOf(value) {
|
|
265
|
+
return value?.constructor?.name;
|
|
266
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluation of whole conversations. A simulated user holds a conversation with an agent for each
|
|
3
|
+
* goal in a dataset, and thread evaluators score the conversation: whether the goal was met, each
|
|
4
|
+
* turn, and the turn where it went wrong. A multi-turn agent is then compared between versions as a
|
|
5
|
+
* single-turn one is, with `evaluate()` and `compareExperiments()`.
|
|
6
|
+
*/
|
|
7
|
+
import type { LLMJudge } from '../evals/judge.js';
|
|
8
|
+
import type { Evaluator } from '../types/evaluate.js';
|
|
9
|
+
import type { CompletionRequest, Message } from '../types/messages.js';
|
|
10
|
+
import type { EvaluationTarget } from './run.js';
|
|
11
|
+
/** One turn of a conversation. */
|
|
12
|
+
export interface ThreadTurn {
|
|
13
|
+
/** Who spoke. */
|
|
14
|
+
role: 'user' | 'assistant';
|
|
15
|
+
/** What they said. */
|
|
16
|
+
content: string;
|
|
17
|
+
/** For an assistant turn, how long the agent took, in milliseconds. */
|
|
18
|
+
latencyMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* For an assistant turn, every message the agent added: its tool calls, their results, and the
|
|
21
|
+
* answer. The agent is given them back on later turns, and an evaluator can read its trajectory.
|
|
22
|
+
*/
|
|
23
|
+
messages?: Message[];
|
|
24
|
+
}
|
|
25
|
+
/** Why a conversation ended. */
|
|
26
|
+
export type ThreadEnding = 'goal' | 'gave-up' | 'max-turns' | 'error';
|
|
27
|
+
/** A finished conversation: what a conversation target returns, and what thread evaluators read. */
|
|
28
|
+
export interface Thread {
|
|
29
|
+
/** Every turn, in order. */
|
|
30
|
+
turns: ThreadTurn[];
|
|
31
|
+
/**
|
|
32
|
+
* `goal` when the user said its goal was met, `gave-up` when it said it never would be, `max-turns`
|
|
33
|
+
* when the turn limit ended it, and `error` when the agent failed.
|
|
34
|
+
*/
|
|
35
|
+
ended: ThreadEnding;
|
|
36
|
+
/** The agent's error, when it failed. */
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
/** What a conversation is about: the user's goal, and optionally who they are and how they open. */
|
|
40
|
+
export interface ThreadInputs {
|
|
41
|
+
/** What the user wants, in their words or the dataset's. */
|
|
42
|
+
goal: string;
|
|
43
|
+
/** Who the user is and how they talk, such as "an impatient customer who writes in short lines". */
|
|
44
|
+
persona?: string;
|
|
45
|
+
/** The user's first message, instead of letting the simulated user write it. */
|
|
46
|
+
opening?: string;
|
|
47
|
+
}
|
|
48
|
+
/** What a simulated user sees when it writes its next message. */
|
|
49
|
+
export interface SimulatedUserContext {
|
|
50
|
+
/** The goal. */
|
|
51
|
+
goal: string;
|
|
52
|
+
/** The persona, when the example gives one. */
|
|
53
|
+
persona?: string;
|
|
54
|
+
/** The conversation so far. Empty for the first message. */
|
|
55
|
+
turns: readonly ThreadTurn[];
|
|
56
|
+
/** Aborted when the evaluation is cancelled or times out. */
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
/** A simulated user's move: its next message, or the end of the conversation and why. */
|
|
60
|
+
export type SimulatedUserReply = {
|
|
61
|
+
message: string;
|
|
62
|
+
} | {
|
|
63
|
+
done: 'goal' | 'gave-up';
|
|
64
|
+
};
|
|
65
|
+
/** Plays the user in a conversation. */
|
|
66
|
+
export interface SimulatedUser {
|
|
67
|
+
/** The next move, given the conversation so far. */
|
|
68
|
+
next(context: SimulatedUserContext): Promise<SimulatedUserReply> | SimulatedUserReply;
|
|
69
|
+
}
|
|
70
|
+
/** A model client, as the judges take one. */
|
|
71
|
+
export interface SimulatedUserClient {
|
|
72
|
+
/** Runs one completion. */
|
|
73
|
+
complete(request: CompletionRequest): Promise<{
|
|
74
|
+
content: string;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
/** Options for `simulatedUser()`. */
|
|
78
|
+
export interface SimulatedUserOptions {
|
|
79
|
+
/** The client the user's model is called through. */
|
|
80
|
+
client: SimulatedUserClient;
|
|
81
|
+
/** The model that plays the user. */
|
|
82
|
+
model: string;
|
|
83
|
+
/** A persona for every conversation, when the examples give none. */
|
|
84
|
+
persona?: string;
|
|
85
|
+
/** Sampling temperature. Defaults to 0, so a conversation repeats. */
|
|
86
|
+
temperature?: number;
|
|
87
|
+
/** Output token limit for each message. Defaults to 300. */
|
|
88
|
+
maxTokens?: number;
|
|
89
|
+
/** Replaces the instructions the model is given. `{goal}` and `{persona}` are filled in. */
|
|
90
|
+
instructions?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A simulated user driven by a model: it is told the goal and the persona, sees the conversation
|
|
94
|
+
* with the roles turned around, and writes the next message, or says it is done and why.
|
|
95
|
+
*/
|
|
96
|
+
export declare function simulatedUser(options: SimulatedUserOptions): SimulatedUser;
|
|
97
|
+
/**
|
|
98
|
+
* A user that follows a script, for deterministic tests: its messages in order, then `done`. A
|
|
99
|
+
* function decides each move itself.
|
|
100
|
+
*/
|
|
101
|
+
export declare function scriptedUser(script: {
|
|
102
|
+
messages: readonly string[];
|
|
103
|
+
done?: 'goal' | 'gave-up';
|
|
104
|
+
} | SimulatedUser['next']): SimulatedUser;
|
|
105
|
+
/** What a thread agent is told besides the conversation. */
|
|
106
|
+
export interface ThreadAgentContext<I extends ThreadInputs = ThreadInputs> {
|
|
107
|
+
/** The example's inputs. */
|
|
108
|
+
inputs: I;
|
|
109
|
+
/** The turn, counted from 1. */
|
|
110
|
+
turn: number;
|
|
111
|
+
/** Names this conversation, the same on every turn and new for every conversation: a thread id. */
|
|
112
|
+
conversationId: string;
|
|
113
|
+
/** Aborted when the evaluation is cancelled or times out. */
|
|
114
|
+
signal?: AbortSignal;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* What a thread agent answers: the reply, or the reply and every message the agent added on the way,
|
|
118
|
+
* such as its tool calls and their results. Those are given back to it on later turns.
|
|
119
|
+
*/
|
|
120
|
+
export type ThreadAgentReply = string | {
|
|
121
|
+
content: string;
|
|
122
|
+
messages?: Message[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* The agent a conversation target talks to. It is given the conversation so far, with its own earlier
|
|
126
|
+
* turns as it recorded them, and answers the user's newest message.
|
|
127
|
+
*/
|
|
128
|
+
export type ThreadAgent<I extends ThreadInputs = ThreadInputs> = (messages: readonly Message[], context: ThreadAgentContext<I>) => Promise<ThreadAgentReply> | ThreadAgentReply;
|
|
129
|
+
/** Options for `conversationTarget()`. */
|
|
130
|
+
export interface ConversationTargetOptions<I extends ThreadInputs = ThreadInputs> {
|
|
131
|
+
/** The user, or a user per example, such as one with the example's own persona. */
|
|
132
|
+
user: SimulatedUser | ((inputs: I) => SimulatedUser);
|
|
133
|
+
/** The most user messages a conversation may have. Defaults to 10. */
|
|
134
|
+
maxTurns?: number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* An `evaluate()` target that holds a conversation per example: the simulated user speaks, the agent
|
|
138
|
+
* answers, until the user says it is done or the turn limit is reached. It returns the `Thread`.
|
|
139
|
+
*/
|
|
140
|
+
export declare function conversationTarget<I extends ThreadInputs = ThreadInputs>(agent: ThreadAgent<I>, options: ConversationTargetOptions<I>): EvaluationTarget<I>;
|
|
141
|
+
/** A graph a thread agent can drive: one that takes `{ messages }`, as `createAgent()` builds. */
|
|
142
|
+
export interface ThreadGraph {
|
|
143
|
+
/** Runs one turn. */
|
|
144
|
+
invoke(input: {
|
|
145
|
+
messages: Message[];
|
|
146
|
+
}, options?: {
|
|
147
|
+
threadId?: string;
|
|
148
|
+
signal?: AbortSignal;
|
|
149
|
+
}): Promise<{
|
|
150
|
+
state: unknown;
|
|
151
|
+
}>;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* An agent from `createAgent()`, or any graph that takes `{ messages }` and ends with an `answer`, as
|
|
155
|
+
* a thread agent. Each turn runs on the conversation's thread with the transcript so far, and the
|
|
156
|
+
* messages the run added to its `messages` are recorded with the turn. The agent therefore sees its
|
|
157
|
+
* own tool calls on later turns, as it does with a real user, without needing a checkpointer.
|
|
158
|
+
*/
|
|
159
|
+
export declare function graphThreadAgent(graph: ThreadGraph): ThreadAgent;
|
|
160
|
+
/** The conversation as text, for a judge or a person. */
|
|
161
|
+
export declare function formatThread(thread: Thread): string;
|
|
162
|
+
/**
|
|
163
|
+
* Whether the user said its goal was met: 1 when the conversation ended on `goal`, otherwise 0, with
|
|
164
|
+
* how it ended as the comment. Key `goal_reached`.
|
|
165
|
+
*/
|
|
166
|
+
export declare function goalReached(): Evaluator<ThreadInputs>;
|
|
167
|
+
/**
|
|
168
|
+
* Whether the goal was met, in a judge's opinion of the whole conversation, not the user's. Key
|
|
169
|
+
* `goal_completion`, with the judge's rationale as the comment.
|
|
170
|
+
*/
|
|
171
|
+
export declare function goalCompletion(judge: Pick<LLMJudge, 'evaluate'>): Evaluator<ThreadInputs>;
|
|
172
|
+
/** How many times the assistant spoke: fewer is better. Key `turns`; compare with `lowerIsBetter: ['turns']`. */
|
|
173
|
+
export declare function turnCount(): Evaluator<ThreadInputs>;
|
|
174
|
+
/** Options for `turnScores()`. */
|
|
175
|
+
export interface TurnScoresOptions {
|
|
176
|
+
/** The score's key. Defaults to `turn_score`. */
|
|
177
|
+
key?: string;
|
|
178
|
+
/** A turn scoring below this went wrong. Defaults to 0.5. */
|
|
179
|
+
threshold?: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Scores every assistant turn with an evaluator of single answers, and the conversation by their
|
|
183
|
+
* mean. The evaluator sees each turn as an example of its own: the user's message as the input, the
|
|
184
|
+
* conversation before it in `metadata.history`, and the turn as the output. The score's metadata holds
|
|
185
|
+
* each turn's score, and `wentWrongAt`, the first turn below the threshold, counted from 1.
|
|
186
|
+
*/
|
|
187
|
+
export declare function turnScores<I = unknown>(evaluator: Evaluator<I>, options?: TurnScoresOptions): Evaluator<ThreadInputs>;
|