melchizedek-agents 0.18.0 → 0.19.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 (281) hide show
  1. package/.env.example +12 -0
  2. package/CHANGELOG.md +825 -0
  3. package/DOCUMENTATION.md +217 -59
  4. package/README.md +42 -1
  5. package/config/agents/examples/harness.yaml +3 -1
  6. package/config/agents/examples/model_zoo.yaml +10 -9
  7. package/config/agents/syndicateSchema.yaml +50 -16
  8. package/config/agents/templates/account_memory.yaml +2 -6
  9. package/config/agents/templates/case_desk.yaml +3 -9
  10. package/config/agents/templates/draft_review.yaml +3 -9
  11. package/config/agents/templates/intake_extractor.yaml +1 -3
  12. package/config/agents/templates/research_brief.yaml +3 -9
  13. package/config/agents/templates/research_desk.yaml +3 -9
  14. package/config/agents/templates/review_panel.yaml +4 -12
  15. package/config/agents/templates/support_triage.yaml +4 -12
  16. package/config/agents/templates/systems_operator.yaml +2 -6
  17. package/db/hardening.sql +7 -1
  18. package/db/migrations/0013_tool_credentials.sql +260 -0
  19. package/dist/lib/a2a/app.d.ts +70 -6
  20. package/dist/lib/a2a/app.js +107 -2
  21. package/dist/lib/a2a/executor.d.ts +20 -3
  22. package/dist/lib/a2a/executor.js +66 -7
  23. package/dist/lib/a2a/remoteAgent.d.ts +24 -9
  24. package/dist/lib/a2a/remoteAgent.js +31 -14
  25. package/dist/lib/compile.d.ts +121 -30
  26. package/dist/lib/compile.js +137 -124
  27. package/dist/lib/compileAdk.d.ts +29 -0
  28. package/dist/lib/compileAdk.js +132 -0
  29. package/dist/lib/compileNative.d.ts +85 -0
  30. package/dist/lib/compileNative.js +222 -0
  31. package/dist/lib/config.d.ts +6 -5
  32. package/dist/lib/config.js +6 -5
  33. package/dist/lib/doctor.js +1 -0
  34. package/dist/lib/index.d.ts +2 -1
  35. package/dist/lib/index.js +2 -1
  36. package/dist/lib/loadSyndicate.d.ts +18 -0
  37. package/dist/lib/memory/erase.d.ts +5 -3
  38. package/dist/lib/memory/erase.js +5 -2
  39. package/dist/lib/memory/namespace.d.ts +8 -1
  40. package/dist/lib/memory/namespace.js +12 -6
  41. package/dist/lib/memory/providers.js +2 -1
  42. package/dist/lib/memory/supabaseMemoryService.d.ts +37 -3
  43. package/dist/lib/memory/supabaseMemoryService.js +42 -2
  44. package/dist/lib/model.d.ts +47 -0
  45. package/dist/lib/model.js +37 -0
  46. package/dist/lib/models/adapterResolver.d.ts +113 -0
  47. package/dist/lib/models/adapterResolver.js +136 -0
  48. package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
  49. package/dist/lib/models/adkGeminiAdapter.js +367 -0
  50. package/dist/lib/models/adkShim.d.ts +111 -0
  51. package/dist/lib/models/adkShim.js +136 -0
  52. package/dist/lib/models/capabilities.d.ts +3 -0
  53. package/dist/lib/models/capabilities.js +20 -13
  54. package/dist/lib/models/chatCompletionsAdapter.d.ts +279 -0
  55. package/dist/lib/models/chatCompletionsAdapter.js +854 -0
  56. package/dist/lib/models/circuitBreaker.d.ts +42 -0
  57. package/dist/lib/models/circuitBreaker.js +70 -0
  58. package/dist/lib/models/claudeAdapter.d.ts +103 -0
  59. package/dist/lib/models/claudeAdapter.js +669 -0
  60. package/dist/lib/models/claudeLlm.d.ts +39 -62
  61. package/dist/lib/models/claudeLlm.js +44 -349
  62. package/dist/lib/models/claudeModels.d.ts +127 -0
  63. package/dist/lib/models/claudeModels.js +237 -0
  64. package/dist/lib/models/contract.d.ts +395 -0
  65. package/dist/lib/models/contract.js +39 -0
  66. package/dist/lib/models/endpoints.js +2 -1
  67. package/dist/lib/models/errorResponse.d.ts +50 -0
  68. package/dist/lib/models/errorResponse.js +48 -0
  69. package/dist/lib/models/fallback.d.ts +17 -10
  70. package/dist/lib/models/fallback.js +53 -42
  71. package/dist/lib/models/fallbackAdapter.d.ts +55 -0
  72. package/dist/lib/models/fallbackAdapter.js +62 -0
  73. package/dist/lib/models/gateway.js +2 -1
  74. package/dist/lib/models/gatewayAdapter.d.ts +51 -0
  75. package/dist/lib/models/gatewayAdapter.js +81 -0
  76. package/dist/lib/models/gatewayLlm.d.ts +12 -33
  77. package/dist/lib/models/gatewayLlm.js +14 -75
  78. package/dist/lib/models/geminiAdapter.d.ts +123 -0
  79. package/dist/lib/models/geminiAdapter.js +831 -0
  80. package/dist/lib/models/geminiState.d.ts +25 -0
  81. package/dist/lib/models/geminiState.js +25 -0
  82. package/dist/lib/models/genaiMapping.d.ts +263 -0
  83. package/dist/lib/models/genaiMapping.js +862 -0
  84. package/dist/lib/models/gptAdapter.d.ts +240 -0
  85. package/dist/lib/models/gptAdapter.js +804 -0
  86. package/dist/lib/models/gptLlm.d.ts +63 -123
  87. package/dist/lib/models/gptLlm.js +88 -602
  88. package/dist/lib/models/grokAdapter.d.ts +86 -0
  89. package/dist/lib/models/grokAdapter.js +142 -0
  90. package/dist/lib/models/grokLlm.d.ts +22 -45
  91. package/dist/lib/models/grokLlm.js +22 -67
  92. package/dist/lib/models/kimiAdapter.d.ts +157 -0
  93. package/dist/lib/models/kimiAdapter.js +203 -0
  94. package/dist/lib/models/kimiLlm.d.ts +15 -107
  95. package/dist/lib/models/kimiLlm.js +17 -153
  96. package/dist/lib/models/ollamaAdapter.d.ts +125 -0
  97. package/dist/lib/models/ollamaAdapter.js +155 -0
  98. package/dist/lib/models/ollamaLlm.d.ts +11 -107
  99. package/dist/lib/models/ollamaLlm.js +13 -151
  100. package/dist/lib/models/openAiCompatibleLlm.d.ts +73 -216
  101. package/dist/lib/models/openAiCompatibleLlm.js +109 -671
  102. package/dist/lib/models/providerState.d.ts +63 -0
  103. package/dist/lib/models/providerState.js +61 -0
  104. package/dist/lib/models/reasoning.d.ts +26 -0
  105. package/dist/lib/models/reasoning.js +59 -0
  106. package/dist/lib/models/registry.d.ts +58 -35
  107. package/dist/lib/models/registry.js +89 -118
  108. package/dist/lib/models/retry.d.ts +2 -1
  109. package/dist/lib/models/retry.js +3 -2
  110. package/dist/lib/models/schemaNormalize.d.ts +98 -0
  111. package/dist/lib/models/schemaNormalize.js +321 -1
  112. package/dist/lib/models/tracedGemini.d.ts +41 -0
  113. package/dist/lib/models/tracedGemini.js +98 -0
  114. package/dist/lib/models/urls.d.ts +11 -0
  115. package/dist/lib/models/urls.js +16 -0
  116. package/dist/lib/net/redirects.d.ts +21 -2
  117. package/dist/lib/net/redirects.js +11 -34
  118. package/dist/lib/observability/audit.d.ts +5 -1
  119. package/dist/lib/observability/lineage.d.ts +14 -0
  120. package/dist/lib/observability/lineage.js +33 -0
  121. package/dist/lib/observability/supabaseSpanExporter.d.ts +10 -4
  122. package/dist/lib/observability/supabaseSpanExporter.js +17 -9
  123. package/dist/lib/observability/tracer.d.ts +26 -8
  124. package/dist/lib/observability/tracer.js +43 -18
  125. package/dist/lib/runtime/adkMemoryBridge.d.ts +46 -0
  126. package/dist/lib/runtime/adkMemoryBridge.js +68 -0
  127. package/dist/lib/runtime/adkSessionBridge.d.ts +78 -0
  128. package/dist/lib/runtime/adkSessionBridge.js +176 -0
  129. package/dist/lib/runtime/credentials.d.ts +59 -0
  130. package/dist/lib/runtime/credentials.js +62 -0
  131. package/dist/lib/runtime/events.d.ts +229 -0
  132. package/dist/lib/runtime/events.js +274 -0
  133. package/dist/lib/runtime/logging.d.ts +32 -0
  134. package/dist/lib/runtime/logging.js +55 -0
  135. package/dist/lib/runtime/memoryService.d.ts +72 -0
  136. package/dist/lib/runtime/memoryService.js +25 -0
  137. package/dist/lib/runtime/native/agentLoop.d.ts +203 -0
  138. package/dist/lib/runtime/native/agentLoop.js +687 -0
  139. package/dist/lib/runtime/native/compaction.d.ts +103 -0
  140. package/dist/lib/runtime/native/compaction.js +191 -0
  141. package/dist/lib/runtime/native/delegate.d.ts +98 -0
  142. package/dist/lib/runtime/native/delegate.js +173 -0
  143. package/dist/lib/runtime/native/history.d.ts +83 -0
  144. package/dist/lib/runtime/native/history.js +312 -0
  145. package/dist/lib/runtime/native/interrupts.d.ts +126 -0
  146. package/dist/lib/runtime/native/interrupts.js +291 -0
  147. package/dist/lib/runtime/native/request.d.ts +215 -0
  148. package/dist/lib/runtime/native/request.js +585 -0
  149. package/dist/lib/runtime/native/selfCorrection.d.ts +106 -0
  150. package/dist/lib/runtime/native/selfCorrection.js +253 -0
  151. package/dist/lib/runtime/native/step.d.ts +148 -0
  152. package/dist/lib/runtime/native/step.js +239 -0
  153. package/dist/lib/runtime/native/taskMode.d.ts +64 -0
  154. package/dist/lib/runtime/native/taskMode.js +126 -0
  155. package/dist/lib/runtime/native/telemetry.d.ts +75 -0
  156. package/dist/lib/runtime/native/telemetry.js +299 -0
  157. package/dist/lib/runtime/native/tempState.d.ts +31 -0
  158. package/dist/lib/runtime/native/tempState.js +56 -0
  159. package/dist/lib/runtime/nativeTurn.d.ts +86 -0
  160. package/dist/lib/runtime/nativeTurn.js +107 -0
  161. package/dist/lib/runtime/questions.d.ts +29 -7
  162. package/dist/lib/runtime/questions.js +47 -32
  163. package/dist/lib/runtime/runtimeFlag.d.ts +30 -0
  164. package/dist/lib/runtime/runtimeFlag.js +50 -0
  165. package/dist/lib/runtime/sessions.d.ts +166 -0
  166. package/dist/lib/runtime/sessions.js +201 -0
  167. package/dist/lib/runtime/syndicateTurn.d.ts +41 -5
  168. package/dist/lib/runtime/syndicateTurn.js +202 -32
  169. package/dist/lib/session/supabaseSessionService.d.ts +64 -17
  170. package/dist/lib/session/supabaseSessionService.js +148 -105
  171. package/dist/lib/session/transcript.d.ts +68 -22
  172. package/dist/lib/session/transcript.js +102 -28
  173. package/dist/lib/skills.js +33 -7
  174. package/dist/lib/storage/postgres/credentialStore.d.ts +20 -0
  175. package/dist/lib/storage/postgres/credentialStore.js +82 -0
  176. package/dist/lib/storage/postgres/index.d.ts +12 -0
  177. package/dist/lib/storage/postgres/index.js +6 -1
  178. package/dist/lib/storage/postgres/sessionService.d.ts +43 -15
  179. package/dist/lib/storage/postgres/sessionService.js +128 -66
  180. package/dist/lib/syndicateSchema.d.ts +47 -0
  181. package/dist/lib/syndicateSchema.js +72 -3
  182. package/dist/lib/toolRegistry.d.ts +15 -2
  183. package/dist/lib/toolRegistry.js +33 -17
  184. package/dist/lib/tools/adkTool.d.ts +81 -0
  185. package/dist/lib/tools/adkTool.js +268 -0
  186. package/dist/lib/tools/auth.d.ts +118 -0
  187. package/dist/lib/tools/auth.js +89 -0
  188. package/dist/lib/tools/collectionsSearchTool.d.ts +7 -6
  189. package/dist/lib/tools/collectionsSearchTool.js +11 -26
  190. package/dist/lib/tools/credentialCipher.d.ts +58 -0
  191. package/dist/lib/tools/credentialCipher.js +116 -0
  192. package/dist/lib/tools/credentialStore.d.ts +77 -0
  193. package/dist/lib/tools/credentialStore.js +246 -0
  194. package/dist/lib/tools/examples.d.ts +43 -0
  195. package/dist/lib/tools/examples.js +62 -0
  196. package/dist/lib/tools/generateImageTool.d.ts +15 -6
  197. package/dist/lib/tools/generateImageTool.js +19 -29
  198. package/dist/lib/tools/inspectImageTool.d.ts +9 -6
  199. package/dist/lib/tools/inspectImageTool.js +13 -17
  200. package/dist/lib/tools/mcpToolFactory.d.ts +42 -1
  201. package/dist/lib/tools/mcpToolFactory.js +46 -47
  202. package/dist/lib/tools/memoryTools.d.ts +61 -0
  203. package/dist/lib/tools/memoryTools.js +119 -0
  204. package/dist/lib/tools/nativeTools.d.ts +38 -0
  205. package/dist/lib/tools/nativeTools.js +43 -0
  206. package/dist/lib/tools/oauthConsent.d.ts +181 -0
  207. package/dist/lib/tools/oauthConsent.js +330 -0
  208. package/dist/lib/tools/openapi/call.d.ts +108 -0
  209. package/dist/lib/tools/openapi/call.js +295 -0
  210. package/dist/lib/tools/openapi/parse.d.ts +147 -0
  211. package/dist/lib/tools/openapi/parse.js +570 -0
  212. package/dist/lib/tools/openapiTools.d.ts +25 -17
  213. package/dist/lib/tools/openapiTools.js +124 -121
  214. package/dist/lib/tools/scienceTools.d.ts +14 -14
  215. package/dist/lib/tools/skillToolset.d.ts +58 -87
  216. package/dist/lib/tools/skillToolset.js +61 -142
  217. package/dist/lib/tools/skills/env.d.ts +58 -0
  218. package/dist/lib/tools/skills/env.js +165 -0
  219. package/dist/lib/tools/skills/executor.d.ts +114 -0
  220. package/dist/lib/tools/skills/executor.js +331 -0
  221. package/dist/lib/tools/skills/frontmatter.d.ts +79 -0
  222. package/dist/lib/tools/skills/frontmatter.js +215 -0
  223. package/dist/lib/tools/skills/loader.d.ts +91 -0
  224. package/dist/lib/tools/skills/loader.js +257 -0
  225. package/dist/lib/tools/skills/tools.d.ts +187 -0
  226. package/dist/lib/tools/skills/tools.js +386 -0
  227. package/dist/lib/tools/taskTools.d.ts +15 -15
  228. package/dist/lib/tools/tool.d.ts +354 -0
  229. package/dist/lib/tools/tool.js +304 -0
  230. package/dist/lib/tools/toolContract.d.ts +89 -33
  231. package/dist/lib/tools/toolContract.js +104 -82
  232. package/dist/lib/tools/urlContextTool.d.ts +3 -0
  233. package/dist/lib/tools/urlContextTool.js +5 -1
  234. package/dist/lib/tools/webExtractTool.d.ts +2 -2
  235. package/dist/lib/tools/webExtractTool.js +2 -1
  236. package/dist/lib/tools/webSearchTool.d.ts +6 -9
  237. package/dist/lib/tools/webSearchTool.js +14 -59
  238. package/dist/lib/tools/wikiTools.d.ts +19 -19
  239. package/dist/lib/tools/wikiTools.js +13 -9
  240. package/dist/lib/tools/xApiSearchTool.d.ts +2 -2
  241. package/dist/lib/tools/xApiSearchTool.js +2 -1
  242. package/dist/lib/tools/xSearchTool.d.ts +7 -9
  243. package/dist/lib/tools/xSearchTool.js +11 -62
  244. package/dist/lib/tools/xaiSearchParams.d.ts +41 -0
  245. package/dist/lib/tools/xaiSearchParams.js +126 -0
  246. package/dist/lib/wiki/agentRun.d.ts +12 -0
  247. package/dist/lib/wiki/agentRun.js +72 -34
  248. package/dist/lib/wiki/entities.d.ts +12 -0
  249. package/dist/lib/wiki/entities.js +45 -3
  250. package/dist/lib/workflow/agentNode.d.ts +136 -0
  251. package/dist/lib/workflow/agentNode.js +274 -0
  252. package/dist/lib/workflow/graph.d.ts +177 -0
  253. package/dist/lib/workflow/graph.js +458 -0
  254. package/dist/lib/workflow/nodeEvents.d.ts +50 -0
  255. package/dist/lib/workflow/nodeEvents.js +101 -0
  256. package/dist/lib/workflow/pause.d.ts +122 -0
  257. package/dist/lib/workflow/pause.js +180 -0
  258. package/dist/lib/workflow/resume.d.ts +165 -0
  259. package/dist/lib/workflow/resume.js +440 -0
  260. package/dist/lib/workflow/route.d.ts +58 -0
  261. package/dist/lib/workflow/route.js +69 -0
  262. package/dist/lib/workflow/scheduler.d.ts +415 -0
  263. package/dist/lib/workflow/scheduler.js +743 -0
  264. package/dist/lib/workflow/toolNode.d.ts +127 -0
  265. package/dist/lib/workflow/toolNode.js +236 -0
  266. package/dist/lib/workflow/turn.d.ts +118 -0
  267. package/dist/lib/workflow/turn.js +216 -0
  268. package/dist/lib/workflowConfig.d.ts +3 -2
  269. package/dist/lib/workflowConfig.js +3 -12
  270. package/dist/scripts/a2a_server.js +4 -3
  271. package/dist/scripts/assistant_worker.d.ts +2 -1
  272. package/dist/scripts/assistant_worker.js +9 -4
  273. package/dist/scripts/syndicate_chat.js +19 -10
  274. package/package.json +4 -1
  275. package/skills/briefs/melchizedek-author.md +3 -2
  276. package/skills/briefs/melchizedek-models.md +13 -9
  277. package/skills/briefs/melchizedek.md +3 -3
  278. package/skills/melchizedek/SKILL.md +2 -1
  279. package/skills/melchizedek-author/SKILL.md +2 -1
  280. package/skills/melchizedek-author/assets/minimal.yaml +2 -0
  281. package/skills/melchizedek-models/SKILL.md +23 -6
