@noodleseed/one 0.139.4 → 0.141.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/metrics-render.d.ts +8 -0
- package/dist/metrics-render.d.ts.map +1 -1
- package/dist/metrics-render.js +7 -1
- package/dist/metrics-render.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +9 -1
- package/node_modules/@noodle-borg/app-package/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/app-package/dist/index.js +1 -0
- package/node_modules/@noodle-borg/{compiler → app-package}/dist/schema-validation.d.ts +2 -1
- package/node_modules/@noodle-borg/app-package/package.json +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.d.ts +13 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.js +204 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +22 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +10 -7
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.d.ts +18 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.js +6 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.d.ts +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.js +1 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +21 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +39 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/session-resume.d.ts +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/session-resume.js +12 -0
- package/node_modules/@noodle-borg/auth/dist/control-plane-token-exchange.d.ts +110 -0
- package/node_modules/@noodle-borg/auth/dist/control-plane-token-exchange.js +217 -0
- package/node_modules/@noodle-borg/auth/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/auth/dist/index.js +1 -0
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +5 -0
- package/node_modules/@noodle-borg/compiler/dist/index.d.ts +1 -2
- package/node_modules/@noodle-borg/compiler/dist/index.js +1 -2
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +16 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +19 -2
- package/node_modules/@noodle-borg/compiler/package.json +0 -2
- package/node_modules/@noodle-borg/compute/dist/code-connector.js +44 -1
- package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js +82 -266
- package/node_modules/@noodle-borg/compute/dist/sandbox-eval.js +276 -0
- package/node_modules/@noodle-borg/compute/dist/worker-pool.js +262 -0
- package/node_modules/@noodle-borg/{service/dist/auth/deploy-gate.d.ts → control-plane/dist/deploy-gates.d.ts} +10 -6
- package/node_modules/@noodle-borg/control-plane/dist/deploy-gates.js +266 -0
- package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +1 -0
- package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/developer-mcp/dist/capabilities.js +11 -1
- package/node_modules/@noodle-borg/developer-mcp/dist/index.js +1 -1
- package/node_modules/@noodle-borg/module/dist/contract.d.ts +0 -64
- package/node_modules/@noodle-borg/module/dist/contract.js +3 -2
- package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/module/dist/index.js +1 -0
- package/node_modules/@noodle-borg/module/dist/intent.d.ts +1 -1
- package/node_modules/@noodle-borg/module/dist/request-analytics.d.ts +80 -0
- package/node_modules/@noodle-borg/module/dist/request-analytics.js +10 -0
- package/node_modules/@noodle-borg/observability/dist/intent-capture.js +4 -0
- package/node_modules/@noodle-borg/observability/dist/request-event-buffer.js +4 -0
- package/node_modules/@noodle-borg/observability/dist/request-event-metrics.js +15 -0
- package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +19 -3
- package/node_modules/@noodle-borg/observability/dist/request-events.js +3 -0
- package/node_modules/@noodle-borg/observability/dist/runtime.js +61 -1
- package/node_modules/@noodle-borg/protocol/dist/execution-error-mapping.js +13 -1
- package/node_modules/@noodle-borg/protocol/dist/handlers/prompts.js +2 -2
- package/node_modules/@noodle-borg/protocol/dist/handlers/resources.js +2 -2
- package/node_modules/@noodle-borg/protocol/dist/handlers/tools.js +3 -3
- package/node_modules/@noodle-borg/protocol/dist/observation.d.ts +18 -0
- package/node_modules/@noodle-borg/protocol/dist/observation.js +15 -0
- package/node_modules/@noodle-borg/protocol/dist/v2/server.js +5 -5
- package/node_modules/@noodle-borg/runtime/dist/connector/failure-details.d.ts +1 -1
- package/node_modules/@noodle-borg/runtime/dist/connector/failure-details.js +5 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +15 -1
- package/node_modules/@noodle-borg/runtime/dist/connector/types.js +6 -0
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/runtime/dist/operation-execution.d.ts +1 -1
- package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +35 -6
- package/node_modules/@noodle-borg/runtime/dist/result.d.ts +18 -0
- package/node_modules/@noodle-borg/service/dist/auth/developer-grant-guard.js +2 -1
- package/node_modules/@noodle-borg/service/dist/modules/route-dispatch.js +1 -1
- package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +15 -3
- package/node_modules/@noodle-borg/service/dist/routes/assistant-clients.js +116 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +12 -3
- package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-stream.js +4 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +8 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +3 -9
- package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +11 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-sandbox.js +12 -47
- package/node_modules/@noodle-borg/service/dist/routes/assistant-session-target.js +34 -8
- package/node_modules/@noodle-borg/service/dist/routes/assistant-transcript.js +27 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +28 -130
- package/node_modules/@noodle-borg/service/dist/routes/config-values.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +8 -2
- package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +1 -6
- package/node_modules/@noodle-borg/service/dist/service.js +15 -1
- package/node_modules/@noodle-borg/transport-http/dist/client-identity.js +124 -0
- package/node_modules/@noodle-borg/transport-http/dist/identity-authorization.js +8 -8
- package/node_modules/@noodle-borg/transport-http/dist/request-capture.js +97 -10
- package/node_modules/@noodle-borg/transport-http/dist/serve-request.js +40 -10
- package/node_modules/@noodle-borg/wire-contracts/dist/assistant.d.ts +17 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/assistant.js +25 -0
- package/node_modules/@noodleseed/assistant/README.md +4 -9
- package/package.json +1 -1
- /package/node_modules/@noodle-borg/{compiler → app-package}/dist/schema-validation.js +0 -0
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { boundedClientName, boundedClientVersion, clientFamily, SessionClientMemo, sessionClientKey, } from './client-identity.js';
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort per-process memo from a legacy analytics session id to its `initialize` clientInfo, so
|
|
4
|
+
* legacy tool calls keep client attribution when the client supplies `Mcp-Session-Id` (#1309).
|
|
5
|
+
*/
|
|
6
|
+
let sessionClientMemo = new SessionClientMemo();
|
|
7
|
+
/** Test seam: drop all memoized session client identities. */
|
|
8
|
+
export function resetSessionClientMemo() {
|
|
9
|
+
sessionClientMemo = new SessionClientMemo();
|
|
10
|
+
}
|
|
1
11
|
/** JSON-RPC methods that are protocol discovery/chatter, excluded from usage metrics (ADR 0121). */
|
|
2
12
|
const DISCOVERY_METHODS = new Set([
|
|
3
13
|
'server/discover',
|
|
@@ -33,6 +43,25 @@ export function emitRequestEvent(input) {
|
|
|
33
43
|
const failed = res.statusCode >= 400;
|
|
34
44
|
const toolName = observed?.toolName ?? (method === 'tools/call' ? rpcTargetName(first, method) : undefined);
|
|
35
45
|
const errorKind = observed?.errorKind ?? (failed ? `http_${res.statusCode}` : undefined);
|
|
46
|
+
// Client identity: the request's own clientInfo wins; a legacy request that supplies a session id
|
|
47
|
+
// falls back to the identity memoized from that session's `initialize` (#1309). Values are bounded.
|
|
48
|
+
let clientName = boundedClientName(modern?.clientName ?? init?.clientName);
|
|
49
|
+
let clientVersion = boundedClientVersion(modern?.clientVersion ?? init?.clientVersion);
|
|
50
|
+
if (sessionId !== undefined) {
|
|
51
|
+
const key = sessionClientKey(org, tenant?.app ?? auth.app, tenant?.env ?? auth.environment, sessionId);
|
|
52
|
+
if (method === 'initialize' && clientName !== undefined) {
|
|
53
|
+
sessionClientMemo.remember(key, {
|
|
54
|
+
clientName,
|
|
55
|
+
...(clientVersion === undefined ? {} : { clientVersion }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else if (clientName === undefined) {
|
|
59
|
+
const recalled = sessionClientMemo.recall(key);
|
|
60
|
+
clientName = recalled?.clientName;
|
|
61
|
+
clientVersion ??= recalled?.clientVersion;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const family = clientFamily(clientName, header(req, 'user-agent'));
|
|
36
65
|
const event = {
|
|
37
66
|
org,
|
|
38
67
|
...(tenant !== undefined ? { app: tenant.app, env: tenant.env } : {}),
|
|
@@ -43,16 +72,9 @@ export function emitRequestEvent(input) {
|
|
|
43
72
|
requestId,
|
|
44
73
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
45
74
|
sessionSource: sessionId !== undefined ? 'mcp' : 'none',
|
|
46
|
-
...(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? { clientName: init.clientName }
|
|
50
|
-
: {}),
|
|
51
|
-
...(modern?.clientVersion !== undefined
|
|
52
|
-
? { clientVersion: modern.clientVersion }
|
|
53
|
-
: init?.clientVersion !== undefined
|
|
54
|
-
? { clientVersion: init.clientVersion }
|
|
55
|
-
: {}),
|
|
75
|
+
...(clientName !== undefined ? { clientName } : {}),
|
|
76
|
+
...(clientVersion !== undefined ? { clientVersion } : {}),
|
|
77
|
+
clientFamily: family,
|
|
56
78
|
...(auth.accessMode !== undefined ? { accessMode: auth.accessMode } : {}),
|
|
57
79
|
subjectKind: subject !== undefined ? 'authenticated' : 'anonymous',
|
|
58
80
|
method,
|
|
@@ -63,9 +85,12 @@ export function emitRequestEvent(input) {
|
|
|
63
85
|
outcome: observed?.outcome ?? (failed ? 'mcp_error' : 'ok'),
|
|
64
86
|
...(errorKind !== undefined ? { errorKind } : {}),
|
|
65
87
|
durationMs: Date.now() - startedAt,
|
|
88
|
+
...(input.timing?.queueMs !== undefined ? { queueMs: input.timing.queueMs } : {}),
|
|
89
|
+
...(input.timing?.execMs !== undefined ? { execMs: input.timing.execMs } : {}),
|
|
66
90
|
...(observed?.outputTokensEst !== undefined
|
|
67
91
|
? { outputTokensEst: observed.outputTokensEst }
|
|
68
92
|
: {}),
|
|
93
|
+
...connectorDetails(observed),
|
|
69
94
|
};
|
|
70
95
|
try {
|
|
71
96
|
capture(event);
|
|
@@ -74,6 +99,68 @@ export function emitRequestEvent(input) {
|
|
|
74
99
|
// Analytics is strictly best-effort; a capture bug must never surface to the caller.
|
|
75
100
|
}
|
|
76
101
|
}
|
|
102
|
+
/** Project the observation's safe connector attribution into the scalar-only `details` bag. */
|
|
103
|
+
function connectorDetails(observed) {
|
|
104
|
+
const connector = observed?.connector;
|
|
105
|
+
if (connector === undefined)
|
|
106
|
+
return {};
|
|
107
|
+
return {
|
|
108
|
+
details: {
|
|
109
|
+
connectorId: connector.connectorId,
|
|
110
|
+
connectorVersion: connector.connectorVersion,
|
|
111
|
+
connectorOperation: connector.operation,
|
|
112
|
+
...(connector.category !== undefined ? { connectorCategory: connector.category } : {}),
|
|
113
|
+
...(connector.statusClass !== undefined
|
|
114
|
+
? { connectorStatusClass: connector.statusClass }
|
|
115
|
+
: {}),
|
|
116
|
+
...(connector.attempts !== undefined ? { connectorAttempts: connector.attempts } : {}),
|
|
117
|
+
...(connector.retryable !== undefined ? { connectorRetryable: connector.retryable } : {}),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reason a front-door denial (identity auth, admission, tool authorization) was sent before any
|
|
123
|
+
* protocol handler ran. Bounded reason tokens compose the analytics `errorKind` (#1309).
|
|
124
|
+
*/
|
|
125
|
+
export function emitDeniedRequestEvent(input) {
|
|
126
|
+
const { req, auth, tenant, target, parsed, startedAt, requestId, errorKind } = input;
|
|
127
|
+
const capture = auth.captureRequestEvent;
|
|
128
|
+
if (capture === undefined)
|
|
129
|
+
return;
|
|
130
|
+
const org = tenant?.org ?? auth.org;
|
|
131
|
+
if (org === undefined)
|
|
132
|
+
return;
|
|
133
|
+
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
134
|
+
const method = first === undefined ? 'unknown' : rpcMethod(first);
|
|
135
|
+
const toolName = method === 'tools/call' ? rpcTargetName(first, method) : undefined;
|
|
136
|
+
const modern = modernRequestInfo(first);
|
|
137
|
+
const sdkProtocolVersion = modern?.protocolVersion ?? header(req, 'mcp-protocol-version');
|
|
138
|
+
const event = {
|
|
139
|
+
org,
|
|
140
|
+
...(tenant !== undefined ? { app: tenant.app, env: tenant.env } : {}),
|
|
141
|
+
...(auth.deploymentId !== undefined ? { deploymentId: auth.deploymentId } : {}),
|
|
142
|
+
serverVersion: target.artifact.server.version,
|
|
143
|
+
...(sdkProtocolVersion !== undefined ? { sdkProtocolVersion } : {}),
|
|
144
|
+
...(first !== undefined ? { protocolEra: modern === undefined ? 'legacy' : 'modern' } : {}),
|
|
145
|
+
requestId,
|
|
146
|
+
sessionSource: 'none',
|
|
147
|
+
clientFamily: clientFamily(undefined, header(req, 'user-agent')),
|
|
148
|
+
...(auth.accessMode !== undefined ? { accessMode: auth.accessMode } : {}),
|
|
149
|
+
subjectKind: 'anonymous',
|
|
150
|
+
method,
|
|
151
|
+
kind: 'usage',
|
|
152
|
+
...(toolName !== undefined ? { toolName } : {}),
|
|
153
|
+
outcome: 'mcp_error',
|
|
154
|
+
errorKind,
|
|
155
|
+
durationMs: Date.now() - startedAt,
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
capture(event);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Analytics is strictly best-effort.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
77
164
|
/** Emit validated intent observations into their separate operator stream. */
|
|
78
165
|
export function emitIntentEvents(input) {
|
|
79
166
|
const { req, target, auth, tenant, parsed, observations, requestId } = input;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -20,6 +20,7 @@ export declare const assistantSessionResponseSchema: z.ZodObject<{
|
|
|
20
20
|
interactions: z.ZodOptional<z.ZodURL>;
|
|
21
21
|
apps: z.ZodOptional<z.ZodURL>;
|
|
22
22
|
sandbox: z.ZodOptional<z.ZodURL>;
|
|
23
|
+
transcript: z.ZodOptional<z.ZodURL>;
|
|
23
24
|
}, z.core.$strip>;
|
|
24
25
|
configuration: z.ZodOptional<z.ZodObject<{
|
|
25
26
|
branding: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -30,6 +31,22 @@ export declare const assistantSessionResponseSchema: z.ZodObject<{
|
|
|
30
31
|
}, z.core.$strip>>;
|
|
31
32
|
}, z.core.$strip>;
|
|
32
33
|
export type AssistantSessionResponse = z.infer<typeof assistantSessionResponseSchema>;
|
|
34
|
+
/**
|
|
35
|
+
* The bounded visible transcript (ADR 0141/0201 amendments 2026-08-26): only rows that actually
|
|
36
|
+
* appeared in the panel — never tool internals, tickets, or model-facing scaffolding. Bounded by the
|
|
37
|
+
* session history window (40 messages); a read spends no turn budget. Strict, because the service
|
|
38
|
+
* parses its own response before sending, like the session exchange.
|
|
39
|
+
*/
|
|
40
|
+
export declare const assistantTranscriptResponseSchema: z.ZodObject<{
|
|
41
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
42
|
+
role: z.ZodEnum<{
|
|
43
|
+
assistant: "assistant";
|
|
44
|
+
user: "user";
|
|
45
|
+
}>;
|
|
46
|
+
text: z.ZodString;
|
|
47
|
+
}, z.core.$strict>>;
|
|
48
|
+
}, z.core.$strict>;
|
|
49
|
+
export type AssistantTranscriptResponse = z.infer<typeof assistantTranscriptResponseSchema>;
|
|
33
50
|
/** MCP-Apps-shaped, author-selected renderer summary included on one assistant turn. */
|
|
34
51
|
export declare const assistantModelContextUpdateSchema: z.ZodObject<{
|
|
35
52
|
content: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -42,6 +42,13 @@ export const assistantSessionResponseSchema = z.object({
|
|
|
42
42
|
* published client falls back to the srcdoc proxy.
|
|
43
43
|
*/
|
|
44
44
|
sandbox: z.url().optional(),
|
|
45
|
+
/**
|
|
46
|
+
* Additive bounded visible-transcript endpoint (ADR 0141/0201 amendments 2026-08-26): a
|
|
47
|
+
* session-token-authenticated POST returning what the panel may repaint after a full-page
|
|
48
|
+
* navigation — the sign-in redirect above all. Absent on older services; the widget then
|
|
49
|
+
* starts visually fresh exactly as before.
|
|
50
|
+
*/
|
|
51
|
+
transcript: z.url().optional(),
|
|
45
52
|
}),
|
|
46
53
|
configuration: assistantSessionConfigurationSchema.optional(),
|
|
47
54
|
/**
|
|
@@ -51,6 +58,24 @@ export const assistantSessionResponseSchema = z.object({
|
|
|
51
58
|
*/
|
|
52
59
|
resume: z.object({ tool: z.string().min(1) }).optional(),
|
|
53
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* The bounded visible transcript (ADR 0141/0201 amendments 2026-08-26): only rows that actually
|
|
63
|
+
* appeared in the panel — never tool internals, tickets, or model-facing scaffolding. Bounded by the
|
|
64
|
+
* session history window (40 messages); a read spends no turn budget. Strict, because the service
|
|
65
|
+
* parses its own response before sending, like the session exchange.
|
|
66
|
+
*/
|
|
67
|
+
export const assistantTranscriptResponseSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
entries: z
|
|
70
|
+
.array(z
|
|
71
|
+
.object({
|
|
72
|
+
role: z.enum(['user', 'assistant']),
|
|
73
|
+
text: z.string(),
|
|
74
|
+
})
|
|
75
|
+
.strict())
|
|
76
|
+
.max(40),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
54
79
|
const assistantModelContentPartSchema = z
|
|
55
80
|
.object({ type: z.literal('text'), text: z.string() })
|
|
56
81
|
.strict();
|
|
@@ -35,8 +35,8 @@ troubleshooting) lives at <https://docs.noodleseed.dev/guides/embedded-assistant
|
|
|
35
35
|
the server itself is `@noodleseed/one` (`npm install -g @noodleseed/one`).
|
|
36
36
|
|
|
37
37
|
If your page sends a `Content-Security-Policy`, allow the Noodle service origin in `connect-src` (turns and
|
|
38
|
-
event streams)
|
|
39
|
-
|
|
38
|
+
event streams) and the exact session-advertised `endpoints.sandbox` origin in `frame-src` (it may be a
|
|
39
|
+
dedicated sandbox origin, so your `script-src` can stay strict). Details are in the guide's
|
|
40
40
|
"Content-Security-Policy on your page" section.
|
|
41
41
|
|
|
42
42
|
## Quick start
|
|
@@ -410,13 +410,8 @@ import "@noodleseed/assistant/app-view";
|
|
|
410
410
|
</template>
|
|
411
411
|
```
|
|
412
412
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
`<noodle-app-view>` is the canonical host; `NoodleAppView` delegates to it. Both use the
|
|
417
|
-
service-advertised sandbox URL, route App calls through the supplied client, publish theme changes without
|
|
418
|
-
replacing the iframe, and request standard teardown on semantic replacement, disconnect, or an App teardown
|
|
419
|
-
request.
|
|
413
|
+
Framework setup, lifecycle ownership, sandbox/CSP behavior, and staged diagnostics are owned by the
|
|
414
|
+
[Bring your own UI guide](https://docs.noodleseed.dev/docs/guides/bring-your-own-ui).
|
|
420
415
|
|
|
421
416
|
The App document owns its action intent: it calls standard `tools/call` after connecting and never relies on
|
|
422
417
|
native form navigation. If that call needs input or confirmation, the same `client` publishes the normal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/one",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.141.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",
|
|
File without changes
|