@noodleseed/one 0.149.0 → 0.151.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 (61) hide show
  1. package/dist/commands/assistant-ops.d.ts.map +1 -1
  2. package/dist/commands/assistant-ops.js +4 -1
  3. package/dist/commands/assistant-ops.js.map +1 -1
  4. package/dist/commands/assistant-sponsorship-ops.d.ts +3 -0
  5. package/dist/commands/assistant-sponsorship-ops.d.ts.map +1 -0
  6. package/dist/commands/assistant-sponsorship-ops.js +170 -0
  7. package/dist/commands/assistant-sponsorship-ops.js.map +1 -0
  8. package/dist/commands/assistant-surface-ops.d.ts +3 -0
  9. package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
  10. package/dist/commands/assistant-surface-ops.js +9 -2
  11. package/dist/commands/assistant-surface-ops.js.map +1 -1
  12. package/node_modules/@noodle-borg/admission-limits/dist/counter-store.d.ts +11 -0
  13. package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +12 -6
  14. package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.d.ts +3 -2
  15. package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.js +22 -0
  16. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  17. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +2 -2
  18. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  19. package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.d.ts +42 -0
  20. package/node_modules/@noodle-borg/assistant-gateway/dist/app-tool-call.js +48 -0
  21. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.d.ts +2 -2
  22. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +17 -3
  23. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.d.ts +13 -1
  24. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.js +53 -0
  25. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.d.ts +8 -0
  26. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +9 -1
  27. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +11 -1
  28. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +2 -0
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -0
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/model-error.js +6 -0
  31. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +11 -0
  32. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +22 -1
  33. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +1 -0
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +1 -0
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +1 -1
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +10 -0
  37. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  38. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +31 -3
  39. package/node_modules/@noodle-borg/authoring/dist/assistant.js +3 -0
  40. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +108 -0
  41. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +12 -0
  42. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +8 -3
  43. package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
  44. package/node_modules/@noodle-borg/module/dist/index.js +1 -0
  45. package/node_modules/@noodle-borg/module/dist/logger.d.ts +15 -0
  46. package/node_modules/@noodle-borg/module/dist/logger.js +16 -0
  47. package/node_modules/@noodle-borg/observability/dist/assistant-app-tool-usage.js +47 -0
  48. package/node_modules/@noodle-borg/observability/dist/index.js +1 -0
  49. package/node_modules/@noodle-borg/service/dist/invocation-context.js +1 -53
  50. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-resolution.js +1 -2
  51. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-configuration.js +1 -1
  52. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +1 -1
  53. package/node_modules/@noodle-borg/service/dist/routes/assistant-suggestions.js +1 -2
  54. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +63 -44
  55. package/node_modules/@noodle-borg/service/package.json +1 -1
  56. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  57. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  58. package/node_modules/@noodle-borg/wire-contracts/dist/managed-assistant-sponsorship.d.ts +297 -0
  59. package/node_modules/@noodle-borg/wire-contracts/dist/managed-assistant-sponsorship.js +94 -0
  60. package/node_modules/@noodleseed/assistant/package.json +1 -1
  61. package/package.json +2 -2
