metergraph-cli 0.0.0-stage → 0.2.0-preview.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/LICENSE +201 -0
- package/README.md +876 -2
- package/assets/skill/SKILL.md +73 -0
- package/assets/skill/manifest.json +9 -0
- package/bin/metergraph.js +11 -0
- package/package.json +44 -4
- package/src/args.js +486 -0
- package/src/auth-binding.js +202 -0
- package/src/auth-browser.js +74 -0
- package/src/auth-callback.js +177 -0
- package/src/auth-login.js +410 -0
- package/src/auth-oauth.js +462 -0
- package/src/auth-session.js +174 -0
- package/src/auth-store.js +397 -0
- package/src/cli.js +166 -0
- package/src/constants.js +176 -0
- package/src/deployment-credential.js +207 -0
- package/src/deployment-route.js +449 -0
- package/src/doctor.js +208 -0
- package/src/http.js +129 -0
- package/src/origin.js +37 -0
- package/src/output.js +726 -0
- package/src/read-contract.js +601 -0
- package/src/read-output.js +216 -0
- package/src/read.js +325 -0
- package/src/setup-deployment.js +178 -0
- package/src/setup-env-acl.js +99 -0
- package/src/setup-env-git.js +103 -0
- package/src/setup-env-parse.js +169 -0
- package/src/setup-env.js +685 -0
- package/src/setup-state.js +141 -0
- package/src/setup.js +317 -0
- package/src/skill-bundle.js +53 -0
- package/src/skill.js +449 -0
- package/src/trace-contract.js +129 -0
- package/src/trace-open.js +36 -0
- package/src/transport.js +171 -0
- package/src/verify-output.js +36 -0
- package/src/verify.js +120 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { normalizeUuid } from "./auth-oauth.js";
|
|
2
|
+
import { AGENT_CONTRACT_VERSION, MAX_CURSOR_LENGTH, METADATA_SCOPE } from "./constants.js";
|
|
3
|
+
|
|
4
|
+
// Validators for the service's Metadata read documents. Each one checks the
|
|
5
|
+
// document against the agent access contract, the bound workspace and the
|
|
6
|
+
// bound deployment profile, and returns a new object built only from fields it
|
|
7
|
+
// validated. Nothing from a document is copied by reflection: unknown fields,
|
|
8
|
+
// including any that would carry captured content, are never returned, and
|
|
9
|
+
// their names are never reported. Results are
|
|
10
|
+
// { ok: true, value } or { ok: false, outcome, reason }
|
|
11
|
+
// with fixed tokens. notices is a Set that collects fixed notice tokens.
|
|
12
|
+
|
|
13
|
+
// Every capability name the contract defines, in its order. The last three
|
|
14
|
+
// read content or replay and are never available to a Metadata grant.
|
|
15
|
+
export const CAPABILITY_NAMES = Object.freeze([
|
|
16
|
+
"workspace_context",
|
|
17
|
+
"capability_discovery",
|
|
18
|
+
"routes",
|
|
19
|
+
"usage",
|
|
20
|
+
"ingestion_health",
|
|
21
|
+
"incidents",
|
|
22
|
+
"trace_metadata",
|
|
23
|
+
"reports",
|
|
24
|
+
"report_detail",
|
|
25
|
+
"report_evidence",
|
|
26
|
+
"trace_content",
|
|
27
|
+
"trace_replay",
|
|
28
|
+
]);
|
|
29
|
+
const PRIVACY_CLASSES = Object.freeze(["metadata", "content", "replay"]);
|
|
30
|
+
const SCOPES = Object.freeze(["agent:metadata", "agent:read", "agent:replay"]);
|
|
31
|
+
|
|
32
|
+
const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/;
|
|
33
|
+
const IDENT = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
34
|
+
const HASH = /^[A-Za-z0-9:._+/=-]{1,256}$/;
|
|
35
|
+
const CURSOR = new RegExp(`^[\\x21-\\x7e]{1,${MAX_CURSOR_LENGTH}}$`);
|
|
36
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
37
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
38
|
+
// Code point ranges that change how a terminal shows text: C0 and C1
|
|
39
|
+
// controls, zero-width and bidirectional formatting, line and paragraph
|
|
40
|
+
// separators, and the byte order mark.
|
|
41
|
+
const UNSAFE_RANGES = Object.freeze([
|
|
42
|
+
[0x0000, 0x001f],
|
|
43
|
+
[0x007f, 0x009f],
|
|
44
|
+
[0x200b, 0x200f],
|
|
45
|
+
[0x2028, 0x202e],
|
|
46
|
+
[0x2060, 0x2069],
|
|
47
|
+
[0xfeff, 0xfeff],
|
|
48
|
+
]);
|
|
49
|
+
const MAX_TEXT = 256;
|
|
50
|
+
const MAX_LIST = 64;
|
|
51
|
+
const MAX_WARNINGS = 50;
|
|
52
|
+
const MAX_ROUTE_ROWS = 10000;
|
|
53
|
+
|
|
54
|
+
// Keys that would carry captured content or credentials. A metadata row that
|
|
55
|
+
// has one is refused as a whole; none of its values is returned.
|
|
56
|
+
const FORBIDDEN_ROW_KEYS = new Set([
|
|
57
|
+
"prompt",
|
|
58
|
+
"prompts",
|
|
59
|
+
"input",
|
|
60
|
+
"inputs",
|
|
61
|
+
"output",
|
|
62
|
+
"outputs",
|
|
63
|
+
"messages",
|
|
64
|
+
"text",
|
|
65
|
+
"content",
|
|
66
|
+
"completion",
|
|
67
|
+
"request",
|
|
68
|
+
"response",
|
|
69
|
+
"spans",
|
|
70
|
+
"system",
|
|
71
|
+
"tool_calls",
|
|
72
|
+
"tool_definitions",
|
|
73
|
+
"api_key",
|
|
74
|
+
"token",
|
|
75
|
+
"access_token",
|
|
76
|
+
"refresh_token",
|
|
77
|
+
"authorization",
|
|
78
|
+
"password",
|
|
79
|
+
"secret",
|
|
80
|
+
"email",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
class Invalid extends Error {
|
|
84
|
+
constructor(outcome, reason) {
|
|
85
|
+
super(reason);
|
|
86
|
+
this.outcome = outcome;
|
|
87
|
+
this.reason = reason;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function run(work) {
|
|
92
|
+
try {
|
|
93
|
+
return { ok: true, value: work() };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof Invalid) return { ok: false, outcome: error.outcome, reason: error.reason };
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
101
|
+
const isCount = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
102
|
+
const isAmount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
103
|
+
const isToken = (value) => typeof value === "string" && TOKEN.test(value);
|
|
104
|
+
const isTimestamp = (value) =>
|
|
105
|
+
typeof value === "string" && value.length <= 40 && TIMESTAMP.test(value) && Number.isFinite(Date.parse(value));
|
|
106
|
+
const isDate = (value) =>
|
|
107
|
+
typeof value === "string" && DATE.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`));
|
|
108
|
+
|
|
109
|
+
function hasUnsafeText(value) {
|
|
110
|
+
for (const char of value) {
|
|
111
|
+
const code = char.codePointAt(0);
|
|
112
|
+
if (UNSAFE_RANGES.some(([low, high]) => code >= low && code <= high)) return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isCursor(value) {
|
|
118
|
+
return typeof value === "string" && CURSOR.test(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isSafeFilter(value) {
|
|
122
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_TEXT && !hasUnsafeText(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Text a person reads, such as a route or model name. It is untrusted
|
|
126
|
+
// metadata, never an instruction. A string that could change how a terminal
|
|
127
|
+
// shows output is replaced with null and noted; any other type is invalid.
|
|
128
|
+
function displayText(value, notices, reason, { nullable = false } = {}) {
|
|
129
|
+
if (value === null && nullable) return null;
|
|
130
|
+
if (typeof value !== "string") throw new Invalid("verification_failed", reason);
|
|
131
|
+
if (!isSafeFilter(value)) {
|
|
132
|
+
notices.add("unsafe_text_omitted");
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function rowObject(value, reason) {
|
|
139
|
+
if (!isObject(value)) throw new Invalid("verification_failed", reason);
|
|
140
|
+
for (const key of Object.keys(value)) {
|
|
141
|
+
if (FORBIDDEN_ROW_KEYS.has(key.toLowerCase())) {
|
|
142
|
+
throw new Invalid("verification_failed", "content_in_metadata_response");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Every document names the bound workspace and profile in its provenance.
|
|
149
|
+
function provenance(body, ctx, reason) {
|
|
150
|
+
if (!isObject(body)) throw new Invalid("verification_failed", reason);
|
|
151
|
+
if (body.schema_version !== AGENT_CONTRACT_VERSION) {
|
|
152
|
+
throw new Invalid("unsupported", "contract_version_unsupported");
|
|
153
|
+
}
|
|
154
|
+
const source = body.provenance;
|
|
155
|
+
if (!isObject(source)) throw new Invalid("verification_failed", reason);
|
|
156
|
+
if (normalizeUuid(source.workspace_id) !== ctx.workspaceId) {
|
|
157
|
+
throw new Invalid("verification_failed", "workspace_context_mismatch");
|
|
158
|
+
}
|
|
159
|
+
if (source.deployment_profile !== ctx.profile) throw new Invalid("verification_failed", "profile_mismatch");
|
|
160
|
+
if (!isTimestamp(source.generated_at) || !isToken(source.source)) {
|
|
161
|
+
throw new Invalid("verification_failed", reason);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
deployment_profile: ctx.profile,
|
|
165
|
+
workspace_id: ctx.workspaceId,
|
|
166
|
+
generated_at: source.generated_at,
|
|
167
|
+
source: source.source,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function contentExcluded(body) {
|
|
172
|
+
if (body.content_included !== false) throw new Invalid("verification_failed", "content_access_granted");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function window(value, days, reason) {
|
|
176
|
+
if (!isObject(value) || value.days !== days || !isTimestamp(value.since) || !isTimestamp(value.until)) {
|
|
177
|
+
throw new Invalid("verification_failed", reason);
|
|
178
|
+
}
|
|
179
|
+
if (Date.parse(value.since) > Date.parse(value.until)) throw new Invalid("verification_failed", reason);
|
|
180
|
+
return { days, since: value.since, until: value.until };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function evidence(value, reason) {
|
|
184
|
+
if (
|
|
185
|
+
!isObject(value) ||
|
|
186
|
+
!Array.isArray(value.sources) ||
|
|
187
|
+
value.sources.length > MAX_LIST ||
|
|
188
|
+
!value.sources.every(isToken) ||
|
|
189
|
+
!isCount(value.rows) ||
|
|
190
|
+
typeof value.complete !== "boolean"
|
|
191
|
+
) {
|
|
192
|
+
throw new Invalid("verification_failed", reason);
|
|
193
|
+
}
|
|
194
|
+
return { sources: [...value.sources], rows: value.rows, complete: value.complete };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Only warning codes are returned. Warning messages are server text and are
|
|
198
|
+
// never printed.
|
|
199
|
+
function warnings(value, notices, reason) {
|
|
200
|
+
if (!Array.isArray(value) || value.length > MAX_WARNINGS) throw new Invalid("verification_failed", reason);
|
|
201
|
+
return value.map((entry) => {
|
|
202
|
+
if (!isObject(entry) || !isToken(entry.code)) throw new Invalid("verification_failed", reason);
|
|
203
|
+
if (entry.message !== undefined && entry.message !== null) {
|
|
204
|
+
if (typeof entry.message !== "string") throw new Invalid("verification_failed", reason);
|
|
205
|
+
notices.add("warning_messages_omitted");
|
|
206
|
+
}
|
|
207
|
+
return { code: entry.code };
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function nameList(value, notices, reason) {
|
|
212
|
+
if (!Array.isArray(value) || value.length > MAX_LIST) throw new Invalid("verification_failed", reason);
|
|
213
|
+
const names = [];
|
|
214
|
+
for (const entry of value) {
|
|
215
|
+
const name = displayText(entry, notices, reason);
|
|
216
|
+
if (name !== null) names.push(name);
|
|
217
|
+
}
|
|
218
|
+
return names;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// GET /v1/agent/workspace, already checked by checkWorkspace during the
|
|
222
|
+
// session. Returns the approved workspace fields only.
|
|
223
|
+
export function workspaceContext(body, ctx, notices) {
|
|
224
|
+
return run(() => {
|
|
225
|
+
const reason = "workspace_response_invalid";
|
|
226
|
+
const origin = provenance(body, ctx, reason);
|
|
227
|
+
const workspace = body.workspace;
|
|
228
|
+
if (!isObject(workspace) || normalizeUuid(workspace.id) !== ctx.workspaceId || !isTimestamp(workspace.created_at)) {
|
|
229
|
+
throw new Invalid("verification_failed", reason);
|
|
230
|
+
}
|
|
231
|
+
if (!isObject(body.retention) || !isCount(body.retention.metadata_days)) {
|
|
232
|
+
throw new Invalid("verification_failed", reason);
|
|
233
|
+
}
|
|
234
|
+
if (!isObject(body.content) || typeof body.content.captured !== "boolean" || body.content.included !== false) {
|
|
235
|
+
throw new Invalid("verification_failed", "content_access_granted");
|
|
236
|
+
}
|
|
237
|
+
const scopes = body.access?.scopes;
|
|
238
|
+
if (!Array.isArray(scopes) || scopes.length !== 1 || scopes[0] !== METADATA_SCOPE) {
|
|
239
|
+
throw new Invalid("verification_failed", "scope_mismatch");
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
provenance: origin,
|
|
243
|
+
workspace: {
|
|
244
|
+
id: ctx.workspaceId,
|
|
245
|
+
slug: displayText(workspace.slug, notices, reason),
|
|
246
|
+
name: displayText(workspace.name, notices, reason),
|
|
247
|
+
created_at: workspace.created_at,
|
|
248
|
+
},
|
|
249
|
+
retention: { metadata_days: body.retention.metadata_days },
|
|
250
|
+
content: { captured: body.content.captured, included: false },
|
|
251
|
+
access: { scopes: [METADATA_SCOPE] },
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function capabilityEntry(entry, reason) {
|
|
257
|
+
if (
|
|
258
|
+
!isObject(entry) ||
|
|
259
|
+
typeof entry.available !== "boolean" ||
|
|
260
|
+
typeof entry.content !== "boolean" ||
|
|
261
|
+
typeof entry.mutates !== "boolean" ||
|
|
262
|
+
typeof entry.external_calls !== "boolean" ||
|
|
263
|
+
!PRIVACY_CLASSES.includes(entry.privacy_class) ||
|
|
264
|
+
!SCOPES.includes(entry.required_scope) ||
|
|
265
|
+
!isToken(entry.schema)
|
|
266
|
+
) {
|
|
267
|
+
throw new Invalid("verification_failed", reason);
|
|
268
|
+
}
|
|
269
|
+
const metadataOnly =
|
|
270
|
+
entry.content === false &&
|
|
271
|
+
entry.mutates === false &&
|
|
272
|
+
entry.external_calls === false &&
|
|
273
|
+
entry.privacy_class === "metadata" &&
|
|
274
|
+
entry.required_scope === METADATA_SCOPE;
|
|
275
|
+
// checkCapabilities already refuses this; it is checked again here so a
|
|
276
|
+
// summary can never show a sensitive capability as available.
|
|
277
|
+
if (!metadataOnly && entry.available) throw new Invalid("verification_failed", "content_access_granted");
|
|
278
|
+
return {
|
|
279
|
+
available: entry.available,
|
|
280
|
+
content: entry.content,
|
|
281
|
+
mutates: entry.mutates,
|
|
282
|
+
external_calls: entry.external_calls,
|
|
283
|
+
privacy_class: entry.privacy_class,
|
|
284
|
+
required_scope: entry.required_scope,
|
|
285
|
+
schema: entry.schema,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// GET /v1/agent/capabilities, already checked by checkCapabilities during the
|
|
290
|
+
// session. Known capabilities are returned by name; absent ones are null.
|
|
291
|
+
// Unknown capability names and privacy class descriptions are not returned.
|
|
292
|
+
export function capabilitySummary(body, ctx, notices) {
|
|
293
|
+
return run(() => {
|
|
294
|
+
const reason = "capabilities_response_invalid";
|
|
295
|
+
const origin = provenance(body, ctx, reason);
|
|
296
|
+
if (body.deployment_profile !== ctx.profile) throw new Invalid("verification_failed", "profile_mismatch");
|
|
297
|
+
const deployment = body.deployment_capabilities;
|
|
298
|
+
if (!Array.isArray(deployment) || deployment.length > MAX_LIST || !deployment.every(isToken)) {
|
|
299
|
+
throw new Invalid("verification_failed", reason);
|
|
300
|
+
}
|
|
301
|
+
if (!isObject(body.agent)) throw new Invalid("verification_failed", reason);
|
|
302
|
+
const agent = {};
|
|
303
|
+
for (const name of CAPABILITY_NAMES) {
|
|
304
|
+
agent[name] = Object.hasOwn(body.agent, name) ? capabilityEntry(body.agent[name], reason) : null;
|
|
305
|
+
}
|
|
306
|
+
if (Object.keys(body.agent).some((name) => !CAPABILITY_NAMES.includes(name))) {
|
|
307
|
+
notices.add("unrecognized_capabilities_ignored");
|
|
308
|
+
}
|
|
309
|
+
const bounds = body.bounds;
|
|
310
|
+
if (
|
|
311
|
+
!isObject(bounds) ||
|
|
312
|
+
!isCount(bounds.max_days) ||
|
|
313
|
+
!isCount(bounds.max_rows) ||
|
|
314
|
+
!isCount(bounds.max_response_bytes) ||
|
|
315
|
+
bounds.content_included_by_default !== false
|
|
316
|
+
) {
|
|
317
|
+
throw new Invalid("verification_failed", reason);
|
|
318
|
+
}
|
|
319
|
+
let contracts = null;
|
|
320
|
+
if (body.contracts !== undefined && body.contracts !== null) {
|
|
321
|
+
const value = body.contracts;
|
|
322
|
+
if (!isObject(value)) throw new Invalid("verification_failed", reason);
|
|
323
|
+
contracts = {};
|
|
324
|
+
for (const key of ["agent_access", "trace_debug"]) {
|
|
325
|
+
if (value[key] === undefined || value[key] === null) contracts[key] = null;
|
|
326
|
+
else if (isToken(value[key])) contracts[key] = value[key];
|
|
327
|
+
else throw new Invalid("verification_failed", reason);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (body.privacy_classes !== undefined) notices.add("privacy_class_details_omitted");
|
|
331
|
+
return {
|
|
332
|
+
provenance: origin,
|
|
333
|
+
deployment_profile: ctx.profile,
|
|
334
|
+
deployment_capabilities: [...deployment],
|
|
335
|
+
agent,
|
|
336
|
+
contracts,
|
|
337
|
+
bounds: {
|
|
338
|
+
max_days: bounds.max_days,
|
|
339
|
+
max_rows: bounds.max_rows,
|
|
340
|
+
max_response_bytes: bounds.max_response_bytes,
|
|
341
|
+
content_included_by_default: false,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// GET /v1/agent/usage?days=N&limit=N. The window is relative days only.
|
|
348
|
+
export function usageReport(body, ctx, request, notices) {
|
|
349
|
+
return run(() => {
|
|
350
|
+
const reason = "usage_response_invalid";
|
|
351
|
+
const origin = provenance(body, ctx, reason);
|
|
352
|
+
contentExcluded(body);
|
|
353
|
+
const span = window(body.window, request.days, reason);
|
|
354
|
+
if (body.days !== request.days || typeof body.truncated !== "boolean") {
|
|
355
|
+
throw new Invalid("verification_failed", reason);
|
|
356
|
+
}
|
|
357
|
+
const proof = evidence(body.evidence, reason);
|
|
358
|
+
const codes = warnings(body.warnings, notices, reason);
|
|
359
|
+
if (!Array.isArray(body.items) || body.items.length > request.limit) {
|
|
360
|
+
throw new Invalid("verification_failed", reason);
|
|
361
|
+
}
|
|
362
|
+
const items = body.items.map((raw) => {
|
|
363
|
+
const row = rowObject(raw, reason);
|
|
364
|
+
const nullableCount = (value) => value === null || isCount(value);
|
|
365
|
+
if (
|
|
366
|
+
!isDate(row.date) ||
|
|
367
|
+
!isCount(row.calls) ||
|
|
368
|
+
!isAmount(row.cost_usd) ||
|
|
369
|
+
!isCount(row.input_tokens) ||
|
|
370
|
+
!isCount(row.output_tokens) ||
|
|
371
|
+
!nullableCount(row.avg_latency_ms) ||
|
|
372
|
+
!nullableCount(row.p95_latency_ms) ||
|
|
373
|
+
!isCount(row.error_calls) ||
|
|
374
|
+
row.error_calls > row.calls
|
|
375
|
+
) {
|
|
376
|
+
throw new Invalid("verification_failed", reason);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
date: row.date,
|
|
380
|
+
route: displayText(row.route, notices, reason, { nullable: true }),
|
|
381
|
+
calls: row.calls,
|
|
382
|
+
error_calls: row.error_calls,
|
|
383
|
+
cost_usd: row.cost_usd,
|
|
384
|
+
input_tokens: row.input_tokens,
|
|
385
|
+
output_tokens: row.output_tokens,
|
|
386
|
+
avg_latency_ms: row.avg_latency_ms,
|
|
387
|
+
p95_latency_ms: row.p95_latency_ms,
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
const complete = proof.complete && !body.truncated;
|
|
391
|
+
if (!proof.complete) notices.add("evidence_incomplete");
|
|
392
|
+
if (body.truncated) notices.add("rows_truncated");
|
|
393
|
+
return {
|
|
394
|
+
provenance: origin,
|
|
395
|
+
window: span,
|
|
396
|
+
evidence: proof,
|
|
397
|
+
warnings: codes,
|
|
398
|
+
content_included: false,
|
|
399
|
+
truncated: body.truncated,
|
|
400
|
+
complete,
|
|
401
|
+
empty: items.length === 0,
|
|
402
|
+
rows: items.length,
|
|
403
|
+
items,
|
|
404
|
+
totals: totalsOf(items, complete),
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Sums over the returned rows only. complete says whether those rows are the
|
|
410
|
+
// whole window; when it is false these are not workspace totals.
|
|
411
|
+
function totalsOf(items, complete) {
|
|
412
|
+
const totals = {
|
|
413
|
+
scope: "returned_rows",
|
|
414
|
+
complete,
|
|
415
|
+
calls: 0,
|
|
416
|
+
error_calls: 0,
|
|
417
|
+
cost_usd: 0,
|
|
418
|
+
input_tokens: 0,
|
|
419
|
+
output_tokens: 0,
|
|
420
|
+
};
|
|
421
|
+
for (const item of items) {
|
|
422
|
+
totals.calls += item.calls;
|
|
423
|
+
totals.error_calls += item.error_calls;
|
|
424
|
+
totals.cost_usd += item.cost_usd;
|
|
425
|
+
totals.input_tokens += item.input_tokens;
|
|
426
|
+
totals.output_tokens += item.output_tokens;
|
|
427
|
+
}
|
|
428
|
+
totals.cost_usd = Math.round(totals.cost_usd * 1e6) / 1e6;
|
|
429
|
+
return totals;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// GET /v1/agent/routes. The endpoint takes no limit or window, so every row
|
|
433
|
+
// in the bounded response is validated and the first request.limit are kept.
|
|
434
|
+
// Descriptions, constraints and evaluation contracts are never returned.
|
|
435
|
+
export function routesReport(body, ctx, request, notices) {
|
|
436
|
+
return run(() => {
|
|
437
|
+
const reason = "routes_response_invalid";
|
|
438
|
+
const origin = provenance(body, ctx, reason);
|
|
439
|
+
if (body.content_included !== undefined) contentExcluded(body);
|
|
440
|
+
if (!Array.isArray(body.routes) || body.routes.length > MAX_ROUTE_ROWS) {
|
|
441
|
+
throw new Invalid("verification_failed", reason);
|
|
442
|
+
}
|
|
443
|
+
const rows = body.routes.map((raw) => {
|
|
444
|
+
const row = rowObject(raw, reason);
|
|
445
|
+
const version = row.evaluation_contract_version;
|
|
446
|
+
const hash = row.evaluation_contract_hash;
|
|
447
|
+
const nullish = (value) => value === null || value === undefined;
|
|
448
|
+
if (
|
|
449
|
+
!(nullish(version) || isCount(version) || isToken(version)) ||
|
|
450
|
+
!(nullish(hash) || (typeof hash === "string" && HASH.test(hash))) ||
|
|
451
|
+
!(nullish(row.updated_at) || isTimestamp(row.updated_at)) ||
|
|
452
|
+
!(nullish(row.description) || typeof row.description === "string") ||
|
|
453
|
+
!(nullish(row.constraints) || isObject(row.constraints)) ||
|
|
454
|
+
!isCount(row.calls) ||
|
|
455
|
+
!isCount(row.replay_eligible_calls)
|
|
456
|
+
) {
|
|
457
|
+
throw new Invalid("verification_failed", reason);
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
route: displayText(row.route, notices, reason),
|
|
461
|
+
updated_at: row.updated_at ?? null,
|
|
462
|
+
calls: row.calls,
|
|
463
|
+
replay_eligible_calls: row.replay_eligible_calls,
|
|
464
|
+
has_description: typeof row.description === "string" && row.description.length > 0,
|
|
465
|
+
has_evaluation_contract: !nullish(row.evaluation_contract),
|
|
466
|
+
evaluation_contract_version: nullish(version) ? null : version,
|
|
467
|
+
evaluation_contract_hash: nullish(hash) ? null : hash,
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
notices.add("route_details_omitted");
|
|
471
|
+
const truncated = rows.length > request.limit;
|
|
472
|
+
if (truncated) notices.add("routes_truncated_locally");
|
|
473
|
+
return {
|
|
474
|
+
provenance: origin,
|
|
475
|
+
content_included: false,
|
|
476
|
+
server_rows: rows.length,
|
|
477
|
+
rows: Math.min(rows.length, request.limit),
|
|
478
|
+
truncated,
|
|
479
|
+
truncation: truncated ? "local" : null,
|
|
480
|
+
empty: rows.length === 0,
|
|
481
|
+
omitted_fields: ["description", "constraints", "evaluation_contract"],
|
|
482
|
+
routes: rows.slice(0, request.limit),
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// GET /v1/agent/traces. One page only. The cursor is opaque and returned for
|
|
488
|
+
// the caller to pass back; the CLI never follows it on its own.
|
|
489
|
+
export function tracesReport(body, ctx, request, notices) {
|
|
490
|
+
return run(() => {
|
|
491
|
+
const reason = "traces_response_invalid";
|
|
492
|
+
const origin = provenance(body, ctx, reason);
|
|
493
|
+
contentExcluded(body);
|
|
494
|
+
const span = window(body.window, request.days, reason);
|
|
495
|
+
const proof = evidence(body.evidence, reason);
|
|
496
|
+
const codes = warnings(body.warnings, notices, reason);
|
|
497
|
+
const page = body.page;
|
|
498
|
+
const cursorOk = (value) => value === null || isCursor(value);
|
|
499
|
+
if (
|
|
500
|
+
!isObject(page) ||
|
|
501
|
+
page.limit !== request.limit ||
|
|
502
|
+
typeof page.truncated !== "boolean" ||
|
|
503
|
+
!cursorOk(page.next_cursor) ||
|
|
504
|
+
body.truncated !== page.truncated ||
|
|
505
|
+
body.next_cursor !== page.next_cursor
|
|
506
|
+
) {
|
|
507
|
+
throw new Invalid("verification_failed", reason);
|
|
508
|
+
}
|
|
509
|
+
if (!Array.isArray(body.traces) || body.traces.length > request.limit) {
|
|
510
|
+
throw new Invalid("verification_failed", reason);
|
|
511
|
+
}
|
|
512
|
+
const rawRows = body.traces.map((raw) => rowObject(raw, reason));
|
|
513
|
+
checkFilters(body, rawRows, request);
|
|
514
|
+
const traces = rawRows.map((row) => {
|
|
515
|
+
const nullableAmount = (value) => value === null || isAmount(value);
|
|
516
|
+
if (
|
|
517
|
+
typeof row.id !== "string" ||
|
|
518
|
+
!IDENT.test(row.id) ||
|
|
519
|
+
typeof row.trace_id !== "string" ||
|
|
520
|
+
!IDENT.test(row.trace_id) ||
|
|
521
|
+
!isTimestamp(row.started_at) ||
|
|
522
|
+
!isTimestamp(row.last_span_at) ||
|
|
523
|
+
!isCount(row.span_count) ||
|
|
524
|
+
!isCount(row.input_tokens) ||
|
|
525
|
+
!isCount(row.output_tokens) ||
|
|
526
|
+
!isCount(row.cache_read_tokens) ||
|
|
527
|
+
!isCount(row.cache_write_tokens) ||
|
|
528
|
+
!nullableAmount(row.cost_usd) ||
|
|
529
|
+
(row.status !== "success" && row.status !== "error")
|
|
530
|
+
) {
|
|
531
|
+
throw new Invalid("verification_failed", reason);
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
id: row.id,
|
|
535
|
+
trace_id: row.trace_id,
|
|
536
|
+
trace_name: displayText(row.trace_name, notices, reason, { nullable: true }),
|
|
537
|
+
status: row.status,
|
|
538
|
+
started_at: row.started_at,
|
|
539
|
+
last_span_at: row.last_span_at,
|
|
540
|
+
span_count: row.span_count,
|
|
541
|
+
input_tokens: row.input_tokens,
|
|
542
|
+
output_tokens: row.output_tokens,
|
|
543
|
+
cache_read_tokens: row.cache_read_tokens,
|
|
544
|
+
cache_write_tokens: row.cache_write_tokens,
|
|
545
|
+
cost_usd: row.cost_usd,
|
|
546
|
+
routes: nameList(row.routes, notices, reason),
|
|
547
|
+
providers: nameList(row.providers, notices, reason),
|
|
548
|
+
models: nameList(row.models, notices, reason),
|
|
549
|
+
link: null,
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
if (!proof.complete) notices.add("evidence_incomplete");
|
|
553
|
+
if (page.truncated) notices.add("more_pages");
|
|
554
|
+
notices.add("trace_links_unavailable");
|
|
555
|
+
return {
|
|
556
|
+
provenance: origin,
|
|
557
|
+
window: span,
|
|
558
|
+
evidence: proof,
|
|
559
|
+
warnings: codes,
|
|
560
|
+
content_included: false,
|
|
561
|
+
// Free-text argument values are not printed back, like any other
|
|
562
|
+
// argument; only whether they were given.
|
|
563
|
+
filters: {
|
|
564
|
+
route_supplied: request.route !== null,
|
|
565
|
+
status: request.status,
|
|
566
|
+
cursor_supplied: request.cursor !== null,
|
|
567
|
+
},
|
|
568
|
+
page: { limit: page.limit, rows: traces.length, truncated: page.truncated, next_cursor: page.next_cursor },
|
|
569
|
+
truncated: page.truncated,
|
|
570
|
+
next_cursor: page.next_cursor,
|
|
571
|
+
complete: proof.complete && !page.truncated,
|
|
572
|
+
empty: traces.length === 0,
|
|
573
|
+
link_status: "server_link_unavailable",
|
|
574
|
+
traces,
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// A filter the service echoes must be exactly the one requested. Without an
|
|
580
|
+
// echo, route and status are checked against the rows. The CLI never sends a
|
|
581
|
+
// workload filter (the rows cannot show whether one applied), so an echoed
|
|
582
|
+
// workload must be absent or null.
|
|
583
|
+
function checkFilters(body, rows, request) {
|
|
584
|
+
const requested = { route: request.route, status: request.status, workload: null };
|
|
585
|
+
if (body.filters !== undefined && body.filters !== null) {
|
|
586
|
+
if (!isObject(body.filters)) throw new Invalid("verification_failed", "traces_response_invalid");
|
|
587
|
+
for (const [key, value] of Object.entries(requested)) {
|
|
588
|
+
const echoed = body.filters[key] === undefined ? null : body.filters[key];
|
|
589
|
+
if (echoed !== value) throw new Invalid("verification_failed", "filter_mismatch");
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
for (const row of rows) {
|
|
594
|
+
if (requested.status !== null && row.status !== requested.status) {
|
|
595
|
+
throw new Invalid("verification_failed", "filter_not_applied");
|
|
596
|
+
}
|
|
597
|
+
if (requested.route !== null && !(Array.isArray(row.routes) && row.routes.includes(requested.route))) {
|
|
598
|
+
throw new Invalid("verification_failed", "filter_not_applied");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|