@noodleseed/one 0.149.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-surface-ops.d.ts +3 -0
- package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
- package/dist/commands/assistant-surface-ops.js +6 -1
- 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/node_modules/@noodle-borg/admission-limits/dist/envelope.js +12 -6
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +1 -1
- 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/embed-operator-view.d.ts +8 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +9 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +11 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +1 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +10 -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 +14 -0
- 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/observability/dist/assistant-app-tool-usage.js +25 -0
- package/node_modules/@noodle-borg/observability/dist/index.js +1 -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/assistant-interaction-resolution.js +1 -2
- 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 +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-suggestions.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +50 -41
- 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/@noodleseed/assistant/package.json +1 -1
- package/package.json +2 -2
|
@@ -168,12 +168,20 @@ export interface AuthenticatedWebsiteAccess {
|
|
|
168
168
|
/** Allowlist of verified session claims; undeclared claims are dropped at session exchange. */
|
|
169
169
|
readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
|
|
170
170
|
readonly capabilities?: readonly CapabilityRef[];
|
|
171
|
+
/** @see AuthenticatedWebsiteInput.webmcp */
|
|
172
|
+
readonly webmcp?: {
|
|
173
|
+
readonly enabled?: boolean;
|
|
174
|
+
};
|
|
171
175
|
}
|
|
172
176
|
export interface PublicWebsiteAccess {
|
|
173
177
|
readonly mode: 'public' | 'mixed';
|
|
174
178
|
readonly origins: readonly string[];
|
|
175
179
|
readonly capabilities: readonly CapabilityRef[];
|
|
176
180
|
readonly instructions?: string;
|
|
181
|
+
/** @see AuthenticatedWebsiteInput.webmcp */
|
|
182
|
+
readonly webmcp?: {
|
|
183
|
+
readonly enabled?: boolean;
|
|
184
|
+
};
|
|
177
185
|
}
|
|
178
186
|
export type AssistantAccess = AuthenticatedWebsiteAccess | PublicWebsiteAccess;
|
|
179
187
|
export interface AuthenticatedWebsiteInput {
|
|
@@ -183,6 +191,19 @@ export interface AuthenticatedWebsiteInput {
|
|
|
183
191
|
readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
|
|
184
192
|
/** Optional narrowing; omitted means the signed-in surface projects the whole server. */
|
|
185
193
|
readonly capabilities?: readonly CapabilityRef[];
|
|
194
|
+
/**
|
|
195
|
+
* Override this assistant's WebMCP opt-in for sessions minted on this surface, in either
|
|
196
|
+
* direction (ADR 0220, amended). Omitted inherits the assistant's value.
|
|
197
|
+
*
|
|
198
|
+
* It governs **discovery**: whether the embed registers this session's tools with
|
|
199
|
+
* `document.modelContext`, and so whether a browser agent learns they exist. It is not a second
|
|
200
|
+
* authorization boundary — every call the bridge makes already carries exactly this session's
|
|
201
|
+
* authority. A deployment serving a marketing page and a signed-in app usually has two honest
|
|
202
|
+
* answers, which one switch cannot express.
|
|
203
|
+
*/
|
|
204
|
+
readonly webmcp?: {
|
|
205
|
+
readonly enabled?: boolean;
|
|
206
|
+
};
|
|
186
207
|
}
|
|
187
208
|
export interface PublicWebsiteInput {
|
|
188
209
|
readonly origins: readonly (string | ConfigRef)[];
|
|
@@ -201,6 +222,10 @@ export interface PublicWebsiteInput {
|
|
|
201
222
|
* never operates one — and its backend completes the exchange.
|
|
202
223
|
*/
|
|
203
224
|
readonly signIn?: boolean;
|
|
225
|
+
/** @see AuthenticatedWebsiteInput.webmcp */
|
|
226
|
+
readonly webmcp?: {
|
|
227
|
+
readonly enabled?: boolean;
|
|
228
|
+
};
|
|
204
229
|
}
|
|
205
230
|
export declare function authenticatedWebsite(input: AuthenticatedWebsiteInput): AuthenticatedWebsiteAccess;
|
|
206
231
|
export declare function publicWebsite(input: PublicWebsiteInput): PublicWebsiteAccess;
|
|
@@ -213,8 +238,7 @@ export interface EmbeddedAssistantOptions extends AssistantUiOptions {
|
|
|
213
238
|
* page's WebMCP API. Off unless set. Calls carry exactly the embed session's authority and take the
|
|
214
239
|
* same authorization, limits, confirmation, and audit path as the assistant's own — see ADR 0220.
|
|
215
240
|
*
|
|
216
|
-
*
|
|
217
|
-
* surface authority rather than presentation, so it is not refinable per surface.
|
|
241
|
+
* The default for every surface, which any surface may override in either direction.
|
|
218
242
|
*/
|
|
219
243
|
readonly webmcp?: {
|
|
220
244
|
readonly enabled?: boolean;
|
|
@@ -227,6 +251,10 @@ export interface AssistantSurfaceConfig {
|
|
|
227
251
|
readonly instructions?: string;
|
|
228
252
|
readonly capabilities?: readonly AssistantCapability[];
|
|
229
253
|
readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
|
|
254
|
+
/** @see AuthenticatedWebsiteInput.webmcp */
|
|
255
|
+
readonly webmcp?: {
|
|
256
|
+
readonly enabled?: boolean;
|
|
257
|
+
};
|
|
230
258
|
}
|
|
231
259
|
export interface EmbeddedAssistantConfig extends AssistantUiOptions {
|
|
232
260
|
readonly model: AssistantModel;
|
|
@@ -238,7 +266,7 @@ export interface EmbeddedAssistantConfig extends AssistantUiOptions {
|
|
|
238
266
|
readonly allowedOrigins: readonly string[];
|
|
239
267
|
/** Mirrored from the authenticated surface, for the same reason. */
|
|
240
268
|
readonly sessionClaims?: Readonly<Record<string, SessionClaimDeclaration>>;
|
|
241
|
-
/** @see EmbeddedAssistantOptions.webmcp —
|
|
269
|
+
/** @see EmbeddedAssistantOptions.webmcp — the default each surface may override. */
|
|
242
270
|
readonly webmcp?: {
|
|
243
271
|
readonly enabled?: boolean;
|
|
244
272
|
};
|
|
@@ -6,6 +6,7 @@ export function authenticatedWebsite(input) {
|
|
|
6
6
|
...(input.instructions === undefined ? {} : { instructions: input.instructions }),
|
|
7
7
|
...(input.sessionClaims ? { sessionClaims: structuredClone(input.sessionClaims) } : {}),
|
|
8
8
|
...(input.capabilities ? { capabilities: [...input.capabilities] } : {}),
|
|
9
|
+
...(input.webmcp === undefined ? {} : { webmcp: { ...input.webmcp } }),
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
export function publicWebsite(input) {
|
|
@@ -14,6 +15,7 @@ export function publicWebsite(input) {
|
|
|
14
15
|
origins: serializeOrigins(input.origins, 'assistant.access.origins'),
|
|
15
16
|
capabilities: [...input.capabilities],
|
|
16
17
|
...(input.instructions === undefined ? {} : { instructions: input.instructions }),
|
|
18
|
+
...(input.webmcp === undefined ? {} : { webmcp: { ...input.webmcp } }),
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
function serializeOrigins(origins, path) {
|
|
@@ -56,6 +58,7 @@ export function embeddedAssistant(input) {
|
|
|
56
58
|
...(surface.mode === 'authenticated' && surface.sessionClaims
|
|
57
59
|
? { sessionClaims: structuredClone(surface.sessionClaims) }
|
|
58
60
|
: {}),
|
|
61
|
+
...(surface.webmcp === undefined ? {} : { webmcp: { ...surface.webmcp } }),
|
|
59
62
|
})),
|
|
60
63
|
allowedOrigins: surfaces.flatMap((surface) => [...surface.origins]),
|
|
61
64
|
...structuredClone(ui),
|
|
@@ -129,6 +129,20 @@ const BUDGET_SUBCOMMAND = subcommand({
|
|
|
129
129
|
value: '<count>',
|
|
130
130
|
summary: 'Model turns one source address may run per hour. The abuse bound, not fairness.',
|
|
131
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
|
+
}),
|
|
132
146
|
flag({
|
|
133
147
|
...FLAG_FIELDS,
|
|
134
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
|
})
|
|
@@ -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
|
|
@@ -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,5 +1,4 @@
|
|
|
1
|
-
import { withAssistantSessionExecutionAuthority } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
-
import { resolveInvocationContextSnapshot } from '../invocation-context.js';
|
|
1
|
+
import { resolveInvocationContextSnapshot, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
2
|
import { now } from './assistant.js';
|
|
4
3
|
export function assistantInteractionScope(id, session, instant) {
|
|
5
4
|
return {
|
|
@@ -29,7 +29,7 @@ export async function handlePublicAssistantConfiguration(req, res, embedId, deps
|
|
|
29
29
|
if (target === undefined) {
|
|
30
30
|
return sendJson(res, 409, { error: 'assistant deployment is unavailable' });
|
|
31
31
|
}
|
|
32
|
-
const resolved = await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(authorized.embed), deps.appearance);
|
|
32
|
+
const resolved = await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(authorized.embed), deps.appearance, 'public');
|
|
33
33
|
applyBrowserCors(req, res, origin);
|
|
34
34
|
res.setHeader('cache-control', 'private, no-cache');
|
|
35
35
|
return sendJson(res, 200, {
|
|
@@ -59,7 +59,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
59
59
|
if (!target?.deploymentId)
|
|
60
60
|
throw new Error('assistant deployment vanished mid-mint');
|
|
61
61
|
const current = now(deps);
|
|
62
|
-
configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(input.embed), deps.appearance)).effective;
|
|
62
|
+
configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(input.embed), deps.appearance, 'public')).effective;
|
|
63
63
|
const created = await deps.store.createSession({
|
|
64
64
|
// A public surface has no client credential, so the embed id stands in as the minting
|
|
65
65
|
// authority. It is not a secret and nothing downstream may begin treating it as one.
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { ADMISSION_DEFAULTS } from '@noodle-borg/admission-limits/portable';
|
|
2
|
-
import { parseAssistantContextPreferences, surfaceEnvelope, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
+
import { parseAssistantContextPreferences, resolveInvocationContextSnapshot, surfaceEnvelope, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
3
|
import { assistantSuggestionsRequestSchema } from '@noodle-borg/wire-contracts';
|
|
4
4
|
import { readJsonBody, sendJson } from '../http-util.js';
|
|
5
|
-
import { resolveInvocationContextSnapshot } from '../invocation-context.js';
|
|
6
5
|
import { generateInitialAssistantSuggestions } from './assistant-agent.js';
|
|
7
6
|
import { applyBrowserCors, authenticateSession, now } from './assistant-route-http.js';
|
|
8
7
|
import { sessionScopedTarget } from './assistant-session-target.js';
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { clientAddressBucket, } from '@noodle-borg/admission-limits/portable';
|
|
2
2
|
import { assistantModelFailure, assistantModelSource, assistantModelTransport, } from '@noodle-borg/assistant-gateway/model-runtime';
|
|
3
|
-
import { ASSISTANT_SESSION_IDLE_MS,
|
|
3
|
+
import { ASSISTANT_SESSION_IDLE_MS, effectiveAssistantBrowserConfiguration, executeAssistantAppToolCall, isAssistantPageContext, isAssistantVerifiedClaims, parseAssistantContextPreferences, parseAssistantCustomerRouting, refuseBridgeToolCall, refusePublicTurn, resolveInvocationContextSnapshot, resumeTurnMessage, resumeUnavailableMessage, shouldAutoResume, surfaceBindingForOrigin, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
4
4
|
import { canonicalizeAuthorizationClaimValues } from '@noodle-borg/auth';
|
|
5
|
-
import { validateJsonSchemaWithDefaults } from '@noodle-borg/compiler';
|
|
6
5
|
import { tenantMcpUrl } from '@noodle-borg/module';
|
|
7
|
-
import { assistantRefusedTurnUsageRequestEvent, assistantSessionUsageRequestEvent, assistantTurnNumber, assistantTurnUsageRequestEvent, captureAssistantUsage, } from '@noodle-borg/observability';
|
|
8
|
-
import {
|
|
6
|
+
import { assistantAppToolCallRequestEvent, assistantRefusedTurnUsageRequestEvent, assistantSessionUsageRequestEvent, assistantTurnNumber, assistantTurnUsageRequestEvent, captureAssistantUsage, } from '@noodle-borg/observability';
|
|
7
|
+
import { mapExecutionError, mapResourceContents, mapResourcesList, mapToolOutput, mapToolsList, } from '@noodle-borg/protocol';
|
|
9
8
|
import { executeResource } from '@noodle-borg/runtime';
|
|
10
9
|
import { assistantMessageTurnRequestSchema, assistantResumeTurnRequestSchema, assistantSessionResponseSchema, } from '@noodle-borg/wire-contracts';
|
|
11
10
|
import { readJsonBody, sendForbidden, sendJson, sendUnauthorized } from '../http-util.js';
|
|
12
|
-
import { resolveInvocationContextSnapshot } from '../invocation-context.js';
|
|
13
11
|
import { createAssistantTurnStats, runAgentTurn } from './assistant-agent.js';
|
|
14
12
|
import { elevateAssistantSession } from './assistant-elevation.js';
|
|
15
13
|
import { executeAssistantKnowledgeSearch, findAssistantKnowledgeComponent, resolveAssistantKnowledge, } from './assistant-knowledge.js';
|
|
@@ -99,7 +97,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
99
97
|
const declaredClaims = assistant.sessionClaims ?? {};
|
|
100
98
|
const claims = Object.fromEntries(Object.entries(isAssistantVerifiedClaims(parsed.claims) ? parsed.claims : {}).filter(([key]) => key in declaredClaims));
|
|
101
99
|
const current = now(deps);
|
|
102
|
-
const configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, client.tenant, deps.appearance)).effective;
|
|
100
|
+
const configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, client.tenant, deps.appearance, surfaceBinding.kind === 'pre-surfaces' ? undefined : surfaceBinding.kind)).effective;
|
|
103
101
|
const roles = canonicalizeAuthorizationClaimValues(parsed.user.roles, 'role');
|
|
104
102
|
const scopes = canonicalizeAuthorizationClaimValues(parsed.user.scopes, 'scope');
|
|
105
103
|
// Hoisted so an elevation and a fresh exchange bind the *same* principal. Two constructions would be
|
|
@@ -405,15 +403,18 @@ export async function handleAssistantAppRequest(req, res, deps) {
|
|
|
405
403
|
const resource = target.served.artifact.resources?.find((candidate) => candidate.uri === params.uri);
|
|
406
404
|
if (!resource)
|
|
407
405
|
return sendJson(res, 404, { error: 'resource not found' });
|
|
406
|
+
// One projection for both the snapshot and the execution: they must agree, and building it
|
|
407
|
+
// twice was two chances for them not to.
|
|
408
|
+
const executeDeps = withAssistantSessionExecutionAuthority(target.served.deps, target.served.artifact, session);
|
|
408
409
|
const context = await resolveInvocationContextSnapshot({
|
|
409
410
|
artifact: target.served.artifact,
|
|
410
|
-
executeDeps
|
|
411
|
+
executeDeps,
|
|
411
412
|
caller: session.caller,
|
|
412
413
|
instant: now(deps),
|
|
413
414
|
...(session.preferences ? { applicationPreference: session.preferences } : {}),
|
|
414
415
|
});
|
|
415
416
|
const execution = await executeResource(target.served.artifact, resource.name, {}, {
|
|
416
|
-
...
|
|
417
|
+
...executeDeps,
|
|
417
418
|
caller: session.caller,
|
|
418
419
|
context,
|
|
419
420
|
});
|
|
@@ -424,56 +425,64 @@ export async function handleAssistantAppRequest(req, res, deps) {
|
|
|
424
425
|
if (method === 'tools/call') {
|
|
425
426
|
if (typeof params.name !== 'string')
|
|
426
427
|
return sendJson(res, 400, { error: 'tool name required' });
|
|
427
|
-
const
|
|
428
|
+
const startedAt = performance.now();
|
|
429
|
+
const toolName = params.name;
|
|
430
|
+
// Recorded on every exit below, including the refusals: a rejected browser-agent call is
|
|
431
|
+
// exactly the traffic an operator watching one needs to see (ADR 0220 decision 6).
|
|
432
|
+
const record = (outcome, errorKind) => captureAssistantUsage(deps.captureRequestEvent, assistantAppToolCallRequestEvent(session, {
|
|
433
|
+
toolName,
|
|
434
|
+
// Attribution only. The marker labels the row; the session decided authority above.
|
|
435
|
+
bridged,
|
|
436
|
+
outcome,
|
|
437
|
+
...(errorKind === undefined ? {} : { errorKind }),
|
|
438
|
+
durationMs: performance.now() - startedAt,
|
|
439
|
+
}));
|
|
440
|
+
const knowledgeComponent = findAssistantKnowledgeComponent(knowledge, toolName);
|
|
428
441
|
if (knowledge !== undefined && knowledgeComponent !== undefined) {
|
|
429
442
|
const content = await executeAssistantKnowledgeSearch(knowledge, knowledgeComponent, params.arguments ?? {});
|
|
443
|
+
record('ok');
|
|
430
444
|
return sendJson(res, 200, mapToolOutput(JSON.parse(content)));
|
|
431
445
|
}
|
|
432
|
-
const
|
|
433
|
-
|
|
446
|
+
const call = await executeAssistantAppToolCall({
|
|
447
|
+
artifact: target.served.artifact,
|
|
448
|
+
deps: target.served.deps,
|
|
449
|
+
session,
|
|
450
|
+
toolName,
|
|
451
|
+
arguments: params.arguments,
|
|
452
|
+
now: () => now(deps),
|
|
453
|
+
store: deps.store,
|
|
454
|
+
audit: deps.audit,
|
|
455
|
+
});
|
|
456
|
+
if (call.kind === 'not-found') {
|
|
457
|
+
record('refused', 'app_tool_not_found');
|
|
434
458
|
return sendJson(res, 404, { error: 'app tool not found' });
|
|
435
459
|
}
|
|
436
|
-
if (
|
|
460
|
+
if (call.kind === 'forbidden') {
|
|
461
|
+
record('refused', 'tool_forbidden');
|
|
437
462
|
return sendJson(res, 403, { error: 'tool forbidden' });
|
|
438
463
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
executeDeps: withAssistantSessionExecutionAuthority(target.served.deps, target.served.artifact, session),
|
|
442
|
-
caller: session.caller,
|
|
443
|
-
instant: now(deps),
|
|
444
|
-
...(session.preferences ? { applicationPreference: session.preferences } : {}),
|
|
445
|
-
});
|
|
446
|
-
const validated = validateJsonSchemaWithDefaults(tool.inputSchema, params.arguments ?? {});
|
|
447
|
-
if (validated.issues.length > 0) {
|
|
464
|
+
if (call.kind === 'invalid-arguments') {
|
|
465
|
+
record('refused', 'invalid_tool_arguments');
|
|
448
466
|
return sendJson(res, 200, {
|
|
449
467
|
content: [{ type: 'text', text: 'invalid tool arguments' }],
|
|
450
468
|
isError: true,
|
|
451
469
|
});
|
|
452
470
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
store: deps.store,
|
|
462
|
-
audit: deps.audit,
|
|
463
|
-
now: () => now(deps),
|
|
464
|
-
});
|
|
465
|
-
if (dispatched.kind === 'event') {
|
|
466
|
-
if (dispatched.event === 'tool_proposed' || dispatched.event === 'input_requested') {
|
|
467
|
-
return sendJson(res, 200, {
|
|
468
|
-
interaction: { event: dispatched.event, data: dispatched.data },
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
+
if (call.kind === 'interaction') {
|
|
472
|
+
// A raised confirmation is not a failure and not yet an execution: the bridge did what it
|
|
473
|
+
// should, and the interaction is answered on its own route.
|
|
474
|
+
record('ok');
|
|
475
|
+
return sendJson(res, 200, { interaction: { event: call.event, data: call.data } });
|
|
476
|
+
}
|
|
477
|
+
if (call.kind === 'error') {
|
|
478
|
+
record('failed', call.code);
|
|
471
479
|
return sendJson(res, 200, {
|
|
472
|
-
content: [{ type: 'text', text:
|
|
480
|
+
content: [{ type: 'text', text: call.code }],
|
|
473
481
|
isError: true,
|
|
474
482
|
});
|
|
475
483
|
}
|
|
476
|
-
|
|
484
|
+
record('ok');
|
|
485
|
+
return sendJson(res, 200, mapToolOutput(call.output));
|
|
477
486
|
}
|
|
478
487
|
return sendJson(res, 400, { error: 'unsupported MCP App method' });
|
|
479
488
|
}
|
|
@@ -14,9 +14,6 @@ export async function handleIntentCaptureSettings(req, res, ref, deps) {
|
|
|
14
14
|
const identity = await authorizeTenantControl(req, res, deps.gate, deps.controlPlane, ref.org);
|
|
15
15
|
if (identity === false)
|
|
16
16
|
return;
|
|
17
|
-
if (!deps.previewOrgs.has(ref.org)) {
|
|
18
|
-
return sendJson(res, 403, { ok: false, error: 'intent capture preview is unavailable' });
|
|
19
|
-
}
|
|
20
17
|
const setting = await deps.settings.get(ref);
|
|
21
18
|
return sendJson(res, 200, {
|
|
22
19
|
ok: true,
|
|
@@ -32,9 +29,6 @@ export async function handleIntentCaptureSettings(req, res, ref, deps) {
|
|
|
32
29
|
const identity = await authorizeOwner(req, res, ref, deps);
|
|
33
30
|
if (identity === undefined)
|
|
34
31
|
return;
|
|
35
|
-
if (!deps.previewOrgs.has(ref.org)) {
|
|
36
|
-
return sendJson(res, 403, { ok: false, error: 'intent capture preview is unavailable' });
|
|
37
|
-
}
|
|
38
32
|
const body = await readJsonBody(req, deps.maxBody);
|
|
39
33
|
if (!body.ok)
|
|
40
34
|
return sendJson(res, body.status, { error: body.error });
|
|
@@ -74,9 +68,6 @@ export async function handleIntentCaptureSettings(req, res, ref, deps) {
|
|
|
74
68
|
const identity = await authorizeOwner(req, res, ref, deps);
|
|
75
69
|
if (identity === undefined)
|
|
76
70
|
return;
|
|
77
|
-
if (!deps.previewOrgs.has(ref.org)) {
|
|
78
|
-
return sendJson(res, 403, { ok: false, error: 'intent capture preview is unavailable' });
|
|
79
|
-
}
|
|
80
71
|
await deps.settings.delete(ref);
|
|
81
72
|
const purged = await deps.intents.purge(ref);
|
|
82
73
|
await deps.audit.emit({
|
|
@@ -98,9 +89,6 @@ export async function handleIntentInsights(req, res, ref, url, deps) {
|
|
|
98
89
|
const identity = await authorizeTenantControl(req, res, deps.gate, deps.controlPlane, ref.org);
|
|
99
90
|
if (identity === false)
|
|
100
91
|
return;
|
|
101
|
-
if (!deps.previewOrgs.has(ref.org)) {
|
|
102
|
-
return sendJson(res, 403, { ok: false, error: 'intent capture preview is unavailable' });
|
|
103
|
-
}
|
|
104
92
|
const windowName = url.searchParams.get('window') ?? '7d';
|
|
105
93
|
const windowMs = WINDOWS_MS.get(windowName);
|
|
106
94
|
if (windowMs === undefined) {
|
|
@@ -57,7 +57,7 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
57
57
|
let activeAudit;
|
|
58
58
|
const configStore = options.configStore ?? registry.configStore;
|
|
59
59
|
const alertRuleStore = options.alertRuleStore ?? new InMemoryAlertRuleStore();
|
|
60
|
-
const [intentSettings, intentEvents, requestEvents
|
|
60
|
+
const [intentSettings, intentEvents, requestEvents] = createObservabilityStores(options);
|
|
61
61
|
const assistantStore = options.assistantStore ?? new InMemoryAssistantStore();
|
|
62
62
|
const assistantAppearance = options.assistantAppearance ?? new InMemoryAssistantAppearanceSettingsStore();
|
|
63
63
|
const anonymousLimiter = new AnonymousConsumerLimiter();
|
|
@@ -76,7 +76,7 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
76
76
|
activeAudit = audit;
|
|
77
77
|
const { publicTenantRouting } = mcp.mcpRoutingOptions(options, controlPlane);
|
|
78
78
|
const resolveEndpointOptions = (org) => mcp.endpointUrlOptionsForOrg(options, controlPlane, org);
|
|
79
|
-
const withIntentMode = createIntentTargetResolver(intentSettings
|
|
79
|
+
const withIntentMode = createIntentTargetResolver(intentSettings);
|
|
80
80
|
const router = createMcpRouter(async (id) => withIntentMode(await registry.getServing(id)), {
|
|
81
81
|
logger,
|
|
82
82
|
tls,
|
|
@@ -122,7 +122,6 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
122
122
|
requestEvents,
|
|
123
123
|
intentEvents,
|
|
124
124
|
intentSettings,
|
|
125
|
-
previewOrgs: intentPreviewOrgs,
|
|
126
125
|
audit,
|
|
127
126
|
maxBody,
|
|
128
127
|
tls,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
41
|
"@noodle-borg/admission-limits": "0.0.0",
|
|
42
|
-
"@noodle-borg/agent-kit": "0.
|
|
42
|
+
"@noodle-borg/agent-kit": "0.89.0",
|
|
43
43
|
"@noodle-borg/app-package": "0.0.0",
|
|
44
44
|
"@noodle-borg/assistant-gateway": "0.0.0",
|
|
45
45
|
"@noodle-borg/auth": "0.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/assistant",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Embed the Noodle Seed customer-branded assistant in your web app with managed or framework-owned UI, a framework-neutral MCP App host, a DOM-free client, and a backend session helper. Authoring and deploying the server is @noodleseed/one.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/one",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.150.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.
|
|
239
|
+
"@noodle-borg/agent-kit": "0.89.0",
|
|
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",
|