@hue-run/sdk 0.9.0 → 0.11.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/CLI.md +240 -49
- package/ENVIRONMENTS.md +48 -9
- package/EVALUATIONS.md +41 -21
- package/README.md +70 -30
- package/dist/cli/eval-direct.d.ts +12 -1
- package/dist/cli/eval-direct.js +145 -33
- package/dist/cli/eval.d.ts +28 -1
- package/dist/cli/eval.js +504 -81
- package/dist/cli/listen.d.ts +76 -0
- package/dist/cli/listen.js +816 -0
- package/dist/cli/mcp.d.ts +65 -7
- package/dist/cli/mcp.js +271 -73
- package/dist/client.d.ts +7 -4
- package/dist/client.js +36 -10
- package/dist/evals/checkpoint.d.ts +15 -1
- package/dist/evals/checkpoint.js +34 -3
- package/dist/evals/client.d.ts +24 -4
- package/dist/evals/client.js +92 -38
- package/dist/evals/environment-target.d.ts +18 -3
- package/dist/evals/environment-target.js +22 -10
- package/dist/evals/exit-cleanup.d.ts +4 -0
- package/dist/evals/exit-cleanup.js +26 -0
- package/dist/evals/files.d.ts +69 -3
- package/dist/evals/files.js +255 -42
- package/dist/evals/json.js +116 -9
- package/dist/evals/local-worker.d.ts +21 -3
- package/dist/evals/local-worker.js +14 -0
- package/dist/evals/runner.d.ts +45 -0
- package/dist/evals/runner.js +199 -21
- package/dist/evals/scenarios.d.ts +7 -5
- package/dist/evals/scenarios.js +71 -14
- package/dist/evals/scorer-publication.js +58 -3
- package/dist/evals/simulation.d.ts +17 -4
- package/dist/evals/simulation.js +8 -0
- package/dist/evals/types.d.ts +48 -0
- package/dist/evals/verdicts.d.ts +26 -2
- package/dist/evals/verdicts.js +72 -9
- package/dist/evals.d.ts +4 -3
- package/dist/evals.js +2 -2
- package/dist/inline-files.d.ts +4 -0
- package/dist/inline-files.js +48 -17
- package/dist/provider-tools.d.ts +12 -1
- package/dist/provider-tools.js +192 -106
- package/dist/safety.d.ts +7 -1
- package/dist/safety.js +9 -7
- package/dist/setup/cli.js +31 -8
- package/dist/setup/peers.d.ts +13 -0
- package/dist/setup/peers.js +44 -0
- package/dist/snapshot.d.ts +2 -2
- package/dist/snapshot.js +47 -12
- package/dist/tool-definitions.d.ts +18 -0
- package/dist/tool-definitions.js +232 -0
- package/dist/transport.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/snapshot.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createTraceState } from "@opentelemetry/api";
|
|
2
2
|
import { types as utilTypes } from "node:util";
|
|
3
3
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
|
+
import { hashInlineFiles, INLINE_FILE_LIMIT, INLINE_FILE_TEXT_PER_RECORD, isMessageKey, } from "./inline-files.js";
|
|
4
5
|
// Intrinsic accessors are captured once and invoked with an explicit receiver so a
|
|
5
6
|
// hostile object cannot override them; the unbound reference is the point.
|
|
6
7
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
@@ -10,20 +11,52 @@ const typedArraySet = Uint8Array.prototype.set;
|
|
|
10
11
|
/** Copies only exported data, with the same finite budget used for admission. */
|
|
11
12
|
class Snapshot {
|
|
12
13
|
limit;
|
|
14
|
+
captureContent;
|
|
13
15
|
bytes = 512;
|
|
14
16
|
unresolvedResource = false;
|
|
15
17
|
nodes = 0;
|
|
16
18
|
ancestors = new Set();
|
|
17
19
|
copied = new Map();
|
|
18
|
-
|
|
20
|
+
hashed = new Map();
|
|
21
|
+
inspected = 0;
|
|
22
|
+
/** In metadata-only mode the recorded messages, which export strips, are not copied at all. */
|
|
23
|
+
constructor(limit, captureContent = true) {
|
|
19
24
|
this.limit = limit;
|
|
25
|
+
this.captureContent = captureContent;
|
|
20
26
|
}
|
|
21
27
|
charge(bytes) {
|
|
22
28
|
this.bytes += bytes;
|
|
23
29
|
if (this.bytes > this.limit)
|
|
24
30
|
throw new RangeError("Telemetry byte budget exceeded");
|
|
25
31
|
}
|
|
26
|
-
|
|
32
|
+
/**
|
|
33
|
+
* An attribute map. A large inline file in a message attribute shrinks to its digest before it
|
|
34
|
+
* is charged: export replaces it anyway, and charging the file could drop the whole record.
|
|
35
|
+
*/
|
|
36
|
+
attributes(value) {
|
|
37
|
+
return this.copy(value, 1, "attributes");
|
|
38
|
+
}
|
|
39
|
+
/** A message attribute with its large inline files hashed. The same text is hashed once per
|
|
40
|
+
* record, and a record inspects at most {@link INLINE_FILE_TEXT_PER_RECORD} of it in all;
|
|
41
|
+
* beyond that, messages are charged as recorded. */
|
|
42
|
+
inlineFiles(key, value) {
|
|
43
|
+
// A UTF-16 unit is at most three UTF-8 bytes, so shorter text cannot hold a file to hash.
|
|
44
|
+
if (typeof value !== "string" || !isMessageKey(key) || value.length * 3 <= INLINE_FILE_LIMIT)
|
|
45
|
+
return value;
|
|
46
|
+
if (this.hashed.has(value))
|
|
47
|
+
return this.hashed.get(value);
|
|
48
|
+
if (this.inspected + value.length > INLINE_FILE_TEXT_PER_RECORD)
|
|
49
|
+
return value;
|
|
50
|
+
this.inspected += value.length;
|
|
51
|
+
const result = hashInlineFiles(key, value);
|
|
52
|
+
this.hashed.set(value, result);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/** Span events, whose attributes are attribute maps. */
|
|
56
|
+
events(value) {
|
|
57
|
+
return this.copy(value, 1, "events");
|
|
58
|
+
}
|
|
59
|
+
copy(value, depth = 0, shape = "value") {
|
|
27
60
|
if (++this.nodes > 16384 || depth > 32)
|
|
28
61
|
throw new RangeError("Telemetry complexity limit exceeded");
|
|
29
62
|
this.charge(16);
|
|
@@ -69,7 +102,7 @@ class Snapshot {
|
|
|
69
102
|
const descriptor = Object.getOwnPropertyDescriptor(value, index);
|
|
70
103
|
if (descriptor && !("value" in descriptor))
|
|
71
104
|
throw new TypeError("Telemetry accessors are unsupported");
|
|
72
|
-
copy.push(this.copy(descriptor?.value, depth + 1));
|
|
105
|
+
copy.push(this.copy(descriptor?.value, depth + 1, shape === "events" ? "event" : "value"));
|
|
73
106
|
}
|
|
74
107
|
}
|
|
75
108
|
else {
|
|
@@ -79,8 +112,10 @@ class Snapshot {
|
|
|
79
112
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
80
113
|
if (!descriptor || !("value" in descriptor))
|
|
81
114
|
throw new TypeError("Telemetry accessors are unsupported");
|
|
115
|
+
if (shape === "attributes" && !this.captureContent && isMessageKey(key))
|
|
116
|
+
continue;
|
|
82
117
|
this.charge(key.length * 2 + 16);
|
|
83
|
-
copy[key] = this.copy(descriptor.value, depth + 1);
|
|
118
|
+
copy[key] = this.copy(shape === "attributes" ? this.inlineFiles(key, descriptor.value) : descriptor.value, depth + 1, shape === "event" && key === "attributes" ? "attributes" : "value");
|
|
84
119
|
}
|
|
85
120
|
}
|
|
86
121
|
this.ancestors.delete(value);
|
|
@@ -126,14 +161,14 @@ class Snapshot {
|
|
|
126
161
|
function contextReader(context) {
|
|
127
162
|
return () => context;
|
|
128
163
|
}
|
|
129
|
-
export function snapshotSpan(source, limit) {
|
|
130
|
-
const snapshot = new Snapshot(limit);
|
|
164
|
+
export function snapshotSpan(source, limit, captureContent = true) {
|
|
165
|
+
const snapshot = new Snapshot(limit, captureContent);
|
|
131
166
|
const context = snapshot.context(source.spanContext());
|
|
132
167
|
if (source.links.length > 16384)
|
|
133
168
|
throw new RangeError("Link complexity limit exceeded");
|
|
134
169
|
const links = source.links.map((link) => ({
|
|
135
170
|
context: snapshot.context(link.context),
|
|
136
|
-
attributes: snapshot.
|
|
171
|
+
attributes: snapshot.attributes(link.attributes),
|
|
137
172
|
droppedAttributesCount: snapshot.copy(link.droppedAttributesCount),
|
|
138
173
|
}));
|
|
139
174
|
const record = {
|
|
@@ -145,13 +180,13 @@ export function snapshotSpan(source, limit) {
|
|
|
145
180
|
duration: source.duration,
|
|
146
181
|
ended: source.ended,
|
|
147
182
|
status: source.status,
|
|
148
|
-
attributes: source.attributes,
|
|
149
|
-
events: source.events,
|
|
150
183
|
instrumentationScope: source.instrumentationScope,
|
|
151
184
|
droppedAttributesCount: source.droppedAttributesCount,
|
|
152
185
|
droppedEventsCount: source.droppedEventsCount,
|
|
153
186
|
droppedLinksCount: source.droppedLinksCount,
|
|
154
187
|
}),
|
|
188
|
+
attributes: snapshot.attributes(source.attributes),
|
|
189
|
+
events: snapshot.events(source.events),
|
|
155
190
|
spanContext: contextReader(context),
|
|
156
191
|
parentSpanContext: snapshot.context(source.parentSpanContext),
|
|
157
192
|
links,
|
|
@@ -159,8 +194,8 @@ export function snapshotSpan(source, limit) {
|
|
|
159
194
|
};
|
|
160
195
|
return { record, bytes: snapshot.bytes, unresolvedResource: snapshot.unresolvedResource };
|
|
161
196
|
}
|
|
162
|
-
export function snapshotLog(source, limit) {
|
|
163
|
-
const snapshot = new Snapshot(limit);
|
|
197
|
+
export function snapshotLog(source, limit, captureContent = true) {
|
|
198
|
+
const snapshot = new Snapshot(limit, captureContent);
|
|
164
199
|
// Only our batching processor sees this copy. Its writer methods deliberately
|
|
165
200
|
// cannot mutate the admitted snapshot or invalidate its charged byte count.
|
|
166
201
|
const record = {
|
|
@@ -171,10 +206,10 @@ export function snapshotLog(source, limit) {
|
|
|
171
206
|
severityNumber: source.severityNumber,
|
|
172
207
|
eventName: source.eventName,
|
|
173
208
|
body: source.body,
|
|
174
|
-
attributes: source.attributes,
|
|
175
209
|
instrumentationScope: source.instrumentationScope,
|
|
176
210
|
droppedAttributesCount: source.droppedAttributesCount,
|
|
177
211
|
}),
|
|
212
|
+
attributes: snapshot.attributes(source.attributes),
|
|
178
213
|
spanContext: snapshot.context(source.spanContext),
|
|
179
214
|
resource: snapshot.resource(source.resource),
|
|
180
215
|
setAttribute() {
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes credentials from free text a provider returned, such as an MCP call's error message,
|
|
3
|
+
* with the rules tool definitions use, extended for text: each `http`, `https`, `ws`, `wss` or
|
|
4
|
+
* `ftp` URL loses its userinfo and fragment and every query value becomes `[redacted]`, as a
|
|
5
|
+
* `url` field does (an unparseable one becomes `[redacted]`), and a URL with any other scheme
|
|
6
|
+
* becomes `[redacted]` when it has an `@`, `?` or `#`; a token with a known credential prefix
|
|
7
|
+
* (`hue_sk_`, `sk-`, `xoxb-`, `ya29.` and others), the credential after an authorization scheme
|
|
8
|
+
* (`Bearer`, `Basic`, `Token`), the whole value of an `Authorization` header, and the value of a
|
|
9
|
+
* `key=value` or `key: value` pair whose key names a credential (quoted, escaped-quoted or bare)
|
|
10
|
+
* become `[redacted]`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function scrubCredentialText(text: string): string;
|
|
1
13
|
/**
|
|
2
14
|
* Removes hosted-tool credentials from an exported attribute value, before the caller's `redact`.
|
|
3
15
|
* Tool definitions come from OpenTelemetry GenAI (`gen_ai.tool.definitions`), AI SDK 6
|
|
@@ -18,3 +30,9 @@ export declare function scrubToolCredentials(key: string, value: unknown): unkno
|
|
|
18
30
|
* already sets are kept.
|
|
19
31
|
*/
|
|
20
32
|
export declare function withToolCatalogSummary<T extends Record<string, unknown>>(source: T): T;
|
|
33
|
+
/**
|
|
34
|
+
* The metadata-only summary of one JSON-encoded definition list: `hue.tool.names` and
|
|
35
|
+
* `hue.tool.definitions.sha256`, exactly as export summarizes a record's
|
|
36
|
+
* `gen_ai.tool.definitions`. Empty when the list cannot be summarized.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toolCatalogSummary(definitions: string): Record<string, string | string[]>;
|
package/dist/tool-definitions.js
CHANGED
|
@@ -72,6 +72,223 @@ function isCredentialKey(key) {
|
|
|
72
72
|
normalized.endsWith("apikey") ||
|
|
73
73
|
normalized.endsWith("credential"));
|
|
74
74
|
}
|
|
75
|
+
/** A URL's `://` and everything after it up to whitespace, a quote or `<>`. A quoted value right
|
|
76
|
+
* after `=` (`?token="…"`) is part of the URL, so the whole value is replaced. */
|
|
77
|
+
const urlRest = /:\/\/(?:[^\s"'<>`]|(?<==)"[^"<>`\r\n]*"|(?<==)'[^'<>`\r\n]*'|(?<==)["'])+/y;
|
|
78
|
+
const isSchemeLetter = (code) => (code | 0x20) >= 0x61 && (code | 0x20) <= 0x7a;
|
|
79
|
+
/** `a-z`, `0-9`, `+`, `.` and `-`, case-insensitively. */
|
|
80
|
+
const isSchemeCharacter = (code) => isSchemeLetter(code) ||
|
|
81
|
+
(code >= 0x30 && code <= 0x39) ||
|
|
82
|
+
code === 0x2b ||
|
|
83
|
+
code === 0x2e ||
|
|
84
|
+
code === 0x2d;
|
|
85
|
+
/** Scrubs each URL in free text. Each `://` is found by search and its scheme read back from it:
|
|
86
|
+
* up to 64 scheme characters, starting at a letter, so a longer run before `://` still leaves a
|
|
87
|
+
* URL to scrub and a long run such as `a.a.a…` costs one pass. */
|
|
88
|
+
function scrubTextUrls(text, state) {
|
|
89
|
+
let result = "";
|
|
90
|
+
let copied = 0;
|
|
91
|
+
for (let index = text.indexOf("://"); index !== -1; index = text.indexOf("://", index + 1)) {
|
|
92
|
+
if (index < copied)
|
|
93
|
+
continue;
|
|
94
|
+
let start = index;
|
|
95
|
+
while (start > copied && index - start < 64 && isSchemeCharacter(text.charCodeAt(start - 1)))
|
|
96
|
+
start--;
|
|
97
|
+
while (start < index && !isSchemeLetter(text.charCodeAt(start)))
|
|
98
|
+
start++;
|
|
99
|
+
if (start === index)
|
|
100
|
+
continue;
|
|
101
|
+
urlRest.lastIndex = index;
|
|
102
|
+
const rest = urlRest.exec(text);
|
|
103
|
+
if (!rest)
|
|
104
|
+
continue;
|
|
105
|
+
const end = index + rest[0].length;
|
|
106
|
+
result += text.slice(copied, start) + scrubTextUrl(text.slice(start, end), state);
|
|
107
|
+
copied = end;
|
|
108
|
+
}
|
|
109
|
+
return result + text.slice(copied);
|
|
110
|
+
}
|
|
111
|
+
/** Schemes WHATWG parses as hierarchical, which both SDKs serialize alike. */
|
|
112
|
+
const specialScheme = /^(?:https?|wss?|ftp):/i;
|
|
113
|
+
/** A URL in free text. One with another scheme, which runtimes parse differently, is replaced
|
|
114
|
+
* whole when it could carry userinfo, a query or a fragment. */
|
|
115
|
+
function scrubTextUrl(url, state) {
|
|
116
|
+
if (specialScheme.test(url))
|
|
117
|
+
return scrubUrl(url, state);
|
|
118
|
+
return /[@?#]/.test(url) ? REDACTED : url;
|
|
119
|
+
}
|
|
120
|
+
/** A credential its own prefix identifies wherever it appears, `%` escapes included: Hue's API, MCP,
|
|
121
|
+
* world, attempt, simulation, setup, install and invocation tokens, and OpenAI and Anthropic
|
|
122
|
+
* (`sk-`), Stripe, Slack, Google OAuth, GitHub and GitLab ones. */
|
|
123
|
+
const prefixedToken = /\b(?:hue_(?:sk|mcp|world|attempt|sim|setup|install|inv)_|sk-|[rs]k_(?:live|test)_|xox[abcdoprs]-|xapp-|ya29\.|gh[opsur]_|github_pat_|glpat-)[a-z0-9_.~+/=%-]{8,}/gi;
|
|
124
|
+
/** An authorization scheme followed by its credential, as in an `Authorization` header, up to
|
|
125
|
+
* whitespace, a quote, a delimiter or a backslash. The credential cannot start with `=` or `:`, so
|
|
126
|
+
* `token = value` and `Token : value` are left to the key-value rule. */
|
|
127
|
+
const authorizationValue = /\b(bearer|basic|token)(\s+)[^\s"'`<>=:,;(){}[\]\\][^\s"'`<>,;(){}[\]\\]*/gi;
|
|
128
|
+
/** The key and separator of a `key=value` or `key: value` pair, the key optionally quoted, with a
|
|
129
|
+
* backslash-escaped quote too (JSON inside a string). A key starts where no key character precedes
|
|
130
|
+
* it, so each word is tried once and a long run stays linear. The value is not consumed, so a pair
|
|
131
|
+
* inside another pair's value (`error: token=…`) is found. */
|
|
132
|
+
const pairKey = /(\\?["']|)(?<![a-z0-9_-])([a-z0-9_-]+)\1(\s*[:=]\s*)/gi;
|
|
133
|
+
/** A quoted value to its closing quote on the same line, spaces and escaped quotes included, or
|
|
134
|
+
* one between backslash-escaped quotes, double or single. */
|
|
135
|
+
const quotedValue = /"(?:[^"\\\r\n]|\\[^\r\n])+"|'(?:[^'\\\r\n]|\\[^\r\n])+'|\\"(?:[^"\\\r\n]|\\[^"\r\n])+\\"|\\'(?:[^'\\\r\n]|\\[^'\r\n])+\\'/y;
|
|
136
|
+
/** An unquoted value, or one whose quote does not close on its line, up to whitespace, a quote or
|
|
137
|
+
* a delimiter; a value already replaced, or a scheme whose credential was, is left alone. */
|
|
138
|
+
const bareValue = /(\\?["']?)(?!\[redacted\]|%5Bredacted%5D|(?:bearer|basic|token)\s)[^\s"',;&})\]]+/iy;
|
|
139
|
+
/** An `Authorization` header's unquoted value: its scheme and the credential after it (`Bot …`,
|
|
140
|
+
* `OAuth1 …`), or a lone credential. One already replaced is left alone. */
|
|
141
|
+
const authorizationBare = /(\\?["']?)(?!\[redacted\]|%5Bredacted%5D)([^\s"',;})\]]+)(?:[ \t]+(?:\[redacted\]|[^\s"',;})\]]+))?/y;
|
|
142
|
+
function normalizedKey(key) {
|
|
143
|
+
return key.toLowerCase().replace(/[-_]/g, "");
|
|
144
|
+
}
|
|
145
|
+
/** A key naming a credential in free text: a tool definition's credential keys, any header ending
|
|
146
|
+
* in `Authorization`, and `Bearer` and `Basic`. */
|
|
147
|
+
function isTextCredentialKey(key) {
|
|
148
|
+
const normalized = normalizedKey(key);
|
|
149
|
+
return (isCredentialKey(key) ||
|
|
150
|
+
normalized.endsWith("authorization") ||
|
|
151
|
+
normalized === "bearer" ||
|
|
152
|
+
normalized === "basic");
|
|
153
|
+
}
|
|
154
|
+
/** `API key: …`: a credential named in two words, `key` right after `API`. */
|
|
155
|
+
const apiBefore = /(?:^|[^a-z0-9_])api[ \t]+$/i;
|
|
156
|
+
function isApiKeyPhrase(text, keyStart, key) {
|
|
157
|
+
return (key.toLowerCase() === "key" && apiBefore.test(text.slice(Math.max(0, keyStart - 16), keyStart)));
|
|
158
|
+
}
|
|
159
|
+
/** Whether a quote, or a backslash-escaped quote, opens at `index`. */
|
|
160
|
+
function opensQuote(text, index) {
|
|
161
|
+
const character = text[index];
|
|
162
|
+
return (character === '"' ||
|
|
163
|
+
character === "'" ||
|
|
164
|
+
text.startsWith('\\"', index) ||
|
|
165
|
+
text.startsWith("\\'", index));
|
|
166
|
+
}
|
|
167
|
+
/** The value of each pair whose key names a credential, without its quotes. A pair inside an
|
|
168
|
+
* earlier pair's value is kept when its value runs past that value. */
|
|
169
|
+
function pairSpans(text) {
|
|
170
|
+
const spans = [];
|
|
171
|
+
let covered = 0;
|
|
172
|
+
// The last `Authorization` value read: where it starts, where its first word ends and where it
|
|
173
|
+
// ends. A value starting inside that first word ends where it does, so each run is read once.
|
|
174
|
+
let authorization;
|
|
175
|
+
for (const match of text.matchAll(pairKey)) {
|
|
176
|
+
const start = match.index + match[0].length;
|
|
177
|
+
const key = match[2];
|
|
178
|
+
const keyStart = match.index + match[1].length;
|
|
179
|
+
if (!(isTextCredentialKey(key) || isApiKeyPhrase(text, keyStart, key)))
|
|
180
|
+
continue;
|
|
181
|
+
const nested = start < covered;
|
|
182
|
+
const isAuthorization = normalizedKey(key).endsWith("authorization");
|
|
183
|
+
// A value inside an earlier value ends where that value's run or quote ends, so it can run
|
|
184
|
+
// past it only by opening a quote (which may be the one that closes the earlier value) or,
|
|
185
|
+
// for `Authorization`, by the word after its scheme.
|
|
186
|
+
if (nested && !isAuthorization && !opensQuote(text, start))
|
|
187
|
+
continue;
|
|
188
|
+
quotedValue.lastIndex = start;
|
|
189
|
+
const quoted = quotedValue.exec(text);
|
|
190
|
+
let open;
|
|
191
|
+
let end;
|
|
192
|
+
if (quoted) {
|
|
193
|
+
open = quoted[0].startsWith("\\") ? 2 : 1;
|
|
194
|
+
end = start + quoted[0].length;
|
|
195
|
+
}
|
|
196
|
+
else if (isAuthorization) {
|
|
197
|
+
if (authorization &&
|
|
198
|
+
start > authorization.from &&
|
|
199
|
+
start < authorization.firstEnd &&
|
|
200
|
+
!opensQuote(text, start) &&
|
|
201
|
+
!text.startsWith("[redacted]", start) &&
|
|
202
|
+
!text.startsWith("%5Bredacted%5D", start)) {
|
|
203
|
+
open = 0;
|
|
204
|
+
end = authorization.end;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
authorizationBare.lastIndex = start;
|
|
208
|
+
const value = authorizationBare.exec(text);
|
|
209
|
+
if (!value)
|
|
210
|
+
continue;
|
|
211
|
+
open = value[1].length;
|
|
212
|
+
end = start + value[0].length;
|
|
213
|
+
authorization = { from: start, firstEnd: start + open + value[2].length, end };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
bareValue.lastIndex = start;
|
|
218
|
+
const value = bareValue.exec(text);
|
|
219
|
+
if (!value)
|
|
220
|
+
continue;
|
|
221
|
+
open = value[1].length;
|
|
222
|
+
end = start + value[0].length;
|
|
223
|
+
}
|
|
224
|
+
if (end <= covered)
|
|
225
|
+
continue;
|
|
226
|
+
spans.push([start + open, end - (quoted ? open : 0)]);
|
|
227
|
+
covered = end;
|
|
228
|
+
}
|
|
229
|
+
return spans;
|
|
230
|
+
}
|
|
231
|
+
/** The credential after each authorization scheme. A scheme word can end an earlier credential
|
|
232
|
+
* (`…~bearer SECRET`), so the search resumes at each credential, which is read at most once more. */
|
|
233
|
+
function authorizationSpans(text) {
|
|
234
|
+
const spans = [];
|
|
235
|
+
authorizationValue.lastIndex = 0;
|
|
236
|
+
for (let match = authorizationValue.exec(text); match; match = authorizationValue.exec(text)) {
|
|
237
|
+
const credential = match.index + match[1].length + match[2].length;
|
|
238
|
+
spans.push([credential, match.index + match[0].length]);
|
|
239
|
+
authorizationValue.lastIndex = credential;
|
|
240
|
+
}
|
|
241
|
+
return spans;
|
|
242
|
+
}
|
|
243
|
+
/** Each match of a global pattern. */
|
|
244
|
+
function matchSpans(text, pattern) {
|
|
245
|
+
return [...text.matchAll(pattern)].map((match) => [match.index, match.index + match[0].length]);
|
|
246
|
+
}
|
|
247
|
+
/** Replaces the union of the spans with `[redacted]`, each run of overlapping or touching spans
|
|
248
|
+
* once. */
|
|
249
|
+
function redactSpans(text, spans) {
|
|
250
|
+
spans.sort(([a, b], [c, d]) => a - c || b - d);
|
|
251
|
+
let result = "";
|
|
252
|
+
let copied = 0;
|
|
253
|
+
let run;
|
|
254
|
+
const flush = () => {
|
|
255
|
+
if (!run)
|
|
256
|
+
return;
|
|
257
|
+
result += `${text.slice(copied, run[0])}${REDACTED}`;
|
|
258
|
+
copied = run[1];
|
|
259
|
+
};
|
|
260
|
+
for (const [start, end] of spans) {
|
|
261
|
+
if (run && start <= run[1])
|
|
262
|
+
run[1] = Math.max(run[1], end);
|
|
263
|
+
else {
|
|
264
|
+
flush();
|
|
265
|
+
run = [start, end];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
flush();
|
|
269
|
+
return result + text.slice(copied);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Removes credentials from free text a provider returned, such as an MCP call's error message,
|
|
273
|
+
* with the rules tool definitions use, extended for text: each `http`, `https`, `ws`, `wss` or
|
|
274
|
+
* `ftp` URL loses its userinfo and fragment and every query value becomes `[redacted]`, as a
|
|
275
|
+
* `url` field does (an unparseable one becomes `[redacted]`), and a URL with any other scheme
|
|
276
|
+
* becomes `[redacted]` when it has an `@`, `?` or `#`; a token with a known credential prefix
|
|
277
|
+
* (`hue_sk_`, `sk-`, `xoxb-`, `ya29.` and others), the credential after an authorization scheme
|
|
278
|
+
* (`Bearer`, `Basic`, `Token`), the whole value of an `Authorization` header, and the value of a
|
|
279
|
+
* `key=value` or `key: value` pair whose key names a credential (quoted, escaped-quoted or bare)
|
|
280
|
+
* become `[redacted]`.
|
|
281
|
+
*/
|
|
282
|
+
export function scrubCredentialText(text) {
|
|
283
|
+
const scrubbed = scrubTextUrls(text, { changed: false });
|
|
284
|
+
// Every rule reads the same text and their matches are replaced together, so no rule's
|
|
285
|
+
// replacement can hide text another rule would have matched.
|
|
286
|
+
return redactSpans(scrubbed, [
|
|
287
|
+
...pairSpans(scrubbed),
|
|
288
|
+
...matchSpans(scrubbed, prefixedToken),
|
|
289
|
+
...authorizationSpans(scrubbed),
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
75
292
|
/** OpenInference records each tool as `llm.tools.{index}.tool.json_schema`. */
|
|
76
293
|
const openInferenceTool = /^llm\.tools\.(\d+)\.tool\.json_schema$/;
|
|
77
294
|
/**
|
|
@@ -272,3 +489,18 @@ export function withToolCatalogSummary(source) {
|
|
|
272
489
|
}
|
|
273
490
|
return { ...summary, ...source };
|
|
274
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* The metadata-only summary of one JSON-encoded definition list: `hue.tool.names` and
|
|
494
|
+
* `hue.tool.definitions.sha256`, exactly as export summarizes a record's
|
|
495
|
+
* `gen_ai.tool.definitions`. Empty when the list cannot be summarized.
|
|
496
|
+
*/
|
|
497
|
+
export function toolCatalogSummary(definitions) {
|
|
498
|
+
const summarized = withToolCatalogSummary({
|
|
499
|
+
"gen_ai.tool.definitions": definitions,
|
|
500
|
+
});
|
|
501
|
+
const summary = {};
|
|
502
|
+
for (const key of ["hue.tool.names", "hue.tool.definitions.sha256"])
|
|
503
|
+
if (key in summarized)
|
|
504
|
+
summary[key] = summarized[key];
|
|
505
|
+
return summary;
|
|
506
|
+
}
|
package/dist/transport.js
CHANGED
|
@@ -191,8 +191,8 @@ export class HueTransport {
|
|
|
191
191
|
const remaining = (advisory ? this.options.maxQueueBytes / 4 : this.options.maxQueueBytes) -
|
|
192
192
|
this.pendingBytes;
|
|
193
193
|
const snapshot = signal === "traces"
|
|
194
|
-
? snapshotSpan(record, remaining)
|
|
195
|
-
: snapshotLog(record, remaining);
|
|
194
|
+
? snapshotSpan(record, remaining, this.options.captureContent)
|
|
195
|
+
: snapshotLog(record, remaining, this.options.captureContent);
|
|
196
196
|
this.pendingBytes += snapshot.bytes;
|
|
197
197
|
if (signal === "traces")
|
|
198
198
|
this.spans.set(snapshot.record, snapshot.bytes);
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Package version shared by the instrumentation scope and the export User-Agent. */
|
|
2
|
-
export declare const sdkVersion = "0.
|
|
2
|
+
export declare const sdkVersion = "0.11.0";
|
package/dist/version.js
CHANGED