@@ -0,0 +1,38 @@
1
+ /**
2
+ * lib/tools/nativeTools.ts — the server-side tools as the engine's own
3
+ * markers (ADR 0062).
4
+ *
5
+ * WHY this file exists:
6
+ * web_search, x_search, url_context and collections_search are run by the
7
+ * provider, not here: the model asks its own vendor to search or read, and
8
+ * the answer arrives inside the model's response. Each was an ADK BaseTool
9
+ * that declared nothing and left itself in the request for the adapter to
10
+ * find. The native runtime needs them without ADK, so each is first a
11
+ * NativeToolMarker (lib/tools/tool.ts): a name, a description for a reader,
12
+ * and the NativeTool it stands for. A marker declares no function; a
13
+ * request carries it in `nativeTools`, and each adapter adds its
14
+ * provider's tool object or drops it (lib/models/capabilities.ts).
15
+ *
16
+ * The ADK runtime keeps its sentinels (lib/tools/webSearchTool.ts and its
17
+ * siblings), which carry the same marker symbol, so every reader
18
+ * (nativeToolOf in lib/models/schemaNormalize.ts) recognises both by
19
+ * marker. lib/tools/adkTool.ts turns a marker into its sentinel
20
+ * (toAdkNativeTool). google_search is ADK's own GOOGLE_SEARCH on that
21
+ * runtime, recognised by ADK's marker.
22
+ *
23
+ * Code execution is not here: it is an agent's `code_execution: gemini`,
24
+ * not a tool it lists, and reaches the request as the agent's code
25
+ * executor.
26
+ *
27
+ * A LEAF: nothing in its import graph names @google/*.
28
+ */
29
+ /** The provider's own web search: Gemini grounding, Anthropic, OpenAI, xAI. */
30
+ export declare const WEB_SEARCH_MARKER: import("./tool.ts").NativeToolMarker;
31
+ /** xAI's live search over X posts. */
32
+ export declare const X_SEARCH_MARKER: import("./tool.ts").NativeToolMarker;
33
+ /** Gemini reads the pages at URLs in the conversation. */
34
+ export declare const URL_CONTEXT_MARKER: import("./tool.ts").NativeToolMarker;
35
+ /** xAI's semantic search over hosted document collections. */
36
+ export declare const COLLECTIONS_SEARCH_MARKER: import("./tool.ts").NativeToolMarker;
37
+ /** Gemini grounding, kept for YAMLs that name it. */
38
+ export declare const GOOGLE_SEARCH_MARKER: import("./tool.ts").NativeToolMarker;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * lib/tools/nativeTools.ts — the server-side tools as the engine's own
3
+ * markers (ADR 0062).
4
+ *
5
+ * WHY this file exists:
6
+ * web_search, x_search, url_context and collections_search are run by the
7
+ * provider, not here: the model asks its own vendor to search or read, and
8
+ * the answer arrives inside the model's response. Each was an ADK BaseTool
9
+ * that declared nothing and left itself in the request for the adapter to
10
+ * find. The native runtime needs them without ADK, so each is first a
11
+ * NativeToolMarker (lib/tools/tool.ts): a name, a description for a reader,
12
+ * and the NativeTool it stands for. A marker declares no function; a
13
+ * request carries it in `nativeTools`, and each adapter adds its
14
+ * provider's tool object or drops it (lib/models/capabilities.ts).
15
+ *
16
+ * The ADK runtime keeps its sentinels (lib/tools/webSearchTool.ts and its
17
+ * siblings), which carry the same marker symbol, so every reader
18
+ * (nativeToolOf in lib/models/schemaNormalize.ts) recognises both by
19
+ * marker. lib/tools/adkTool.ts turns a marker into its sentinel
20
+ * (toAdkNativeTool). google_search is ADK's own GOOGLE_SEARCH on that
21
+ * runtime, recognised by ADK's marker.
22
+ *
23
+ * Code execution is not here: it is an agent's `code_execution: gemini`,
24
+ * not a tool it lists, and reaches the request as the agent's code
25
+ * executor.
26
+ *
27
+ * A LEAF: nothing in its import graph names @google/*.
28
+ */
29
+ import { nativeToolMarker } from './tool.js';
30
+ /** The provider's own web search: Gemini grounding, Anthropic, OpenAI, xAI. */
31
+ export const WEB_SEARCH_MARKER = nativeToolMarker('web_search', "Web search via the agent model's native search capability " +
32
+ '(Gemini grounding / Anthropic web_search / OpenAI web_search / xAI web_search).');
33
+ /** xAI's live search over X posts. */
34
+ export const X_SEARCH_MARKER = nativeToolMarker('x_search', 'Live search over X (Twitter) posts via xAI Agent Tools — ' +
35
+ 'grok-* models only; a no-op sentinel on every other provider.');
36
+ /** Gemini reads the pages at URLs in the conversation. */
37
+ export const URL_CONTEXT_MARKER = nativeToolMarker('url_context', 'Gemini reads the pages at URLs in the conversation (server-side).');
38
+ /** xAI's semantic search over hosted document collections. */
39
+ export const COLLECTIONS_SEARCH_MARKER = nativeToolMarker('collections_search', 'Semantic search over xAI Collections (hosted document stores) via ' +
40
+ 'Agent Tools — grok-* models only; a no-op sentinel on every other ' +
41
+ 'provider. Collections are selected by XAI_COLLECTION_IDS.');
42
+ /** Gemini grounding, kept for YAMLs that name it. */
43
+ export const GOOGLE_SEARCH_MARKER = nativeToolMarker('google_search', 'Gemini grounding with Google Search.');
@@ -0,0 +1,181 @@
1
+ /**
2
+ * lib/tools/oauthConsent.ts — the consent step for a tool's OAuth grant: the
3
+ * authorization URL a paused call hands the person, and the callback that
4
+ * completes the authorization-code flow with PKCE (WS6-3b, ADR 0085).
5
+ *
6
+ * WHY this file exists:
7
+ * A tool that acts for a person needs their delegated access
8
+ * (`ctx.accessToken(provider)`, ADR 0072). When the person has not
9
+ * granted it yet, the call asks for it (`ctx.requestCredential(provider)`)
10
+ * and the turn pauses on ADK's own `adk_request_credential` call. The
11
+ * person opens the authorization URL, the provider redirects their browser
12
+ * to the server's callback route (lib/a2a/app.ts), and this module
13
+ * exchanges the code for tokens, server-side, and puts them in the
14
+ * credential store, sealed. The person's next message resumes the call.
15
+ *
16
+ * WHAT IT GUARANTEES:
17
+ * - The state nonce is 256 random bits, single-use, short-lived (10
18
+ * minutes by default) and bound to the app, the user, the session, the
19
+ * provider and the paused call. Only its SHA-256 is a key in the
20
+ * pending store: a dump of the store cannot complete a flow.
21
+ * - PKCE S256: the code verifier never leaves this process. It is not in
22
+ * the authorization URL, an event, a log line or the browser.
23
+ * - The redirect URI comes from configuration only. The callback never
24
+ * reads one from the request, and the token request repeats the
25
+ * configured one.
26
+ * - The code is exchanged here, server-side. The tokens go to the
27
+ * credential store and nowhere else: never into the callback's page, an
28
+ * event, a log line, an audit row or the model's context.
29
+ * - A refusal (`ConsentError`) names its reason and the provider, never a
30
+ * code, a token, a state or a provider's own error text, which may echo
31
+ * what it refused.
32
+ *
33
+ * MULTIPLE INSTANCES: the default pending store is this process's memory, so
34
+ * the callback must reach the instance that paused the call (sticky routing,
35
+ * as the A2A task store already needs). `ConsentStates` is the plug point
36
+ * for a shared store.
37
+ */
38
+ import type { AuditSink } from '../observability/audit.ts';
39
+ import type { CredentialStore } from './auth.ts';
40
+ /** One provider's OAuth client, as registered at the provider. */
41
+ export interface OAuthClientConfig {
42
+ /** The provider's authorization endpoint (https, or http on a loopback host). */
43
+ authorizationUrl: string;
44
+ /** The provider's token endpoint (https, or http on a loopback host). */
45
+ tokenUrl: string;
46
+ clientId: string;
47
+ /** A confidential client's secret, sent in the token request body. Omit for a public client (PKCE alone). */
48
+ clientSecret?: string;
49
+ /** The scopes the grant asks for. */
50
+ scopes?: string[];
51
+ /** Extra authorization parameters the provider needs (`access_type=offline`, say). Never `state`, `redirect_uri` or the PKCE pair. */
52
+ authorizationParams?: Record<string, string>;
53
+ }
54
+ export interface OAuthConsentOptions {
55
+ /** Each provider's client, by provider name (lowercase, as the credential store names it). */
56
+ providers: Record<string, OAuthClientConfig>;
57
+ /**
58
+ * The server's callback URL, exactly as registered at every provider
59
+ * (`https://agents.example.com/oauth/callback`). The A2A server mounts the
60
+ * callback route at its path. Never taken from a request.
61
+ */
62
+ redirectUri: string;
63
+ /** Where a completed grant is put. Its keys are the paused run's app, user and provider. */
64
+ credentials: Pick<CredentialStore, 'put'>;
65
+ /** The pending flows. Default: this process's memory (memoryConsentStates). */
66
+ states?: ConsentStates;
67
+ /** How long a flow may take, ms. Default 10 minutes. */
68
+ ttlMs?: number;
69
+ /** The token request's time limit, ms. Default 10 s. */
70
+ exchangeTimeoutMs?: number;
71
+ /** The audit trail (ADR 0042): one `consent.callback` row per completion or refusal, never a value. */
72
+ audit?: AuditSink;
73
+ /** For tests: the fetch the token request uses, and the clock. */
74
+ fetch?: typeof fetch;
75
+ now?: () => number;
76
+ }
77
+ /** Who and what a flow is for: the paused call it resumes. */
78
+ export interface ConsentBinding {
79
+ appName: string;
80
+ userId: string;
81
+ sessionId: string;
82
+ /** The paused call's id: the one the resume runs again. */
83
+ functionCallId: string;
84
+ provider: string;
85
+ }
86
+ /** A flow in progress. Holds the PKCE verifier, so it lives server-side only. */
87
+ export interface PendingFlow extends ConsentBinding {
88
+ codeVerifier: string;
89
+ scopes: string[];
90
+ /** Epoch ms after which the flow is refused. */
91
+ expiresAt: number;
92
+ }
93
+ /**
94
+ * Where flows wait for their callback, keyed by the state's SHA-256.
95
+ * `take` removes what it returns, so a state completes at most once.
96
+ */
97
+ export interface ConsentStates {
98
+ put(stateHash: string, flow: PendingFlow): Promise<void>;
99
+ take(stateHash: string): Promise<PendingFlow | undefined>;
100
+ }
101
+ /**
102
+ * Flows in this process's memory, at most `max` of them: the oldest is
103
+ * dropped first, and an expired one whenever another is put.
104
+ */
105
+ export declare function memoryConsentStates(options?: {
106
+ max?: number;
107
+ now?: () => number;
108
+ }): ConsentStates & {
109
+ size(): number;
110
+ };
111
+ export type ConsentErrorCode =
112
+ /** The callback's parameters are missing or malformed. */
113
+ 'invalid_request'
114
+ /** No flow has this state: tampered, invented, or already used. */
115
+ | 'unknown_state' | 'expired'
116
+ /** The callback's authenticated caller is not the user the flow is for. */
117
+ | 'wrong_user'
118
+ /** The person declined at the provider, or the provider refused the request. */
119
+ | 'denied'
120
+ /** The token endpoint refused the code or answered something unusable. */
121
+ | 'exchange_failed'
122
+ /** The grant could not be stored. */
123
+ | 'store_failed'
124
+ /** The provider has no client configured. */
125
+ | 'unknown_provider';
126
+ /** A consent step that cannot proceed. The message names the reason, never a value. */
127
+ export declare class ConsentError extends Error {
128
+ readonly code: ConsentErrorCode;
129
+ readonly provider?: string;
130
+ constructor(code: ConsentErrorCode, provider?: string);
131
+ }
132
+ /** What a paused call carries: the URL the person opens, and the stored request (no secret in either). */
133
+ export interface ConsentRequest {
134
+ provider: string;
135
+ /** The provider's authorization URL with client id, redirect URI, scopes, state and the S256 challenge. */
136
+ authUri: string;
137
+ /** The state nonce the URL carries. */
138
+ state: string;
139
+ scopes: string[];
140
+ /**
141
+ * ADK's AuthConfig for the `adk_request_credential` call, as ADK's
142
+ * generateAuthRequest shapes it, without the client secret and without the
143
+ * code verifier: `credentialKey`, `authScheme` (an oauth2
144
+ * authorizationCode flow) and `exchangedAuthCredential.oauth2`
145
+ * (`clientId`, `redirectUri`, `authUri`, `state`).
146
+ */
147
+ authConfig: Record<string, unknown>;
148
+ }
149
+ /** A completed flow: whose grant was stored, for which paused call. */
150
+ export type ConsentCompletion = ConsentBinding;
151
+ export interface OAuthConsent {
152
+ /** The configured redirect URI; the callback route is mounted at its path. */
153
+ readonly redirectUri: string;
154
+ /** True when `provider` has a client configured. */
155
+ has(provider: string): boolean;
156
+ /** Starts a flow for a paused call: a fresh state and verifier, held here. */
157
+ begin(binding: ConsentBinding): Promise<ConsentRequest>;
158
+ /**
159
+ * Completes a flow from the callback's parameters: the state taken (once),
160
+ * checked for expiry and, when the callback knows its caller, for the
161
+ * user; the code exchanged with the verifier; the grant stored. Throws
162
+ * ConsentError.
163
+ */
164
+ complete(input: {
165
+ state?: unknown;
166
+ code?: unknown;
167
+ error?: unknown;
168
+ callerUserId?: string;
169
+ }): Promise<ConsentCompletion>;
170
+ }
171
+ /** RFC 7636 S256: the challenge for a verifier. */
172
+ export declare function s256Challenge(verifier: string): string;
173
+ /** The consent manager over a credential store. Validates the configuration at once: a bad URL throws here, at boot. */
174
+ export declare function oauthConsent(options: OAuthConsentOptions): OAuthConsent;
175
+ /** The credential store and the consent step a run reaches its tools' third-party grants through. */
176
+ export interface ToolCredentials {
177
+ /** The sealed per-user store (ADR 0072). A run reads it pinned to its own app. */
178
+ store: CredentialStore;
179
+ /** The consent step. Without it, a missing grant is a ToolCredentialError, as before. */
180
+ consent?: OAuthConsent;
181
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * lib/tools/oauthConsent.ts — the consent step for a tool's OAuth grant: the
3
+ * authorization URL a paused call hands the person, and the callback that
4
+ * completes the authorization-code flow with PKCE (WS6-3b, ADR 0085).
5
+ *
6
+ * WHY this file exists:
7
+ * A tool that acts for a person needs their delegated access
8
+ * (`ctx.accessToken(provider)`, ADR 0072). When the person has not
9
+ * granted it yet, the call asks for it (`ctx.requestCredential(provider)`)
10
+ * and the turn pauses on ADK's own `adk_request_credential` call. The
11
+ * person opens the authorization URL, the provider redirects their browser
12
+ * to the server's callback route (lib/a2a/app.ts), and this module
13
+ * exchanges the code for tokens, server-side, and puts them in the
14
+ * credential store, sealed. The person's next message resumes the call.
15
+ *
16
+ * WHAT IT GUARANTEES:
17
+ * - The state nonce is 256 random bits, single-use, short-lived (10
18
+ * minutes by default) and bound to the app, the user, the session, the
19
+ * provider and the paused call. Only its SHA-256 is a key in the
20
+ * pending store: a dump of the store cannot complete a flow.
21
+ * - PKCE S256: the code verifier never leaves this process. It is not in
22
+ * the authorization URL, an event, a log line or the browser.
23
+ * - The redirect URI comes from configuration only. The callback never
24
+ * reads one from the request, and the token request repeats the
25
+ * configured one.
26
+ * - The code is exchanged here, server-side. The tokens go to the
27
+ * credential store and nowhere else: never into the callback's page, an
28
+ * event, a log line, an audit row or the model's context.
29
+ * - A refusal (`ConsentError`) names its reason and the provider, never a
30
+ * code, a token, a state or a provider's own error text, which may echo
31
+ * what it refused.
32
+ *
33
+ * MULTIPLE INSTANCES: the default pending store is this process's memory, so
34
+ * the callback must reach the instance that paused the call (sticky routing,
35
+ * as the A2A task store already needs). `ConsentStates` is the plug point
36
+ * for a shared store.
37
+ */
38
+ import { createHash, randomBytes } from 'node:crypto';
39
+ import { scopeHashOf } from '../observability/audit.js';
40
+ import { PROVIDER_NAME } from './auth.js';
41
+ /**
42
+ * Flows in this process's memory, at most `max` of them: the oldest is
43
+ * dropped first, and an expired one whenever another is put.
44
+ */
45
+ export function memoryConsentStates(options = {}) {
46
+ const max = options.max ?? 10_000;
47
+ const now = options.now ?? Date.now;
48
+ const flows = new Map();
49
+ return {
50
+ async put(stateHash, flow) {
51
+ const at = now();
52
+ for (const [k, f] of flows)
53
+ if (f.expiresAt <= at)
54
+ flows.delete(k);
55
+ while (flows.size >= max)
56
+ flows.delete(flows.keys().next().value);
57
+ flows.set(stateHash, { ...flow, scopes: [...flow.scopes] });
58
+ },
59
+ async take(stateHash) {
60
+ const flow = flows.get(stateHash);
61
+ flows.delete(stateHash);
62
+ return flow;
63
+ },
64
+ size: () => flows.size,
65
+ };
66
+ }
67
+ /** A consent step that cannot proceed. The message names the reason, never a value. */
68
+ export class ConsentError extends Error {
69
+ code;
70
+ provider;
71
+ constructor(code, provider) {
72
+ const p = provider ? `"${provider}"` : 'the provider';
73
+ super(code === 'invalid_request'
74
+ ? 'The authorization response is incomplete.'
75
+ : code === 'unknown_state'
76
+ ? 'This authorization link is not valid, or was already used. Ask the agent again for a new one.'
77
+ : code === 'expired'
78
+ ? 'This authorization link has expired. Ask the agent again for a new one.'
79
+ : code === 'wrong_user'
80
+ ? 'This authorization link belongs to another user.'
81
+ : code === 'denied'
82
+ ? `The authorization at ${p} was declined.`
83
+ : code === 'exchange_failed'
84
+ ? `The authorization at ${p} could not be completed.`
85
+ : code === 'store_failed'
86
+ ? `The authorization at ${p} could not be saved.`
87
+ : `No OAuth client is configured for ${p}.`);
88
+ this.name = 'ConsentError';
89
+ this.code = code;
90
+ if (provider !== undefined)
91
+ this.provider = provider;
92
+ }
93
+ }
94
+ /** A state as begin issues it: 32 random bytes, base64url, 43 characters. */
95
+ const STATE_FORMAT = /^[A-Za-z0-9_-]{43}$/;
96
+ /** A code as a provider sends it: visible ASCII, bounded. */
97
+ const CODE_FORMAT = /^[\x21-\x7e]{1,2048}$/;
98
+ const MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
99
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex');
100
+ const base64url = (bytes) => bytes.toString('base64url');
101
+ /** RFC 7636 S256: the challenge for a verifier. */
102
+ export function s256Challenge(verifier) {
103
+ return base64url(createHash('sha256').update(verifier).digest());
104
+ }
105
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
106
+ /** An endpoint the configuration may name: https, or http on a loopback host (development, tests). */
107
+ function checkEndpoint(value, what) {
108
+ let url;
109
+ try {
110
+ url = new URL(value);
111
+ }
112
+ catch {
113
+ throw new Error(`${what} is not a URL.`);
114
+ }
115
+ const ok = url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK.has(url.hostname));
116
+ if (!ok)
117
+ throw new Error(`${what} must use https (http only on a loopback host).`);
118
+ if (url.username || url.password)
119
+ throw new Error(`${what} must not carry credentials.`);
120
+ return url;
121
+ }
122
+ const RESERVED_PARAMS = new Set(['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope']);
123
+ /** The consent manager over a credential store. Validates the configuration at once: a bad URL throws here, at boot. */
124
+ export function oauthConsent(options) {
125
+ const redirect = checkEndpoint(options.redirectUri, 'The OAuth redirect URI');
126
+ if (redirect.search || redirect.hash)
127
+ throw new Error('The OAuth redirect URI must not carry a query or a fragment.');
128
+ const providers = new Map();
129
+ for (const [name, client] of Object.entries(options.providers)) {
130
+ if (!PROVIDER_NAME.test(name))
131
+ throw new Error(`OAuth provider "${name.slice(0, 64)}" must match ${PROVIDER_NAME}.`);
132
+ checkEndpoint(client.authorizationUrl, `${name}: authorizationUrl`);
133
+ checkEndpoint(client.tokenUrl, `${name}: tokenUrl`);
134
+ if (typeof client.clientId !== 'string' || !client.clientId)
135
+ throw new Error(`${name}: clientId is required.`);
136
+ for (const key of Object.keys(client.authorizationParams ?? {})) {
137
+ if (RESERVED_PARAMS.has(key))
138
+ throw new Error(`${name}: authorizationParams may not set "${key}".`);
139
+ }
140
+ providers.set(name, { ...client, scopes: [...(client.scopes ?? [])] });
141
+ }
142
+ const states = options.states ?? memoryConsentStates({ ...(options.now ? { now: options.now } : {}) });
143
+ const ttlMs = options.ttlMs ?? 10 * 60_000;
144
+ const now = options.now ?? Date.now;
145
+ const doFetch = options.fetch ?? fetch;
146
+ const timeoutMs = options.exchangeTimeoutMs ?? 10_000;
147
+ const record = (outcome, flow, reason) => {
148
+ try {
149
+ options.audit?.({
150
+ event: 'consent.callback',
151
+ outcome,
152
+ scopeHash: scopeHashOf(flow?.userId),
153
+ detail: { ...(flow?.provider ? { provider: flow.provider } : {}), ...(flow?.appName ? { appName: flow.appName } : {}), ...(reason ? { reason } : {}) },
154
+ });
155
+ }
156
+ catch {
157
+ // The audit sink never fails the flow (ADR 0042).
158
+ }
159
+ };
160
+ /** The token response, read whole up to a bound, as JSON. */
161
+ const readJson = async (res) => {
162
+ const reader = res.body?.getReader();
163
+ if (!reader)
164
+ return undefined;
165
+ const chunks = [];
166
+ let size = 0;
167
+ for (;;) {
168
+ const { done, value } = await reader.read();
169
+ if (done)
170
+ break;
171
+ size += value.byteLength;
172
+ if (size > MAX_TOKEN_RESPONSE_BYTES) {
173
+ await reader.cancel().catch(() => { });
174
+ return undefined;
175
+ }
176
+ chunks.push(value);
177
+ }
178
+ try {
179
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
180
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
181
+ }
182
+ catch {
183
+ return undefined;
184
+ }
185
+ };
186
+ /** The authorization code exchanged for a token set (RFC 6749 4.1.3, RFC 7636 4.5). Throws ConsentError, never the provider's text. */
187
+ const exchange = async (client, flow, code) => {
188
+ const body = new URLSearchParams({
189
+ grant_type: 'authorization_code',
190
+ code,
191
+ redirect_uri: options.redirectUri,
192
+ client_id: client.clientId,
193
+ code_verifier: flow.codeVerifier,
194
+ });
195
+ if (client.clientSecret)
196
+ body.set('client_secret', client.clientSecret);
197
+ let json;
198
+ try {
199
+ const res = await doFetch(client.tokenUrl, {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
202
+ body,
203
+ // A redirect would carry the code to wherever it points.
204
+ redirect: 'error',
205
+ signal: AbortSignal.timeout(timeoutMs),
206
+ });
207
+ json = await readJson(res);
208
+ if (!res.ok)
209
+ json = undefined;
210
+ }
211
+ catch {
212
+ json = undefined;
213
+ }
214
+ const accessToken = json?.access_token;
215
+ if (typeof accessToken !== 'string' || !accessToken)
216
+ throw new ConsentError('exchange_failed', flow.provider);
217
+ const tokenType = json?.token_type;
218
+ if (tokenType !== undefined && (typeof tokenType !== 'string' || tokenType.toLowerCase() !== 'bearer'))
219
+ throw new ConsentError('exchange_failed', flow.provider);
220
+ const refreshToken = typeof json?.refresh_token === 'string' && json.refresh_token ? json.refresh_token : undefined;
221
+ const expiresIn = typeof json?.expires_in === 'number' ? json.expires_in : typeof json?.expires_in === 'string' ? Number(json.expires_in) : undefined;
222
+ const granted = typeof json?.scope === 'string' ? json.scope.split(' ').filter((s) => s !== '') : flow.scopes;
223
+ return {
224
+ accessToken,
225
+ ...(refreshToken ? { refreshToken } : {}),
226
+ ...(expiresIn !== undefined && Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresAt: new Date(now() + expiresIn * 1000) } : {}),
227
+ scopes: granted,
228
+ };
229
+ };
230
+ return {
231
+ redirectUri: options.redirectUri,
232
+ has: (provider) => typeof provider === 'string' && providers.has(provider),
233
+ async begin(binding) {
234
+ const client = providers.get(binding.provider);
235
+ if (!client)
236
+ throw new ConsentError('unknown_provider', PROVIDER_NAME.test(binding.provider) ? binding.provider : undefined);
237
+ for (const field of ['appName', 'userId', 'sessionId', 'functionCallId']) {
238
+ if (typeof binding[field] !== 'string' || !binding[field])
239
+ throw new Error(`A consent flow needs the paused call's ${field}.`);
240
+ }
241
+ const state = base64url(randomBytes(32));
242
+ const codeVerifier = base64url(randomBytes(32));
243
+ const scopes = [...(client.scopes ?? [])];
244
+ await states.put(sha256(state), {
245
+ appName: binding.appName,
246
+ userId: binding.userId,
247
+ sessionId: binding.sessionId,
248
+ functionCallId: binding.functionCallId,
249
+ provider: binding.provider,
250
+ codeVerifier,
251
+ scopes,
252
+ expiresAt: now() + ttlMs,
253
+ });
254
+ const url = new URL(client.authorizationUrl);
255
+ for (const [k, v] of Object.entries(client.authorizationParams ?? {}))
256
+ url.searchParams.set(k, v);
257
+ url.searchParams.set('response_type', 'code');
258
+ url.searchParams.set('client_id', client.clientId);
259
+ url.searchParams.set('redirect_uri', options.redirectUri);
260
+ if (scopes.length)
261
+ url.searchParams.set('scope', scopes.join(' '));
262
+ url.searchParams.set('state', state);
263
+ url.searchParams.set('code_challenge', s256Challenge(codeVerifier));
264
+ url.searchParams.set('code_challenge_method', 'S256');
265
+ const authUri = url.toString();
266
+ const authConfig = {
267
+ credentialKey: binding.provider,
268
+ authScheme: {
269
+ type: 'oauth2',
270
+ flows: {
271
+ authorizationCode: {
272
+ authorizationUrl: client.authorizationUrl,
273
+ tokenUrl: client.tokenUrl,
274
+ scopes: Object.fromEntries(scopes.map((s) => [s, ''])),
275
+ },
276
+ },
277
+ },
278
+ exchangedAuthCredential: {
279
+ authType: 'oauth2',
280
+ oauth2: { clientId: client.clientId, redirectUri: options.redirectUri, authUri, state },
281
+ },
282
+ };
283
+ return { provider: binding.provider, authUri, state, scopes, authConfig };
284
+ },
285
+ async complete(input) {
286
+ const state = input.state;
287
+ if (typeof state !== 'string' || !STATE_FORMAT.test(state)) {
288
+ record('denied', undefined, 'invalid_request');
289
+ throw new ConsentError('invalid_request');
290
+ }
291
+ // Taken before anything else is checked: a state is spent by its first use, whatever the outcome.
292
+ const flow = await states.take(sha256(state));
293
+ if (!flow) {
294
+ record('denied', undefined, 'unknown_state');
295
+ throw new ConsentError('unknown_state');
296
+ }
297
+ const fail = (code) => {
298
+ record('denied', flow, code);
299
+ throw new ConsentError(code, flow.provider);
300
+ };
301
+ if (flow.expiresAt <= now())
302
+ fail('expired');
303
+ if (input.callerUserId !== undefined && input.callerUserId !== flow.userId)
304
+ fail('wrong_user');
305
+ if (input.error !== undefined)
306
+ fail('denied');
307
+ const code = input.code;
308
+ if (typeof code !== 'string' || !CODE_FORMAT.test(code))
309
+ fail('invalid_request');
310
+ const client = providers.get(flow.provider);
311
+ if (!client)
312
+ fail('unknown_provider');
313
+ let tokens;
314
+ try {
315
+ tokens = await exchange(client, flow, code);
316
+ }
317
+ catch {
318
+ return fail('exchange_failed');
319
+ }
320
+ try {
321
+ await options.credentials.put({ appName: flow.appName, userId: flow.userId, provider: flow.provider }, tokens);
322
+ }
323
+ catch {
324
+ return fail('store_failed');
325
+ }
326
+ record('ok', flow);
327
+ return { appName: flow.appName, userId: flow.userId, sessionId: flow.sessionId, functionCallId: flow.functionCallId, provider: flow.provider };
328
+ },
329
+ };
330
+ }