@agent-native/core 0.131.9 → 0.132.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 (292) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +99 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
  4. package/corpus/core/docs/design/durable-agent-runs.md +15 -11
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/activity.ts +127 -10
  7. package/corpus/core/src/a2a/index.ts +3 -0
  8. package/corpus/core/src/agent/durable-background.ts +38 -28
  9. package/corpus/core/src/agent/harness/background.ts +12 -2
  10. package/corpus/core/src/agent/production-agent.ts +18 -15
  11. package/corpus/core/src/agent/run-store.ts +45 -3
  12. package/corpus/core/src/application-state/handlers.ts +45 -0
  13. package/corpus/core/src/application-state/index.ts +3 -0
  14. package/corpus/core/src/application-state/store.ts +28 -11
  15. package/corpus/core/src/audit/redact.ts +104 -18
  16. package/corpus/core/src/client/AgentPanel.tsx +2 -7
  17. package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
  18. package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
  19. package/corpus/core/src/client/agent-chat.ts +4 -9
  20. package/corpus/core/src/client/application-state.ts +115 -12
  21. package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
  22. package/corpus/core/src/client/guided-questions.tsx +12 -23
  23. package/corpus/core/src/db/client.ts +148 -28
  24. package/corpus/core/src/db/create-get-db.ts +39 -6
  25. package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
  26. package/corpus/core/src/deploy/build.ts +26 -33
  27. package/corpus/core/src/feature-flags/store.ts +10 -5
  28. package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
  29. package/corpus/core/src/integrations/config-store.ts +13 -0
  30. package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
  31. package/corpus/core/src/mcp-client/routes.ts +33 -2
  32. package/corpus/core/src/onboarding/plugin.ts +31 -25
  33. package/corpus/core/src/org/accept-pending.ts +2 -0
  34. package/corpus/core/src/org/auto-join-domain.ts +2 -0
  35. package/corpus/core/src/org/context.ts +35 -10
  36. package/corpus/core/src/org/handlers.ts +6 -0
  37. package/corpus/core/src/org/request-org-cache.ts +71 -0
  38. package/corpus/core/src/resources/store.ts +10 -9
  39. package/corpus/core/src/scripts/runner.ts +6 -3
  40. package/corpus/core/src/secrets/storage.ts +97 -5
  41. package/corpus/core/src/server/action-change.ts +33 -0
  42. package/corpus/core/src/server/action-routes.ts +11 -9
  43. package/corpus/core/src/server/agent-teams.ts +10 -2
  44. package/corpus/core/src/server/better-auth-instance.ts +39 -12
  45. package/corpus/core/src/server/core-routes-plugin.ts +19 -2
  46. package/corpus/core/src/server/credential-provider.ts +57 -14
  47. package/corpus/core/src/server/http-response-telemetry.ts +1 -0
  48. package/corpus/core/src/server/poll.ts +95 -57
  49. package/corpus/core/src/server/voice-providers-status.ts +19 -4
  50. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
  51. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
  52. package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
  53. package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
  54. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
  55. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
  56. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
  57. package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
  58. package/corpus/templates/analytics/app/i18n-data.ts +40 -0
  59. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
  60. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
  61. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
  62. package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
  63. package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
  65. package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
  66. package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
  67. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
  68. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  69. package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
  70. package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
  71. package/corpus/templates/content/netlify.toml +3 -0
  72. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
  73. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
  74. package/corpus/templates/slides/actions/patch-deck.ts +25 -1
  75. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
  76. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
  77. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  78. package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
  79. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
  80. package/corpus/templates/slides/app/pages/Index.tsx +2 -2
  81. package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
  82. package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
  83. package/corpus/toolkit/CHANGELOG.md +8 -0
  84. package/corpus/toolkit/package.json +1 -1
  85. package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
  86. package/dist/a2a/activity.d.ts +22 -4
  87. package/dist/a2a/activity.d.ts.map +1 -1
  88. package/dist/a2a/activity.js +97 -11
  89. package/dist/a2a/activity.js.map +1 -1
  90. package/dist/a2a/index.d.ts +1 -1
  91. package/dist/a2a/index.d.ts.map +1 -1
  92. package/dist/a2a/index.js +1 -1
  93. package/dist/a2a/index.js.map +1 -1
  94. package/dist/agent/durable-background.d.ts +13 -8
  95. package/dist/agent/durable-background.d.ts.map +1 -1
  96. package/dist/agent/durable-background.js +36 -28
  97. package/dist/agent/durable-background.js.map +1 -1
  98. package/dist/agent/harness/background.d.ts.map +1 -1
  99. package/dist/agent/harness/background.js +12 -2
  100. package/dist/agent/harness/background.js.map +1 -1
  101. package/dist/agent/production-agent.d.ts.map +1 -1
  102. package/dist/agent/production-agent.js +18 -15
  103. package/dist/agent/production-agent.js.map +1 -1
  104. package/dist/agent/run-store.d.ts +2 -0
  105. package/dist/agent/run-store.d.ts.map +1 -1
  106. package/dist/agent/run-store.js +44 -3
  107. package/dist/agent/run-store.js.map +1 -1
  108. package/dist/application-state/handlers.d.ts +15 -0
  109. package/dist/application-state/handlers.d.ts.map +1 -1
  110. package/dist/application-state/handlers.js +42 -2
  111. package/dist/application-state/handlers.js.map +1 -1
  112. package/dist/application-state/index.d.ts +2 -2
  113. package/dist/application-state/index.d.ts.map +1 -1
  114. package/dist/application-state/index.js +2 -2
  115. package/dist/application-state/index.js.map +1 -1
  116. package/dist/application-state/store.d.ts +11 -0
  117. package/dist/application-state/store.d.ts.map +1 -1
  118. package/dist/application-state/store.js +23 -10
  119. package/dist/application-state/store.js.map +1 -1
  120. package/dist/audit/redact.d.ts +25 -2
  121. package/dist/audit/redact.d.ts.map +1 -1
  122. package/dist/audit/redact.js +85 -21
  123. package/dist/audit/redact.js.map +1 -1
  124. package/dist/client/AgentPanel.d.ts.map +1 -1
  125. package/dist/client/AgentPanel.js +2 -7
  126. package/dist/client/AgentPanel.js.map +1 -1
  127. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  128. package/dist/client/MultiTabAssistantChat.js +1 -1
  129. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  130. package/dist/client/RunStuckBanner.d.ts +11 -1
  131. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  132. package/dist/client/RunStuckBanner.js +20 -7
  133. package/dist/client/RunStuckBanner.js.map +1 -1
  134. package/dist/client/agent-chat.d.ts.map +1 -1
  135. package/dist/client/agent-chat.js +4 -6
  136. package/dist/client/agent-chat.js.map +1 -1
  137. package/dist/client/application-state.d.ts +11 -0
  138. package/dist/client/application-state.d.ts.map +1 -1
  139. package/dist/client/application-state.js +87 -9
  140. package/dist/client/application-state.js.map +1 -1
  141. package/dist/client/dynamic-suggestions.d.ts.map +1 -1
  142. package/dist/client/dynamic-suggestions.js +4 -13
  143. package/dist/client/dynamic-suggestions.js.map +1 -1
  144. package/dist/client/guided-questions.d.ts.map +1 -1
  145. package/dist/client/guided-questions.js +6 -22
  146. package/dist/client/guided-questions.js.map +1 -1
  147. package/dist/collab/awareness.d.ts +2 -2
  148. package/dist/collab/awareness.d.ts.map +1 -1
  149. package/dist/db/client.d.ts +57 -10
  150. package/dist/db/client.d.ts.map +1 -1
  151. package/dist/db/client.js +122 -31
  152. package/dist/db/client.js.map +1 -1
  153. package/dist/db/create-get-db.d.ts.map +1 -1
  154. package/dist/db/create-get-db.js +31 -7
  155. package/dist/db/create-get-db.js.map +1 -1
  156. package/dist/db/runtime-diagnostics.d.ts +1 -0
  157. package/dist/db/runtime-diagnostics.d.ts.map +1 -1
  158. package/dist/db/runtime-diagnostics.js +31 -6
  159. package/dist/db/runtime-diagnostics.js.map +1 -1
  160. package/dist/deploy/build.d.ts +5 -20
  161. package/dist/deploy/build.d.ts.map +1 -1
  162. package/dist/deploy/build.js +18 -30
  163. package/dist/deploy/build.js.map +1 -1
  164. package/dist/feature-flags/store.d.ts.map +1 -1
  165. package/dist/feature-flags/store.js +10 -5
  166. package/dist/feature-flags/store.js.map +1 -1
  167. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  168. package/dist/integrations/a2a-continuations-store.js +12 -0
  169. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  170. package/dist/integrations/config-store.d.ts +1 -0
  171. package/dist/integrations/config-store.d.ts.map +1 -1
  172. package/dist/integrations/config-store.js +11 -0
  173. package/dist/integrations/config-store.js.map +1 -1
  174. package/dist/integrations/google-docs-poller.d.ts.map +1 -1
  175. package/dist/integrations/google-docs-poller.js +19 -2
  176. package/dist/integrations/google-docs-poller.js.map +1 -1
  177. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  178. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  179. package/dist/mcp-client/routes.d.ts.map +1 -1
  180. package/dist/mcp-client/routes.js +30 -2
  181. package/dist/mcp-client/routes.js.map +1 -1
  182. package/dist/notifications/routes.d.ts +6 -6
  183. package/dist/observability/routes.d.ts +3 -3
  184. package/dist/onboarding/plugin.d.ts.map +1 -1
  185. package/dist/onboarding/plugin.js +12 -8
  186. package/dist/onboarding/plugin.js.map +1 -1
  187. package/dist/org/accept-pending.d.ts.map +1 -1
  188. package/dist/org/accept-pending.js +2 -0
  189. package/dist/org/accept-pending.js.map +1 -1
  190. package/dist/org/auto-join-domain.d.ts.map +1 -1
  191. package/dist/org/auto-join-domain.js +2 -0
  192. package/dist/org/auto-join-domain.js.map +1 -1
  193. package/dist/org/context.d.ts.map +1 -1
  194. package/dist/org/context.js +29 -9
  195. package/dist/org/context.js.map +1 -1
  196. package/dist/org/handlers.d.ts.map +1 -1
  197. package/dist/org/handlers.js +5 -0
  198. package/dist/org/handlers.js.map +1 -1
  199. package/dist/org/request-org-cache.d.ts +16 -0
  200. package/dist/org/request-org-cache.d.ts.map +1 -0
  201. package/dist/org/request-org-cache.js +62 -0
  202. package/dist/org/request-org-cache.js.map +1 -0
  203. package/dist/progress/routes.d.ts +1 -1
  204. package/dist/resources/handlers.d.ts +1 -1
  205. package/dist/resources/store.d.ts.map +1 -1
  206. package/dist/resources/store.js +11 -9
  207. package/dist/resources/store.js.map +1 -1
  208. package/dist/scripts/runner.d.ts.map +1 -1
  209. package/dist/scripts/runner.js +3 -3
  210. package/dist/scripts/runner.js.map +1 -1
  211. package/dist/secrets/storage.d.ts.map +1 -1
  212. package/dist/secrets/storage.js +82 -5
  213. package/dist/secrets/storage.js.map +1 -1
  214. package/dist/server/action-change.d.ts +20 -0
  215. package/dist/server/action-change.d.ts.map +1 -1
  216. package/dist/server/action-change.js +29 -0
  217. package/dist/server/action-change.js.map +1 -1
  218. package/dist/server/action-routes.d.ts.map +1 -1
  219. package/dist/server/action-routes.js +7 -8
  220. package/dist/server/action-routes.js.map +1 -1
  221. package/dist/server/agent-teams.d.ts.map +1 -1
  222. package/dist/server/agent-teams.js +10 -2
  223. package/dist/server/agent-teams.js.map +1 -1
  224. package/dist/server/better-auth-instance.d.ts.map +1 -1
  225. package/dist/server/better-auth-instance.js +31 -14
  226. package/dist/server/better-auth-instance.js.map +1 -1
  227. package/dist/server/core-routes-plugin.d.ts +1 -0
  228. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  229. package/dist/server/core-routes-plugin.js +18 -4
  230. package/dist/server/core-routes-plugin.js.map +1 -1
  231. package/dist/server/credential-provider.d.ts +16 -0
  232. package/dist/server/credential-provider.d.ts.map +1 -1
  233. package/dist/server/credential-provider.js +49 -13
  234. package/dist/server/credential-provider.js.map +1 -1
  235. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  236. package/dist/server/http-response-telemetry.js +1 -0
  237. package/dist/server/http-response-telemetry.js.map +1 -1
  238. package/dist/server/poll.d.ts +6 -0
  239. package/dist/server/poll.d.ts.map +1 -1
  240. package/dist/server/poll.js +84 -47
  241. package/dist/server/poll.js.map +1 -1
  242. package/dist/server/transcribe-voice.d.ts +1 -1
  243. package/dist/server/voice-providers-status.d.ts.map +1 -1
  244. package/dist/server/voice-providers-status.js +12 -3
  245. package/dist/server/voice-providers-status.js.map +1 -1
  246. package/docs/content/durable-background-runs.mdx +15 -6
  247. package/docs/design/durable-agent-runs.md +15 -11
  248. package/package.json +2 -2
  249. package/src/a2a/activity.ts +127 -10
  250. package/src/a2a/index.ts +3 -0
  251. package/src/agent/durable-background.ts +38 -28
  252. package/src/agent/harness/background.ts +12 -2
  253. package/src/agent/production-agent.ts +18 -15
  254. package/src/agent/run-store.ts +45 -3
  255. package/src/application-state/handlers.ts +45 -0
  256. package/src/application-state/index.ts +3 -0
  257. package/src/application-state/store.ts +28 -11
  258. package/src/audit/redact.ts +104 -18
  259. package/src/client/AgentPanel.tsx +2 -7
  260. package/src/client/MultiTabAssistantChat.tsx +3 -0
  261. package/src/client/RunStuckBanner.tsx +29 -6
  262. package/src/client/agent-chat.ts +4 -9
  263. package/src/client/application-state.ts +115 -12
  264. package/src/client/dynamic-suggestions.ts +4 -12
  265. package/src/client/guided-questions.tsx +12 -23
  266. package/src/db/client.ts +148 -28
  267. package/src/db/create-get-db.ts +39 -6
  268. package/src/db/runtime-diagnostics.ts +41 -6
  269. package/src/deploy/build.ts +26 -33
  270. package/src/feature-flags/store.ts +10 -5
  271. package/src/integrations/a2a-continuations-store.ts +11 -0
  272. package/src/integrations/config-store.ts +13 -0
  273. package/src/integrations/google-docs-poller.ts +26 -2
  274. package/src/mcp-client/routes.ts +33 -2
  275. package/src/onboarding/plugin.ts +31 -25
  276. package/src/org/accept-pending.ts +2 -0
  277. package/src/org/auto-join-domain.ts +2 -0
  278. package/src/org/context.ts +35 -10
  279. package/src/org/handlers.ts +6 -0
  280. package/src/org/request-org-cache.ts +71 -0
  281. package/src/resources/store.ts +10 -9
  282. package/src/scripts/runner.ts +6 -3
  283. package/src/secrets/storage.ts +97 -5
  284. package/src/server/action-change.ts +33 -0
  285. package/src/server/action-routes.ts +11 -9
  286. package/src/server/agent-teams.ts +10 -2
  287. package/src/server/better-auth-instance.ts +39 -12
  288. package/src/server/core-routes-plugin.ts +19 -2
  289. package/src/server/credential-provider.ts +57 -14
  290. package/src/server/http-response-telemetry.ts +1 -0
  291. package/src/server/poll.ts +95 -57
  292. package/src/server/voice-providers-status.ts +19 -4
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA+FjD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI3C;AAMD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAqF3E;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAmID;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA8BlC;AAED,wEAAwE;AACxE,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAoCzC;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAItD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAsC5B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CAuCvB;AAeD,wBAAsB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAStE"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAcH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA+FjD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI3C;AAMD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAuF3E;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAiLD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAOlC;AAoCD,wEAAwE;AACxE,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAmEzC;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAItD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAsC5B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CAuCvB;AAeD,wBAAsB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAUtE"}
@@ -14,6 +14,7 @@
14
14
  import { randomUUID } from "node:crypto";
