@noodleseed/one 0.169.0 → 0.171.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 (189) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +6 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/capabilities.d.ts +19 -0
  5. package/dist/commands/capabilities.d.ts.map +1 -0
  6. package/dist/commands/capabilities.js +128 -0
  7. package/dist/commands/capabilities.js.map +1 -0
  8. package/dist/commands/catalog.d.ts.map +1 -1
  9. package/dist/commands/catalog.js +2 -1
  10. package/dist/commands/catalog.js.map +1 -1
  11. package/dist/commands/whatsapp-ops.d.ts +13 -0
  12. package/dist/commands/whatsapp-ops.d.ts.map +1 -0
  13. package/dist/commands/whatsapp-ops.js +244 -0
  14. package/dist/commands/whatsapp-ops.js.map +1 -0
  15. package/dist/commands/whatsapp-output.d.ts +3 -0
  16. package/dist/commands/whatsapp-output.d.ts.map +1 -0
  17. package/dist/commands/whatsapp-output.js +70 -0
  18. package/dist/commands/whatsapp-output.js.map +1 -0
  19. package/dist/dev.js +2 -0
  20. package/dist/dev.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/plugin-mode/plugin-operation-contract.d.ts +3 -3
  26. package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +8 -0
  27. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +1 -0
  28. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +2 -2
  29. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +5 -0
  30. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +3 -2
  31. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +6 -0
  32. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  33. package/node_modules/@noodle-borg/app-audit/dist/audit-embedded-assistant.js +2 -0
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +2 -1
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.d.ts +1 -0
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.js +4 -1
  37. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-model-context.js +9 -4
  38. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.d.ts +16 -0
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.js +19 -0
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +145 -0
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +378 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +160 -0
  43. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.d.ts +18 -0
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.js +69 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-spend.js +82 -0
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +42 -0
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.js +76 -0
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +134 -0
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.js +72 -0
  50. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +20 -0
  51. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +221 -0
  52. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +15 -0
  53. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +34 -0
  54. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +19 -1
  55. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +22 -6
  56. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +7 -0
  57. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +7 -0
  58. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-appearance-store.js +83 -0
  59. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-elevation.js +151 -0
  60. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interaction-writes.js +45 -0
  61. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interactions.js +340 -0
  62. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant.js +439 -0
  63. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-channel-store.js +102 -0
  64. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-continuity-store.js +159 -0
  65. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-elevation-store.js +110 -0
  66. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-embed-store.js +134 -0
  67. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres.js +11 -0
  68. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +8 -0
  69. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +23 -0
  70. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +19 -4
  71. package/node_modules/@noodle-borg/authoring/dist/assistant.js +51 -20
  72. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +2 -1
  73. package/node_modules/@noodle-borg/authoring/dist/index.js +2 -1
  74. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  75. package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +2 -1
  76. package/node_modules/@noodle-borg/authoring/dist/recording.js +29 -1
  77. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
  78. package/node_modules/@noodle-borg/authoring/dist/server.js +5 -1
  79. package/node_modules/@noodle-borg/authoring/dist/web-capabilities.d.ts +17 -0
  80. package/node_modules/@noodle-borg/authoring/dist/web-capabilities.js +15 -0
  81. package/node_modules/@noodle-borg/authoring/package.json +1 -0
  82. package/node_modules/@noodle-borg/capabilities/dist/index.js +7 -0
  83. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +121 -0
  84. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-observability.js +102 -0
  85. package/node_modules/@noodle-borg/cli-catalog/dist/index.js +1 -0
  86. package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +3 -0
  87. package/node_modules/@noodle-borg/compiler/dist/artifact/config.js +1 -1
  88. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +2 -0
  89. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
  90. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
  91. package/node_modules/@noodle-borg/compiler/dist/compile.js +2 -0
  92. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  93. package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js +18 -3
  94. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +106 -12
  95. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +60 -29
  96. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +1 -0
  97. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +19 -0
  98. package/node_modules/@noodle-borg/compiler/dist/web-capabilities.js +55 -0
  99. package/node_modules/@noodle-borg/compiler/package.json +1 -0
  100. package/node_modules/@noodle-borg/connector-http/dist/ssrf.js +5 -0
  101. package/node_modules/@noodle-borg/knowledge-crawl/dist/bounded-body.js +38 -0
  102. package/node_modules/@noodle-borg/knowledge-crawl/dist/first-party.js +2 -3
  103. package/node_modules/@noodle-borg/knowledge-crawl/dist/html-text.js +10 -2
  104. package/node_modules/@noodle-borg/knowledge-crawl/dist/index.d.ts +1 -0
  105. package/node_modules/@noodle-borg/knowledge-crawl/dist/index.js +1 -0
  106. package/node_modules/@noodle-borg/knowledge-crawl/dist/page-reader.d.ts +26 -0
  107. package/node_modules/@noodle-borg/knowledge-crawl/dist/page-reader.js +127 -0
  108. package/node_modules/@noodle-borg/knowledge-crawl/dist/robots.js +48 -23
  109. package/node_modules/@noodle-borg/knowledge-crawl/package.json +1 -0
  110. package/node_modules/@noodle-borg/managed-capabilities/dist/budget.d.ts +29 -0
  111. package/node_modules/@noodle-borg/managed-capabilities/dist/budget.js +124 -0
  112. package/node_modules/@noodle-borg/managed-capabilities/dist/contracts.d.ts +212 -0
  113. package/node_modules/@noodle-borg/managed-capabilities/dist/contracts.js +113 -0
  114. package/node_modules/@noodle-borg/managed-capabilities/dist/errors.d.ts +8 -0
  115. package/node_modules/@noodle-borg/managed-capabilities/dist/errors.js +21 -0
  116. package/node_modules/@noodle-borg/managed-capabilities/dist/executor.d.ts +26 -0
  117. package/node_modules/@noodle-borg/managed-capabilities/dist/executor.js +156 -0
  118. package/node_modules/@noodle-borg/managed-capabilities/dist/limits.d.ts +15 -0
  119. package/node_modules/@noodle-borg/managed-capabilities/dist/limits.js +15 -0
  120. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-contracts.d.ts +159 -0
  121. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-contracts.js +69 -0
  122. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-route.d.ts +5 -0
  123. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-route.js +160 -0
  124. package/node_modules/@noodle-borg/managed-capabilities/dist/policy-store.d.ts +34 -0
  125. package/node_modules/@noodle-borg/managed-capabilities/dist/policy-store.js +61 -0
  126. package/node_modules/@noodle-borg/managed-capabilities/dist/portable.d.ts +10 -0
  127. package/node_modules/@noodle-borg/managed-capabilities/dist/portable.js +10 -0
  128. package/node_modules/@noodle-borg/managed-capabilities/dist/service.d.ts +83 -0
  129. package/node_modules/@noodle-borg/managed-capabilities/dist/service.js +139 -0
  130. package/node_modules/@noodle-borg/managed-capabilities/dist/urls.d.ts +3 -0
  131. package/node_modules/@noodle-borg/managed-capabilities/dist/urls.js +30 -0
  132. package/node_modules/@noodle-borg/managed-capabilities/package.json +49 -0
  133. package/node_modules/@noodle-borg/module/dist/contract.d.ts +11 -0
  134. package/node_modules/@noodle-borg/module-audit/dist/index.js +37 -0
  135. package/node_modules/@noodle-borg/module-audit/dist/postgres-audit.js +126 -0
  136. package/node_modules/@noodle-borg/module-audit/dist/route.js +66 -0
  137. package/node_modules/@noodle-borg/runtime/dist/confirmation.js +29 -0
  138. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +2 -0
  139. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +3 -0
  140. package/node_modules/@noodle-borg/runtime/dist/execute.js +6 -1
  141. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +2 -1
  142. package/node_modules/@noodle-borg/runtime/dist/index.js +2 -1
  143. package/node_modules/@noodle-borg/runtime/dist/interactive.js +10 -1
  144. package/node_modules/@noodle-borg/runtime/dist/managed-origins.js +8 -4
  145. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +3 -0
  146. package/node_modules/@noodle-borg/runtime/dist/postgres-state-handles.js +311 -0
  147. package/node_modules/@noodle-borg/runtime/dist/postgres.js +4 -0
  148. package/node_modules/@noodle-borg/runtime/dist/result-meta.d.ts +2 -0
  149. package/node_modules/@noodle-borg/runtime/dist/result-meta.js +6 -0
  150. package/node_modules/@noodle-borg/runtime/dist/result.d.ts +4 -0
  151. package/node_modules/@noodle-borg/runtime/dist/web-capabilities.d.ts +71 -0
  152. package/node_modules/@noodle-borg/runtime/dist/web-capabilities.js +41 -0
  153. package/node_modules/@noodle-borg/runtime/package.json +1 -0
  154. package/node_modules/@noodle-borg/service/dist/assistant-composition.js +18 -0
  155. package/node_modules/@noodle-borg/service/dist/build-info.js +5 -2
  156. package/node_modules/@noodle-borg/service/dist/business-information/source-ingestion-coordinator.js +7 -0
  157. package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +233 -0
  158. package/node_modules/@noodle-borg/service/dist/channels/cipher.js +37 -0
  159. package/node_modules/@noodle-borg/service/dist/channels/composition.js +37 -0
  160. package/node_modules/@noodle-borg/service/dist/channels/inference-guard.js +53 -0
  161. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +337 -0
  162. package/node_modules/@noodle-borg/service/dist/channels/worker-loop.js +50 -0
  163. package/node_modules/@noodle-borg/service/dist/local-options.d.ts +1 -0
  164. package/node_modules/@noodle-borg/service/dist/local-web-capabilities.js +13 -0
  165. package/node_modules/@noodle-borg/service/dist/mcp-protocol-runtime.js +21 -0
  166. package/node_modules/@noodle-borg/service/dist/modules/route-dispatch.js +13 -0
  167. package/node_modules/@noodle-borg/service/dist/registry-compile.js +3 -0
  168. package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +1 -0
  169. package/node_modules/@noodle-borg/service/dist/registry.js +3 -0
  170. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +66 -35
  171. package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +7 -4
  172. package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +6 -2
  173. package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +14 -1
  174. package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +238 -0
  175. package/node_modules/@noodle-borg/service/dist/serve-local.js +14 -0
  176. package/node_modules/@noodle-borg/service/dist/serve-resource-auth.js +10 -0
  177. package/node_modules/@noodle-borg/service/dist/service.js +37 -33
  178. package/node_modules/@noodle-borg/service/dist/web-capabilities.js +39 -0
  179. package/node_modules/@noodle-borg/service/package.json +3 -1
  180. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +33 -0
  181. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +9 -0
  182. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +2 -0
  183. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +2 -0
  184. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.d.ts +2 -0
  185. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.js +10 -2
  186. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +426 -0
  187. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +213 -0
  188. package/node_modules/@noodle-borg/wire-contracts/package.json +1 -0
  189. package/package.json +3 -2
