@noodleseed/one 0.139.3 → 0.140.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.
Files changed (40) hide show
  1. package/dist/metrics-render.d.ts +8 -0
  2. package/dist/metrics-render.d.ts.map +1 -1
  3. package/dist/metrics-render.js +7 -1
  4. package/dist/metrics-render.js.map +1 -1
  5. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  6. package/node_modules/@noodle-borg/control-plane/dist/personal-workspace.d.ts +7 -0
  7. package/node_modules/@noodle-borg/{service → control-plane}/dist/personal-workspace.js +1 -2
  8. package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +1 -0
  9. package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
  10. package/node_modules/@noodle-borg/module/dist/contract.d.ts +0 -64
  11. package/node_modules/@noodle-borg/module/dist/contract.js +3 -2
  12. package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
  13. package/node_modules/@noodle-borg/module/dist/index.js +1 -0
  14. package/node_modules/@noodle-borg/module/dist/intent.d.ts +1 -1
  15. package/node_modules/@noodle-borg/module/dist/request-analytics.d.ts +80 -0
  16. package/node_modules/@noodle-borg/module/dist/request-analytics.js +10 -0
  17. package/node_modules/@noodle-borg/observability/dist/intent-capture.js +4 -0
  18. package/node_modules/@noodle-borg/observability/dist/request-event-buffer.js +4 -0
  19. package/node_modules/@noodle-borg/observability/dist/request-event-metrics.js +15 -0
  20. package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +19 -3
  21. package/node_modules/@noodle-borg/observability/dist/request-events.js +3 -0
  22. package/node_modules/@noodle-borg/observability/dist/runtime.js +61 -1
  23. package/node_modules/@noodle-borg/protocol/dist/handlers/prompts.js +2 -2
  24. package/node_modules/@noodle-borg/protocol/dist/handlers/resources.js +2 -2
  25. package/node_modules/@noodle-borg/protocol/dist/handlers/tools.js +3 -3
  26. package/node_modules/@noodle-borg/protocol/dist/observation.d.ts +17 -0
  27. package/node_modules/@noodle-borg/protocol/dist/observation.js +14 -0
  28. package/node_modules/@noodle-borg/protocol/dist/v2/server.js +5 -5
  29. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
  30. package/node_modules/@noodle-borg/runtime/dist/operation-execution.d.ts +1 -1
  31. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +34 -6
  32. package/node_modules/@noodle-borg/runtime/dist/result.d.ts +18 -0
  33. package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +1 -1
  34. package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +1 -6
  35. package/node_modules/@noodle-borg/service/package.json +1 -1
  36. package/node_modules/@noodle-borg/transport-http/dist/client-identity.js +124 -0
  37. package/node_modules/@noodle-borg/transport-http/dist/identity-authorization.js +8 -8
  38. package/node_modules/@noodle-borg/transport-http/dist/request-capture.js +97 -10
  39. package/node_modules/@noodle-borg/transport-http/dist/serve-request.js +40 -10
  40. package/package.json +2 -2
@@ -3,7 +3,7 @@ import { createDualEraMcpHandler, handleStatelessHttp, JSON_RPC, } from '@noodle
3
3
  import { admissionContexts, requestsInvocationContext } from './admission-context.js';
4
4
  import { authorizeIdentityMode, authorizeMixedMode, authorizePublicServiceMode, canonicalResourceUrl, isIdentityMode, } from './identity-authorization.js';
5
5
  import { invocationClientHint } from './invocation-client-hints.js';
6
- import { emitIntentEvents, emitRequestEvent, header } from './request-capture.js';
6
+ import { emitDeniedRequestEvent, emitIntentEvents, emitRequestEvent, header, } from './request-capture.js';
7
7
  import { admissionRpcError, rpcError, sendJson } from './responses.js';
8
8
  import { preflightServicePrincipalToolCall, sendServicePrincipalToolDenial, } from './service-principal-authorization.js';
9
9
  import { preflightToolAuthorization, sendToolAuthorizationDenial } from './tool-authorization.js';
