@cygnuxlabs/tracekit 0.0.0-stage → 1.2.2
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/README.md +294 -2
- package/bin/tracekit.mjs +30 -0
- package/dist/v1.d.ts +97 -0
- package/dist/v1.js +535 -0
- package/dist/v2/adapters/claude-agent-sdk.d.ts +45 -0
- package/dist/v2/adapters/claude-agent-sdk.js +142 -0
- package/dist/v2/adapters/gate.d.ts +62 -0
- package/dist/v2/adapters/gate.js +195 -0
- package/dist/v2/adapters/openai-agents.d.ts +85 -0
- package/dist/v2/adapters/openai-agents.js +114 -0
- package/dist/v2/adapters/vercel-ai.d.ts +51 -0
- package/dist/v2/adapters/vercel-ai.js +132 -0
- package/dist/v2/client.d.ts +159 -0
- package/dist/v2/client.js +749 -0
- package/dist/v2/executor.d.ts +49 -0
- package/dist/v2/executor.js +57 -0
- package/dist/v2/jcs.d.ts +5 -0
- package/dist/v2/jcs.js +12 -0
- package/dist/v2/launcher.d.ts +10 -0
- package/dist/v2/launcher.js +50 -0
- package/dist/v2/rpc_schema.d.ts +3 -0
- package/dist/v2/rpc_schema.js +43 -0
- package/dist/v2/validate.d.ts +6 -0
- package/dist/v2/validate.js +70 -0
- package/dist/verify/cli.d.ts +1 -0
- package/dist/verify/cli.js +46 -0
- package/dist/verify/detect.d.ts +41 -0
- package/dist/verify/detect.js +289 -0
- package/dist/verify/index.d.ts +34 -0
- package/dist/verify/index.js +930 -0
- package/dist/verify/jcs.d.ts +19 -0
- package/dist/verify/jcs.js +154 -0
- package/dist/verify/primitives.d.ts +62 -0
- package/dist/verify/primitives.js +298 -0
- package/dist/verify/schema.d.ts +11 -0
- package/dist/verify/schema.js +98 -0
- package/dist/verify/tracekit.event.v2.json +4588 -0
- package/dist/verify/zip.d.ts +17 -0
- package/dist/verify/zip.js +145 -0
- package/package.json +42 -4
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
const RUN_CAP = 100, TENANT_CAP = 60, RUN_SHARE = 20, IDLE_S = 3600, UPDATE_S = 60;
|
|
2
|
+
const NOT_COUNTED = ["TK-ARGS-INVALID", "TK-OVERSIZE", "TK-QUARANTINE"];
|
|
3
|
+
const SECTIONS = ["deny", "ask", "flag"];
|
|
4
|
+
const cut = (s, n) => [...s].slice(0, n).join("");
|
|
5
|
+
/** Seconds since the epoch of a record's `ts`, as Python's datetime.timestamp() computes it. */
|
|
6
|
+
function ts(t) {
|
|
7
|
+
const m = /^(.{19})(?:\.(\d{1,6}))?Z$/.exec(t);
|
|
8
|
+
if (!m)
|
|
9
|
+
throw new TypeError(`not a timestamp: ${t}`);
|
|
10
|
+
return (Date.parse(m[1] + "Z") / 1000 * 1e6 + Number((m[2] ?? "").padEnd(6, "0"))) / 1e6;
|
|
11
|
+
}
|
|
12
|
+
/** fnmatch.fnmatchcase's pattern as a RegExp: `*`, `?` and `[...]` (`[!...]` negated). */
|
|
13
|
+
function glob(pat) {
|
|
14
|
+
let out = "";
|
|
15
|
+
for (let i = 0; i < pat.length; i++) {
|
|
16
|
+
const c = pat[i], end = c === "[" ? pat.indexOf("]", i + 2) : -1;
|
|
17
|
+
if (c === "*")
|
|
18
|
+
out += "[\\s\\S]*";
|
|
19
|
+
else if (c === "?")
|
|
20
|
+
out += "[\\s\\S]";
|
|
21
|
+
else if (end > 0) {
|
|
22
|
+
const body = pat.slice(i + 1, end);
|
|
23
|
+
out += "[" + (body[0] === "!" ? "^" + body.slice(1) : body).replace(/\\/g, "\\\\") + "]";
|
|
24
|
+
i = end;
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
out += c.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
28
|
+
}
|
|
29
|
+
return new RegExp(`^${out}$`, "u");
|
|
30
|
+
}
|
|
31
|
+
export class Detector {
|
|
32
|
+
label = new Map();
|
|
33
|
+
untrusted = new Set();
|
|
34
|
+
deny = new Set();
|
|
35
|
+
sequences;
|
|
36
|
+
limits;
|
|
37
|
+
respond = new Map();
|
|
38
|
+
every = new Map();
|
|
39
|
+
idle;
|
|
40
|
+
tools;
|
|
41
|
+
constructor(policy) {
|
|
42
|
+
for (const sec of SECTIONS)
|
|
43
|
+
for (const r of policy[sec] ?? []) {
|
|
44
|
+
if (Object.hasOwn(r, "label"))
|
|
45
|
+
this.label.set(r.id, r.label);
|
|
46
|
+
if (Object.hasOwn(r, "from"))
|
|
47
|
+
this.untrusted.add(r.id);
|
|
48
|
+
}
|
|
49
|
+
for (const r of policy.deny ?? [])
|
|
50
|
+
this.deny.add(r.id);
|
|
51
|
+
if (policy.unknown_tools === "deny")
|
|
52
|
+
this.deny.add("TK-UNKNOWN-TOOL");
|
|
53
|
+
this.sequences = policy.sequences ?? [];
|
|
54
|
+
this.limits = policy.limits ?? [];
|
|
55
|
+
for (const r of policy.respond ?? [])
|
|
56
|
+
this.respond.set(r.trigger, r);
|
|
57
|
+
for (const x of [...this.sequences, ...this.limits])
|
|
58
|
+
this.every.set(x.id, x.window);
|
|
59
|
+
if (this.respond.has("denied_calls"))
|
|
60
|
+
this.every.set("denied_calls", this.respond.get("denied_calls").window);
|
|
61
|
+
this.idle = Math.max(IDLE_S, 0, ...this.every.values());
|
|
62
|
+
this.tools = policy.tools ?? {};
|
|
63
|
+
}
|
|
64
|
+
toolClass(tool) {
|
|
65
|
+
if (Object.hasOwn(this.tools, tool))
|
|
66
|
+
return this.tools[tool];
|
|
67
|
+
const hits = Object.keys(this.tools).filter((p) => glob(p).test(tool))
|
|
68
|
+
.map((p) => [-p.replace(/[*?]/g, "").length, p])
|
|
69
|
+
.sort((a, b) => a[0] - b[0] || (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
|
|
70
|
+
return hits.length ? this.tools[hits[0][1]] : "unknown";
|
|
71
|
+
}
|
|
72
|
+
names(ids) {
|
|
73
|
+
return new Set([...ids, ...ids.filter((i) => this.label.has(i)).map((i) => this.label.get(i)), "*"]);
|
|
74
|
+
}
|
|
75
|
+
on(trigger) {
|
|
76
|
+
return (this.respond.get(trigger)?.mode ?? "off") !== "off";
|
|
77
|
+
}
|
|
78
|
+
static state() {
|
|
79
|
+
return { runs: new Map(), scopes: new Map(), minute: new Map() };
|
|
80
|
+
}
|
|
81
|
+
scope(det, e, run, kind) {
|
|
82
|
+
if (kind === "principal" && !(run && run.principal))
|
|
83
|
+
kind = "owner";
|
|
84
|
+
if (!run)
|
|
85
|
+
kind = "run";
|
|
86
|
+
const val = cut(kind === "run" ? e.run_id : run[kind], 256), key = `${kind}\0${e.tenant}\0${val}`;
|
|
87
|
+
const old = det.scopes.get(key);
|
|
88
|
+
if (!old || ts(e.ts) - ts(old.t) > this.idle)
|
|
89
|
+
det.scopes.set(key, { seq: new Map(), lim: new Map(), den: null, open: new Map(), t: e.ts });
|
|
90
|
+
const st = det.scopes.get(key);
|
|
91
|
+
st.t = e.ts;
|
|
92
|
+
return [key, { kind, id: val }, st];
|
|
93
|
+
}
|
|
94
|
+
/** The incident records (type, data without ids) record `e` fires, after indexing it into `det`. */
|
|
95
|
+
observe(det, e) {
|
|
96
|
+
const typ = e.type, d = e.data, rk = `${e.tenant}\0${e.run_id}`, t = ts(e.ts), fired = [];
|
|
97
|
+
const run = det.runs.get(rk) ?? null;
|
|
98
|
+
if (typ === "run.registered") {
|
|
99
|
+
det.runs.set(rk, { session: d.session ?? e.run_id, owner: `${d.identity.scheme}:${d.identity.subject}`,
|
|
100
|
+
principal: e.principal_attested ? e.principal ?? null : null, pending: new Map(), n: new Map(),
|
|
101
|
+
capped: new Map(), minute: [0, 0, false] });
|
|
102
|
+
}
|
|
103
|
+
else if (typ === "run.final") {
|
|
104
|
+
det.runs.delete(rk);
|
|
105
|
+
det.scopes.delete("run\0" + rk);
|
|
106
|
+
}
|
|
107
|
+
else if (typ === "incident.opened" || typ === "incident.capped")
|
|
108
|
+
this.countIncident(det, e, run, t);
|
|
109
|
+
else if (typ === "policy.decision") {
|
|
110
|
+
const inp = d.detect ?? {}, ids = inp.rule_ids ?? d.rule_ids, cite = [e.run_id, e.run_seq];
|
|
111
|
+
const names = this.names(ids), unknown = this.toolClass(d.tool) === "unknown";
|
|
112
|
+
if (run) {
|
|
113
|
+
const k = `call\0${e.tool_call_id}`;
|
|
114
|
+
if (d.decision === "ask")
|
|
115
|
+
run.pending.set(k, [d.decision_id ?? null, [...names], unknown, inp.sums ?? {},
|
|
116
|
+
Object.hasOwn(inp, "rule_ids")]);
|
|
117
|
+
else
|
|
118
|
+
run.pending.delete(k);
|
|
119
|
+
}
|
|
120
|
+
if (d.decision === "allow" || d.decision === "flag") // detect.rule_ids: a client's class hint read it too
|
|
121
|
+
this.count(det, e, run, names, unknown, inp.sums ?? {}, Object.hasOwn(inp, "rule_ids"), cite, fired);
|
|
122
|
+
else if (d.decision === "deny")
|
|
123
|
+
this.denied(det, e, run, t, cite, fired);
|
|
124
|
+
if (d.decision !== "allow" && this.on("provenance") && d.rule_ids.some((i) => this.untrusted.has(i))
|
|
125
|
+
&& (Array.isArray(d.provenance) ? d.provenance.length : d.provenance) && d.provenance_state === "complete")
|
|
126
|
+
fired.push(["provenance", d.rule_ids, ...this.scope(det, e, run, "run").slice(0, 2), [cite]]);
|
|
127
|
+
}
|
|
128
|
+
else if (typ === "approval.request" && run) {
|
|
129
|
+
const k = `call\0${e.tool_call_id ?? null}`, p = run.pending.get(k);
|
|
130
|
+
if (p && p[0] === d.decision_id) {
|
|
131
|
+
run.pending.delete(k);
|
|
132
|
+
run.pending.set(d.approval_id, p.slice(1));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (["approval.consumed", "approval.abandoned", "approval.expired"].includes(typ) && run) {
|
|
136
|
+
const p = run.pending.get(d.approval_id);
|
|
137
|
+
if (p) {
|
|
138
|
+
run.pending.delete(d.approval_id);
|
|
139
|
+
if (typ === "approval.consumed")
|
|
140
|
+
this.count(det, e, run, new Set(p[0]), p[1], p[2], Boolean(p[3]), [e.run_id, e.run_seq], fired);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
let trigger = { "trace.tamper": "tamper", "refusal.summary": "refusal_burst" }[typ];
|
|
145
|
+
if (typ === "capture.gap" && ["executed_against_policy", "class_mismatch"].includes(d.kind))
|
|
146
|
+
trigger = d.kind;
|
|
147
|
+
if (trigger && this.on(trigger) && (trigger !== "refusal_burst" || d.count > this.respond.get(trigger).max))
|
|
148
|
+
fired.push([trigger, [], ...this.scope(det, e, run, "run").slice(0, 2), [[e.run_id, e.run_seq]]]);
|
|
149
|
+
}
|
|
150
|
+
return this.incidents(det, e, run, t, fired);
|
|
151
|
+
}
|
|
152
|
+
/** As detect.py's _count: `unless` exempts only a call the policy's own reading names, never one a hint added. */
|
|
153
|
+
count(det, e, run, names, unknown, sums, hinted, cite, fired) {
|
|
154
|
+
const own = unknown ? new Set() : new Set([...names].filter((n) => n !== "*"));
|
|
155
|
+
const at = ts(e.ts);
|
|
156
|
+
for (const s of this.sequences) {
|
|
157
|
+
if (!unknown && !hinted && (s.unless ?? []).some((n) => names.has(n)))
|
|
158
|
+
continue;
|
|
159
|
+
const steps = s.steps, last = steps.length - 1;
|
|
160
|
+
if (!(unknown || steps.some((n) => names.has(n))))
|
|
161
|
+
continue;
|
|
162
|
+
const [key, scope, st] = this.scope(det, e, run, s.scope ?? "run");
|
|
163
|
+
const old = st.seq.get(s.id) ?? Array(last).fill(null), next = [...old];
|
|
164
|
+
let done = null;
|
|
165
|
+
for (let j = 0; j <= last; j++) {
|
|
166
|
+
if (!(unknown || names.has(steps[j])))
|
|
167
|
+
continue;
|
|
168
|
+
const prev = j ? old[j - 1] : null;
|
|
169
|
+
if (j && (prev === null || at - ts(prev[0]) > s.window))
|
|
170
|
+
continue;
|
|
171
|
+
const [start, cites] = j ? [prev[0], [...prev[1], cite]] : [e.ts, [cite]];
|
|
172
|
+
const used = new Set();
|
|
173
|
+
for (let k = j;; k++) {
|
|
174
|
+
if (k === last) {
|
|
175
|
+
if (done === null || ts(start) >= ts(done[0]))
|
|
176
|
+
done = [start, cites];
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (next[k] === null || ts(start) >= ts(next[k][0]))
|
|
180
|
+
next[k] = [start, cites];
|
|
181
|
+
used.add(steps[k]);
|
|
182
|
+
if (!own.has(steps[k]) || !own.has(steps[k + 1]) || used.has(steps[k + 1]))
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
st.seq.set(s.id, next);
|
|
187
|
+
if (done)
|
|
188
|
+
fired.push(["sequence", [s.id], key, scope, done[1]]);
|
|
189
|
+
}
|
|
190
|
+
for (const lim of this.limits) {
|
|
191
|
+
if (!(unknown || names.has(lim.match)) || Object.hasOwn(lim, "sum") && !Object.hasOwn(sums, lim.id))
|
|
192
|
+
continue;
|
|
193
|
+
const [key, scope, st] = this.scope(det, e, run, lim.scope ?? "run");
|
|
194
|
+
let [start, val] = st.lim.get(lim.id) ?? [null, null], before, after;
|
|
195
|
+
if (start === null || at - ts(start) > lim.window)
|
|
196
|
+
[start, val] = [e.ts, Object.hasOwn(lim, "sum") ? {} : 0];
|
|
197
|
+
if (Object.hasOwn(lim, "sum")) {
|
|
198
|
+
const [amount, cur] = sums[lim.id];
|
|
199
|
+
before = val[cur] ?? 0;
|
|
200
|
+
val = { ...val, [cur]: Math.min(before + amount, Number.MAX_SAFE_INTEGER) }; // saturated, as detect.py
|
|
201
|
+
after = val[cur];
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
[before, val, after] = [val, val + 1, val + 1];
|
|
205
|
+
st.lim.set(lim.id, [start, val]);
|
|
206
|
+
if (before <= lim.max && lim.max < after)
|
|
207
|
+
fired.push(["limit", [lim.id], key, scope, [cite]]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
denied(det, e, run, t, cite, fired) {
|
|
211
|
+
const r = this.respond.get("denied_calls"), d = e.data, ids = d.rule_ids;
|
|
212
|
+
if (!this.on("denied_calls") || d.nondeterministic
|
|
213
|
+
|| ids.some((i) => NOT_COUNTED.includes(i)) && !ids.some((i) => this.deny.has(i)))
|
|
214
|
+
return;
|
|
215
|
+
const [key, scope, st] = this.scope(det, e, run, r.scope ?? "run");
|
|
216
|
+
if (st.den === null || t - ts(st.den[0]) > r.window)
|
|
217
|
+
st.den = [e.ts, new Set()];
|
|
218
|
+
const seen = st.den[1], call = `${e.tool_call_id}\0${e.attempt ?? 0}`;
|
|
219
|
+
if (seen.has(call) || seen.size > r.max)
|
|
220
|
+
return;
|
|
221
|
+
seen.add(call);
|
|
222
|
+
if (seen.size > r.max)
|
|
223
|
+
fired.push(["denied_calls", ids, key, scope, [cite]]);
|
|
224
|
+
}
|
|
225
|
+
incidents(det, e, run, t, fired) {
|
|
226
|
+
if (!fired.length)
|
|
227
|
+
return [];
|
|
228
|
+
const out = [], minute = Math.floor(t / 60), m = det.minute.get(e.tenant);
|
|
229
|
+
let nTen = m && m[0] === minute ? m[1] : 0;
|
|
230
|
+
const nRun = new Map(run ? run.n : []), capped = new Map(run ? run.capped : []);
|
|
231
|
+
let [nMine, shared] = run && run.minute[0] === minute ? [run.minute[1], run.minute[2]] : [0, false];
|
|
232
|
+
for (const [trigger, ruleIds, key, scope, cites] of fired) {
|
|
233
|
+
const rule = ["sequence", "limit"].includes(trigger) ? ruleIds[0] : trigger;
|
|
234
|
+
const r = this.respond.get(rule) ?? { mode: "shadow" };
|
|
235
|
+
if (r.mode === "off")
|
|
236
|
+
continue;
|
|
237
|
+
const st = det.scopes.get(key), k = `${trigger}\0${rule}`, cited = cites.map(([a, b]) => ({ run_id: a, run_seq: b }));
|
|
238
|
+
if (st.open.has(k)) {
|
|
239
|
+
const [inc, last] = st.open.get(k);
|
|
240
|
+
if (t - ts(last) >= (this.every.get(rule) ?? UPDATE_S)) {
|
|
241
|
+
st.open.set(k, [inc, e.ts]);
|
|
242
|
+
out.push(["incident.updated", { incident_id: inc, cites: cited }]);
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (run && (nRun.get(trigger) ?? 0) >= RUN_CAP) {
|
|
247
|
+
if (capped.get(trigger))
|
|
248
|
+
continue;
|
|
249
|
+
capped.set(trigger, true);
|
|
250
|
+
out.push(["incident.capped", { trigger }]);
|
|
251
|
+
}
|
|
252
|
+
else if (nTen >= TENANT_CAP || nMine >= RUN_SHARE) {
|
|
253
|
+
if (shared)
|
|
254
|
+
continue;
|
|
255
|
+
shared = true;
|
|
256
|
+
out.push(["incident.capped", { trigger }]);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
nRun.set(trigger, (nRun.get(trigger) ?? 0) + 1);
|
|
260
|
+
nTen++;
|
|
261
|
+
nMine++;
|
|
262
|
+
out.push(["incident.opened", { trigger, rule_ids: ruleIds, scope, cites: cited, mode: r.mode,
|
|
263
|
+
would: r.action ? [r.action] : [] }]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
countIncident(det, e, run, t) {
|
|
269
|
+
const d = e.data, minute = Math.floor(t / 60);
|
|
270
|
+
if (e.type === "incident.capped") {
|
|
271
|
+
if (run && d.cap === "run")
|
|
272
|
+
run.capped.set(d.trigger, true);
|
|
273
|
+
else if (run)
|
|
274
|
+
run.minute = [minute, run.minute[0] === minute ? run.minute[1] : 0, true];
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const m = det.minute.get(e.tenant);
|
|
278
|
+
det.minute.set(e.tenant, [minute, (m && m[0] === minute ? m[1] : 0) + 1]);
|
|
279
|
+
if (run) {
|
|
280
|
+
run.n.set(d.trigger, (run.n.get(d.trigger) ?? 0) + 1);
|
|
281
|
+
const mine = run.minute;
|
|
282
|
+
run.minute = mine[0] === minute ? [minute, mine[1] + 1, mine[2]] : [minute, 1, false];
|
|
283
|
+
}
|
|
284
|
+
const st = det.scopes.get(`${d.scope.kind}\0${e.tenant}\0${d.scope.id}`);
|
|
285
|
+
const rule = ["sequence", "limit"].includes(d.trigger) ? d.rule_ids[0] : d.trigger;
|
|
286
|
+
if (st)
|
|
287
|
+
st.open.set(`${d.trigger}\0${rule}`, [d.incident_id, e.ts]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { MAX_ENTRIES, MAX_ENTRY, MAX_TOTAL, Unusable, readZip } from "./zip.js";
|
|
2
|
+
export { StrictJSONError, canonicalize, strictParse, strictParseBytes } from "./jcs.js";
|
|
3
|
+
/** The bundle format version this verifier implements: a bundle needing a later one is UNVERIFIABLE. */
|
|
4
|
+
export declare const VERSION = "1.2.2";
|
|
5
|
+
export declare const EXIT_OK = 0, EXIT_FAIL = 1, EXIT_BAD = 2, EXIT_WARN = 3;
|
|
6
|
+
export type Check = {
|
|
7
|
+
check: string;
|
|
8
|
+
status: "pass" | "warn" | "fail";
|
|
9
|
+
detail: string;
|
|
10
|
+
problems: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare class Report {
|
|
13
|
+
checks: Check[];
|
|
14
|
+
failures: string[];
|
|
15
|
+
warnings: string[];
|
|
16
|
+
notes: string[];
|
|
17
|
+
integrity: string;
|
|
18
|
+
assurance: string;
|
|
19
|
+
/** Checks the bundle needs that this verifier does not implement. */
|
|
20
|
+
unsupported: string[];
|
|
21
|
+
check(check: string, ok: boolean, detail?: string, problems?: string[], warn?: boolean): void;
|
|
22
|
+
unsupport(what: string): void;
|
|
23
|
+
}
|
|
24
|
+
type J = any;
|
|
25
|
+
/** Verify a v2 bundle (zip bytes) against the verifier's own pinned trust config (JSON bytes). Never throws. */
|
|
26
|
+
export declare function verify(bundle: Uint8Array, trust: Uint8Array): Promise<{
|
|
27
|
+
report: Report;
|
|
28
|
+
code: number;
|
|
29
|
+
}>;
|
|
30
|
+
/** §4: null when `r` is a v2 record signed by one of `keys` (SPKI DER) with an allowed algorithm that is the key's own,
|
|
31
|
+
* else why not. */
|
|
32
|
+
export declare function verifyRecord(r: J, keys: Uint8Array[], algs: string[]): Promise<string | null>;
|
|
33
|
+
/** The report as text, as the reference CLI prints it; control characters in bundle-derived strings are dropped. */
|
|
34
|
+
export declare function formatReport(rep: Report): string;
|