@hue-run/sdk 0.17.0 → 0.18.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 +8 -8
- package/EVALUATIONS.md +53 -2
- package/README.md +262 -37
- package/dist/attachment.d.ts +82 -0
- package/dist/attachment.js +225 -0
- package/dist/blobs.d.ts +41 -3
- package/dist/blobs.js +126 -41
- package/dist/client.d.ts +18 -0
- package/dist/client.js +148 -13
- package/dist/evals/client.d.ts +14 -1
- package/dist/evals/client.js +73 -9
- package/dist/evals/files.d.ts +11 -1
- package/dist/evals/files.js +160 -20
- package/dist/evals/media.d.ts +217 -0
- package/dist/evals/media.js +907 -0
- package/dist/evals/runner.js +130 -106
- package/dist/evals/types.d.ts +48 -0
- package/dist/evals.d.ts +2 -0
- package/dist/evals.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inline-files.d.ts +131 -8
- package/dist/inline-files.js +565 -47
- package/dist/json-edits.d.ts +24 -0
- package/dist/json-edits.js +187 -0
- package/dist/privacy.d.ts +13 -0
- package/dist/privacy.js +84 -17
- package/dist/safety.d.ts +6 -2
- package/dist/safety.js +15 -3
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +123 -29
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +285 -55
- package/dist/types.d.ts +25 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edits to recorded JSON text that keep every byte they do not change. Placing a file in a value
|
|
3
|
+
* read from JSON text (an attribute, or a tool call's JSON-encoded arguments) replaces only the
|
|
4
|
+
* file's own value, or the members of the part that held it; the rest of the text, its key order,
|
|
5
|
+
* number spellings and whitespace included, is kept as it was recorded. Mirrors the Python SDK's
|
|
6
|
+
* `_json_edits.py`, so both SDKs write the same bytes.
|
|
7
|
+
*/
|
|
8
|
+
/** One change at a JSON pointer of a document: its value replaced by `value` (JSON text), or,
|
|
9
|
+
* for an object, the member `remove` dropped and each `set` member's value replaced in place or
|
|
10
|
+
* appended. */
|
|
11
|
+
export type JsonEdit = {
|
|
12
|
+
pointer: string;
|
|
13
|
+
value: string;
|
|
14
|
+
} | {
|
|
15
|
+
pointer: string;
|
|
16
|
+
remove?: string;
|
|
17
|
+
set?: readonly [string, string][];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* `text` with `edits` applied, every byte outside the changed values kept. Each object edit
|
|
21
|
+
* keeps the object's other members, the separators between them and its whitespace; appended
|
|
22
|
+
* members are written compactly (`,"key":value`) before the closing brace.
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyJsonEdits(text: string, edits: readonly JsonEdit[]): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edits to recorded JSON text that keep every byte they do not change. Placing a file in a value
|
|
3
|
+
* read from JSON text (an attribute, or a tool call's JSON-encoded arguments) replaces only the
|
|
4
|
+
* file's own value, or the members of the part that held it; the rest of the text, its key order,
|
|
5
|
+
* number spellings and whitespace included, is kept as it was recorded. Mirrors the Python SDK's
|
|
6
|
+
* `_json_edits.py`, so both SDKs write the same bytes.
|
|
7
|
+
*/
|
|
8
|
+
const segment = (key) => key.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
9
|
+
/** The spans of the values at `wanted` pointers of valid JSON `text`, with the members of each
|
|
10
|
+
* object among them. A duplicate key's last member is the one a pointer names. */
|
|
11
|
+
function locate(text, wanted) {
|
|
12
|
+
const prefixes = new Set();
|
|
13
|
+
for (const pointer of wanted)
|
|
14
|
+
for (let at = pointer.length; at >= 0; at = pointer.lastIndexOf("/", at - 1)) {
|
|
15
|
+
prefixes.add(pointer.slice(0, at));
|
|
16
|
+
if (at === 0)
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
const found = new Map();
|
|
20
|
+
const space = (index) => {
|
|
21
|
+
let at = index;
|
|
22
|
+
for (;;) {
|
|
23
|
+
const code = text.charCodeAt(at);
|
|
24
|
+
if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d)
|
|
25
|
+
at++;
|
|
26
|
+
else
|
|
27
|
+
return at;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const stringEnd = (index) => {
|
|
31
|
+
let at = index + 1;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const code = text.charCodeAt(at);
|
|
34
|
+
if (code === 0x22)
|
|
35
|
+
return at + 1;
|
|
36
|
+
at += code === 0x5c ? 2 : 1;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** The end of the value starting at `index`, and an object's members when asked. */
|
|
40
|
+
const valueEnd = (index, members) => {
|
|
41
|
+
const code = text.charCodeAt(index);
|
|
42
|
+
if (code === 0x22)
|
|
43
|
+
return stringEnd(index);
|
|
44
|
+
if (code === 0x7b || code === 0x5b) {
|
|
45
|
+
const object = code === 0x7b;
|
|
46
|
+
let at = space(index + 1);
|
|
47
|
+
if (text.charCodeAt(at) === (object ? 0x7d : 0x5d))
|
|
48
|
+
return at + 1;
|
|
49
|
+
for (;;) {
|
|
50
|
+
let key = "";
|
|
51
|
+
let start = at;
|
|
52
|
+
if (object) {
|
|
53
|
+
const keyEnd = stringEnd(at);
|
|
54
|
+
if (members)
|
|
55
|
+
key = JSON.parse(text.slice(at, keyEnd));
|
|
56
|
+
at = space(space(keyEnd) + 1);
|
|
57
|
+
}
|
|
58
|
+
const valueStart = at;
|
|
59
|
+
const end = valueEnd(at);
|
|
60
|
+
if (members)
|
|
61
|
+
members.push({ key, start, valueStart, end });
|
|
62
|
+
at = space(end);
|
|
63
|
+
if (text.charCodeAt(at) === 0x2c) {
|
|
64
|
+
at = space(at + 1);
|
|
65
|
+
start = at;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return at + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let at = index;
|
|
72
|
+
for (;;) {
|
|
73
|
+
const code = text.charCodeAt(at);
|
|
74
|
+
// A number's characters, or a literal's letters.
|
|
75
|
+
if ((code >= 0x30 && code <= 0x39) ||
|
|
76
|
+
code === 0x2d ||
|
|
77
|
+
code === 0x2b ||
|
|
78
|
+
code === 0x2e ||
|
|
79
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
80
|
+
code === 0x45)
|
|
81
|
+
at++;
|
|
82
|
+
else
|
|
83
|
+
return at;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const visit = (index, pointer) => {
|
|
87
|
+
const code = text.charCodeAt(index);
|
|
88
|
+
const descend = code === 0x7b || code === 0x5b;
|
|
89
|
+
const members = descend ? [] : undefined;
|
|
90
|
+
const end = valueEnd(index, members);
|
|
91
|
+
if (wanted.has(pointer))
|
|
92
|
+
found.set(pointer, code === 0x7b ? { start: index, end, members } : { start: index, end });
|
|
93
|
+
if (!members)
|
|
94
|
+
return;
|
|
95
|
+
const last = new Map();
|
|
96
|
+
members.forEach((member, position) => last.set(code === 0x7b ? member.key : String(position), member));
|
|
97
|
+
for (const [key, member] of last) {
|
|
98
|
+
const child = `${pointer}/${segment(key)}`;
|
|
99
|
+
if (prefixes.has(child))
|
|
100
|
+
visit(member.valueStart, child);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
visit(space(0), "");
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `text` with `edits` applied, every byte outside the changed values kept. Each object edit
|
|
108
|
+
* keeps the object's other members, the separators between them and its whitespace; appended
|
|
109
|
+
* members are written compactly (`,"key":value`) before the closing brace.
|
|
110
|
+
*/
|
|
111
|
+
export function applyJsonEdits(text, edits) {
|
|
112
|
+
if (!edits.length)
|
|
113
|
+
return text;
|
|
114
|
+
const values = new Map();
|
|
115
|
+
const objects = new Map();
|
|
116
|
+
for (const edit of edits) {
|
|
117
|
+
if ("value" in edit)
|
|
118
|
+
values.set(edit.pointer, edit.value);
|
|
119
|
+
else {
|
|
120
|
+
const object = objects.get(edit.pointer) ?? { remove: new Set(), set: new Map() };
|
|
121
|
+
if (edit.remove !== undefined)
|
|
122
|
+
object.remove.add(edit.remove);
|
|
123
|
+
for (const [key, value] of edit.set ?? [])
|
|
124
|
+
object.set.set(key, value);
|
|
125
|
+
objects.set(edit.pointer, object);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const located = locate(text, new Set([...values.keys(), ...objects.keys()]));
|
|
129
|
+
const pieces = [];
|
|
130
|
+
for (const [pointer, value] of values) {
|
|
131
|
+
const span = located.get(pointer);
|
|
132
|
+
if (span)
|
|
133
|
+
pieces.push({ start: span.start, end: span.end, render: () => value });
|
|
134
|
+
}
|
|
135
|
+
for (const [pointer, edit] of objects) {
|
|
136
|
+
const span = located.get(pointer);
|
|
137
|
+
if (!span?.members)
|
|
138
|
+
continue;
|
|
139
|
+
const members = span.members;
|
|
140
|
+
// The last member of each key set in place keeps its position; the others are appended.
|
|
141
|
+
const lastOf = new Map();
|
|
142
|
+
for (const member of members)
|
|
143
|
+
lastOf.set(member.key, member);
|
|
144
|
+
for (const [key, value] of edit.set) {
|
|
145
|
+
const member = lastOf.get(key);
|
|
146
|
+
if (member && !edit.remove.has(key))
|
|
147
|
+
pieces.push({ start: member.valueStart, end: member.end, render: () => value });
|
|
148
|
+
}
|
|
149
|
+
pieces.push({
|
|
150
|
+
start: span.start,
|
|
151
|
+
end: span.end,
|
|
152
|
+
render: () => {
|
|
153
|
+
const kept = members.filter((member) => !edit.remove.has(member.key));
|
|
154
|
+
const first = members[0]?.start ?? span.end - 1;
|
|
155
|
+
const close = members.length ? members.at(-1).end : span.end - 1;
|
|
156
|
+
let out = text.slice(span.start, first);
|
|
157
|
+
kept.forEach((member, position) => {
|
|
158
|
+
const index = members.indexOf(member);
|
|
159
|
+
// The separator that preceded this member, comma included, unless it now comes first.
|
|
160
|
+
if (position > 0)
|
|
161
|
+
out += text.slice(members[index - 1].end, member.start);
|
|
162
|
+
out += render(member.start, member.end);
|
|
163
|
+
});
|
|
164
|
+
let appended = "";
|
|
165
|
+
for (const [key, value] of edit.set)
|
|
166
|
+
if (!lastOf.has(key) || edit.remove.has(key))
|
|
167
|
+
appended += `${kept.length || appended ? "," : ""}${JSON.stringify(key)}:${value}`;
|
|
168
|
+
return out + appended + text.slice(close, span.end);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
pieces.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
173
|
+
/** The text from `start` to `end` with the pieces within it rendered; a piece inside another
|
|
174
|
+
* is rendered by the outer one. */
|
|
175
|
+
const render = (start, end) => {
|
|
176
|
+
let out = "";
|
|
177
|
+
let at = start;
|
|
178
|
+
for (const piece of pieces) {
|
|
179
|
+
if (piece.start < at || piece.end > end)
|
|
180
|
+
continue;
|
|
181
|
+
out += text.slice(at, piece.start) + piece.render();
|
|
182
|
+
at = piece.end;
|
|
183
|
+
}
|
|
184
|
+
return out + text.slice(at, end);
|
|
185
|
+
};
|
|
186
|
+
return render(0, text.length);
|
|
187
|
+
}
|
package/dist/privacy.d.ts
CHANGED
|
@@ -44,6 +44,19 @@ export interface RedactionLimits {
|
|
|
44
44
|
*/
|
|
45
45
|
offload?: OffloadCollector;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A log record's body, placed by the upload step, cut as {@link redactLog} cuts one: each string
|
|
49
|
+
* over the cap cut to it and bytes over it replaced by the receiver's marker, and a structured
|
|
50
|
+
* body still over the cap replaced by the marker. Whether anything was cut, for `hue.truncated`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function capLogBody(body: unknown, valueBytes: number): {
|
|
53
|
+
body: unknown;
|
|
54
|
+
cut: boolean;
|
|
55
|
+
};
|
|
56
|
+
/** Whether a log record is a GenAI event, whose structured body carries messages. */
|
|
57
|
+
export declare function isGenAiLog(log: ReadableLogRecord): boolean;
|
|
58
|
+
/** Whether a log body is structured (an object or array), so a pointer can address inside it. */
|
|
59
|
+
export declare function isStructuredBody(body: unknown): boolean;
|
|
47
60
|
export type ResourceCache = WeakMap<Resource, Resource>;
|
|
48
61
|
export declare function redactSpan(span: ReadableSpan, options: HueOptions, cache: ResourceCache, limits?: RedactionLimits): ReadableSpan;
|
|
49
62
|
export declare function redactLog(log: ReadableLogRecord, options: HueOptions, cache: ResourceCache, limits?: RedactionLimits): ReadableLogRecord;
|
package/dist/privacy.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SpanStatusCode } from "@opentelemetry/api";
|
|
|
2
2
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
3
3
|
import { MAX_CONTENT_BYTES, MAX_RECORD_NODES, REDACTION_CONTEXT_UNITS } from "./config.js";
|
|
4
4
|
import { scrubToolCredentials, withToolCatalogSummary } from "./tool-definitions.js";
|
|
5
|
-
import { hashInlineFiles, mayInlineFiles } from "./inline-files.js";
|
|
5
|
+
import { hashInlineFiles, isMediaKey, mayInlineFiles } from "./inline-files.js";
|
|
6
6
|
import { truncateUtf8 } from "./safety.js";
|
|
7
7
|
export { truncateUtf8 };
|
|
8
8
|
/** Attribute keys (and their dotted children) removed in metadata-only mode. */
|
|
@@ -152,6 +152,10 @@ const NOT_HELD = Symbol("not held");
|
|
|
152
152
|
*/
|
|
153
153
|
function offloadValue(key, value, path, options, budget) {
|
|
154
154
|
const collector = budget.offload;
|
|
155
|
+
// A log record uploads only the media in its inspected attributes: anything else is redacted
|
|
156
|
+
// and cut as before.
|
|
157
|
+
if (collector.log && (typeof value !== "string" || !isMediaKey(key) || budget.cut?.has(value)))
|
|
158
|
+
return NOT_HELD;
|
|
155
159
|
if (value instanceof Uint8Array) {
|
|
156
160
|
if (value.byteLength <= budget.valueBytes)
|
|
157
161
|
return NOT_HELD;
|
|
@@ -178,6 +182,13 @@ function offloadValue(key, value, path, options, budget) {
|
|
|
178
182
|
if (++budget.nodes > MAX_RECORD_NODES)
|
|
179
183
|
throw new Error("Telemetry value exceeds the supported nesting limit");
|
|
180
184
|
const result = redactedText(scrubbed, path, options, budget.valueBytes);
|
|
185
|
+
if (collector.log && !mayInlineFiles(key, result)) {
|
|
186
|
+
if (Buffer.byteLength(result) <= budget.valueBytes)
|
|
187
|
+
return result;
|
|
188
|
+
if (!budget.truncated.includes(key))
|
|
189
|
+
budget.truncated.push(key);
|
|
190
|
+
return truncateUtf8(result, budget.valueBytes);
|
|
191
|
+
}
|
|
181
192
|
if (mayInlineFiles(key, result) ||
|
|
182
193
|
(result.length * 3 > budget.valueBytes && Buffer.byteLength(result) > budget.valueBytes))
|
|
183
194
|
collector.candidates.push({ key, value: result });
|
|
@@ -190,18 +201,19 @@ function offloadValue(key, value, path, options, budget) {
|
|
|
190
201
|
* is exported whole with the rest of its evidence, and a reader knows the one value is partial.
|
|
191
202
|
* Dropping the record for a large value lost every call, result and reply it recorded.
|
|
192
203
|
*/
|
|
193
|
-
function redactValue(value, path, options, budget, depth = 0, listAs = path) {
|
|
204
|
+
function redactValue(value, path, options, budget, depth = 0, listAs = path, whole = false) {
|
|
194
205
|
if (++budget.nodes > MAX_RECORD_NODES || depth > 32)
|
|
195
206
|
throw new Error("Telemetry value exceeds the supported nesting limit");
|
|
196
207
|
if (typeof value === "string") {
|
|
197
|
-
|
|
208
|
+
// `whole`: a log record's body left for the upload step, which cuts what it does not upload.
|
|
209
|
+
const cap = whole && budget.cut?.has(value) !== true ? Infinity : budget.valueBytes;
|
|
198
210
|
// The redactor sees the recorded text past the cap, so a secret that crosses the cap is still
|
|
199
211
|
// its to recognize; what it returns is cut to the cap afterwards. Text the queue cut when the
|
|
200
212
|
// record was admitted (far past the cap) reached the redactor without its continuation, so
|
|
201
213
|
// the end of the answer, which may hold the start of a secret it could not recognize, is
|
|
202
214
|
// never exported.
|
|
203
215
|
const cutWhenQueued = budget.cut?.has(value) === true;
|
|
204
|
-
let result = redactedText(value, path, options, cap);
|
|
216
|
+
let result = redactedText(value, path, options, Math.min(cap, budget.valueBytes));
|
|
205
217
|
if (cutWhenQueued && options.redact)
|
|
206
218
|
result = withoutTail(result, REDACTION_CONTEXT_UNITS);
|
|
207
219
|
const bytes = Buffer.byteLength(result);
|
|
@@ -214,7 +226,11 @@ function redactValue(value, path, options, budget, depth = 0, listAs = path) {
|
|
|
214
226
|
if (!budget.truncated.includes(listAs))
|
|
215
227
|
budget.truncated.push(listAs);
|
|
216
228
|
budget.bytes += cap;
|
|
217
|
-
|
|
229
|
+
const cut = truncateUtf8(result, cap);
|
|
230
|
+
// A log body's string the queue cut is no whole file: the upload step leaves it as cut.
|
|
231
|
+
if (whole && cutWhenQueued && budget.offload)
|
|
232
|
+
(budget.offload.bodyCut ??= new Set()).add(cut);
|
|
233
|
+
return cut;
|
|
218
234
|
}
|
|
219
235
|
// A value the queue replaced by the receiver's marker (a tool definition or recorded request
|
|
220
236
|
// too long to scrub when it was queued) is listed as cut.
|
|
@@ -226,7 +242,7 @@ function redactValue(value, path, options, budget, depth = 0, listAs = path) {
|
|
|
226
242
|
if (Array.isArray(value)) {
|
|
227
243
|
if (value.length > MAX_RECORD_NODES)
|
|
228
244
|
throw new Error("Telemetry array exceeds the complexity limit");
|
|
229
|
-
return value.map((item, index) => redactValue(item, `${path}.${index}`, options, budget, depth + 1, listAs));
|
|
245
|
+
return value.map((item, index) => redactValue(item, `${path}.${index}`, options, budget, depth + 1, listAs, whole));
|
|
230
246
|
}
|
|
231
247
|
if (value instanceof Uint8Array) {
|
|
232
248
|
if (value.byteLength <= budget.valueBytes) {
|
|
@@ -241,10 +257,51 @@ function redactValue(value, path, options, budget, depth = 0, listAs = path) {
|
|
|
241
257
|
if (value !== null && typeof value === "object")
|
|
242
258
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
243
259
|
key,
|
|
244
|
-
redactValue(item, `${path}.${key}`, options, budget, depth + 1, listAs),
|
|
260
|
+
redactValue(item, `${path}.${key}`, options, budget, depth + 1, listAs, whole),
|
|
245
261
|
]));
|
|
246
262
|
return value;
|
|
247
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* A log record's body, placed by the upload step, cut as {@link redactLog} cuts one: each string
|
|
266
|
+
* over the cap cut to it and bytes over it replaced by the receiver's marker, and a structured
|
|
267
|
+
* body still over the cap replaced by the marker. Whether anything was cut, for `hue.truncated`.
|
|
268
|
+
*/
|
|
269
|
+
export function capLogBody(body, valueBytes) {
|
|
270
|
+
let cut = false;
|
|
271
|
+
const visit = (value) => {
|
|
272
|
+
if (typeof value === "string") {
|
|
273
|
+
if (Buffer.byteLength(value) <= valueBytes)
|
|
274
|
+
return value;
|
|
275
|
+
cut = true;
|
|
276
|
+
return truncateUtf8(value, valueBytes);
|
|
277
|
+
}
|
|
278
|
+
if (value instanceof Uint8Array) {
|
|
279
|
+
if (value.byteLength <= valueBytes)
|
|
280
|
+
return value;
|
|
281
|
+
cut = true;
|
|
282
|
+
return truncatedMarker(value.byteLength);
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(value))
|
|
285
|
+
return value.map(visit);
|
|
286
|
+
if (value !== null && typeof value === "object")
|
|
287
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, visit(item)]));
|
|
288
|
+
return value;
|
|
289
|
+
};
|
|
290
|
+
const size = structuredBytes(body);
|
|
291
|
+
const result = visit(body);
|
|
292
|
+
if (result !== null && typeof result === "object" && structuredBytes(result) > valueBytes)
|
|
293
|
+
return { body: truncatedMarker(size), cut: true };
|
|
294
|
+
return { body: result, cut };
|
|
295
|
+
}
|
|
296
|
+
/** Whether a log record is a GenAI event, whose structured body carries messages. */
|
|
297
|
+
export function isGenAiLog(log) {
|
|
298
|
+
const name = log.eventName ?? log.attributes["event.name"];
|
|
299
|
+
return typeof name === "string" && name.startsWith("gen_ai.");
|
|
300
|
+
}
|
|
301
|
+
/** Whether a log body is structured (an object or array), so a pointer can address inside it. */
|
|
302
|
+
export function isStructuredBody(body) {
|
|
303
|
+
return body !== null && typeof body === "object" && !(body instanceof Uint8Array);
|
|
304
|
+
}
|
|
248
305
|
/**
|
|
249
306
|
* A record's attributes through the redactor. `listPrefix` is how a cut value of these
|
|
250
307
|
* attributes is listed under the record's `hue.truncated`: the key alone for the record's own
|
|
@@ -265,7 +322,7 @@ function attributes(source, options, path, budget, listPrefix = "", own = false)
|
|
|
265
322
|
return [
|
|
266
323
|
[
|
|
267
324
|
key,
|
|
268
|
-
redactValue(scrubToolCredentials(key, own ? hashInlineFiles(key, value) : value), `${path}.${key}`, options, budget, 0, `${listPrefix}${key}`),
|
|
325
|
+
redactValue(scrubToolCredentials(key, own === true ? hashInlineFiles(key, value) : value), `${path}.${key}`, options, budget, 0, `${listPrefix}${key}`),
|
|
269
326
|
],
|
|
270
327
|
];
|
|
271
328
|
}));
|
|
@@ -371,17 +428,27 @@ export function redactSpan(span, options, cache, limits = {}) {
|
|
|
371
428
|
}
|
|
372
429
|
export function redactLog(log, options, cache, limits = {}) {
|
|
373
430
|
const budget = budgetOf(limits);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
431
|
+
const offload = limits.offload;
|
|
432
|
+
let body;
|
|
433
|
+
if (offload && options.captureContent && isGenAiLog(log) && isStructuredBody(log.body)) {
|
|
434
|
+
// Left whole for the upload step, which uploads its media and cuts what is left over the cap.
|
|
435
|
+
body = redactValue(log.body, "body", options, budget, 0, "body", true);
|
|
436
|
+
offload.body = { value: body };
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
body = options.captureContent ? redactValue(log.body, "body", options, budget) : undefined;
|
|
440
|
+
if (body !== undefined && typeof body !== "string") {
|
|
441
|
+
// A structured body over the cap is replaced by the receiver's marker, as the receiver
|
|
442
|
+
// would replace it; the marker names the body's size as recorded, before any text part
|
|
443
|
+
// was cut.
|
|
444
|
+
if (Buffer.byteLength(JSON.stringify(body)) > budget.valueBytes) {
|
|
445
|
+
body = truncatedMarker(structuredBytes(log.body));
|
|
446
|
+
if (!budget.truncated.includes("body"))
|
|
447
|
+
budget.truncated.push("body");
|
|
448
|
+
}
|
|
382
449
|
}
|
|
383
450
|
}
|
|
384
|
-
const redactedAttributes = attributes(log.attributes, options, "attributes", budget);
|
|
451
|
+
const redactedAttributes = attributes(log.attributes, options, "attributes", budget, "", offload ? "log" : false);
|
|
385
452
|
const resource = redactResource(log.resource, options, cache, budget);
|
|
386
453
|
const scopeAttributes = log.instrumentationScope.attributes
|
|
387
454
|
? attributes(log.instrumentationScope.attributes, options, "scope.attributes", budget, "scope.")
|
package/dist/safety.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Span } from "@opentelemetry/api";
|
|
2
|
+
import { Attachment } from "./attachment.js";
|
|
2
3
|
export declare function noopSpan(): Span;
|
|
3
4
|
export declare function safeSpan(source: Span, failed: () => void): Span;
|
|
4
5
|
export declare function spanSource(span: Span): Span;
|
|
@@ -10,8 +11,11 @@ export interface EncodeLimits {
|
|
|
10
11
|
}
|
|
11
12
|
/** One content attribute's bounds under a receiver's value cap of `valueBytes`. */
|
|
12
13
|
export declare function contentLimitsOf(valueBytes: number): EncodeLimits;
|
|
14
|
+
/** Writes an {@link Attachment} met at `pointer`, `depth` levels deep: the JSON value written in
|
|
15
|
+
* its place. Without one, an attachment is no JSON data. */
|
|
16
|
+
export type PlaceAttachment = (attachment: Attachment, pointer: string, depth: number) => unknown;
|
|
13
17
|
/** Validate a bounded data tree without invoking toJSON or property getters. */
|
|
14
|
-
export declare function encodeContent(value: unknown, limits?: EncodeLimits): string;
|
|
18
|
+
export declare function encodeContent(value: unknown, limits?: EncodeLimits, place?: PlaceAttachment): string;
|
|
15
19
|
/**
|
|
16
20
|
* The value as JSON text within `limits.bytes`, and whether it was cut to fit. A value over the
|
|
17
21
|
* cap is cut to a UTF-8 prefix of its JSON text at the cap, as Hue's receiver cuts a value over
|
|
@@ -22,7 +26,7 @@ export declare function encodeContent(value: unknown, limits?: EncodeLimits): st
|
|
|
22
26
|
* among its members is never a shorter document that reads whole: the prefix ends at the cap,
|
|
23
27
|
* inside whatever member the cap fell in.
|
|
24
28
|
*/
|
|
25
|
-
export declare function encodeBoundedContent(value: unknown, limits?: EncodeLimits, cut?: boolean): {
|
|
29
|
+
export declare function encodeBoundedContent(value: unknown, limits?: EncodeLimits, cut?: boolean, place?: PlaceAttachment): {
|
|
26
30
|
text: string;
|
|
27
31
|
truncated: boolean;
|
|
28
32
|
};
|
package/dist/safety.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { INVALID_SPAN_CONTEXT, trace } from "@opentelemetry/api";
|
|
2
2
|
import { types as utilTypes } from "node:util";
|
|
3
|
+
import { Attachment, pointerSegment } from "./attachment.js";
|
|
3
4
|
import { MAX_CONTENT_BYTES, MAX_RECORD_NODES } from "./config.js";
|
|
4
5
|
export function noopSpan() {
|
|
5
6
|
return trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
|
|
@@ -114,8 +115,8 @@ export function contentLimitsOf(valueBytes) {
|
|
|
114
115
|
return { ...contentLimits, bytes: valueBytes };
|
|
115
116
|
}
|
|
116
117
|
/** Validate a bounded data tree without invoking toJSON or property getters. */
|
|
117
|
-
export function encodeContent(value, limits = contentLimits) {
|
|
118
|
-
const { text, truncated } = encodeBoundedContent(value, limits, false);
|
|
118
|
+
export function encodeContent(value, limits = contentLimits, place) {
|
|
119
|
+
const { text, truncated } = encodeBoundedContent(value, limits, false, place);
|
|
119
120
|
if (truncated)
|
|
120
121
|
throw new RangeError("Content limit exceeded");
|
|
121
122
|
return text;
|
|
@@ -130,12 +131,14 @@ export function encodeContent(value, limits = contentLimits) {
|
|
|
130
131
|
* among its members is never a shorter document that reads whole: the prefix ends at the cap,
|
|
131
132
|
* inside whatever member the cap fell in.
|
|
132
133
|
*/
|
|
133
|
-
export function encodeBoundedContent(value, limits = contentLimits, cut = true) {
|
|
134
|
+
export function encodeBoundedContent(value, limits = contentLimits, cut = true, place) {
|
|
134
135
|
let nodes = 0;
|
|
135
136
|
let bytes = 0;
|
|
136
137
|
let truncated = false;
|
|
137
138
|
const parts = [];
|
|
138
139
|
const ancestors = new Set();
|
|
140
|
+
// The keys from the root to the value being written, kept only to place attachments.
|
|
141
|
+
const path = [];
|
|
139
142
|
const write = (text) => {
|
|
140
143
|
parts.push(text);
|
|
141
144
|
bytes += Buffer.byteLength(text);
|
|
@@ -172,6 +175,11 @@ export function encodeBoundedContent(value, limits = contentLimits, cut = true)
|
|
|
172
175
|
// The native check also handles revoked proxies without invoking their traps.
|
|
173
176
|
if (utilTypes.isProxy(item))
|
|
174
177
|
throw new TypeError("JSON proxies are unsupported");
|
|
178
|
+
if (place && item instanceof Attachment) {
|
|
179
|
+
const pointer = path.length ? `/${path.map(pointerSegment).join("/")}` : "";
|
|
180
|
+
write(JSON.stringify(place(item, pointer, depth) ?? null));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
175
183
|
const array = Array.isArray(item);
|
|
176
184
|
if (!array && ![Object.prototype, null].includes(Object.getPrototypeOf(item)))
|
|
177
185
|
throw new TypeError("Expected JSON data");
|
|
@@ -193,7 +201,11 @@ export function encodeBoundedContent(value, limits = contentLimits, cut = true)
|
|
|
193
201
|
throw new TypeError("Expected JSON data property");
|
|
194
202
|
write(`${first ? "" : ","}${array ? "" : `${JSON.stringify(key)}:`}`);
|
|
195
203
|
first = false;
|
|
204
|
+
if (place)
|
|
205
|
+
path.push(key);
|
|
196
206
|
visit(descriptor.value, depth + 1);
|
|
207
|
+
if (place)
|
|
208
|
+
path.pop();
|
|
197
209
|
}
|
|
198
210
|
write(array ? "]" : "}");
|
|
199
211
|
ancestors.delete(item);
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -34,4 +34,4 @@ export interface RecordSnapshot<T> {
|
|
|
34
34
|
inspected: ReadonlySet<string>;
|
|
35
35
|
}
|
|
36
36
|
export declare function snapshotSpan(source: ReadableSpan, limit: number, captureContent?: boolean, valueUnits?: number, hold?: HoldLimits, memo?: MediaDigestMemo): RecordSnapshot<ReadableSpan>;
|
|
37
|
-
export declare function snapshotLog(source: ReadableLogRecord, limit: number, captureContent?: boolean, valueUnits?: number): RecordSnapshot<ReadWriteLogRecord>;
|
|
37
|
+
export declare function snapshotLog(source: ReadableLogRecord, limit: number, captureContent?: boolean, valueUnits?: number, hold?: HoldLimits, memo?: MediaDigestMemo): RecordSnapshot<ReadWriteLogRecord>;
|