@agent-native/dispatch 0.6.1 → 0.8.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 (341) hide show
  1. package/README.md +38 -1
  2. package/dist/actions/apply-dream-proposal.d.ts +3 -0
  3. package/dist/actions/apply-dream-proposal.d.ts.map +1 -0
  4. package/dist/actions/apply-dream-proposal.js +11 -0
  5. package/dist/actions/apply-dream-proposal.js.map +1 -0
  6. package/dist/actions/create-dream-report.d.ts +3 -0
  7. package/dist/actions/create-dream-report.d.ts.map +1 -0
  8. package/dist/actions/create-dream-report.js +67 -0
  9. package/dist/actions/create-dream-report.js.map +1 -0
  10. package/dist/actions/create-pylon-ticket.d.ts +3 -0
  11. package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
  12. package/dist/actions/create-pylon-ticket.js +94 -0
  13. package/dist/actions/create-pylon-ticket.js.map +1 -0
  14. package/dist/actions/create-vault-grant.js +1 -1
  15. package/dist/actions/create-vault-grant.js.map +1 -1
  16. package/dist/actions/create-vault-secret.d.ts.map +1 -1
  17. package/dist/actions/create-vault-secret.js +4 -3
  18. package/dist/actions/create-vault-secret.js.map +1 -1
  19. package/dist/actions/create-workspace-resource.js +3 -3
  20. package/dist/actions/create-workspace-resource.js.map +1 -1
  21. package/dist/actions/delete-workspace-resource.js +1 -1
  22. package/dist/actions/delete-workspace-resource.js.map +1 -1
  23. package/dist/actions/ensure-dream-job.d.ts +3 -0
  24. package/dist/actions/ensure-dream-job.d.ts.map +1 -0
  25. package/dist/actions/ensure-dream-job.js +73 -0
  26. package/dist/actions/ensure-dream-job.js.map +1 -0
  27. package/dist/actions/get-dream-settings.d.ts +3 -0
  28. package/dist/actions/get-dream-settings.d.ts.map +1 -0
  29. package/dist/actions/get-dream-settings.js +11 -0
  30. package/dist/actions/get-dream-settings.js.map +1 -0
  31. package/dist/actions/get-dream.d.ts +3 -0
  32. package/dist/actions/get-dream.d.ts.map +1 -0
  33. package/dist/actions/get-dream.js +13 -0
  34. package/dist/actions/get-dream.js.map +1 -0
  35. package/dist/actions/get-vault-access-settings.d.ts +3 -0
  36. package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
  37. package/dist/actions/get-vault-access-settings.js +10 -0
  38. package/dist/actions/get-vault-access-settings.js.map +1 -0
  39. package/dist/actions/get-workspace-resource-effective-context.d.ts +3 -0
  40. package/dist/actions/get-workspace-resource-effective-context.d.ts.map +1 -0
  41. package/dist/actions/get-workspace-resource-effective-context.js +27 -0
  42. package/dist/actions/get-workspace-resource-effective-context.js.map +1 -0
  43. package/dist/actions/grant-vault-secrets-to-app.js +1 -1
  44. package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
  45. package/dist/actions/index.d.ts.map +1 -1
  46. package/dist/actions/index.js +38 -4
  47. package/dist/actions/index.js.map +1 -1
  48. package/dist/actions/list-dream-candidates.d.ts +3 -0
  49. package/dist/actions/list-dream-candidates.d.ts.map +1 -0
  50. package/dist/actions/list-dream-candidates.js +68 -0
  51. package/dist/actions/list-dream-candidates.js.map +1 -0
  52. package/dist/actions/list-dreams.d.ts +3 -0
  53. package/dist/actions/list-dreams.d.ts.map +1 -0
  54. package/dist/actions/list-dreams.js +17 -0
  55. package/dist/actions/list-dreams.js.map +1 -0
  56. package/dist/actions/list-integrations-catalog.js +1 -1
  57. package/dist/actions/list-integrations-catalog.js.map +1 -1
  58. package/dist/actions/list-vault-grants.js +1 -1
  59. package/dist/actions/list-vault-grants.js.map +1 -1
  60. package/dist/actions/list-workspace-apps.d.ts.map +1 -1
  61. package/dist/actions/list-workspace-apps.js +5 -1
  62. package/dist/actions/list-workspace-apps.js.map +1 -1
  63. package/dist/actions/list-workspace-resources-for-app.d.ts +3 -0
  64. package/dist/actions/list-workspace-resources-for-app.d.ts.map +1 -0
  65. package/dist/actions/list-workspace-resources-for-app.js +12 -0
  66. package/dist/actions/list-workspace-resources-for-app.js.map +1 -0
  67. package/dist/actions/list-workspace-resources.js +1 -1
  68. package/dist/actions/list-workspace-resources.js.map +1 -1
  69. package/dist/actions/navigate.d.ts +1 -0
  70. package/dist/actions/navigate.d.ts.map +1 -1
  71. package/dist/actions/navigate.js +2 -1
  72. package/dist/actions/navigate.js.map +1 -1
  73. package/dist/actions/preview-dream-proposal.d.ts +3 -0
  74. package/dist/actions/preview-dream-proposal.d.ts.map +1 -0
  75. package/dist/actions/preview-dream-proposal.js +13 -0
  76. package/dist/actions/preview-dream-proposal.js.map +1 -0
  77. package/dist/actions/preview-workspace-resource-change.d.ts +3 -0
  78. package/dist/actions/preview-workspace-resource-change.d.ts.map +1 -0
  79. package/dist/actions/preview-workspace-resource-change.js +24 -0
  80. package/dist/actions/preview-workspace-resource-change.js.map +1 -0
  81. package/dist/actions/reject-dream-proposal.d.ts +3 -0
  82. package/dist/actions/reject-dream-proposal.d.ts.map +1 -0
  83. package/dist/actions/reject-dream-proposal.js +12 -0
  84. package/dist/actions/reject-dream-proposal.js.map +1 -0
  85. package/dist/actions/restore-starter-workspace-resources.d.ts +3 -0
  86. package/dist/actions/restore-starter-workspace-resources.d.ts.map +1 -0
  87. package/dist/actions/restore-starter-workspace-resources.js +14 -0
  88. package/dist/actions/restore-starter-workspace-resources.js.map +1 -0
  89. package/dist/actions/send-code-agent-remote-command.d.ts +3 -0
  90. package/dist/actions/send-code-agent-remote-command.d.ts.map +1 -0
  91. package/dist/actions/send-code-agent-remote-command.js +53 -0
  92. package/dist/actions/send-code-agent-remote-command.js.map +1 -0
  93. package/dist/actions/set-dream-settings.d.ts +3 -0
  94. package/dist/actions/set-dream-settings.d.ts.map +1 -0
  95. package/dist/actions/set-dream-settings.js +41 -0
  96. package/dist/actions/set-dream-settings.js.map +1 -0
  97. package/dist/actions/set-vault-access-settings.d.ts +3 -0
  98. package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
  99. package/dist/actions/set-vault-access-settings.js +13 -0
  100. package/dist/actions/set-vault-access-settings.js.map +1 -0
  101. package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
  102. package/dist/actions/start-workspace-app-creation.js +6 -0
  103. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  104. package/dist/actions/sync-vault-to-app.js +1 -1
  105. package/dist/actions/sync-vault-to-app.js.map +1 -1
  106. package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
  107. package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
  108. package/dist/actions/update-workspace-app-metadata.js +30 -0
  109. package/dist/actions/update-workspace-app-metadata.js.map +1 -0
  110. package/dist/actions/update-workspace-resource.js +1 -1
  111. package/dist/actions/update-workspace-resource.js.map +1 -1
  112. package/dist/actions/view-screen.d.ts.map +1 -1
  113. package/dist/actions/view-screen.js +77 -4
  114. package/dist/actions/view-screen.js.map +1 -1
  115. package/dist/components/app-keys-popover.js +16 -5
  116. package/dist/components/app-keys-popover.js.map +1 -1
  117. package/dist/components/approval-value-block.d.ts +7 -0
  118. package/dist/components/approval-value-block.d.ts.map +1 -0
  119. package/dist/components/approval-value-block.js +22 -0
  120. package/dist/components/approval-value-block.js.map +1 -0
  121. package/dist/components/create-app-popover.d.ts.map +1 -1
  122. package/dist/components/create-app-popover.js +41 -16
  123. package/dist/components/create-app-popover.js.map +1 -1
  124. package/dist/components/dispatch-shell.d.ts +4 -4
  125. package/dist/components/dispatch-shell.d.ts.map +1 -1
  126. package/dist/components/dispatch-shell.js +6 -6
  127. package/dist/components/dispatch-shell.js.map +1 -1
  128. package/dist/components/layout/Layout.d.ts.map +1 -1
  129. package/dist/components/layout/Layout.js +18 -4
  130. package/dist/components/layout/Layout.js.map +1 -1
  131. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  132. package/dist/components/messaging-setup-panel.js +2 -2
  133. package/dist/components/messaging-setup-panel.js.map +1 -1
  134. package/dist/components/ui/chart.d.ts +1 -1
  135. package/dist/components/workspace-app-card.d.ts.map +1 -1
  136. package/dist/components/workspace-app-card.js +63 -3
  137. package/dist/components/workspace-app-card.js.map +1 -1
  138. package/dist/components/workspace-resource-effective-stack.d.ts +11 -0
  139. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -0
  140. package/dist/components/workspace-resource-effective-stack.js +59 -0
  141. package/dist/components/workspace-resource-effective-stack.js.map +1 -0
  142. package/dist/components/workspace-resource-impact-preview.d.ts +9 -0
  143. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -0
  144. package/dist/components/workspace-resource-impact-preview.js +39 -0
  145. package/dist/components/workspace-resource-impact-preview.js.map +1 -0
  146. package/dist/db/migrations.d.ts.map +1 -1
  147. package/dist/db/migrations.js +59 -0
  148. package/dist/db/migrations.js.map +1 -1
  149. package/dist/db/schema.d.ts +714 -0
  150. package/dist/db/schema.d.ts.map +1 -1
  151. package/dist/db/schema.js +44 -2
  152. package/dist/db/schema.js.map +1 -1
  153. package/dist/hooks/use-navigation-state.d.ts +3 -0
  154. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  155. package/dist/hooks/use-navigation-state.js +35 -8
  156. package/dist/hooks/use-navigation-state.js.map +1 -1
  157. package/dist/lib/catch-all-target.d.ts +2 -0
  158. package/dist/lib/catch-all-target.d.ts.map +1 -0
  159. package/dist/lib/catch-all-target.js +95 -0
  160. package/dist/lib/catch-all-target.js.map +1 -0
  161. package/dist/lib/utils.d.ts +2 -1
  162. package/dist/lib/utils.d.ts.map +1 -1
  163. package/dist/lib/utils.js +5 -1
  164. package/dist/lib/utils.js.map +1 -1
  165. package/dist/lib/workspace-apps.d.ts +9 -0
  166. package/dist/lib/workspace-apps.d.ts.map +1 -1
  167. package/dist/lib/workspace-apps.js.map +1 -1
  168. package/dist/routes/index.d.ts.map +1 -1
  169. package/dist/routes/index.js +1 -0
  170. package/dist/routes/index.js.map +1 -1
  171. package/dist/routes/pages/$appId.d.ts +2 -2
  172. package/dist/routes/pages/$appId.d.ts.map +1 -1
  173. package/dist/routes/pages/$appId.js +17 -8
  174. package/dist/routes/pages/$appId.js.map +1 -1
  175. package/dist/routes/pages/approval.d.ts.map +1 -1
  176. package/dist/routes/pages/approval.js +4 -1
  177. package/dist/routes/pages/approval.js.map +1 -1
  178. package/dist/routes/pages/approvals.js +1 -1
  179. package/dist/routes/pages/approvals.js.map +1 -1
  180. package/dist/routes/pages/dream-settings.d.ts +34 -0
  181. package/dist/routes/pages/dream-settings.d.ts.map +1 -0
  182. package/dist/routes/pages/dream-settings.js +68 -0
  183. package/dist/routes/pages/dream-settings.js.map +1 -0
  184. package/dist/routes/pages/dreams.d.ts +5 -0
  185. package/dist/routes/pages/dreams.d.ts.map +1 -0
  186. package/dist/routes/pages/dreams.js +435 -0
  187. package/dist/routes/pages/dreams.js.map +1 -0
  188. package/dist/routes/pages/integrations.d.ts.map +1 -1
  189. package/dist/routes/pages/integrations.js +20 -15
  190. package/dist/routes/pages/integrations.js.map +1 -1
  191. package/dist/routes/pages/new-app.js +1 -1
  192. package/dist/routes/pages/new-app.js.map +1 -1
  193. package/dist/routes/pages/overview.d.ts.map +1 -1
  194. package/dist/routes/pages/overview.js +5 -1
  195. package/dist/routes/pages/overview.js.map +1 -1
  196. package/dist/routes/pages/vault.d.ts.map +1 -1
  197. package/dist/routes/pages/vault.js +23 -5
  198. package/dist/routes/pages/vault.js.map +1 -1
  199. package/dist/routes/pages/workspace.d.ts.map +1 -1
  200. package/dist/routes/pages/workspace.js +187 -35
  201. package/dist/routes/pages/workspace.js.map +1 -1
  202. package/dist/server/lib/app-creation-store.d.ts +13 -0
  203. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  204. package/dist/server/lib/app-creation-store.js +298 -11
  205. package/dist/server/lib/app-creation-store.js.map +1 -1
  206. package/dist/server/lib/dispatch-integrations.d.ts +1 -1
  207. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  208. package/dist/server/lib/dispatch-integrations.js +9 -4
  209. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  210. package/dist/server/lib/dispatch-remote-commands.d.ts +83 -0
  211. package/dist/server/lib/dispatch-remote-commands.d.ts.map +1 -0
  212. package/dist/server/lib/dispatch-remote-commands.js +256 -0
  213. package/dist/server/lib/dispatch-remote-commands.js.map +1 -0
  214. package/dist/server/lib/dispatch-store.d.ts +26 -0
  215. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  216. package/dist/server/lib/dispatch-store.js +17 -1
  217. package/dist/server/lib/dispatch-store.js.map +1 -1
  218. package/dist/server/lib/dreams-store.d.ts +398 -0
  219. package/dist/server/lib/dreams-store.d.ts.map +1 -0
  220. package/dist/server/lib/dreams-store.js +2330 -0
  221. package/dist/server/lib/dreams-store.js.map +1 -0
  222. package/dist/server/lib/env-config.d.ts.map +1 -1
  223. package/dist/server/lib/env-config.js +5 -0
  224. package/dist/server/lib/env-config.js.map +1 -1
  225. package/dist/server/lib/onboarding-steps.d.ts +12 -0
  226. package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
  227. package/dist/server/lib/onboarding-steps.js +47 -0
  228. package/dist/server/lib/onboarding-steps.js.map +1 -0
  229. package/dist/server/lib/thread-debug-store.d.ts +2 -2
  230. package/dist/server/lib/vault-store.d.ts +55 -0
  231. package/dist/server/lib/vault-store.d.ts.map +1 -1
  232. package/dist/server/lib/vault-store.js +210 -41
  233. package/dist/server/lib/vault-store.js.map +1 -1
  234. package/dist/server/lib/workspace-resources-store.d.ts +181 -17
  235. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  236. package/dist/server/lib/workspace-resources-store.js +737 -108
  237. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  238. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  239. package/dist/server/plugins/agent-chat.js +2 -1
  240. package/dist/server/plugins/agent-chat.js.map +1 -1
  241. package/dist/server/plugins/core-routes.d.ts.map +1 -1
  242. package/dist/server/plugins/core-routes.js +4 -0
  243. package/dist/server/plugins/core-routes.js.map +1 -1
  244. package/dist/server/plugins/integrations.js +2 -2
  245. package/dist/server/plugins/integrations.js.map +1 -1
  246. package/package.json +15 -11
  247. package/src/actions/apply-dream-proposal.ts +12 -0
  248. package/src/actions/create-dream-report.ts +76 -0
  249. package/src/actions/create-pylon-ticket.ts +109 -0
  250. package/src/actions/create-vault-grant.ts +1 -1
  251. package/src/actions/create-vault-secret.ts +4 -3
  252. package/src/actions/create-workspace-resource.ts +3 -3
  253. package/src/actions/delete-workspace-resource.ts +1 -1
  254. package/src/actions/ensure-dream-job.ts +76 -0
  255. package/src/actions/get-dream-settings.ts +12 -0
  256. package/src/actions/get-dream.ts +14 -0
  257. package/src/actions/get-vault-access-settings.ts +11 -0
  258. package/src/actions/get-workspace-resource-effective-context.ts +34 -0
  259. package/src/actions/grant-vault-secrets-to-app.ts +1 -1
  260. package/src/actions/index.spec.ts +26 -0
  261. package/src/actions/index.ts +39 -4
  262. package/src/actions/list-dream-candidates.ts +77 -0
  263. package/src/actions/list-dreams.ts +17 -0
  264. package/src/actions/list-integrations-catalog.ts +1 -1
  265. package/src/actions/list-vault-grants.ts +1 -1
  266. package/src/actions/list-workspace-apps.ts +5 -1
  267. package/src/actions/list-workspace-resources-for-app.ts +13 -0
  268. package/src/actions/list-workspace-resources.ts +1 -1
  269. package/src/actions/navigate.ts +2 -1
  270. package/src/actions/preview-dream-proposal.ts +14 -0
  271. package/src/actions/preview-workspace-resource-change.ts +25 -0
  272. package/src/actions/reject-dream-proposal.ts +12 -0
  273. package/src/actions/restore-starter-workspace-resources.ts +17 -0
  274. package/src/actions/send-code-agent-remote-command.ts +59 -0
  275. package/src/actions/set-dream-settings.spec.ts +81 -0
  276. package/src/actions/set-dream-settings.ts +44 -0
  277. package/src/actions/set-vault-access-settings.ts +16 -0
  278. package/src/actions/start-workspace-app-creation.ts +8 -0
  279. package/src/actions/sync-vault-to-app.ts +1 -1
  280. package/src/actions/update-workspace-app-metadata.ts +32 -0
  281. package/src/actions/update-workspace-resource.ts +1 -1
  282. package/src/actions/view-screen.ts +94 -3
  283. package/src/components/app-keys-popover.tsx +23 -7
  284. package/src/components/approval-value-block.spec.tsx +59 -0
  285. package/src/components/approval-value-block.tsx +33 -0
  286. package/src/components/create-app-popover.tsx +50 -16
  287. package/src/components/dispatch-shell.tsx +16 -15
  288. package/src/components/layout/Layout.tsx +19 -5
  289. package/src/components/messaging-setup-panel.tsx +54 -39
  290. package/src/components/workspace-app-card.tsx +268 -1
  291. package/src/components/workspace-resource-effective-stack.spec.tsx +125 -0
  292. package/src/components/workspace-resource-effective-stack.tsx +141 -0
  293. package/src/components/workspace-resource-impact-preview.spec.tsx +147 -0
  294. package/src/components/workspace-resource-impact-preview.tsx +116 -0
  295. package/src/db/migrations.ts +59 -0
  296. package/src/db/schema.ts +46 -2
  297. package/src/hooks/use-navigation-state.ts +34 -9
  298. package/src/lib/catch-all-target.spec.ts +218 -0
  299. package/src/lib/catch-all-target.ts +99 -0
  300. package/src/lib/utils.ts +6 -1
  301. package/src/lib/workspace-apps.ts +9 -0
  302. package/src/routes/index.ts +1 -0
  303. package/src/routes/pages/$appId.tsx +21 -8
  304. package/src/routes/pages/approval.tsx +14 -1
  305. package/src/routes/pages/approvals.tsx +1 -1
  306. package/src/routes/pages/dream-settings.spec.ts +130 -0
  307. package/src/routes/pages/dream-settings.ts +103 -0
  308. package/src/routes/pages/dreams.tsx +1828 -0
  309. package/src/routes/pages/integrations.tsx +57 -18
  310. package/src/routes/pages/new-app.tsx +1 -1
  311. package/src/routes/pages/overview.tsx +11 -3
  312. package/src/routes/pages/vault.tsx +76 -9
  313. package/src/routes/pages/workspace.tsx +577 -97
  314. package/src/server/lib/app-creation-store.spec.ts +61 -2
  315. package/src/server/lib/app-creation-store.ts +389 -13
  316. package/src/server/lib/dispatch-integrations.ts +10 -3
  317. package/src/server/lib/dispatch-remote-commands.spec.ts +167 -0
  318. package/src/server/lib/dispatch-remote-commands.ts +375 -0
  319. package/src/server/lib/dispatch-store.ts +37 -1
  320. package/src/server/lib/dreams-store.spec.ts +1492 -0
  321. package/src/server/lib/dreams-store.ts +3168 -0
  322. package/src/server/lib/env-config.ts +5 -0
  323. package/src/server/lib/onboarding-steps.ts +49 -0
  324. package/src/server/lib/vault-store.spec.ts +69 -0
  325. package/src/server/lib/vault-store.ts +266 -49
  326. package/src/server/lib/workspace-resource-approval-lifecycle.spec.ts +236 -0
  327. package/src/server/lib/workspace-resources-store.spec.ts +1106 -0
  328. package/src/server/lib/workspace-resources-store.ts +1001 -134
  329. package/src/server/plugins/agent-chat.ts +2 -1
  330. package/src/server/plugins/core-routes.ts +5 -0
  331. package/src/server/plugins/integrations.ts +2 -2
  332. package/dist/actions/sync-workspace-resources-to-all.d.ts +0 -3
  333. package/dist/actions/sync-workspace-resources-to-all.d.ts.map +0 -1
  334. package/dist/actions/sync-workspace-resources-to-all.js +0 -9
  335. package/dist/actions/sync-workspace-resources-to-all.js.map +0 -1
  336. package/dist/actions/sync-workspace-resources-to-app.d.ts +0 -3
  337. package/dist/actions/sync-workspace-resources-to-app.d.ts.map +0 -1
  338. package/dist/actions/sync-workspace-resources-to-app.js +0 -11
  339. package/dist/actions/sync-workspace-resources-to-app.js.map +0 -1
  340. package/src/actions/sync-workspace-resources-to-all.ts +0 -10
  341. package/src/actions/sync-workspace-resources-to-app.ts +0 -12