@@ -1,8 +1,9 @@
1
1
  import { ADMISSION_DEFAULTS } from '@noodle-borg/admission-limits/portable';
2
2
  import { assistantCoreModelMessages, assistantTurnModelContextMessages, requestAssistantSuggestedPrompts, requestModelCompletion, resolveAssistantContextProviderModelResult, } from '@noodle-borg/assistant-gateway/model-runtime';
3
- import { assistantGuideModelContext, assistantModelToolOncePerSession, assistantModelToolRequiredWhenVisible, assistantOmittedToolResult, assistantViewAvailableData, dispatchAssistantTool, projectAssistantGuide, publicSurfaceOf, recoverableAssistantView, remainingTurnTokens, selectAssistantModelTools, toolTouchesDelegatedAuth, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
3
+ import { assistantGuideModelContext, assistantModelToolOncePerSession, assistantModelToolRequiredWhenVisible, assistantOmittedToolResult, assistantViewAvailableData, dispatchAssistantTool, dispatchMessagingReadTool, isMessagingTurn, projectAssistantGuide, publicSurfaceOf, recoverableAssistantView, remainingTurnTokens, selectAssistantModelTools, toolTouchesDelegatedAuth, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
4
4
  import { validateJsonSchemaWithDefaults } from '@noodle-borg/compiler';
5
5
  import { guardedFetch } from '@noodle-borg/connector-http';
6
+ import { CapabilityBudget } from '@noodle-borg/managed-capabilities';
6
7
  import { evaluateToolAuthorization } from '@noodle-borg/protocol';
7
8
  import { interceptForElevation, offersSignIn } from './assistant-elevation.js';
8
9
  import { assistantKnowledgeModelTools, executeAssistantKnowledgeSearch, findAssistantKnowledgeComponent, KNOWLEDGE_CITATION_GUIDANCE, resolveAssistantKnowledge, } from './assistant-knowledge.js';
@@ -33,14 +34,15 @@ export function createAssistantTurnStats() {
33
34
  };
34
35
  }
35
36
  /** Run one model/tool loop against the immutable invocation-context snapshot for this turn. */
36
- export async function runAgentTurn(target, session, message, context, deps, emit, modelContext, pageContext, stats = createAssistantTurnStats(), suggestions = false) {
37
+ export async function runAgentTurn(target, session, message, context, deps, emit, modelContext, pageContext, stats = createAssistantTurnStats(), suggestions = false, transport) {
37
38
  // Every terminal failure in this loop is one shape: a code, and the turn ends. Naming it keeps the
38
39
  // dozen sites readable and stops a new one inventing a different envelope.
39
40
  const fail = (code) => emit({ event: 'error', data: { code } });
40
41
  const assistant = target.served.artifact.server.assistant;
41
42
  if (!assistant)
42
43
  return fail('assistant_unavailable');
43
- const binding = await resolveAssistantModelBinding(target, session.tenant, session.deploymentId, deps);
44
+ const binding = transport?.binding ??
45
+ (await resolveAssistantModelBinding(target, session.tenant, session.deploymentId, deps));
44
46
  if (!binding) {
45
47
  return emit({
46
48
  event: 'error',
@@ -98,12 +100,15 @@ export async function runAgentTurn(target, session, message, context, deps, emit
98
100
  // Across every step of this turn, not per step: eight tool calls is eight, however the model splits
99
101
  // them, or a model that loops one call per step would spend the budget a step at a time.
100
102
  let toolCallsThisTurn = 0;
103
+ const capabilityBudget = new CapabilityBudget();
101
104
  let omittedToolRecoveries = 0;
102
105
  let remainingTokens = binding.requestPolicy?.maxTokensPerTurn;
103
106
  const turnSignal = binding.requestPolicy?.maxTurnMs === undefined
104
107
  ? undefined
105
108
  : AbortSignal.timeout(binding.requestPolicy.maxTurnMs);
106
109
  for (let step = 0; step < bounds.steps; step += 1) {
110
+ await transport?.beforeStep();
111
+ transport?.signal.throwIfAborted();
107
112
  if (remainingTokens !== undefined && remainingTokens <= 0)
108
113
  return fail('model_token_budget_exhausted');
109
114
  const requestTokenLimit = Math.min(binding.requestPolicy?.maxCompletionTokens ?? Number.MAX_SAFE_INTEGER, remainingTokens ?? Number.MAX_SAFE_INTEGER);
@@ -116,7 +121,11 @@ export async function runAgentTurn(target, session, message, context, deps, emit
116
121
  streamedContent += delta;
117
122
  emit({ event: 'content', data: { delta } });
118
123
  }
119
- : () => undefined, requiredToolForStep === undefined ? assistantKnowledgeModelTools(knowledge) : [], stepModelTools, requestTokenLimit === Number.MAX_SAFE_INTEGER ? undefined : requestTokenLimit, turnSignal, requiredToolForStep === undefined ? undefined : 'required');
124
+ : () => undefined, requiredToolForStep === undefined ? assistantKnowledgeModelTools(knowledge) : [], stepModelTools, requestTokenLimit === Number.MAX_SAFE_INTEGER ? undefined : requestTokenLimit, transport
125
+ ? turnSignal
126
+ ? AbortSignal.any([transport.signal, turnSignal])
127
+ : transport.signal
128
+ : turnSignal, requiredToolForStep === undefined ? undefined : 'required');
120
129
  stats.promptTokens += completion.usage?.promptTokens ?? 0;
121
130
  stats.completionTokens += completion.usage?.completionTokens ?? 0;
122
131
  stats.reasoningTokens += completion.usage?.reasoningTokens ?? 0;
@@ -135,7 +144,10 @@ export async function runAgentTurn(target, session, message, context, deps, emit
135
144
  const assistantContent = response.content || streamedContent;
136
145
  if (assistantContent)
137
146
  messages.push({ role: 'assistant', content: assistantContent });
138
- if (suggestions && step + 1 < bounds.steps && (remainingTokens ?? 1) > 0) {
147
+ if (!isMessagingTurn(session) &&
148
+ suggestions &&
149
+ step + 1 < bounds.steps &&
150
+ (remainingTokens ?? 1) > 0) {
139
151
  try {
140
152
  const prompts = await requestAssistantSuggestedPrompts(binding, messages, assistantModelFetcher(deps.modelFetch), stats, remainingTokens, turnSignal);
141
153
  if (prompts.length > 0) {
@@ -162,6 +174,7 @@ export async function runAgentTurn(target, session, message, context, deps, emit
162
174
  stats.toolCalls += 1;
163
175
  if (toolCallsThisTurn > bounds.toolCalls)
164
176
  return fail('tool_call_budget_exhausted');
177
+ await transport?.beforeStep();
165
178
  const knowledgeComponent = findAssistantKnowledgeComponent(knowledge, call.function.name);
166
179
  if (knowledge !== undefined && knowledgeComponent !== undefined) {
167
180
  let knowledgeArgs;
@@ -203,21 +216,23 @@ export async function runAgentTurn(target, session, message, context, deps, emit
203
216
  // The hosted broker exposes its delegated binding keys so the intercept can classify on
204
217
  // connector auth kind too; a broker without the method leaves the base classification alone.
205
218
  const delegatedKeys = target.served.deps.broker.assistantDelegatedAuthKeys?.();
206
- const elevation = await interceptForElevation({
207
- tool,
208
- session,
209
- assistant,
210
- ...(target.served.artifact.server.state === undefined
211
- ? {}
212
- : { state: target.served.artifact.server.state }),
213
- elevations: deps.elevations,
214
- ...(delegatedKeys !== undefined && delegatedKeys.size > 0
215
- ? {
216
- requiresDelegatedIdentity: (candidate) => toolTouchesDelegatedAuth(candidate, delegatedKeys),
217
- }
218
- : {}),
219
- now: deps.clock?.() ?? new Date(),
220
- });
219
+ const elevation = isMessagingTurn(session)
220
+ ? undefined
221
+ : await interceptForElevation({
222
+ tool,
223
+ session,
224
+ assistant,
225
+ ...(target.served.artifact.server.state === undefined
226
+ ? {}
227
+ : { state: target.served.artifact.server.state }),
228
+ elevations: deps.elevations,
229
+ ...(delegatedKeys !== undefined && delegatedKeys.size > 0
230
+ ? {
231
+ requiresDelegatedIdentity: (candidate) => toolTouchesDelegatedAuth(candidate, delegatedKeys),
232
+ }
233
+ : {}),
234
+ now: deps.clock?.() ?? new Date(),
235
+ });
221
236
  if (elevation) {
222
237
  stats.interactionCount += 1;
223
238
  emit(elevation.event);
@@ -243,19 +258,34 @@ export async function runAgentTurn(target, session, message, context, deps, emit
243
258
  if (coerced.issues.length > 0)
244
259
  return fail('invalid_tool_arguments');
245
260
  args = coerced.value;
246
- const dispatch = await dispatchAssistantTool({
247
- artifact: target.served.artifact,
248
- tool,
249
- arguments: args,
250
- executeDeps: withAssistantSessionExecutionAuthority(target.served.deps, target.served.artifact, session),
251
- caller: session.caller,
252
- context,
253
- session,
254
- store: deps.store,
255
- audit: deps.audit,
256
- now: () => deps.clock?.() ?? new Date(),
257
- onToolStarted: () => emit({ event: 'tool_started', data: { id: call.id, tool: tool.name } }),
258
- });
261
+ await transport?.beforeStep();
262
+ const dispatch = isMessagingTurn(session)
263
+ ? await dispatchMessagingReadTool({
264
+ artifact: target.served.artifact,
265
+ tool,
266
+ arguments: args,
267
+ executeDeps: {
268
+ ...target.served.deps,
269
+ capabilityBudget,
270
+ ...(transport?.signal ? { signal: transport.signal } : {}),
271
+ },
272
+ context,
273
+ session,
274
+ claimTool: transport?.claimTool ?? (async () => false),
275
+ })
276
+ : await dispatchAssistantTool({
277
+ artifact: target.served.artifact,
278
+ tool,
279
+ arguments: args,
280
+ executeDeps: withAssistantSessionExecutionAuthority({ ...target.served.deps, capabilityBudget }, target.served.artifact, session),
281
+ caller: session.caller,
282
+ context,
283
+ session,
284
+ store: deps.store,
285
+ audit: deps.audit,
286
+ now: () => deps.clock?.() ?? new Date(),
287
+ onToolStarted: () => emit({ event: 'tool_started', data: { id: call.id, tool: tool.name } }),
288
+ });
259
289
  if (assistantModelToolOncePerSession(tool) &&
260
290
  (dispatch.kind !== 'event' || dispatch.event !== 'error')) {
261
291
  modelTools = modelTools.filter((candidate) => candidate.name !== tool.name);
@@ -272,8 +302,9 @@ export async function runAgentTurn(target, session, message, context, deps, emit
272
302
  result: dispatch.output,
273
303
  arguments: args,
274
304
  }, (failure) => deps.logger?.warn('assistant.view.unresolved', { ...failure }));
275
- if (view) {
276
- await deps.store.replaceLatestView(session.id, recoverableAssistantView(view));
305
+ if (view && !isMessagingTurn(session)) {
306
+ if (!dispatch.ephemeral)
307
+ await deps.store.replaceLatestView(session.id, recoverableAssistantView(view));
277
308
  emit({ event: 'view_available', data: { ...view } });
278
309
  }
279
310
  messages.push({
@@ -1,5 +1,5 @@
1
1
  import { AssistantInteractionCapacityError, assistantConfirmationReview, assistantConfirmationTransition, assistantInputTransition, assistantInteractionTool, assistantPreparedArgumentReview, assistantPreparedToolContinuation, assistantSafeOutput, assistantToolContinuation, assistantViewAvailableData, auditAssistantInteraction, boundedAssistantExecution, recoverableAssistantView, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
2
- import { executePreparedTool, executeToolInteractive, resumeTool, resumeToolPreparation, } from '@noodle-borg/runtime';
2
+ import { executePreparedTool, executeToolInteractive, hasEphemeralEvidence, resumeTool, resumeToolPreparation, } from '@noodle-borg/runtime';
3
3
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
4
4
  import { applyBrowserCors, authenticateSession, now, } from './assistant.js';
5
5
  import { replayOrConflict, writeUnknownOutcome } from './assistant-interaction-replay.js';
@@ -347,7 +347,9 @@ async function finishExecution(res, deps, session, interaction, context, result,
347
347
  }
348
348
  const completedTool = target.served.artifact.tools.find((tool) => tool.name === toolName);
349
349
  const safeOutput = assistantSafeOutput(completedTool?.outputSchema, result.output);
350
- const completed = await completeAssistantInteractionExecution(deps, session, interaction, 'succeeded', 'tool_completed', toolName, { result: safeOutput });
350
+ const ephemeral = hasEphemeralEvidence(result.output);
351
+ const retainedOutput = ephemeral ? { evidenceRetained: false } : safeOutput;
352
+ const completed = await completeAssistantInteractionExecution(deps, session, interaction, 'succeeded', 'tool_completed', toolName, { result: retainedOutput });
351
353
  if (!completed)
352
354
  return writeUnknownOutcome(res, deps, session, interaction, 'accept');
353
355
  await auditAssistantInteraction({
@@ -364,7 +366,7 @@ async function finishExecution(res, deps, session, interaction, context, result,
364
366
  // Model-facing scaffolding: the panel saw the tool_completed card, never this row's tool JSON.
365
367
  {
366
368
  role: 'assistant',
367
- content: `Completed ${toolName}: ${JSON.stringify(safeOutput)}`,
369
+ content: `Completed ${toolName}: ${JSON.stringify(retainedOutput)}`,
368
370
  kind: 'narration',
369
371
  },
370
372
  ]);
@@ -377,7 +379,8 @@ async function finishExecution(res, deps, session, interaction, context, result,
377
379
  result: safeOutput,
378
380
  }, (failure) => deps.logger?.warn('assistant.view.unresolved', { ...failure }));
379
381
  if (view) {
380
- await deps.store.replaceLatestView(session.id, recoverableAssistantView(view));
382
+ if (!ephemeral)
383
+ await deps.store.replaceLatestView(session.id, recoverableAssistantView(view));
381
384
  writeViewAvailable(res, view);
382
385
  }
383
386
  await narrateResolvedInteraction(res, deps, target, session, toolName, 'accept', safeOutput, context, suggestions);
@@ -1,11 +1,15 @@
1
1
  import { resolveManagedVariablesInString } from '../managed-config-expressions.js';
2
2
  import { resolveConfigScope } from '../store.js';
3
- export async function resolveAssistantModelBinding(target, tenant, deploymentId, deps) {
3
+ export async function resolveAssistantModelBinding(target, tenant, deploymentId, deps, channel) {
4
4
  const declaration = target.served.artifact.server.assistant?.model;
5
5
  if (declaration === undefined)
6
6
  return undefined;
7
7
  if (declaration.kind === 'noodle-managed') {
8
- return deps.managedModelResolver?.resolve({ tenant, deploymentId });
8
+ return deps.managedModelResolver?.resolve({
9
+ tenant,
10
+ deploymentId,
11
+ ...(channel ? { channel } : {}),
12
+ });
9
13
  }
10
14
  const scope = resolveConfigScope(tenant);
11
15
  const [variables, secrets] = await Promise.all([
@@ -59,7 +59,20 @@ export async function projectBusinessChannels(installation, canEdit, base, deps)
59
59
  };
60
60
  const registered = await deps.registry.getActiveByTenant(tenant);
61
61
  const target = registered && (await resolveTargetOrigins(registered));
62
- const shared = { revision: installation.revision, active: installation.intakeActive, canEdit };
62
+ const whatsapp = await deps.readWhatsApp?.(tenant);
63
+ const shared = {
64
+ revision: installation.revision,
65
+ active: installation.intakeActive,
66
+ canEdit,
67
+ ...(whatsapp
68
+ ? {
69
+ whatsapp: {
70
+ ...whatsapp,
71
+ ...(!installation.intakeActive ? { status: 'paused' } : {}),
72
+ },
73
+ }
74
+ : {}),
75
+ };
63
76
  if (!target?.deploymentId)
64
77
  return {
65
78
  ...shared,
@@ -0,0 +1,238 @@
1
+ import { ChannelError, channelParticipantId, recordChannelDelivery, } from '@noodle-borg/assistant-gateway/portable';
2
+ import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
3
+ import * as wire from '@noodle-borg/wire-contracts';
4
+ import { parseWhatsAppWebhook, verifyWhatsAppWebhookSecret, WHATSAPP_CALLBACK_HEADER, } from '../channels/360dialog.js';
5
+ import { authorizeTenantControl, developerGrantRouteAccess } from './control-plane.js';
6
+ const participant = /^p_[a-f0-9]{64}$/;
7
+ const eventId = /^e_[0-9_]+$/;
8
+ function eventView(event) {
9
+ return {
10
+ id: event.id,
11
+ participantId: event.participantId,
12
+ state: event.state,
13
+ eventAt: event.eventAt,
14
+ receivedAt: event.receivedAt,
15
+ attempts: event.attempts,
16
+ deploymentId: event.deploymentId,
17
+ ...(event.code ? { code: event.code } : {}),
18
+ };
19
+ }
20
+ function respond(res, schema, data) {
21
+ sendJson(res, 200, schema.parse({ ok: true, data }));
22
+ }
23
+ async function body(req, schema) {
24
+ const input = await readJsonBody(req, 16_384);
25
+ if (!input.ok)
26
+ throw new ChannelError('request_invalid');
27
+ const parsed = schema.safeParse(input.value);
28
+ if (!parsed.success)
29
+ throw new ChannelError('request_invalid');
30
+ return parsed.data;
31
+ }
32
+ export function dispatchWhatsAppRoutes(req, res, url, runtime, grants) {
33
+ const callback = /^\/v1\/channels\/whatsapp\/webhooks\/([a-z0-9-]+)$/.exec(url.pathname);
34
+ const operator = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/channels\/whatsapp(?:\/(.*))?$/.exec(url.pathname);
35
+ if (!callback && !operator)
36
+ return false;
37
+ res.setHeader('Cache-Control', 'no-store');
38
+ if (!runtime) {
39
+ sendJson(res, 503, {
40
+ error: 'WhatsApp is unavailable on this service',
41
+ code: 'whatsapp_unavailable',
42
+ });
43
+ return true;
44
+ }
45
+ const run = Promise.resolve().then(() => callback
46
+ ? webhook(req, res, callback[1], runtime)
47
+ : operate(req, res, url, {
48
+ org: decodeURIComponent(operator[1]),
49
+ app: decodeURIComponent(operator[2]),
50
+ env: decodeURIComponent(operator[3]),
51
+ }, operator[4] ?? '', runtime, grants));
52
+ void run.catch((error) => {
53
+ const code = error instanceof ChannelError ? error.code : 'channel_unavailable';
54
+ const status = code === 'channel_not_found'
55
+ ? 404
56
+ : [
57
+ 'revision_conflict',
58
+ 'idempotency_conflict',
59
+ 'asset_in_use',
60
+ 'not_ready',
61
+ 'deployment_changed',
62
+ ].includes(code)
63
+ ? 409
64
+ : code === 'channel_unavailable' || code === 'ingress_capacity'
65
+ ? 503
66
+ : 400;
67
+ if (!res.headersSent)
68
+ sendJson(res, status, { error: code, code });
69
+ });
70
+ return true;
71
+ }
72
+ async function webhook(req, res, id, runtime) {
73
+ if (req.method !== 'POST')
74
+ return sendJson(res, 405, { error: 'method_not_allowed' });
75
+ const binding = await runtime.channels.internal(id);
76
+ const { webhookSecret } = await runtime.provider(binding);
77
+ if (!verifyWhatsAppWebhookSecret(req.headers[WHATSAPP_CALLBACK_HEADER], webhookSecret))
78
+ return sendJson(res, 401, { error: 'unauthorized' });
79
+ const input = await readJsonBody(req, 1 << 20);
80
+ if (!input.ok)
81
+ return sendJson(res, input.status, { error: 'webhook_invalid' });
82
+ const batch = parseWhatsAppWebhook(input.value, binding.phoneNumberId);
83
+ await runtime.channels.receive(id, batch.messages, await runtime.retention(binding));
84
+ for (const status of batch.statuses)
85
+ await recordChannelDelivery(runtime.channels.store, id, status.providerMessageId, status.state, runtime.channels.now());
86
+ sendJson(res, 200, { ok: true });
87
+ runtime.options.worker.wake();
88
+ }
89
+ async function operate(req, res, url, tenant, path, runtime, grants) {
90
+ const { deps, channels } = runtime;
91
+ const read = req.method === 'GET';
92
+ const identity = await authorizeTenantControl(req, res, deps.gate, deps.controlPlane, tenant.org, developerGrantRouteAccess(grants, read ? 'cloud:read' : 'config:write'));
93
+ if (identity === false)
94
+ return;
95
+ const member = await deps.controlPlane.getOrgMember({
96
+ org: tenant.org,
97
+ subject: identity.subject,
98
+ });
99
+ if (!read && !identity.superAdmin && member?.role !== 'owner' && member?.role !== 'developer')
100
+ return sendJson(res, 403, { error: 'operator_required' });
101
+ const key = req.headers['idempotency-key'];
102
+ if (!read && (typeof key !== 'string' || !key || key.length > 200))
103
+ throw new ChannelError('idempotency_key_invalid');
104
+ const idempotency = typeof key === 'string' ? key : '';
105
+ const visible = await channels.get(tenant);
106
+ const audit = async (action) => deps.audit.emit({
107
+ eventType: `assistant.channel.${action}`,
108
+ ...tenant,
109
+ actorSubject: identity.subject,
110
+ decision: 'allow',
111
+ status: 200,
112
+ });
113
+ if (!path && req.method === 'GET')
114
+ return respond(res, wire.WhatsAppBindingResponseSchema, visible ?? null);
115
+ if (!path && req.method === 'PUT') {
116
+ const parsed = await body(req, wire.WhatsAppConfigureRequestSchema);
117
+ const target = await deps.registry.getActiveByTenant(tenant);
118
+ if (!target?.deploymentId)
119
+ throw new ChannelError('deployment_missing');
120
+ const { expectedRevision, ...configuration } = parsed;
121
+ const configured = await channels.configure({ ...configuration, tenant, deploymentId: target.deploymentId }, identity.subject, idempotency, expectedRevision);
122
+ await audit('configured');
123
+ return respond(res, wire.WhatsAppBindingResponseSchema, configured);
124
+ }
125
+ if (!visible)
126
+ throw new ChannelError('channel_not_found');
127
+ const id = visible.id;
128
+ if (path === 'readiness' && req.method === 'POST') {
129
+ await body(req, wire.WhatsAppEmptyRequestSchema);
130
+ return respond(res, wire.WhatsAppReadinessResponseSchema, await runtime.doctor(id, req));
131
+ }
132
+ if ((path === 'state' && req.method === 'PATCH') || (!path && req.method === 'DELETE')) {
133
+ const parsed = path
134
+ ? await body(req, wire.WhatsAppStateRequestSchema)
135
+ : {
136
+ ...(await body(req, wire.WhatsAppRevisionRequestSchema)),
137
+ state: 'disconnected',
138
+ };
139
+ const updated = await channels.setState(id, parsed.state, identity.subject, idempotency, parsed.expectedRevision);
140
+ await audit(parsed.state);
141
+ return respond(res, wire.WhatsAppBindingResponseSchema, updated);
142
+ }
143
+ if (path === 'limits' && req.method === 'GET')
144
+ return respond(res, wire.WhatsAppBindingResponseSchema, visible);
145
+ if (path === 'limits' && req.method === 'PATCH') {
146
+ const parsed = await body(req, wire.WhatsAppLimitsRequestSchema);
147
+ const updated = await channels.updateLimits(id, parsed.limits, identity.subject, idempotency, parsed.expectedRevision);
148
+ await audit('limits_changed');
149
+ return respond(res, wire.WhatsAppBindingResponseSchema, updated);
150
+ }
151
+ if (path === 'usage' && read)
152
+ return respond(res, wire.WhatsAppUsageResponseSchema, await channels.usage(id));
153
+ if (path === 'webhook' && (read || req.method === 'POST')) {
154
+ if (!read) {
155
+ const parsed = await body(req, wire.WhatsAppRevisionRequestSchema);
156
+ if (parsed.expectedRevision !== visible.revision)
157
+ throw new ChannelError('revision_conflict');
158
+ await runtime.configureWebhook(id, req);
159
+ await audit('webhook_configured');
160
+ }
161
+ return respond(res, wire.WhatsAppWebhookResponseSchema, await runtime.webhook(id, req));
162
+ }
163
+ if (path === 'blocks' && read) {
164
+ const after = url.searchParams.get('after') ?? undefined;
165
+ if (after && !participant.test(after))
166
+ throw new ChannelError('request_invalid');
167
+ return sendJson(res, 200, wire.WhatsAppBlocksResponseSchema.parse({ ok: true, ...(await runtime.blocks(id, after)) }));
168
+ }
169
+ if (path === 'blocks' && req.method === 'POST') {
170
+ const parsed = await body(req, wire.WhatsAppBlockRequestSchema);
171
+ const participantId = parsed.participantId ??
172
+ channelParticipantId(await channels.internal(id), {
173
+ kind: 'phone',
174
+ value: parsed.phone.slice(1),
175
+ });
176
+ await channels.block(id, participantId, identity.subject, idempotency, parsed.until);
177
+ if (parsed.scope === 'provider')
178
+ await runtime.providerBlock(id, participantId, true, identity.subject, idempotency);
179
+ await audit('blocked');
180
+ return sendJson(res, 200, wire.WhatsAppMutationResponseSchema.parse({ ok: true }));
181
+ }
182
+ const [kind, value, action] = path.split('/');
183
+ if (kind === 'blocks' && value && participant.test(value) && req.method === 'DELETE') {
184
+ const parsed = await body(req, wire.WhatsAppUnblockRequestSchema);
185
+ if (parsed.scope === 'provider')
186
+ await runtime.providerBlock(id, value, false, identity.subject, idempotency, parsed.phone);
187
+ else
188
+ await channels.unblock(id, value, identity.subject, idempotency);
189
+ await audit('unblocked');
190
+ return sendJson(res, 200, { ok: true });
191
+ }
192
+ if (kind === 'cooldowns' &&
193
+ value &&
194
+ participant.test(value) &&
195
+ (read || req.method === 'DELETE')) {
196
+ if (!read) {
197
+ await body(req, wire.WhatsAppEmptyRequestSchema);
198
+ await channels.clearCooldown(id, value, identity.subject, idempotency);
199
+ await audit('cooldown_cleared');
200
+ }
201
+ return respond(res, wire.WhatsAppCooldownResponseSchema, await channels.cooldown(id, value));
202
+ }
203
+ if (kind === 'participants' &&
204
+ value &&
205
+ participant.test(value) &&
206
+ action === 'forget' &&
207
+ req.method === 'POST') {
208
+ await body(req, wire.WhatsAppEmptyRequestSchema);
209
+ await channels.forget(id, value, identity.subject, idempotency);
210
+ await audit('conversation_forgotten');
211
+ return sendJson(res, 200, { ok: true });
212
+ }
213
+ if (kind === 'events' &&
214
+ (!value || eventId.test(value)) &&
215
+ (read || (action === 'reconcile' && req.method === 'POST'))) {
216
+ if (!read) {
217
+ await body(req, wire.WhatsAppEmptyRequestSchema);
218
+ await audit('receipt_inspected');
219
+ }
220
+ if (value) {
221
+ const event = await channels.event(id, value);
222
+ if (!event)
223
+ throw new ChannelError('event_not_found');
224
+ return respond(res, wire.WhatsAppEventsResponseSchema, [eventView(event)]);
225
+ }
226
+ const after = url.searchParams.get('after') ?? undefined;
227
+ if (after && !eventId.test(after))
228
+ throw new ChannelError('cursor_invalid');
229
+ const events = await channels.events(id, after);
230
+ return sendJson(res, 200, wire.WhatsAppEventsResponseSchema.parse({
231
+ ok: true,
232
+ data: events.map(eventView),
233
+ ...(events.length === 100 ? { next: events.at(-1).id } : {}),
234
+ }));
235
+ }
236
+ sendJson(res, 404, { error: 'not_found' });
237
+ }
238
+ //# sourceMappingURL=whatsapp.js.map
@@ -3,6 +3,7 @@ import { InMemoryControlPlaneStore } from '@noodle-borg/control-plane/portable';
3
3
  import { InMemoryAssetStore } from './assets.js';
4
4
  import { createCustomerVerifierFactory, createHostedCustomerVerifierFactory, createLocalDevtoolsCustomerVerifierFactory, } from './customer-verifier.js';
5
5
  import { createLocalDevtoolsDelegatedCredentialSource } from './local-devtools-delegated-credentials.js';
6
+ import { localWebCapabilities } from './local-web-capabilities.js';
6
7
  import { ServerRegistry } from './registry.js';
7
8
  import { isLoopbackHost } from './serve-resource-auth.js';
8
9
  import { createServiceHandler } from './service.js';
@@ -48,7 +49,9 @@ export async function serveLocalService(options = {}) {
48
49
  const delegatedCredentials = directCustomerAuth
49
50
  ? createLocalDevtoolsDelegatedCredentialSource()
50
51
  : undefined;
52
+ const capabilities = options.runtime?.capabilities ?? localWebCapabilities();
51
53
  const registry = new ServerRegistry(undefined, undefined, configStore, {
54
+ capabilities,
52
55
  customerVerifierFactory,
53
56
  ...(delegatedCredentials === undefined
54
57
  ? {}
@@ -62,6 +65,17 @@ export async function serveLocalService(options = {}) {
62
65
  : { localDevtoolsDelegatedExchange: options.localDevtoolsDelegatedExchange }),
63
66
  });
64
67
  const handler = createServiceHandler(registry, {
68
+ capabilities,
69
+ deployGate: {
70
+ authorize: () => ({
71
+ ok: true,
72
+ identity: {
73
+ subject: 'local-operator',
74
+ email: 'local-operator@localhost',
75
+ superAdmin: true,
76
+ },
77
+ }),
78
+ },
65
79
  controlPlaneStore,
66
80
  configStore,
67
81
  assetStore: new InMemoryAssetStore(),
@@ -1,10 +1,20 @@
1
1
  import { resolveMcpSubdomainTenant } from '@noodle-borg/control-plane/portable';
2
2
  import { normalizePublicBaseDomain, parseCanonicalPublicMcpUrl, parseLegacyTenantMcpPath, } from '@noodle-borg/module';
3
3
  import { resolveTenantBridgeAuthVariables } from './managed-config-expressions.js';
4
+ import { createHostedMcpRequestStateManager } from './mcp-protocol-runtime.js';
4
5
  import { resolveConfigScope, } from './store.js';
5
6
  export function isLoopbackHost(host) {
6
7
  return host === '127.0.0.1' || host === '::1' || host === 'localhost';
7
8
  }
9
+ export function resolveMcpRequestState(options, secretBox) {
10
+ return (options.mcpRequestState ??
11
+ createHostedMcpRequestStateManager({
12
+ ...(options.wrappingMasterKey === undefined && options.secretMasterKey !== undefined
13
+ ? { secretMasterKey: options.secretMasterKey }
14
+ : {}),
15
+ ...(secretBox === undefined ? {} : { secretBox }),
16
+ }));
17
+ }
8
18
  export function assertLocalDevtoolsServiceBoundary(host, options) {
9
19
  if (options.localDevtoolsDirectFirebaseAuth === true && !isLoopbackHost(host)) {
10
20
  throw new Error('local Devtools Firebase verification requires a loopback service bind');