@@ -18,6 +18,22 @@ import { widgetDomainProjectionForRequest } from './widget-host.js';
18
18
  export async function serveRequest(req, res, target, auth, routeId, maxBody, allowed, tenant) {
19
19
  const startedAt = Date.now();
20
20
  const requestId = randomUUID();
21
+ // Every structured log line for this request carries the analytics correlation id (#1309).
22
+ // Field merge, not `child()`: injected loggers are only guaranteed the four level methods.
23
+ const logger = {
24
+ info: (event, fields) => auth.logger.info(event, { requestId, ...fields }),
25
+ warn: (event, fields) => auth.logger.warn(event, { requestId, ...fields }),
26
+ };
27
+ const denied = (errorKind, parsedBody) => emitDeniedRequestEvent({
28
+ req,
29
+ auth,
30
+ tenant,
31
+ target,
32
+ ...(parsedBody === undefined ? {} : { parsed: parsedBody }),
33
+ startedAt,
34
+ requestId,
35
+ errorKind,
36
+ });
21
37
  const origin = header(req, 'origin');
22
38
  if (origin !== undefined && !originAllowed(origin, allowed)) {
23
39
  return sendJson(res, 403, rpcError(JSON_RPC.INVALID_REQUEST, 'origin not allowed'));
@@ -37,7 +53,7 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
37
53
  if (isIdentityMode(auth.accessMode)) {
38
54
  const authResult = await authorizeIdentityMode(req, res, auth);
39
55
  if (!authResult.allow)
40
- return;
56
+ return denied(`auth_denied.${authResult.reason}`);
41
57
  if (authResult.caller !== undefined) {
42
58
  protocolContext = {
43
59
  ...protocolContext,
@@ -59,7 +75,7 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
59
75
  else if (auth.accessMode === 'mixed') {
60
76
  const authResult = await authorizeMixedMode(req, res, auth);
61
77
  if (!authResult.allow)
62
- return;
78
+ return denied(`auth_denied.${authResult.reason}`);
63
79
  if (authResult.caller !== undefined) {
64
80
  protocolContext = {
65
81
  ...protocolContext,
@@ -117,7 +133,7 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
117
133
  const decision = auth.admissionGate
118
134
  ? await auth.admissionGate(context)
119
135
  : { allow: true };
120
- auth.logger.info('mcp.admission', {
136
+ logger.info('mcp.admission', {
121
137
  routeId: context.routeId,
122
138
  method: context.method,
123
139
  category: context.category,
@@ -134,12 +150,13 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
134
150
  if (decision.retryAfterSeconds !== undefined) {
135
151
  res.setHeader('Retry-After', String(decision.retryAfterSeconds));
136
152
  }
137
- return sendJson(res, decision.status ?? 403, admissionRpcError(context, decision));
153
+ sendJson(res, decision.status ?? 403, admissionRpcError(context, decision));
154
+ return denied(`admission_denied.${boundedReason(decision.reason)}`, parsed);
138
155
  }
139
156
  }
140
157
  const authorization = preflightToolAuthorization(parsed, target, protocolContext.caller);
141
158
  for (const observation of authorization.observations) {
142
- auth.logger.info('mcp.tool_authorization', {
159
+ logger.info('mcp.tool_authorization', {
143
160
  toolName: observation.toolName,
144
161
  decision: observation.decision,
145
162
  reason: observation.reason,
@@ -149,12 +166,12 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
149
166
  }
150
167
  if (authorization.denial !== undefined) {
151
168
  sendToolAuthorizationDenial(req, res, auth, authorization.denial);
152
- return;
169
+ return denied(`auth_denied.${authorization.denial.decision.reason}`, parsed);
153
170
  }
154
171
  const serviceAuthorization = preflightServicePrincipalToolCall(parsed, target, protocolContext.caller);
155
172
  const serviceSubject = protocolContext.caller?.identityKind === 'service' ? protocolContext.caller.subject : undefined;
156
173
  for (const observation of serviceAuthorization.observations) {
157
- auth.logger.info('mcp.service_principal_tool_authorization', {
174
+ logger.info('mcp.service_principal_tool_authorization', {
158
175
  subject: serviceSubject,
159
176
  toolName: observation.toolName,
160
177
  decision: observation.decision,
@@ -176,7 +193,7 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
176
193
  }
177
194
  catch {
178
195
  try {
179
- auth.logger.warn('mcp.tool_authorization.observer_failed', {
196
+ logger.warn('mcp.tool_authorization.observer_failed', {
180
197
  toolName: observation.toolName,
181
198
  decision: observation.decision,
182
199
  });
@@ -188,7 +205,7 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
188
205
  }
189
206
  if (serviceAuthorization.denial !== undefined) {
190
207
  sendServicePrincipalToolDenial(res, serviceAuthorization.denial);
191
- return;
208
+ return denied(`auth_denied.${serviceAuthorization.denial.reason}`, parsed);
192
209
  }
193
210
  if (auth.resolveInvocationContext !== undefined && requestsInvocationContext(parsed)) {
194
211
  const clientHint = invocationClientHint(parsed);
@@ -220,6 +237,7 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
220
237
  },
221
238
  };
222
239
  }
240
+ const dispatchedAt = Date.now();
223
241
  if (auth.protocolMode === 'legacy-only') {
224
242
  await handleStatelessHttp(target, req, res, parsed, protocolContext);
225
243
  }
@@ -251,9 +269,21 @@ export async function serveRequest(req, res, target, auth, routeId, maxBody, all
251
269
  observed,
252
270
  startedAt,
253
271
  requestId,
272
+ timing: { queueMs: dispatchedAt - startedAt, execMs: Date.now() - dispatchedAt },
254
273
  });
255
274
  emitIntentEvents({ req, target, auth, tenant, parsed, observations, requestId });
256
275
  }
276
+ /** Bound a gate-supplied denial reason into a low-cardinality token for the analytics errorKind. */
277
+ function boundedReason(reason) {
278
+ if (reason === undefined)
279
+ return 'denied';
280
+ const token = reason
281
+ .toLowerCase()
282
+ .replace(/[^a-z0-9_-]+/g, '_')
283
+ .replace(/^_+|_+$/g, '')
284
+ .slice(0, 32);
285
+ return token.length === 0 ? 'denied' : token;
286
+ }
257
287
  export function originAllowed(origin, allowed) {
258
288
  if (allowed === undefined)
259
289
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/one",
3
- "version": "0.139.3",
3
+ "version": "0.140.0",
4
4
  "private": false,
5
5
  "description": "Noodle CLI by Noodle Seed — author, run, and deploy declarative MCP servers. Embedding the assistant in your own web app is @noodleseed/assistant.",
6
6
  "license": "Apache-2.0",
@@ -236,7 +236,7 @@
236
236
  "@modelcontextprotocol/client": "2.0.0",
237
237
  "@modelcontextprotocol/server": "2.0.0",
238
238
  "@noodle-borg/admission-limits": "0.0.0",
239
- "@noodle-borg/agent-kit": "0.84.0",
239
+ "@noodle-borg/agent-kit": "0.84.1",
240
240
  "@noodle-borg/app-audit": "0.0.0",
241
241
  "@noodle-borg/app-package": "0.0.0",
242
242
  "@noodle-borg/assistant-gateway": "0.0.0",