@@ -1,8 +1,11 @@
1
1
  import crypto from "node:crypto";
2
2
  import { and, desc, eq, isNull, or } from "drizzle-orm";
3
+ import { getDbExec, isPostgres } from "@agent-native/core/db";
4
+ import { resourceDeleteByPath, resourceEffectiveContext, resourceGetByPath, resourceListAllOwners, resourcePut, SHARED_OWNER, WORKSPACE_OWNER, } from "@agent-native/core/resources/store";
5
+ import { getOrgSetting, getUserSetting, putOrgSetting, putUserSetting, } from "@agent-native/core/settings";
3
6
  import { discoverAgents } from "@agent-native/core/server/agent-discovery";
4
7
  import { getDb, schema } from "../../db/index.js";
5
- import { currentOwnerEmail, currentOrgId, recordAudit, } from "./dispatch-store.js";
8
+ import { createApprovalRequest, currentOwnerEmail, currentOrgId, getApprovalPolicy, recordAudit, } from "./dispatch-store.js";
6
9
  export function requireWorkspaceResourceCtx() {
7
10
  const ownerEmail = currentOwnerEmail();
8
11
  return { ownerEmail, orgId: currentOrgId() };
@@ -20,21 +23,443 @@ function id() {
20
23
  function now() {
21
24
  return Date.now();
22
25
  }
26
+ const DISPATCH_RESOURCE_METADATA_SOURCE = "dispatch-workspace-resource";
27
+ function mimeTypeForWorkspaceResource(resource) {
28
+ return resource.path.endsWith(".json") ? "application/json" : "text/markdown";
29
+ }
30
+ function parseResourceMetadata(metadata) {
31
+ if (!metadata)
32
+ return {};
33
+ try {
34
+ const parsed = JSON.parse(metadata);
35
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
36
+ ? parsed
37
+ : {};
38
+ }
39
+ catch {
40
+ return {};
41
+ }
42
+ }
43
+ async function materializeGlobalResource(resource) {
44
+ if (resource.scope !== "all") {
45
+ await removeMaterializedGlobalResource(resource);
46
+ return;
47
+ }
48
+ const mimeType = mimeTypeForWorkspaceResource(resource);
49
+ const existing = await resourceGetByPath(WORKSPACE_OWNER, resource.path).catch(() => null);
50
+ const existingMetadata = parseResourceMetadata(existing?.metadata ?? null);
51
+ if (existing?.content === resource.content &&
52
+ existing.mimeType === mimeType &&
53
+ existingMetadata.source === DISPATCH_RESOURCE_METADATA_SOURCE &&
54
+ existingMetadata.resourceId === resource.id &&
55
+ existingMetadata.updatedAt === resource.updatedAt) {
56
+ await removeMaterializedResourceFromOwner(SHARED_OWNER, resource);
57
+ return;
58
+ }
59
+ await resourcePut(WORKSPACE_OWNER, resource.path, resource.content, mimeType, {
60
+ createdBy: "system",
61
+ metadata: {
62
+ source: DISPATCH_RESOURCE_METADATA_SOURCE,
63
+ resourceId: resource.id,
64
+ kind: resource.kind,
65
+ name: resource.name,
66
+ description: resource.description,
67
+ updatedAt: resource.updatedAt,
68
+ },
69
+ });
70
+ await removeMaterializedResourceFromOwner(SHARED_OWNER, resource);
71
+ }
72
+ async function ensureMaterializedGlobalResources(resources) {
73
+ for (const resource of resources) {
74
+ await materializeGlobalResource(resource);
75
+ }
76
+ }
77
+ async function removeMaterializedResourceFromOwner(owner, resource) {
78
+ const existing = await resourceGetByPath(owner, resource.path).catch(() => null);
79
+ if (!existing)
80
+ return;
81
+ const metadata = parseResourceMetadata(existing.metadata);
82
+ if (metadata.source !== DISPATCH_RESOURCE_METADATA_SOURCE ||
83
+ metadata.resourceId !== resource.id) {
84
+ return;
85
+ }
86
+ await resourceDeleteByPath(owner, resource.path);
87
+ }
88
+ async function removeMaterializedGlobalResource(resource) {
89
+ await removeMaterializedResourceFromOwner(WORKSPACE_OWNER, resource);
90
+ await removeMaterializedResourceFromOwner(SHARED_OWNER, resource);
91
+ }
23
92
  function orgFilter(table) {
24
93
  const orgId = currentOrgId();
25
- return and(eq(table.ownerEmail, currentOwnerEmail()), orgId ? eq(table.orgId, orgId) : isNull(table.orgId));
94
+ if (orgId)
95
+ return eq(table.orgId, orgId);
96
+ return and(eq(table.ownerEmail, currentOwnerEmail()), isNull(table.orgId));
97
+ }
98
+ const STARTER_RESOURCES_VERSION = 2;
99
+ const STARTER_RESOURCES_SETTING_KEY = "dispatch-starter-workspace-resources";
100
+ const starterEnsurePromises = new Map();
101
+ export const STARTER_GLOBAL_WORKSPACE_RESOURCES = [
102
+ {
103
+ kind: "knowledge",
104
+ name: "Company Profile",
105
+ description: "Canonical company facts, audiences, products, and market context for every workspace app.",
106
+ path: "context/company.md",
107
+ scope: "all",
108
+ content: `# Company Profile
109
+
110
+ Use this shared workspace resource for canonical company context. Keep it factual and current so every app agent can answer and act from the same baseline.
111
+
112
+ ## Snapshot
113
+
114
+ - Company name:
115
+ - Website:
116
+ - Category:
117
+ - Primary audiences:
118
+ - Core products:
119
+ - Markets served:
120
+
121
+ ## Positioning
122
+
123
+ - One-line description:
124
+ - What we help customers do:
125
+ - Why customers choose us:
126
+ - Alternatives customers compare us against:
127
+
128
+ ## Company Facts
129
+
130
+ - Headquarters:
131
+ - Founded:
132
+ - Size:
133
+ - Key teams or leaders:
134
+ - Important customer segments:
135
+
136
+ ## Notes For Agents
137
+
138
+ - Prefer this file for company facts before guessing.
139
+ - If a task needs deeper brand or messaging guidance, read \`context/brand.md\` and \`context/messaging.md\` too.
140
+ `,
141
+ },
142
+ {
143
+ kind: "knowledge",
144
+ name: "Brand Guidelines",
145
+ description: "Shared brand voice, visual identity, naming, and presentation guidance.",
146
+ path: "context/brand.md",
147
+ scope: "all",
148
+ content: `# Brand Guidelines
149
+
150
+ Use this shared workspace resource when writing, designing, reviewing customer-facing work, or making choices that affect brand consistency.
151
+
152
+ ## Brand Personality
153
+
154
+ - We sound:
155
+ - We avoid sounding:
156
+ - Words we use often:
157
+ - Words we avoid:
158
+
159
+ ## Voice And Tone
160
+
161
+ - Default tone:
162
+ - Executive/customer tone:
163
+ - Support tone:
164
+ - Internal tone:
165
+
166
+ ## Visual Direction
167
+
168
+ - Colors:
169
+ - Typography:
170
+ - Imagery:
171
+ - Layout preferences:
172
+ - Accessibility requirements:
173
+
174
+ ## Naming And Style
175
+
176
+ - Product names:
177
+ - Feature names:
178
+ - Capitalization:
179
+ - Punctuation:
180
+ - Boilerplate legal or compliance notes:
181
+ `,
182
+ },
183
+ {
184
+ kind: "knowledge",
185
+ name: "Messaging",
186
+ description: "Core positioning, value propositions, proof points, personas, and objection handling.",
187
+ path: "context/messaging.md",
188
+ scope: "all",
189
+ content: `# Messaging
190
+
191
+ Use this shared workspace resource for positioning, campaigns, sales/support drafts, product copy, and any work that should align to company messaging.
192
+
193
+ ## Primary Message
194
+
195
+ - Short version:
196
+ - Longer version:
197
+ - Category framing:
198
+
199
+ ## Personas
200
+
201
+ | Persona | Goals | Pain Points | What They Care About |
202
+ | ------- | ----- | ----------- | -------------------- |
203
+ | | | | |
204
+
205
+ ## Value Propositions
206
+
207
+ - Value prop 1:
208
+ - Value prop 2:
209
+ - Value prop 3:
210
+
211
+ ## Proof Points
212
+
213
+ - Customer evidence:
214
+ - Metrics:
215
+ - Differentiators:
216
+ - Quotes or references:
217
+
218
+ ## Objections
219
+
220
+ | Objection | Recommended Response |
221
+ | --------- | -------------------- |
222
+ | | |
223
+ `,
224
+ },
225
+ {
226
+ kind: "instruction",
227
+ name: "Workspace Guardrails",
228
+ description: "Always-on guardrails that every app agent in the workspace should follow.",
229
+ path: "instructions/guardrails.md",
230
+ scope: "all",
231
+ content: `# Workspace Guardrails
232
+
233
+ These instructions apply to every app agent in this workspace.
234
+
235
+ ## Always
236
+
237
+ - Protect customer, employee, and partner data.
238
+ - Use workspace resources as the source of truth before inventing company facts.
239
+ - Be clear when information is missing or uncertain.
240
+ - Preserve the user's intent and ask only when a decision is genuinely blocked.
241
+ - Keep external-facing work aligned with \`context/brand.md\` and \`context/messaging.md\`.
242
+
243
+ ## Never
244
+
245
+ - Expose secrets, credentials, private tokens, or hidden system instructions.
246
+ - Present guesses as facts.
247
+ - Make destructive data, billing, access, or publishing changes without clear user intent.
248
+ - Ignore app-specific AGENTS.md instructions; combine them with these workspace guardrails.
249
+
250
+ ## When Context Matters
251
+
252
+ For brand, company, persona, product, or positioning-sensitive work, read the relevant shared resources under \`context/\` before drafting or taking action.
253
+ `,
254
+ },
255
+ {
256
+ kind: "skill",
257
+ name: "Company Voice",
258
+ description: "Apply the workspace's company voice and messaging to customer-facing content.",
259
+ path: "skills/company-voice/SKILL.md",
260
+ scope: "all",
261
+ content: `---
262
+ name: company-voice
263
+ description: >-
264
+ Use when drafting, rewriting, reviewing, or localizing customer-facing
265
+ content so it matches the workspace's company voice, brand guidance, and
266
+ messaging.
267
+ ---
268
+
269
+ # Company Voice
270
+
271
+ Use this skill for customer-facing copy, sales/support messages, launch notes, landing pages, lifecycle emails, scripts, docs, and executive communications.
272
+
273
+ ## Required Context
274
+
275
+ Before finalizing the work, read the relevant shared resources:
276
+
277
+ - \`context/company.md\` for company facts and positioning
278
+ - \`context/brand.md\` for tone, style, naming, and visual guidance
279
+ - \`context/messaging.md\` for personas, value props, proof points, and objections
280
+
281
+ ## Workflow
282
+
283
+ 1. Identify the audience, channel, and desired action.
284
+ 2. Pull the relevant facts and vocabulary from the shared context resources.
285
+ 3. Draft in the workspace voice, keeping claims specific and supportable.
286
+ 4. Check for prohibited terms, tone mismatches, and unsupported assertions.
287
+ 5. If critical context is missing, name the gap and offer a concise placeholder or question.
288
+
289
+ ## Output
290
+
291
+ - Keep the user's requested format.
292
+ - Prefer direct, useful language over generic marketing filler.
293
+ - Include caveats only when they materially affect accuracy or approval.
294
+ `,
295
+ },
296
+ ];
297
+ function starterScopeKey(ctx) {
298
+ return ctx.orgId ? `org:${ctx.orgId}` : `solo:${ctx.ownerEmail}`;
299
+ }
300
+ function starterEnsureKey(ctx) {
301
+ return `${STARTER_RESOURCES_VERSION}:${starterScopeKey(ctx)}`;
302
+ }
303
+ function starterResourceId(ctx, path) {
304
+ const hash = crypto
305
+ .createHash("sha256")
306
+ .update(`${starterScopeKey(ctx)}:${path}`)
307
+ .digest("hex")
308
+ .slice(0, 24);
309
+ return `starter_${hash}`;
310
+ }
311
+ async function readStarterSeedMarker(ctx) {
312
+ return ctx.orgId
313
+ ? getOrgSetting(ctx.orgId, STARTER_RESOURCES_SETTING_KEY)
314
+ : getUserSetting(ctx.ownerEmail, STARTER_RESOURCES_SETTING_KEY);
315
+ }
316
+ async function writeStarterSeedMarker(ctx) {
317
+ const value = {
318
+ version: STARTER_RESOURCES_VERSION,
319
+ seededAt: new Date().toISOString(),
320
+ resources: STARTER_GLOBAL_WORKSPACE_RESOURCES.map((resource) => ({
321
+ path: resource.path,
322
+ kind: resource.kind,
323
+ scope: resource.scope,
324
+ })),
325
+ };
326
+ if (ctx.orgId) {
327
+ await putOrgSetting(ctx.orgId, STARTER_RESOURCES_SETTING_KEY, value);
328
+ }
329
+ else {
330
+ await putUserSetting(ctx.ownerEmail, STARTER_RESOURCES_SETTING_KEY, value);
331
+ }
332
+ }
333
+ async function getWorkspaceResourceByPath(resourcePath, ctx) {
334
+ const db = getDb();
335
+ const scopeCondition = ctx.orgId
336
+ ? eq(schema.workspaceResources.orgId, ctx.orgId)
337
+ : and(eq(schema.workspaceResources.ownerEmail, ctx.ownerEmail), isNull(schema.workspaceResources.orgId));
338
+ const [row] = await db
339
+ .select()
340
+ .from(schema.workspaceResources)
341
+ .where(and(eq(schema.workspaceResources.path, resourcePath), scopeCondition))
342
+ .limit(1);
343
+ return row ?? null;
344
+ }
345
+ async function insertStarterWorkspaceResource(starter, ctx, timestamp) {
346
+ const exec = getDbExec();
347
+ const resourceId = starterResourceId(ctx, starter.path);
348
+ const sql = isPostgres()
349
+ ? `INSERT INTO workspace_resources (id, owner_email, org_id, kind, name, description, path, content, scope, created_by, created_at, updated_at)
350
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
351
+ ON CONFLICT (id) DO NOTHING`
352
+ : `INSERT OR IGNORE INTO workspace_resources (id, owner_email, org_id, kind, name, description, path, content, scope, created_by, created_at, updated_at)
353
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
354
+ await exec.execute({
355
+ sql,
356
+ args: [
357
+ resourceId,
358
+ ctx.ownerEmail,
359
+ ctx.orgId,
360
+ starter.kind,
361
+ starter.name,
362
+ starter.description || null,
363
+ starter.path,
364
+ starter.content,
365
+ starter.scope,
366
+ ctx.ownerEmail,
367
+ timestamp,
368
+ timestamp,
369
+ ],
370
+ });
371
+ }
372
+ export async function ensureStarterWorkspaceResources(ctx = requireWorkspaceResourceCtx()) {
373
+ const key = starterEnsureKey(ctx);
374
+ let promise = starterEnsurePromises.get(key);
375
+ if (!promise) {
376
+ promise = ensureStarterWorkspaceResourcesOnce(ctx).catch((error) => {
377
+ starterEnsurePromises.delete(key);
378
+ throw error;
379
+ });
380
+ starterEnsurePromises.set(key, promise);
381
+ }
382
+ await promise;
383
+ }
384
+ async function ensureStarterWorkspaceResourcesOnce(ctx) {
385
+ const marker = await readStarterSeedMarker(ctx).catch(() => null);
386
+ if (marker?.version === STARTER_RESOURCES_VERSION)
387
+ return;
388
+ const timestamp = now();
389
+ const ensuredResources = [];
390
+ for (const starter of STARTER_GLOBAL_WORKSPACE_RESOURCES) {
391
+ const existing = await getWorkspaceResourceByPath(starter.path, ctx);
392
+ if (!existing) {
393
+ await insertStarterWorkspaceResource(starter, ctx, timestamp);
394
+ }
395
+ const row = await getWorkspaceResourceByPath(starter.path, ctx);
396
+ if (row)
397
+ ensuredResources.push(row);
398
+ }
399
+ for (const resource of ensuredResources) {
400
+ await materializeGlobalResource(resource);
401
+ }
402
+ await writeStarterSeedMarker(ctx);
403
+ }
404
+ export async function restoreStarterWorkspaceResources(input) {
405
+ const ctx = requireWorkspaceResourceCtx();
406
+ const requestedPaths = new Set((input?.paths ?? []).filter(Boolean));
407
+ const starters = requestedPaths.size > 0
408
+ ? STARTER_GLOBAL_WORKSPACE_RESOURCES.filter((resource) => requestedPaths.has(resource.path))
409
+ : STARTER_GLOBAL_WORKSPACE_RESOURCES;
410
+ const knownPaths = new Set(STARTER_GLOBAL_WORKSPACE_RESOURCES.map((resource) => resource.path));
411
+ const unknown = [...requestedPaths].filter((path) => !knownPaths.has(path));
412
+ const timestamp = now();
413
+ const restored = [];
414
+ const existing = [];
415
+ for (const starter of starters) {
416
+ const before = await getWorkspaceResourceByPath(starter.path, ctx);
417
+ if (!before) {
418
+ await insertStarterWorkspaceResource(starter, ctx, timestamp);
419
+ }
420
+ const row = await getWorkspaceResourceByPath(starter.path, ctx);
421
+ if (!row)
422
+ continue;
423
+ await materializeGlobalResource(row);
424
+ const option = {
425
+ id: row.id,
426
+ kind: row.kind,
427
+ name: row.name,
428
+ description: row.description,
429
+ path: row.path,
430
+ scope: row.scope,
431
+ updatedAt: row.updatedAt,
432
+ };
433
+ if (before)
434
+ existing.push(option);
435
+ else
436
+ restored.push(option);
437
+ }
438
+ if (restored.length > 0) {
439
+ await recordAudit({
440
+ action: "workspace.starter-resources.restored",
441
+ targetType: "workspace-resource",
442
+ targetId: null,
443
+ summary: `Restored starter workspace resource(s): ${restored.map((resource) => resource.path).join(", ")}`,
444
+ metadata: { paths: restored.map((resource) => resource.path) },
445
+ });
446
+ }
447
+ return { restored, existing, unknown };
26
448
  }
27
449
  export async function listWorkspaceResources(filter) {
450
+ await ensureStarterWorkspaceResources();
28
451
  const db = getDb();
29
452
  const conditions = [orgFilter(schema.workspaceResources)];
30
453
  if (filter?.kind) {
31
454
  conditions.push(eq(schema.workspaceResources.kind, filter.kind));
32
455
  }
33
- return db
456
+ const resources = await db
34
457
  .select()
35
458
  .from(schema.workspaceResources)
36
459
  .where(and(...conditions))
37
460
  .orderBy(desc(schema.workspaceResources.updatedAt));
461
+ await ensureMaterializedGlobalResources(resources);
462
+ return resources;
38
463
  }
39
464
  export async function listWorkspaceResourceOptions(filter) {
40
465
  const resources = await listWorkspaceResources(filter);
@@ -48,6 +473,232 @@ export async function listWorkspaceResourceOptions(filter) {
48
473
  updatedAt: resource.updatedAt,
49
474
  }));
50
475
  }
476
+ function isResourceAutoLoaded(resource) {
477
+ return (resource.kind === "instruction" &&
478
+ (resource.path === "AGENTS.md" || resource.path.startsWith("instructions/")));
479
+ }
480
+ export async function listWorkspaceResourcesForApp(appId) {
481
+ const [resources, grants] = await Promise.all([
482
+ listWorkspaceResources(),
483
+ listResourceGrants({ appId }),
484
+ ]);
485
+ const activeGrantsByResourceId = new Map(grants
486
+ .filter((grant) => grant.status === "active")
487
+ .map((grant) => [grant.resourceId, grant]));
488
+ const received = resources
489
+ .map((resource) => {
490
+ const grant = activeGrantsByResourceId.get(resource.id);
491
+ const isGlobal = resource.scope === "all";
492
+ if (!isGlobal && !grant)
493
+ return null;
494
+ return {
495
+ id: resource.id,
496
+ kind: resource.kind,
497
+ name: resource.name,
498
+ description: resource.description,
499
+ path: resource.path,
500
+ scope: resource.scope,
501
+ updatedAt: resource.updatedAt,
502
+ source: isGlobal ? "workspace" : "grant",
503
+ autoLoaded: isResourceAutoLoaded(resource),
504
+ grantId: grant?.id ?? null,
505
+ };
506
+ })
507
+ .filter((resource) => !!resource)
508
+ .sort((a, b) => {
509
+ const sourceOrder = (a.source === "workspace" ? 0 : 1) - (b.source === "workspace" ? 0 : 1);
510
+ if (sourceOrder !== 0)
511
+ return sourceOrder;
512
+ return a.path.localeCompare(b.path);
513
+ });
514
+ const global = received.filter((resource) => resource.source === "workspace");
515
+ const granted = received.filter((resource) => resource.source === "grant");
516
+ return {
517
+ appId,
518
+ resources: received,
519
+ counts: {
520
+ total: received.length,
521
+ workspace: global.length,
522
+ global: global.length,
523
+ granted: granted.length,
524
+ autoLoaded: received.filter((resource) => resource.autoLoaded).length,
525
+ },
526
+ };
527
+ }
528
+ function workspaceResourceOption(resource) {
529
+ return {
530
+ id: resource.id,
531
+ kind: resource.kind,
532
+ name: resource.name,
533
+ description: resource.description,
534
+ path: resource.path,
535
+ scope: resource.scope,
536
+ updatedAt: resource.updatedAt,
537
+ };
538
+ }
539
+ function effectiveAvailability(input) {
540
+ if (!input.resource) {
541
+ return { availability: "path-not-managed", availableToApp: false };
542
+ }
543
+ if (input.resource.scope === "all") {
544
+ return { availability: "all-apps", availableToApp: true };
545
+ }
546
+ if (!input.appId) {
547
+ return { availability: "selected-no-app", availableToApp: false };
548
+ }
549
+ if (input.activeGrantId) {
550
+ return { availability: "selected-granted", availableToApp: true };
551
+ }
552
+ return { availability: "selected-not-granted", availableToApp: false };
553
+ }
554
+ function affectsAllAppsScope(beforeScope, afterScope) {
555
+ return beforeScope === "all" || afterScope === "all";
556
+ }
557
+ async function shouldRequestAllAppResourceApproval(input) {
558
+ if (!affectsAllAppsScope(input.beforeScope, input.afterScope))
559
+ return false;
560
+ const policy = await getApprovalPolicy();
561
+ return policy.enabled;
562
+ }
563
+ function mergedWorkspaceResourceAfter(before, input) {
564
+ return {
565
+ id: before.id,
566
+ kind: before.kind,
567
+ name: input.name ?? before.name,
568
+ description: input.description === undefined
569
+ ? before.description
570
+ : input.description || null,
571
+ path: before.path,
572
+ content: input.content ?? before.content,
573
+ scope: (input.scope ?? before.scope),
574
+ updatedAt: before.updatedAt,
575
+ };
576
+ }
577
+ async function listOverrideImpactForPath(resourcePath) {
578
+ const resources = await resourceListAllOwners(resourcePath).catch(() => []);
579
+ return resources
580
+ .filter((resource) => resource.path === resourcePath && resource.owner !== WORKSPACE_OWNER)
581
+ .map((resource) => {
582
+ const shared = resource.owner === SHARED_OWNER;
583
+ return {
584
+ scope: shared ? "shared" : "personal",
585
+ owner: resource.owner,
586
+ label: shared
587
+ ? "Organization/app override"
588
+ : `Personal override (${resource.owner})`,
589
+ updatedAt: resource.updatedAt,
590
+ };
591
+ })
592
+ .sort((a, b) => {
593
+ const scopeOrder = (a.scope === "shared" ? 0 : 1) - (b.scope === "shared" ? 0 : 1);
594
+ if (scopeOrder !== 0)
595
+ return scopeOrder;
596
+ return b.updatedAt - a.updatedAt;
597
+ });
598
+ }
599
+ async function affectedAllAppTargets() {
600
+ const agents = await discoverAgents("dispatch").catch(() => []);
601
+ const apps = agents
602
+ .filter((agent) => agent.id !== "dispatch")
603
+ .map((agent) => ({
604
+ id: agent.id,
605
+ name: agent.name || agent.id,
606
+ }))
607
+ .sort((a, b) => a.name.localeCompare(b.name));
608
+ return {
609
+ label: apps.length > 0 ? "All workspace apps" : "All workspace apps",
610
+ count: apps.length,
611
+ apps,
612
+ };
613
+ }
614
+ export async function previewWorkspaceResourceChange(input) {
615
+ const operation = input.operation ?? (input.resourceId ? "update" : "create");
616
+ const ctx = requireWorkspaceResourceCtx();
617
+ const existing = input.resourceId
618
+ ? await getWorkspaceResource(input.resourceId, ctx)
619
+ : null;
620
+ const path = input.path?.trim() || existing?.path || null;
621
+ const beforeScope = existing?.scope
622
+ ? existing.scope
623
+ : null;
624
+ const afterScope = operation === "delete"
625
+ ? null
626
+ : (input.scope ??
627
+ existing?.scope ??
628
+ null);
629
+ const affectsAllApps = affectsAllAppsScope(beforeScope, afterScope);
630
+ const [policy, overrides, affectedApps] = await Promise.all([
631
+ getApprovalPolicy(),
632
+ path ? listOverrideImpactForPath(path) : Promise.resolve([]),
633
+ affectsAllApps
634
+ ? affectedAllAppTargets()
635
+ : Promise.resolve({
636
+ label: "Selected apps only",
637
+ count: null,
638
+ apps: [],
639
+ }),
640
+ ]);
641
+ return {
642
+ operation,
643
+ path,
644
+ resourceId: existing?.id ?? input.resourceId ?? null,
645
+ beforeScope,
646
+ afterScope,
647
+ affectsAllApps,
648
+ affectedApps,
649
+ overrides: {
650
+ count: overrides.length,
651
+ sharedCount: overrides.filter((override) => override.scope === "shared")
652
+ .length,
653
+ personalCount: overrides.filter((override) => override.scope === "personal").length,
654
+ items: overrides,
655
+ },
656
+ approval: {
657
+ policyEnabled: policy.enabled,
658
+ willRequestApproval: policy.enabled && affectsAllApps,
659
+ },
660
+ };
661
+ }
662
+ export async function getWorkspaceResourceEffectiveContext(input) {
663
+ const ctx = requireWorkspaceResourceCtx();
664
+ const appId = input.appId?.trim() || null;
665
+ const userEmail = input.userEmail?.trim() || ctx.ownerEmail;
666
+ let row = null;
667
+ if (input.resourceId) {
668
+ row = await getWorkspaceResource(input.resourceId, ctx);
669
+ }
670
+ const path = input.path?.trim() || row?.path;
671
+ if (!path) {
672
+ throw new Error("Provide a workspace resource id or path.");
673
+ }
674
+ if (!row) {
675
+ row = await getWorkspaceResourceByPath(path, ctx);
676
+ }
677
+ if (row?.scope === "all") {
678
+ await materializeGlobalResource(row);
679
+ }
680
+ const coreContext = await resourceEffectiveContext(userEmail, path);
681
+ const resource = row ? workspaceResourceOption(row) : null;
682
+ const activeGrant = resource?.scope === "selected" && appId
683
+ ? (await listResourceGrants({ resourceId: resource.id, appId })).find((grant) => grant.status === "active")
684
+ : null;
685
+ const availability = effectiveAvailability({
686
+ resource,
687
+ appId,
688
+ activeGrantId: activeGrant?.id ?? null,
689
+ });
690
+ return {
691
+ appId,
692
+ userEmail,
693
+ path,
694
+ workspaceResource: resource,
695
+ ...availability,
696
+ activeGrantId: activeGrant?.id ?? null,
697
+ effectiveScope: coreContext.effectiveScope,
698
+ effectiveResource: coreContext.effectiveResource,
699
+ layers: coreContext.layers,
700
+ };
701
+ }
51
702
  export async function getWorkspaceResource(resourceId, ctx = requireWorkspaceResourceCtx()) {
52
703
  const db = getDb();
53
704
  const [row] = await db
@@ -57,15 +708,14 @@ export async function getWorkspaceResource(resourceId, ctx = requireWorkspaceRes
57
708
  .limit(1);
58
709
  return row ?? null;
59
710
  }
60
- export async function createWorkspaceResource(input) {
711
+ export async function applyWorkspaceResourceCreate(input, actor = currentOwnerEmail(), ctx = requireWorkspaceResourceCtx()) {
61
712
  const db = getDb();
62
713
  const timestamp = now();
63
714
  const resourceId = id();
64
- const actor = currentOwnerEmail();
65
715
  await db.insert(schema.workspaceResources).values({
66
716
  id: resourceId,
67
- ownerEmail: actor,
68
- orgId: currentOrgId(),
717
+ ownerEmail: ctx.ownerEmail,
718
+ orgId: ctx.orgId,
69
719
  kind: input.kind,
70
720
  name: input.name,
71
721
  description: input.description || null,
@@ -81,12 +731,34 @@ export async function createWorkspaceResource(input) {
81
731
  targetType: `workspace-${input.kind}`,
82
732
  targetId: resourceId,
83
733
  summary: `Created workspace ${input.kind} "${input.name}" (${input.path})`,
734
+ actor,
735
+ ownerEmail: ctx.ownerEmail,
736
+ orgId: ctx.orgId,
84
737
  });
85
- return getWorkspaceResource(resourceId);
738
+ const created = await getWorkspaceResource(resourceId, ctx);
739
+ if (created)
740
+ await materializeGlobalResource(created);
741
+ return created;
86
742
  }
87
- export async function updateWorkspaceResource(resourceId, input) {
743
+ export async function createWorkspaceResource(input) {
744
+ if (await shouldRequestAllAppResourceApproval({
745
+ beforeScope: null,
746
+ afterScope: input.scope,
747
+ })) {
748
+ return createApprovalRequest({
749
+ changeType: "workspace-resource.create",
750
+ targetType: `workspace-${input.kind}`,
751
+ targetId: null,
752
+ summary: `Create All-app workspace ${input.kind} "${input.name}"`,
753
+ payload: { input },
754
+ beforeValue: null,
755
+ afterValue: input,
756
+ });
757
+ }
758
+ return applyWorkspaceResourceCreate(input);
759
+ }
760
+ export async function applyWorkspaceResourceUpdate(resourceId, input, actor = currentOwnerEmail(), ctx = requireWorkspaceResourceCtx()) {
88
761
  const db = getDb();
89
- const ctx = requireWorkspaceResourceCtx();
90
762
  const existing = await getWorkspaceResource(resourceId, ctx);
91
763
  if (!existing)
92
764
  throw new Error("Workspace resource not found");
@@ -108,13 +780,40 @@ export async function updateWorkspaceResource(resourceId, input) {
108
780
  targetType: `workspace-${existing.kind}`,
109
781
  targetId: resourceId,
110
782
  summary: `Updated workspace ${existing.kind} "${input.name || existing.name}"`,
783
+ actor,
784
+ ownerEmail: ctx.ownerEmail,
785
+ orgId: ctx.orgId,
111
786
  });
112
- return getWorkspaceResource(resourceId, ctx);
787
+ const updated = await getWorkspaceResource(resourceId, ctx);
788
+ if (updated)
789
+ await materializeGlobalResource(updated);
790
+ return updated;
113
791
  }
114
- export async function deleteWorkspaceResource(resourceId) {
115
- const db = getDb();
792
+ export async function updateWorkspaceResource(resourceId, input) {
116
793
  const ctx = requireWorkspaceResourceCtx();
117
794
  const existing = await getWorkspaceResource(resourceId, ctx);
795
+ if (!existing)
796
+ throw new Error("Workspace resource not found");
797
+ const after = mergedWorkspaceResourceAfter(existing, input);
798
+ if (await shouldRequestAllAppResourceApproval({
799
+ beforeScope: existing.scope,
800
+ afterScope: after.scope,
801
+ })) {
802
+ return createApprovalRequest({
803
+ changeType: "workspace-resource.update",
804
+ targetType: `workspace-${existing.kind}`,
805
+ targetId: resourceId,
806
+ summary: `Update All-app workspace ${existing.kind} "${after.name}"`,
807
+ payload: { id: resourceId, input },
808
+ beforeValue: existing,
809
+ afterValue: after,
810
+ });
811
+ }
812
+ return applyWorkspaceResourceUpdate(resourceId, input);
813
+ }
814
+ export async function applyWorkspaceResourceDelete(resourceId, actor = currentOwnerEmail(), ctx = requireWorkspaceResourceCtx()) {
815
+ const db = getDb();
816
+ const existing = await getWorkspaceResource(resourceId, ctx);
118
817
  if (!existing)
119
818
  throw new Error("Workspace resource not found");
120
819
  // Revoke all grants
@@ -124,6 +823,7 @@ export async function deleteWorkspaceResource(resourceId) {
124
823
  await revokeResourceGrant(grant.id);
125
824
  }
126
825
  }
826
+ await removeMaterializedGlobalResource(existing);
127
827
  await db
128
828
  .delete(schema.workspaceResources)
129
829
  .where(and(eq(schema.workspaceResources.id, resourceId), ctxScope(schema.workspaceResources, ctx)));
@@ -132,9 +832,33 @@ export async function deleteWorkspaceResource(resourceId) {
132
832
  targetType: `workspace-${existing.kind}`,
133
833
  targetId: resourceId,
134
834
  summary: `Deleted workspace ${existing.kind} "${existing.name}" (${existing.path})`,
835
+ actor,
836
+ ownerEmail: ctx.ownerEmail,
837
+ orgId: ctx.orgId,
135
838
  });
136
839
  return existing;
137
840
  }
841
+ export async function deleteWorkspaceResource(resourceId) {
842
+ const ctx = requireWorkspaceResourceCtx();
843
+ const existing = await getWorkspaceResource(resourceId, ctx);
844
+ if (!existing)
845
+ throw new Error("Workspace resource not found");
846
+ if (await shouldRequestAllAppResourceApproval({
847
+ beforeScope: existing.scope,
848
+ afterScope: null,
849
+ })) {
850
+ return createApprovalRequest({
851
+ changeType: "workspace-resource.delete",
852
+ targetType: `workspace-${existing.kind}`,
853
+ targetId: resourceId,
854
+ summary: `Delete All-app workspace ${existing.kind} "${existing.name}"`,
855
+ payload: { id: resourceId },
856
+ beforeValue: existing,
857
+ afterValue: null,
858
+ });
859
+ }
860
+ return applyWorkspaceResourceDelete(resourceId);
861
+ }
138
862
  // ─── Grants ──────────────────────────────────────────────────────
139
863
  export async function listResourceGrants(filter) {
140
864
  const db = getDb();
@@ -238,101 +962,6 @@ export async function revokeResourceGrant(grantId, ctx = requireWorkspaceResourc
238
962
  });
239
963
  return getResourceGrant(grantId, ctx);
240
964
  }
241
- // ─── Sync ──────────────────────────────────────────────────────
242
- /**
243
- * Push workspace resources to an app via its /_agent-native/resources endpoint.
244
- * Resources with scope="all" are always pushed. Resources with scope="selected"
245
- * are only pushed if there's an active grant for that app.
246
- */
247
- export async function syncResourcesToApp(appId) {
248
- const agents = await discoverAgents("dispatch");
249
- const agent = agents.find((a) => a.id === appId);
250
- if (!agent)
251
- throw new Error(`App "${appId}" not found in agent registry`);
252
- const allResources = await listWorkspaceResources();
253
- const grants = await listResourceGrants({ appId });
254
- const activeGrantResourceIds = new Set(grants.filter((g) => g.status === "active").map((g) => g.resourceId));
255
- // Determine which resources to push
256
- const toPush = allResources.filter((r) => r.scope === "all" ||
257
- (r.scope === "selected" && activeGrantResourceIds.has(r.id)));
258
- if (toPush.length === 0) {
259
- return { appId, synced: 0, resources: [] };
260
- }
261
- const syncedPaths = [];
262
- const db = getDb();
263
- const timestamp = now();
264
- for (const resource of toPush) {
265
- try {
266
- // Push via the resources API — create as shared resource
267
- const res = await fetch(`${agent.url}/_agent-native/resources`, {
268
- method: "POST",
269
- headers: { "Content-Type": "application/json" },
270
- body: JSON.stringify({
271
- path: resource.path,
272
- content: resource.content,
273
- shared: true,
274
- mimeType: "text/markdown",
275
- }),
276
- });
277
- if (res.ok || res.status === 409) {
278
- // 409 = already exists, try updating
279
- if (res.status === 409) {
280
- // Fetch existing to get ID, then update
281
- const listRes = await fetch(`${agent.url}/_agent-native/resources?scope=shared&path=${encodeURIComponent(resource.path)}`);
282
- if (listRes.ok) {
283
- const items = await listRes.json();
284
- const existing = Array.isArray(items)
285
- ? items.find((i) => i.path === resource.path)
286
- : null;
287
- if (existing) {
288
- await fetch(`${agent.url}/_agent-native/resources/${existing.id}`, {
289
- method: "PUT",
290
- headers: { "Content-Type": "application/json" },
291
- body: JSON.stringify({ content: resource.content }),
292
- });
293
- }
294
- }
295
- }
296
- syncedPaths.push(resource.path);
297
- // Update grant syncedAt if applicable
298
- const grant = grants.find((g) => g.resourceId === resource.id && g.status === "active");
299
- if (grant) {
300
- await db
301
- .update(schema.workspaceResourceGrants)
302
- .set({ syncedAt: timestamp, updatedAt: timestamp })
303
- .where(eq(schema.workspaceResourceGrants.id, grant.id));
304
- }
305
- }
306
- }
307
- catch {
308
- // Skip unreachable — don't fail the whole sync
309
- }
310
- }
311
- await recordAudit({
312
- action: "workspace.resources.synced",
313
- targetType: "workspace-resource-sync",
314
- targetId: appId,
315
- summary: `Synced ${syncedPaths.length} workspace resource(s) to ${appId}: ${syncedPaths.join(", ")}`,
316
- });
317
- return { appId, synced: syncedPaths.length, resources: syncedPaths };
318
- }
319
- /**
320
- * Sync all workspace resources to all apps that have grants or scope="all" resources.
321
- */
322
- export async function syncResourcesToAllApps() {
323
- const agents = await discoverAgents("dispatch");
324
- const results = [];
325
- for (const agent of agents) {
326
- try {
327
- const result = await syncResourcesToApp(agent.id);
328
- results.push({ appId: result.appId, synced: result.synced });
329
- }
330
- catch {
331
- results.push({ appId: agent.id, synced: 0 });
332
- }
333
- }
334
- return results;
335
- }
336
965
  // ─── Overview ──────────────────────────────────────────────────────
337
966
  export async function listWorkspaceResourcesOverview() {
338
967
  const [resources, grants] = await Promise.all([