nexus-ai-pro 2.2.0 → 2.4.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 +225 -0
- package/CHANGELOG.md +325 -1
- package/README.md +20 -6
- package/SECURITY.md +140 -0
- package/dist/adapter-kit/contracts.d.ts +87 -0
- package/dist/adapter-kit/contracts.js +237 -0
- package/dist/adapter-kit/errors.d.ts +39 -0
- package/dist/adapter-kit/errors.js +52 -0
- package/dist/adapter-kit/index.d.ts +144 -0
- package/dist/adapter-kit/index.js +178 -0
- package/dist/adapter-kit/semver.d.ts +12 -0
- package/dist/adapter-kit/semver.js +55 -0
- package/dist/agent/capabilities.d.ts +39 -0
- package/dist/agent/capabilities.js +36 -0
- package/dist/agent/create-agent.d.ts +61 -11
- package/dist/agent/create-agent.js +136 -22
- package/dist/agent/index.d.ts +3 -2
- package/dist/agent/index.js +1 -1
- package/dist/agent/middleware/approval.d.ts +26 -0
- package/dist/agent/middleware/approval.js +23 -0
- package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist/agent/middleware/context-editor.d.ts +52 -0
- package/dist/agent/middleware/context-editor.js +66 -0
- package/dist/agent/middleware/filesystem.d.ts +44 -0
- package/dist/agent/middleware/filesystem.js +83 -0
- package/dist/agent/middleware/index.d.ts +19 -0
- package/dist/agent/middleware/index.js +9 -0
- package/dist/agent/middleware/limits.d.ts +42 -0
- package/dist/agent/middleware/limits.js +52 -0
- package/dist/agent/middleware/models.d.ts +48 -0
- package/dist/agent/middleware/models.js +44 -0
- package/dist/agent/middleware/pii.d.ts +67 -0
- package/dist/agent/middleware/pii.js +142 -0
- package/dist/agent/middleware/retry.d.ts +66 -0
- package/dist/agent/middleware/retry.js +59 -0
- package/dist/agent/middleware/shared.d.ts +41 -0
- package/dist/agent/middleware/shared.js +73 -0
- package/dist/agent/middleware/tool-selector.d.ts +64 -0
- package/dist/agent/middleware/tool-selector.js +106 -0
- package/dist/agent/permissions.d.ts +113 -0
- package/dist/agent/permissions.js +180 -0
- package/dist/agent/sandbox.d.ts +151 -0
- package/dist/agent/sandbox.js +257 -0
- package/dist/agent/tool.d.ts +8 -3
- package/dist/agent/tool.js +14 -3
- package/dist/core/secure-stream.js +4 -3
- package/dist/deep-agent/index.d.ts +109 -0
- package/dist/deep-agent/index.js +216 -0
- package/dist/embeddings/adapters.js +2 -1
- package/dist/evals/judge.js +2 -5
- package/dist/evaluate/cache.d.ts +0 -14
- package/dist/evaluate/cache.js +0 -30
- package/dist/evaluate/file-cache.d.ts +15 -0
- package/dist/evaluate/file-cache.js +30 -0
- package/dist/evaluate/index.d.ts +2 -1
- package/dist/evaluate/index.js +2 -1
- package/dist/evaluate/review.js +2 -2
- package/dist/evaluate/run.js +2 -2
- package/dist/evaluate/version.js +3 -3
- package/dist/graph/functional.d.ts +9 -0
- package/dist/graph/functional.js +20 -3
- package/dist/graph/graph.js +27 -6
- package/dist/graph/lint.d.ts +4 -1
- package/dist/graph/lint.js +35 -0
- package/dist/graph/node-cache.js +3 -3
- package/dist/hallucination/rag.d.ts +4 -1
- package/dist/hallucination/rag.js +6 -1
- package/dist/images/asset-stores.js +2 -3
- package/dist/images/asset-support.js +2 -2
- package/dist/images/assets.js +1 -2
- package/dist/images/evals.js +4 -4
- package/dist/insights/proposals.js +1 -2
- package/dist/insights/regressions.d.ts +26 -2
- package/dist/insights/regressions.js +129 -3
- package/dist/loaders/git.js +8 -1
- package/dist/loaders/web.js +7 -1
- package/dist/mcp/client.d.ts +14 -0
- package/dist/mcp/client.js +59 -1
- package/dist/mcp/registry.d.ts +6 -0
- package/dist/mcp/registry.js +7 -0
- package/dist/postgres/fulltext.d.ts +67 -0
- package/dist/postgres/fulltext.js +91 -0
- package/dist/prompts/file.js +28 -11
- package/dist/protocols/a2a.d.ts +235 -0
- package/dist/protocols/a2a.js +386 -0
- package/dist/protocols/acp.d.ts +87 -0
- package/dist/protocols/acp.js +322 -0
- package/dist/protocols/ag-ui.d.ts +120 -0
- package/dist/protocols/ag-ui.js +180 -0
- package/dist/protocols/shared.d.ts +80 -0
- package/dist/protocols/shared.js +49 -0
- package/dist/providers/anthropic.js +1 -0
- package/dist/providers/cohere.js +2 -2
- package/dist/providers/google.js +2 -11
- package/dist/providers/openai.js +1 -0
- package/dist/rag/elasticsearch.d.ts +71 -0
- package/dist/rag/elasticsearch.js +130 -0
- package/dist/rag/file-ingestion.js +15 -9
- package/dist/rag/pipeline.d.ts +140 -0
- package/dist/rag/pipeline.js +187 -0
- package/dist/rag/rerankers.d.ts +92 -0
- package/dist/rag/rerankers.js +97 -0
- package/dist/rag/retrievers.d.ts +12 -1
- package/dist/runtime/detect.d.ts +17 -0
- package/dist/runtime/detect.js +16 -0
- package/dist/runtime/index.d.ts +19 -0
- package/dist/runtime/index.js +7 -0
- package/dist/security/injection-detector.d.ts +2 -0
- package/dist/security/injection-detector.js +7 -1
- package/dist/security/input-guard.js +5 -2
- package/dist/security/output-guard.js +8 -4
- package/dist/security/pii-detector.js +22 -6
- package/dist/security/prompt-hardening.js +6 -2
- package/dist/security/semantic-injection-classifier.d.ts +2 -1
- package/dist/security/semantic-injection-classifier.js +31 -35
- package/dist/security/upload-scanner.d.ts +8 -0
- package/dist/security/upload-scanner.js +51 -9
- package/dist/server/assistant.d.ts +5 -1
- package/dist/server/assistant.js +4 -2
- package/dist/server/auth.d.ts +250 -0
- package/dist/server/auth.js +392 -0
- package/dist/server/deployments.d.ts +63 -1
- package/dist/server/deployments.js +75 -2
- package/dist/server/runs.d.ts +9 -0
- package/dist/server/runs.js +20 -0
- package/dist/server/server.js +5 -0
- package/dist/store/memory.d.ts +10 -0
- package/dist/store/memory.js +10 -0
- package/dist/testing/record.js +2 -2
- package/dist/tracing/stores.js +2 -1
- package/dist/tracing/tracer.js +2 -2
- package/dist/types/config.d.ts +20 -0
- package/dist/types/graph.d.ts +38 -0
- package/dist/types/messages.d.ts +36 -2
- package/dist/types/principal.d.ts +21 -0
- package/dist/types/principal.js +1 -0
- package/dist/types/server.d.ts +8 -9
- package/dist/utils/base64.d.ts +4 -0
- package/dist/utils/base64.js +21 -0
- package/dist/utils/checksums.d.ts +22 -0
- package/dist/utils/checksums.js +39 -0
- package/dist/utils/ids.d.ts +8 -0
- package/dist/utils/ids.js +26 -2
- package/dist/utils/sha256.d.ts +22 -0
- package/dist/utils/sha256.js +120 -0
- package/dist/utils/tenant.d.ts +11 -0
- package/dist/utils/tenant.js +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/adapter-kit/contracts.d.ts +87 -0
- package/dist-cjs/adapter-kit/contracts.js +243 -0
- package/dist-cjs/adapter-kit/errors.d.ts +39 -0
- package/dist-cjs/adapter-kit/errors.js +57 -0
- package/dist-cjs/adapter-kit/index.d.ts +144 -0
- package/dist-cjs/adapter-kit/index.js +194 -0
- package/dist-cjs/adapter-kit/semver.d.ts +12 -0
- package/dist-cjs/adapter-kit/semver.js +59 -0
- package/dist-cjs/agent/capabilities.d.ts +39 -0
- package/dist-cjs/agent/capabilities.js +41 -0
- package/dist-cjs/agent/create-agent.d.ts +61 -11
- package/dist-cjs/agent/create-agent.js +136 -22
- package/dist-cjs/agent/index.d.ts +3 -2
- package/dist-cjs/agent/index.js +4 -4
- package/dist-cjs/agent/middleware/approval.d.ts +26 -0
- package/dist-cjs/agent/middleware/approval.js +26 -0
- package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
- package/dist-cjs/agent/middleware/context-editor.js +70 -0
- package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
- package/dist-cjs/agent/middleware/filesystem.js +86 -0
- package/dist-cjs/agent/middleware/index.d.ts +19 -0
- package/dist-cjs/agent/middleware/index.js +28 -0
- package/dist-cjs/agent/middleware/limits.d.ts +42 -0
- package/dist-cjs/agent/middleware/limits.js +57 -0
- package/dist-cjs/agent/middleware/models.d.ts +48 -0
- package/dist-cjs/agent/middleware/models.js +48 -0
- package/dist-cjs/agent/middleware/pii.d.ts +67 -0
- package/dist-cjs/agent/middleware/pii.js +147 -0
- package/dist-cjs/agent/middleware/retry.d.ts +66 -0
- package/dist-cjs/agent/middleware/retry.js +63 -0
- package/dist-cjs/agent/middleware/shared.d.ts +41 -0
- package/dist-cjs/agent/middleware/shared.js +83 -0
- package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
- package/dist-cjs/agent/middleware/tool-selector.js +109 -0
- package/dist-cjs/agent/permissions.d.ts +113 -0
- package/dist-cjs/agent/permissions.js +189 -0
- package/dist-cjs/agent/sandbox.d.ts +151 -0
- package/dist-cjs/agent/sandbox.js +297 -0
- package/dist-cjs/agent/tool.d.ts +8 -3
- package/dist-cjs/agent/tool.js +14 -3
- package/dist-cjs/core/secure-stream.js +4 -3
- package/dist-cjs/deep-agent/index.d.ts +109 -0
- package/dist-cjs/deep-agent/index.js +219 -0
- package/dist-cjs/embeddings/adapters.js +2 -1
- package/dist-cjs/evals/judge.js +2 -5
- package/dist-cjs/evaluate/cache.d.ts +0 -14
- package/dist-cjs/evaluate/cache.js +1 -65
- package/dist-cjs/evaluate/file-cache.d.ts +15 -0
- package/dist-cjs/evaluate/file-cache.js +67 -0
- package/dist-cjs/evaluate/index.d.ts +2 -1
- package/dist-cjs/evaluate/index.js +3 -2
- package/dist-cjs/evaluate/review.js +2 -2
- package/dist-cjs/evaluate/run.js +2 -2
- package/dist-cjs/evaluate/version.js +3 -3
- package/dist-cjs/graph/functional.d.ts +9 -0
- package/dist-cjs/graph/functional.js +20 -3
- package/dist-cjs/graph/graph.js +27 -6
- package/dist-cjs/graph/lint.d.ts +4 -1
- package/dist-cjs/graph/lint.js +35 -0
- package/dist-cjs/graph/node-cache.js +3 -3
- package/dist-cjs/hallucination/rag.d.ts +4 -1
- package/dist-cjs/hallucination/rag.js +6 -1
- package/dist-cjs/images/asset-stores.js +2 -3
- package/dist-cjs/images/asset-support.js +2 -2
- package/dist-cjs/images/assets.js +1 -2
- package/dist-cjs/images/evals.js +4 -4
- package/dist-cjs/insights/proposals.js +1 -2
- package/dist-cjs/insights/regressions.d.ts +26 -2
- package/dist-cjs/insights/regressions.js +129 -3
- package/dist-cjs/loaders/git.js +8 -1
- package/dist-cjs/loaders/web.js +7 -1
- package/dist-cjs/mcp/client.d.ts +14 -0
- package/dist-cjs/mcp/client.js +60 -1
- package/dist-cjs/mcp/registry.d.ts +6 -0
- package/dist-cjs/mcp/registry.js +7 -0
- package/dist-cjs/postgres/fulltext.d.ts +67 -0
- package/dist-cjs/postgres/fulltext.js +129 -0
- package/dist-cjs/prompts/file.js +28 -11
- package/dist-cjs/protocols/a2a.d.ts +235 -0
- package/dist-cjs/protocols/a2a.js +393 -0
- package/dist-cjs/protocols/acp.d.ts +87 -0
- package/dist-cjs/protocols/acp.js +326 -0
- package/dist-cjs/protocols/ag-ui.d.ts +120 -0
- package/dist-cjs/protocols/ag-ui.js +186 -0
- package/dist-cjs/protocols/shared.d.ts +80 -0
- package/dist-cjs/protocols/shared.js +59 -0
- package/dist-cjs/providers/anthropic.js +1 -0
- package/dist-cjs/providers/cohere.js +2 -2
- package/dist-cjs/providers/google.js +3 -12
- package/dist-cjs/providers/openai.js +1 -0
- package/dist-cjs/rag/elasticsearch.d.ts +71 -0
- package/dist-cjs/rag/elasticsearch.js +135 -0
- package/dist-cjs/rag/file-ingestion.js +14 -8
- package/dist-cjs/rag/pipeline.d.ts +140 -0
- package/dist-cjs/rag/pipeline.js +191 -0
- package/dist-cjs/rag/rerankers.d.ts +92 -0
- package/dist-cjs/rag/rerankers.js +107 -0
- package/dist-cjs/rag/retrievers.d.ts +12 -1
- package/dist-cjs/runtime/detect.d.ts +17 -0
- package/dist-cjs/runtime/detect.js +19 -0
- package/dist-cjs/runtime/index.d.ts +19 -0
- package/dist-cjs/runtime/index.js +31 -0
- package/dist-cjs/security/injection-detector.d.ts +2 -0
- package/dist-cjs/security/injection-detector.js +7 -1
- package/dist-cjs/security/input-guard.js +5 -2
- package/dist-cjs/security/output-guard.js +8 -4
- package/dist-cjs/security/pii-detector.js +22 -6
- package/dist-cjs/security/prompt-hardening.js +6 -2
- package/dist-cjs/security/semantic-injection-classifier.d.ts +2 -1
- package/dist-cjs/security/semantic-injection-classifier.js +31 -35
- package/dist-cjs/security/upload-scanner.d.ts +8 -0
- package/dist-cjs/security/upload-scanner.js +53 -9
- package/dist-cjs/server/assistant.d.ts +5 -1
- package/dist-cjs/server/assistant.js +4 -2
- package/dist-cjs/server/auth.d.ts +250 -0
- package/dist-cjs/server/auth.js +402 -0
- package/dist-cjs/server/deployments.d.ts +63 -1
- package/dist-cjs/server/deployments.js +75 -2
- package/dist-cjs/server/runs.d.ts +9 -0
- package/dist-cjs/server/runs.js +20 -0
- package/dist-cjs/server/server.js +5 -0
- package/dist-cjs/store/memory.d.ts +10 -0
- package/dist-cjs/store/memory.js +10 -0
- package/dist-cjs/testing/record.js +2 -2
- package/dist-cjs/tracing/stores.js +2 -1
- package/dist-cjs/tracing/tracer.js +2 -2
- package/dist-cjs/types/config.d.ts +20 -0
- package/dist-cjs/types/graph.d.ts +38 -0
- package/dist-cjs/types/messages.d.ts +36 -2
- package/dist-cjs/types/principal.d.ts +21 -0
- package/dist-cjs/types/principal.js +2 -0
- package/dist-cjs/types/server.d.ts +8 -9
- package/dist-cjs/utils/base64.d.ts +4 -0
- package/dist-cjs/utils/base64.js +25 -0
- package/dist-cjs/utils/checksums.d.ts +22 -0
- package/dist-cjs/utils/checksums.js +46 -0
- package/dist-cjs/utils/ids.d.ts +8 -0
- package/dist-cjs/utils/ids.js +28 -2
- package/dist-cjs/utils/sha256.d.ts +22 -0
- package/dist-cjs/utils/sha256.js +125 -0
- package/dist-cjs/utils/tenant.d.ts +11 -0
- package/dist-cjs/utils/tenant.js +15 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +7 -3
- package/package.json +193 -6
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JwtError = void 0;
|
|
4
|
+
exports.createJwtVerifier = createJwtVerifier;
|
|
5
|
+
exports.jwtAuth = jwtAuth;
|
|
6
|
+
exports.hashApiKey = hashApiKey;
|
|
7
|
+
exports.apiKeyAuth = apiKeyAuth;
|
|
8
|
+
exports.trustedProxyAuth = trustedProxyAuth;
|
|
9
|
+
exports.anyAuth = anyAuth;
|
|
10
|
+
const tenant_js_1 = require("../utils/tenant.js");
|
|
11
|
+
class JwtError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'JwtError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.JwtError = JwtError;
|
|
20
|
+
const HMAC = ['HS256', 'HS384', 'HS512'];
|
|
21
|
+
const ASYMMETRIC = [
|
|
22
|
+
'RS256',
|
|
23
|
+
'RS384',
|
|
24
|
+
'RS512',
|
|
25
|
+
'PS256',
|
|
26
|
+
'PS384',
|
|
27
|
+
'PS512',
|
|
28
|
+
'ES256',
|
|
29
|
+
'ES384',
|
|
30
|
+
'ES512',
|
|
31
|
+
'EdDSA',
|
|
32
|
+
];
|
|
33
|
+
function cryptoParams(alg) {
|
|
34
|
+
const bits = alg.slice(2);
|
|
35
|
+
const hash = `SHA-${bits === 'DSA' ? '512' : bits}`;
|
|
36
|
+
switch (alg.slice(0, 2)) {
|
|
37
|
+
case 'HS':
|
|
38
|
+
return { importAs: { name: 'HMAC', hash }, verifyAs: { name: 'HMAC' } };
|
|
39
|
+
case 'RS':
|
|
40
|
+
return { importAs: { name: 'RSASSA-PKCS1-v1_5', hash }, verifyAs: { name: 'RSASSA-PKCS1-v1_5' } };
|
|
41
|
+
case 'PS':
|
|
42
|
+
return { importAs: { name: 'RSA-PSS', hash }, verifyAs: { name: 'RSA-PSS', saltLength: Number(bits) / 8 } };
|
|
43
|
+
case 'ES': {
|
|
44
|
+
const namedCurve = bits === '512' ? 'P-521' : `P-${bits}`;
|
|
45
|
+
return { importAs: { name: 'ECDSA', namedCurve }, verifyAs: { name: 'ECDSA', hash } };
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
return { importAs: { name: 'Ed25519' }, verifyAs: { name: 'Ed25519' } };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function jwkFits(jwk, alg) {
|
|
52
|
+
if (jwk.alg && jwk.alg !== alg && !(alg === 'EdDSA' && jwk.alg === 'Ed25519'))
|
|
53
|
+
return false;
|
|
54
|
+
if (jwk.use && jwk.use !== 'sig')
|
|
55
|
+
return false;
|
|
56
|
+
if (jwk.key_ops && !jwk.key_ops.includes('verify'))
|
|
57
|
+
return false;
|
|
58
|
+
if (alg.startsWith('RS') || alg.startsWith('PS'))
|
|
59
|
+
return jwk.kty === 'RSA';
|
|
60
|
+
if (alg.startsWith('ES')) {
|
|
61
|
+
const curve = alg === 'ES512' ? 'P-521' : `P-${alg.slice(2)}`;
|
|
62
|
+
return jwk.kty === 'EC' && jwk.crv === curve;
|
|
63
|
+
}
|
|
64
|
+
if (alg === 'EdDSA')
|
|
65
|
+
return jwk.kty === 'OKP' && jwk.crv === 'Ed25519';
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function createJwtVerifier(options) {
|
|
69
|
+
const sources = [options.secret, options.publicKey, options.jwks].filter((source) => source !== undefined);
|
|
70
|
+
if (sources.length !== 1) {
|
|
71
|
+
throw new TypeError('createJwtVerifier() needs exactly one of secret, publicKey, or jwks');
|
|
72
|
+
}
|
|
73
|
+
if (options.jwks !== undefined && options.audience === undefined) {
|
|
74
|
+
throw new TypeError('A key set signs tokens for every application its provider serves: set audience, or audience: false to accept any');
|
|
75
|
+
}
|
|
76
|
+
const allowed = new Set((options.algorithms ?? (options.secret !== undefined ? HMAC : ASYMMETRIC)).filter((alg) => options.secret !== undefined ? HMAC.includes(alg) : ASYMMETRIC.includes(alg)));
|
|
77
|
+
if (allowed.size === 0)
|
|
78
|
+
throw new TypeError('None of the algorithms given can be used with this kind of key');
|
|
79
|
+
const now = options.now ?? Date.now;
|
|
80
|
+
const tolerance = options.clockToleranceSec ?? 60;
|
|
81
|
+
const subtle = globalThis.crypto.subtle;
|
|
82
|
+
const imported = new Map();
|
|
83
|
+
const importOnce = (cacheKey, load) => {
|
|
84
|
+
let key = imported.get(cacheKey);
|
|
85
|
+
if (!key) {
|
|
86
|
+
key = load();
|
|
87
|
+
key.catch(() => imported.delete(cacheKey));
|
|
88
|
+
imported.set(cacheKey, key);
|
|
89
|
+
}
|
|
90
|
+
return key;
|
|
91
|
+
};
|
|
92
|
+
let keySet = options.jwks !== undefined && typeof options.jwks === 'object' && !(options.jwks instanceof URL)
|
|
93
|
+
? { keys: options.jwks.keys, fetchedAt: Number.POSITIVE_INFINITY }
|
|
94
|
+
: undefined;
|
|
95
|
+
let lastUnknownFetch = Number.NEGATIVE_INFINITY;
|
|
96
|
+
let inFlight;
|
|
97
|
+
const refresh = () => {
|
|
98
|
+
if (inFlight)
|
|
99
|
+
return inFlight;
|
|
100
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
101
|
+
inFlight = (async () => {
|
|
102
|
+
const response = await fetcher(String(options.jwks), { headers: { accept: 'application/json' } });
|
|
103
|
+
if (!response.ok)
|
|
104
|
+
throw new JwtError('JWT_KEY', `The key set answered ${response.status}`);
|
|
105
|
+
const body = (await response.json());
|
|
106
|
+
if (!Array.isArray(body.keys))
|
|
107
|
+
throw new JwtError('JWT_KEY', 'The key set has no keys');
|
|
108
|
+
keySet = { keys: body.keys, fetchedAt: now() };
|
|
109
|
+
imported.clear();
|
|
110
|
+
})().finally(() => {
|
|
111
|
+
inFlight = undefined;
|
|
112
|
+
});
|
|
113
|
+
return inFlight;
|
|
114
|
+
};
|
|
115
|
+
const keyFor = async (header, alg) => {
|
|
116
|
+
const { importAs } = cryptoParams(alg);
|
|
117
|
+
if (options.secret !== undefined) {
|
|
118
|
+
const raw = typeof options.secret === 'string' ? new TextEncoder().encode(options.secret) : options.secret;
|
|
119
|
+
return importOnce(`secret:${alg}`, () => subtle.importKey('raw', raw, importAs, false, ['verify']));
|
|
120
|
+
}
|
|
121
|
+
if (options.publicKey !== undefined) {
|
|
122
|
+
const key = options.publicKey;
|
|
123
|
+
if (typeof key === 'object' && 'type' in key && 'algorithm' in key)
|
|
124
|
+
return key;
|
|
125
|
+
if (typeof key === 'string') {
|
|
126
|
+
return importOnce(`pem:${alg}`, () => subtle.importKey('spki', pemToDer(key), importAs, false, ['verify']));
|
|
127
|
+
}
|
|
128
|
+
if (!jwkFits(key, alg))
|
|
129
|
+
throw new JwtError('JWT_KEY', `The key cannot verify ${alg}`);
|
|
130
|
+
return importOnce(`jwk:${alg}`, () => subtle.importKey('jwk', key, importAs, false, ['verify']));
|
|
131
|
+
}
|
|
132
|
+
const remote = typeof options.jwks === 'string' || options.jwks instanceof URL;
|
|
133
|
+
const cacheMs = options.jwksCacheMs ?? 10 * 60_000;
|
|
134
|
+
if (remote && (!keySet || now() - keySet.fetchedAt > cacheMs))
|
|
135
|
+
await refresh();
|
|
136
|
+
const find = () => keySet?.keys.filter((jwk) => (header.kid === undefined || jwk.kid === header.kid) && jwkFits(jwk, alg)) ?? [];
|
|
137
|
+
let matches = find();
|
|
138
|
+
if (matches.length === 0 && remote && now() - lastUnknownFetch >= (options.jwksCooldownMs ?? 30_000)) {
|
|
139
|
+
lastUnknownFetch = now();
|
|
140
|
+
await refresh();
|
|
141
|
+
matches = find();
|
|
142
|
+
}
|
|
143
|
+
const [jwk] = matches;
|
|
144
|
+
if (!jwk || (header.kid === undefined && matches.length > 1)) {
|
|
145
|
+
throw new JwtError('JWT_KEY', header.kid ? `No key "${header.kid}" for ${alg}` : `No single key for ${alg}`);
|
|
146
|
+
}
|
|
147
|
+
return importOnce(`jwks:${jwk.kid ?? ''}:${alg}`, () => subtle.importKey('jwk', jwk, importAs, false, ['verify']));
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
async verify(token) {
|
|
151
|
+
const parts = token.split('.');
|
|
152
|
+
if (parts.length !== 3 || parts.some((part) => !/^[A-Za-z0-9_-]*$/.test(part)) || !parts[2]) {
|
|
153
|
+
throw new JwtError('JWT_MALFORMED', 'The token is not a signed JWT');
|
|
154
|
+
}
|
|
155
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
156
|
+
const header = decodeJson(encodedHeader, 'header');
|
|
157
|
+
const payload = decodeJson(encodedPayload, 'payload');
|
|
158
|
+
if (typeof header.alg !== 'string' || !allowed.has(header.alg)) {
|
|
159
|
+
throw new JwtError('JWT_ALGORITHM', `The algorithm "${String(header.alg)}" is not accepted`);
|
|
160
|
+
}
|
|
161
|
+
if (header.crit !== undefined)
|
|
162
|
+
throw new JwtError('JWT_MALFORMED', 'The token has critical headers this verifier does not understand');
|
|
163
|
+
const alg = header.alg;
|
|
164
|
+
const key = await keyFor(header, alg).catch((error) => {
|
|
165
|
+
throw error instanceof JwtError ? error : new JwtError('JWT_KEY', `No usable key for ${alg}`);
|
|
166
|
+
});
|
|
167
|
+
const valid = await subtle
|
|
168
|
+
.verify(cryptoParams(alg).verifyAs, key, base64UrlBytes(encodedSignature), new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`))
|
|
169
|
+
.catch(() => false);
|
|
170
|
+
if (!valid)
|
|
171
|
+
throw new JwtError('JWT_SIGNATURE', 'The signature does not match');
|
|
172
|
+
checkClaims(payload, options, now() / 1000, tolerance);
|
|
173
|
+
return { header, payload };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function checkClaims(payload, options, seconds, tolerance) {
|
|
178
|
+
const numeric = (name) => {
|
|
179
|
+
const value = payload[name];
|
|
180
|
+
if (value !== undefined && typeof value !== 'number')
|
|
181
|
+
throw new JwtError('JWT_CLAIM', `"${name}" is not a number`);
|
|
182
|
+
return value;
|
|
183
|
+
};
|
|
184
|
+
const exp = numeric('exp');
|
|
185
|
+
const nbf = numeric('nbf');
|
|
186
|
+
const iat = numeric('iat');
|
|
187
|
+
if (exp === undefined && options.requireExpiry !== false)
|
|
188
|
+
throw new JwtError('JWT_CLAIM', 'The token has no expiry');
|
|
189
|
+
if (exp !== undefined && seconds >= exp + tolerance)
|
|
190
|
+
throw new JwtError('JWT_EXPIRED', 'The token has expired');
|
|
191
|
+
if (nbf !== undefined && seconds + tolerance < nbf)
|
|
192
|
+
throw new JwtError('JWT_NOT_YET_VALID', 'The token is not valid yet');
|
|
193
|
+
if (options.maxAgeSec !== undefined) {
|
|
194
|
+
if (iat === undefined)
|
|
195
|
+
throw new JwtError('JWT_CLAIM', 'The token has no issue time');
|
|
196
|
+
if (seconds - iat > options.maxAgeSec + tolerance)
|
|
197
|
+
throw new JwtError('JWT_TOO_OLD', 'The token is too old');
|
|
198
|
+
}
|
|
199
|
+
if (options.issuer !== undefined) {
|
|
200
|
+
const issuers = typeof options.issuer === 'string' ? [options.issuer] : options.issuer;
|
|
201
|
+
if (typeof payload.iss !== 'string' || !issuers.includes(payload.iss)) {
|
|
202
|
+
throw new JwtError('JWT_ISSUER', 'The token is from another issuer');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (options.audience !== undefined && options.audience !== false) {
|
|
206
|
+
const wanted = typeof options.audience === 'string' ? [options.audience] : options.audience;
|
|
207
|
+
const audiences = typeof payload.aud === 'string' ? [payload.aud] : Array.isArray(payload.aud) ? payload.aud : [];
|
|
208
|
+
if (!audiences.some((audience) => wanted.includes(audience))) {
|
|
209
|
+
throw new JwtError('JWT_AUDIENCE', 'The token is for another audience');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const claim of options.requiredClaims ?? []) {
|
|
213
|
+
if (payload[claim] === undefined)
|
|
214
|
+
throw new JwtError('JWT_CLAIM', `The token has no "${claim}" claim`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function jwtAuth(options) {
|
|
218
|
+
const verifier = createJwtVerifier(options);
|
|
219
|
+
const read = options.token ?? bearerToken;
|
|
220
|
+
return async (request) => {
|
|
221
|
+
const token = read(request);
|
|
222
|
+
if (token?.split('.').length !== 3)
|
|
223
|
+
return undefined;
|
|
224
|
+
let payload;
|
|
225
|
+
try {
|
|
226
|
+
payload = (await verifier.verify(token)).payload;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!(error instanceof JwtError))
|
|
230
|
+
throw error;
|
|
231
|
+
const description = error.message.replace(/["\\]/g, "'");
|
|
232
|
+
return refuse(401, error.message, `Bearer error="invalid_token", error_description="${description}"`);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const principal = options.principal
|
|
236
|
+
? await options.principal(payload, request)
|
|
237
|
+
: principalFromClaims(payload, options.claims);
|
|
238
|
+
if (!principal)
|
|
239
|
+
return refuse(403, 'The token is valid, but not for this server');
|
|
240
|
+
return { method: 'jwt', claims: payload, ...principal };
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
return refuse(403, error instanceof Error ? error.message : String(error));
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function principalFromClaims(payload, names = {}) {
|
|
248
|
+
const subject = claimAt(payload, names.subject ?? 'sub');
|
|
249
|
+
const tenant = claimAt(payload, names.tenant ?? 'tenant_id');
|
|
250
|
+
const roles = listOf(claimAt(payload, names.roles ?? 'roles'));
|
|
251
|
+
const scopes = listOf(names.scopes ? claimAt(payload, names.scopes) : (payload.scope ?? payload.scp));
|
|
252
|
+
return {
|
|
253
|
+
...(typeof subject === 'string' ? { userId: subject } : {}),
|
|
254
|
+
...(typeof tenant === 'string' ? { tenantId: (0, tenant_js_1.assertTenantId)(tenant) } : {}),
|
|
255
|
+
...(roles ? { roles } : {}),
|
|
256
|
+
...(scopes ? { scopes } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async function hashApiKey(key) {
|
|
260
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));
|
|
261
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
262
|
+
}
|
|
263
|
+
function apiKeyAuth(options) {
|
|
264
|
+
if (!options.keys && !options.lookup)
|
|
265
|
+
throw new TypeError('apiKeyAuth() needs keys, a lookup, or both');
|
|
266
|
+
const header = options.header?.toLowerCase();
|
|
267
|
+
return async (request) => {
|
|
268
|
+
const key = header
|
|
269
|
+
? (request.headers.get(header) ?? undefined)
|
|
270
|
+
: (request.headers.get('x-api-key') ?? bearerToken(request));
|
|
271
|
+
if (!key)
|
|
272
|
+
return undefined;
|
|
273
|
+
if (options.prefix ? !key.startsWith(options.prefix) : /^[\w-]+\.[\w-]+\.[\w-]+$/.test(key))
|
|
274
|
+
return undefined;
|
|
275
|
+
const hash = await hashApiKey(key);
|
|
276
|
+
const known = Object.hasOwn(options.keys ?? {}, hash) ? options.keys?.[hash] : undefined;
|
|
277
|
+
const principal = known ?? (await options.lookup?.(hash, request));
|
|
278
|
+
if (!principal)
|
|
279
|
+
return refuse(401, 'The API key is not valid', 'Bearer error="invalid_token"');
|
|
280
|
+
if (principal.tenantId !== undefined)
|
|
281
|
+
(0, tenant_js_1.assertTenantId)(principal.tenantId);
|
|
282
|
+
return { method: 'api-key', ...principal };
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function trustedProxyAuth(options) {
|
|
286
|
+
if (!options.secret && !options.trust) {
|
|
287
|
+
throw new TypeError('trustedProxyAuth() needs a secret or a trust check: otherwise anyone who sets the headers is trusted');
|
|
288
|
+
}
|
|
289
|
+
const names = {
|
|
290
|
+
user: 'x-forwarded-user',
|
|
291
|
+
tenant: 'x-forwarded-tenant',
|
|
292
|
+
roles: 'x-forwarded-roles',
|
|
293
|
+
scopes: 'x-forwarded-scopes',
|
|
294
|
+
...options.headers,
|
|
295
|
+
};
|
|
296
|
+
const separator = options.separator ?? ',';
|
|
297
|
+
const list = (value) => value
|
|
298
|
+
?.split(separator)
|
|
299
|
+
.map((item) => item.trim())
|
|
300
|
+
.filter(Boolean);
|
|
301
|
+
return async (request) => {
|
|
302
|
+
if (options.secret && !sameText(request.headers.get(options.secret.header) ?? '', options.secret.value)) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
if (options.trust && !(await options.trust(request)))
|
|
306
|
+
return undefined;
|
|
307
|
+
const user = request.headers.get(names.user);
|
|
308
|
+
if (!user)
|
|
309
|
+
return undefined;
|
|
310
|
+
const tenant = request.headers.get(names.tenant);
|
|
311
|
+
if (tenant && !/^[A-Za-z0-9_-]{1,128}$/.test(tenant))
|
|
312
|
+
return refuse(403, `"${tenant}" is not a valid tenant id`);
|
|
313
|
+
const roles = list(request.headers.get(names.roles));
|
|
314
|
+
const scopes = list(request.headers.get(names.scopes));
|
|
315
|
+
return {
|
|
316
|
+
method: 'proxy',
|
|
317
|
+
userId: user,
|
|
318
|
+
...(tenant ? { tenantId: tenant } : {}),
|
|
319
|
+
...(roles?.length ? { roles } : {}),
|
|
320
|
+
...(scopes?.length ? { scopes } : {}),
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function anyAuth(...hooks) {
|
|
325
|
+
return async (request) => {
|
|
326
|
+
let refusal;
|
|
327
|
+
for (const hook of hooks) {
|
|
328
|
+
const result = await hook(request);
|
|
329
|
+
if (result instanceof Response)
|
|
330
|
+
refusal ??= result;
|
|
331
|
+
else if (result)
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
return refusal;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function bearerToken(request) {
|
|
338
|
+
const match = /^Bearer\s+(\S+)\s*$/i.exec(request.headers.get('authorization') ?? '');
|
|
339
|
+
return match?.[1];
|
|
340
|
+
}
|
|
341
|
+
function refuse(status, message, challenge) {
|
|
342
|
+
return new Response(JSON.stringify({ error: { code: status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN', message } }), {
|
|
343
|
+
status,
|
|
344
|
+
headers: {
|
|
345
|
+
'content-type': 'application/json; charset=utf-8',
|
|
346
|
+
...(challenge ? { 'www-authenticate': challenge } : {}),
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function sameText(left, right) {
|
|
351
|
+
let difference = left.length ^ right.length;
|
|
352
|
+
const length = Math.max(left.length, right.length);
|
|
353
|
+
for (let index = 0; index < length; index += 1) {
|
|
354
|
+
difference |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
|
|
355
|
+
}
|
|
356
|
+
return difference === 0;
|
|
357
|
+
}
|
|
358
|
+
function base64UrlBytes(text) {
|
|
359
|
+
const base64 = text
|
|
360
|
+
.replace(/-/g, '+')
|
|
361
|
+
.replace(/_/g, '/')
|
|
362
|
+
.padEnd(Math.ceil(text.length / 4) * 4, '=');
|
|
363
|
+
const binary = atob(base64);
|
|
364
|
+
const bytes = new Uint8Array(binary.length);
|
|
365
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
366
|
+
bytes[index] = binary.charCodeAt(index);
|
|
367
|
+
return bytes;
|
|
368
|
+
}
|
|
369
|
+
function decodeJson(part, what) {
|
|
370
|
+
try {
|
|
371
|
+
const value = JSON.parse(new TextDecoder().decode(base64UrlBytes(part)));
|
|
372
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
}
|
|
377
|
+
throw new JwtError('JWT_MALFORMED', `The token's ${what} is not a JSON object`);
|
|
378
|
+
}
|
|
379
|
+
function pemToDer(pem) {
|
|
380
|
+
const body = pem.replace(/-----(?:BEGIN|END) PUBLIC KEY-----/g, '').replace(/\s+/g, '');
|
|
381
|
+
if (!body || pem.includes('PRIVATE KEY'))
|
|
382
|
+
throw new JwtError('JWT_KEY', 'The public key is not a PEM public key');
|
|
383
|
+
return base64UrlBytes(body.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')).buffer;
|
|
384
|
+
}
|
|
385
|
+
function claimAt(payload, path) {
|
|
386
|
+
if (path in payload)
|
|
387
|
+
return payload[path];
|
|
388
|
+
let current = payload;
|
|
389
|
+
for (const segment of path.split('.')) {
|
|
390
|
+
if (!current || typeof current !== 'object')
|
|
391
|
+
return undefined;
|
|
392
|
+
current = current[segment];
|
|
393
|
+
}
|
|
394
|
+
return current;
|
|
395
|
+
}
|
|
396
|
+
function listOf(value) {
|
|
397
|
+
if (typeof value === 'string')
|
|
398
|
+
return value.split(/\s+/).filter(Boolean);
|
|
399
|
+
if (Array.isArray(value))
|
|
400
|
+
return value.filter((item) => typeof item === 'string');
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Regression, type RegressionMetric, type RunSample } from '../insights/regressions.js';
|
|
2
|
+
import type { EvaluationScore, Evaluator } from '../types/evaluate.js';
|
|
2
3
|
import type { DeploymentChange, DeploymentChangeRecord, DeploymentRecord, ReplicaReport, RevisionChoice, RevisionRequest, ServerAssistant, ServerDeployments, ServerStateStore } from '../types/server.js';
|
|
3
4
|
export type { DeploymentChange, DeploymentChangeRecord, DeploymentRecord, ReplicaReport, RevisionChoice, RevisionReason, RevisionRequest, RunRevision, } from '../types/server.js';
|
|
4
5
|
/** Options for `Deployments`. */
|
|
@@ -58,6 +59,30 @@ export interface RevisionStats {
|
|
|
58
59
|
/** Mean recorded cost of finished runs, in US dollars. */
|
|
59
60
|
meanCost?: number;
|
|
60
61
|
}
|
|
62
|
+
/** Options for `Deployments.evaluate()`. */
|
|
63
|
+
export interface RunEvaluationOptions {
|
|
64
|
+
/** What each run is scored by: evaluators from `nexus-ai-pro/evaluate`, such as an LLM judge. */
|
|
65
|
+
evaluators: ReadonlyArray<Evaluator>;
|
|
66
|
+
/** Only runs created from this time, such as the start of a canary. */
|
|
67
|
+
since?: string;
|
|
68
|
+
/** Only this revision's runs. Defaults to every revision's. */
|
|
69
|
+
revision?: string;
|
|
70
|
+
/** The share of runs scored, from 0 to 1, chosen by run id so every replica picks the same ones. Defaults to 1. */
|
|
71
|
+
sampleRate?: number;
|
|
72
|
+
/** How many recent runs are read. Defaults to 2,000. */
|
|
73
|
+
limit?: number;
|
|
74
|
+
/** Recorded as each score's source; a run already scored by this source is skipped. Defaults to `online-evaluation`. */
|
|
75
|
+
source?: string;
|
|
76
|
+
}
|
|
77
|
+
/** What `Deployments.evaluate()` did. */
|
|
78
|
+
export interface RunEvaluationReport {
|
|
79
|
+
/** Runs scored. */
|
|
80
|
+
evaluated: number;
|
|
81
|
+
/** Runs passed over because this source had already scored them. */
|
|
82
|
+
skipped: number;
|
|
83
|
+
/** Every score given. */
|
|
84
|
+
scores: EvaluationScore[];
|
|
85
|
+
}
|
|
61
86
|
/**
|
|
62
87
|
* Revisions, traffic splits, and replica health for the agent server, recorded in a shared store.
|
|
63
88
|
*
|
|
@@ -75,6 +100,8 @@ export interface RevisionStats {
|
|
|
75
100
|
export declare class Deployments implements ServerDeployments {
|
|
76
101
|
private readonly options;
|
|
77
102
|
private readonly state;
|
|
103
|
+
/** The change in progress per deployment, which the next one waits for. */
|
|
104
|
+
private readonly changing;
|
|
78
105
|
private readonly now;
|
|
79
106
|
private readonly registrations;
|
|
80
107
|
private readonly cache;
|
|
@@ -108,10 +135,16 @@ export declare class Deployments implements ServerDeployments {
|
|
|
108
135
|
* `expectedVersion` applies a change only to the version it was decided on, and throws
|
|
109
136
|
* `DEPLOYMENT_CONFLICT` otherwise, so two people — or a person and a canary guard — cannot undo each
|
|
110
137
|
* other without noticing.
|
|
138
|
+
*
|
|
139
|
+
* Changes to one deployment through this object apply one at a time, so two made at once on one
|
|
140
|
+
* replica both land, in order. The state store has no atomic compare-and-set, so two replicas
|
|
141
|
+
* changing one deployment in the same instant can still lose one change: make changes from one
|
|
142
|
+
* place, or pass `expectedVersion` and retry a conflict.
|
|
111
143
|
*/
|
|
112
144
|
change(assistant: string, change: DeploymentChange & {
|
|
113
145
|
expectedVersion?: number;
|
|
114
146
|
}, by?: string): Promise<DeploymentRecord>;
|
|
147
|
+
private applyChange;
|
|
115
148
|
/** Sets each revision's share of new runs. */
|
|
116
149
|
split(assistant: string, traffic: Record<string, number>, options?: {
|
|
117
150
|
by?: string;
|
|
@@ -159,6 +192,17 @@ export declare class Deployments implements ServerDeployments {
|
|
|
159
192
|
* expired run is an error, a cancelled one is left out.
|
|
160
193
|
*/
|
|
161
194
|
samples(assistant: string, revision: string, since?: string, limit?: number): Promise<RunSample[]>;
|
|
195
|
+
/**
|
|
196
|
+
* Online evaluation of the server's own runs: scores each finished run's output with evaluators
|
|
197
|
+
* from `nexus-ai-pro/evaluate`, and records the scores as the run's feedback. A canary guard given
|
|
198
|
+
* those keys in `feedback` then compares quality between revisions, beside errors, latency, and
|
|
199
|
+
* cost.
|
|
200
|
+
*
|
|
201
|
+
* A run is scored once per `source`, so calling this on a schedule never counts a run twice.
|
|
202
|
+
* Sampling is stable: the same run is always in or out of a sample, on every replica. Evaluators
|
|
203
|
+
* judge a run by its output, since a run record does not keep its input.
|
|
204
|
+
*/
|
|
205
|
+
evaluate(assistant: string, options: RunEvaluationOptions): Promise<RunEvaluationReport>;
|
|
162
206
|
/** The deployment the router uses, reusing a recent read. */
|
|
163
207
|
private current;
|
|
164
208
|
/** The deployment of a registered assistant before anything is recorded: its live revision, all traffic. */
|
|
@@ -201,8 +245,26 @@ export interface CanaryGuardOptions {
|
|
|
201
245
|
revision: string;
|
|
202
246
|
since: string;
|
|
203
247
|
}) => Promise<RunSample[]>;
|
|
204
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* Feedback keys whose mean score is compared: the scores `Deployments.evaluate()` records from
|
|
250
|
+
* online evaluation, or any a person left through the feedback route. A canary that is worse on one
|
|
251
|
+
* is rolled back like one that fails more.
|
|
252
|
+
*/
|
|
205
253
|
feedback?: readonly string[];
|
|
254
|
+
/**
|
|
255
|
+
* Judges each metric as an evaluation comparison does, at this confidence, such as 0.95. A bootstrap
|
|
256
|
+
* interval must not reach zero, and the change must be at least the metric's minimum effect:
|
|
257
|
+
* `minErrorRateIncrease`, `latencyIncrease` with `minLatencyChangeMs`, `costIncrease`, and
|
|
258
|
+
* `minFeedbackDrop`. A real regression rolls back, and noise or a trivial change does not. Without
|
|
259
|
+
* it, the error rate uses a z-test at 95%, and latency and cost use their margins alone.
|
|
260
|
+
*/
|
|
261
|
+
confidence?: number;
|
|
262
|
+
/** The smallest rise in the error rate, absolute, that counts under `confidence`. Defaults to 0.02. */
|
|
263
|
+
minErrorRateIncrease?: number;
|
|
264
|
+
/** The smallest fall in a feedback score's mean, absolute, that counts under `confidence`. Defaults to 0.05. */
|
|
265
|
+
minFeedbackDrop?: number;
|
|
266
|
+
/** Bootstrap resamples under `confidence`. Defaults to 1,000. */
|
|
267
|
+
resamples?: number;
|
|
206
268
|
/** Recorded as who made a change. Defaults to `guard`. */
|
|
207
269
|
by?: string;
|
|
208
270
|
/** Hears every decision, including holds. */
|
|
@@ -12,6 +12,7 @@ const EPSILON = 1e-9;
|
|
|
12
12
|
class Deployments {
|
|
13
13
|
options;
|
|
14
14
|
state;
|
|
15
|
+
changing = new Map();
|
|
15
16
|
now;
|
|
16
17
|
registrations = new Map();
|
|
17
18
|
cache = new Map();
|
|
@@ -86,6 +87,18 @@ class Deployments {
|
|
|
86
87
|
return (await this.get(assistant))?.history ?? [];
|
|
87
88
|
}
|
|
88
89
|
async change(assistant, change, by) {
|
|
90
|
+
const previous = this.changing.get(assistant) ?? Promise.resolve();
|
|
91
|
+
const next = previous.catch(() => undefined).then(() => this.applyChange(assistant, change, by));
|
|
92
|
+
this.changing.set(assistant, next);
|
|
93
|
+
try {
|
|
94
|
+
return await next;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (this.changing.get(assistant) === next)
|
|
98
|
+
this.changing.delete(assistant);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async applyChange(assistant, change, by) {
|
|
89
102
|
const current = await this.get(assistant);
|
|
90
103
|
if (!current) {
|
|
91
104
|
throw new errors_js_1.BadRequestError(`Assistant "${assistant}" has no deployment yet: serve it through Deployments.assistant() first`, 'UNKNOWN_DEPLOYMENT');
|
|
@@ -233,15 +246,67 @@ class Deployments {
|
|
|
233
246
|
return (await this.runs(assistant, { since, limit }))
|
|
234
247
|
.filter((run) => (run.revision?.id ?? '') === revision)
|
|
235
248
|
.flatMap((run) => {
|
|
249
|
+
const feedback = run.feedback?.length ? { feedback: run.feedback } : {};
|
|
236
250
|
if (run.status === 'failed' || run.status === 'expired') {
|
|
237
|
-
return [{ status: 'error', latencyMs: run.durationMs, cost: run.cost }];
|
|
251
|
+
return [{ status: 'error', latencyMs: run.durationMs, cost: run.cost, ...feedback }];
|
|
238
252
|
}
|
|
239
253
|
if (run.status === 'succeeded' || run.status === 'awaiting_input') {
|
|
240
|
-
return [{ status: 'success', latencyMs: run.durationMs, cost: run.cost }];
|
|
254
|
+
return [{ status: 'success', latencyMs: run.durationMs, cost: run.cost, ...feedback }];
|
|
241
255
|
}
|
|
242
256
|
return [];
|
|
243
257
|
});
|
|
244
258
|
}
|
|
259
|
+
async evaluate(assistant, options) {
|
|
260
|
+
const source = options.source ?? 'online-evaluation';
|
|
261
|
+
const rate = options.sampleRate ?? 1;
|
|
262
|
+
const report = { evaluated: 0, skipped: 0, scores: [] };
|
|
263
|
+
for (const run of await this.runs(assistant, options)) {
|
|
264
|
+
if (run.status !== 'succeeded')
|
|
265
|
+
continue;
|
|
266
|
+
if (options.revision !== undefined && (run.revision?.id ?? '') !== options.revision)
|
|
267
|
+
continue;
|
|
268
|
+
if (rate < 1 && bucket(`evaluate:${run.id}`) >= rate)
|
|
269
|
+
continue;
|
|
270
|
+
if (run.feedback?.some((item) => item.source === source)) {
|
|
271
|
+
report.skipped += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const scores = [];
|
|
275
|
+
for (const evaluator of options.evaluators) {
|
|
276
|
+
const value = await evaluator({
|
|
277
|
+
example: { id: run.id, inputs: undefined, ...(run.metadata ? { metadata: run.metadata } : {}) },
|
|
278
|
+
output: run.output,
|
|
279
|
+
latencyMs: run.durationMs ?? 0,
|
|
280
|
+
...(run.cost === undefined ? {} : { cost: run.cost }),
|
|
281
|
+
run: 0,
|
|
282
|
+
});
|
|
283
|
+
if (typeof value === 'number')
|
|
284
|
+
scores.push({ key: 'score', score: value });
|
|
285
|
+
else if (typeof value === 'boolean')
|
|
286
|
+
scores.push({ key: 'score', score: value ? 1 : 0, passed: value });
|
|
287
|
+
else
|
|
288
|
+
scores.push(...(Array.isArray(value) ? value : [value]));
|
|
289
|
+
}
|
|
290
|
+
const at = new Date().toISOString();
|
|
291
|
+
const latest = (await this.state.get(state_js_1.RUNS_NAMESPACE, run.id)) ?? run;
|
|
292
|
+
await this.state.put(state_js_1.RUNS_NAMESPACE, run.id, {
|
|
293
|
+
...latest,
|
|
294
|
+
feedback: [
|
|
295
|
+
...(latest.feedback ?? []),
|
|
296
|
+
...scores.map((score) => ({
|
|
297
|
+
key: score.key,
|
|
298
|
+
score: score.score,
|
|
299
|
+
...(score.comment ? { comment: score.comment } : {}),
|
|
300
|
+
source,
|
|
301
|
+
createdAt: at,
|
|
302
|
+
})),
|
|
303
|
+
].slice(-100),
|
|
304
|
+
});
|
|
305
|
+
report.evaluated += 1;
|
|
306
|
+
report.scores.push(...scores);
|
|
307
|
+
}
|
|
308
|
+
return report;
|
|
309
|
+
}
|
|
245
310
|
async current(assistant) {
|
|
246
311
|
const cached = this.cache.get(assistant);
|
|
247
312
|
if (cached && Date.now() - cached.at < (this.options.cacheMs ?? 2_000))
|
|
@@ -311,6 +376,14 @@ function watchCanaries(options) {
|
|
|
311
376
|
latencyIncrease: options.latencyIncrease,
|
|
312
377
|
minLatencyChangeMs: options.minLatencyChangeMs ?? 50,
|
|
313
378
|
costIncrease: options.costIncrease,
|
|
379
|
+
...(options.confidence === undefined
|
|
380
|
+
? {}
|
|
381
|
+
: {
|
|
382
|
+
confidence: options.confidence,
|
|
383
|
+
minErrorRateIncrease: options.minErrorRateIncrease,
|
|
384
|
+
minFeedbackDrop: options.minFeedbackDrop,
|
|
385
|
+
resamples: options.resamples,
|
|
386
|
+
}),
|
|
314
387
|
});
|
|
315
388
|
if (regressions.length > 0) {
|
|
316
389
|
await deployments.change(deployment.assistant, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OperationRunnerConfig } from '../types/operations.js';
|
|
2
|
+
import type { RunFeedback } from '../types/tracing.js';
|
|
2
3
|
import type { Principal, ReplicaReport, RunEventLog, RunRecord, ScalingSnapshot, ServerAssistant, ServerStateStore, TenantGate, ThreadBusyPolicy, ThreadRecord } from '../types/server.js';
|
|
3
4
|
import { ServerMetrics } from './metrics.js';
|
|
4
5
|
/** What a run needs to start. */
|
|
@@ -161,6 +162,14 @@ export declare class RunManager {
|
|
|
161
162
|
start(options: StartRunOptions): Promise<RunRecord>;
|
|
162
163
|
/** The rest of `start()`, once the thread, if any, is this run's. */
|
|
163
164
|
private startClaimed;
|
|
165
|
+
/**
|
|
166
|
+
* Leaves feedback on a run: a person's rating, or an online evaluator's score. A canary guard
|
|
167
|
+
* compares it between revisions as their quality. Another tenant's run is not found, and a run
|
|
168
|
+
* keeps its latest 100 pieces of feedback.
|
|
169
|
+
*/
|
|
170
|
+
addFeedback(runId: string, feedback: Omit<RunFeedback, 'createdAt'> & {
|
|
171
|
+
createdAt?: string;
|
|
172
|
+
}, principal?: Principal): Promise<RunRecord>;
|
|
164
173
|
/** Cancels a run, including one another replica is executing. */
|
|
165
174
|
cancel(runId: string, principal?: Principal, reason?: string): Promise<RunRecord>;
|
|
166
175
|
/**
|
package/dist-cjs/server/runs.js
CHANGED
|
@@ -180,6 +180,26 @@ class RunManager {
|
|
|
180
180
|
throw error;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
+
async addFeedback(runId, feedback, principal) {
|
|
184
|
+
const run = await this.run(runId, principal);
|
|
185
|
+
if (typeof feedback.key !== 'string' || !feedback.key.trim())
|
|
186
|
+
throw new errors_js_2.BadRequestError('Feedback needs a key');
|
|
187
|
+
if (feedback.score !== undefined && !(typeof feedback.score === 'number' && Number.isFinite(feedback.score))) {
|
|
188
|
+
throw new errors_js_2.BadRequestError('A feedback score is a finite number');
|
|
189
|
+
}
|
|
190
|
+
const at = this.now().toISOString();
|
|
191
|
+
const entry = {
|
|
192
|
+
key: feedback.key,
|
|
193
|
+
...(feedback.score === undefined ? {} : { score: feedback.score }),
|
|
194
|
+
...(feedback.value === undefined ? {} : { value: feedback.value }),
|
|
195
|
+
...(typeof feedback.comment === 'string' ? { comment: feedback.comment } : {}),
|
|
196
|
+
...(typeof feedback.source === 'string' ? { source: feedback.source } : {}),
|
|
197
|
+
createdAt: feedback.createdAt ?? at,
|
|
198
|
+
};
|
|
199
|
+
const next = { ...run, feedback: [...(run.feedback ?? []), entry].slice(-100), updatedAt: at };
|
|
200
|
+
await this.state.put(RUNS, runId, next);
|
|
201
|
+
return next;
|
|
202
|
+
}
|
|
183
203
|
async cancel(runId, principal, reason) {
|
|
184
204
|
const run = await this.run(runId, principal);
|
|
185
205
|
this.local.get(runId)?.cancel(reason);
|