@@ -15,10 +15,18 @@ export interface AssistantEmbedOperatorView {
15
15
  readonly capabilities: readonly string[];
16
16
  readonly turnsPerDay: number;
17
17
  readonly mintsPerDay: number;
18
+ /**
19
+ * What a browser agent may spend on this surface through the WebMCP bridge. Reported beside the
20
+ * turn caps because "is my budget about to run out" is the same question, and a bridge call spends
21
+ * neither a turn nor a mint — an operator reading only those two would see a quiet day.
22
+ */
23
+ readonly bridgeCallsPerSession: number;
24
+ readonly bridgeCallsPerDay: number;
18
25
  /** True when the caps are the deployment defaults rather than something an operator chose. */
19
26
  readonly budgetIsDefault: boolean;
20
27
  readonly turnsToday: number;
21
28
  readonly mintsToday: number;
29
+ readonly bridgeCallsToday: number;
22
30
  readonly createdAt: string;
23
31
  /**
24
32
  * Present only when Noodle is funding this surface's model. It exists so an operator can tell
@@ -42,9 +42,17 @@ export async function assistantEmbedOperatorView(input) {
42
42
  capabilities: surface?.capabilities.map((entry) => entry.name) ?? [],
43
43
  turnsPerDay: envelope.turnsPerDay,
44
44
  mintsPerDay: envelope.mintsPerDay,
45
- budgetIsDefault: record.turnsPerDay === undefined && record.mintsPerDay === undefined,
45
+ bridgeCallsPerSession: envelope.bridgeToolCallsPerSession,
46
+ bridgeCallsPerDay: envelope.bridgeToolCallsPerDay,
47
+ budgetIsDefault: record.turnsPerDay === undefined &&
48
+ record.mintsPerDay === undefined &&
49
+ record.bridgeToolCallsPerSession === undefined &&
50
+ record.bridgeToolCallsPerDay === undefined,
46
51
  turnsToday: await input.peek(`turns:${record.embedId}`),
47
52
  mintsToday: await input.peek(`mints:${record.embedId}`),
53
+ // Keyed as `admitBridgeToolCall` writes it, so the number an operator reads is the one the
54
+ // gate consumed rather than a parallel count that can drift from it.
55
+ bridgeCallsToday: await input.peek(`bridge:${record.embedId}`),
48
56
  createdAt: record.createdAt.toISOString(),
49
57
  ...(spend === undefined
50
58
  ? {}
@@ -38,6 +38,14 @@ export interface PublicEmbedRecord {
38
38
  */
39
39
  readonly mintsPerAddressHour?: number;
40
40
  readonly turnsPerAddressHour?: number;
41
+ /**
42
+ * Operator overrides for this surface's WebMCP bridge budgets. The shipped defaults are
43
+ * proportionate rather than measured (ADR 0220), so an operator who can see their own agent
44
+ * traffic needs to move them without waiting for a release. `0` is the kill switch here too,
45
+ * though zeroing `turnsPerDay` already stops bridge calls with it.
46
+ */
47
+ readonly bridgeToolCallsPerSession?: number;
48
+ readonly bridgeToolCallsPerDay?: number;
41
49
  }
42
50
  /** Project the non-secret embed coordinate onto the shared assistant tenant shape. */
43
51
  export declare function publicEmbedTenant(embed: PublicEmbedRecord): TenantRef;
@@ -47,6 +55,8 @@ export interface PublicEmbedBudget {
47
55
  readonly mintsPerDay?: number;
48
56
  readonly mintsPerAddressHour?: number;
49
57
  readonly turnsPerAddressHour?: number;
58
+ readonly bridgeToolCallsPerSession?: number;
59
+ readonly bridgeToolCallsPerDay?: number;
50
60
  }
