@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.
Files changed (45) hide show
  1. package/dist/commands/assistant-surface-ops.d.ts +3 -0
  2. package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
  3. package/dist/commands/assistant-surface-ops.js +6 -1
  4. package/dist/commands/assistant-surface-ops.js.map +1 -1
  5. package/dist/commands/intents-ops.js +1 -1
  6. package/dist/commands/intents-ops.js.map +1 -1
  7. package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +12 -6
  8. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +1 -1
  9. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  10. package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.d.ts +42 -0
  11. package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.js +45 -0
  12. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.d.ts +2 -2
  13. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +17 -3
  14. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.d.ts +13 -1
  15. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.js +53 -0
  16. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.d.ts +8 -0
  17. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +9 -1
  18. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +11 -1
  19. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +2 -0
  20. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -0
  21. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +1 -0
  22. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +1 -0
  23. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +1 -1
  24. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +10 -0
  25. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  26. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +31 -3
  27. package/node_modules/@noodle-borg/authoring/dist/assistant.js +3 -0
  28. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +14 -0
  29. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-observability.js +1 -1
  30. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +12 -0
  31. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +8 -3
  32. package/node_modules/@noodle-borg/observability/dist/assistant-app-tool-usage.js +25 -0
  33. package/node_modules/@noodle-borg/observability/dist/index.js +1 -0
  34. package/node_modules/@noodle-borg/service/dist/invocation-context.js +1 -53
  35. package/node_modules/@noodle-borg/service/dist/observability-runtime.js +4 -6
  36. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-resolution.js +1 -2
  37. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-configuration.js +1 -1
  38. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +1 -1
  39. package/node_modules/@noodle-borg/service/dist/routes/assistant-suggestions.js +1 -2
  40. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +50 -41
  41. package/node_modules/@noodle-borg/service/dist/routes/intent-capture.js +0 -12
  42. package/node_modules/@noodle-borg/service/dist/service.js +2 -3
  43. package/node_modules/@noodle-borg/service/package.json +1 -1
  44. package/node_modules/@noodleseed/assistant/package.json +1 -1
  45. 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
- * Deployment-wide on purpose: it decides which callers may reach the server's tools, which is
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 — deployment-wide, never per surface. */
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 for private-preview environments.',
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
- // Declared here rather than in `assistantUiSchema` deliberately that shape is also spread into
364
- // every surface, and which callers may reach the server's tools is surface authority, not
365
- // renderer-owned presentation a surface may refine.
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,3 +1,4 @@
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';
@@ -1,56 +1,4 @@
1
- import { resolveInvocationContext, toolTouchesDelegatedAuth, } from '@noodle-borg/assistant-gateway/portable';
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, previewOrgs, settings) {
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 || !previewOrgs.has(resolved.org))
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, previewOrgs) {
32
- return (target, ref) => resolveIntentMode(target, ref, previewOrgs, settings);
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, dispatchAssistantTool, effectiveAssistantBrowserConfiguration, isAssistantPageContext, isAssistantVerifiedClaims, parseAssistantContextPreferences, parseAssistantCustomerRouting, refuseBridgeToolCall, refusePublicTurn, resumeTurnMessage, resumeUnavailableMessage, shouldAutoResume, surfaceBindingForOrigin, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
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 { evaluateToolAuthorization, mapExecutionError, mapResourceContents, mapResourcesList, mapToolOutput, mapToolsList, } from '@noodle-borg/protocol';
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: withAssistantSessionExecutionAuthority(target.served.deps, target.served.artifact, session),
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
- ...withAssistantSessionExecutionAuthority(target.served.deps, target.served.artifact, session),
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 knowledgeComponent = findAssistantKnowledgeComponent(knowledge, params.name);
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 tool = target.served.artifact.tools.find((candidate) => candidate.name === params.name);
433
- if (!tool || tool._meta?.ui?.visibility?.includes('app') === false) {
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 (!evaluateToolAuthorization(tool.authorization, session.caller).allow) {
460
+ if (call.kind === 'forbidden') {
461
+ record('refused', 'tool_forbidden');
437
462
  return sendJson(res, 403, { error: 'tool forbidden' });
438
463
  }
439
- const context = await resolveInvocationContextSnapshot({
440
- artifact: target.served.artifact,
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
- const dispatched = await dispatchAssistantTool({
454
- artifact: target.served.artifact,
455
- tool,
456
- arguments: validated.value,
457
- executeDeps: withAssistantSessionExecutionAuthority(target.served.deps, target.served.artifact, session),
458
- caller: session.caller,
459
- context,
460
- session,
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: String(dispatched.data.code ?? dispatched.event) }],
480
+ content: [{ type: 'text', text: call.code }],
473
481
  isError: true,
474
482
  });
475
483
  }
476
- return sendJson(res, 200, mapToolOutput(dispatched.output));
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, intentPreviewOrgs] = createObservabilityStores(options);
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, intentPreviewOrgs);
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.88.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.29.0",
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.149.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.88.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",