15
15
  import { getDbExec, isPostgres } from "../db/client.js";
16
16
  import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
17
+ import { getRequestContext } from "../server/request-context.js";
17
18
  import { encryptSecretValue as encryptLegacyValue, encryptSharedSecretValue as encryptValue, decryptSharedSecretValueDetailed as decryptValue, decryptSecretValue as decryptLegacyValue, hasSharedSecretEncryptionKeyMaterial, } from "./crypto.js";
18
19
  import { APP_SECRETS_CREATE_SQL } from "./schema.js";
19
20
  // ---------------------------------------------------------------------------
@@ -162,6 +163,7 @@ export async function writeAppSecret(args) {
162
163
  now,
163
164
  now,
164
165
  ];
166
+ invalidateRequestSecret(args);
165
167
  if (isPostgres()) {
166
168
  const { rows } = await client.execute({
167
169
  sql: `${upsertSql} RETURNING id`,
@@ -250,6 +252,45 @@ async function populateSharedAppSecret(id, value, sharedCiphertextToReplace) {
250
252
  // is unavailable due to a read-only role, transient DB failure, or race.
251
253
  }
252
254
  }
255
+ // ---------------------------------------------------------------------------
256
+ // Per-request read memo
257
+ // ---------------------------------------------------------------------------
258
+ /**
259
+ * Per-request memo of secret reads, keyed on the active AsyncLocalStorage
260
+ * RequestContext (WeakMap → freed with the request). Mirrors the settings
261
+ * store's cache, including its staleness rule: a write in THIS request is
262
+ * written through, while other in-flight requests keep their snapshot for
263
+ * their own (short) lifetime.
264
+ *
265
+ * The map key is `scope|scopeId|key`, which is the secret's full identity —
266
+ * `user:<email>`, `org:<id>`, `solo:<email>`, `workspace` — so a hit can never
267
+ * answer one caller with another caller's secret. Do NOT reduce the key to
268
+ * just `key`.
269
+ *
270
+ * This exists because one credential resolution is a WATERFALL: user scope,
271
+ * then org, then workspace, then solo. Callers re-resolve the same credential
272
+ * several times per request (engine detection, config resolution, usability
273
+ * checks), and against a remote database each probe is a network round trip.
274
+ */
275
+ const _requestSecretsCache = new WeakMap();
276
+ function requestSecretsCache() {
277
+ const ctx = getRequestContext();
278
+ if (!ctx || typeof ctx !== "object")
279
+ return null;
280
+ let cache = _requestSecretsCache.get(ctx);
281
+ if (!cache) {
282
+ cache = new Map();
283
+ _requestSecretsCache.set(ctx, cache);
284
+ }
285
+ return cache;
286
+ }
287
+ function secretCacheKey(ref) {
288
+ return `${ref.scope}|${ref.scopeId}|${ref.key}`;
289
+ }
290
+ /** Drop this request's memo for a secret whose stored value just changed. */
291
+ function invalidateRequestSecret(ref) {
292
+ requestSecretsCache()?.delete(secretCacheKey(ref));
293
+ }
253
294
  /**
254
295
  * True only when the database says the app_secrets table itself is missing.
255
296
  * Other query failures must propagate unchanged: attempting schema bootstrap
@@ -286,6 +327,15 @@ async function executeAppSecretsRead(query) {
286
327
  * is responsible for never logging the returned value.
287
328
  */
288
329
  export async function readAppSecret(ref) {
330
+ const cache = requestSecretsCache();
331
+ const cacheKey = secretCacheKey(ref);
332
+ if (cache?.has(cacheKey))
333
+ return cache.get(cacheKey) ?? null;
334
+ const result = await readAppSecretUncached(ref);
335
+ cache?.set(cacheKey, result);
336
+ return result;
337
+ }
338
+ async function readAppSecretUncached(ref) {
289
339
  const { key, scope, scopeId } = ref;
290
340
  const { rows } = await executeAppSecretsRead({
291
341
  sql: `SELECT encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,
@@ -313,15 +363,39 @@ export async function readAppSecret(ref) {
313
363
  }
314
364
  /** Read several keys from one scope in a single database round trip. */
315
365
  export async function readAppSecrets(args) {
316
- const keys = [...new Set(args.keys.filter(Boolean))];
317
- if (keys.length === 0)
366
+ const requested = [...new Set(args.keys.filter(Boolean))];
367
+ if (requested.length === 0)
318
368
  return new Map();
369
+ const cache = requestSecretsCache();
370
+ const results = new Map();
371
+ const keys = [];
372
+ for (const key of requested) {
373
+ const cacheKey = secretCacheKey({
374
+ key,
375
+ scope: args.scope,
376
+ scopeId: args.scopeId,
377
+ });
378
+ if (cache?.has(cacheKey)) {
379
+ const cached = cache.get(cacheKey);
380
+ if (cached)
381
+ results.set(key, cached);
382
+ continue;
383
+ }
384
+ keys.push(key);
385
+ }
386
+ if (keys.length === 0)
387
+ return results;
319
388
  const placeholders = keys.map(() => "?").join(", ");
320
389
  const { rows } = await executeAppSecretsRead({
321
390
  sql: `SELECT key, encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key IN (${placeholders})`,
322
391
  args: [args.scope, args.scopeId, ...keys],
323
392
  });
324
- const results = new Map();
393
+ // The statement covered every uncached key in this scope, so a key missing
394
+ // from `rows` is genuinely absent — memo it as such rather than leaving a
395
+ // single-key read to go ask again.
396
+ for (const key of keys) {
397
+ cache?.set(secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }), null);
398
+ }
325
399
  for (const row of rows) {
326
400
  const key = String(row.key ?? "");
327
401
  if (!key)
@@ -332,11 +406,13 @@ export async function readAppSecrets(args) {
332
406
  if (decrypted.needsSharedCiphertext) {
333
407
  await populateSharedAppSecret(row.id, decrypted.value, decrypted.sharedCiphertextToReplace ?? null);
334
408
  }
335
- results.set(key, {
409
+ const result = {
336
410
  value: decrypted.value,
337
411
  last4: last4(decrypted.value),
338
412
  updatedAt: Number(row.updated_at ?? 0),
339
- });
413
+ };
414
+ results.set(key, result);
415
+ cache?.set(secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }), result);
340
416
  }
341
417
  catch {
342
418
  // Match readAppSecret: corrupted or stale ciphertext behaves as missing.
@@ -449,6 +525,7 @@ function parseAllowlist(raw) {
449
525
  }
450
526
  export async function deleteAppSecret(ref) {
451
527
  await ensureTable();
528
+ invalidateRequestSecret(ref);
452
529
  const { key, scope, scopeId } = ref;
453
530
  const client = getDbExec();
454
531
  const { rowsAffected } = await client.execute({
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Storage layer for the framework secrets registry.\n *\n * Values are encrypted at rest with AES-256-GCM. The workspace-shared\n * encryption key prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy\n * combined `SECRETS_ENCRYPTION_KEY`, then the workspace-wide `A2A_SECRET`.\n * A configured previous workspace key, `BETTER_AUTH_SECRET`, and app-scoped\n * keys remain read fallbacks for rows written before sibling apps converged on\n * the shared key. Successful fallback reads race-safely refresh the shared\n * ciphertext.\n *\n * Secret values are NEVER logged and NEVER returned from any route handler.\n */\n\nimport { randomUUID } from \"node:crypto\";\n\nimport { getDbExec, isPostgres } from \"../db/client.js\";\nimport { ensureColumnExists, ensureTableExists } from \"../db/ddl-guard.js\";\nimport {\n encryptSecretValue as encryptLegacyValue,\n encryptSharedSecretValue as encryptValue,\n decryptSharedSecretValueDetailed as decryptValue,\n decryptSecretValue as decryptLegacyValue,\n hasSharedSecretEncryptionKeyMaterial,\n} from \"./crypto.js\";\nimport type { SecretScope } from \"./register.js\";\nimport { APP_SECRETS_CREATE_SQL } from \"./schema.js\";\n\n// ---------------------------------------------------------------------------\n// Table bootstrap\n// ---------------------------------------------------------------------------\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n // Postgres version of the CREATE TABLE — the generic `INTEGER` maps to\n // BIGINT on Postgres, which we need for millisecond timestamps.\n const createSql = isPostgres()\n ? APP_SECRETS_CREATE_SQL.replace(/\\bINTEGER\\b/g, \"BIGINT\")\n : APP_SECRETS_CREATE_SQL;\n\n if (isPostgres()) {\n // Hot path: in production the table and both additive columns are\n // virtually always already present. Issuing `CREATE`/`ALTER` would\n // still take an ACCESS EXCLUSIVE lock — which, in a fresh background\n // worker process behind a concurrent connection on the shared Neon DB,\n // can block ~indefinitely. `ensureTableExists` / `ensureColumnExists`\n // check `information_schema` first (a plain read, no lock) and run DDL\n // ONLY for what is actually missing, wrapping any DDL that must run in a\n // transaction-scoped `lock_timeout` so a contended lock fails fast. They\n // also re-probe after a swallowed lock-timeout and THROW if the schema\n // is still missing, so a timed-out DDL never poisons this init memo.\n await ensureTableExists(\"app_secrets\", createSql);\n await ensureColumnExists(\n \"app_secrets\",\n \"description\",\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS description TEXT`,\n );\n await ensureColumnExists(\n \"app_secrets\",\n \"url_allowlist\",\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS url_allowlist TEXT`,\n );\n await ensureColumnExists(\n \"app_secrets\",\n \"shared_encrypted_value\",\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS shared_encrypted_value TEXT`,\n );\n return;\n }\n\n // SQLite (local dev): no ACCESS EXCLUSIVE lock problem, keep the original\n // create-then-additive-alter behaviour. SQLite has no\n // `ADD COLUMN IF NOT EXISTS`, so the ALTERs stay wrapped in try/catch.\n await client.execute(createSql);\n\n // Additive migration: description column (for ad-hoc keys)\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN description TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: url_allowlist column\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN url_allowlist TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: workspace-shared ciphertext. Keep the legacy\n // encrypted_value column so older app versions remain readable.\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN shared_encrypted_value TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n })().catch((err) => {\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Encryption — see ./crypto.ts. Keep encrypted_value on the legacy app-key\n// format for mixed-version deployments, and add shared_encrypted_value when a\n// stable workspace key is configured so sibling apps can read the same row.\n// ---------------------------------------------------------------------------\n\n/**\n * Return the last 4 characters of a secret, with any leading characters\n * masked. Used to show a preview without leaking the value.\n */\nexport function last4(value: string): string {\n if (!value) return \"\";\n if (value.length <= 4) return \"••••\";\n return \"••••\" + value.slice(-4);\n}\n\n// ---------------------------------------------------------------------------\n// CRUD\n// ---------------------------------------------------------------------------\n\nexport interface SecretRef {\n key: string;\n scope: SecretScope;\n scopeId: string;\n}\n\nexport interface WriteSecretArgs extends SecretRef {\n value: string;\n /** Optional human-readable description (used for ad-hoc keys). */\n description?: string;\n /** Optional JSON-stringified array of allowed URL origins. */\n urlAllowlist?: string;\n}\n\n/**\n * Write (insert or update) a secret. The value is encrypted before being\n * stored — the caller's plaintext is never persisted. Returns the new\n * record's id.\n */\nexport async function writeAppSecret(args: WriteSecretArgs): Promise<string> {\n await ensureTable();\n const { key, value, scope, scopeId, description, urlAllowlist } = args;\n if (!key || !value || !scope || !scopeId) {\n throw new Error(\n \"writeAppSecret: key, value, scope, and scopeId are all required\",\n );\n }\n const client = getDbExec();\n const now = Date.now();\n // Dual-write during rollout: old readers continue using encrypted_value,\n // while new readers prefer the nullable shared ciphertext. An app-only\n // deployment leaves the shared column null; on an update, a writer without\n // shared key material clears any existing shared ciphertext rather than\n // preserving it (see the upsert SQL below for why).\n const encrypted = encryptLegacyValue(value);\n const sharedEncrypted = hasSharedSecretEncryptionKeyMaterial()\n ? encryptValue(value)\n : null;\n const id = randomUUID();\n\n // Atomic upsert by (scope, scope_id, key). Previously this was a\n // SELECT-then-branch (UPDATE if found, else INSERT): under concurrent\n // writers for the same key both could see \"no row\" and both attempt\n // INSERT, and the loser threw a raw UNIQUE(scope, scope_id, key)\n // constraint violation (a user-facing 500) instead of updating. A single\n // `INSERT ... ON CONFLICT DO UPDATE` closes that window — it's one\n // statement, so there's no gap between \"check\" and \"act\". `id` is\n // deliberately left out of the `DO UPDATE SET` list so an existing row\n // keeps its original id (any stored references stay stable); only a\n // genuinely new row gets the freshly generated `id`. This syntax is\n // portable across SQLite (UPSERT since 3.24) and Postgres.\n //\n // shared_encrypted_value is overwritten with `excluded.shared_encrypted_value`\n // (NULL when this writer lacks shared key material) rather than preserved\n // via COALESCE. Preserving an existing shared ciphertext across a value\n // update would let a sibling app silently decrypt a STALE value after the\n // owner rotates it — a material-less writer has no way to produce the new\n // shared ciphertext, so it must clear the old one instead of leaving it\n // pointing at data that's no longer current. Siblings then get an honest\n // cache miss (falling back to the legacy column or reporting missing) until\n // the owning app's next read repopulates shared_encrypted_value via\n // `populateSharedAppSecret`, which fills a NULL column or compare-and-swap\n // replaces the exact legacy ciphertext it just decrypted. A temporary miss\n // is safer than serving rotated-away plaintext.\n const upsertSql = `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (scope, scope_id, key) DO UPDATE SET\n encrypted_value = excluded.encrypted_value,\n shared_encrypted_value = excluded.shared_encrypted_value,\n description = excluded.description,\n url_allowlist = excluded.url_allowlist,\n updated_at = excluded.updated_at`;\n const upsertArgs = [\n id,\n scope,\n scopeId,\n key,\n encrypted,\n sharedEncrypted,\n description ?? null,\n urlAllowlist ?? null,\n now,\n now,\n ];\n\n if (isPostgres()) {\n const { rows } = await client.execute({\n sql: `${upsertSql} RETURNING id`,\n args: upsertArgs,\n });\n return String(rows[0]?.id ?? id);\n }\n\n // SQLite: RETURNING support varies across better-sqlite3/libsql builds, so\n // (matching the convention elsewhere in this codebase, e.g.\n // integrations/pending-tasks-store.ts) re-read the id afterward instead of\n // relying on it. The row is guaranteed to exist at this point, so this is\n // a plain lookup rather than a TOCTOU-prone gate on the write itself.\n await client.execute({ sql: upsertSql, args: upsertArgs });\n const { rows } = await client.execute({\n sql: `SELECT id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n return String(rows[0]?.id ?? id);\n}\n\nexport interface ReadSecretResult {\n value: string;\n last4: string;\n updatedAt: number;\n}\n\n/**\n * Read the shared-key format and retain compatibility with rows written before\n * app_secrets moved to its workspace-shared encryption boundary. The legacy\n * fallback is only useful when the current app owns the old row; sibling apps\n * will receive shared-key ciphertext after the next vault sync or update.\n */\ninterface DecryptedAppSecretValue {\n value: string;\n /** True when the app-scoped fallback decrypted encrypted_value. */\n usedLegacyKey: boolean;\n /** True when shared_encrypted_value needs to be created or refreshed. */\n needsSharedCiphertext: boolean;\n /**\n * Existing ciphertext that must still match before a refresh. Null means the\n * migration may only fill an empty shared column.\n */\n sharedCiphertextToReplace?: string | null;\n}\n\nfunction decryptAppSecretValue(\n encrypted: string,\n sharedEncrypted?: string | null,\n): DecryptedAppSecretValue {\n if (sharedEncrypted) {\n try {\n const decrypted = decryptValue(sharedEncrypted);\n return {\n value: decrypted.value,\n usedLegacyKey: false,\n needsSharedCiphertext: decrypted.needsReencrypt,\n sharedCiphertextToReplace: decrypted.needsReencrypt\n ? sharedEncrypted\n : undefined,\n };\n } catch {\n // Fall through to the legacy column. A partially migrated row may have\n // a stale shared ciphertext while the old app-key value is valid.\n }\n }\n try {\n const decrypted = decryptValue(encrypted);\n return {\n value: decrypted.value,\n usedLegacyKey: false,\n needsSharedCiphertext: true,\n sharedCiphertextToReplace: sharedEncrypted ?? null,\n };\n } catch {\n return {\n value: decryptLegacyValue(encrypted),\n usedLegacyKey: true,\n needsSharedCiphertext: true,\n sharedCiphertextToReplace: sharedEncrypted ?? null,\n };\n }\n}\n\n/**\n * Create or refresh the shared column without touching encrypted_value. This\n * is intentionally best-effort: a read must still succeed if a deployment's\n * DB role cannot update the row. The compare-and-swap predicate prevents a\n * concurrent writer from being overwritten, and leaving updated_at untouched\n * preserves the row's user-visible ordering/metadata.\n */\nasync function populateSharedAppSecret(\n id: unknown,\n value: string,\n sharedCiphertextToReplace: string | null,\n): Promise<void> {\n if (\n id === undefined ||\n id === null ||\n !hasSharedSecretEncryptionKeyMaterial()\n ) {\n return;\n }\n try {\n const encrypted = encryptValue(value);\n if (sharedCiphertextToReplace === null) {\n await getDbExec().execute({\n sql: `UPDATE app_secrets SET shared_encrypted_value = ? WHERE id = ? AND shared_encrypted_value IS NULL`,\n args: [encrypted, id],\n });\n } else {\n await getDbExec().execute({\n sql: `UPDATE app_secrets SET shared_encrypted_value = ? WHERE id = ? AND shared_encrypted_value = ?`,\n args: [encrypted, id, sharedCiphertextToReplace],\n });\n }\n } catch {\n // Migration is opportunistic. Preserve the successful read if the update\n // is unavailable due to a read-only role, transient DB failure, or race.\n }\n}\n\ntype AppSecretsReadQuery = { sql: string; args: unknown[] };\n\n/**\n * True only when the database says the app_secrets table itself is missing.\n * Other query failures must propagate unchanged: attempting schema bootstrap\n * for connectivity, permission, or syntax errors both hides the real failure\n * and can introduce DDL onto a latency-sensitive read path.\n */\nfunction isMissingAppSecretsTableError(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n const code = String((error as Error & { code?: unknown }).code ?? \"\");\n const message = error.message.toLowerCase();\n const namesAppSecrets = /(?:app_secrets|\"app_secrets\")/.test(message);\n\n if (code === \"42P01\") return namesAppSecrets;\n return (\n namesAppSecrets &&\n (message.includes(\"no such table\") ||\n (message.includes(\"relation\") && message.includes(\"does not exist\")))\n );\n}\n\n/** Execute a read without schema probes on the normal path. */\nasync function executeAppSecretsRead(query: AppSecretsReadQuery) {\n const client = getDbExec();\n try {\n return await client.execute(query);\n } catch (error) {\n if (!isMissingAppSecretsTableError(error)) throw error;\n await ensureTable();\n return client.execute(query);\n }\n}\n\n/**\n * Read a secret's plaintext value. Returns null when not found. The caller\n * is responsible for never logging the returned value.\n */\nexport async function readAppSecret(\n ref: SecretRef,\n): Promise<ReadSecretResult | null> {\n const { key, scope, scopeId } = ref;\n const { rows } = await executeAppSecretsRead({\n sql: `SELECT encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n try {\n const encrypted = rows[0].encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n rows[0].shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n rows[0].id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n return {\n value: decrypted.value,\n last4: last4(decrypted.value),\n updatedAt: Number(rows[0].updated_at ?? 0),\n };\n } catch {\n // Decryption failure — key rotated, tampered row, etc. Don't throw up the\n // stack in a way that could leak the ciphertext; just report missing.\n return null;\n }\n}\n\n/** Read several keys from one scope in a single database round trip. */\nexport async function readAppSecrets(args: {\n keys: readonly string[];\n scope: SecretScope;\n scopeId: string;\n}): Promise<Map<string, ReadSecretResult>> {\n const keys = [...new Set(args.keys.filter(Boolean))];\n if (keys.length === 0) return new Map();\n\n const placeholders = keys.map(() => \"?\").join(\", \");\n const { rows } = await executeAppSecretsRead({\n sql: `SELECT key, encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key IN (${placeholders})`,\n args: [args.scope, args.scopeId, ...keys],\n });\n const results = new Map<string, ReadSecretResult>();\n for (const row of rows) {\n const key = String(row.key ?? \"\");\n if (!key) continue;\n try {\n const encrypted = row.encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n row.shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n row.id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n results.set(key, {\n value: decrypted.value,\n last4: last4(decrypted.value),\n updatedAt: Number(row.updated_at ?? 0),\n });\n } catch {\n // Match readAppSecret: corrupted or stale ciphertext behaves as missing.\n }\n }\n return results;\n}\n\n/**\n * Return just the metadata for a secret (no value). Used by the list route so\n * the UI can show the \"Set\" pill and last-4 without the decrypted value going\n * over the wire.\n */\nexport async function getAppSecretMeta(\n ref: SecretRef,\n): Promise<{ last4: string; updatedAt: number } | null> {\n const result = await readAppSecret(ref);\n if (!result) return null;\n return { last4: result.last4, updatedAt: result.updatedAt };\n}\n\nexport interface SecretMeta {\n key: string;\n scope: SecretScope;\n scopeId: string;\n last4: string;\n description: string | null;\n urlAllowlist: string[] | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/**\n * Read a secret's metadata, including ad-hoc fields (description, allowlist),\n * without ever decrypting or returning the plaintext value. Used by the\n * ad-hoc list route and any UI that wants to render a key tile.\n */\nexport async function readAppSecretMeta(\n ref: SecretRef,\n): Promise<SecretMeta | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n const row = rows[0];\n let last4Value = \"\";\n try {\n const encrypted = row.encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n row.shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n row.id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n last4Value = last4(decrypted.value);\n } catch {\n last4Value = \"\";\n }\n return {\n key,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n}\n\n/**\n * List all secrets for a given scope. Returns metadata only — values are\n * never decrypted or returned. Used by the ad-hoc list route to surface\n * user-created keys.\n */\nexport async function listAppSecretsForScope(\n scope: SecretScope,\n scopeId: string,\n): Promise<SecretMeta[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,\n args: [scope, scopeId],\n });\n const results: SecretMeta[] = [];\n for (const row of rows) {\n let last4Value = \"\";\n try {\n const encrypted = row.encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n row.shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n row.id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n last4Value = last4(decrypted.value);\n } catch {\n last4Value = \"\";\n }\n results.push({\n key: row.key as string,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n });\n }\n return results;\n}\n\nfunction parseAllowlist(raw: string | null): string[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed) && parsed.every((v) => typeof v === \"string\")) {\n return parsed;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function deleteAppSecret(ref: SecretRef): Promise<boolean> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rowsAffected } = await client.execute({\n sql: `DELETE FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n return rowsAffected > 0;\n}\n"]}
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Storage layer for the framework secrets registry.\n *\n * Values are encrypted at rest with AES-256-GCM. The workspace-shared\n * encryption key prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy\n * combined `SECRETS_ENCRYPTION_KEY`, then the workspace-wide `A2A_SECRET`.\n * A configured previous workspace key, `BETTER_AUTH_SECRET`, and app-scoped\n * keys remain read fallbacks for rows written before sibling apps converged on\n * the shared key. Successful fallback reads race-safely refresh the shared\n * ciphertext.\n *\n * Secret values are NEVER logged and NEVER returned from any route handler.\n */\n\nimport { randomUUID } from \"node:crypto\";\n\nimport { getDbExec, isPostgres } from \"../db/client.js\";\nimport { ensureColumnExists, ensureTableExists } from \"../db/ddl-guard.js\";\nimport { getRequestContext } from \"../server/request-context.js\";\nimport {\n encryptSecretValue as encryptLegacyValue,\n encryptSharedSecretValue as encryptValue,\n decryptSharedSecretValueDetailed as decryptValue,\n decryptSecretValue as decryptLegacyValue,\n hasSharedSecretEncryptionKeyMaterial,\n} from \"./crypto.js\";\nimport type { SecretScope } from \"./register.js\";\nimport { APP_SECRETS_CREATE_SQL } from \"./schema.js\";\n\n// ---------------------------------------------------------------------------\n// Table bootstrap\n// ---------------------------------------------------------------------------\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n // Postgres version of the CREATE TABLE — the generic `INTEGER` maps to\n // BIGINT on Postgres, which we need for millisecond timestamps.\n const createSql = isPostgres()\n ? APP_SECRETS_CREATE_SQL.replace(/\\bINTEGER\\b/g, \"BIGINT\")\n : APP_SECRETS_CREATE_SQL;\n\n if (isPostgres()) {\n // Hot path: in production the table and both additive columns are\n // virtually always already present. Issuing `CREATE`/`ALTER` would\n // still take an ACCESS EXCLUSIVE lock — which, in a fresh background\n // worker process behind a concurrent connection on the shared Neon DB,\n // can block ~indefinitely. `ensureTableExists` / `ensureColumnExists`\n // check `information_schema` first (a plain read, no lock) and run DDL\n // ONLY for what is actually missing, wrapping any DDL that must run in a\n // transaction-scoped `lock_timeout` so a contended lock fails fast. They\n // also re-probe after a swallowed lock-timeout and THROW if the schema\n // is still missing, so a timed-out DDL never poisons this init memo.\n await ensureTableExists(\"app_secrets\", createSql);\n await ensureColumnExists(\n \"app_secrets\",\n \"description\",\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS description TEXT`,\n );\n await ensureColumnExists(\n \"app_secrets\",\n \"url_allowlist\",\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS url_allowlist TEXT`,\n );\n await ensureColumnExists(\n \"app_secrets\",\n \"shared_encrypted_value\",\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS shared_encrypted_value TEXT`,\n );\n return;\n }\n\n // SQLite (local dev): no ACCESS EXCLUSIVE lock problem, keep the original\n // create-then-additive-alter behaviour. SQLite has no\n // `ADD COLUMN IF NOT EXISTS`, so the ALTERs stay wrapped in try/catch.\n await client.execute(createSql);\n\n // Additive migration: description column (for ad-hoc keys)\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN description TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: url_allowlist column\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN url_allowlist TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: workspace-shared ciphertext. Keep the legacy\n // encrypted_value column so older app versions remain readable.\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN shared_encrypted_value TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n })().catch((err) => {\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Encryption — see ./crypto.ts. Keep encrypted_value on the legacy app-key\n// format for mixed-version deployments, and add shared_encrypted_value when a\n// stable workspace key is configured so sibling apps can read the same row.\n// ---------------------------------------------------------------------------\n\n/**\n * Return the last 4 characters of a secret, with any leading characters\n * masked. Used to show a preview without leaking the value.\n */\nexport function last4(value: string): string {\n if (!value) return \"\";\n if (value.length <= 4) return \"••••\";\n return \"••••\" + value.slice(-4);\n}\n\n// ---------------------------------------------------------------------------\n// CRUD\n// ---------------------------------------------------------------------------\n\nexport interface SecretRef {\n key: string;\n scope: SecretScope;\n scopeId: string;\n}\n\nexport interface WriteSecretArgs extends SecretRef {\n value: string;\n /** Optional human-readable description (used for ad-hoc keys). */\n description?: string;\n /** Optional JSON-stringified array of allowed URL origins. */\n urlAllowlist?: string;\n}\n\n/**\n * Write (insert or update) a secret. The value is encrypted before being\n * stored — the caller's plaintext is never persisted. Returns the new\n * record's id.\n */\nexport async function writeAppSecret(args: WriteSecretArgs): Promise<string> {\n await ensureTable();\n const { key, value, scope, scopeId, description, urlAllowlist } = args;\n if (!key || !value || !scope || !scopeId) {\n throw new Error(\n \"writeAppSecret: key, value, scope, and scopeId are all required\",\n );\n }\n const client = getDbExec();\n const now = Date.now();\n // Dual-write during rollout: old readers continue using encrypted_value,\n // while new readers prefer the nullable shared ciphertext. An app-only\n // deployment leaves the shared column null; on an update, a writer without\n // shared key material clears any existing shared ciphertext rather than\n // preserving it (see the upsert SQL below for why).\n const encrypted = encryptLegacyValue(value);\n const sharedEncrypted = hasSharedSecretEncryptionKeyMaterial()\n ? encryptValue(value)\n : null;\n const id = randomUUID();\n\n // Atomic upsert by (scope, scope_id, key). Previously this was a\n // SELECT-then-branch (UPDATE if found, else INSERT): under concurrent\n // writers for the same key both could see \"no row\" and both attempt\n // INSERT, and the loser threw a raw UNIQUE(scope, scope_id, key)\n // constraint violation (a user-facing 500) instead of updating. A single\n // `INSERT ... ON CONFLICT DO UPDATE` closes that window — it's one\n // statement, so there's no gap between \"check\" and \"act\". `id` is\n // deliberately left out of the `DO UPDATE SET` list so an existing row\n // keeps its original id (any stored references stay stable); only a\n // genuinely new row gets the freshly generated `id`. This syntax is\n // portable across SQLite (UPSERT since 3.24) and Postgres.\n //\n // shared_encrypted_value is overwritten with `excluded.shared_encrypted_value`\n // (NULL when this writer lacks shared key material) rather than preserved\n // via COALESCE. Preserving an existing shared ciphertext across a value\n // update would let a sibling app silently decrypt a STALE value after the\n // owner rotates it — a material-less writer has no way to produce the new\n // shared ciphertext, so it must clear the old one instead of leaving it\n // pointing at data that's no longer current. Siblings then get an honest\n // cache miss (falling back to the legacy column or reporting missing) until\n // the owning app's next read repopulates shared_encrypted_value via\n // `populateSharedAppSecret`, which fills a NULL column or compare-and-swap\n // replaces the exact legacy ciphertext it just decrypted. A temporary miss\n // is safer than serving rotated-away plaintext.\n const upsertSql = `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (scope, scope_id, key) DO UPDATE SET\n encrypted_value = excluded.encrypted_value,\n shared_encrypted_value = excluded.shared_encrypted_value,\n description = excluded.description,\n url_allowlist = excluded.url_allowlist,\n updated_at = excluded.updated_at`;\n const upsertArgs = [\n id,\n scope,\n scopeId,\n key,\n encrypted,\n sharedEncrypted,\n description ?? null,\n urlAllowlist ?? null,\n now,\n now,\n ];\n\n invalidateRequestSecret(args);\n\n if (isPostgres()) {\n const { rows } = await client.execute({\n sql: `${upsertSql} RETURNING id`,\n args: upsertArgs,\n });\n return String(rows[0]?.id ?? id);\n }\n\n // SQLite: RETURNING support varies across better-sqlite3/libsql builds, so\n // (matching the convention elsewhere in this codebase, e.g.\n // integrations/pending-tasks-store.ts) re-read the id afterward instead of\n // relying on it. The row is guaranteed to exist at this point, so this is\n // a plain lookup rather than a TOCTOU-prone gate on the write itself.\n await client.execute({ sql: upsertSql, args: upsertArgs });\n const { rows } = await client.execute({\n sql: `SELECT id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n return String(rows[0]?.id ?? id);\n}\n\nexport interface ReadSecretResult {\n value: string;\n last4: string;\n updatedAt: number;\n}\n\n/**\n * Read the shared-key format and retain compatibility with rows written before\n * app_secrets moved to its workspace-shared encryption boundary. The legacy\n * fallback is only useful when the current app owns the old row; sibling apps\n * will receive shared-key ciphertext after the next vault sync or update.\n */\ninterface DecryptedAppSecretValue {\n value: string;\n /** True when the app-scoped fallback decrypted encrypted_value. */\n usedLegacyKey: boolean;\n /** True when shared_encrypted_value needs to be created or refreshed. */\n needsSharedCiphertext: boolean;\n /**\n * Existing ciphertext that must still match before a refresh. Null means the\n * migration may only fill an empty shared column.\n */\n sharedCiphertextToReplace?: string | null;\n}\n\nfunction decryptAppSecretValue(\n encrypted: string,\n sharedEncrypted?: string | null,\n): DecryptedAppSecretValue {\n if (sharedEncrypted) {\n try {\n const decrypted = decryptValue(sharedEncrypted);\n return {\n value: decrypted.value,\n usedLegacyKey: false,\n needsSharedCiphertext: decrypted.needsReencrypt,\n sharedCiphertextToReplace: decrypted.needsReencrypt\n ? sharedEncrypted\n : undefined,\n };\n } catch {\n // Fall through to the legacy column. A partially migrated row may have\n // a stale shared ciphertext while the old app-key value is valid.\n }\n }\n try {\n const decrypted = decryptValue(encrypted);\n return {\n value: decrypted.value,\n usedLegacyKey: false,\n needsSharedCiphertext: true,\n sharedCiphertextToReplace: sharedEncrypted ?? null,\n };\n } catch {\n return {\n value: decryptLegacyValue(encrypted),\n usedLegacyKey: true,\n needsSharedCiphertext: true,\n sharedCiphertextToReplace: sharedEncrypted ?? null,\n };\n }\n}\n\n/**\n * Create or refresh the shared column without touching encrypted_value. This\n * is intentionally best-effort: a read must still succeed if a deployment's\n * DB role cannot update the row. The compare-and-swap predicate prevents a\n * concurrent writer from being overwritten, and leaving updated_at untouched\n * preserves the row's user-visible ordering/metadata.\n */\nasync function populateSharedAppSecret(\n id: unknown,\n value: string,\n sharedCiphertextToReplace: string | null,\n): Promise<void> {\n if (\n id === undefined ||\n id === null ||\n !hasSharedSecretEncryptionKeyMaterial()\n ) {\n return;\n }\n try {\n const encrypted = encryptValue(value);\n if (sharedCiphertextToReplace === null) {\n await getDbExec().execute({\n sql: `UPDATE app_secrets SET shared_encrypted_value = ? WHERE id = ? AND shared_encrypted_value IS NULL`,\n args: [encrypted, id],\n });\n } else {\n await getDbExec().execute({\n sql: `UPDATE app_secrets SET shared_encrypted_value = ? WHERE id = ? AND shared_encrypted_value = ?`,\n args: [encrypted, id, sharedCiphertextToReplace],\n });\n }\n } catch {\n // Migration is opportunistic. Preserve the successful read if the update\n // is unavailable due to a read-only role, transient DB failure, or race.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Per-request read memo\n// ---------------------------------------------------------------------------\n\n/**\n * Per-request memo of secret reads, keyed on the active AsyncLocalStorage\n * RequestContext (WeakMap → freed with the request). Mirrors the settings\n * store's cache, including its staleness rule: a write in THIS request is\n * written through, while other in-flight requests keep their snapshot for\n * their own (short) lifetime.\n *\n * The map key is `scope|scopeId|key`, which is the secret's full identity —\n * `user:<email>`, `org:<id>`, `solo:<email>`, `workspace` — so a hit can never\n * answer one caller with another caller's secret. Do NOT reduce the key to\n * just `key`.\n *\n * This exists because one credential resolution is a WATERFALL: user scope,\n * then org, then workspace, then solo. Callers re-resolve the same credential\n * several times per request (engine detection, config resolution, usability\n * checks), and against a remote database each probe is a network round trip.\n */\nconst _requestSecretsCache = new WeakMap<\n object,\n Map<string, ReadSecretResult | null>\n>();\n\nfunction requestSecretsCache(): Map<string, ReadSecretResult | null> | null {\n const ctx = getRequestContext();\n if (!ctx || typeof ctx !== \"object\") return null;\n let cache = _requestSecretsCache.get(ctx);\n if (!cache) {\n cache = new Map();\n _requestSecretsCache.set(ctx, cache);\n }\n return cache;\n}\n\nfunction secretCacheKey(ref: SecretRef): string {\n return `${ref.scope}|${ref.scopeId}|${ref.key}`;\n}\n\n/** Drop this request's memo for a secret whose stored value just changed. */\nfunction invalidateRequestSecret(ref: SecretRef): void {\n requestSecretsCache()?.delete(secretCacheKey(ref));\n}\n\ntype AppSecretsReadQuery = { sql: string; args: unknown[] };\n\n/**\n * True only when the database says the app_secrets table itself is missing.\n * Other query failures must propagate unchanged: attempting schema bootstrap\n * for connectivity, permission, or syntax errors both hides the real failure\n * and can introduce DDL onto a latency-sensitive read path.\n */\nfunction isMissingAppSecretsTableError(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n const code = String((error as Error & { code?: unknown }).code ?? \"\");\n const message = error.message.toLowerCase();\n const namesAppSecrets = /(?:app_secrets|\"app_secrets\")/.test(message);\n\n if (code === \"42P01\") return namesAppSecrets;\n return (\n namesAppSecrets &&\n (message.includes(\"no such table\") ||\n (message.includes(\"relation\") && message.includes(\"does not exist\")))\n );\n}\n\n/** Execute a read without schema probes on the normal path. */\nasync function executeAppSecretsRead(query: AppSecretsReadQuery) {\n const client = getDbExec();\n try {\n return await client.execute(query);\n } catch (error) {\n if (!isMissingAppSecretsTableError(error)) throw error;\n await ensureTable();\n return client.execute(query);\n }\n}\n\n/**\n * Read a secret's plaintext value. Returns null when not found. The caller\n * is responsible for never logging the returned value.\n */\nexport async function readAppSecret(\n ref: SecretRef,\n): Promise<ReadSecretResult | null> {\n const cache = requestSecretsCache();\n const cacheKey = secretCacheKey(ref);\n if (cache?.has(cacheKey)) return cache.get(cacheKey) ?? null;\n const result = await readAppSecretUncached(ref);\n cache?.set(cacheKey, result);\n return result;\n}\n\nasync function readAppSecretUncached(\n ref: SecretRef,\n): Promise<ReadSecretResult | null> {\n const { key, scope, scopeId } = ref;\n const { rows } = await executeAppSecretsRead({\n sql: `SELECT encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n try {\n const encrypted = rows[0].encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n rows[0].shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n rows[0].id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n return {\n value: decrypted.value,\n last4: last4(decrypted.value),\n updatedAt: Number(rows[0].updated_at ?? 0),\n };\n } catch {\n // Decryption failure — key rotated, tampered row, etc. Don't throw up the\n // stack in a way that could leak the ciphertext; just report missing.\n return null;\n }\n}\n\n/** Read several keys from one scope in a single database round trip. */\nexport async function readAppSecrets(args: {\n keys: readonly string[];\n scope: SecretScope;\n scopeId: string;\n}): Promise<Map<string, ReadSecretResult>> {\n const requested = [...new Set(args.keys.filter(Boolean))];\n if (requested.length === 0) return new Map();\n\n const cache = requestSecretsCache();\n const results = new Map<string, ReadSecretResult>();\n const keys: string[] = [];\n for (const key of requested) {\n const cacheKey = secretCacheKey({\n key,\n scope: args.scope,\n scopeId: args.scopeId,\n });\n if (cache?.has(cacheKey)) {\n const cached = cache.get(cacheKey);\n if (cached) results.set(key, cached);\n continue;\n }\n keys.push(key);\n }\n if (keys.length === 0) return results;\n\n const placeholders = keys.map(() => \"?\").join(\", \");\n const { rows } = await executeAppSecretsRead({\n sql: `SELECT key, encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key IN (${placeholders})`,\n args: [args.scope, args.scopeId, ...keys],\n });\n // The statement covered every uncached key in this scope, so a key missing\n // from `rows` is genuinely absent — memo it as such rather than leaving a\n // single-key read to go ask again.\n for (const key of keys) {\n cache?.set(\n secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }),\n null,\n );\n }\n for (const row of rows) {\n const key = String(row.key ?? \"\");\n if (!key) continue;\n try {\n const encrypted = row.encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n row.shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n row.id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n const result = {\n value: decrypted.value,\n last4: last4(decrypted.value),\n updatedAt: Number(row.updated_at ?? 0),\n };\n results.set(key, result);\n cache?.set(\n secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }),\n result,\n );\n } catch {\n // Match readAppSecret: corrupted or stale ciphertext behaves as missing.\n }\n }\n return results;\n}\n\n/**\n * Return just the metadata for a secret (no value). Used by the list route so\n * the UI can show the \"Set\" pill and last-4 without the decrypted value going\n * over the wire.\n */\nexport async function getAppSecretMeta(\n ref: SecretRef,\n): Promise<{ last4: string; updatedAt: number } | null> {\n const result = await readAppSecret(ref);\n if (!result) return null;\n return { last4: result.last4, updatedAt: result.updatedAt };\n}\n\nexport interface SecretMeta {\n key: string;\n scope: SecretScope;\n scopeId: string;\n last4: string;\n description: string | null;\n urlAllowlist: string[] | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/**\n * Read a secret's metadata, including ad-hoc fields (description, allowlist),\n * without ever decrypting or returning the plaintext value. Used by the\n * ad-hoc list route and any UI that wants to render a key tile.\n */\nexport async function readAppSecretMeta(\n ref: SecretRef,\n): Promise<SecretMeta | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n const row = rows[0];\n let last4Value = \"\";\n try {\n const encrypted = row.encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n row.shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n row.id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n last4Value = last4(decrypted.value);\n } catch {\n last4Value = \"\";\n }\n return {\n key,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n}\n\n/**\n * List all secrets for a given scope. Returns metadata only — values are\n * never decrypted or returned. Used by the ad-hoc list route to surface\n * user-created keys.\n */\nexport async function listAppSecretsForScope(\n scope: SecretScope,\n scopeId: string,\n): Promise<SecretMeta[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, key, encrypted_value, shared_encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,\n args: [scope, scopeId],\n });\n const results: SecretMeta[] = [];\n for (const row of rows) {\n let last4Value = \"\";\n try {\n const encrypted = row.encrypted_value as string;\n const decrypted = decryptAppSecretValue(\n encrypted,\n row.shared_encrypted_value as string | null,\n );\n if (decrypted.needsSharedCiphertext) {\n await populateSharedAppSecret(\n row.id,\n decrypted.value,\n decrypted.sharedCiphertextToReplace ?? null,\n );\n }\n last4Value = last4(decrypted.value);\n } catch {\n last4Value = \"\";\n }\n results.push({\n key: row.key as string,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n });\n }\n return results;\n}\n\nfunction parseAllowlist(raw: string | null): string[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed) && parsed.every((v) => typeof v === \"string\")) {\n return parsed;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function deleteAppSecret(ref: SecretRef): Promise<boolean> {\n await ensureTable();\n invalidateRequestSecret(ref);\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rowsAffected } = await client.execute({\n sql: `DELETE FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n return rowsAffected > 0;\n}\n"]}
@@ -1,3 +1,23 @@
1
+ import type { ActionPlanModeConfig } from "../action.js";
2
+ /**
3
+ * Whether THIS invocation should announce a change.
4
+ *
5
+ * `readOnly` describes an action name, but a tool that discriminates on an
6
+ * argument reads on some calls and writes on others — `manage-agent-engine`
7
+ * with `action: "list"` is a status poll, with `action: "set"` a write. Such
8
+ * actions already publish the per-call answer as a Plan-mode `effect`
9
+ * predicate, so read it here instead of treating every call as a write: a poll
10
+ * that bumps the `"action"` change version makes every query keyed on it
11
+ * refetch forever.
12
+ *
13
+ * `readOnly` stays authoritative otherwise. Widening the flag to cover mixed
14
+ * tools would also open them to read-only A2A peers and to the agent's
15
+ * read-result cache, where it would be false.
16
+ */
17
+ export declare function actionCallIsReadOnly(entry: {
18
+ readOnly?: boolean;
19
+ planMode?: ActionPlanModeConfig<any>;
20
+ }, params: unknown, fallback: boolean): boolean;
1
21
  export interface NotifyActionChangeOptions {
2
22
  actionName: string;
3
23
  owner?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"action-change.d.ts","sourceRoot":"","sources":["../../src/server/action-change.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAcD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAiBf"}
1
+ {"version":3,"file":"action-change.d.ts","sourceRoot":"","sources":["../../src/server/action-change.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAKzD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAC,GAAG,CAAC,CAAA;CAAE,EACnE,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,OAAO,GAChB,OAAO,CAWT;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAcD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -2,6 +2,35 @@ import { ACTION_CHANGE_MARKER_KEY, actionChangeMarkerSession, actionChangeMarker
2
2
  import { appStatePut } from "../application-state/store.js";
3
3
  import { recordChange } from "./poll.js";
4
4
  import { getRequestOrgId, getRequestUserEmail } from "./request-context.js";
5
+ /**
6
+ * Whether THIS invocation should announce a change.
7
+ *
8
+ * `readOnly` describes an action name, but a tool that discriminates on an
9
+ * argument reads on some calls and writes on others — `manage-agent-engine`
10
+ * with `action: "list"` is a status poll, with `action: "set"` a write. Such
11
+ * actions already publish the per-call answer as a Plan-mode `effect`
12
+ * predicate, so read it here instead of treating every call as a write: a poll
13
+ * that bumps the `"action"` change version makes every query keyed on it
14
+ * refetch forever.
15
+ *
16
+ * `readOnly` stays authoritative otherwise. Widening the flag to cover mixed
17
+ * tools would also open them to read-only A2A peers and to the agent's
18
+ * read-result cache, where it would be false.
19
+ */
20
+ export function actionCallIsReadOnly(entry, params, fallback) {
21
+ const effect = entry.planMode?.effect;
22
+ if (typeof effect === "function") {
23
+ try {
24
+ return effect(params) === "read";
25
+ }
26
+ catch {
27
+ // A predicate that throws says nothing; fall through to the flag.
28
+ }
29
+ }
30
+ if (typeof entry.readOnly === "boolean")
31
+ return entry.readOnly;
32
+ return fallback;
33
+ }
5
34
  function actionChangeTarget(options) {
6
35
  const owner = options.owner ?? getRequestUserEmail() ?? undefined;
7
36
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"action-change.js","sourceRoot":"","sources":["../../src/server/action-change.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,yBAAyB,EACzB,uBAAuB,GAExB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAS5E,SAAS,kBAAkB,CACzB,OAAkC;IAElC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,mBAAmB,EAAE,IAAI,SAAS,CAAC;IAClE,OAAO;QACL,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,KAAK;QACL,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,IAAI,eAAe,EAAE,CAAC;QAC/D,aAAa,EAAE,OAAO,CAAC,aAAa;KACrC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,OAAkC;IAElC,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,CAAC,SAAS;QAAE,OAAO;IACvB,MAAM,WAAW,CACf,SAAS,EACT,wBAAwB,EACxB;QACE,GAAG,uBAAuB,CAAC,MAAM,CAAC;QAClC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;KAC9D,EACD,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,OAAO,EAAE,CACpD,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAkC;IAElC,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC3C,YAAY,CAAC;QACX,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,OAAO,CAAC,UAAU;QACvB,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAC,CAAC;IAEH,MAAM,uBAAuB,CAAC;QAC5B,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n ACTION_CHANGE_MARKER_KEY,\n actionChangeMarkerSession,\n actionChangeMarkerValue,\n type ActionChangeTarget,\n} from \"../action-change-marker.js\";\nimport { appStatePut } from \"../application-state/store.js\";\nimport { recordChange } from \"./poll.js\";\nimport { getRequestOrgId, getRequestUserEmail } from \"./request-context.js\";\n\nexport interface NotifyActionChangeOptions {\n actionName: string;\n owner?: string;\n orgId?: string;\n requestSource?: string;\n}\n\nfunction actionChangeTarget(\n options: NotifyActionChangeOptions,\n): ActionChangeTarget {\n const owner = options.owner ?? getRequestUserEmail() ?? undefined;\n return {\n actionName: options.actionName,\n owner,\n orgId: owner ? undefined : (options.orgId ?? getRequestOrgId()),\n requestSource: options.requestSource,\n };\n}\n\nexport async function writeActionChangeMarker(\n options: NotifyActionChangeOptions,\n): Promise<void> {\n const target = actionChangeTarget(options);\n const sessionId = actionChangeMarkerSession(target);\n if (!sessionId) return;\n await appStatePut(\n sessionId,\n ACTION_CHANGE_MARKER_KEY,\n {\n ...actionChangeMarkerValue(target),\n nonce: `${Date.now()}-${Math.random().toString(36).slice(2)}`,\n },\n { requestSource: options.requestSource ?? \"agent\" },\n );\n}\n\nexport async function notifyActionChange(\n options: NotifyActionChangeOptions,\n): Promise<void> {\n const target = actionChangeTarget(options);\n recordChange({\n source: \"action\",\n type: \"change\",\n key: options.actionName,\n ...(target.owner ? { owner: target.owner } : {}),\n ...(target.orgId ? { orgId: target.orgId } : {}),\n ...(options.requestSource ? { requestSource: options.requestSource } : {}),\n });\n\n await writeActionChangeMarker({\n actionName: options.actionName,\n ...(target.owner ? { owner: target.owner } : {}),\n ...(target.orgId ? { orgId: target.orgId } : {}),\n ...(options.requestSource ? { requestSource: options.requestSource } : {}),\n });\n}\n"]}
1
+ {"version":3,"file":"action-change.js","sourceRoot":"","sources":["../../src/server/action-change.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ACTION_CHANGE_MARKER_KEY,\n actionChangeMarkerSession,\n actionChangeMarkerValue,\n type ActionChangeTarget,\n} from \"../action-change-marker.js\";\nimport type { ActionPlanModeConfig } from \"../action.js\";\nimport { appStatePut } from \"../application-state/store.js\";\nimport { recordChange } from \"./poll.js\";\nimport { getRequestOrgId, getRequestUserEmail } from \"./request-context.js\";\n\n/**\n * Whether THIS invocation should announce a change.\n *\n * `readOnly` describes an action name, but a tool that discriminates on an\n * argument reads on some calls and writes on others — `manage-agent-engine`\n * with `action: \"list\"` is a status poll, with `action: \"set\"` a write. Such\n * actions already publish the per-call answer as a Plan-mode `effect`\n * predicate, so read it here instead of treating every call as a write: a poll\n * that bumps the `\"action\"` change version makes every query keyed on it\n * refetch forever.\n *\n * `readOnly` stays authoritative otherwise. Widening the flag to cover mixed\n * tools would also open them to read-only A2A peers and to the agent's\n * read-result cache, where it would be false.\n */\nexport function actionCallIsReadOnly(\n entry: { readOnly?: boolean; planMode?: ActionPlanModeConfig<any> },\n params: unknown,\n fallback: boolean,\n): boolean {\n const effect = entry.planMode?.effect;\n if (typeof effect === \"function\") {\n try {\n return effect(params) === \"read\";\n } catch {\n // A predicate that throws says nothing; fall through to the flag.\n }\n }\n if (typeof entry.readOnly === \"boolean\") return entry.readOnly;\n return fallback;\n}\n\nexport interface NotifyActionChangeOptions {\n actionName: string;\n owner?: string;\n orgId?: string;\n requestSource?: string;\n}\n\nfunction actionChangeTarget(\n options: NotifyActionChangeOptions,\n): ActionChangeTarget {\n const owner = options.owner ?? getRequestUserEmail() ?? undefined;\n return {\n actionName: options.actionName,\n owner,\n orgId: owner ? undefined : (options.orgId ?? getRequestOrgId()),\n requestSource: options.requestSource,\n };\n}\n\nexport async function writeActionChangeMarker(\n options: NotifyActionChangeOptions,\n): Promise<void> {\n const target = actionChangeTarget(options);\n const sessionId = actionChangeMarkerSession(target);\n if (!sessionId) return;\n await appStatePut(\n sessionId,\n ACTION_CHANGE_MARKER_KEY,\n {\n ...actionChangeMarkerValue(target),\n nonce: `${Date.now()}-${Math.random().toString(36).slice(2)}`,\n },\n { requestSource: options.requestSource ?? \"agent\" },\n );\n}\n\nexport async function notifyActionChange(\n options: NotifyActionChangeOptions,\n): Promise<void> {\n const target = actionChangeTarget(options);\n recordChange({\n source: \"action\",\n type: \"change\",\n key: options.actionName,\n ...(target.owner ? { owner: target.owner } : {}),\n ...(target.orgId ? { orgId: target.orgId } : {}),\n ...(options.requestSource ? { requestSource: options.requestSource } : {}),\n });\n\n await writeActionChangeMarker({\n actionName: options.actionName,\n ...(target.owner ? { owner: target.owner } : {}),\n ...(target.orgId ? { orgId: target.orgId } : {}),\n ...(options.requestSource ? { requestSource: options.requestSource } : {}),\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"action-routes.d.ts","sourceRoot":"","sources":["../../src/server/action-routes.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAWhE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAC;AAoEhC,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,eAAe,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAMrB;AA2HD;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG;IAC7D;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,EAAE,CACd,KAAK,EAAE,GAAG,KAER,yBAAyB,GACzB,IAAI,GACJ,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,wBAAwB;IACvC,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,CACrB,KAAK,EAAE,GAAG,KACP,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,0DAA0D;IAC1D,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtE;;;;OAIG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAC;CAC1C;AAyCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,GAAG,EACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACpC,OAAO,CAAC,EAAE,wBAAwB,QAkXnC"}
1
+ {"version":3,"file":"action-routes.d.ts","sourceRoot":"","sources":["../../src/server/action-routes.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAWhE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAC;AAoEhC,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,eAAe,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAMrB;AA2HD;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG;IAC7D;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,EAAE,CACd,KAAK,EAAE,GAAG,KAER,yBAAyB,GACzB,IAAI,GACJ,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,wBAAwB;IACvC,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,CACrB,KAAK,EAAE,GAAG,KACP,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,0DAA0D;IAC1D,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtE;;;;OAIG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAC;CAC1C;AAyCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,GAAG,EACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACpC,OAAO,CAAC,EAAE,wBAAwB,QAoXnC"}
@@ -6,7 +6,7 @@ import { resolveOrgIdForEmail } from "../org/context.js";
6
6
  import { readBody } from "../server/h3-helpers.js";
7
7
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
8
8
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
9
- import { notifyActionChange } from "./action-change.js";
9
+ import { actionCallIsReadOnly, notifyActionChange } from "./action-change.js";
10
10
  import { seedAgentRunOwnerContext, } from "./agent-run-context.js";
11
11
  import { captureError } from "./capture-error.js";
12
12
  import { getAllowedCorsOrigin as resolveAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
@@ -437,13 +437,12 @@ export function mountActionRoutes(nitroApp, actions, options) {
437
437
  // their action queries. The calling tab already refetches via
438
438
  // useActionMutation's onSuccess, so this is mainly cross-tab
439
439
  // sync (and parity with the agent's tool-call path).
440
- // Explicit entry.readOnly (true OR false) wins over the method
441
- // heuristic. defineAction already auto-infers GET readOnly=true,
442
- // so for actions registered through that path entry.readOnly is
443
- // always set and the fallback just guards legacy wrap paths.
444
- const isReadOnly = typeof entry.readOnly === "boolean"
445
- ? entry.readOnly
446
- : method === "GET";
440
+ // A per-call Plan-mode effect wins over entry.readOnly, which
441
+ // wins (true OR false) over the method heuristic. defineAction
442
+ // already auto-infers GET readOnly=true, so for actions
443
+ // registered through that path entry.readOnly is always set and
444
+ // the fallback just guards legacy wrap paths.
445
+ const isReadOnly = actionCallIsReadOnly(entry, params, method === "GET");
447
446
  if (!isReadOnly) {
448
447
  try {
449
448
  await notifyActionChange({