51
61
  export interface PublicEmbedStore {
52
62
  /**
@@ -89,7 +99,7 @@ import type { TenantRef } from './tenant-ref.js';
89
99
  * cap may be, not about HTTP, and a second caller must not be able to reach the store with a shape
90
100
  * this would have rejected.
91
101
  */
92
- export type BudgetField = 'turnsPerDay' | 'mintsPerDay' | 'mintsPerAddressHour' | 'turnsPerAddressHour';
102
+ export type BudgetField = 'turnsPerDay' | 'mintsPerDay' | 'mintsPerAddressHour' | 'turnsPerAddressHour' | 'bridgeToolCallsPerSession' | 'bridgeToolCallsPerDay';
93
103
  type BudgetValue = Partial<Record<BudgetField, number>>;
94
104
  type ParsedBudget = {
95
105
  readonly ok: true;
@@ -28,6 +28,8 @@ const BUDGET_FIELDS = [
28
28
  'mintsPerDay',
29
29
  'mintsPerAddressHour',
30
30
  'turnsPerAddressHour',
31
+ 'bridgeToolCallsPerSession',
32
+ 'bridgeToolCallsPerDay',
31
33
  ];
32
34
  export function parsePublicEmbedBudget(body) {
33
35
  // A JSON body of `null` is an object to `typeof` and reads as a crash to a property access, so it
@@ -45,6 +45,12 @@ export class InMemoryPublicEmbedStore {
45
45
  ...(budget.turnsPerAddressHour !== undefined
46
46
  ? { turnsPerAddressHour: budget.turnsPerAddressHour }
47
47
  : {}),
48
+ ...(budget.bridgeToolCallsPerSession !== undefined
49
+ ? { bridgeToolCallsPerSession: budget.bridgeToolCallsPerSession }
50
+ : {}),
51
+ ...(budget.bridgeToolCallsPerDay !== undefined
52
+ ? { bridgeToolCallsPerDay: budget.bridgeToolCallsPerDay }
53
+ : {}),
48
54
  };
49
55
  this.#byId.set(embedId, updated);
50
56
  return updated;
@@ -52,6 +52,12 @@ export function modelFetchError(cause) {
52
52
  export function modelResponseError(cause) {
53
53
  return new AssistantModelError('model_response_invalid', { retryable: false, cause });
54
54
  }
55
+ export function managedSponsorshipExhausted() {
56
+ return new AssistantModelError('daily_turn_budget_exhausted', { retryable: false });
57
+ }
58
+ export function managedSponsorshipUnavailable(cause) {
59
+ return new AssistantModelError('model_unavailable', { retryable: true, cause });
60
+ }
55
61
  function isTimeoutError(error) {
56
62
  return error instanceof Error && error.name === 'TimeoutError';
57
63
  }
@@ -22,6 +22,17 @@ interface ResolvedAssistantModelBase {
22
22
  readonly baseUrl: string;
23
23
  readonly model: string;
24
24
  readonly requestPolicy?: AssistantModelRequestPolicy;
25
+ /**
26
+ * Private hosted sponsorship admission. It is deliberately attached to the resolved binding so
27
+ * every path that reaches provider I/O passes the same gate, including suggestions, interaction
28
+ * narration, authenticated assistants, and doctor probes.
29
+ */
30
+ readonly sponsorship?: {
31
+ readonly accountKey: string;
32
+ readonly allowance: number;
33
+ readonly units: number;
34
+ admit(): Promise<boolean>;
35
+ };
25
36
  readonly publicAdmission?: {
26
37
  readonly defaults: {
27
38
  readonly turnsPerSession: number;
@@ -1,4 +1,4 @@
1
- import { AssistantModelError, modelFetchError, modelHttpError, modelRequestError, modelResponseError, } from './model-error.js';
1
+ import { AssistantModelError, managedSponsorshipExhausted, managedSponsorshipUnavailable, modelFetchError, modelHttpError, modelRequestError, modelResponseError, } from './model-error.js';
2
2
  import { readResponsesCompletion, responsesInput, responsesTools } from './model-responses.js';
3
3
  import { readModelCompletion } from './model-stream.js';
4
4
  export async function requestModelCompletion(input) {
@@ -34,6 +34,7 @@ export async function requestModelCompletion(input) {
34
34
  if (maxRequestBytes !== undefined && requestBytes > maxRequestBytes) {
35
35
  throw modelRequestError('model request too large');
36
36
  }
37
+ await admitSponsorship(binding);
37
38
  const bearerToken = binding.apiKey ?? (await binding.bearerToken());
38
39
  if (!/^[^\s]{1,8192}$/.test(bearerToken)) {
39
40
  throw modelRequestError('invalid model bearer token');
@@ -63,4 +64,24 @@ export async function requestModelCompletion(input) {
63
64
  throw modelResponseError(error);
64
65
  }
65
66
  }
67
+ /** One resolved binding represents one bounded model operation, even when its agent loop has steps. */
68
+ const sponsorshipAdmissions = new WeakMap();
69
+ async function admitSponsorship(binding) {
70
+ if (binding.source !== 'noodle-managed' || binding.sponsorship === undefined)
71
+ return;
72
+ let admission = sponsorshipAdmissions.get(binding);
73
+ if (admission === undefined) {
74
+ admission = binding.sponsorship.admit();
75
+ sponsorshipAdmissions.set(binding, admission);
76
+ }
77
+ let allowed;
78
+ try {
79
+ allowed = await admission;
80
+ }
81
+ catch (error) {
82
+ throw managedSponsorshipUnavailable(error);
83
+ }
84
+ if (!allowed)
85
+ throw managedSponsorshipExhausted();
86
+ }
66
87
  //# sourceMappingURL=model-request.js.map
@@ -1,4 +1,5 @@
1
1
  /** Assistant gateway contracts and in-memory adapters safe for the published CLI runtime. */
2
+ export * from './app-tool-call.js';
2
3
  export * from './artifact-projection.js';
3
4
  export * from './assistant-appearance.js';
4
5
  export * from './assistant-appearance-store.js';
@@ -1,4 +1,5 @@
1
1
  /** Assistant gateway contracts and in-memory adapters safe for the published CLI runtime. */
2
+ export * from './app-tool-call.js';
2
3
  export * from './artifact-projection.js';
3
4
  export * from './assistant-appearance.js';
4
5
  export * from './assistant-appearance-store.js';
@@ -31,5 +31,5 @@ export interface SurfaceBudgetBounds {
31
31
  * operator any more than by app code, so an over-eager `--turns-per-day 999999999` degrades to the
32
32
  * ceiling instead of becoming the ceiling. Zero survives clamping and is the kill switch.
33
33
  */
34
- export declare function surfaceEnvelope(base: AdmissionEnvelope, embed: Pick<PublicEmbedRecord, 'turnsPerDay' | 'mintsPerDay' | 'mintsPerAddressHour' | 'turnsPerAddressHour'>, bounds?: SurfaceBudgetBounds): AdmissionEnvelope;
34
+ export declare function surfaceEnvelope(base: AdmissionEnvelope, embed: Pick<PublicEmbedRecord, 'turnsPerDay' | 'mintsPerDay' | 'mintsPerAddressHour' | 'turnsPerAddressHour' | 'bridgeToolCallsPerSession' | 'bridgeToolCallsPerDay'>, bounds?: SurfaceBudgetBounds): AdmissionEnvelope;
35
35
  //# sourceMappingURL=surface-budget.d.ts.map
@@ -31,6 +31,16 @@ export function surfaceEnvelope(base, embed, bounds) {
31
31
  turnsPerAddressHour: Math.min(embed.turnsPerAddressHour, effectiveBase.turnsPerAddressHour),
32
32
  }
33
33
  : {}),
34
+ // The bridge bounds are the operator's to spend in either direction, like the daily caps and
35
+ // unlike the per-address ones: they govern this surface's own agent traffic against its own
36
+ // connectors, not a bound shared with everyone behind one address. `clamp` still holds them
37
+ // under the structural maximum.
38
+ ...(embed.bridgeToolCallsPerSession !== undefined
39
+ ? { bridgeToolCallsPerSession: embed.bridgeToolCallsPerSession }
40
+ : {}),
41
+ ...(embed.bridgeToolCallsPerDay !== undefined
42
+ ? { bridgeToolCallsPerDay: embed.bridgeToolCallsPerDay }
43
+ : {}),
34
44
  });
35
45
  if (bounds === undefined)
36
46
  return requested;
@@ -41,7 +41,7 @@
41
41
  "@noodle-borg/module": "0.0.0",
42
42
  "@noodle-borg/protocol": "0.0.0",
43
43
  "@noodle-borg/runtime": "0.0.0",
44
- "@noodleseed/assistant": "1.29.0",
44
+ "@noodleseed/assistant": "1.31.0",
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {
@@ -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',
@@ -140,6 +154,99 @@ const BUDGET_SUBCOMMAND = subcommand({
140
154
  }),
141
155
  ],
142
156
  });
157
+ const SPONSORSHIP_ACCOUNT_ARGUMENT = argument({
158
+ ...ARGUMENT_FIELDS,
159
+ name: 'billing-account-id',
160
+ type: 'string',
161
+ summary: 'Billing account receiving the sponsored allowance.',
162
+ required: true,
163
+ });
164
+ const SPONSORSHIP_FLAGS = [SERVICE_FLAG, AUTH_TOKEN_FLAG, JSON_FLAG];
165
+ const SPONSORSHIP_REASON_FLAG = flag({
166
+ ...FLAG_FIELDS,
167
+ name: 'reason',
168
+ type: 'string',
169
+ value: '<text>',
170
+ summary: 'Auditable reason for granting or revoking sponsored usage.',
171
+ required: true,
172
+ });
173
+ const SPONSORSHIP_IDEMPOTENCY_FLAG = flag({
174
+ ...FLAG_FIELDS,
175
+ name: 'idempotency-key',
176
+ type: 'string',
177
+ value: '<key>',
178
+ summary: 'Stable retry key for exactly-once mutation semantics.',
179
+ required: true,
180
+ });
181
+ const SPONSORSHIP_SUBCOMMAND = subcommand({
182
+ ...SUBCOMMAND_FIELDS,
183
+ name: 'sponsorship',
184
+ summary: 'Inspect sponsored managed-assistant usage or operate expiring bonus grants.',
185
+ subcommands: [
186
+ subcommand({
187
+ ...SUBCOMMAND_FIELDS,
188
+ name: 'inspect',
189
+ summary: 'Show one billing account’s allowance, usage, reset, and grants.',
190
+ arguments: [SPONSORSHIP_ACCOUNT_ARGUMENT],
191
+ flags: [...SPONSORSHIP_FLAGS],
192
+ jsonOutput: { mode: 'single' },
193
+ }),
194
+ subcommand({
195
+ ...SUBCOMMAND_FIELDS,
196
+ name: 'grant',
197
+ summary: 'Add a bounded daily allowance until an explicit expiry.',
198
+ arguments: [SPONSORSHIP_ACCOUNT_ARGUMENT],
199
+ flags: [
200
+ flag({
201
+ ...FLAG_FIELDS,
202
+ name: 'turns-per-day',
203
+ type: 'integer',
204
+ value: '<count>',
205
+ summary: 'Additional sponsored turns per UTC day.',
206
+ required: true,
207
+ constraints: { minimum: 1, maximum: 100_000 },
208
+ }),
209
+ flag({
210
+ ...FLAG_FIELDS,
211
+ name: 'starts-at',
212
+ type: 'string',
213
+ value: '<timestamp>',
214
+ summary: 'Optional ISO timestamp; defaults to now.',
215
+ constraints: { default: 'now' },
216
+ }),
217
+ flag({
218
+ ...FLAG_FIELDS,
219
+ name: 'expires-at',
220
+ type: 'string',
221
+ value: '<timestamp>',
222
+ summary: 'Required ISO timestamp after which the grant stops applying.',
223
+ required: true,
224
+ }),
225
+ SPONSORSHIP_REASON_FLAG,
226
+ SPONSORSHIP_IDEMPOTENCY_FLAG,
227
+ ...SPONSORSHIP_FLAGS,
228
+ ],
229
+ jsonOutput: { mode: 'single' },
230
+ }),
231
+ subcommand({
232
+ ...SUBCOMMAND_FIELDS,
233
+ name: 'revoke',
234
+ summary: 'Revoke one bonus grant without deleting its audit history.',
235
+ arguments: [
236
+ SPONSORSHIP_ACCOUNT_ARGUMENT,
237
+ argument({
238
+ ...ARGUMENT_FIELDS,
239
+ name: 'grant-id',
240
+ type: 'string',
241
+ summary: 'Managed-assistant sponsorship grant identifier.',
242
+ required: true,
243
+ }),
244
+ ],
245
+ flags: [SPONSORSHIP_REASON_FLAG, SPONSORSHIP_IDEMPOTENCY_FLAG, ...SPONSORSHIP_FLAGS],
246
+ jsonOutput: { mode: 'single' },
247
+ }),
248
+ ],
249
+ });
143
250
  const CLIENTS_SUBCOMMAND = subcommand({
144
251
  ...SUBCOMMAND_FIELDS,
145
252
  name: 'clients',
@@ -255,6 +362,7 @@ export const ASSISTANT_COMMAND = {
255
362
  CLIENTS_SUBCOMMAND,
256
363
  EMBEDS_SUBCOMMAND,
257
364
  BUDGET_SUBCOMMAND,
365
+ SPONSORSHIP_SUBCOMMAND,
258
366
  subcommand({
259
367
  ...SUBCOMMAND_FIELDS,
260
368
  name: 'usage',
@@ -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
  })
@@ -4,6 +4,7 @@ export * from './deployment-activation.js';
4
4
  export * from './deployment-automation.js';
5
5
  export * from './endpoint-url.js';
6
6
  export * from './intent.js';
7
+ export * from './logger.js';
7
8
  export * from './organization-provisioning.js';
8
9
  export * from './platform-human-identity.js';
9
10
  export * from './request-analytics.js';
@@ -4,6 +4,7 @@ export * from './deployment-activation.js';
4
4
  export * from './deployment-automation.js';
5
5
  export * from './endpoint-url.js';
6
6
  export * from './intent.js';
7
+ export * from './logger.js';
7
8
  export * from './organization-provisioning.js';
8
9
  export * from './platform-human-identity.js';
9
10
  export * from './request-analytics.js';
@@ -0,0 +1,15 @@
1
+ import type { ModuleLogger } from './contract.js';
2
+ export type ModuleLogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ export type ModuleLogFields = Readonly<Record<string, unknown>>;
4
+ /** Adapt a module host logger to the child-capable shape used by service integrations. */
5
+ export interface AdaptedModuleLogger {
6
+ readonly level: ModuleLogLevel;
7
+ log(level: ModuleLogLevel, event: string, fields?: ModuleLogFields): void;
8
+ debug(event: string, fields?: ModuleLogFields): void;
9
+ info(event: string, fields?: ModuleLogFields): void;
10
+ warn(event: string, fields?: ModuleLogFields): void;
11
+ error(event: string, fields?: ModuleLogFields): void;
12
+ child(fields: ModuleLogFields): AdaptedModuleLogger;
13
+ }
14
+ export declare function adaptModuleLogger(logger: ModuleLogger, base?: ModuleLogFields): AdaptedModuleLogger;
15
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1,16 @@
1
+ export function adaptModuleLogger(logger, base = {}) {
2
+ const fields = (extra) => ({ ...base, ...extra });
3
+ const log = (level, event, extra) => {
4
+ logger[level](event, fields(extra));
5
+ };
6
+ return {
7
+ level: 'debug',
8
+ log,
9
+ debug: (event, extra) => log('debug', event, extra),
10
+ info: (event, extra) => log('info', event, extra),
11
+ warn: (event, extra) => log('warn', event, extra),
12
+ error: (event, extra) => log('error', event, extra),
13
+ child: (extra) => adaptModuleLogger(logger, fields(extra)),
14
+ };
15
+ }
16
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,47 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { captureAssistantUsage } from './assistant-usage.js';
3
+ /** Project one apps-bridge tool call onto the shared tenant request stream. */
4
+ export function assistantAppToolCallRequestEvent(session, input) {
5
+ const isPublic = session.publicEmbedId !== undefined;
6
+ return {
7
+ ...session.tenant,
8
+ deploymentId: session.deploymentId,
9
+ requestId: randomUUID(),
10
+ sessionId: session.id,
11
+ sessionSource: 'synthetic',
12
+ subjectKind: isPublic ? 'anonymous' : 'authenticated',
13
+ accessMode: isPublic ? 'public' : 'authenticated',
14
+ surface: input.bridged ? 'webmcp' : isPublic ? 'assistant-public' : 'assistant-authenticated',
15
+ method: 'tools/call',
16
+ kind: 'usage',
17
+ ...(input.toolName === undefined ? {} : { toolName: input.toolName }),
18
+ outcome: input.outcome === 'ok' ? 'ok' : input.outcome === 'refused' ? 'tool_error' : 'mcp_error',
19
+ ...(input.errorKind === undefined ? {} : { errorKind: input.errorKind }),
20
+ durationMs: Number.isFinite(input.durationMs) && input.durationMs > 0 ? input.durationMs : 0,
21
+ // Scalar-only, like every other assistant event: tool arguments, results, page context, and
22
+ // credentials have no field to enter through.
23
+ details: { eventKind: 'appToolCall', bridged: input.bridged },
24
+ };
25
+ }
26
+ /**
27
+ * Bind one apps-bridge tool call's fixed facts, leaving the caller a recorder it can run at any
28
+ * exit. The route reaches several of them -- a budget refusal before any resolution work, an
29
+ * unprojected tool, bad arguments, a raised confirmation, a result -- and assembling the event at
30
+ * each one is how one of them ends up forgotten, which is what happened to the budget refusal.
31
+ */
32
+ export function assistantAppToolCallRecorder(input) {
33
+ const startedAt = input.clock();
34
+ return (outcome, errorKind) => {
35
+ captureAssistantUsage(input.capture, assistantAppToolCallRequestEvent(input.session, {
36
+ // Absent when the caller named no tool. That row still matters: a nameless call that
37
+ // exhausts a budget is the shape abusive traffic takes, and it is the row an operator
38
+ // watching a browser agent would miss if a missing name suppressed the event.
39
+ ...(input.toolName === undefined ? {} : { toolName: input.toolName }),
40
+ bridged: input.bridged,
41
+ outcome,
42
+ ...(errorKind === undefined ? {} : { errorKind }),
43
+ durationMs: input.clock() - startedAt,
44
+ }));
45
+ };
46
+ }
47
+ //# 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';