@noodleseed/one 0.148.0 → 0.150.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/dist/commands/assistant-ops.d.ts.map +1 -1
- package/dist/commands/assistant-ops.js +34 -0
- package/dist/commands/assistant-ops.js.map +1 -1
- package/dist/commands/assistant-surface-ops.d.ts +17 -1
- package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
- package/dist/commands/assistant-surface-ops.js +38 -2
- package/dist/commands/assistant-surface-ops.js.map +1 -1
- package/dist/commands/intents-ops.js +1 -1
- package/dist/commands/intents-ops.js.map +1 -1
- package/dist/dev-local-runtime.d.ts +1 -0
- package/dist/dev-local-runtime.d.ts.map +1 -1
- package/dist/dev-local-runtime.js +3 -0
- package/dist/dev-local-runtime.js.map +1 -1
- package/node_modules/@noodle-borg/admission-limits/dist/counter-store.d.ts +20 -0
- package/node_modules/@noodle-borg/admission-limits/dist/counter-store.js +12 -0
- package/node_modules/@noodle-borg/admission-limits/dist/envelope.d.ts +34 -2
- package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +45 -15
- package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.d.ts +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.js +14 -1
- package/node_modules/@noodle-borg/admission-limits/dist/portable.d.ts +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.d.ts +20 -0
- package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.js +30 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-tool-design-ref.js +4 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.d.ts +42 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.js +45 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.d.ts +2 -2
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +17 -3
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.d.ts +13 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.js +53 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.d.ts +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.js +15 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.d.ts +79 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +67 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +37 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +37 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.d.ts +80 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.js +186 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +16 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +3 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +3 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +14 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.js +23 -6
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.js +22 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +15 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +24 -0
- package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +31 -3
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +3 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +29 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-observability.js +1 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +12 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +8 -3
- package/node_modules/@noodle-borg/module/dist/request-analytics.d.ts +19 -2
- package/node_modules/@noodle-borg/module/dist/request-analytics.js +15 -2
- package/node_modules/@noodle-borg/observability/dist/assistant-app-tool-usage.js +25 -0
- package/node_modules/@noodle-borg/observability/dist/assistant-usage.js +68 -10
- package/node_modules/@noodle-borg/observability/dist/index.js +2 -0
- package/node_modules/@noodle-borg/observability/dist/request-event-query.js +48 -0
- package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +13 -2
- package/node_modules/@noodle-borg/observability/dist/request-events.js +3 -0
- package/node_modules/@noodle-borg/service/dist/invocation-context.js +1 -53
- package/node_modules/@noodle-borg/service/dist/observability-runtime.js +4 -6
- package/node_modules/@noodle-borg/service/dist/routes/analytics.js +5 -29
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +57 -29
- package/node_modules/@noodle-borg/service/dist/routes/assistant-embeds.js +26 -38
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-resolution.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +1 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-configuration.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +8 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-suggestions.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +64 -49
- package/node_modules/@noodle-borg/service/dist/routes/intent-capture.js +0 -12
- package/node_modules/@noodle-borg/service/dist/service.js +2 -3
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/request-capture.js +2 -0
- package/node_modules/@noodleseed/assistant/package.json +1 -1
- package/package.json +2 -2
|
@@ -96,7 +96,7 @@ const EMBEDS_SUBCOMMAND = subcommand({
|
|
|
96
96
|
const BUDGET_SUBCOMMAND = subcommand({
|
|
97
97
|
...SUBCOMMAND_FIELDS,
|
|
98
98
|
name: 'budget',
|
|
99
|
-
summary: 'Set what a public website surface may spend per day
|
|
99
|
+
summary: 'Set what a public website surface may spend, per day and per source address.',
|
|
100
100
|
subcommands: [
|
|
101
101
|
clientLeaf('set', 'Raise, lower, or switch off a public surface’s daily caps.', {
|
|
102
102
|
...LEAF_FIELDS,
|
|
@@ -115,6 +115,34 @@ const BUDGET_SUBCOMMAND = subcommand({
|
|
|
115
115
|
value: '<count>',
|
|
116
116
|
summary: 'Sessions this surface may open per UTC day. 0 stops it serving anyone.',
|
|
117
117
|
}),
|
|
118
|
+
flag({
|
|
119
|
+
...FLAG_FIELDS,
|
|
120
|
+
name: 'mints-per-address-hour',
|
|
121
|
+
type: 'string',
|
|
122
|
+
value: '<count>',
|
|
123
|
+
summary: 'Sessions one source address may open per hour. The abuse bound, not fairness.',
|
|
124
|
+
}),
|
|
125
|
+
flag({
|
|
126
|
+
...FLAG_FIELDS,
|
|
127
|
+
name: 'turns-per-address-hour',
|
|
128
|
+
type: 'string',
|
|
129
|
+
value: '<count>',
|
|
130
|
+
summary: 'Model turns one source address may run per hour. The abuse bound, not fairness.',
|
|
131
|
+
}),
|
|
132
|
+
flag({
|
|
133
|
+
...FLAG_FIELDS,
|
|
134
|
+
name: 'bridge-calls-per-session',
|
|
135
|
+
type: 'string',
|
|
136
|
+
value: '<count>',
|
|
137
|
+
summary: 'Tool calls a browser agent may make in one session over the WebMCP bridge.',
|
|
138
|
+
}),
|
|
139
|
+
flag({
|
|
140
|
+
...FLAG_FIELDS,
|
|
141
|
+
name: 'bridge-calls-per-day',
|
|
142
|
+
type: 'string',
|
|
143
|
+
value: '<count>',
|
|
144
|
+
summary: 'Bridge tool calls this surface may serve per UTC day. 0 closes the bridge.',
|
|
145
|
+
}),
|
|
118
146
|
flag({
|
|
119
147
|
...FLAG_FIELDS,
|
|
120
148
|
name: 'surface',
|
|
@@ -352,7 +352,7 @@ export const CATALOG_HOSTED_OBSERVABILITY = [
|
|
|
352
352
|
name: 'intents',
|
|
353
353
|
section: 'operate',
|
|
354
354
|
helpRank: 5,
|
|
355
|
-
summary: 'Optional model-supplied user-goal insights
|
|
355
|
+
summary: 'Optional model-supplied user-goal insights, enabled per environment.',
|
|
356
356
|
arguments: [],
|
|
357
357
|
subcommands: [
|
|
358
358
|
subcommand({
|
|
@@ -377,6 +377,9 @@ export declare const manifestV1Schema: z.ZodObject<{
|
|
|
377
377
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
378
378
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
379
379
|
}, z.core.$strict>>>;
|
|
380
|
+
webmcp: z.ZodOptional<z.ZodObject<{
|
|
381
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
382
|
+
}, z.core.$strict>>;
|
|
380
383
|
}, z.core.$strict>>>;
|
|
381
384
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
382
385
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -856,6 +859,9 @@ export declare const manifestV2Schema: z.ZodObject<{
|
|
|
856
859
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
857
860
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
858
861
|
}, z.core.$strict>>>;
|
|
862
|
+
webmcp: z.ZodOptional<z.ZodObject<{
|
|
863
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
864
|
+
}, z.core.$strict>>;
|
|
859
865
|
}, z.core.$strict>>>;
|
|
860
866
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
861
867
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -1545,6 +1551,9 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1545
1551
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
1546
1552
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
1547
1553
|
}, z.core.$strict>>>;
|
|
1554
|
+
webmcp: z.ZodOptional<z.ZodObject<{
|
|
1555
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
1556
|
+
}, z.core.$strict>>;
|
|
1548
1557
|
}, z.core.$strict>>>;
|
|
1549
1558
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
1550
1559
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -2023,6 +2032,9 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
2023
2032
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
2024
2033
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
2025
2034
|
}, z.core.$strict>>>;
|
|
2035
|
+
webmcp: z.ZodOptional<z.ZodObject<{
|
|
2036
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
2037
|
+
}, z.core.$strict>>;
|
|
2026
2038
|
}, z.core.$strict>>>;
|
|
2027
2039
|
sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
2028
2040
|
exposeToModel: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -352,6 +352,11 @@ const embeddedAssistantSchema = z
|
|
|
352
352
|
instructions: z.string().trim().min(1).max(MAX_SERVER_INSTRUCTIONS).optional(),
|
|
353
353
|
capabilities: z.array(assistantCapabilitySchema).optional(),
|
|
354
354
|
sessionClaims: sessionClaimsSchema.optional(),
|
|
355
|
+
// Overrides the assistant's own opt-in for sessions minted on this surface, in either
|
|
356
|
+
// direction (ADR 0220, amended). Declared explicitly rather than inherited from
|
|
357
|
+
// `assistantUiSchema.shape` — this is not renderer presentation, and a surface that
|
|
358
|
+
// acquired it by a spread would be an accident rather than a decision.
|
|
359
|
+
webmcp: optionalStrictObject({ enabled: z.boolean().optional() }),
|
|
355
360
|
})
|
|
356
361
|
.strict())
|
|
357
362
|
.min(1)
|
|
@@ -360,9 +365,9 @@ const embeddedAssistantSchema = z
|
|
|
360
365
|
// 2026-07-14). Keys become `${user.claims.<key>}`; `exposeToModel` adds it to the identity line.
|
|
361
366
|
sessionClaims: sessionClaimsSchema.optional(),
|
|
362
367
|
// Additive optional field: a v1.x minor under ADR 0150, so no existing manifest can observe it.
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
368
|
+
// The deployment's default, which any surface may override. Declared here rather than in
|
|
369
|
+
// `assistantUiSchema` because that shape is spread into every surface as renderer-owned
|
|
370
|
+
// presentation, and this is not presentation — the surface carries its own copy on purpose.
|
|
366
371
|
webmcp: optionalStrictObject({ enabled: z.boolean().optional() }),
|
|
367
372
|
...assistantUiSchema.shape,
|
|
368
373
|
})
|
|
@@ -5,6 +5,16 @@ export type RequestOutcome = 'ok' | 'tool_error' | 'mcp_error';
|
|
|
5
5
|
export type RequestKind = 'usage' | 'discovery';
|
|
6
6
|
/** How the session id was derived (stateless transport mints synthetic ids). */
|
|
7
7
|
export type SessionSource = 'mcp' | 'synthetic' | 'none';
|
|
8
|
+
/**
|
|
9
|
+
* Which product surface the request arrived through. One vocabulary for the whole stream: the MCP
|
|
10
|
+
* endpoint, either embedded-assistant surface, and a browser agent calling through the WebMCP
|
|
11
|
+
* provider bridge ([ADR 0220](../../../docs/decisions/0220-webmcp-provider-bridge.md)). Attribution
|
|
12
|
+
* only — no code may read this to decide what a caller is allowed to do, because the bridge value
|
|
13
|
+
* originates in a client-declared marker.
|
|
14
|
+
*/
|
|
15
|
+
export type RequestSurface = 'mcp' | 'assistant-public' | 'assistant-authenticated' | 'webmcp';
|
|
16
|
+
export declare const REQUEST_SURFACES: readonly ["mcp", "assistant-public", "assistant-authenticated", "webmcp"];
|
|
17
|
+
export declare function isRequestSurface(value: unknown): value is RequestSurface;
|
|
8
18
|
/** Whether the caller was an anonymous or an authenticated identity (never the raw subject). */
|
|
9
19
|
export type SubjectKind = 'anonymous' | 'authenticated';
|
|
10
20
|
/**
|
|
@@ -31,6 +41,8 @@ export interface RequestEventInput {
|
|
|
31
41
|
/** Bounded client family; always present in practice (`unknown` when nothing safe was supplied). */
|
|
32
42
|
readonly clientFamily?: string;
|
|
33
43
|
readonly accessMode?: AccessMode;
|
|
44
|
+
/** Originating surface; absent on rows written before schema v3, which read back as unknown. */
|
|
45
|
+
readonly surface?: RequestSurface;
|
|
34
46
|
readonly subjectKind: SubjectKind;
|
|
35
47
|
readonly method: string;
|
|
36
48
|
readonly kind: RequestKind;
|
|
@@ -64,6 +76,7 @@ export interface RequestEventFilter {
|
|
|
64
76
|
readonly outcome?: RequestOutcome;
|
|
65
77
|
readonly toolName?: string;
|
|
66
78
|
readonly clientName?: string;
|
|
79
|
+
readonly surface?: RequestSurface;
|
|
67
80
|
readonly sessionId?: string;
|
|
68
81
|
readonly since?: string;
|
|
69
82
|
readonly until?: string;
|
|
@@ -75,6 +88,10 @@ export interface RequestEventSink {
|
|
|
75
88
|
export interface RequestEventStore extends RequestEventSink {
|
|
76
89
|
list(filter: RequestEventFilter): Promise<readonly RequestEvent[]>;
|
|
77
90
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
91
|
+
/**
|
|
92
|
+
* v2 (issue #1309): adds `clientFamily`, `queueMs`, `execMs`, and connector-attribution `details`.
|
|
93
|
+
* v3 (ADR 0220): adds `surface`. Additive and unbackfilled — a row at v2 or below simply has no
|
|
94
|
+
* surface, which a reader must render as unknown rather than as any particular surface.
|
|
95
|
+
*/
|
|
96
|
+
export declare const REQUEST_EVENT_SCHEMA_VERSION = 3;
|
|
80
97
|
//# sourceMappingURL=request-analytics.d.ts.map
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export const REQUEST_SURFACES = [
|
|
2
|
+
'mcp',
|
|
3
|
+
'assistant-public',
|
|
4
|
+
'assistant-authenticated',
|
|
5
|
+
'webmcp',
|
|
6
|
+
];
|
|
7
|
+
export function isRequestSurface(value) {
|
|
8
|
+
return REQUEST_SURFACES.includes(value);
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
* Bounded client-family projection (issue #1309): a stable low-cardinality token derived from MCP
|
|
3
12
|
* `clientInfo` when supplied, else from a closed table of known HTTP client user agents (`other`
|
|
@@ -5,6 +14,10 @@
|
|
|
5
14
|
*/
|
|
6
15
|
export const CLIENT_FAMILY_UNKNOWN = 'unknown';
|
|
7
16
|
export const CLIENT_FAMILY_OTHER = 'other';
|
|
8
|
-
/**
|
|
9
|
-
|
|
17
|
+
/**
|
|
18
|
+
* v2 (issue #1309): adds `clientFamily`, `queueMs`, `execMs`, and connector-attribution `details`.
|
|
19
|
+
* v3 (ADR 0220): adds `surface`. Additive and unbackfilled — a row at v2 or below simply has no
|
|
20
|
+
* surface, which a reader must render as unknown rather than as any particular surface.
|
|
21
|
+
*/
|
|
22
|
+
export const REQUEST_EVENT_SCHEMA_VERSION = 3;
|
|
10
23
|
//# sourceMappingURL=request-analytics.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** Project one apps-bridge tool call onto the shared tenant request stream. */
|
|
3
|
+
export function assistantAppToolCallRequestEvent(session, input) {
|
|
4
|
+
const isPublic = session.publicEmbedId !== undefined;
|
|
5
|
+
return {
|
|
6
|
+
...session.tenant,
|
|
7
|
+
deploymentId: session.deploymentId,
|
|
8
|
+
requestId: randomUUID(),
|
|
9
|
+
sessionId: session.id,
|
|
10
|
+
sessionSource: 'synthetic',
|
|
11
|
+
subjectKind: isPublic ? 'anonymous' : 'authenticated',
|
|
12
|
+
accessMode: isPublic ? 'public' : 'authenticated',
|
|
13
|
+
surface: input.bridged ? 'webmcp' : isPublic ? 'assistant-public' : 'assistant-authenticated',
|
|
14
|
+
method: 'tools/call',
|
|
15
|
+
kind: 'usage',
|
|
16
|
+
toolName: input.toolName,
|
|
17
|
+
outcome: input.outcome === 'ok' ? 'ok' : input.outcome === 'refused' ? 'tool_error' : 'mcp_error',
|
|
18
|
+
...(input.errorKind === undefined ? {} : { errorKind: input.errorKind }),
|
|
19
|
+
durationMs: Number.isFinite(input.durationMs) && input.durationMs > 0 ? input.durationMs : 0,
|
|
20
|
+
// Scalar-only, like every other assistant event: tool arguments, results, page context, and
|
|
21
|
+
// credentials have no field to enter through.
|
|
22
|
+
details: { eventKind: 'appToolCall', bridged: input.bridged },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=assistant-app-tool-usage.js.map
|
|
@@ -34,6 +34,40 @@ export function assistantTurnUsageRequestEvent(session, input) {
|
|
|
34
34
|
...(input.errorKind === undefined ? {} : { errorKind: input.errorKind }),
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A mint refused before any session existed. There is no session id and no turn to count: the event
|
|
39
|
+
* carries the tenant it was refused for and the code that refused it, and nothing else.
|
|
40
|
+
*/
|
|
41
|
+
export function assistantRefusedSessionUsageRequestEvent(input) {
|
|
42
|
+
return assistantUsageRequestEvent({
|
|
43
|
+
...input.tenant,
|
|
44
|
+
deploymentId: input.deploymentId ?? '',
|
|
45
|
+
eventKind: 'session',
|
|
46
|
+
surface: 'public',
|
|
47
|
+
outcome: 'refused',
|
|
48
|
+
errorKind: input.code,
|
|
49
|
+
durationMs: input.durationMs,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Record a refused mint, when it belongs to a tenant.
|
|
54
|
+
*
|
|
55
|
+
* Only attributable refusals are recorded: an unknown or malformed embed id is prober noise and
|
|
56
|
+
* belongs in platform logs, not in a customer's usage. What an operator actually needs — the surface
|
|
57
|
+
* is out of budget, one address is at its hourly ceiling, the origin is not on the live allowlist —
|
|
58
|
+
* all know the embed by the time they refuse, which is why the decision lives here rather than at
|
|
59
|
+
* the call site: a route that forgets it produces silence, and silence reads as health.
|
|
60
|
+
*/
|
|
61
|
+
export function captureRefusedAssistantSession(capture, refusal, durationMs) {
|
|
62
|
+
if (refusal.embed === undefined)
|
|
63
|
+
return;
|
|
64
|
+
const { org, app, env } = refusal.embed;
|
|
65
|
+
captureAssistantUsage(capture, assistantRefusedSessionUsageRequestEvent({
|
|
66
|
+
tenant: { org, app, env },
|
|
67
|
+
code: refusal.code,
|
|
68
|
+
durationMs,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
37
71
|
export function assistantRefusedTurnUsageRequestEvent(session, errorKind, durationMs) {
|
|
38
72
|
return assistantTurnUsageRequestEvent(session, {
|
|
39
73
|
outcome: 'refused',
|
|
@@ -64,9 +98,11 @@ export function captureAssistantUsage(capture, event) {
|
|
|
64
98
|
* credentials, and page context have no field to enter through.
|
|
65
99
|
*/
|
|
66
100
|
export function assistantUsageRequestEvent(input) {
|
|
101
|
+
const refused = input.eventKind === 'session' ? input.outcome === 'refused' : false;
|
|
67
102
|
const details = {
|
|
68
103
|
eventKind: input.eventKind,
|
|
69
104
|
surface: input.surface,
|
|
105
|
+
...(refused ? { assistantOutcome: 'refused' } : {}),
|
|
70
106
|
...(input.modelSource === undefined ? {} : { modelSource: input.modelSource }),
|
|
71
107
|
...(input.eventKind === 'turn'
|
|
72
108
|
? {
|
|
@@ -90,26 +126,29 @@ export function assistantUsageRequestEvent(input) {
|
|
|
90
126
|
env: input.env,
|
|
91
127
|
deploymentId: input.deploymentId,
|
|
92
128
|
requestId: randomUUID(),
|
|
93
|
-
sessionId: input.sessionId,
|
|
129
|
+
...(input.sessionId === undefined ? {} : { sessionId: input.sessionId }),
|
|
94
130
|
sessionSource: 'synthetic',
|
|
95
131
|
subjectKind: input.surface === 'public' ? 'anonymous' : 'authenticated',
|
|
96
132
|
accessMode: input.surface === 'public' ? 'public' : 'authenticated',
|
|
133
|
+
// The shared stream vocabulary, not the assistant's own: `details.surface` keeps the internal
|
|
134
|
+
// spelling for the assistant's own aggregates, while this field is what the whole request
|
|
135
|
+
// stream groups by.
|
|
136
|
+
surface: input.surface === 'public' ? 'assistant-public' : 'assistant-authenticated',
|
|
97
137
|
method: ASSISTANT_USAGE_METHOD,
|
|
98
138
|
kind: 'usage',
|
|
99
|
-
outcome: input.
|
|
100
|
-
? '
|
|
101
|
-
: input.outcome === '
|
|
102
|
-
? '
|
|
139
|
+
outcome: input.outcome === 'refused'
|
|
140
|
+
? 'tool_error'
|
|
141
|
+
: input.eventKind === 'session' || input.outcome === 'delivered'
|
|
142
|
+
? 'ok'
|
|
103
143
|
: 'mcp_error',
|
|
104
|
-
...(input.
|
|
105
|
-
? { errorKind: input.errorKind }
|
|
106
|
-
: {}),
|
|
144
|
+
...(input.errorKind === undefined ? {} : { errorKind: input.errorKind }),
|
|
107
145
|
durationMs: finiteNonNegative(input.durationMs),
|
|
108
146
|
details,
|
|
109
147
|
};
|
|
110
148
|
}
|
|
111
149
|
export function aggregateAssistantUsage(events) {
|
|
112
150
|
let minted = 0;
|
|
151
|
+
let refusedSessions = 0;
|
|
113
152
|
let publicSessions = 0;
|
|
114
153
|
let authenticatedSessions = 0;
|
|
115
154
|
let delivered = 0;
|
|
@@ -125,7 +164,13 @@ export function aggregateAssistantUsage(events) {
|
|
|
125
164
|
let noodleManaged = 0;
|
|
126
165
|
let operator = 0;
|
|
127
166
|
const latencies = [];
|
|
167
|
+
const refusalsByCode = new Map();
|
|
128
168
|
const sessionDepth = new Map();
|
|
169
|
+
const countRefusal = (code) => {
|
|
170
|
+
if (code === undefined)
|
|
171
|
+
return;
|
|
172
|
+
refusalsByCode.set(code, (refusalsByCode.get(code) ?? 0) + 1);
|
|
173
|
+
};
|
|
129
174
|
for (const event of events) {
|
|
130
175
|
if (event.method !== ASSISTANT_USAGE_METHOD)
|
|
131
176
|
continue;
|
|
@@ -133,6 +178,11 @@ export function aggregateAssistantUsage(events) {
|
|
|
133
178
|
const eventKind = scalarString(details?.eventKind);
|
|
134
179
|
const surface = scalarString(details?.surface);
|
|
135
180
|
if (eventKind === 'session') {
|
|
181
|
+
if (scalarString(details?.assistantOutcome) === 'refused') {
|
|
182
|
+
refusedSessions += 1;
|
|
183
|
+
countRefusal(event.errorKind);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
136
186
|
minted += 1;
|
|
137
187
|
if (surface === 'public')
|
|
138
188
|
publicSessions += 1;
|
|
@@ -145,8 +195,10 @@ export function aggregateAssistantUsage(events) {
|
|
|
145
195
|
const outcome = scalarString(details?.assistantOutcome);
|
|
146
196
|
if (outcome === 'delivered')
|
|
147
197
|
delivered += 1;
|
|
148
|
-
else if (outcome === 'refused')
|
|
198
|
+
else if (outcome === 'refused') {
|
|
149
199
|
refused += 1;
|
|
200
|
+
countRefusal(event.errorKind);
|
|
201
|
+
}
|
|
150
202
|
else if (outcome === 'failed')
|
|
151
203
|
failed += 1;
|
|
152
204
|
else
|
|
@@ -174,7 +226,12 @@ export function aggregateAssistantUsage(events) {
|
|
|
174
226
|
const depths = [...sessionDepth.values()].sort((a, b) => a - b);
|
|
175
227
|
const sortedLatencies = latencies.sort((a, b) => a - b);
|
|
176
228
|
return {
|
|
177
|
-
sessions: {
|
|
229
|
+
sessions: {
|
|
230
|
+
minted,
|
|
231
|
+
public: publicSessions,
|
|
232
|
+
authenticated: authenticatedSessions,
|
|
233
|
+
refused: refusedSessions,
|
|
234
|
+
},
|
|
178
235
|
turns: {
|
|
179
236
|
attempted: delivered + failed + refused,
|
|
180
237
|
delivered,
|
|
@@ -197,6 +254,7 @@ export function aggregateAssistantUsage(events) {
|
|
|
197
254
|
},
|
|
198
255
|
tokens: { prompt, completion, reasoning, total },
|
|
199
256
|
byModelSource: { noodleManaged, operator },
|
|
257
|
+
refusalsByCode: Object.fromEntries([...refusalsByCode].sort(([a], [b]) => a.localeCompare(b))),
|
|
200
258
|
};
|
|
201
259
|
}
|
|
202
260
|
function scalarString(value) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
export * from './assistant-app-tool-usage.js';
|
|
1
2
|
export * from './assistant-usage.js';
|
|
2
3
|
export * from './intent-capture.js';
|
|
3
4
|
export * from './request-event-buffer.js';
|
|
4
5
|
export * from './request-event-metrics.js';
|
|
6
|
+
export * from './request-event-query.js';
|
|
5
7
|
export * from './request-events.js';
|
|
6
8
|
export * from './request-events-postgres.js';
|
|
7
9
|
export * from './runtime.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isRequestSurface, REQUEST_SURFACES, } from '@noodle-borg/module';
|
|
2
|
+
/**
|
|
3
|
+
* Query parsing for the tenant-facing analytics event listing. It lives beside the store rather
|
|
4
|
+
* than in the HTTP route because the vocabulary it validates — outcomes, surfaces, the bounded page
|
|
5
|
+
* size — belongs to the analytics stream, not to one transport. The route keeps the HTTP concern:
|
|
6
|
+
* this returns a filter or a reason, and never a response.
|
|
7
|
+
*/
|
|
8
|
+
/** Bounded page size: a tenant may ask for fewer, never for more. */
|
|
9
|
+
const MAX_REQUEST_EVENTS_LIMIT = 500;
|
|
10
|
+
const DEFAULT_LIMIT = 100;
|
|
11
|
+
const OUTCOMES = new Set(['ok', 'tool_error', 'mcp_error']);
|
|
12
|
+
export function parseRequestEventQuery(params, tenant) {
|
|
13
|
+
const limitParam = params.get('limit');
|
|
14
|
+
let limit = DEFAULT_LIMIT;
|
|
15
|
+
if (limitParam !== null) {
|
|
16
|
+
limit = Number(limitParam);
|
|
17
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_REQUEST_EVENTS_LIMIT) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
error: `"limit" must be an integer from 1 to ${MAX_REQUEST_EVENTS_LIMIT}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const status = params.get('status') ?? undefined;
|
|
25
|
+
if (status !== undefined && !OUTCOMES.has(status)) {
|
|
26
|
+
return { ok: false, error: '"status" must be one of ok, tool_error, mcp_error' };
|
|
27
|
+
}
|
|
28
|
+
const surface = params.get('surface') ?? undefined;
|
|
29
|
+
if (surface !== undefined && !isRequestSurface(surface)) {
|
|
30
|
+
return { ok: false, error: `"surface" must be one of ${REQUEST_SURFACES.join(', ')}` };
|
|
31
|
+
}
|
|
32
|
+
const tool = params.get('tool') ?? undefined;
|
|
33
|
+
const client = params.get('client') ?? undefined;
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
filter: {
|
|
37
|
+
org: tenant.org,
|
|
38
|
+
...(tenant.app !== undefined ? { app: tenant.app } : {}),
|
|
39
|
+
...(tenant.env !== undefined ? { env: tenant.env } : {}),
|
|
40
|
+
limit,
|
|
41
|
+
...(status !== undefined ? { outcome: status } : {}),
|
|
42
|
+
...(tool !== undefined ? { toolName: tool } : {}),
|
|
43
|
+
...(client !== undefined ? { clientName: client } : {}),
|
|
44
|
+
...(surface !== undefined ? { surface } : {}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=request-event-query.js.map
|
|
@@ -31,6 +31,7 @@ export async function ensureRequestEventSchema(pool) {
|
|
|
31
31
|
client_version text,
|
|
32
32
|
client_family text,
|
|
33
33
|
access_mode text,
|
|
34
|
+
surface text,
|
|
34
35
|
subject_kind text NOT NULL,
|
|
35
36
|
method text NOT NULL,
|
|
36
37
|
kind text NOT NULL,
|
|
@@ -57,6 +58,12 @@ export async function ensureRequestEventSchema(pool) {
|
|
|
57
58
|
ADD COLUMN IF NOT EXISTS client_family text,
|
|
58
59
|
ADD COLUMN IF NOT EXISTS queue_ms double precision,
|
|
59
60
|
ADD COLUMN IF NOT EXISTS exec_ms double precision
|
|
61
|
+
`);
|
|
62
|
+
// Schema v3 (ADR 0220): the originating surface. Additive and unbackfilled — existing rows keep
|
|
63
|
+
// a NULL surface and read back as unknown.
|
|
64
|
+
await pool.query(`
|
|
65
|
+
ALTER TABLE request_events
|
|
66
|
+
ADD COLUMN IF NOT EXISTS surface text
|
|
60
67
|
`);
|
|
61
68
|
await pool.query(`
|
|
62
69
|
CREATE INDEX IF NOT EXISTS request_events_tenant_time_idx
|
|
@@ -97,10 +104,10 @@ export class PostgresRequestEventStore {
|
|
|
97
104
|
await this.#pool.query(`INSERT INTO request_events
|
|
98
105
|
(id, schema_version, created_at, org_slug, app_slug, environment, deployment_id,
|
|
99
106
|
server_version, sdk_protocol_version, protocol_era, request_id, session_id, session_source,
|
|
100
|
-
client_name, client_version, client_family, access_mode, subject_kind, method, kind,
|
|
107
|
+
client_name, client_version, client_family, access_mode, surface, subject_kind, method, kind,
|
|
101
108
|
tool_name, resource_name, prompt_name, outcome, error_kind, duration_ms, queue_ms, exec_ms,
|
|
102
109
|
output_tokens_est, country, details)
|
|
103
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31)`, [
|
|
110
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32)`, [
|
|
104
111
|
randomUUID(),
|
|
105
112
|
REQUEST_EVENT_SCHEMA_VERSION,
|
|
106
113
|
this.#now(),
|
|
@@ -118,6 +125,7 @@ export class PostgresRequestEventStore {
|
|
|
118
125
|
input.clientVersion ?? null,
|
|
119
126
|
input.clientFamily ?? null,
|
|
120
127
|
input.accessMode ?? null,
|
|
128
|
+
input.surface ?? null,
|
|
121
129
|
input.subjectKind,
|
|
122
130
|
input.method,
|
|
123
131
|
input.kind,
|
|
@@ -153,6 +161,8 @@ export class PostgresRequestEventStore {
|
|
|
153
161
|
add('tool_name', filter.toolName);
|
|
154
162
|
if (filter.clientName !== undefined)
|
|
155
163
|
add('client_name', filter.clientName);
|
|
164
|
+
if (filter.surface !== undefined)
|
|
165
|
+
add('surface', filter.surface);
|
|
156
166
|
if (filter.sessionId !== undefined)
|
|
157
167
|
add('session_id', filter.sessionId);
|
|
158
168
|
if (filter.since !== undefined) {
|
|
@@ -203,6 +213,7 @@ function rowToEvent(row) {
|
|
|
203
213
|
...(row.client_version !== null ? { clientVersion: row.client_version } : {}),
|
|
204
214
|
...(row.client_family !== null ? { clientFamily: row.client_family } : {}),
|
|
205
215
|
...(row.access_mode !== null ? { accessMode: row.access_mode } : {}),
|
|
216
|
+
...(row.surface !== null ? { surface: row.surface } : {}),
|
|
206
217
|
...(row.tool_name !== null ? { toolName: row.tool_name } : {}),
|
|
207
218
|
...(row.resource_name !== null ? { resourceName: row.resource_name } : {}),
|
|
208
219
|
...(row.prompt_name !== null ? { promptName: row.prompt_name } : {}),
|
|
@@ -28,6 +28,7 @@ function toRequestEvent(input) {
|
|
|
28
28
|
...(input.clientVersion !== undefined ? { clientVersion: input.clientVersion } : {}),
|
|
29
29
|
...(input.clientFamily !== undefined ? { clientFamily: input.clientFamily } : {}),
|
|
30
30
|
...(input.accessMode !== undefined ? { accessMode: input.accessMode } : {}),
|
|
31
|
+
...(input.surface !== undefined ? { surface: input.surface } : {}),
|
|
31
32
|
...(input.toolName !== undefined ? { toolName: input.toolName } : {}),
|
|
32
33
|
...(input.resourceName !== undefined ? { resourceName: input.resourceName } : {}),
|
|
33
34
|
...(input.promptName !== undefined ? { promptName: input.promptName } : {}),
|
|
@@ -52,6 +53,8 @@ function matches(event, filter) {
|
|
|
52
53
|
return false;
|
|
53
54
|
if (filter.clientName !== undefined && event.clientName !== filter.clientName)
|
|
54
55
|
return false;
|
|
56
|
+
if (filter.surface !== undefined && event.surface !== filter.surface)
|
|
57
|
+
return false;
|
|
55
58
|
if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId)
|
|
56
59
|
return false;
|
|
57
60
|
if (filter.since !== undefined && event.createdAt < filter.since)
|
|
@@ -1,56 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { executeAmbientContext, } from '@noodle-borg/runtime';
|
|
3
|
-
/** Resolve one immutable snapshot, including a schema-validated read-only ambient provider. */
|
|
4
|
-
export async function resolveInvocationContextSnapshot(input) {
|
|
5
|
-
const callerPreference = {
|
|
6
|
-
...(input.caller?.locale ? { locale: input.caller.locale } : {}),
|
|
7
|
-
...(input.caller?.timeZone ? { timeZone: input.caller.timeZone } : {}),
|
|
8
|
-
...(input.applicationPreference ?? {}),
|
|
9
|
-
};
|
|
10
|
-
const clientHint = {
|
|
11
|
-
...(input.clientLocationHint?.timeZone === undefined
|
|
12
|
-
? {}
|
|
13
|
-
: { timeZone: input.clientLocationHint.timeZone }),
|
|
14
|
-
...(input.clientHint ?? {}),
|
|
15
|
-
};
|
|
16
|
-
const temporal = resolveInvocationContext({
|
|
17
|
-
instant: input.instant,
|
|
18
|
-
...(Object.keys(callerPreference).length > 0
|
|
19
|
-
? { applicationPreference: callerPreference }
|
|
20
|
-
: {}),
|
|
21
|
-
...(Object.keys(clientHint).length === 0 ? {} : { clientHint }),
|
|
22
|
-
...(input.artifact.server.context?.defaults
|
|
23
|
-
? { defaults: input.artifact.server.context.defaults }
|
|
24
|
-
: {}),
|
|
25
|
-
});
|
|
26
|
-
const context = {
|
|
27
|
-
...temporal,
|
|
28
|
-
...(input.clientLocationHint === undefined
|
|
29
|
-
? {}
|
|
30
|
-
: { location: { ...input.clientLocationHint, source: 'client-hint' } }),
|
|
31
|
-
};
|
|
32
|
-
const ambient = input.artifact.server.context?.ambient;
|
|
33
|
-
if (!ambient)
|
|
34
|
-
return context;
|
|
35
|
-
// An anonymous principal can never satisfy a delegated-auth fulfilment: the broker exchange
|
|
36
|
-
// is guaranteed to fail, so skip the doomed round trip instead of paying it every turn.
|
|
37
|
-
if (input.caller?.identityKind === 'anonymous') {
|
|
38
|
-
const delegatedKeys = input.executeDeps.broker.assistantDelegatedAuthKeys?.();
|
|
39
|
-
if (delegatedKeys !== undefined &&
|
|
40
|
-
delegatedKeys.size > 0 &&
|
|
41
|
-
toolTouchesDelegatedAuth(ambient, delegatedKeys)) {
|
|
42
|
-
return { ...context, ambientStatus: 'unavailable' };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const result = await executeAmbientContext(input.artifact, {
|
|
46
|
-
...input.executeDeps,
|
|
47
|
-
...(input.caller ? { caller: input.caller } : {}),
|
|
48
|
-
context,
|
|
49
|
-
});
|
|
50
|
-
if (!result.ok)
|
|
51
|
-
return { ...context, ambientStatus: 'unavailable' };
|
|
52
|
-
return { ...context, ambientStatus: 'available', ambient: result.output };
|
|
53
|
-
}
|
|
1
|
+
import { resolveInvocationContextSnapshot } from '@noodle-borg/assistant-gateway/portable';
|
|
54
2
|
/** Bind the shared snapshot resolver to stateless MCP requests without coupling it to router code. */
|
|
55
3
|
export function createMcpInvocationContextResolver(clock = () => new Date()) {
|
|
56
4
|
return ({ target, caller, clientHint }) => resolveInvocationContextSnapshot({
|
|
@@ -8,17 +8,16 @@ export function createObservabilityStores(options) {
|
|
|
8
8
|
options.intentCaptureSettingsStore ?? new InMemoryIntentCaptureSettingsStore(),
|
|
9
9
|
options.intentEventStore ?? new InMemoryIntentEventStore(),
|
|
10
10
|
options.requestEventStore ?? new InMemoryRequestEventStore(),
|
|
11
|
-
new Set(options.intentCapturePreviewOrgs ?? []),
|
|
12
11
|
];
|
|
13
12
|
}
|
|
14
|
-
export async function resolveIntentMode(target, ref,
|
|
13
|
+
export async function resolveIntentMode(target, ref, settings) {
|
|
15
14
|
if (target === undefined)
|
|
16
15
|
return undefined;
|
|
17
16
|
const resolved = ref ??
|
|
18
17
|
(target.org !== undefined && target.app !== undefined && target.environment !== undefined
|
|
19
18
|
? { org: target.org, app: target.app, env: target.environment }
|
|
20
19
|
: undefined);
|
|
21
|
-
if (resolved === undefined
|
|
20
|
+
if (resolved === undefined)
|
|
22
21
|
return target;
|
|
23
22
|
try {
|
|
24
23
|
const setting = await settings.get(resolved);
|
|
@@ -28,8 +27,8 @@ export async function resolveIntentMode(target, ref, previewOrgs, settings) {
|
|
|
28
27
|
return target;
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
|
-
export function createIntentTargetResolver(settings
|
|
32
|
-
return (target, ref) => resolveIntentMode(target, ref,
|
|
30
|
+
export function createIntentTargetResolver(settings) {
|
|
31
|
+
return (target, ref) => resolveIntentMode(target, ref, settings);
|
|
33
32
|
}
|
|
34
33
|
export function createObservabilityDispatcher(deps) {
|
|
35
34
|
return (req, res, url) => dispatchAnalyticsReads(req, res, url, {
|
|
@@ -50,7 +49,6 @@ export function createObservabilityDispatcher(deps) {
|
|
|
50
49
|
requests: deps.requestEvents,
|
|
51
50
|
audit: deps.audit,
|
|
52
51
|
maxBody: deps.maxBody,
|
|
53
|
-
previewOrgs: deps.previewOrgs,
|
|
54
52
|
applySecurityHeaders,
|
|
55
53
|
enforceHttps,
|
|
56
54
|
sendJson,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aggregateRequestEvents } from '@noodle-borg/observability';
|
|
1
|
+
import { aggregateRequestEvents, parseRequestEventQuery } from '@noodle-borg/observability';
|
|
2
2
|
import { normalizedTimestamps, sendJson } from '../http-util.js';
|
|
3
3
|
import { authorizeTenantControl, developerGrantRouteAccess } from './control-plane.js';
|
|
4
4
|
/**
|
|
@@ -13,8 +13,6 @@ const WINDOWS_MS = new Map([
|
|
|
13
13
|
['7d', 7 * 24 * 60 * 60 * 1000],
|
|
14
14
|
['30d', 30 * 24 * 60 * 60 * 1000],
|
|
15
15
|
]);
|
|
16
|
-
const OUTCOMES = new Set(['ok', 'tool_error', 'mcp_error']);
|
|
17
|
-
const MAX_EVENTS_LIMIT = 500;
|
|
18
16
|
/** Session replay cap: newest N of one session, rendered chronologically. */
|
|
19
17
|
const SESSION_EVENTS_LIMIT = 1000;
|
|
20
18
|
/** Aggregation reads a bounded slice; pre-aggregated rollups are the Stage-F scale path. */
|
|
@@ -60,33 +58,11 @@ export async function handleTenantEvents(req, res, gate, controlPlane, events, r
|
|
|
60
58
|
const identity = await authorizeTenantControl(req, res, gate, controlPlane, ref.org, developerGrantRouteAccess(developerGrants, 'cloud:read'));
|
|
61
59
|
if (identity === false)
|
|
62
60
|
return;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
limit = Number(limitParam);
|
|
67
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_EVENTS_LIMIT) {
|
|
68
|
-
return sendJson(res, 400, {
|
|
69
|
-
error: `"limit" must be an integer from 1 to ${MAX_EVENTS_LIMIT}`,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const status = url.searchParams.get('status') ?? undefined;
|
|
74
|
-
if (status !== undefined && !OUTCOMES.has(status)) {
|
|
75
|
-
return sendJson(res, 400, { error: '"status" must be one of ok, tool_error, mcp_error' });
|
|
76
|
-
}
|
|
77
|
-
const tool = url.searchParams.get('tool') ?? undefined;
|
|
78
|
-
const client = url.searchParams.get('client') ?? undefined;
|
|
79
|
-
const filter = {
|
|
80
|
-
org: ref.org,
|
|
81
|
-
app: ref.app,
|
|
82
|
-
env: ref.env,
|
|
83
|
-
limit,
|
|
84
|
-
...(status !== undefined ? { outcome: status } : {}),
|
|
85
|
-
...(tool !== undefined ? { toolName: tool } : {}),
|
|
86
|
-
...(client !== undefined ? { clientName: client } : {}),
|
|
87
|
-
};
|
|
61
|
+
const query = parseRequestEventQuery(url.searchParams, ref);
|
|
62
|
+
if (!query.ok)
|
|
63
|
+
return sendJson(res, 400, { error: query.error });
|
|
88
64
|
// Store rejections surface as the dispatcher's 500 (validation above already handled 400s).
|
|
89
|
-
return sendJson(res, 200, { ok: true, events: await events.list(filter) });
|
|
65
|
+
return sendJson(res, 200, { ok: true, events: await events.list(query.filter) });
|
|
90
66
|
}
|
|
91
67
|
export async function handleTenantSession(req, res, gate, controlPlane, events, ref, sessionId, developerGrants) {
|
|
92
68
|
const identity = await authorizeTenantControl(req, res, gate, controlPlane, ref.org, developerGrantRouteAccess(developerGrants, 'cloud:read'));
|