@desplega.ai/agent-swarm 1.113.0 → 1.115.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 (237) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
  3. package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
  4. package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
  5. package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
  6. package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
  7. package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
  8. package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
  9. package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
  10. package/dist/cli-0pygq9r1.js +429 -0
  11. package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
  12. package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
  13. package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
  14. package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
  15. package/dist/cli-52fg7s7x.js +269 -0
  16. package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
  17. package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
  18. package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
  19. package/dist/cli-86fdnerz.js +405 -0
  20. package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
  21. package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
  22. package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
  23. package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
  24. package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
  25. package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
  26. package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
  27. package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
  28. package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
  29. package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
  30. package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
  31. package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
  32. package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
  33. package/dist/cli-jkhwmmgg.js +136 -0
  34. package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
  35. package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
  36. package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
  37. package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
  38. package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
  39. package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
  40. package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
  41. package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
  42. package/dist/cli-t0vqevnm.js +393 -0
  43. package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
  44. package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
  45. package/dist/cli-wh91zby6.js +187222 -0
  46. package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
  47. package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
  48. package/dist/cli.js +32 -15
  49. package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
  50. package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
  51. package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
  52. package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
  53. package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
  54. package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
  55. package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
  56. package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
  57. package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
  58. package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
  59. package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
  60. package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
  61. package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
  62. package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
  63. package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
  64. package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
  65. package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
  66. package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
  67. package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
  68. package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
  69. package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
  70. package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
  71. package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
  72. package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
  73. package/dist/rbac-roles-kfesjy9y.js +28 -0
  74. package/dist/rbac-roles-mwcwhbxc.js +283 -0
  75. package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
  76. package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
  77. package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
  78. package/openapi.json +1327 -20
  79. package/package.json +3 -3
  80. package/src/agentmail/handlers.ts +16 -6
  81. package/src/agentmail/templates.ts +25 -5
  82. package/src/be/audit-user.ts +28 -4
  83. package/src/be/db-queries/oauth.ts +42 -0
  84. package/src/be/db.ts +314 -28
  85. package/src/be/identity.ts +79 -0
  86. package/src/be/mcp-proxy.ts +132 -0
  87. package/src/be/migrations/109_rbac_roles.sql +79 -0
  88. package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
  89. package/src/be/migrations/112_script_connections_graphql.sql +96 -0
  90. package/src/be/migrations/113_task_routing_affinity.sql +9 -0
  91. package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
  92. package/src/be/modelsdev-cache.json +135 -0
  93. package/src/be/oauth-credential-bindings.ts +35 -0
  94. package/src/be/oauth-refresh-sweep.ts +159 -0
  95. package/src/be/rbac-audit.ts +52 -13
  96. package/src/be/rbac-roles.ts +424 -0
  97. package/src/be/script-connections.ts +459 -40
  98. package/src/be/script-credential-broker.ts +29 -3
  99. package/src/be/scripts/typecheck.ts +51 -7
  100. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
  101. package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
  102. package/src/be/users.ts +24 -0
  103. package/src/cli.tsx +17 -0
  104. package/src/github/handlers.ts +10 -9
  105. package/src/github/templates.ts +45 -9
  106. package/src/gitlab/handlers.ts +20 -8
  107. package/src/gitlab/templates.ts +18 -6
  108. package/src/heartbeat/heartbeat.ts +143 -21
  109. package/src/http/all-routes.ts +3 -0
  110. package/src/http/approval-requests.ts +12 -0
  111. package/src/http/config.ts +44 -10
  112. package/src/http/core.ts +30 -3
  113. package/src/http/favorites.ts +1 -5
  114. package/src/http/index.ts +25 -1
  115. package/src/http/mcp-oauth.ts +2 -0
  116. package/src/http/mcp-servers.ts +44 -3
  117. package/src/http/oauth-generic.ts +105 -0
  118. package/src/http/poll.ts +20 -3
  119. package/src/http/script-connection-proxy.ts +116 -0
  120. package/src/http/script-connections.ts +1657 -0
  121. package/src/http/scripts.ts +27 -7
  122. package/src/http/skills.ts +11 -0
  123. package/src/http/tasks.ts +27 -13
  124. package/src/http/x.ts +6 -2
  125. package/src/integrations/kapso/inbound.ts +23 -6
  126. package/src/jira/sync.ts +111 -17
  127. package/src/jira/templates.ts +10 -2
  128. package/src/mcp-client/http-client.ts +194 -0
  129. package/src/oauth/app-validation.ts +16 -0
  130. package/src/oauth/ensure-token.ts +69 -14
  131. package/src/oauth/mcp-wrapper.ts +15 -0
  132. package/src/oauth/wrapper.ts +48 -20
  133. package/src/prompts/session-templates.ts +3 -3
  134. package/src/providers/codex-models.ts +100 -30
  135. package/src/providers/modelsdev-reasoning.json +51 -0
  136. package/src/providers/pi-mono-mcp-client.ts +2 -133
  137. package/src/providers/reasoning-effort.ts +11 -5
  138. package/src/rbac/admission.ts +68 -0
  139. package/src/rbac/index.ts +2 -0
  140. package/src/rbac/legacy-policy.ts +12 -0
  141. package/src/rbac/permissions.ts +48 -0
  142. package/src/scheduler/scheduler.ts +9 -1
  143. package/src/scripts-runtime/api-client.ts +151 -19
  144. package/src/scripts-runtime/api-types.ts +38 -6
  145. package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
  146. package/src/scripts-runtime/credential-broker/types.ts +8 -0
  147. package/src/scripts-runtime/ctx.ts +11 -1
  148. package/src/scripts-runtime/eval-harness.ts +5 -1
  149. package/src/scripts-runtime/executors/types.ts +2 -1
  150. package/src/scripts-runtime/loader.ts +3 -1
  151. package/src/scripts-runtime/mcp-client.ts +87 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/types/stdlib.d.ts +6 -0
  154. package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
  155. package/src/server-user.ts +114 -59
  156. package/src/server.ts +12 -0
  157. package/src/slack/assistant.ts +14 -6
  158. package/src/slack/enrich.ts +17 -0
  159. package/src/slack/handlers.ts +8 -22
  160. package/src/slack/responses.ts +91 -1
  161. package/src/slack/thread-buffer.ts +6 -3
  162. package/src/slack/watcher.ts +9 -6
  163. package/src/tasks/worker-follow-up.ts +100 -3
  164. package/src/tests/agentmail-handlers.test.ts +32 -3
  165. package/src/tests/approval-requests.test.ts +61 -0
  166. package/src/tests/audit-user.test.ts +47 -1
  167. package/src/tests/codex-adapter.test.ts +46 -23
  168. package/src/tests/credential-broker.test.ts +5 -2
  169. package/src/tests/delete-page-tool.test.ts +197 -0
  170. package/src/tests/github-handlers.test.ts +28 -2
  171. package/src/tests/gitlab-handlers.test.ts +125 -0
  172. package/src/tests/heartbeat.test.ts +48 -3
  173. package/src/tests/identity.test.ts +117 -0
  174. package/src/tests/jira-sync.test.ts +99 -1
  175. package/src/tests/kapso-inbound.test.ts +7 -0
  176. package/src/tests/mcp-tools-user.test.ts +60 -6
  177. package/src/tests/model-control.test.ts +8 -2
  178. package/src/tests/oauth-credential-bindings.test.ts +490 -0
  179. package/src/tests/oauth-refresh-sweep.test.ts +160 -0
  180. package/src/tests/pool-affinity.test.ts +401 -0
  181. package/src/tests/prompt-template-remaining.test.ts +2 -2
  182. package/src/tests/rbac-admission-e2e.test.ts +386 -0
  183. package/src/tests/rbac-admission.test.ts +492 -0
  184. package/src/tests/rbac-e2e-helpers.ts +3 -0
  185. package/src/tests/rbac-engine.test.ts +50 -0
  186. package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
  187. package/src/tests/rbac-roles.test.ts +328 -0
  188. package/src/tests/reasoning-effort.test.ts +15 -2
  189. package/src/tests/schedule-target-type.test.ts +50 -0
  190. package/src/tests/scheduled-tasks.test.ts +40 -0
  191. package/src/tests/script-apis-mcp.test.ts +37 -13
  192. package/src/tests/script-connections-http.test.ts +1158 -0
  193. package/src/tests/script-connections-mcp.test.ts +894 -0
  194. package/src/tests/script-connections.test.ts +754 -6
  195. package/src/tests/scripts-external-api.test.ts +49 -1
  196. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
  197. package/src/tests/scripts-runtime.test.ts +291 -0
  198. package/src/tests/seed-scripts.test.ts +36 -0
  199. package/src/tests/slack-identity-resolution.test.ts +37 -3
  200. package/src/tests/slack-inline-output.test.ts +65 -0
  201. package/src/tests/slack-thread-buffer.test.ts +28 -0
  202. package/src/tests/tool-annotations.test.ts +1 -0
  203. package/src/tests/user-token-rest-auth.test.ts +140 -1
  204. package/src/tests/workflow-swarm-script.test.ts +44 -0
  205. package/src/tests/workflow-triggers-v2.test.ts +300 -1
  206. package/src/tools/credential-bindings/tool.ts +277 -18
  207. package/src/tools/delete-page.ts +145 -0
  208. package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
  209. package/src/tools/request-human-input.ts +2 -0
  210. package/src/tools/resolve-user.ts +77 -27
  211. package/src/tools/script-apis.ts +62 -3
  212. package/src/tools/script-connections/tool.ts +275 -31
  213. package/src/tools/send-task.ts +15 -1
  214. package/src/tools/task-action.ts +34 -2
  215. package/src/tools/templates.ts +53 -1
  216. package/src/tools/tool-config.ts +2 -1
  217. package/src/tools/utils.ts +2 -0
  218. package/src/tools/workflows/create-workflow.ts +8 -1
  219. package/src/tools/workflows/update-workflow.ts +11 -2
  220. package/src/types.ts +90 -11
  221. package/src/utils/scoped-resource.ts +25 -0
  222. package/src/workflows/executors/human-in-the-loop.ts +1 -0
  223. package/src/workflows/executors/swarm-script.ts +7 -1
  224. package/src/workflows/triggers.ts +146 -18
  225. package/templates/skills/swarm-scripts/content.md +33 -8
  226. package/dist/cli-bgef578c.js +0 -771
  227. package/dist/cli-c9mtm09b.js +0 -43
  228. package/dist/cli-ceewqajw.js +0 -1698
  229. package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
  230. package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
  231. package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
  232. package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
  233. package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
  234. package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
  235. package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
  236. package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
  237. package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
@@ -7,14 +7,14 @@ import {
7
7
  SUPPORTED_PROTOCOL_VERSIONS,
8
8
  agentWithCapacity,
9
9
  applyRating,
10
+ assertOAuthAppUrlsSafe,
11
+ assertOAuthProviderIsNotReserved,
10
12
  assertSelectOnlyQuery,
11
- can,
12
13
  canClaim,
13
14
  canReadMemory,
14
15
  cancelTaskHandler,
15
16
  cancelTaskInputSchema,
16
17
  cancelTaskOutputSchema,
17
- clearAuditSink,
18
18
  clearJiraMetadata,
19
19
  createEvent,
20
20
  createEventsBatch,
@@ -25,8 +25,11 @@ import {
25
25
  describeRequestRoute,
26
26
  emitBudgetRefusalSideEffects,
27
27
  enforceContentLengthCap,
28
+ enqueueAdmissionRow,
29
+ enqueueAuditRow,
28
30
  executeReadOnlyQuery,
29
31
  expandCandidatesWithGraph,
32
+ flushAuditBuffer,
30
33
  getEnabledCapabilities,
31
34
  getEventCountsFiltered,
32
35
  getEventsFiltered,
@@ -89,18 +92,24 @@ import {
89
92
  registerJiraWebhook,
90
93
  reloadGlobalConfigsAndIntegrations,
91
94
  resolveHttpAuditUserId,
95
+ resolveScopedResourceId,
96
+ resolveTaskAuditUserId,
92
97
  route,
93
98
  safeRequestUrlForLog,
94
99
  scheduleIntegrationsReload,
100
+ scopedResourceScopeIdSchema,
95
101
  sendKapsoReaction,
96
102
  sendTaskHandler,
97
103
  sendTaskOutputSchema,
98
- setAuditSink,
99
104
  setCorsHeaders,
100
105
  shouldPersistAutomaticTaskMemory,
101
106
  snapshotMetric,
102
107
  snapshotPage,
108
+ startAuditGc,
109
+ startAuditWriter,
103
110
  startSpan,
111
+ stopAuditGc,
112
+ stopAuditWriter,
104
113
  storeLinks,
105
114
  taskActionHandler,
106
115
  taskActionInputSchema,
@@ -114,16 +123,16 @@ import {
114
123
  withRemoteContext,
115
124
  withSpanContext,
116
125
  writeSkillsToFilesystem
117
- } from "./cli-4xyj9jtq.js";
126
+ } from "./cli-hrhye2rm.js";
127
+ import"./cli-1z7048dy.js";
118
128
  import {
119
129
  BROWSER_SDK_JS,
120
130
  SWARM_UI_JS
121
131
  } from "./cli-xm2tyqfe.js";
122
- import"./cli-1z7048dy.js";
123
132
  import {
124
133
  REFERENCES_SOURCE_MAX_LENGTH,
125
134
  sanitizeReferencesSource
126
- } from "./cli-dcfyyh3t.js";
135
+ } from "./cli-ftyf58xj.js";
127
136
  import {
128
137
  deriveCodexKeySuffix,
129
138
  getValidCodexOAuth,
@@ -134,41 +143,38 @@ import {
134
143
  } from "./cli-gz9sbyk3.js";
135
144
  import {
136
145
  reasoningCapability
137
- } from "./cli-tkmm5nf9.js";
138
- import {
139
- acquireOAuthRefreshLock,
140
- buildAuthorizationUrl,
141
- calculatePKCECodeChallenge,
142
- deleteOAuthTokens,
143
- ensureTokenOrThrow,
144
- exchangeCode,
145
- generateRandomCodeVerifier,
146
- generateRandomState,
147
- getOAuthApp,
148
- getOAuthTokens,
149
- releaseOAuthRefreshLock
150
- } from "./cli-bgef578c.js";
151
- import"./cli-36ymmpmp.js";
146
+ } from "./cli-e61fs2hp.js";
147
+ import"./cli-3samkyap.js";
152
148
  import {
153
149
  normalizeModelKey
154
- } from "./cli-tbw13sx8.js";
150
+ } from "./cli-tece6zgm.js";
151
+ import {
152
+ getUserGrant
153
+ } from "./cli-52fg7s7x.js";
154
+ import {
155
+ can,
156
+ clearAuditSink,
157
+ decideToolAdmission,
158
+ isRbacEnabled,
159
+ setAuditSink
160
+ } from "./cli-86fdnerz.js";
155
161
  import {
156
- SCRIPT_STDLIB_TYPES,
157
162
  ensureAgentFsCredentialsForAgent,
158
163
  extractArgsJsonSchema,
159
164
  extractScriptSignature,
160
165
  scriptSdkTypesWithGeneratedApis,
166
+ scriptStdlibTypesWithGeneratedApis,
161
167
  typecheckScript
162
- } from "./cli-hsetkkd3.js";
168
+ } from "./cli-12vb0xg6.js";
163
169
  import {
164
170
  checkHeartbeatChecklist,
165
171
  runHeartbeatSweep,
166
172
  stopHeartbeat
167
- } from "./cli-bnzbn8j8.js";
173
+ } from "./cli-beea4a3y.js";
168
174
  import {
169
175
  createResumeFollowUp,
170
176
  createWorkerTaskFollowUp
171
- } from "./cli-r5g6ngtn.js";
177
+ } from "./cli-dcca6j8m.js";
172
178
  import"./cli-fad8m16k.js";
173
179
  import {
174
180
  calculateNextRun,
@@ -176,7 +182,7 @@ import {
176
182
  ensure,
177
183
  initialize,
178
184
  require_dist
179
- } from "./cli-gtkqc144.js";
185
+ } from "./cli-shvaxw27.js";
180
186
  import {
181
187
  RawLlmConfigSchema,
182
188
  TriggerSchemaError,
@@ -187,6 +193,8 @@ import {
187
193
  executeRawLlm,
188
194
  generateEdges,
189
195
  getExecutorRegistry,
196
+ getOAuthBindingTokenStatus,
197
+ getOAuthProviderConfig,
190
198
  handleWebhookTrigger,
191
199
  initWorkflows,
192
200
  retryFailedRun,
@@ -196,31 +204,63 @@ import {
196
204
  validateDefinition,
197
205
  validateJsonSchema,
198
206
  verifyHmacSignature
199
- } from "./cli-z46pnksz.js";
207
+ } from "./cli-q1rx7g0d.js";
200
208
  import {
201
209
  getApiKey
202
210
  } from "./cli-f14fvzag.js";
203
211
  import {
212
+ CredentialBindingSchema,
213
+ applyMcpOAuthRefresh,
214
+ assertUrlSafe,
215
+ buildAuthorizeUrl,
216
+ callMcpServerTool,
217
+ computeExpiresAt,
218
+ consumeMcpOAuthPending,
204
219
  createScriptApi,
220
+ deleteMcpOAuthToken,
205
221
  deleteScript,
206
222
  deleteScriptApi,
223
+ discoverAuthorizationServerMetadata,
224
+ discoverProtectedResourceMetadata,
225
+ ensureMcpToken,
226
+ exchangeCodeForTokens,
227
+ gcMcpOAuthPending,
228
+ getMcpOAuthToken,
207
229
  getScript,
208
230
  getScriptApiById,
209
231
  getScriptApiConnectionDescriptors,
210
232
  getScriptApiSecret,
211
233
  getScriptApiTypes,
212
234
  getScriptById,
235
+ getScriptConnectionById,
236
+ getScriptMcpConnectionDescriptors,
237
+ getScriptMcpTypes,
238
+ insertMcpOAuthPending,
239
+ listRelationalCredentialBindings,
213
240
  listScriptApisForScript,
241
+ listScriptConnections,
214
242
  listScriptVersions,
215
243
  listScripts,
244
+ placeholderForConfigKey,
245
+ publicEndpointSsrfOptions,
216
246
  recordScriptApiUsage,
247
+ refreshMcpToken,
248
+ refreshScriptConnection,
249
+ registerClient,
250
+ revokeMcpToken,
217
251
  rotateScriptApiSecret,
252
+ setMcpServerAuthMethod,
253
+ setScriptConnectionEnabled,
218
254
  updateScriptApi,
219
- upsertScriptByName
220
- } from "./cli-qwgtacd6.js";
255
+ upsertCredentialBinding,
256
+ upsertMcpOAuthToken,
257
+ upsertScriptByName,
258
+ upsertScriptConnection
259
+ } from "./cli-r9f6ngrf.js";
260
+ import"./cli-jkhwmmgg.js";
221
261
  import {
222
262
  searchScripts
223
- } from "./cli-30bbaveh.js";
263
+ } from "./cli-cmw9rzy5.js";
224
264
  import {
225
265
  CANDIDATE_SET_MULTIPLIER,
226
266
  getEmbeddingProvider,
@@ -228,22 +268,33 @@ import {
228
268
  init_constants,
229
269
  init_reranker,
230
270
  rerank
231
- } from "./cli-f3qa069v.js";
271
+ } from "./cli-7xje5amz.js";
232
272
  import"./cli-q25ejbyn.js";
233
- import"./cli-bgvskydy.js";
273
+ import {
274
+ acquireOAuthRefreshLock,
275
+ buildAuthorizationUrl,
276
+ deleteOAuthTokens,
277
+ ensureTokenOrThrow,
278
+ exchangeCode,
279
+ forceRefreshTokenOrThrow,
280
+ getOAuthApp,
281
+ getOAuthTokens,
282
+ releaseOAuthRefreshLock,
283
+ upsertOAuthApp
284
+ } from "./cli-t0vqevnm.js";
285
+ import"./cli-0pygq9r1.js";
286
+ import"./cli-tffvrh6j.js";
234
287
  import {
235
288
  getSlackApp,
236
289
  startSlackApp,
237
290
  stopSlackApp
238
- } from "./cli-trmapn9k.js";
291
+ } from "./cli-pet3aev6.js";
239
292
  import"./cli-rkndnn90.js";
240
293
  import"./cli-b0p7rfnd.js";
241
- import {
242
- recordUnmappedIdentity
243
- } from "./cli-c9mtm09b.js";
244
294
  import {
245
295
  findOrCreateUserByEmail,
246
296
  findUserByExternalId,
297
+ findUserById,
247
298
  fingerprintApiKey,
248
299
  getUserIdentities,
249
300
  linkIdentity,
@@ -251,18 +302,21 @@ import {
251
302
  listUserTokens,
252
303
  mintToken,
253
304
  recordIdentityEvent,
305
+ recordUnmappedIdentity,
306
+ renderIdentity,
307
+ resolveIdentity,
254
308
  resolveUserByToken,
255
309
  revokeToken,
256
310
  unlinkIdentity
257
- } from "./cli-h3k622d0.js";
258
- import"./cli-6xd0bvf8.js";
311
+ } from "./cli-6aq8xhvm.js";
312
+ import"./cli-ma04faw5.js";
259
313
  import"./cli-wspgs9bt.js";
260
314
  import {
261
315
  agentContextKey,
262
316
  createTaskWithSiblingAwareness,
263
317
  gitlabContextKey,
264
318
  pageContextKey
265
- } from "./cli-y4ycrnjc.js";
319
+ } from "./cli-k3y4z3h5.js";
266
320
  import {
267
321
  getAppUrl,
268
322
  getConfiguredAppUrls,
@@ -357,7 +411,6 @@ import {
357
411
  createTaskExtended,
358
412
  createUser,
359
413
  createWorkflow,
360
- decryptSecret,
361
414
  deleteActiveSession,
362
415
  deleteActiveSessionById,
363
416
  deleteBudget,
@@ -376,7 +429,6 @@ import {
376
429
  deleteTaskAttachment,
377
430
  deleteUser,
378
431
  deleteWorkflow,
379
- encryptSecret,
380
432
  failTask,
381
433
  findTaskByVcs,
382
434
  getActivePricingRow,
@@ -406,7 +458,6 @@ import {
406
458
  getCurrentRequestAuth,
407
459
  getDashboardCostSummary,
408
460
  getDb,
409
- getEncryptionKey,
410
461
  getInboxSummary,
411
462
  getInstanceActivity,
412
463
  getKeyCostSummary,
@@ -464,7 +515,7 @@ import {
464
515
  getTaskStats,
465
516
  getTasksCount,
466
517
  getTemplateDefinition,
467
- getUnassignedTaskIds,
518
+ getUnassignedTaskIdsForAgent,
468
519
  getUserById,
469
520
  getWorkflow,
470
521
  getWorkflowRun,
@@ -477,7 +528,6 @@ import {
477
528
  incrKv,
478
529
  incrementPageViewCount,
479
530
  initTelemetry,
480
- init_crypto,
481
531
  init_date_utils,
482
532
  init_db,
483
533
  init_registry,
@@ -516,7 +566,7 @@ import {
516
566
  listWorkflows,
517
567
  markKeyRateLimited,
518
568
  maskSecrets,
519
- normalizeDateRequired,
569
+ normalizeDate,
520
570
  pauseTask,
521
571
  recordBudgetRefusalNotification,
522
572
  recordInlineScriptRun,
@@ -574,22 +624,22 @@ import {
574
624
  upsertSwarmConfig,
575
625
  validateConfigValue,
576
626
  withFavoriteFlags
577
- } from "./cli-ncfgsqbd.js";
627
+ } from "./cli-15y7qhzp.js";
578
628
  import"./cli-z2zcxes1.js";
579
629
  import {
580
630
  init_zod
581
- } from "./cli-q21d49ac.js";
631
+ } from "./cli-bw80ck94.js";
582
632
  import {
583
633
  array,
584
634
  exports_external,
585
635
  number,
586
636
  object,
587
637
  string
588
- } from "./cli-anrj584m.js";
638
+ } from "./cli-xz9aq0rf.js";
589
639
  import {
590
640
  init_package,
591
641
  package_default
592
- } from "./cli-c3frqf65.js";
642
+ } from "./cli-b23axgz4.js";
593
643
  import {
594
644
  init_secret_scrubber,
595
645
  registerVolatileSecret,
@@ -603,18 +653,18 @@ import {
603
653
  } from "./cli-9aryf9tv.js";
604
654
  import {
605
655
  bedrockProviderModule
606
- } from "./cli-vbraamxr.js";
656
+ } from "./cli-6b4wwmm8.js";
657
+ import"./cli-cwy5hqbq.js";
607
658
  import"./cli-jvv0g8bh.js";
608
659
  import {
609
660
  setBedrockProviderModule
610
661
  } from "./cli-xw7p4wkq.js";
611
662
  import"./cli-hv2fndhm.js";
612
- import"./cli-cwy5hqbq.js";
613
663
  import"./cli-pscrc6tw.js";
614
- import"./cli-4wgngmwv.js";
615
- import"./cli-vapyk8sw.js";
616
664
  import"./cli-zyxs93y9.js";
665
+ import"./cli-4wgngmwv.js";
617
666
  import"./cli-bsyka9ta.js";
667
+ import"./cli-vapyk8sw.js";
618
668
  import"./cli-y2apkfe2.js";
619
669
  import"./cli-m6pzzhqr.js";
620
670
  import"./cli-7wmybrt5.js";
@@ -639,146 +689,6 @@ import {
639
689
  } from "node:http";
640
690
  init_db();
641
691
 
642
- // src/be/rbac-audit.ts
643
- init_db();
644
- var FLUSH_INTERVAL_MS = 2000;
645
- var FLUSH_MAX_ROWS = 200;
646
- var AUDIT_GC_INTERVAL_MS = 24 * 60 * 60 * 1000;
647
- var DEFAULT_RETENTION_DAYS = 30;
648
- var buffer = [];
649
- var flushTimer = null;
650
- var auditGcTimer = null;
651
- var thresholdFlushScheduled = false;
652
- function isAuditDisabled() {
653
- return process.env.RBAC_AUDIT_DISABLED === "true";
654
- }
655
- function principalIdOf(principal) {
656
- switch (principal.kind) {
657
- case "agent":
658
- return principal.agentId;
659
- case "user":
660
- return principal.userId;
661
- case "operator":
662
- return null;
663
- }
664
- }
665
- function resourceIdOf(resource) {
666
- if (!resource)
667
- return null;
668
- switch (resource.kind) {
669
- case "task":
670
- return resource.taskId;
671
- case "agent":
672
- return resource.agentId;
673
- case "kv-namespace":
674
- return resource.namespace;
675
- case "owned":
676
- return resource.ownerAgentId ?? null;
677
- case "none":
678
- return null;
679
- }
680
- }
681
- function toRow(check, decision) {
682
- return {
683
- principalType: check.principal.kind,
684
- principalId: principalIdOf(check.principal),
685
- originatorUserId: null,
686
- verb: check.verb,
687
- resourceType: check.resource?.kind ?? null,
688
- resourceId: resourceIdOf(check.resource),
689
- decision: decision.allow ? "allow" : "deny",
690
- reason: decision.allow ? null : decision.reason,
691
- source: check.source
692
- };
693
- }
694
- function enqueueAuditRow(check, decision) {
695
- if (isAuditDisabled())
696
- return;
697
- try {
698
- buffer.push(toRow(check, decision));
699
- if (buffer.length >= FLUSH_MAX_ROWS && !thresholdFlushScheduled) {
700
- thresholdFlushScheduled = true;
701
- const t = setTimeout(() => {
702
- thresholdFlushScheduled = false;
703
- flushAuditBuffer();
704
- }, 0);
705
- if (typeof t.unref === "function")
706
- t.unref();
707
- }
708
- } catch (err) {
709
- console.warn("[rbac-audit] enqueue failed, dropping row:", err.message);
710
- }
711
- }
712
- function flushAuditBuffer() {
713
- if (buffer.length === 0)
714
- return;
715
- const rows = buffer;
716
- buffer = [];
717
- try {
718
- const db = getDb();
719
- const stmt = db.prepare(`INSERT INTO permission_audit
720
- (principalType, principalId, originatorUserId, verb, resourceType, resourceId, decision, reason, source)
721
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
722
- const insertAll = db.transaction((batch) => {
723
- for (const r of batch) {
724
- stmt.run(r.principalType, r.principalId, r.originatorUserId, r.verb, r.resourceType, r.resourceId, r.decision, r.reason, r.source);
725
- }
726
- });
727
- insertAll(rows);
728
- } catch (err) {
729
- console.warn(`[rbac-audit] flush failed, dropping ${rows.length} row(s):`, err.message);
730
- }
731
- }
732
- function flushIntervalMs() {
733
- const v = Number(process.env.RBAC_AUDIT_FLUSH_MS);
734
- return Number.isFinite(v) && v > 0 ? v : FLUSH_INTERVAL_MS;
735
- }
736
- function startAuditWriter(intervalMs = flushIntervalMs()) {
737
- if (flushTimer)
738
- return;
739
- flushTimer = setInterval(() => flushAuditBuffer(), intervalMs);
740
- if (typeof flushTimer?.unref === "function")
741
- flushTimer.unref();
742
- }
743
- function stopAuditWriter() {
744
- if (flushTimer) {
745
- clearInterval(flushTimer);
746
- flushTimer = null;
747
- }
748
- }
749
- function purgeExpiredAuditRows() {
750
- try {
751
- const days = Number(process.env.RBAC_AUDIT_RETENTION_DAYS) || DEFAULT_RETENTION_DAYS;
752
- const result = getDb().prepare("DELETE FROM permission_audit WHERE ts < datetime('now', ?)").run(`-${days} days`);
753
- return result.changes;
754
- } catch (err) {
755
- console.warn("[rbac-audit] retention purge failed:", err.message);
756
- return 0;
757
- }
758
- }
759
- function startAuditGc(intervalMs = AUDIT_GC_INTERVAL_MS) {
760
- if (auditGcTimer)
761
- return;
762
- const purged = purgeExpiredAuditRows();
763
- if (purged > 0) {
764
- console.log(`[rbac-audit] Initial retention purge removed ${purged} audit row(s)`);
765
- }
766
- auditGcTimer = setInterval(() => {
767
- const n = purgeExpiredAuditRows();
768
- if (n > 0) {
769
- console.log(`[rbac-audit] Retention purge removed ${n} audit row(s)`);
770
- }
771
- }, intervalMs);
772
- if (typeof auditGcTimer?.unref === "function")
773
- auditGcTimer.unref();
774
- }
775
- function stopAuditGc() {
776
- if (auditGcTimer) {
777
- clearInterval(auditGcTimer);
778
- auditGcTimer = null;
779
- }
780
- }
781
-
782
692
  // src/gitlab/auth.ts
783
693
  var initialized = false;
784
694
  var webhookSecret = null;
@@ -896,7 +806,7 @@ registerTemplate({
896
806
  eventType: "gitlab.merge_request.opened",
897
807
  header: "[GitLab MR #{{mr_iid}}] {{mr_title}}",
898
808
  defaultBody: `Repo: {{repo}}
899
- Author: @{{username}}
809
+ Author: {{username}}
900
810
  Branch: {{source_branch}} → {{target_branch}}
901
811
  URL: {{mr_url}}
902
812
 
@@ -905,7 +815,10 @@ URL: {{mr_url}}
905
815
  { name: "mr_iid", description: "Merge request IID" },
906
816
  { name: "mr_title", description: "Merge request title" },
907
817
  { name: "repo", description: "Project path with namespace" },
908
- { name: "username", description: "Event sender username" },
818
+ {
819
+ name: "username",
820
+ description: "Author identity — resolved canonical name (e.g. 'Luis (gitlab:username)') or the UNKNOWN sentinel"
821
+ },
909
822
  { name: "source_branch", description: "Source branch name" },
910
823
  { name: "target_branch", description: "Target branch name" },
911
824
  { name: "mr_url", description: "Merge request URL" },
@@ -917,7 +830,7 @@ registerTemplate({
917
830
  eventType: "gitlab.issue.assigned",
918
831
  header: "[GitLab Issue #{{issue_iid}}] {{issue_title}}",
919
832
  defaultBody: `Repo: {{repo}}
920
- Author: @{{username}}
833
+ Author: {{username}}
921
834
  URL: {{issue_url}}
922
835
 
923
836
  {{context_section}}{{@template[common.command_suggestions.gitlab_issue]}}{{@template[common.delegation_instruction.gitlab]}}`,
@@ -925,7 +838,10 @@ URL: {{issue_url}}
925
838
  { name: "issue_iid", description: "Issue IID" },
926
839
  { name: "issue_title", description: "Issue title" },
927
840
  { name: "repo", description: "Project path with namespace" },
928
- { name: "username", description: "Event sender username" },
841
+ {
842
+ name: "username",
843
+ description: "Author identity — resolved canonical name (e.g. 'Luis (gitlab:username)') or the UNKNOWN sentinel"
844
+ },
929
845
  { name: "issue_url", description: "Issue URL" },
930
846
  { name: "context_section", description: "Context section with description or empty string" }
931
847
  ],
@@ -933,7 +849,7 @@ URL: {{issue_url}}
933
849
  });
934
850
  registerTemplate({
935
851
  eventType: "gitlab.comment.mentioned",
936
- header: "[GitLab Comment on {{entity_label}}] @{{username}} mentioned bot",
852
+ header: "[GitLab Comment on {{entity_label}}] {{username}} mentioned bot",
937
853
  defaultBody: `Repo: {{repo}}
938
854
  URL: {{target_url}}
939
855
 
@@ -941,7 +857,10 @@ Comment:
941
857
  {{context}}{{existing_task_note}}{{@template[common.delegation_instruction.gitlab]}}`,
942
858
  variables: [
943
859
  { name: "entity_label", description: "Entity label (e.g. 'MR #1' or 'Issue #2')" },
944
- { name: "username", description: "Comment author username" },
860
+ {
861
+ name: "username",
862
+ description: "Comment author identity — resolved canonical name (e.g. 'Luis (gitlab:username)') or the UNKNOWN sentinel"
863
+ },
945
864
  { name: "repo", description: "Project path with namespace" },
946
865
  { name: "target_url", description: "Target entity URL" },
947
866
  { name: "context", description: "Extracted mention context from comment" },
@@ -1025,6 +944,9 @@ function resolveGitLabSender(user, sampleEventType, sampleContext) {
1025
944
  incrKv(UNMAPPED_NAMESPACE, `${user.username}:count`, 1);
1026
945
  return;
1027
946
  }
947
+ function renderGitLabIdentity(username) {
948
+ return renderIdentity(resolveIdentity("gitlab", username));
949
+ }
1028
950
  async function handleMergeRequest(event) {
1029
951
  const { user, project, object_attributes: mr } = event;
1030
952
  const action = mr.action;
@@ -1052,7 +974,7 @@ async function handleMergeRequest(event) {
1052
974
  mr_iid: mr.iid,
1053
975
  mr_title: mr.title,
1054
976
  repo,
1055
- username: user.username,
977
+ username: renderGitLabIdentity(user.username),
1056
978
  source_branch: mr.source_branch,
1057
979
  target_branch: mr.target_branch,
1058
980
  mr_url: mr.url,
@@ -1135,7 +1057,7 @@ async function handleIssue2(event) {
1135
1057
  issue_iid: issue.iid,
1136
1058
  issue_title: issue.title,
1137
1059
  repo,
1138
- username: user.username,
1060
+ username: renderGitLabIdentity(user.username),
1139
1061
  issue_url: issue.url,
1140
1062
  context_section: contextSection
1141
1063
  });
@@ -1179,7 +1101,7 @@ async function handleIssue2(event) {
1179
1101
  async function handleNote(event) {
1180
1102
  const { user, project, object_attributes: note } = event;
1181
1103
  const repo = project.path_with_namespace;
1182
- const _requestedByUserId = resolveGitLabSender(user, "note", note.note);
1104
+ const requestedByUserId = resolveGitLabSender(user, "note", note.note);
1183
1105
  if (!detectMention(note.note)) {
1184
1106
  return { created: false };
1185
1107
  }
@@ -1214,7 +1136,7 @@ async function handleNote(event) {
1214
1136
  _Note: There's an active task (${existingTask.id}) for this ${entityLabel}._` : "";
1215
1137
  const noteResult = resolveTemplate("gitlab.comment.mentioned", {
1216
1138
  entity_label: entityLabel,
1217
- username: user.username,
1139
+ username: renderGitLabIdentity(user.username),
1218
1140
  repo,
1219
1141
  target_url: targetUrl,
1220
1142
  context,
@@ -1233,6 +1155,7 @@ _Note: There's an active task (${existingTask.id}) for this ${entityLabel}._` :
1233
1155
  vcsNumber: targetNumber,
1234
1156
  vcsCommentId: note.id,
1235
1157
  vcsAuthor: user.username,
1158
+ requestedByUserId,
1236
1159
  vcsUrl: targetUrl,
1237
1160
  parentTaskId: existingTask?.id,
1238
1161
  contextKey: targetNumber ? gitlabContextKey({
@@ -1247,8 +1170,9 @@ _Note: There's an active task (${existingTask.id}) for this ${entityLabel}._` :
1247
1170
  return { created: true, taskId: task.id };
1248
1171
  }
1249
1172
  async function handlePipeline(event) {
1250
- const { project, object_attributes: pipeline } = event;
1173
+ const { user, project, object_attributes: pipeline } = event;
1251
1174
  const repo = project.path_with_namespace;
1175
+ const requestedByUserId = resolveGitLabSender(user, "pipeline", `Pipeline #${pipeline.id}`);
1252
1176
  if (pipeline.status !== "failed") {
1253
1177
  return { created: false };
1254
1178
  }
@@ -1286,6 +1210,7 @@ async function handlePipeline(event) {
1286
1210
  vcsEventType: "pipeline",
1287
1211
  vcsNumber: mrIid,
1288
1212
  vcsAuthor: "",
1213
+ requestedByUserId,
1289
1214
  vcsUrl: event.merge_request.url,
1290
1215
  parentTaskId: existingTask.id,
1291
1216
  contextKey: gitlabContextKey({
@@ -2539,6 +2464,7 @@ async function handleApiKeys(req, res, pathSegments, queryParams) {
2539
2464
  init_zod();
2540
2465
  init_db();
2541
2466
  init_resolver();
2467
+ init_request_auth_context();
2542
2468
  init_event_bus();
2543
2469
  var QuestionSchema = exports_external.object({
2544
2470
  id: exports_external.string(),
@@ -2723,6 +2649,10 @@ async function handleApprovalRequests(req, res, pathSegments, queryParams) {
2723
2649
  if (!parsed)
2724
2650
  return true;
2725
2651
  const id = crypto.randomUUID();
2652
+ const auth = getRequestAuth(req);
2653
+ const rawCallerAgentId = req.headers["x-agent-id"];
2654
+ const callerAgentId = Array.isArray(rawCallerAgentId) ? rawCallerAgentId[0] : rawCallerAgentId;
2655
+ const createdBy = auth?.kind === "user" ? auth.userId : resolveTaskAuditUserId(parsed.body.sourceTaskId, callerAgentId) ?? undefined;
2726
2656
  const request = createApprovalRequest({
2727
2657
  id,
2728
2658
  title: parsed.body.title,
@@ -2732,7 +2662,8 @@ async function handleApprovalRequests(req, res, pathSegments, queryParams) {
2732
2662
  workflowRunStepId: parsed.body.workflowRunStepId,
2733
2663
  sourceTaskId: parsed.body.sourceTaskId,
2734
2664
  timeoutSeconds: parsed.body.timeoutSeconds,
2735
- notificationChannels: parsed.body.notifications
2665
+ notificationChannels: parsed.body.notifications,
2666
+ createdBy
2736
2667
  });
2737
2668
  res.writeHead(201, { "Content-Type": "application/json" });
2738
2669
  res.end(JSON.stringify({ approvalRequest: request }));
@@ -2991,6 +2922,7 @@ init_swarm_config_guard();
2991
2922
  init_request_auth_context();
2992
2923
  init_secret_scrubber();
2993
2924
  var MAX_ENV_PRESENCE_KEYS = 200;
2925
+ var SECRETS_FORCE_MASK_NOTE = " (secret values masked: reading unmasked secrets requires the lead agent)";
2994
2926
  var API_ONLY_CONFIG_KEYS = new Set(["API_AGENT_FS_API_KEY"]);
2995
2927
  function stripApiOnlyKeys(configs) {
2996
2928
  return configs.filter((config) => !API_ONLY_CONFIG_KEYS.has(config.key));
@@ -2999,6 +2931,23 @@ function singleHeader(req, name) {
2999
2931
  const raw = req.headers[name];
3000
2932
  return Array.isArray(raw) ? raw[0] : raw;
3001
2933
  }
2934
+ function userMayReadSecrets(req) {
2935
+ const auth = getRequestAuth(req);
2936
+ if (auth?.kind === "operator")
2937
+ return true;
2938
+ if (auth?.kind !== "user")
2939
+ return true;
2940
+ if (!isRbacEnabled())
2941
+ return true;
2942
+ const grant = getUserGrant(auth.userId);
2943
+ return grant.grantsAll || grant.verbs.has("config.read.secrets");
2944
+ }
2945
+ function resolveSecretsRead(req, includeSecrets) {
2946
+ if (!includeSecrets || userMayReadSecrets(req)) {
2947
+ return { effectiveIncludeSecrets: includeSecrets, secretsNote: "" };
2948
+ }
2949
+ return { effectiveIncludeSecrets: false, secretsNote: SECRETS_FORCE_MASK_NOTE };
2950
+ }
3002
2951
  function ensureConfigAdmin(req, res, verb) {
3003
2952
  const auth = getRequestAuth(req);
3004
2953
  if (auth?.kind === "operator" || auth?.kind === "user")
@@ -3128,16 +3077,20 @@ async function handleConfig(req, res, pathSegments, queryParams) {
3128
3077
  if (!parsed)
3129
3078
  return true;
3130
3079
  const includeSecrets = parsed.query.includeSecrets === "true";
3080
+ const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
3131
3081
  const configs = stripApiOnlyKeys(getResolvedConfig(parsed.query.agentId || undefined, parsed.query.repoId || undefined));
3132
- const result = includeSecrets ? configs : maskSecrets(configs);
3133
- if (includeSecrets) {
3082
+ const result = effectiveIncludeSecrets ? configs : maskSecrets(configs);
3083
+ if (effectiveIncludeSecrets) {
3134
3084
  for (const c of result) {
3135
3085
  if (c.isSecret && c.value) {
3136
3086
  registerVolatileSecret(c.value, `config:${c.key}`);
3137
3087
  }
3138
3088
  }
3139
3089
  }
3140
- json(res, { configs: result });
3090
+ json(res, {
3091
+ configs: result,
3092
+ ...secretsNote ? { message: `Found ${result.length} config(s).${secretsNote}` } : {}
3093
+ });
3141
3094
  return true;
3142
3095
  }
3143
3096
  if (envPresence.match(req.method, pathSegments)) {
@@ -3178,11 +3131,15 @@ async function handleConfig(req, res, pathSegments, queryParams) {
3178
3131
  jsonError(res, "Config not found", 404);
3179
3132
  return true;
3180
3133
  }
3181
- const singleResult = includeSecrets ? config : maskSecrets([config])[0];
3182
- if (includeSecrets && singleResult.isSecret && singleResult.value) {
3134
+ const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
3135
+ const singleResult = effectiveIncludeSecrets ? config : maskSecrets([config])[0];
3136
+ if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
3183
3137
  registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
3184
3138
  }
3185
- json(res, singleResult);
3139
+ json(res, {
3140
+ ...singleResult,
3141
+ ...secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}
3142
+ });
3186
3143
  return true;
3187
3144
  }
3188
3145
  if (listConfig.match(req.method, pathSegments)) {
@@ -3190,19 +3147,23 @@ async function handleConfig(req, res, pathSegments, queryParams) {
3190
3147
  if (!parsed)
3191
3148
  return true;
3192
3149
  const includeSecrets = parsed.query.includeSecrets === "true";
3150
+ const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
3193
3151
  const configs = stripApiOnlyKeys(getSwarmConfigs({
3194
3152
  scope: parsed.query.scope || undefined,
3195
3153
  scopeId: parsed.query.scopeId || undefined
3196
3154
  }));
3197
- const listResult = includeSecrets ? configs : maskSecrets(configs);
3198
- if (includeSecrets) {
3155
+ const listResult = effectiveIncludeSecrets ? configs : maskSecrets(configs);
3156
+ if (effectiveIncludeSecrets) {
3199
3157
  for (const c of listResult) {
3200
3158
  if (c.isSecret && c.value) {
3201
3159
  registerVolatileSecret(c.value, `config:${c.key}`);
3202
3160
  }
3203
3161
  }
3204
3162
  }
3205
- json(res, { configs: listResult });
3163
+ json(res, {
3164
+ configs: listResult,
3165
+ ...secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}
3166
+ });
3206
3167
  return true;
3207
3168
  }
3208
3169
  if (upsertConfig.match(req.method, pathSegments)) {
@@ -3592,7 +3553,7 @@ var putFavorite = route({
3592
3553
  pattern: ["api", "favorites"],
3593
3554
  summary: "Set favorite state for an item",
3594
3555
  tags: ["Favorites"],
3595
- rbac: { ungated: "self-scoped to the authenticated user; no cross-principal access" },
3556
+ rbac: { permission: "favorite.write.own" },
3596
3557
  body: exports_external.object({
3597
3558
  itemType: FavoriteItemTypeSchema,
3598
3559
  itemId: exports_external.string().min(1),
@@ -4472,11 +4433,11 @@ async function readRawBody(req, maxBytes) {
4472
4433
  const chunks = [];
4473
4434
  let total = 0;
4474
4435
  for await (const chunk of req) {
4475
- const buffer2 = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4476
- total += buffer2.byteLength;
4436
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4437
+ total += buffer.byteLength;
4477
4438
  if (total > maxBytes)
4478
4439
  return BODY_TOO_LARGE;
4479
- chunks.push(buffer2);
4440
+ chunks.push(buffer);
4480
4441
  }
4481
4442
  return Buffer.concat(chunks);
4482
4443
  }
@@ -6559,6 +6520,7 @@ var SDK_TOOL_NAME_MAP = {
6559
6520
  mcpServer_install: "mcp-server-install",
6560
6521
  mcpServer_uninstall: "mcp-server-uninstall",
6561
6522
  page_create: "create_page",
6523
+ page_delete: "delete-page",
6562
6524
  metric_create: "create_metric",
6563
6525
  request_humanInput: "request-human-input"
6564
6526
  };
@@ -6651,466 +6613,6 @@ async function handleMcpBridge(req, res, pathSegments, _queryParams, myAgentId)
6651
6613
  // src/http/mcp-oauth.ts
6652
6614
  init_zod();
6653
6615
  init_db();
6654
-
6655
- // src/be/db-queries/mcp-oauth.ts
6656
- init_crypto();
6657
- init_date_utils();
6658
- init_db();
6659
- function encryptOrNull(plaintext) {
6660
- if (plaintext == null || plaintext === "")
6661
- return null;
6662
- return encryptSecret(plaintext, getEncryptionKey());
6663
- }
6664
- function decryptOrNull(ciphertext) {
6665
- if (ciphertext == null || ciphertext === "")
6666
- return null;
6667
- return decryptSecret(ciphertext, getEncryptionKey());
6668
- }
6669
- function decryptTokenRow(row) {
6670
- const key = getEncryptionKey();
6671
- return {
6672
- ...row,
6673
- accessToken: decryptSecret(row.accessToken, key),
6674
- refreshToken: decryptOrNull(row.refreshToken),
6675
- dcrClientSecret: decryptOrNull(row.dcrClientSecret),
6676
- createdAt: normalizeDateRequired(row.createdAt),
6677
- updatedAt: normalizeDateRequired(row.updatedAt)
6678
- };
6679
- }
6680
- function getMcpOAuthToken(mcpServerId, userId = null) {
6681
- const row = getDb().query(userId == null ? "SELECT * FROM mcp_oauth_tokens WHERE mcpServerId = ? AND userId IS NULL" : "SELECT * FROM mcp_oauth_tokens WHERE mcpServerId = ? AND userId = ?").get(...userId == null ? [mcpServerId] : [mcpServerId, userId]);
6682
- return row ? decryptTokenRow(row) : null;
6683
- }
6684
- function upsertMcpOAuthToken(input) {
6685
- const userId = input.userId ?? null;
6686
- const encryptedAccess = encryptSecret(input.accessToken, getEncryptionKey());
6687
- const encryptedRefresh = encryptOrNull(input.refreshToken);
6688
- const encryptedClientSecret = encryptOrNull(input.dcrClientSecret);
6689
- const existing = getMcpOAuthToken(input.mcpServerId, userId);
6690
- if (!existing) {
6691
- getDb().query(`INSERT INTO mcp_oauth_tokens (
6692
- mcpServerId, userId,
6693
- accessToken, refreshToken, tokenType, expiresAt, scope,
6694
- resourceUrl, authorizationServerIssuer, authorizeUrl, tokenUrl, revocationUrl,
6695
- dcrClientId, dcrClientSecret, clientSource,
6696
- status, lastErrorMessage, lastRefreshedAt, connectedByUserId
6697
- )
6698
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.mcpServerId, userId, encryptedAccess, encryptedRefresh, input.tokenType ?? "Bearer", input.expiresAt ?? null, input.scope ?? null, input.resourceUrl, input.authorizationServerIssuer, input.authorizeUrl, input.tokenUrl, input.revocationUrl ?? null, input.dcrClientId ?? null, encryptedClientSecret, input.clientSource, input.status ?? "connected", input.lastErrorMessage ?? null, input.lastRefreshedAt ?? null, input.connectedByUserId ?? null);
6699
- return;
6700
- }
6701
- getDb().query(`UPDATE mcp_oauth_tokens SET
6702
- accessToken = ?,
6703
- refreshToken = COALESCE(?, refreshToken),
6704
- tokenType = ?,
6705
- expiresAt = ?,
6706
- scope = COALESCE(?, scope),
6707
- resourceUrl = ?,
6708
- authorizationServerIssuer = ?,
6709
- authorizeUrl = ?,
6710
- tokenUrl = ?,
6711
- revocationUrl = ?,
6712
- dcrClientId = COALESCE(?, dcrClientId),
6713
- dcrClientSecret = COALESCE(?, dcrClientSecret),
6714
- clientSource = ?,
6715
- status = ?,
6716
- lastErrorMessage = ?,
6717
- lastRefreshedAt = ?,
6718
- connectedByUserId = COALESCE(?, connectedByUserId),
6719
- updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
6720
- WHERE id = ?`).run(encryptedAccess, encryptedRefresh, input.tokenType ?? "Bearer", input.expiresAt ?? null, input.scope ?? null, input.resourceUrl, input.authorizationServerIssuer, input.authorizeUrl, input.tokenUrl, input.revocationUrl ?? null, input.dcrClientId ?? null, encryptedClientSecret, input.clientSource, input.status ?? "connected", input.lastErrorMessage ?? null, input.lastRefreshedAt ?? null, input.connectedByUserId ?? null, existing.id);
6721
- }
6722
- function applyMcpOAuthRefresh(id, data) {
6723
- const key = getEncryptionKey();
6724
- const encryptedAccess = encryptSecret(data.accessToken, key);
6725
- const encryptedRefresh = data.refreshToken === undefined ? undefined : data.refreshToken == null ? null : encryptSecret(data.refreshToken, key);
6726
- if (encryptedRefresh === undefined) {
6727
- getDb().query(`UPDATE mcp_oauth_tokens
6728
- SET accessToken = ?,
6729
- expiresAt = COALESCE(?, expiresAt),
6730
- scope = COALESCE(?, scope),
6731
- status = 'connected',
6732
- lastErrorMessage = NULL,
6733
- lastRefreshedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
6734
- updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
6735
- WHERE id = ?`).run(encryptedAccess, data.expiresAt ?? null, data.scope ?? null, id);
6736
- } else {
6737
- getDb().query(`UPDATE mcp_oauth_tokens
6738
- SET accessToken = ?,
6739
- refreshToken = ?,
6740
- expiresAt = COALESCE(?, expiresAt),
6741
- scope = COALESCE(?, scope),
6742
- status = 'connected',
6743
- lastErrorMessage = NULL,
6744
- lastRefreshedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
6745
- updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
6746
- WHERE id = ?`).run(encryptedAccess, encryptedRefresh, data.expiresAt ?? null, data.scope ?? null, id);
6747
- }
6748
- }
6749
- function markMcpOAuthTokenStatus(id, status, errorMessage) {
6750
- getDb().query(`UPDATE mcp_oauth_tokens
6751
- SET status = ?,
6752
- lastErrorMessage = ?,
6753
- updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
6754
- WHERE id = ?`).run(status, errorMessage ?? null, id);
6755
- }
6756
- function deleteMcpOAuthToken(mcpServerId, userId = null) {
6757
- const result = getDb().query(userId == null ? "DELETE FROM mcp_oauth_tokens WHERE mcpServerId = ? AND userId IS NULL" : "DELETE FROM mcp_oauth_tokens WHERE mcpServerId = ? AND userId = ?").run(...userId == null ? [mcpServerId] : [mcpServerId, userId]);
6758
- return result.changes > 0;
6759
- }
6760
- function isMcpTokenExpiringSoon(token, bufferMs = 5 * 60 * 1000) {
6761
- if (!token.expiresAt)
6762
- return false;
6763
- const expiresAt = new Date(token.expiresAt).getTime();
6764
- if (Number.isNaN(expiresAt))
6765
- return true;
6766
- return expiresAt - Date.now() < bufferMs;
6767
- }
6768
- function insertMcpOAuthPending(input) {
6769
- const key = getEncryptionKey();
6770
- getDb().query(`INSERT INTO mcp_oauth_pending (
6771
- state, mcpServerId, userId,
6772
- codeVerifier, nonce,
6773
- resourceUrl, authorizationServerIssuer, authorizeUrl, tokenUrl, revocationUrl,
6774
- scopes, dcrClientId, dcrClientSecret, redirectUri, finalRedirect
6775
- )
6776
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.state, input.mcpServerId, input.userId ?? null, encryptSecret(input.codeVerifier, key), input.nonce ?? null, input.resourceUrl, input.authorizationServerIssuer, input.authorizeUrl, input.tokenUrl, input.revocationUrl ?? null, input.scopes ?? null, input.dcrClientId ?? null, encryptOrNull(input.dcrClientSecret), input.redirectUri, input.finalRedirect ?? null);
6777
- }
6778
- function consumeMcpOAuthPending(state) {
6779
- const row = getDb().query("SELECT * FROM mcp_oauth_pending WHERE state = ?").get(state);
6780
- if (!row)
6781
- return null;
6782
- getDb().query("DELETE FROM mcp_oauth_pending WHERE state = ?").run(state);
6783
- const key = getEncryptionKey();
6784
- return {
6785
- ...row,
6786
- codeVerifier: decryptSecret(row.codeVerifier, key),
6787
- dcrClientSecret: decryptOrNull(row.dcrClientSecret)
6788
- };
6789
- }
6790
- function gcMcpOAuthPending(olderThanMs = 10 * 60 * 1000) {
6791
- const cutoff = new Date(Date.now() - olderThanMs).toISOString();
6792
- const result = getDb().query("DELETE FROM mcp_oauth_pending WHERE createdAt < ?").run(cutoff);
6793
- return result.changes;
6794
- }
6795
- function setMcpServerAuthMethod(mcpServerId, authMethod) {
6796
- getDb().query("UPDATE mcp_servers SET authMethod = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?").run(authMethod, mcpServerId);
6797
- }
6798
-
6799
- // src/oauth/mcp-wrapper.ts
6800
- var PRIVATE_IPV4_BLOCKS = [
6801
- { prefix: "10.", mask: 8 },
6802
- { prefix: "127.", mask: 8 },
6803
- { prefix: "169.254.", mask: 16 },
6804
- { prefix: "172.16.", mask: 12 },
6805
- { prefix: "192.168.", mask: 16 },
6806
- { prefix: "0.", mask: 8 }
6807
- ];
6808
- function isPrivateIPv4(host) {
6809
- if (host === "127.0.0.1")
6810
- return true;
6811
- if (host.startsWith("169.254."))
6812
- return true;
6813
- if (host.startsWith("10."))
6814
- return true;
6815
- if (host.startsWith("192.168."))
6816
- return true;
6817
- if (host.startsWith("0."))
6818
- return true;
6819
- if (host.startsWith("172.")) {
6820
- const parts = host.split(".");
6821
- const second = parseInt(parts[1] ?? "", 10);
6822
- if (Number.isFinite(second) && second >= 16 && second <= 31)
6823
- return true;
6824
- }
6825
- for (const block of PRIVATE_IPV4_BLOCKS) {
6826
- if (block.prefix !== "172." && host.startsWith(block.prefix))
6827
- return true;
6828
- }
6829
- return false;
6830
- }
6831
- function isPrivateIPv6(host) {
6832
- const lower = host.toLowerCase();
6833
- if (lower === "::1" || lower === "[::1]")
6834
- return true;
6835
- if (lower.startsWith("fe80:") || lower.startsWith("[fe80:"))
6836
- return true;
6837
- if (lower.startsWith("fc") || lower.startsWith("fd"))
6838
- return true;
6839
- if (lower.startsWith("[fc") || lower.startsWith("[fd"))
6840
- return true;
6841
- return false;
6842
- }
6843
- function assertUrlSafe(rawUrl, opts = {}) {
6844
- let parsed;
6845
- try {
6846
- parsed = new URL(rawUrl);
6847
- } catch {
6848
- throw new Error(`Invalid URL: ${rawUrl}`);
6849
- }
6850
- const allowPrivate = opts.allowPrivateHosts === true;
6851
- const allowInsecure = opts.allowInsecure === true;
6852
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
6853
- throw new Error(`Refusing unsupported protocol: ${parsed.protocol}`);
6854
- }
6855
- if (parsed.protocol === "http:" && !allowInsecure) {
6856
- throw new Error(`Refusing insecure (http://) URL in production: ${rawUrl}`);
6857
- }
6858
- const host = parsed.hostname;
6859
- if (!host) {
6860
- throw new Error(`Missing hostname: ${rawUrl}`);
6861
- }
6862
- if (host === "localhost" && !allowPrivate) {
6863
- throw new Error(`Refusing loopback hostname: ${host}`);
6864
- }
6865
- if (isPrivateIPv4(host) && !allowPrivate) {
6866
- throw new Error(`Refusing private IPv4 host: ${host}`);
6867
- }
6868
- if (isPrivateIPv6(host) && !allowPrivate) {
6869
- throw new Error(`Refusing private IPv6 host: ${host}`);
6870
- }
6871
- return parsed;
6872
- }
6873
- function defaultSsrfOptions() {
6874
- return {
6875
- allowPrivateHosts: process.env.MCP_OAUTH_ALLOW_PRIVATE_HOSTS === "true",
6876
- allowInsecure: true
6877
- };
6878
- }
6879
- async function safeFetch(url, init) {
6880
- assertUrlSafe(url, defaultSsrfOptions());
6881
- return fetch(url, init);
6882
- }
6883
- async function discoverProtectedResourceMetadata(resourceUrl) {
6884
- const base = new URL(resourceUrl);
6885
- const wellKnown = new URL("/.well-known/oauth-protected-resource", base).toString();
6886
- try {
6887
- const res = await safeFetch(wellKnown, {
6888
- headers: { Accept: "application/json" }
6889
- });
6890
- if (res.ok) {
6891
- return await res.json();
6892
- }
6893
- } catch {}
6894
- try {
6895
- const probe = await safeFetch(resourceUrl, { method: "HEAD" });
6896
- const wwwAuth = probe.headers.get("www-authenticate");
6897
- if (wwwAuth) {
6898
- const match = /resource_metadata="([^"]+)"/i.exec(wwwAuth);
6899
- if (match) {
6900
- const metaRes = await safeFetch(match[1], {
6901
- headers: { Accept: "application/json" }
6902
- });
6903
- if (metaRes.ok) {
6904
- return await metaRes.json();
6905
- }
6906
- }
6907
- }
6908
- } catch {}
6909
- return null;
6910
- }
6911
- async function discoverAuthorizationServerMetadata(issuer) {
6912
- const issuerUrl = new URL(issuer);
6913
- const candidates = [
6914
- new URL("/.well-known/oauth-authorization-server", issuerUrl).toString(),
6915
- new URL("/.well-known/openid-configuration", issuerUrl).toString()
6916
- ];
6917
- let lastError = null;
6918
- for (const candidate of candidates) {
6919
- try {
6920
- const res = await safeFetch(candidate, {
6921
- headers: { Accept: "application/json" }
6922
- });
6923
- if (res.ok) {
6924
- return await res.json();
6925
- }
6926
- lastError = new Error(`Metadata fetch ${candidate} → ${res.status}`);
6927
- } catch (err) {
6928
- lastError = err instanceof Error ? err : new Error(String(err));
6929
- }
6930
- }
6931
- throw lastError ?? new Error(`Authorization server metadata not found at ${issuer}`);
6932
- }
6933
- async function registerClient(registrationEndpoint, req) {
6934
- const res = await safeFetch(registrationEndpoint, {
6935
- method: "POST",
6936
- headers: {
6937
- "Content-Type": "application/json",
6938
- Accept: "application/json"
6939
- },
6940
- body: JSON.stringify(req)
6941
- });
6942
- if (!res.ok) {
6943
- const body = await res.text().catch(() => "");
6944
- throw new Error(`Dynamic client registration failed (${res.status}): ${body}`);
6945
- }
6946
- return await res.json();
6947
- }
6948
- async function buildAuthorizeUrl(input) {
6949
- const state = input.state ?? generateRandomState();
6950
- const codeVerifier = generateRandomCodeVerifier();
6951
- const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
6952
- const url = new URL(input.authorizeUrl);
6953
- url.searchParams.set("response_type", "code");
6954
- url.searchParams.set("client_id", input.clientId);
6955
- url.searchParams.set("redirect_uri", input.redirectUri);
6956
- if (input.scopes.length > 0) {
6957
- url.searchParams.set("scope", input.scopes.join(" "));
6958
- }
6959
- url.searchParams.set("state", state);
6960
- url.searchParams.set("code_challenge", codeChallenge);
6961
- url.searchParams.set("code_challenge_method", "S256");
6962
- url.searchParams.set("resource", input.resource);
6963
- if (input.extraParams) {
6964
- const RESERVED = new Set([
6965
- "response_type",
6966
- "client_id",
6967
- "redirect_uri",
6968
- "scope",
6969
- "state",
6970
- "code_challenge",
6971
- "code_challenge_method",
6972
- "resource"
6973
- ]);
6974
- for (const [k, v] of Object.entries(input.extraParams)) {
6975
- if (RESERVED.has(k.toLowerCase())) {
6976
- console.warn(`[mcp-oauth] extraParams key "${k}" is reserved and skipped`);
6977
- continue;
6978
- }
6979
- url.searchParams.set(k, v);
6980
- }
6981
- }
6982
- return { url: url.toString(), state, codeVerifier, codeChallenge };
6983
- }
6984
- async function exchangeCodeForTokens(input) {
6985
- const body = new URLSearchParams({
6986
- grant_type: "authorization_code",
6987
- code: input.code,
6988
- redirect_uri: input.redirectUri,
6989
- client_id: input.clientId,
6990
- code_verifier: input.codeVerifier,
6991
- resource: input.resource
6992
- });
6993
- if (input.clientSecret)
6994
- body.set("client_secret", input.clientSecret);
6995
- const res = await safeFetch(input.tokenUrl, {
6996
- method: "POST",
6997
- headers: {
6998
- "Content-Type": "application/x-www-form-urlencoded",
6999
- Accept: "application/json"
7000
- },
7001
- body: body.toString()
7002
- });
7003
- if (!res.ok) {
7004
- const text = await res.text().catch(() => "");
7005
- throw new Error(`Token exchange failed (${res.status}): ${text}`);
7006
- }
7007
- return await res.json();
7008
- }
7009
- async function refreshMcpToken(input) {
7010
- const body = new URLSearchParams({
7011
- grant_type: "refresh_token",
7012
- refresh_token: input.refreshToken,
7013
- client_id: input.clientId,
7014
- resource: input.resource
7015
- });
7016
- if (input.clientSecret)
7017
- body.set("client_secret", input.clientSecret);
7018
- if (input.scopes && input.scopes.length > 0)
7019
- body.set("scope", input.scopes.join(" "));
7020
- const res = await safeFetch(input.tokenUrl, {
7021
- method: "POST",
7022
- headers: {
7023
- "Content-Type": "application/x-www-form-urlencoded",
7024
- Accept: "application/json"
7025
- },
7026
- body: body.toString()
7027
- });
7028
- if (!res.ok) {
7029
- const text = await res.text().catch(() => "");
7030
- throw new Error(`Token refresh failed (${res.status}): ${text}`);
7031
- }
7032
- return await res.json();
7033
- }
7034
- async function revokeMcpToken(input) {
7035
- const body = new URLSearchParams({
7036
- token: input.token,
7037
- client_id: input.clientId
7038
- });
7039
- if (input.tokenTypeHint)
7040
- body.set("token_type_hint", input.tokenTypeHint);
7041
- if (input.clientSecret)
7042
- body.set("client_secret", input.clientSecret);
7043
- const res = await safeFetch(input.revocationUrl, {
7044
- method: "POST",
7045
- headers: {
7046
- "Content-Type": "application/x-www-form-urlencoded",
7047
- Accept: "application/json"
7048
- },
7049
- body: body.toString()
7050
- });
7051
- if (!res.ok && res.status !== 200 && res.status !== 204) {
7052
- const text = await res.text().catch(() => "");
7053
- throw new Error(`Token revocation failed (${res.status}): ${text}`);
7054
- }
7055
- }
7056
- function computeExpiresAt(expiresInSeconds) {
7057
- if (!expiresInSeconds || expiresInSeconds <= 0)
7058
- return null;
7059
- return new Date(Date.now() + expiresInSeconds * 1000).toISOString();
7060
- }
7061
-
7062
- // src/oauth/ensure-mcp-token.ts
7063
- var inflight = new Map;
7064
- async function ensureMcpToken(mcpServerId, opts = {}) {
7065
- const userId = opts.userId ?? null;
7066
- const key = `${mcpServerId}::${userId ?? "_"}`;
7067
- const existing = inflight.get(key);
7068
- if (existing)
7069
- return existing;
7070
- const work = (async () => {
7071
- const token = getMcpOAuthToken(mcpServerId, userId);
7072
- if (!token)
7073
- return null;
7074
- if (token.status === "revoked")
7075
- return token;
7076
- if (!isMcpTokenExpiringSoon(token, opts.bufferMs))
7077
- return token;
7078
- if (!token.refreshToken) {
7079
- markMcpOAuthTokenStatus(token.id, "expired", "No refresh token available; reconnect required.");
7080
- return { ...token, status: "expired" };
7081
- }
7082
- try {
7083
- const refreshed = await refreshMcpToken({
7084
- tokenUrl: token.tokenUrl,
7085
- clientId: token.dcrClientId ?? "",
7086
- clientSecret: token.dcrClientSecret ?? undefined,
7087
- refreshToken: token.refreshToken,
7088
- resource: token.resourceUrl,
7089
- scopes: token.scope ? token.scope.split(" ").filter(Boolean) : undefined
7090
- });
7091
- applyMcpOAuthRefresh(token.id, {
7092
- accessToken: refreshed.access_token,
7093
- refreshToken: refreshed.refresh_token ?? undefined,
7094
- expiresAt: computeExpiresAt(refreshed.expires_in),
7095
- scope: refreshed.scope ?? null
7096
- });
7097
- return getMcpOAuthToken(mcpServerId, userId);
7098
- } catch (err) {
7099
- const message = err instanceof Error ? err.message : String(err);
7100
- markMcpOAuthTokenStatus(token.id, "error", message);
7101
- console.error(`[mcp-oauth] refresh failed for ${mcpServerId}: ${message}`);
7102
- return { ...token, status: "error", lastErrorMessage: message };
7103
- }
7104
- })();
7105
- inflight.set(key, work);
7106
- try {
7107
- return await work;
7108
- } finally {
7109
- inflight.delete(key);
7110
- }
7111
- }
7112
-
7113
- // src/http/mcp-oauth.ts
7114
6616
  function ssrfOptions() {
7115
6617
  return {
7116
6618
  allowPrivateHosts: process.env.MCP_OAUTH_ALLOW_PRIVATE_HOSTS === "true",
@@ -7227,6 +6729,7 @@ var authorizeRoute = route({
7227
6729
  userId: exports_external.string().optional(),
7228
6730
  scopes: exports_external.string().optional()
7229
6731
  }),
6732
+ rbac: { permission: "mcp-oauth.authorize.any" },
7230
6733
  responses: {
7231
6734
  302: { description: "Redirect to authorization server" },
7232
6735
  400: { description: "MCP has no URL / does not require OAuth" },
@@ -7246,6 +6749,7 @@ var authorizeUrlRoute = route({
7246
6749
  userId: exports_external.string().optional(),
7247
6750
  scopes: exports_external.string().optional()
7248
6751
  }),
6752
+ rbac: { permission: "mcp-oauth.authorize.any" },
7249
6753
  responses: {
7250
6754
  200: { description: "{ providerUrl: string }" },
7251
6755
  400: { description: "MCP has no URL / does not require OAuth" },
@@ -7713,6 +7217,7 @@ function stopMcpOAuthPendingGc() {
7713
7217
  // src/http/mcp-servers.ts
7714
7218
  init_zod();
7715
7219
  init_db();
7220
+ init_request_auth_context();
7716
7221
  var listMcpServersRoute = route({
7717
7222
  method: "get",
7718
7223
  path: "/api/mcp-servers",
@@ -7767,7 +7272,8 @@ var createMcpServerRoute = route({
7767
7272
  responses: {
7768
7273
  201: { description: "MCP server created" },
7769
7274
  400: { description: "Validation error" }
7770
- }
7275
+ },
7276
+ rbac: { permission: "mcp-server.create.swarm" }
7771
7277
  });
7772
7278
  var updateMcpServerRoute = route({
7773
7279
  method: "put",
@@ -7781,7 +7287,8 @@ var updateMcpServerRoute = route({
7781
7287
  responses: {
7782
7288
  200: { description: "MCP server updated" },
7783
7289
  404: { description: "MCP server not found" }
7784
- }
7290
+ },
7291
+ rbac: { permission: "mcp-server.update.any" }
7785
7292
  });
7786
7293
  var deleteMcpServerRoute = route({
7787
7294
  method: "delete",
@@ -7794,7 +7301,8 @@ var deleteMcpServerRoute = route({
7794
7301
  responses: {
7795
7302
  200: { description: "MCP server deleted" },
7796
7303
  404: { description: "MCP server not found" }
7797
- }
7304
+ },
7305
+ rbac: { permission: "mcp-server.delete.any" }
7798
7306
  });
7799
7307
  var installMcpServerRoute = route({
7800
7308
  method: "post",
@@ -7810,7 +7318,8 @@ var installMcpServerRoute = route({
7810
7318
  responses: {
7811
7319
  200: { description: "MCP server installed" },
7812
7320
  404: { description: "MCP server not found" }
7813
- }
7321
+ },
7322
+ rbac: { permission: "mcp-server.install.any" }
7814
7323
  });
7815
7324
  var uninstallMcpServerRoute = route({
7816
7325
  method: "delete",
@@ -7822,7 +7331,8 @@ var uninstallMcpServerRoute = route({
7822
7331
  params: exports_external.object({ id: exports_external.string(), agentId: exports_external.string() }),
7823
7332
  responses: {
7824
7333
  200: { description: "MCP server uninstalled" }
7825
- }
7334
+ },
7335
+ rbac: { permission: "mcp-server.uninstall.any" }
7826
7336
  });
7827
7337
  var getAgentMcpServersRoute = route({
7828
7338
  method: "get",
@@ -7839,14 +7349,39 @@ var getAgentMcpServersRoute = route({
7839
7349
  200: { description: "Agent MCP servers list" }
7840
7350
  }
7841
7351
  });
7842
- async function handleMcpServers(req, res, pathSegments, queryParams) {
7843
- if (getAgentMcpServersRoute.match(req.method, pathSegments)) {
7844
- const parsed = await getAgentMcpServersRoute.parse(req, res, pathSegments, queryParams);
7845
- if (!parsed)
7846
- return true;
7352
+ function canResolveMcpSecretsForHttpUser(req) {
7353
+ const auth = getRequestAuth(req);
7354
+ if (auth?.kind !== "user")
7355
+ return true;
7356
+ if (!isRbacEnabled())
7357
+ return true;
7358
+ const verb = "mcp-server.read.secrets";
7359
+ const grant = getUserGrant(auth.userId);
7360
+ const decision = grant.grantsAll || grant.verbs.has(verb) ? { allow: true, verb } : {
7361
+ allow: false,
7362
+ reason: `admission: missing permission '${verb}'`,
7363
+ verb
7364
+ };
7365
+ enqueueAdmissionRow({
7366
+ userId: auth.userId,
7367
+ decision,
7368
+ method: req.method,
7369
+ route: getAgentMcpServersRoute.def.path
7370
+ });
7371
+ return decision.allow;
7372
+ }
7373
+ async function handleMcpServers(req, res, pathSegments, queryParams) {
7374
+ if (getAgentMcpServersRoute.match(req.method, pathSegments)) {
7375
+ const parsed = await getAgentMcpServersRoute.parse(req, res, pathSegments, queryParams);
7376
+ if (!parsed)
7377
+ return true;
7847
7378
  const servers = getAgentMcpServers(parsed.params.id);
7848
7379
  const resolveSecrets = parsed.query.resolveSecrets === "true";
7849
7380
  if (resolveSecrets) {
7381
+ if (!canResolveMcpSecretsForHttpUser(req)) {
7382
+ jsonError(res, "Forbidden: admission: missing permission 'mcp-server.read.secrets'", 403);
7383
+ return true;
7384
+ }
7850
7385
  const configs = getResolvedConfig(parsed.params.id);
7851
7386
  const configMap = new Map(configs.map((c) => [c.key, c.value]));
7852
7387
  const serversWithSecrets = await Promise.all(servers.map(async (server) => {
@@ -8034,12 +7569,15 @@ async function handleMcpServers(req, res, pathSegments, queryParams) {
8034
7569
  const parsed = await deleteMcpServerRoute.parse(req, res, pathSegments, queryParams);
8035
7570
  if (!parsed)
8036
7571
  return true;
8037
- const deleted = deleteMcpServer(parsed.params.id);
8038
- if (!deleted) {
7572
+ const result = deleteMcpServer(parsed.params.id);
7573
+ if (!result.deleted) {
8039
7574
  jsonError(res, "MCP server not found", 404);
8040
7575
  return true;
8041
7576
  }
8042
- json(res, { success: true });
7577
+ json(res, {
7578
+ success: true,
7579
+ deletedScriptConnectionCount: result.deletedScriptConnectionCount
7580
+ });
8043
7581
  return true;
8044
7582
  }
8045
7583
  return false;
@@ -8060,6 +7598,33 @@ var userSendTaskInputSchema = object({
8060
7598
  model: string().trim().min(1).optional().describe("Concrete model override interpreted by the assignee's harness/provider."),
8061
7599
  modelTier: ModelTierSchema.optional().describe("Portable model tier: 'smol', 'regular', 'smart', or 'ultra'. Resolved by the assignee's harness/provider.")
8062
7600
  });
7601
+ function permission(verb) {
7602
+ return { permission: verb };
7603
+ }
7604
+ async function maybeDenyUserToolAdmission(user, toolName, config) {
7605
+ if (!isRbacEnabled())
7606
+ return;
7607
+ const grant = getUserGrant(user.id);
7608
+ if (grant.grantsAll)
7609
+ return;
7610
+ const decision = decideToolAdmission({
7611
+ rbac: config.rbac,
7612
+ readOnly: config.annotations?.readOnlyHint ?? false,
7613
+ grant
7614
+ });
7615
+ enqueueAdmissionRow({
7616
+ userId: user.id,
7617
+ decision,
7618
+ source: "mcp",
7619
+ toolName
7620
+ });
7621
+ if (decision.allow)
7622
+ return;
7623
+ return {
7624
+ isError: true,
7625
+ content: [{ type: "text", text: `Forbidden: ${decision.reason}` }]
7626
+ };
7627
+ }
8063
7628
  function createUserServer(user) {
8064
7629
  const server = new McpServer({
8065
7630
  name: `${package_default.name}-user`,
@@ -8071,44 +7636,77 @@ function createUserServer(user) {
8071
7636
  }
8072
7637
  });
8073
7638
  const registerTool = createToolRegistrar(server);
8074
- registerTool("send-task", {
7639
+ const sendTaskConfig = {
8075
7640
  title: "Send a task",
8076
7641
  annotations: { destructiveHint: false },
7642
+ rbac: permission("task.create.own"),
8077
7643
  description: "Creates an unassigned task requested by the authenticated user.",
8078
7644
  inputSchema: userSendTaskInputSchema,
8079
7645
  outputSchema: sendTaskOutputSchema
8080
- }, async (args, info, _meta) => sendTaskHandler(userCtx(user, info.sessionId), {
8081
- offerMode: false,
8082
- allowDuplicate: false,
8083
- ...args
8084
- }));
8085
- registerTool("get-tasks", {
7646
+ };
7647
+ registerTool("send-task", sendTaskConfig, async (args, info, _meta) => {
7648
+ const denied = await maybeDenyUserToolAdmission(user, "send-task", sendTaskConfig);
7649
+ if (denied)
7650
+ return denied;
7651
+ return sendTaskHandler(userCtx(user, info.sessionId), {
7652
+ offerMode: false,
7653
+ allowDuplicate: false,
7654
+ ...args
7655
+ });
7656
+ });
7657
+ const getTasksConfig = {
8086
7658
  title: "Get tasks",
8087
7659
  description: "Returns tasks requested by the authenticated user.",
8088
7660
  annotations: { readOnlyHint: true },
8089
7661
  inputSchema: getTasksInputSchema,
8090
7662
  outputSchema: getTasksOutputSchema
8091
- }, async (args, info, _meta) => getTasksHandler(userCtx(user, info.sessionId), args));
8092
- registerTool("get-task-details", {
7663
+ };
7664
+ registerTool("get-tasks", getTasksConfig, async (args, info, _meta) => {
7665
+ const denied = await maybeDenyUserToolAdmission(user, "get-tasks", getTasksConfig);
7666
+ if (denied)
7667
+ return denied;
7668
+ return getTasksHandler(userCtx(user, info.sessionId), args);
7669
+ });
7670
+ const getTaskDetailsConfig = {
8093
7671
  title: "Get task details",
8094
7672
  description: "Returns detailed information about one of your tasks.",
8095
7673
  annotations: { readOnlyHint: true },
8096
7674
  inputSchema: getTaskDetailsInputSchema,
8097
7675
  outputSchema: getTaskDetailsOutputSchema
8098
- }, async (args, info, _meta) => getTaskDetailsHandler(userCtx(user, info.sessionId), args));
8099
- registerTool("cancel-task", {
7676
+ };
7677
+ registerTool("get-task-details", getTaskDetailsConfig, async (args, info, _meta) => {
7678
+ const denied = await maybeDenyUserToolAdmission(user, "get-task-details", getTaskDetailsConfig);
7679
+ if (denied)
7680
+ return denied;
7681
+ return getTaskDetailsHandler(userCtx(user, info.sessionId), args);
7682
+ });
7683
+ const cancelTaskConfig = {
8100
7684
  title: "Cancel Task",
8101
7685
  description: "Cancel one of your pending or in-progress tasks.",
8102
7686
  annotations: { destructiveHint: true },
7687
+ rbac: permission("task.cancel.own"),
8103
7688
  inputSchema: cancelTaskInputSchema,
8104
7689
  outputSchema: cancelTaskOutputSchema
8105
- }, async (args, info, _meta) => cancelTaskHandler(userCtx(user, info.sessionId), args));
8106
- registerTool("task-action", {
7690
+ };
7691
+ registerTool("cancel-task", cancelTaskConfig, async (args, info, _meta) => {
7692
+ const denied = await maybeDenyUserToolAdmission(user, "cancel-task", cancelTaskConfig);
7693
+ if (denied)
7694
+ return denied;
7695
+ return cancelTaskHandler(userCtx(user, info.sessionId), args);
7696
+ });
7697
+ const taskActionConfig = {
8107
7698
  title: "Task Pool Action",
8108
7699
  description: "Move one of your tasks to or from backlog.",
7700
+ rbac: permission("task.action.own"),
8109
7701
  inputSchema: taskActionInputSchema,
8110
7702
  outputSchema: taskActionOutputSchema
8111
- }, async (args, info, _meta) => taskActionHandler(userCtx(user, info.sessionId), args));
7703
+ };
7704
+ registerTool("task-action", taskActionConfig, async (args, info, _meta) => {
7705
+ const denied = await maybeDenyUserToolAdmission(user, "task-action", taskActionConfig);
7706
+ if (denied)
7707
+ return denied;
7708
+ return taskActionHandler(userCtx(user, info.sessionId), args);
7709
+ });
8112
7710
  return server;
8113
7711
  }
8114
7712
 
@@ -9731,6 +9329,82 @@ async function handleMetrics(req, res, pathSegments, queryParams, myAgentId) {
9731
9329
  return false;
9732
9330
  }
9733
9331
 
9332
+ // src/http/oauth-generic.ts
9333
+ init_zod();
9334
+ var DEDICATED_CALLBACK_PROVIDERS = new Set(["linear"]);
9335
+ var genericOAuthCallbackRoute = route({
9336
+ method: "get",
9337
+ path: "/api/oauth/{provider}/callback",
9338
+ pattern: ["api", "oauth", null, "callback"],
9339
+ operationId: "oauth_generic_callback",
9340
+ summary: "Generic OAuth redirect target for script credential bindings",
9341
+ tags: ["OAuth"],
9342
+ auth: { apiKey: false },
9343
+ params: exports_external.object({
9344
+ provider: exports_external.string().min(1).max(255).regex(/^[A-Za-z0-9_-]+$/)
9345
+ }),
9346
+ query: exports_external.object({
9347
+ code: exports_external.string().optional(),
9348
+ state: exports_external.string().optional(),
9349
+ error: exports_external.string().optional(),
9350
+ error_description: exports_external.string().optional()
9351
+ }),
9352
+ responses: {
9353
+ 200: { description: "OAuth authorization completed" },
9354
+ 400: { description: "Missing or invalid OAuth callback parameters" },
9355
+ 404: { description: "OAuth app not configured" },
9356
+ 409: { description: "Provider has a dedicated OAuth callback" },
9357
+ 502: { description: "Token exchange failed" }
9358
+ }
9359
+ });
9360
+ function sendAuthorizedHtml(res, provider) {
9361
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
9362
+ res.end(`<!DOCTYPE html>
9363
+ <html>
9364
+ <head><title>OAuth Authorized</title></head>
9365
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
9366
+ <main style="text-align: center;">
9367
+ <h1>${provider} authorized</h1>
9368
+ <p>You can close this tab.</p>
9369
+ </main>
9370
+ </body>
9371
+ </html>`);
9372
+ }
9373
+ async function handleGenericOAuth(req, res, pathSegments, queryParams) {
9374
+ if (!genericOAuthCallbackRoute.match(req.method, pathSegments))
9375
+ return false;
9376
+ const parsed = await genericOAuthCallbackRoute.parse(req, res, pathSegments, queryParams);
9377
+ if (!parsed)
9378
+ return true;
9379
+ const { provider } = parsed.params;
9380
+ if (DEDICATED_CALLBACK_PROVIDERS.has(provider)) {
9381
+ jsonError(res, "Provider linear uses the dedicated OAuth flow at /api/trackers/linear/authorize and /api/trackers/linear/callback.", 409);
9382
+ return true;
9383
+ }
9384
+ if (parsed.query.error) {
9385
+ jsonError(res, parsed.query.error_description ?? parsed.query.error, 400);
9386
+ return true;
9387
+ }
9388
+ if (!parsed.query.code || !parsed.query.state) {
9389
+ jsonError(res, "Missing code or state parameter", 400);
9390
+ return true;
9391
+ }
9392
+ const config = getOAuthProviderConfig(provider);
9393
+ if (!config) {
9394
+ jsonError(res, `OAuth app ${provider} is not configured`, 404);
9395
+ return true;
9396
+ }
9397
+ try {
9398
+ await exchangeCode(config, parsed.query.code, parsed.query.state);
9399
+ sendAuthorizedHtml(res, provider);
9400
+ } catch (err) {
9401
+ const message = err instanceof Error ? err.message : String(err);
9402
+ const status = message.includes("Invalid or expired OAuth state") ? 400 : 502;
9403
+ jsonError(res, status === 400 ? "Invalid or expired OAuth state" : `Token exchange failed: ${message}`, status);
9404
+ }
9405
+ return true;
9406
+ }
9407
+
9734
9408
  // src/http/oauth-locks.ts
9735
9409
  init_zod();
9736
9410
  var MAX_LOCK_TTL_MS = 5 * 60 * 1000;
@@ -11186,6 +10860,7 @@ async function handlePoll(req, res, pathSegments, queryParams, myAgentId) {
11186
10860
  });
11187
10861
  const requestedByUser = pendingTask.requestedByUserId ? getUserById(pendingTask.requestedByUserId) : undefined;
11188
10862
  const requestedByNotes = getRequesterNotes(requestedByUser?.notes);
10863
+ const requestedByUnknownName = !requestedByUser && pendingTask.slackUserId ? renderIdentity(resolveIdentity("slack", pendingTask.slackUserId)) : undefined;
11189
10864
  return {
11190
10865
  trigger: {
11191
10866
  type: "task_assigned",
@@ -11202,6 +10877,9 @@ async function handlePoll(req, res, pathSegments, queryParams, myAgentId) {
11202
10877
  role: requestedByUser.role,
11203
10878
  notes: requestedByNotes
11204
10879
  }
10880
+ },
10881
+ ...requestedByUnknownName && {
10882
+ requestedBy: { name: requestedByUnknownName }
11205
10883
  }
11206
10884
  }
11207
10885
  };
@@ -11220,7 +10898,7 @@ async function handlePoll(req, res, pathSegments, queryParams, myAgentId) {
11220
10898
  }
11221
10899
  if (agent.isLead) {} else {
11222
10900
  if (hasCapacity(myAgentId)) {
11223
- const unassignedIds = getUnassignedTaskIds(5);
10901
+ const unassignedIds = getUnassignedTaskIdsForAgent(myAgentId, 5);
11224
10902
  if (unassignedIds.length > 0) {
11225
10903
  const candidateId = unassignedIds[0];
11226
10904
  const candidateTask = getTaskById(candidateId);
@@ -12348,182 +12026,1501 @@ async function handleSchedules(req, res, pathSegments, queryParams, myAgentId) {
12348
12026
  }
12349
12027
  return true;
12350
12028
  }
12351
- if (runScheduleNow.match(req.method, pathSegments)) {
12352
- const parsed = await runScheduleNow.parse(req, res, pathSegments, queryParams);
12029
+ if (runScheduleNow.match(req.method, pathSegments)) {
12030
+ const parsed = await runScheduleNow.parse(req, res, pathSegments, queryParams);
12031
+ if (!parsed)
12032
+ return true;
12033
+ const schedule = getScheduledTaskById(parsed.params.id);
12034
+ if (!schedule) {
12035
+ jsonError(res, "Schedule not found", 404);
12036
+ return true;
12037
+ }
12038
+ if (!schedule.enabled) {
12039
+ jsonError(res, "Schedule is disabled", 400);
12040
+ return true;
12041
+ }
12042
+ try {
12043
+ const { workflowRunIds, task } = await dispatchScheduleTarget(schedule, ["manual-run"]);
12044
+ const now = new Date().toISOString();
12045
+ if (schedule.scheduleType === "one_time") {
12046
+ updateScheduledTask(schedule.id, {
12047
+ lastRunAt: now,
12048
+ nextRunAt: null,
12049
+ enabled: false,
12050
+ lastUpdatedAt: now
12051
+ });
12052
+ } else {
12053
+ updateScheduledTask(schedule.id, {
12054
+ lastRunAt: now,
12055
+ lastUpdatedAt: now
12056
+ });
12057
+ }
12058
+ const updatedSchedule = getScheduledTaskById(parsed.params.id);
12059
+ json(res, { schedule: updatedSchedule, workflowRunIds, task });
12060
+ } catch (error) {
12061
+ jsonError(res, error instanceof Error ? error.message : "Failed to run schedule", 500);
12062
+ }
12063
+ return true;
12064
+ }
12065
+ if (getSchedule.match(req.method, pathSegments)) {
12066
+ const parsed = await getSchedule.parse(req, res, pathSegments, queryParams);
12067
+ if (!parsed)
12068
+ return true;
12069
+ const schedule = getScheduledTaskById(parsed.params.id);
12070
+ if (!schedule) {
12071
+ jsonError(res, "Schedule not found", 404);
12072
+ return true;
12073
+ }
12074
+ const userId = resolveHttpAuditUserId(req, myAgentId);
12075
+ const [decorated] = withFavoriteFlags([schedule], { userId, itemType: "schedule" });
12076
+ json(res, decorated ?? schedule);
12077
+ return true;
12078
+ }
12079
+ if (updateSchedule.match(req.method, pathSegments) || patchSchedule.match(req.method, pathSegments)) {
12080
+ const routeHandle = updateSchedule.match(req.method, pathSegments) ? updateSchedule : patchSchedule;
12081
+ const parsed = await routeHandle.parse(req, res, pathSegments, queryParams);
12082
+ if (!parsed)
12083
+ return true;
12084
+ const body = parsed.body;
12085
+ if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
12086
+ const normalizedModel = splitLegacyModelAlias({
12087
+ model: parsed.body.model,
12088
+ modelTier: parsed.body.modelTier
12089
+ });
12090
+ if (parsed.body.model !== undefined)
12091
+ body.model = normalizedModel.model ?? null;
12092
+ if (parsed.body.modelTier !== undefined || normalizedModel.modelTier) {
12093
+ body.modelTier = normalizedModel.modelTier ?? null;
12094
+ }
12095
+ }
12096
+ const existing = getScheduledTaskById(parsed.params.id);
12097
+ if (!existing) {
12098
+ jsonError(res, "Schedule not found", 404);
12099
+ return true;
12100
+ }
12101
+ if (existing.scheduleType === "one_time" && !existing.enabled && existing.lastRunAt) {
12102
+ jsonError(res, "One-time schedule has already executed. Create a new one instead.", 400);
12103
+ return true;
12104
+ }
12105
+ if (existing.scheduleType !== "one_time") {
12106
+ const timing = mergeScheduleTiming({
12107
+ cronExpression: existing.cronExpression ?? null,
12108
+ intervalMs: existing.intervalMs ?? null
12109
+ }, { cronExpression: parsed.body.cronExpression, intervalMs: parsed.body.intervalMs });
12110
+ if (validateRecurringTiming(timing)) {
12111
+ jsonError(res, "At least one of intervalMs or cronExpression must be set", 400);
12112
+ return true;
12113
+ }
12114
+ }
12115
+ if (parsed.body.cronExpression) {
12116
+ try {
12117
+ import_cron_parser.CronExpressionParser.parse(parsed.body.cronExpression);
12118
+ } catch {
12119
+ jsonError(res, "Invalid cron expression", 400);
12120
+ return true;
12121
+ }
12122
+ }
12123
+ if (parsed.body.targetAgentId) {
12124
+ const agent = getAgentById(parsed.body.targetAgentId);
12125
+ if (!agent) {
12126
+ jsonError(res, "Target agent not found", 400);
12127
+ return true;
12128
+ }
12129
+ }
12130
+ const mergedTargetType = parsed.body.targetType ?? existing.targetType;
12131
+ const mergedTaskTemplate = parsed.body.taskTemplate !== undefined ? parsed.body.taskTemplate : existing.taskTemplate;
12132
+ const mergedWorkflowId = parsed.body.workflowId !== undefined ? parsed.body.workflowId : existing.workflowId;
12133
+ const mergedScriptName = parsed.body.scriptName !== undefined ? parsed.body.scriptName : existing.scriptName;
12134
+ if (mergedTargetType === "agent-task" && !mergedTaskTemplate) {
12135
+ jsonError(res, "taskTemplate is required when targetType is 'agent-task'.", 400);
12136
+ return true;
12137
+ }
12138
+ if (mergedTargetType === "workflow") {
12139
+ if (!mergedWorkflowId) {
12140
+ jsonError(res, "workflowId is required when targetType is 'workflow'.", 400);
12141
+ return true;
12142
+ }
12143
+ if (!getWorkflow(mergedWorkflowId)) {
12144
+ jsonError(res, `Workflow not found: ${mergedWorkflowId}`, 400);
12145
+ return true;
12146
+ }
12147
+ }
12148
+ if (mergedTargetType === "script") {
12149
+ if (!mergedScriptName) {
12150
+ jsonError(res, "scriptName is required when targetType is 'script'.", 400);
12151
+ return true;
12152
+ }
12153
+ if (!getScript({ name: mergedScriptName, scope: "global" })) {
12154
+ jsonError(res, `Script not found: ${mergedScriptName}`, 400);
12155
+ return true;
12156
+ }
12157
+ }
12158
+ if (parsed.body.name && parsed.body.name !== existing.name) {
12159
+ const nameConflict = getScheduledTaskByName(parsed.body.name);
12160
+ if (nameConflict) {
12161
+ jsonError(res, "Schedule with this name already exists", 409);
12162
+ return true;
12163
+ }
12164
+ }
12165
+ const newEnabled = parsed.body.enabled !== undefined ? parsed.body.enabled : existing.enabled;
12166
+ if (existing.scheduleType === "one_time") {
12167
+ if (!newEnabled) {
12168
+ body.nextRunAt = null;
12169
+ }
12170
+ } else {
12171
+ if (!newEnabled) {
12172
+ body.nextRunAt = null;
12173
+ } else if (parsed.body.cronExpression !== undefined || parsed.body.intervalMs !== undefined || parsed.body.enabled === true && !existing.enabled) {
12174
+ const timing = mergeScheduleTiming({
12175
+ cronExpression: existing.cronExpression ?? null,
12176
+ intervalMs: existing.intervalMs ?? null
12177
+ }, { cronExpression: parsed.body.cronExpression, intervalMs: parsed.body.intervalMs });
12178
+ const mergedTimezone = parsed.body.timezone !== undefined ? parsed.body.timezone : existing.timezone;
12179
+ if (timing.mergedCron || timing.mergedInterval) {
12180
+ body.nextRunAt = calculateNextRun({
12181
+ cronExpression: timing.mergedCron,
12182
+ intervalMs: timing.mergedInterval,
12183
+ timezone: mergedTimezone
12184
+ });
12185
+ }
12186
+ }
12187
+ }
12188
+ const updatedBy = resolveHttpAuditUserId(req, myAgentId);
12189
+ if (updatedBy !== null)
12190
+ body.updatedBy = updatedBy;
12191
+ const schedule = updateScheduledTask(parsed.params.id, body);
12192
+ json(res, schedule);
12193
+ return true;
12194
+ }
12195
+ if (deleteSchedule.match(req.method, pathSegments)) {
12196
+ const parsed = await deleteSchedule.parse(req, res, pathSegments, queryParams);
12197
+ if (!parsed)
12198
+ return true;
12199
+ const deleted = deleteScheduledTask(parsed.params.id);
12200
+ if (!deleted) {
12201
+ jsonError(res, "Schedule not found", 404);
12202
+ return true;
12203
+ }
12204
+ json(res, { success: true });
12205
+ return true;
12206
+ }
12207
+ return false;
12208
+ }
12209
+
12210
+ // src/http/script-connection-proxy.ts
12211
+ init_zod();
12212
+ init_db();
12213
+ var mcpCallRoute = route({
12214
+ method: "post",
12215
+ path: "/api/script-connections/{id}/mcp-call",
12216
+ pattern: ["api", "script-connections", null, "mcp-call"],
12217
+ summary: "Invoke a tool on an MCP script connection",
12218
+ tags: ["Script Connections"],
12219
+ auth: { apiKey: true, agentId: true },
12220
+ rbac: { permission: "script-connection.invoke" },
12221
+ params: exports_external.object({ id: exports_external.string().uuid() }),
12222
+ body: exports_external.object({
12223
+ tool: exports_external.string().min(1),
12224
+ arguments: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
12225
+ }),
12226
+ responses: {
12227
+ 200: { description: "MCP call result" },
12228
+ 400: { description: "Invalid MCP connection or request" },
12229
+ 403: { description: "Not allowed to invoke this MCP connection" },
12230
+ 404: { description: "Script connection or agent not found" }
12231
+ }
12232
+ });
12233
+ function connectionScopeMatches(connection, agentId, syntheticPrincipal) {
12234
+ if (connection.scope === "global")
12235
+ return true;
12236
+ if (connection.scope === "agent")
12237
+ return !syntheticPrincipal && connection.scopeId === agentId;
12238
+ if (connection.scope === "repo")
12239
+ return true;
12240
+ return false;
12241
+ }
12242
+ function isSyntheticScriptPrincipal(agentId) {
12243
+ return agentId === "schedule" || agentId === "workflow";
12244
+ }
12245
+ async function handleScriptConnectionProxy(req, res, pathSegments, queryParams, agentId) {
12246
+ if (!mcpCallRoute.match(req.method, pathSegments))
12247
+ return false;
12248
+ const parsed = await mcpCallRoute.parse(req, res, pathSegments, queryParams);
12249
+ if (!parsed)
12250
+ return true;
12251
+ if (!agentId) {
12252
+ jsonError(res, "X-Agent-ID required for script connection MCP proxy", 400);
12253
+ return true;
12254
+ }
12255
+ const syntheticPrincipal = isSyntheticScriptPrincipal(agentId);
12256
+ const agent = syntheticPrincipal ? null : getAgentById(agentId);
12257
+ if (!agent && !syntheticPrincipal) {
12258
+ jsonError(res, "Agent not found", 404);
12259
+ return true;
12260
+ }
12261
+ const decision = can({
12262
+ principal: { kind: "agent", agentId, isLead: agent?.isLead ?? false },
12263
+ verb: "script-connection.invoke",
12264
+ resource: { kind: "none" },
12265
+ source: "http"
12266
+ });
12267
+ if (!decision.allow) {
12268
+ jsonError(res, decision.reason, 403);
12269
+ return true;
12270
+ }
12271
+ const connection = getScriptConnectionById(parsed.params.id);
12272
+ if (!connection) {
12273
+ jsonError(res, "Script connection not found", 404);
12274
+ return true;
12275
+ }
12276
+ if (!connection.enabled) {
12277
+ jsonError(res, "Script connection is disabled", 403);
12278
+ return true;
12279
+ }
12280
+ if (connection.kind !== "mcp") {
12281
+ jsonError(res, "Script connection is not an MCP connection", 400);
12282
+ return true;
12283
+ }
12284
+ if (!connectionScopeMatches(connection, agentId, syntheticPrincipal)) {
12285
+ jsonError(res, "Script connection is not available to this agent", 403);
12286
+ return true;
12287
+ }
12288
+ if (!connection.mcpServerId) {
12289
+ jsonError(res, "Script MCP connection is missing mcpServerId", 400);
12290
+ return true;
12291
+ }
12292
+ try {
12293
+ const result = await callMcpServerTool(connection.mcpServerId, parsed.body.tool, parsed.body.arguments ?? {}, { agentId, timeoutMs: 30000 });
12294
+ json(res, result);
12295
+ } catch (err) {
12296
+ json(res, { ok: false, error: err instanceof Error ? err.message : String(err) });
12297
+ }
12298
+ return true;
12299
+ }
12300
+
12301
+ // src/http/script-connections.ts
12302
+ init_zod();
12303
+ init_date_utils();
12304
+ init_db();
12305
+ init_request_auth_context();
12306
+ init_secret_scrubber();
12307
+ var providerSchema = exports_external.string().min(1).max(255).regex(/^[A-Za-z0-9_-]+$/);
12308
+ var scopeSchema = exports_external.enum(["global", "agent", "repo"]);
12309
+ var connectionKindSchema = exports_external.enum(["openapi", "graphql", "mcp"]);
12310
+ var idParamsSchema = exports_external.object({ id: exports_external.string().uuid() });
12311
+ var providerParamsSchema = exports_external.object({ provider: providerSchema });
12312
+ var listConnectionsQuerySchema = exports_external.object({
12313
+ kind: connectionKindSchema.optional(),
12314
+ scope: scopeSchema.optional(),
12315
+ scopeId: exports_external.string().optional()
12316
+ });
12317
+ var connectionBaseBodySchema = exports_external.object({
12318
+ id: exports_external.string().uuid().optional(),
12319
+ slug: exports_external.string().min(1).max(80),
12320
+ displayName: exports_external.string().max(160).optional(),
12321
+ scope: scopeSchema.optional(),
12322
+ scopeId: scopedResourceScopeIdSchema.nullable().optional(),
12323
+ allowedHosts: exports_external.array(exports_external.string().min(1)).optional(),
12324
+ credentialBindingId: exports_external.string().uuid().nullable().optional(),
12325
+ configKey: exports_external.string().min(1).max(255).optional(),
12326
+ headerTemplate: exports_external.string().min(1).optional(),
12327
+ queryTemplate: exports_external.string().min(1).optional(),
12328
+ authKind: exports_external.enum(["config", "oauth"]).optional(),
12329
+ oauthProvider: providerSchema.optional(),
12330
+ enabled: exports_external.boolean().optional()
12331
+ });
12332
+ var upsertConnectionBodySchema = exports_external.discriminatedUnion("kind", [
12333
+ connectionBaseBodySchema.extend({
12334
+ kind: exports_external.literal("openapi"),
12335
+ baseUrl: exports_external.string().url(),
12336
+ openapiSpecUrl: exports_external.string().url().optional(),
12337
+ openapiSpecJson: exports_external.string().optional()
12338
+ }),
12339
+ connectionBaseBodySchema.extend({
12340
+ kind: exports_external.literal("graphql"),
12341
+ baseUrl: exports_external.string().url(),
12342
+ allowedHosts: exports_external.array(exports_external.string().min(1)).min(1)
12343
+ }),
12344
+ connectionBaseBodySchema.extend({
12345
+ kind: exports_external.literal("mcp"),
12346
+ mcpServerId: exports_external.string().uuid()
12347
+ })
12348
+ ]);
12349
+ var disableConnectionBodySchema = exports_external.object({ enabled: exports_external.boolean() });
12350
+ var credentialBindingBodySchema = exports_external.object({
12351
+ id: exports_external.string().uuid().optional(),
12352
+ configKey: exports_external.string().min(1).max(255),
12353
+ allowedHosts: exports_external.array(exports_external.string().min(1)).min(1),
12354
+ headerTemplate: exports_external.string().min(1).optional(),
12355
+ queryTemplate: exports_external.string().min(1).optional(),
12356
+ scope: scopeSchema.default("global").optional(),
12357
+ scopeId: scopedResourceScopeIdSchema.nullable().optional(),
12358
+ active: exports_external.boolean().default(true).optional(),
12359
+ authKind: exports_external.enum(["config", "oauth"]).default("config").optional(),
12360
+ oauthProvider: providerSchema.optional()
12361
+ });
12362
+ var oauthAppBodySchema = exports_external.object({
12363
+ provider: providerSchema,
12364
+ clientId: exports_external.string().min(1),
12365
+ clientSecret: exports_external.string().min(1).optional(),
12366
+ authorizeUrl: exports_external.string().url(),
12367
+ tokenUrl: exports_external.string().url(),
12368
+ scopes: exports_external.array(exports_external.string().min(1)).default([]).optional(),
12369
+ extraParams: exports_external.record(exports_external.string(), exports_external.string()).optional(),
12370
+ tokenAuthStyle: exports_external.enum(["body", "basic"]).optional(),
12371
+ tokenBodyFormat: exports_external.enum(["form", "json"]).optional()
12372
+ });
12373
+ var discoverOAuthAppBodySchema = exports_external.object({
12374
+ url: exports_external.string().url()
12375
+ });
12376
+ var listConnectionsRoute = route({
12377
+ method: "get",
12378
+ path: "/api/script-connections",
12379
+ pattern: ["api", "script-connections"],
12380
+ operationId: "script_connections_list",
12381
+ summary: "List script connections",
12382
+ description: "Dashboard read of OpenAPI, GraphQL, and MCP script connections with credential summaries.",
12383
+ tags: ["Script Connections"],
12384
+ query: listConnectionsQuerySchema,
12385
+ responses: {
12386
+ 200: { description: "Script connections" },
12387
+ 400: { description: "Validation error" }
12388
+ }
12389
+ });
12390
+ var getConnectionRoute = route({
12391
+ method: "get",
12392
+ path: "/api/script-connections/{id}",
12393
+ pattern: ["api", "script-connections", null],
12394
+ operationId: "script_connections_get",
12395
+ summary: "Get script connection detail",
12396
+ tags: ["Script Connections"],
12397
+ params: idParamsSchema,
12398
+ responses: {
12399
+ 200: { description: "Script connection detail" },
12400
+ 404: { description: "Script connection not found" }
12401
+ }
12402
+ });
12403
+ var upsertConnectionRoute = route({
12404
+ method: "post",
12405
+ path: "/api/script-connections",
12406
+ pattern: ["api", "script-connections"],
12407
+ operationId: "script_connections_upsert",
12408
+ summary: "Create or update a script connection",
12409
+ tags: ["Script Connections"],
12410
+ body: upsertConnectionBodySchema,
12411
+ responses: {
12412
+ 200: { description: "Saved script connection" },
12413
+ 400: { description: "Validation or generation error" },
12414
+ 403: { description: "Only the lead agent can manage script connections" }
12415
+ },
12416
+ rbac: { permission: "script-connection.manage" }
12417
+ });
12418
+ var refreshConnectionRoute = route({
12419
+ method: "post",
12420
+ path: "/api/script-connections/{id}/refresh",
12421
+ pattern: ["api", "script-connections", null, "refresh"],
12422
+ operationId: "script_connections_refresh",
12423
+ summary: "Refresh a script connection",
12424
+ tags: ["Script Connections"],
12425
+ params: idParamsSchema,
12426
+ responses: {
12427
+ 200: { description: "Refreshed script connection" },
12428
+ 400: { description: "Connection cannot be refreshed" },
12429
+ 403: { description: "Only the lead agent can manage script connections" },
12430
+ 404: { description: "Script connection not found" }
12431
+ },
12432
+ rbac: { permission: "script-connection.manage" }
12433
+ });
12434
+ var setConnectionEnabledRoute = route({
12435
+ method: "post",
12436
+ path: "/api/script-connections/{id}/disable",
12437
+ pattern: ["api", "script-connections", null, "disable"],
12438
+ operationId: "script_connections_set_enabled",
12439
+ summary: "Enable or disable a script connection",
12440
+ tags: ["Script Connections"],
12441
+ params: idParamsSchema,
12442
+ body: disableConnectionBodySchema,
12443
+ responses: {
12444
+ 200: { description: "Updated script connection" },
12445
+ 403: { description: "Only the lead agent can manage script connections" },
12446
+ 404: { description: "Script connection not found" }
12447
+ },
12448
+ rbac: { permission: "script-connection.manage" }
12449
+ });
12450
+ var listCredentialBindingsRoute = route({
12451
+ method: "get",
12452
+ path: "/api/credential-bindings",
12453
+ pattern: ["api", "credential-bindings"],
12454
+ operationId: "credential_bindings_list",
12455
+ summary: "List script credential bindings",
12456
+ tags: ["Script Connections"],
12457
+ responses: {
12458
+ 200: { description: "Credential bindings" }
12459
+ }
12460
+ });
12461
+ var upsertCredentialBindingRoute = route({
12462
+ method: "post",
12463
+ path: "/api/credential-bindings",
12464
+ pattern: ["api", "credential-bindings"],
12465
+ operationId: "credential_bindings_upsert",
12466
+ summary: "Create or update a script credential binding",
12467
+ tags: ["Script Connections"],
12468
+ body: credentialBindingBodySchema,
12469
+ responses: {
12470
+ 200: { description: "Saved credential binding" },
12471
+ 400: { description: "Validation error" },
12472
+ 403: { description: "Only the lead agent can manage script connections" }
12473
+ },
12474
+ rbac: { permission: "script-connection.manage" }
12475
+ });
12476
+ var listOAuthAppsRoute = route({
12477
+ method: "get",
12478
+ path: "/api/oauth-apps",
12479
+ pattern: ["api", "oauth-apps"],
12480
+ operationId: "oauth_apps_list",
12481
+ summary: "List OAuth apps for script credential bindings",
12482
+ tags: ["Script Connections"],
12483
+ responses: {
12484
+ 200: { description: "OAuth apps without client secrets" }
12485
+ }
12486
+ });
12487
+ var upsertOAuthAppRoute = route({
12488
+ method: "post",
12489
+ path: "/api/oauth-apps",
12490
+ pattern: ["api", "oauth-apps"],
12491
+ operationId: "oauth_apps_upsert",
12492
+ summary: "Create or update an OAuth app for script credential bindings",
12493
+ tags: ["Script Connections"],
12494
+ body: oauthAppBodySchema,
12495
+ responses: {
12496
+ 200: { description: "Saved OAuth app without client secret" },
12497
+ 400: { description: "Validation error" },
12498
+ 403: { description: "Only the lead agent can manage script connections" }
12499
+ },
12500
+ rbac: { permission: "script-connection.manage" }
12501
+ });
12502
+ var discoverOAuthAppRoute = route({
12503
+ method: "post",
12504
+ path: "/api/oauth-apps/discover",
12505
+ pattern: ["api", "oauth-apps", "discover"],
12506
+ operationId: "oauth_apps_discover",
12507
+ summary: "Discover OAuth endpoints from provider metadata",
12508
+ tags: ["Script Connections"],
12509
+ body: discoverOAuthAppBodySchema,
12510
+ responses: {
12511
+ 200: { description: "Discovered OAuth metadata" },
12512
+ 400: { description: "Discovery failed" },
12513
+ 403: { description: "Only the lead agent can manage script connections" }
12514
+ },
12515
+ rbac: { permission: "script-connection.manage" }
12516
+ });
12517
+ var deleteOAuthAppRoute = route({
12518
+ method: "delete",
12519
+ path: "/api/oauth-apps/{provider}",
12520
+ pattern: ["api", "oauth-apps", null],
12521
+ operationId: "oauth_apps_delete",
12522
+ summary: "Delete an OAuth app and its tokens",
12523
+ tags: ["Script Connections"],
12524
+ params: providerParamsSchema,
12525
+ responses: {
12526
+ 200: { description: "OAuth app deleted" },
12527
+ 403: { description: "Only the lead agent can manage script connections" },
12528
+ 404: { description: "OAuth app not found" }
12529
+ },
12530
+ rbac: { permission: "script-connection.manage" }
12531
+ });
12532
+ var authorizeUrlRoute2 = route({
12533
+ method: "post",
12534
+ path: "/api/oauth-apps/{provider}/authorize-url",
12535
+ pattern: ["api", "oauth-apps", null, "authorize-url"],
12536
+ operationId: "oauth_apps_authorize_url",
12537
+ summary: "Build an OAuth authorization URL",
12538
+ tags: ["Script Connections"],
12539
+ params: providerParamsSchema,
12540
+ responses: {
12541
+ 200: { description: "OAuth authorization URL" },
12542
+ 403: { description: "Only the lead agent can manage script connections" },
12543
+ 404: { description: "OAuth app not found" }
12544
+ },
12545
+ rbac: { permission: "script-connection.manage" }
12546
+ });
12547
+ var integrationsCatalogRoute = route({
12548
+ method: "get",
12549
+ path: "/api/integrations-catalog",
12550
+ pattern: ["api", "integrations-catalog"],
12551
+ operationId: "integrations_catalog_list",
12552
+ summary: "Proxy integrations.sh catalog entries",
12553
+ tags: ["Script Connections"],
12554
+ responses: {
12555
+ 200: { description: "Integrations catalog entries" },
12556
+ 502: { description: "Catalog upstream unavailable" }
12557
+ }
12558
+ });
12559
+ var surfaceDomainSchema = exports_external.string().min(1).max(255).regex(/^[a-z0-9.-]+$/i);
12560
+ var integrationsSurfaceRoute = route({
12561
+ method: "get",
12562
+ path: "/api/integrations-catalog/{domain}/surface",
12563
+ pattern: ["api", "integrations-catalog", null, "surface"],
12564
+ operationId: "integrations_catalog_surface",
12565
+ summary: "Proxy integrations.sh per-domain surface details (trimmed for the Add Connection flow)",
12566
+ tags: ["Script Connections"],
12567
+ params: exports_external.object({ domain: surfaceDomainSchema }),
12568
+ responses: {
12569
+ 200: { description: "Trimmed integration surface details for a domain" },
12570
+ 404: { description: "No surface data for this domain" },
12571
+ 502: { description: "Surface upstream unavailable" }
12572
+ }
12573
+ });
12574
+ var disconnectOAuthAppRoute = route({
12575
+ method: "delete",
12576
+ path: "/api/oauth-apps/{provider}/tokens",
12577
+ pattern: ["api", "oauth-apps", null, "tokens"],
12578
+ operationId: "oauth_app_disconnect",
12579
+ summary: "Disconnect an OAuth app: delete stored tokens (best-effort remote revocation when a revocation endpoint is known)",
12580
+ tags: ["Script Connections"],
12581
+ params: providerParamsSchema,
12582
+ responses: {
12583
+ 200: { description: "Disconnect result" },
12584
+ 403: { description: "Only the lead agent can manage script connections" },
12585
+ 404: { description: "OAuth app not found" }
12586
+ },
12587
+ rbac: { permission: "script-connection.manage" }
12588
+ });
12589
+ var refreshOAuthAppTokensRoute = route({
12590
+ method: "post",
12591
+ path: "/api/oauth-apps/{provider}/refresh",
12592
+ pattern: ["api", "oauth-apps", null, "refresh"],
12593
+ operationId: "oauth_app_refresh_tokens",
12594
+ summary: "Force-refresh the stored OAuth tokens for a provider (never returns token values)",
12595
+ tags: ["Script Connections"],
12596
+ params: providerParamsSchema,
12597
+ responses: {
12598
+ 200: { description: "Refresh result with token status and new expiry" },
12599
+ 400: { description: "No stored tokens or provider does not support refresh" },
12600
+ 403: { description: "Only the lead agent can manage script connections" },
12601
+ 404: { description: "OAuth app not found" },
12602
+ 502: { description: "Provider token endpoint rejected the refresh" }
12603
+ },
12604
+ rbac: { permission: "script-connection.manage" }
12605
+ });
12606
+ var DISCOVERY_TIMEOUT_MS = 1e4;
12607
+ var INTEGRATIONS_CATALOG_TIMEOUT_MS = 15000;
12608
+ var INTEGRATIONS_CATALOG_TTL_MS = 60 * 60 * 1000;
12609
+ var SPEC_PREVIEW_MAX_BYTES = 50 * 1024;
12610
+ var integrationsCatalogCache = null;
12611
+ var INTEGRATIONS_SURFACE_CACHE_MAX_ENTRIES = 200;
12612
+ var integrationsSurfaceCache = new Map;
12613
+
12614
+ class SurfaceNotFoundError extends Error {
12615
+ }
12616
+ function singleHeader4(req, name) {
12617
+ const raw = req.headers[name];
12618
+ return Array.isArray(raw) ? raw[0] : raw;
12619
+ }
12620
+ function ensureConnectionAdmin(req, res, agentId) {
12621
+ const auth = getRequestAuth(req);
12622
+ if (auth?.kind === "operator" || auth?.kind === "user")
12623
+ return true;
12624
+ const callerAgentId = agentId ?? singleHeader4(req, "x-agent-id");
12625
+ const agent = callerAgentId ? getAgentById(callerAgentId) : undefined;
12626
+ const decision = can({
12627
+ principal: {
12628
+ kind: "agent",
12629
+ agentId: callerAgentId ?? "",
12630
+ isLead: agent?.isLead ?? false
12631
+ },
12632
+ verb: "script-connection.manage",
12633
+ resource: { kind: "none" },
12634
+ source: "http"
12635
+ });
12636
+ if (!decision.allow) {
12637
+ jsonError(res, "Only the lead can manage script connections.", 403);
12638
+ return false;
12639
+ }
12640
+ return true;
12641
+ }
12642
+ function tokenStatusForBinding(binding) {
12643
+ return binding.authKind === "oauth" && binding.oauthProvider ? getOAuthBindingTokenStatus(binding.oauthProvider) : undefined;
12644
+ }
12645
+ function decorateBinding(binding) {
12646
+ const tokenStatus = tokenStatusForBinding(binding);
12647
+ return tokenStatus ? { ...binding, tokenStatus } : binding;
12648
+ }
12649
+ function bindingSummary(binding) {
12650
+ if (!binding)
12651
+ return null;
12652
+ const tokenStatus = tokenStatusForBinding(binding);
12653
+ return {
12654
+ id: binding.id,
12655
+ configKey: binding.configKey,
12656
+ authKind: binding.authKind ?? "config",
12657
+ ...binding.oauthProvider ? { oauthProvider: binding.oauthProvider } : {},
12658
+ ...tokenStatus ? { tokenStatus } : {}
12659
+ };
12660
+ }
12661
+ function runtimeCounts(connection) {
12662
+ if (!connection.generatedRuntimeJson) {
12663
+ return { operationCount: 0, toolCount: 0 };
12664
+ }
12665
+ try {
12666
+ const runtime = JSON.parse(connection.generatedRuntimeJson);
12667
+ const operationCount = Array.isArray(runtime.operations) ? runtime.operations.length : connection.kind === "graphql" ? 1 : 0;
12668
+ const toolCount = Array.isArray(runtime.tools) ? runtime.tools.length : 0;
12669
+ return { operationCount, toolCount };
12670
+ } catch {
12671
+ return { operationCount: 0, toolCount: 0 };
12672
+ }
12673
+ }
12674
+ function parseRecord(value) {
12675
+ if (!value)
12676
+ return null;
12677
+ try {
12678
+ const parsed = JSON.parse(value);
12679
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
12680
+ } catch {
12681
+ return null;
12682
+ }
12683
+ }
12684
+ function connectionDetail(connection) {
12685
+ const decorated = decorateConnections([connection])[0];
12686
+ if (!decorated)
12687
+ throw new Error("Failed to decorate connection detail.");
12688
+ const runtime = parseRecord(connection.generatedRuntimeJson);
12689
+ const operations = Array.isArray(runtime?.operations) ? runtime.operations.filter((operation) => {
12690
+ return operation !== null && typeof operation === "object" && !Array.isArray(operation);
12691
+ }).map((operation) => ({
12692
+ name: String(operation.name ?? ""),
12693
+ method: String(operation.method ?? ""),
12694
+ path: String(operation.path ?? ""),
12695
+ ...Array.isArray(operation.parameters) ? {
12696
+ parameters: operation.parameters.filter((param) => {
12697
+ return param !== null && typeof param === "object" && !Array.isArray(param);
12698
+ }).map((param) => ({
12699
+ name: String(param.name ?? ""),
12700
+ in: String(param.in ?? "query"),
12701
+ required: param.required === true,
12702
+ ...param.schema !== undefined ? { schema: param.schema } : {}
12703
+ })).filter((param) => param.name)
12704
+ } : {},
12705
+ ...typeof operation.hasBody === "boolean" ? { hasBody: operation.hasBody } : {},
12706
+ ...typeof operation.successStatus === "string" ? { successStatus: operation.successStatus } : {},
12707
+ ...operation.requestBodySchema !== undefined ? { requestBodySchema: operation.requestBodySchema } : {},
12708
+ ...operation.responseSchema !== undefined ? { responseSchema: operation.responseSchema } : {}
12709
+ })).filter((operation) => operation.name && operation.method && operation.path) : [];
12710
+ const tools = Array.isArray(runtime?.tools) ? runtime.tools.filter((tool) => {
12711
+ return tool !== null && typeof tool === "object" && !Array.isArray(tool);
12712
+ }).map((tool) => ({
12713
+ name: String(tool.name ?? ""),
12714
+ ...typeof tool.description === "string" ? { description: tool.description } : {},
12715
+ ...tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}
12716
+ })).filter((tool) => tool.name) : [];
12717
+ const detail = {
12718
+ ...decorated,
12719
+ operations,
12720
+ tools,
12721
+ graphql: connection.kind === "graphql",
12722
+ generatedTypes: connection.generatedTypes ?? ""
12723
+ };
12724
+ if (connection.kind === "openapi" && connection.openapiSpecJson) {
12725
+ try {
12726
+ const spec = JSON.parse(connection.openapiSpecJson);
12727
+ const info = spec.info && typeof spec.info === "object" && !Array.isArray(spec.info) ? spec.info : {};
12728
+ const paths = spec.paths && typeof spec.paths === "object" && !Array.isArray(spec.paths) ? spec.paths : {};
12729
+ const pretty = JSON.stringify(spec, null, 2);
12730
+ const truncated = pretty.length > SPEC_PREVIEW_MAX_BYTES;
12731
+ detail.specSummary = {
12732
+ ...typeof info.title === "string" ? { title: info.title } : {},
12733
+ ...typeof info.version === "string" ? { version: info.version } : {},
12734
+ pathCount: Object.keys(paths).length
12735
+ };
12736
+ detail.specPreview = {
12737
+ json: truncated ? pretty.slice(0, SPEC_PREVIEW_MAX_BYTES) : pretty,
12738
+ truncated
12739
+ };
12740
+ } catch {
12741
+ detail.specPreview = {
12742
+ json: connection.openapiSpecJson.slice(0, SPEC_PREVIEW_MAX_BYTES),
12743
+ truncated: connection.openapiSpecJson.length > SPEC_PREVIEW_MAX_BYTES
12744
+ };
12745
+ }
12746
+ }
12747
+ return detail;
12748
+ }
12749
+ function decorateConnections(connections) {
12750
+ const bindings = new Map(listRelationalCredentialBindings({ includeInactive: true }).map((binding) => [
12751
+ binding.id,
12752
+ binding
12753
+ ]));
12754
+ return connections.map((connection) => {
12755
+ const {
12756
+ openapiSpecJson: _openapiSpecJson,
12757
+ generatedTypes: _generatedTypes,
12758
+ generatedRuntimeJson: _generatedRuntimeJson,
12759
+ ...safeConnection
12760
+ } = connection;
12761
+ return {
12762
+ ...safeConnection,
12763
+ ...runtimeCounts(connection),
12764
+ credentialBinding: bindingSummary(connection.credentialBindingId ? bindings.get(connection.credentialBindingId) : undefined)
12765
+ };
12766
+ });
12767
+ }
12768
+ function listConnections(query) {
12769
+ const connections = listScriptConnections({
12770
+ includeDisabled: true,
12771
+ allScopes: true,
12772
+ kind: query.kind
12773
+ }).filter((connection) => {
12774
+ if (query.scope && connection.scope !== query.scope)
12775
+ return false;
12776
+ if (query.scopeId && connection.scopeId !== query.scopeId)
12777
+ return false;
12778
+ return true;
12779
+ });
12780
+ return decorateConnections(connections);
12781
+ }
12782
+ function connectionScopeId(scope, scopeId, subject = "connections") {
12783
+ return resolveScopedResourceId(scope, scopeId, subject);
12784
+ }
12785
+ function validateCredentialTemplate(input) {
12786
+ if (input.requireTemplate && !input.headerTemplate && !input.queryTemplate) {
12787
+ throw new Error("At least one of headerTemplate or queryTemplate is required.");
12788
+ }
12789
+ const placeholder = placeholderForConfigKey(input.configKey);
12790
+ if (input.headerTemplate && !input.headerTemplate.includes(placeholder)) {
12791
+ throw new Error(`headerTemplate must include ${placeholder}.`);
12792
+ }
12793
+ if (input.queryTemplate && !input.queryTemplate.includes(placeholder)) {
12794
+ throw new Error(`queryTemplate must include ${placeholder}.`);
12795
+ }
12796
+ }
12797
+ function maybeCreateInlineBinding(data, resolvedScope, resolvedScopeId) {
12798
+ if (data.credentialBindingId || !data.configKey)
12799
+ return data.credentialBindingId ?? null;
12800
+ const scope = resolvedScope ?? data.scope ?? "global";
12801
+ const scopeId = resolvedScopeId !== undefined ? resolvedScopeId : connectionScopeId(scope, data.scopeId, "bindings");
12802
+ const allowedHosts = data.allowedHosts ?? ("baseUrl" in data ? [new URL(data.baseUrl).hostname] : []);
12803
+ const authKind = data.authKind ?? "config";
12804
+ const placeholder = placeholderForConfigKey(data.configKey);
12805
+ const headerTemplate = data.headerTemplate ?? (data.queryTemplate ? undefined : `Authorization: Bearer ${placeholder}`);
12806
+ validateCredentialTemplate({
12807
+ configKey: data.configKey,
12808
+ headerTemplate,
12809
+ queryTemplate: data.queryTemplate
12810
+ });
12811
+ if (authKind === "oauth" && !data.oauthProvider) {
12812
+ throw new Error("oauthProvider is required for oauth credential bindings.");
12813
+ }
12814
+ return upsertCredentialBinding({
12815
+ configKey: data.configKey,
12816
+ allowedHosts,
12817
+ headerTemplate,
12818
+ queryTemplate: data.queryTemplate,
12819
+ scope,
12820
+ scopeId,
12821
+ active: true,
12822
+ authKind,
12823
+ oauthProvider: data.oauthProvider ?? null
12824
+ }).id;
12825
+ }
12826
+ function parseMetadata(metadata) {
12827
+ try {
12828
+ const parsed = JSON.parse(metadata);
12829
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
12830
+ } catch {
12831
+ return {};
12832
+ }
12833
+ }
12834
+ function parseScopes(scopes) {
12835
+ return scopes.split(",").map((scope) => scope.trim()).filter(Boolean);
12836
+ }
12837
+ function sanitizeOAuthApp(row) {
12838
+ const metadata = parseMetadata(row.metadata);
12839
+ const extraParams = metadata.extraParams && typeof metadata.extraParams === "object" && !Array.isArray(metadata.extraParams) ? Object.fromEntries(Object.entries(metadata.extraParams).filter((entry) => typeof entry[1] === "string")) : undefined;
12840
+ return {
12841
+ id: row.id,
12842
+ provider: row.provider,
12843
+ clientId: row.clientId,
12844
+ authorizeUrl: row.authorizeUrl,
12845
+ tokenUrl: row.tokenUrl,
12846
+ redirectUri: row.redirectUri,
12847
+ scopes: parseScopes(row.scopes),
12848
+ extraParams,
12849
+ tokenAuthStyle: metadata.tokenAuthStyle === "basic" ? "basic" : "body",
12850
+ tokenBodyFormat: metadata.tokenBodyFormat === "json" ? "json" : "form",
12851
+ tokenStatus: getOAuthBindingTokenStatus(row.provider),
12852
+ expiresAt: row.tokenExpiresAt,
12853
+ lastRefreshedAt: normalizeDate(row.tokenUpdatedAt),
12854
+ createdAt: row.createdAt,
12855
+ updatedAt: row.updatedAt
12856
+ };
12857
+ }
12858
+ function listOAuthApps() {
12859
+ const rows = getDb().prepare(`SELECT a.id, a.provider, a.clientId, a.authorizeUrl, a.tokenUrl, a.redirectUri,
12860
+ a.scopes, a.metadata, t.expiresAt AS tokenExpiresAt,
12861
+ t.updatedAt AS tokenUpdatedAt, a.createdAt, a.updatedAt
12862
+ FROM oauth_apps a
12863
+ LEFT JOIN oauth_tokens t ON t.provider = a.provider
12864
+ ORDER BY a.provider ASC`).all();
12865
+ return rows.map(sanitizeOAuthApp);
12866
+ }
12867
+ async function attemptRemoteRevocation(app, accessToken) {
12868
+ const metadata = parseMetadata(app.metadata);
12869
+ const revocationUrl = typeof metadata.revocationUrl === "string" ? metadata.revocationUrl : undefined;
12870
+ if (!revocationUrl)
12871
+ return false;
12872
+ const body = new URLSearchParams({
12873
+ token: accessToken,
12874
+ token_type_hint: "access_token"
12875
+ });
12876
+ const headers2 = {
12877
+ "content-type": "application/x-www-form-urlencoded"
12878
+ };
12879
+ if (metadata.tokenAuthStyle === "basic") {
12880
+ headers2.authorization = `Basic ${Buffer.from(`${app.clientId}:${app.clientSecret}`).toString("base64")}`;
12881
+ } else {
12882
+ body.set("client_id", app.clientId);
12883
+ body.set("client_secret", app.clientSecret);
12884
+ }
12885
+ const controller = new AbortController;
12886
+ const timeout = setTimeout(() => controller.abort(), 5000);
12887
+ try {
12888
+ await fetch(revocationUrl, {
12889
+ method: "POST",
12890
+ headers: headers2,
12891
+ body: body.toString(),
12892
+ signal: controller.signal
12893
+ });
12894
+ } catch (err) {
12895
+ console.warn(scrubSecrets(`OAuth token revocation request failed for provider ${app.provider}: ${err instanceof Error ? err.message : String(err)}`));
12896
+ } finally {
12897
+ clearTimeout(timeout);
12898
+ }
12899
+ return true;
12900
+ }
12901
+ function genericOAuthRedirectUri(provider) {
12902
+ return `${getPublicMcpBaseUrl()}/api/oauth/${encodeURIComponent(provider)}/callback`;
12903
+ }
12904
+ function oauthDiscoveryUrls(inputUrl) {
12905
+ const parsed = new URL(inputUrl);
12906
+ const pathname = parsed.pathname.replace(/\/+$/, "");
12907
+ const base = `${parsed.origin}${pathname === "/" ? "" : pathname}`;
12908
+ return [
12909
+ `${base}/.well-known/oauth-authorization-server`,
12910
+ `${base}/.well-known/openid-configuration`,
12911
+ parsed.toString()
12912
+ ].filter((url, index, urls) => urls.indexOf(url) === index);
12913
+ }
12914
+ async function fetchJsonMetadata(url, signal) {
12915
+ let current = assertUrlSafe(url, publicEndpointSsrfOptions());
12916
+ let response = null;
12917
+ for (let hop = 0;hop <= 5; hop += 1) {
12918
+ response = await fetch(current, {
12919
+ headers: { accept: "application/json" },
12920
+ signal,
12921
+ redirect: "manual"
12922
+ });
12923
+ if (response.status < 300 || response.status >= 400 || response.status === 304)
12924
+ break;
12925
+ const location = response.headers.get("location");
12926
+ if (!location) {
12927
+ throw new Error(`HTTP ${response.status} redirect missing Location header`);
12928
+ }
12929
+ current = assertUrlSafe(new URL(location, current).toString(), publicEndpointSsrfOptions());
12930
+ if (hop === 5)
12931
+ throw new Error("OAuth discovery exceeded 5 redirects.");
12932
+ }
12933
+ if (!response)
12934
+ throw new Error("OAuth discovery failed before receiving a response.");
12935
+ if (!response.ok) {
12936
+ throw new Error(`HTTP ${response.status}`);
12937
+ }
12938
+ const text = await response.text();
12939
+ try {
12940
+ return JSON.parse(text);
12941
+ } catch {
12942
+ throw new Error("Response was not JSON.");
12943
+ }
12944
+ }
12945
+ function extractOAuthDiscovery(metadata) {
12946
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
12947
+ return null;
12948
+ const record = metadata;
12949
+ if (typeof record.authorization_endpoint !== "string" || typeof record.token_endpoint !== "string") {
12950
+ return null;
12951
+ }
12952
+ const scopes = Array.isArray(record.scopes_supported) ? record.scopes_supported.filter((scope) => typeof scope === "string") : [];
12953
+ return {
12954
+ authorizeUrl: record.authorization_endpoint,
12955
+ tokenUrl: record.token_endpoint,
12956
+ scopes
12957
+ };
12958
+ }
12959
+ async function discoverOAuthApp(url) {
12960
+ const controller = new AbortController;
12961
+ const timeout = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);
12962
+ const failures = [];
12963
+ try {
12964
+ for (const candidate of oauthDiscoveryUrls(url)) {
12965
+ try {
12966
+ const metadata = await fetchJsonMetadata(candidate, controller.signal);
12967
+ const discovered = extractOAuthDiscovery(metadata);
12968
+ if (discovered) {
12969
+ assertOAuthAppUrlsSafe(discovered);
12970
+ return { ...discovered, sourceUrl: candidate };
12971
+ }
12972
+ failures.push(`${candidate}: missing authorization_endpoint or token_endpoint`);
12973
+ } catch (error) {
12974
+ if (error instanceof DOMException && error.name === "AbortError") {
12975
+ throw new Error("OAuth discovery timed out after 10s.");
12976
+ }
12977
+ if (isUrlSafetyError(error))
12978
+ throw error;
12979
+ failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
12980
+ }
12981
+ }
12982
+ } finally {
12983
+ clearTimeout(timeout);
12984
+ }
12985
+ throw new Error(`OAuth discovery failed. ${failures.join(" ")}`);
12986
+ }
12987
+ function isUrlSafetyError(error) {
12988
+ if (!(error instanceof Error))
12989
+ return false;
12990
+ return error.message.startsWith("Refusing ") || error.message.startsWith("Invalid URL:") || error.message.startsWith("Missing hostname:");
12991
+ }
12992
+ function stringFromCatalogEntry(entry, keys) {
12993
+ for (const key of keys) {
12994
+ const value = entry[key];
12995
+ if (typeof value === "string")
12996
+ return value;
12997
+ }
12998
+ return "";
12999
+ }
13000
+ function stringArrayFromCatalogEntry(entry, key) {
13001
+ const value = entry[key];
13002
+ if (!Array.isArray(value))
13003
+ return [];
13004
+ return value.filter((item) => typeof item === "string");
13005
+ }
13006
+ function normalizeCatalogEntry(entry) {
13007
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
13008
+ return null;
13009
+ const record = entry;
13010
+ const kind = stringFromCatalogEntry(record, ["kind", "type"]);
13011
+ if (!kind || kind === "cli")
13012
+ return null;
13013
+ const slug = stringFromCatalogEntry(record, ["slug", "id", "name"]);
13014
+ const name = stringFromCatalogEntry(record, ["name", "title"]) || slug;
13015
+ const domain = stringFromCatalogEntry(record, ["domain", "hostname"]);
13016
+ return {
13017
+ id: stringFromCatalogEntry(record, ["id", "slug"]) || slug || name,
13018
+ kind,
13019
+ slug,
13020
+ name,
13021
+ description: stringFromCatalogEntry(record, ["description", "summary"]),
13022
+ url: stringFromCatalogEntry(record, ["url", "homepage", "baseUrl"]),
13023
+ icon: stringFromCatalogEntry(record, ["icon", "logo"]) || null,
13024
+ domain,
13025
+ categories: stringArrayFromCatalogEntry(record, "categories"),
13026
+ feeds: stringArrayFromCatalogEntry(record, "feeds")
13027
+ };
13028
+ }
13029
+ function catalogEntriesFromPayload(payload) {
13030
+ if (Array.isArray(payload))
13031
+ return payload;
13032
+ if (!payload || typeof payload !== "object")
13033
+ return [];
13034
+ const record = payload;
13035
+ for (const key of ["entries", "integrations", "data", "items"]) {
13036
+ if (Array.isArray(record[key]))
13037
+ return record[key];
13038
+ }
13039
+ return [];
13040
+ }
13041
+ async function fetchIntegrationsCatalog() {
13042
+ const now = Date.now();
13043
+ if (integrationsCatalogCache && integrationsCatalogCache.expiresAtMs > now) {
13044
+ return integrationsCatalogCache.payload;
13045
+ }
13046
+ const controller = new AbortController;
13047
+ const timeout = setTimeout(() => controller.abort(), INTEGRATIONS_CATALOG_TIMEOUT_MS);
13048
+ try {
13049
+ const response = await fetch("https://integrations.sh/api.json", {
13050
+ headers: { accept: "application/json" },
13051
+ signal: controller.signal
13052
+ });
13053
+ if (!response.ok) {
13054
+ throw new Error(`integrations.sh returned HTTP ${response.status}`);
13055
+ }
13056
+ const payload = await response.json();
13057
+ const entries = catalogEntriesFromPayload(payload).map(normalizeCatalogEntry).filter((entry) => Boolean(entry));
13058
+ const cachedAt = new Date().toISOString();
13059
+ integrationsCatalogCache = {
13060
+ expiresAtMs: now + INTEGRATIONS_CATALOG_TTL_MS,
13061
+ payload: { entries, cachedAt }
13062
+ };
13063
+ return integrationsCatalogCache.payload;
13064
+ } catch (error) {
13065
+ if (error instanceof DOMException && error.name === "AbortError") {
13066
+ throw new Error("Timed out fetching integrations catalog after 15s.");
13067
+ }
13068
+ throw error;
13069
+ } finally {
13070
+ clearTimeout(timeout);
13071
+ }
13072
+ }
13073
+ function stringOrNull(value) {
13074
+ return typeof value === "string" && value ? value : null;
13075
+ }
13076
+ function trimSurfacePayload(domain, payload) {
13077
+ const record = payload && typeof payload === "object" ? payload : {};
13078
+ const rawSurfaces = Array.isArray(record.surfaces) ? record.surfaces : [];
13079
+ const surfaces = [];
13080
+ const referencedCredentialIds = new Set;
13081
+ for (const raw of rawSurfaces) {
13082
+ if (!raw || typeof raw !== "object")
13083
+ continue;
13084
+ const surface = raw;
13085
+ const type = typeof surface.type === "string" ? surface.type : "";
13086
+ if (type !== "http" && type !== "mcp")
13087
+ continue;
13088
+ const auth = surface.auth && typeof surface.auth === "object" ? surface.auth : {};
13089
+ const entries = Array.isArray(auth.entries) ? auth.entries : [];
13090
+ const credentialIds = [];
13091
+ let mechanics = null;
13092
+ for (const entry of entries) {
13093
+ if (!entry || typeof entry !== "object")
13094
+ continue;
13095
+ const uses = Array.isArray(entry.use) ? entry.use : [];
13096
+ for (const use of uses) {
13097
+ if (!use || typeof use !== "object")
13098
+ continue;
13099
+ const useRecord = use;
13100
+ const id = typeof useRecord.id === "string" ? useRecord.id : "";
13101
+ if (id && !credentialIds.includes(id))
13102
+ credentialIds.push(id);
13103
+ const rawMechanics = useRecord.mechanics && typeof useRecord.mechanics === "object" ? useRecord.mechanics : {};
13104
+ const mechanicsIn = typeof rawMechanics.in === "string" ? rawMechanics.in : "";
13105
+ if (mechanicsIn && (!mechanics || mechanics.in !== "header" && mechanicsIn === "header")) {
13106
+ mechanics = {
13107
+ in: mechanicsIn,
13108
+ headerName: stringOrNull(rawMechanics.headerName),
13109
+ scheme: stringOrNull(rawMechanics.scheme)
13110
+ };
13111
+ }
13112
+ }
13113
+ }
13114
+ for (const id of credentialIds)
13115
+ referencedCredentialIds.add(id);
13116
+ surfaces.push({
13117
+ type,
13118
+ name: typeof surface.name === "string" ? surface.name : "",
13119
+ url: stringOrNull(surface.url),
13120
+ docs: stringOrNull(surface.docs),
13121
+ spec: stringOrNull(surface.spec),
13122
+ auth: { required: auth.status === "required", credentialIds, mechanics }
13123
+ });
13124
+ }
13125
+ const rawCredentials = record.credentials && typeof record.credentials === "object" ? record.credentials : {};
13126
+ const credentials = {};
13127
+ for (const [id, raw] of Object.entries(rawCredentials)) {
13128
+ if (!referencedCredentialIds.has(id) || !raw || typeof raw !== "object")
13129
+ continue;
13130
+ const credential = raw;
13131
+ credentials[id] = {
13132
+ type: typeof credential.type === "string" ? credential.type : "unknown",
13133
+ label: typeof credential.label === "string" ? credential.label : id,
13134
+ generateUrl: stringOrNull(credential.generateUrl),
13135
+ setup: stringOrNull(credential.setup)
13136
+ };
13137
+ }
13138
+ return {
13139
+ domain: typeof record.domain === "string" && record.domain ? record.domain : domain,
13140
+ summary: typeof record.summary === "string" ? record.summary : "",
13141
+ surfaces,
13142
+ credentials
13143
+ };
13144
+ }
13145
+ async function fetchIntegrationsSurface(domain) {
13146
+ const cacheKey2 = domain.toLowerCase();
13147
+ const now = Date.now();
13148
+ const cached = integrationsSurfaceCache.get(cacheKey2);
13149
+ if (cached && cached.expiresAtMs > now)
13150
+ return cached.payload;
13151
+ integrationsSurfaceCache.delete(cacheKey2);
13152
+ const controller = new AbortController;
13153
+ const timeout = setTimeout(() => controller.abort(), INTEGRATIONS_CATALOG_TIMEOUT_MS);
13154
+ try {
13155
+ const response = await fetch(`https://integrations.sh/api/${encodeURIComponent(cacheKey2)}/surface`, { headers: { accept: "application/json" }, signal: controller.signal });
13156
+ if (response.status === 404) {
13157
+ throw new SurfaceNotFoundError(`No integration surface found for ${domain}.`);
13158
+ }
13159
+ if (!response.ok) {
13160
+ throw new Error(`integrations.sh returned HTTP ${response.status}`);
13161
+ }
13162
+ const payload = trimSurfacePayload(cacheKey2, await response.json());
13163
+ if (integrationsSurfaceCache.size >= INTEGRATIONS_SURFACE_CACHE_MAX_ENTRIES) {
13164
+ const oldestKey = integrationsSurfaceCache.keys().next().value;
13165
+ if (oldestKey !== undefined)
13166
+ integrationsSurfaceCache.delete(oldestKey);
13167
+ }
13168
+ integrationsSurfaceCache.set(cacheKey2, {
13169
+ expiresAtMs: now + INTEGRATIONS_CATALOG_TTL_MS,
13170
+ payload
13171
+ });
13172
+ return payload;
13173
+ } catch (error) {
13174
+ if (error instanceof DOMException && error.name === "AbortError") {
13175
+ throw new Error("Timed out fetching integration surface after 15s.");
13176
+ }
13177
+ throw error;
13178
+ } finally {
13179
+ clearTimeout(timeout);
13180
+ }
13181
+ }
13182
+ function deleteOAuthApp(provider) {
13183
+ const existing = getOAuthApp(provider);
13184
+ if (!existing)
13185
+ return false;
13186
+ const tx = getDb().transaction(() => {
13187
+ deleteOAuthTokens(provider);
13188
+ getDb().query("DELETE FROM oauth_apps WHERE provider = ?").run(provider);
13189
+ });
13190
+ tx();
13191
+ return true;
13192
+ }
13193
+ async function refreshHttpConnection(id, userId, agentId) {
13194
+ return refreshScriptConnection(id, userId, agentId);
13195
+ }
13196
+ async function handleScriptConnections(req, res, pathSegments, queryParams, agentId) {
13197
+ if (listConnectionsRoute.match(req.method, pathSegments)) {
13198
+ const parsed = await listConnectionsRoute.parse(req, res, pathSegments, queryParams);
13199
+ if (!parsed)
13200
+ return true;
13201
+ json(res, { connections: listConnections(parsed.query) });
13202
+ return true;
13203
+ }
13204
+ if (getConnectionRoute.match(req.method, pathSegments)) {
13205
+ const parsed = await getConnectionRoute.parse(req, res, pathSegments, queryParams);
13206
+ if (!parsed)
13207
+ return true;
13208
+ const connection = listScriptConnections({ includeDisabled: true, allScopes: true }).find((candidate) => candidate.id === parsed.params.id) ?? null;
13209
+ if (!connection) {
13210
+ jsonError(res, "Script connection not found.", 404);
13211
+ return true;
13212
+ }
13213
+ json(res, { connection: connectionDetail(connection) });
13214
+ return true;
13215
+ }
13216
+ if (upsertConnectionRoute.match(req.method, pathSegments)) {
13217
+ const parsed = await upsertConnectionRoute.parse(req, res, pathSegments, queryParams);
12353
13218
  if (!parsed)
12354
13219
  return true;
12355
- const schedule = getScheduledTaskById(parsed.params.id);
12356
- if (!schedule) {
12357
- jsonError(res, "Schedule not found", 404);
12358
- return true;
12359
- }
12360
- if (!schedule.enabled) {
12361
- jsonError(res, "Schedule is disabled", 400);
13220
+ if (!ensureConnectionAdmin(req, res, agentId))
12362
13221
  return true;
12363
- }
12364
13222
  try {
12365
- const { workflowRunIds, task } = await dispatchScheduleTarget(schedule, ["manual-run"]);
12366
- const now = new Date().toISOString();
12367
- if (schedule.scheduleType === "one_time") {
12368
- updateScheduledTask(schedule.id, {
12369
- lastRunAt: now,
12370
- nextRunAt: null,
12371
- enabled: false,
12372
- lastUpdatedAt: now
12373
- });
12374
- } else {
12375
- updateScheduledTask(schedule.id, {
12376
- lastRunAt: now,
12377
- lastUpdatedAt: now
12378
- });
13223
+ if (parsed.body.kind === "openapi" && Boolean(parsed.body.openapiSpecJson) && Boolean(parsed.body.openapiSpecUrl)) {
13224
+ jsonError(res, "Provide exactly one of openapiSpecJson or openapiSpecUrl.", 400);
13225
+ return true;
12379
13226
  }
12380
- const updatedSchedule = getScheduledTaskById(parsed.params.id);
12381
- json(res, { schedule: updatedSchedule, workflowRunIds, task });
12382
- } catch (error) {
12383
- jsonError(res, error instanceof Error ? error.message : "Failed to run schedule", 500);
13227
+ const existingConnection = parsed.body.id ? getScriptConnectionById(parsed.body.id) : null;
13228
+ const existingOpenapiConnection = existingConnection?.kind === "openapi" ? existingConnection : null;
13229
+ if (parsed.body.kind === "openapi" && !parsed.body.openapiSpecJson && !parsed.body.openapiSpecUrl && !existingOpenapiConnection) {
13230
+ jsonError(res, "Provide exactly one of openapiSpecJson or openapiSpecUrl.", 400);
13231
+ return true;
13232
+ }
13233
+ const scopeWasProvided = Object.hasOwn(parsed.body, "scope");
13234
+ const scopeIdWasProvided = Object.hasOwn(parsed.body, "scopeId");
13235
+ const enabledWasProvided = Object.hasOwn(parsed.body, "enabled");
13236
+ const scope = (scopeWasProvided ? parsed.body.scope : existingConnection?.scope) ?? "global";
13237
+ const scopeIdInput = scopeIdWasProvided ? parsed.body.scopeId : existingConnection && scope === existingConnection.scope ? existingConnection.scopeId : null;
13238
+ const scopeId = connectionScopeId(scope, scopeIdInput);
13239
+ const enabled = enabledWasProvided ? parsed.body.enabled !== false : existingConnection?.enabled ?? true;
13240
+ const credentialBindingId = maybeCreateInlineBinding(parsed.body, scope, scopeId);
13241
+ const userId = resolveHttpAuditUserId(req, agentId);
13242
+ const openapiSpecUrl = parsed.body.kind === "openapi" ? parsed.body.openapiSpecUrl : undefined;
13243
+ const openapiSpecJson = parsed.body.kind === "openapi" ? parsed.body.openapiSpecJson : undefined;
13244
+ const openapiSpecUrlChanged = parsed.body.kind === "openapi" && Boolean(openapiSpecUrl) && openapiSpecUrl !== existingOpenapiConnection?.openapiSpecSource;
13245
+ const reuseExistingOpenapiSpec = parsed.body.kind === "openapi" && Boolean(existingOpenapiConnection) && openapiSpecJson === undefined && !openapiSpecUrlChanged;
13246
+ const connection = await upsertScriptConnection({
13247
+ id: parsed.body.id,
13248
+ slug: parsed.body.slug,
13249
+ displayName: parsed.body.displayName,
13250
+ kind: parsed.body.kind,
13251
+ scope,
13252
+ scopeId,
13253
+ baseUrl: "baseUrl" in parsed.body ? parsed.body.baseUrl : null,
13254
+ allowedHosts: parsed.body.allowedHosts ?? ("baseUrl" in parsed.body ? [new URL(parsed.body.baseUrl).hostname] : []),
13255
+ credentialBindingId,
13256
+ openapiSpecSourceKind: reuseExistingOpenapiSpec && !openapiSpecUrl ? existingOpenapiConnection?.openapiSpecSourceKind : undefined,
13257
+ openapiSpecSource: reuseExistingOpenapiSpec && !openapiSpecUrl ? existingOpenapiConnection?.openapiSpecSource : undefined,
13258
+ openapiSpecUrl,
13259
+ openapiSpecJson: parsed.body.kind === "openapi" ? openapiSpecJson ?? (reuseExistingOpenapiSpec ? existingOpenapiConnection?.openapiSpecJson ?? undefined : undefined) : undefined,
13260
+ openapiSpecEtag: reuseExistingOpenapiSpec ? existingOpenapiConnection?.openapiSpecEtag : undefined,
13261
+ openapiSpecFetchedAt: reuseExistingOpenapiSpec ? existingOpenapiConnection?.openapiSpecFetchedAt : undefined,
13262
+ mcpServerId: parsed.body.kind === "mcp" ? parsed.body.mcpServerId : null,
13263
+ enabled,
13264
+ agentId,
13265
+ userId
13266
+ });
13267
+ json(res, { connection: decorateConnections([connection])[0] });
13268
+ } catch (err) {
13269
+ jsonError(res, err instanceof Error ? err.message : String(err), 400);
12384
13270
  }
12385
13271
  return true;
12386
13272
  }
12387
- if (getSchedule.match(req.method, pathSegments)) {
12388
- const parsed = await getSchedule.parse(req, res, pathSegments, queryParams);
13273
+ if (refreshConnectionRoute.match(req.method, pathSegments)) {
13274
+ const parsed = await refreshConnectionRoute.parse(req, res, pathSegments, queryParams);
12389
13275
  if (!parsed)
12390
13276
  return true;
12391
- const schedule = getScheduledTaskById(parsed.params.id);
12392
- if (!schedule) {
12393
- jsonError(res, "Schedule not found", 404);
13277
+ if (!ensureConnectionAdmin(req, res, agentId))
12394
13278
  return true;
13279
+ try {
13280
+ const refreshed = await refreshHttpConnection(parsed.params.id, resolveHttpAuditUserId(req, agentId), agentId);
13281
+ if (!refreshed) {
13282
+ jsonError(res, "Script connection not found.", 404);
13283
+ return true;
13284
+ }
13285
+ json(res, { connection: decorateConnections([refreshed])[0] });
13286
+ } catch (err) {
13287
+ jsonError(res, err instanceof Error ? err.message : String(err), 400);
12395
13288
  }
12396
- const userId = resolveHttpAuditUserId(req, myAgentId);
12397
- const [decorated] = withFavoriteFlags([schedule], { userId, itemType: "schedule" });
12398
- json(res, decorated ?? schedule);
12399
13289
  return true;
12400
13290
  }
12401
- if (updateSchedule.match(req.method, pathSegments) || patchSchedule.match(req.method, pathSegments)) {
12402
- const routeHandle = updateSchedule.match(req.method, pathSegments) ? updateSchedule : patchSchedule;
12403
- const parsed = await routeHandle.parse(req, res, pathSegments, queryParams);
13291
+ if (setConnectionEnabledRoute.match(req.method, pathSegments)) {
13292
+ const parsed = await setConnectionEnabledRoute.parse(req, res, pathSegments, queryParams);
12404
13293
  if (!parsed)
12405
13294
  return true;
12406
- const body = parsed.body;
12407
- if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
12408
- const normalizedModel = splitLegacyModelAlias({
12409
- model: parsed.body.model,
12410
- modelTier: parsed.body.modelTier
12411
- });
12412
- if (parsed.body.model !== undefined)
12413
- body.model = normalizedModel.model ?? null;
12414
- if (parsed.body.modelTier !== undefined || normalizedModel.modelTier) {
12415
- body.modelTier = normalizedModel.modelTier ?? null;
12416
- }
12417
- }
12418
- const existing = getScheduledTaskById(parsed.params.id);
12419
- if (!existing) {
12420
- jsonError(res, "Schedule not found", 404);
13295
+ if (!ensureConnectionAdmin(req, res, agentId))
12421
13296
  return true;
12422
- }
12423
- if (existing.scheduleType === "one_time" && !existing.enabled && existing.lastRunAt) {
12424
- jsonError(res, "One-time schedule has already executed. Create a new one instead.", 400);
13297
+ const updated = setScriptConnectionEnabled(parsed.params.id, parsed.body.enabled, resolveHttpAuditUserId(req, agentId));
13298
+ if (!updated) {
13299
+ jsonError(res, "Script connection not found.", 404);
12425
13300
  return true;
12426
13301
  }
12427
- if (existing.scheduleType !== "one_time") {
12428
- const timing = mergeScheduleTiming({
12429
- cronExpression: existing.cronExpression ?? null,
12430
- intervalMs: existing.intervalMs ?? null
12431
- }, { cronExpression: parsed.body.cronExpression, intervalMs: parsed.body.intervalMs });
12432
- if (validateRecurringTiming(timing)) {
12433
- jsonError(res, "At least one of intervalMs or cronExpression must be set", 400);
13302
+ json(res, { connection: decorateConnections([updated])[0] });
13303
+ return true;
13304
+ }
13305
+ if (listCredentialBindingsRoute.match(req.method, pathSegments)) {
13306
+ json(res, {
13307
+ bindings: listRelationalCredentialBindings({ includeInactive: true }).map(decorateBinding)
13308
+ });
13309
+ return true;
13310
+ }
13311
+ if (upsertCredentialBindingRoute.match(req.method, pathSegments)) {
13312
+ const parsed = await upsertCredentialBindingRoute.parse(req, res, pathSegments, queryParams);
13313
+ if (!parsed)
13314
+ return true;
13315
+ if (!ensureConnectionAdmin(req, res, agentId))
13316
+ return true;
13317
+ try {
13318
+ const scope = parsed.body.scope ?? "global";
13319
+ const scopeId = connectionScopeId(scope, parsed.body.scopeId, "bindings");
13320
+ if (!parsed.body.headerTemplate && !parsed.body.queryTemplate) {
13321
+ jsonError(res, "At least one of headerTemplate or queryTemplate is required.", 400);
12434
13322
  return true;
12435
13323
  }
12436
- }
12437
- if (parsed.body.cronExpression) {
12438
- try {
12439
- import_cron_parser.CronExpressionParser.parse(parsed.body.cronExpression);
12440
- } catch {
12441
- jsonError(res, "Invalid cron expression", 400);
13324
+ if ((parsed.body.authKind ?? "config") === "oauth" && !parsed.body.oauthProvider) {
13325
+ jsonError(res, "oauthProvider is required for oauth credential bindings.", 400);
12442
13326
  return true;
12443
13327
  }
13328
+ validateCredentialTemplate({
13329
+ configKey: parsed.body.configKey,
13330
+ headerTemplate: parsed.body.headerTemplate,
13331
+ queryTemplate: parsed.body.queryTemplate,
13332
+ requireTemplate: true
13333
+ });
13334
+ const nextBinding = CredentialBindingSchema.parse({
13335
+ configKey: parsed.body.configKey,
13336
+ allowedHosts: parsed.body.allowedHosts,
13337
+ headerTemplate: parsed.body.headerTemplate,
13338
+ queryTemplate: parsed.body.queryTemplate,
13339
+ scope,
13340
+ scopeId,
13341
+ active: parsed.body.active ?? true,
13342
+ authKind: parsed.body.authKind ?? "config",
13343
+ oauthProvider: parsed.body.oauthProvider
13344
+ });
13345
+ const binding = upsertCredentialBinding({
13346
+ id: parsed.body.id,
13347
+ configKey: nextBinding.configKey,
13348
+ allowedHosts: nextBinding.allowedHosts,
13349
+ headerTemplate: nextBinding.headerTemplate,
13350
+ queryTemplate: nextBinding.queryTemplate,
13351
+ scope: nextBinding.scope,
13352
+ scopeId: nextBinding.scopeId ?? null,
13353
+ active: nextBinding.active,
13354
+ authKind: nextBinding.authKind,
13355
+ oauthProvider: nextBinding.oauthProvider ?? null,
13356
+ userId: resolveHttpAuditUserId(req, agentId)
13357
+ });
13358
+ json(res, { binding: decorateBinding(binding) });
13359
+ } catch (err) {
13360
+ jsonError(res, err instanceof Error ? err.message : String(err), 400);
12444
13361
  }
12445
- if (parsed.body.targetAgentId) {
12446
- const agent = getAgentById(parsed.body.targetAgentId);
12447
- if (!agent) {
12448
- jsonError(res, "Target agent not found", 400);
13362
+ return true;
13363
+ }
13364
+ if (listOAuthAppsRoute.match(req.method, pathSegments)) {
13365
+ json(res, { oauthApps: listOAuthApps() });
13366
+ return true;
13367
+ }
13368
+ if (upsertOAuthAppRoute.match(req.method, pathSegments)) {
13369
+ const parsed = await upsertOAuthAppRoute.parse(req, res, pathSegments, queryParams);
13370
+ if (!parsed)
13371
+ return true;
13372
+ if (!ensureConnectionAdmin(req, res, agentId))
13373
+ return true;
13374
+ try {
13375
+ assertOAuthProviderIsNotReserved(parsed.body.provider);
13376
+ assertOAuthAppUrlsSafe(parsed.body);
13377
+ const existing = getOAuthApp(parsed.body.provider);
13378
+ const clientSecret = parsed.body.clientSecret ?? existing?.clientSecret;
13379
+ if (!clientSecret) {
13380
+ jsonError(res, "clientSecret is required when creating a new OAuth app.", 400);
12449
13381
  return true;
12450
13382
  }
13383
+ const redirectUri = genericOAuthRedirectUri(parsed.body.provider);
13384
+ upsertOAuthApp(parsed.body.provider, {
13385
+ clientId: parsed.body.clientId,
13386
+ clientSecret,
13387
+ authorizeUrl: parsed.body.authorizeUrl,
13388
+ tokenUrl: parsed.body.tokenUrl,
13389
+ redirectUri,
13390
+ scopes: (parsed.body.scopes ?? []).join(","),
13391
+ ...parsed.body.extraParams || parsed.body.tokenAuthStyle || parsed.body.tokenBodyFormat ? {
13392
+ metadata: JSON.stringify({
13393
+ ...parsed.body.extraParams ? { extraParams: parsed.body.extraParams } : {},
13394
+ ...parsed.body.tokenAuthStyle ? { tokenAuthStyle: parsed.body.tokenAuthStyle } : {},
13395
+ ...parsed.body.tokenBodyFormat ? { tokenBodyFormat: parsed.body.tokenBodyFormat } : {}
13396
+ })
13397
+ } : {}
13398
+ });
13399
+ const app = listOAuthApps().find((row) => row.provider === parsed.body.provider);
13400
+ json(res, { oauthApp: app });
13401
+ } catch (err) {
13402
+ jsonError(res, err instanceof Error ? err.message : String(err), 400);
12451
13403
  }
12452
- const mergedTargetType = parsed.body.targetType ?? existing.targetType;
12453
- const mergedTaskTemplate = parsed.body.taskTemplate !== undefined ? parsed.body.taskTemplate : existing.taskTemplate;
12454
- const mergedWorkflowId = parsed.body.workflowId !== undefined ? parsed.body.workflowId : existing.workflowId;
12455
- const mergedScriptName = parsed.body.scriptName !== undefined ? parsed.body.scriptName : existing.scriptName;
12456
- if (mergedTargetType === "agent-task" && !mergedTaskTemplate) {
12457
- jsonError(res, "taskTemplate is required when targetType is 'agent-task'.", 400);
13404
+ return true;
13405
+ }
13406
+ if (discoverOAuthAppRoute.match(req.method, pathSegments)) {
13407
+ const parsed = await discoverOAuthAppRoute.parse(req, res, pathSegments, queryParams);
13408
+ if (!parsed)
12458
13409
  return true;
13410
+ if (!ensureConnectionAdmin(req, res, agentId))
13411
+ return true;
13412
+ try {
13413
+ const discovered = await discoverOAuthApp(parsed.body.url);
13414
+ json(res, discovered);
13415
+ } catch (err) {
13416
+ jsonError(res, err instanceof Error ? err.message : String(err), 400);
12459
13417
  }
12460
- if (mergedTargetType === "workflow") {
12461
- if (!mergedWorkflowId) {
12462
- jsonError(res, "workflowId is required when targetType is 'workflow'.", 400);
12463
- return true;
12464
- }
12465
- if (!getWorkflow(mergedWorkflowId)) {
12466
- jsonError(res, `Workflow not found: ${mergedWorkflowId}`, 400);
12467
- return true;
12468
- }
13418
+ return true;
13419
+ }
13420
+ if (deleteOAuthAppRoute.match(req.method, pathSegments)) {
13421
+ const parsed = await deleteOAuthAppRoute.parse(req, res, pathSegments, queryParams);
13422
+ if (!parsed)
13423
+ return true;
13424
+ if (!ensureConnectionAdmin(req, res, agentId))
13425
+ return true;
13426
+ if (!deleteOAuthApp(parsed.params.provider)) {
13427
+ jsonError(res, `OAuth app ${parsed.params.provider} not found.`, 404);
13428
+ return true;
12469
13429
  }
12470
- if (mergedTargetType === "script") {
12471
- if (!mergedScriptName) {
12472
- jsonError(res, "scriptName is required when targetType is 'script'.", 400);
12473
- return true;
12474
- }
12475
- if (!getScript({ name: mergedScriptName, scope: "global" })) {
12476
- jsonError(res, `Script not found: ${mergedScriptName}`, 400);
12477
- return true;
12478
- }
13430
+ json(res, { success: true });
13431
+ return true;
13432
+ }
13433
+ if (authorizeUrlRoute2.match(req.method, pathSegments)) {
13434
+ const parsed = await authorizeUrlRoute2.parse(req, res, pathSegments, queryParams);
13435
+ if (!parsed)
13436
+ return true;
13437
+ if (!ensureConnectionAdmin(req, res, agentId))
13438
+ return true;
13439
+ const config = getOAuthProviderConfig(parsed.params.provider);
13440
+ if (!config) {
13441
+ jsonError(res, `OAuth app ${parsed.params.provider} is not configured.`, 404);
13442
+ return true;
12479
13443
  }
12480
- if (parsed.body.name && parsed.body.name !== existing.name) {
12481
- const nameConflict = getScheduledTaskByName(parsed.body.name);
12482
- if (nameConflict) {
12483
- jsonError(res, "Schedule with this name already exists", 409);
13444
+ const result = await buildAuthorizationUrl(config);
13445
+ json(res, { authorizeUrl: result.url, redirectUri: config.redirectUri });
13446
+ return true;
13447
+ }
13448
+ if (integrationsCatalogRoute.match(req.method, pathSegments)) {
13449
+ try {
13450
+ json(res, await fetchIntegrationsCatalog());
13451
+ } catch (err) {
13452
+ jsonError(res, `Failed to fetch integrations catalog: ${err instanceof Error ? err.message : String(err)}`, 502);
13453
+ }
13454
+ return true;
13455
+ }
13456
+ if (integrationsSurfaceRoute.match(req.method, pathSegments)) {
13457
+ const parsed = await integrationsSurfaceRoute.parse(req, res, pathSegments, queryParams);
13458
+ if (!parsed)
13459
+ return true;
13460
+ try {
13461
+ json(res, await fetchIntegrationsSurface(parsed.params.domain));
13462
+ } catch (err) {
13463
+ if (err instanceof SurfaceNotFoundError) {
13464
+ jsonError(res, err.message, 404);
12484
13465
  return true;
12485
13466
  }
13467
+ jsonError(res, `Failed to fetch integration surface: ${err instanceof Error ? err.message : String(err)}`, 502);
12486
13468
  }
12487
- const newEnabled = parsed.body.enabled !== undefined ? parsed.body.enabled : existing.enabled;
12488
- if (existing.scheduleType === "one_time") {
12489
- if (!newEnabled) {
12490
- body.nextRunAt = null;
12491
- }
12492
- } else {
12493
- if (!newEnabled) {
12494
- body.nextRunAt = null;
12495
- } else if (parsed.body.cronExpression !== undefined || parsed.body.intervalMs !== undefined || parsed.body.enabled === true && !existing.enabled) {
12496
- const timing = mergeScheduleTiming({
12497
- cronExpression: existing.cronExpression ?? null,
12498
- intervalMs: existing.intervalMs ?? null
12499
- }, { cronExpression: parsed.body.cronExpression, intervalMs: parsed.body.intervalMs });
12500
- const mergedTimezone = parsed.body.timezone !== undefined ? parsed.body.timezone : existing.timezone;
12501
- if (timing.mergedCron || timing.mergedInterval) {
12502
- body.nextRunAt = calculateNextRun({
12503
- cronExpression: timing.mergedCron,
12504
- intervalMs: timing.mergedInterval,
12505
- timezone: mergedTimezone
12506
- });
12507
- }
12508
- }
13469
+ return true;
13470
+ }
13471
+ if (disconnectOAuthAppRoute.match(req.method, pathSegments)) {
13472
+ const parsed = await disconnectOAuthAppRoute.parse(req, res, pathSegments, queryParams);
13473
+ if (!parsed)
13474
+ return true;
13475
+ if (!ensureConnectionAdmin(req, res, agentId))
13476
+ return true;
13477
+ const app = getOAuthApp(parsed.params.provider);
13478
+ if (!app) {
13479
+ jsonError(res, `OAuth app ${parsed.params.provider} is not configured.`, 404);
13480
+ return true;
12509
13481
  }
12510
- const updatedBy = resolveHttpAuditUserId(req, myAgentId);
12511
- if (updatedBy !== null)
12512
- body.updatedBy = updatedBy;
12513
- const schedule = updateScheduledTask(parsed.params.id, body);
12514
- json(res, schedule);
13482
+ const tokens = getOAuthTokens(parsed.params.provider);
13483
+ if (!tokens) {
13484
+ json(res, { disconnected: false, message: "no stored tokens" });
13485
+ return true;
13486
+ }
13487
+ const revocationAttempted = await attemptRemoteRevocation(app, tokens.accessToken);
13488
+ deleteOAuthTokens(parsed.params.provider);
13489
+ json(res, { disconnected: true, revocationAttempted });
12515
13490
  return true;
12516
13491
  }
12517
- if (deleteSchedule.match(req.method, pathSegments)) {
12518
- const parsed = await deleteSchedule.parse(req, res, pathSegments, queryParams);
13492
+ if (refreshOAuthAppTokensRoute.match(req.method, pathSegments)) {
13493
+ const parsed = await refreshOAuthAppTokensRoute.parse(req, res, pathSegments, queryParams);
12519
13494
  if (!parsed)
12520
13495
  return true;
12521
- const deleted = deleteScheduledTask(parsed.params.id);
12522
- if (!deleted) {
12523
- jsonError(res, "Schedule not found", 404);
13496
+ if (!ensureConnectionAdmin(req, res, agentId))
13497
+ return true;
13498
+ const provider = parsed.params.provider;
13499
+ if (!getOAuthApp(provider)) {
13500
+ jsonError(res, `OAuth app ${provider} is not configured.`, 404);
12524
13501
  return true;
12525
13502
  }
12526
- json(res, { success: true });
13503
+ const tokens = getOAuthTokens(provider);
13504
+ if (!tokens) {
13505
+ jsonError(res, "Nothing to refresh — authorize first.", 400);
13506
+ return true;
13507
+ }
13508
+ if (!tokens.refreshToken) {
13509
+ jsonError(res, `OAuth app ${provider} does not support refresh (no refresh token stored).`, 400);
13510
+ return true;
13511
+ }
13512
+ try {
13513
+ await forceRefreshTokenOrThrow(provider);
13514
+ } catch (err) {
13515
+ const message = err instanceof Error ? err.message : String(err);
13516
+ jsonError(res, scrubSecrets(`Token refresh failed: ${message}`), 502);
13517
+ return true;
13518
+ }
13519
+ json(res, {
13520
+ refreshed: true,
13521
+ tokenStatus: getOAuthBindingTokenStatus(provider),
13522
+ expiresAt: getOAuthTokens(provider)?.expiresAt ?? null
13523
+ });
12527
13524
  return true;
12528
13525
  }
12529
13526
  return false;
@@ -12579,7 +13576,7 @@ function lintWorkflowLabels(source) {
12579
13576
  init_types();
12580
13577
  var DEFAULT_SCRIPT_RUN_CONCURRENCY_CAP = 10;
12581
13578
  var runIdParamsSchema = exports_external.object({ runId: exports_external.string().uuid() });
12582
- var idParamsSchema = exports_external.object({ id: exports_external.string().uuid() });
13579
+ var idParamsSchema2 = exports_external.object({ id: exports_external.string().uuid() });
12583
13580
  var stepParamsSchema = exports_external.object({
12584
13581
  runId: exports_external.string().uuid(),
12585
13582
  stepKey: exports_external.string().min(1)
@@ -12663,7 +13660,7 @@ var getScriptRunRoute = route({
12663
13660
  operationId: "script_runs_get",
12664
13661
  summary: "Get a script workflow run with journal",
12665
13662
  tags: ["Script Runs"],
12666
- params: idParamsSchema,
13663
+ params: idParamsSchema2,
12667
13664
  responses: {
12668
13665
  200: { description: "Script run detail" },
12669
13666
  404: { description: "Script run not found" }
@@ -12676,7 +13673,7 @@ var deleteScriptRunRoute = route({
12676
13673
  operationId: "script_runs_cancel",
12677
13674
  summary: "Cancel a script workflow run",
12678
13675
  tags: ["Script Runs"],
12679
- params: idParamsSchema,
13676
+ params: idParamsSchema2,
12680
13677
  responses: {
12681
13678
  204: { description: "Script run cancelled, or already terminal" },
12682
13679
  404: { description: "Script run not found" }
@@ -13089,7 +14086,7 @@ var searchBodySchema = exports_external.object({
13089
14086
  var nameParamsSchema = exports_external.object({ name: scriptNameSchema });
13090
14087
  var scopeQuerySchema = exports_external.object({ scope: ScriptScopeSchema.default("agent") });
13091
14088
  var optionalScopeQuerySchema = exports_external.object({ scope: ScriptScopeSchema.optional() });
13092
- var idParamsSchema2 = exports_external.object({ id: exports_external.string().uuid() });
14089
+ var idParamsSchema3 = exports_external.object({ id: exports_external.string().uuid() });
13093
14090
  var listScriptsQuerySchema = exports_external.object({
13094
14091
  scope: ScriptScopeSchema.optional(),
13095
14092
  includeScratch: exports_external.enum(["true", "false"]).optional()
@@ -13138,7 +14135,8 @@ var searchRoute = route({
13138
14135
  responses: {
13139
14136
  200: { description: "Matching scripts" },
13140
14137
  400: { description: "Validation error" }
13141
- }
14138
+ },
14139
+ rbac: { permission: "script.search" }
13142
14140
  });
13143
14141
  var deleteRoute = route({
13144
14142
  method: "delete",
@@ -13204,7 +14202,7 @@ var getScriptByIdRoute = route({
13204
14202
  summary: "Get a saved script by id",
13205
14203
  description: "Dashboard read: full record including source and parsed signature.",
13206
14204
  tags: ["Scripts"],
13207
- params: idParamsSchema2,
14205
+ params: idParamsSchema3,
13208
14206
  responses: {
13209
14207
  200: { description: "Script detail" },
13210
14208
  404: { description: "Script not found" }
@@ -13218,7 +14216,7 @@ var listVersionsRoute = route({
13218
14216
  summary: "List versions of a saved script",
13219
14217
  description: "Dashboard read: version history, newest first.",
13220
14218
  tags: ["Scripts"],
13221
- params: idParamsSchema2,
14219
+ params: idParamsSchema3,
13222
14220
  responses: {
13223
14221
  200: { description: "Script versions" },
13224
14222
  404: { description: "Script not found" }
@@ -13245,13 +14243,14 @@ var createScriptApiRoute = route({
13245
14243
  summary: "Expose a script as an external HTTP API endpoint",
13246
14244
  description: "Returns the endpoint plus the plaintext bearer token (when authMode is 'bearer').",
13247
14245
  tags: ["Scripts"],
13248
- params: idParamsSchema2,
14246
+ params: idParamsSchema3,
13249
14247
  body: createScriptApiBodySchema,
13250
14248
  responses: {
13251
14249
  201: { description: "Endpoint created" },
13252
14250
  400: { description: "Validation error or script has no owning agent" },
13253
14251
  404: { description: "Script not found" }
13254
- }
14252
+ },
14253
+ rbac: { permission: "script.api.create" }
13255
14254
  });
13256
14255
  var listScriptApisRoute = route({
13257
14256
  method: "get",
@@ -13260,7 +14259,7 @@ var listScriptApisRoute = route({
13260
14259
  operationId: "scripts_api_list",
13261
14260
  summary: "List external API endpoints for a script",
13262
14261
  tags: ["Scripts"],
13263
- params: idParamsSchema2,
14262
+ params: idParamsSchema3,
13264
14263
  responses: {
13265
14264
  200: { description: "Endpoints (without secrets)" },
13266
14265
  404: { description: "Script not found" }
@@ -13277,7 +14276,8 @@ var revealScriptApiSecretRoute = route({
13277
14276
  responses: {
13278
14277
  200: { description: "Decrypted token (null when authMode is 'none')" },
13279
14278
  404: { description: "Endpoint not found" }
13280
- }
14279
+ },
14280
+ rbac: { permission: "script.api.read.secrets" }
13281
14281
  });
13282
14282
  var patchScriptApiRoute = route({
13283
14283
  method: "patch",
@@ -13291,7 +14291,8 @@ var patchScriptApiRoute = route({
13291
14291
  responses: {
13292
14292
  200: { description: "Updated endpoint" },
13293
14293
  404: { description: "Endpoint not found" }
13294
- }
14294
+ },
14295
+ rbac: { permission: "script.api.update" }
13295
14296
  });
13296
14297
  var rotateScriptApiRoute = route({
13297
14298
  method: "post",
@@ -13305,7 +14306,8 @@ var rotateScriptApiRoute = route({
13305
14306
  200: { description: "Endpoint with new plaintext token" },
13306
14307
  400: { description: "Endpoint uses 'none' auth — nothing to rotate" },
13307
14308
  404: { description: "Endpoint not found" }
13308
- }
14309
+ },
14310
+ rbac: { permission: "script.api.rotate" }
13309
14311
  });
13310
14312
  var deleteScriptApiRoute = route({
13311
14313
  method: "delete",
@@ -13318,7 +14320,8 @@ var deleteScriptApiRoute = route({
13318
14320
  responses: {
13319
14321
  200: { description: "Deleted" },
13320
14322
  404: { description: "Endpoint not found" }
13321
- }
14323
+ },
14324
+ rbac: { permission: "script.api.delete" }
13322
14325
  });
13323
14326
  function requireAgent2(res, agentId) {
13324
14327
  if (!agentId) {
@@ -13454,8 +14457,9 @@ async function handleScripts(req, res, pathSegments, queryParams, agentId) {
13454
14457
  args: parsed.body.args,
13455
14458
  fsMode,
13456
14459
  agentId: agent.id,
13457
- egressSecrets: buildScriptCredentialBindings({ agentId: agent.id }),
13458
- apiConnections: getScriptApiConnectionDescriptors({ agentId: agent.id })
14460
+ egressSecrets: await buildScriptCredentialBindings({ agentId: agent.id }),
14461
+ apiConnections: getScriptApiConnectionDescriptors({ agentId: agent.id }),
14462
+ mcpConnections: getScriptMcpConnectionDescriptors({ agentId: agent.id })
13459
14463
  });
13460
14464
  let kvSaved;
13461
14465
  if (parsed.body.idempotencyKey && !output.error && output.exitCode === 0) {
@@ -13578,9 +14582,11 @@ async function handleScripts(req, res, pathSegments, queryParams, agentId) {
13578
14582
  return true;
13579
14583
  }
13580
14584
  if (typeDefsRoute.match(req.method, pathSegments)) {
14585
+ const apiTypes = getScriptApiTypes();
14586
+ const mcpTypes = getScriptMcpTypes();
13581
14587
  json(res, {
13582
- sdkTypes: scriptSdkTypesWithGeneratedApis(getScriptApiTypes()),
13583
- stdlibTypes: SCRIPT_STDLIB_TYPES
14588
+ sdkTypes: scriptSdkTypesWithGeneratedApis(apiTypes, mcpTypes),
14589
+ stdlibTypes: scriptStdlibTypesWithGeneratedApis(apiTypes, mcpTypes)
13584
14590
  });
13585
14591
  return true;
13586
14592
  }
@@ -13627,8 +14633,8 @@ async function handleScripts(req, res, pathSegments, queryParams, agentId) {
13627
14633
  json(res, {
13628
14634
  signature: JSON.parse(script.signatureJson),
13629
14635
  argsJsonSchema: script.argsJsonSchema ? JSON.parse(script.argsJsonSchema) : null,
13630
- sdkTypes: scriptSdkTypesWithGeneratedApis(getScriptApiTypes({ agentId: agent.id })),
13631
- stdlibTypes: SCRIPT_STDLIB_TYPES
14636
+ sdkTypes: scriptSdkTypesWithGeneratedApis(getScriptApiTypes({ agentId: agent.id }), getScriptMcpTypes({ agentId: agent.id })),
14637
+ stdlibTypes: scriptStdlibTypesWithGeneratedApis(getScriptApiTypes({ agentId: agent.id }), getScriptMcpTypes({ agentId: agent.id }))
13632
14638
  });
13633
14639
  return true;
13634
14640
  }
@@ -14202,6 +15208,7 @@ var bulkUpsertSkillFilesRoute = route({
14202
15208
  summary: "Bulk upsert bundled files for a skill",
14203
15209
  tags: ["Skills"],
14204
15210
  auth: { apiKey: true },
15211
+ rbac: { permission: "skill.update.any" },
14205
15212
  params: exports_external.object({ id: exports_external.string() }),
14206
15213
  body: exports_external.object({
14207
15214
  files: exports_external.array(skillFileWithPathSchema).max(100)
@@ -14234,6 +15241,7 @@ var upsertSkillFileRoute = route({
14234
15241
  summary: "Upsert a bundled skill file",
14235
15242
  tags: ["Skills"],
14236
15243
  auth: { apiKey: true },
15244
+ rbac: { permission: "skill.update.any" },
14237
15245
  params: exports_external.object({ id: exports_external.string(), path: exports_external.string() }),
14238
15246
  body: skillFileBodySchema,
14239
15247
  responses: {
@@ -14250,6 +15258,7 @@ var deleteSkillFileRoute = route({
14250
15258
  summary: "Delete a bundled skill file",
14251
15259
  tags: ["Skills"],
14252
15260
  auth: { apiKey: true },
15261
+ rbac: { permission: "skill.update.any" },
14253
15262
  params: exports_external.object({ id: exports_external.string(), path: exports_external.string() }),
14254
15263
  responses: {
14255
15264
  200: { description: "Skill file deleted" },
@@ -14263,6 +15272,7 @@ var createSkillRoute = route({
14263
15272
  summary: "Create a new skill",
14264
15273
  tags: ["Skills"],
14265
15274
  auth: { apiKey: true },
15275
+ rbac: { permission: "skill.create.swarm" },
14266
15276
  body: exports_external.object({
14267
15277
  content: exports_external.string().min(1),
14268
15278
  type: exports_external.string().optional(),
@@ -14282,6 +15292,7 @@ var updateSkillRoute = route({
14282
15292
  summary: "Update a skill",
14283
15293
  tags: ["Skills"],
14284
15294
  auth: { apiKey: true },
15295
+ rbac: { permission: "skill.update.any" },
14285
15296
  params: exports_external.object({ id: exports_external.string() }),
14286
15297
  body: exports_external.record(exports_external.string(), exports_external.unknown()),
14287
15298
  responses: {
@@ -14297,6 +15308,7 @@ var deleteSkillRoute = route({
14297
15308
  summary: "Delete a skill",
14298
15309
  tags: ["Skills"],
14299
15310
  auth: { apiKey: true },
15311
+ rbac: { permission: "skill.delete.any" },
14300
15312
  params: exports_external.object({ id: exports_external.string() }),
14301
15313
  responses: {
14302
15314
  200: { description: "Skill deleted" },
@@ -14311,6 +15323,7 @@ var installSkillRoute = route({
14311
15323
  summary: "Install skill for an agent",
14312
15324
  tags: ["Skills"],
14313
15325
  auth: { apiKey: true },
15326
+ rbac: { permission: "skill.install.any" },
14314
15327
  params: exports_external.object({ id: exports_external.string() }),
14315
15328
  body: exports_external.object({
14316
15329
  agentId: exports_external.string()
@@ -14327,6 +15340,7 @@ var uninstallSkillRoute = route({
14327
15340
  summary: "Uninstall skill for an agent",
14328
15341
  tags: ["Skills"],
14329
15342
  auth: { apiKey: true },
15343
+ rbac: { permission: "skill.uninstall.any" },
14330
15344
  params: exports_external.object({ id: exports_external.string(), agentId: exports_external.string() }),
14331
15345
  responses: {
14332
15346
  200: { description: "Skill uninstalled" }
@@ -14339,6 +15353,7 @@ var installRemoteRoute = route({
14339
15353
  summary: "Install a remote skill from GitHub",
14340
15354
  tags: ["Skills"],
14341
15355
  auth: { apiKey: true },
15356
+ rbac: { permission: "skill.install.global" },
14342
15357
  body: exports_external.object({
14343
15358
  sourceRepo: exports_external.string(),
14344
15359
  sourcePath: exports_external.string().optional(),
@@ -14357,6 +15372,7 @@ var syncRemoteRoute = route({
14357
15372
  summary: "Trigger remote skill sync",
14358
15373
  tags: ["Skills"],
14359
15374
  auth: { apiKey: true },
15375
+ rbac: { permission: "skill.update.any" },
14360
15376
  body: exports_external.object({
14361
15377
  skillId: exports_external.string().optional(),
14362
15378
  force: exports_external.boolean().optional()
@@ -14372,6 +15388,7 @@ var syncFilesystemRoute = route({
14372
15388
  summary: "Sync installed skills to agent filesystem",
14373
15389
  tags: ["Skills"],
14374
15390
  auth: { apiKey: true, agentId: true },
15391
+ rbac: { ungated: "self-scoped: syncs the caller's own agent FS" },
14375
15392
  responses: {
14376
15393
  200: { description: "Filesystem sync results" }
14377
15394
  }
@@ -15450,7 +16467,6 @@ async function handleTaskTemplates(req, res, pathSegments, queryParams) {
15450
16467
  init_zod();
15451
16468
  init_db();
15452
16469
  init_types();
15453
- init_request_auth_context();
15454
16470
  var listTasks = route({
15455
16471
  method: "get",
15456
16472
  path: "/api/tasks",
@@ -15465,7 +16481,9 @@ var listTasks = route({
15465
16481
  search: exports_external.string().optional(),
15466
16482
  includeHeartbeat: exports_external.enum(["true", "false"]).optional(),
15467
16483
  createdAfter: exports_external.string().datetime().optional(),
16484
+ createdBefore: exports_external.string().datetime().optional(),
15468
16485
  source: exports_external.string().optional(),
16486
+ orderBy: exports_external.enum(["lastUpdatedAt", "createdAt"]).optional(),
15469
16487
  limit: exports_external.coerce.number().int().optional(),
15470
16488
  offset: exports_external.coerce.number().int().optional(),
15471
16489
  fields: exports_external.enum(["full", "slim"]).optional()
@@ -15714,7 +16732,9 @@ async function handleTasks(req, res, pathSegments, queryParams, myAgentId) {
15714
16732
  search: parsed.query.search || undefined,
15715
16733
  includeHeartbeat: parsed.query.includeHeartbeat === "true" || undefined,
15716
16734
  createdAfter: parsed.query.createdAfter || undefined,
16735
+ createdBefore: parsed.query.createdBefore || undefined,
15717
16736
  source,
16737
+ orderBy: parsed.query.orderBy,
15718
16738
  limit: parsed.query.limit,
15719
16739
  offset: parsed.query.offset
15720
16740
  };
@@ -15727,11 +16747,12 @@ async function handleTasks(req, res, pathSegments, queryParams, myAgentId) {
15727
16747
  const parsed = await createTask.parse(req, res, pathSegments, queryParams);
15728
16748
  if (!parsed)
15729
16749
  return true;
15730
- const auth = getRequestAuth(req);
15731
- let requestedByUserId = auth?.kind === "user" ? auth.userId : parsed.body.requestedByUserId || undefined;
15732
- if (requestedByUserId && !getUserById(requestedByUserId)) {
15733
- console.warn(`[tasks] requestedByUserId ${requestedByUserId} does not exist — coercing to NULL`);
15734
- requestedByUserId = undefined;
16750
+ let requestedByUserId = resolveHttpAuditUserId(req, myAgentId) ?? undefined;
16751
+ const trustBodyRequestedByUserId = process.env.TRUST_BODY_REQUESTED_BY_USER_ID === "true";
16752
+ if (trustBodyRequestedByUserId && !requestedByUserId && parsed.body.requestedByUserId) {
16753
+ const candidate = findUserById(parsed.body.requestedByUserId);
16754
+ if (candidate)
16755
+ requestedByUserId = candidate.id;
15735
16756
  }
15736
16757
  let defaultAgentId = parsed.body.agentId || undefined;
15737
16758
  if (!defaultAgentId) {
@@ -17655,11 +18676,13 @@ function extractText(message) {
17655
18676
  function buildTaskDescription(payload) {
17656
18677
  const message = payload.message ?? {};
17657
18678
  const conversation = payload.conversation ?? {};
18679
+ const externalId = normalizeKapsoSender(payload);
18680
+ const contactDisplay = externalId ? renderIdentity(resolveKapsoIdentity(externalId)) : "(unknown user)";
17658
18681
  return resolveTemplate("kapso.message.received", {
17659
18682
  conversation_id: conversation.id ?? "unknown",
17660
18683
  inbound_wamid: message.id ?? "unknown",
17661
18684
  sender_phone: message.from ?? conversation.phone_number ?? "unknown",
17662
- contact_name: conversation.contact_name ?? "unknown",
18685
+ contact_name: contactDisplay,
17663
18686
  phone_number_id: payload.phone_number_id ?? "unknown",
17664
18687
  test_note: payload.test ? `
17665
18688
  - test: true (do NOT send a real WhatsApp reply)` : "",
@@ -17671,13 +18694,22 @@ function normalizeKapsoSender(payload) {
17671
18694
  const digits = raw.replace(/\D/g, "");
17672
18695
  return digits || null;
17673
18696
  }
18697
+ function resolveKapsoIdentity(externalId) {
18698
+ const kapsoResolution = resolveIdentity(KAPSO_IDENTITY_KIND, externalId);
18699
+ if (kapsoResolution.status === "resolved")
18700
+ return kapsoResolution;
18701
+ const whatsappResolution = resolveIdentity(WHATSAPP_IDENTITY_KIND, externalId);
18702
+ if (whatsappResolution.status === "resolved")
18703
+ return whatsappResolution;
18704
+ return kapsoResolution;
18705
+ }
17674
18706
  function resolveKapsoRequestedByUserId(payload) {
17675
18707
  const externalId = normalizeKapsoSender(payload);
17676
18708
  if (!externalId)
17677
18709
  return;
17678
- const mapped = findUserByExternalId(KAPSO_IDENTITY_KIND, externalId) ?? findUserByExternalId(WHATSAPP_IDENTITY_KIND, externalId);
17679
- if (mapped)
17680
- return mapped.id;
18710
+ const resolution = resolveKapsoIdentity(externalId);
18711
+ if (resolution.status === "resolved")
18712
+ return resolution.userId;
17681
18713
  recordUnmappedIdentity(KAPSO_IDENTITY_KIND, externalId, {
17682
18714
  sampleEventType: "kapso.message.received",
17683
18715
  sampleContext: [
@@ -19030,8 +20062,9 @@ async function handleX(req, res, pathSegments) {
19030
20062
  fsMode: "none",
19031
20063
  agentId: endpoint.agentId,
19032
20064
  timeoutMs,
19033
- egressSecrets: buildScriptCredentialBindings({ agentId: endpoint.agentId }),
19034
- apiConnections: getScriptApiConnectionDescriptors({ agentId: endpoint.agentId })
20065
+ egressSecrets: await buildScriptCredentialBindings({ agentId: endpoint.agentId }),
20066
+ apiConnections: getScriptApiConnectionDescriptors({ agentId: endpoint.agentId }),
20067
+ mcpConnections: getScriptMcpConnectionDescriptors({ agentId: endpoint.agentId })
19035
20068
  });
19036
20069
  const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
19037
20070
  const error = ok ? null : buildExecutionError(output);
@@ -19213,6 +20246,8 @@ var httpServer = createHttpServer(async (req, res) => {
19213
20246
  () => handleMetrics(req, res, pathSegments, queryParams, myAgentId),
19214
20247
  () => handleRepos(req, res, pathSegments, queryParams),
19215
20248
  () => handleSkills(req, res, pathSegments, queryParams, myAgentId),
20249
+ () => handleScriptConnections(req, res, pathSegments, queryParams, myAgentId),
20250
+ () => handleScriptConnectionProxy(req, res, pathSegments, queryParams, myAgentId),
19216
20251
  () => handleScriptRuns(req, res, pathSegments, queryParams, myAgentId),
19217
20252
  () => handleScripts(req, res, pathSegments, queryParams, myAgentId),
19218
20253
  () => handleX(req, res, pathSegments),
@@ -19221,6 +20256,7 @@ var httpServer = createHttpServer(async (req, res) => {
19221
20256
  () => handleMcpOAuth(req, res, pathSegments, queryParams),
19222
20257
  () => handleMemory(req, res, pathSegments, myAgentId),
19223
20258
  () => handleOAuthLocks(req, res, pathSegments, queryParams),
20259
+ () => handleGenericOAuth(req, res, pathSegments, queryParams),
19224
20260
  () => handleCodexOAuthKeepWarm(req, res, pathSegments),
19225
20261
  () => handlePagesPublic(req, res, pathSegments, queryParams),
19226
20262
  () => handlePageProxy(req, res),
@@ -19280,14 +20316,14 @@ async function shutdown() {
19280
20316
  sessionsProcessed: getServerSessionsProcessed()
19281
20317
  });
19282
20318
  if (hasCapability("scheduling")) {
19283
- const { stopScheduler } = await import("./index-0gzmqj4k.js");
20319
+ const { stopScheduler } = await import("./index-9ht8gr56.js");
19284
20320
  stopScheduler();
19285
20321
  }
19286
20322
  stopHeartbeat();
19287
20323
  stopScriptRunSupervisor();
19288
20324
  await stopSlackApp();
19289
20325
  if (process.env.OAUTH_KEEPALIVE_DISABLE !== "true") {
19290
- const { stopOAuthKeepalive } = await import("./keepalive-62gfj91d.js");
20326
+ const { stopOAuthKeepalive } = await import("./keepalive-3cps6e3e.js");
19291
20327
  await stopOAuthKeepalive();
19292
20328
  }
19293
20329
  stopMcpOAuthPendingGc();
@@ -19344,15 +20380,23 @@ try {
19344
20380
  throw err;
19345
20381
  }
19346
20382
  try {
19347
- const { seedPricingFromModelsDev } = await import("./seed-pricing-1r1y1xkq.js");
20383
+ const { seedPricingFromModelsDev } = await import("./seed-pricing-h2kdq4cq.js");
19348
20384
  seedPricingFromModelsDev();
19349
- const { startPricingRefreshLoop } = await import("./pricing-refresh-cgtkffpa.js");
20385
+ const { startPricingRefreshLoop } = await import("./pricing-refresh-0hvazmkf.js");
19350
20386
  startPricingRefreshLoop();
19351
20387
  } catch (err) {
19352
20388
  console.error("[startup] Failed to seed pricing rows:", err);
19353
20389
  }
19354
20390
  try {
19355
- const { runAllSeeders } = await import("./index-vfedgdw6.js");
20391
+ const { ensureRbacSeedsSynced } = await import("./rbac-roles-kfesjy9y.js");
20392
+ ensureRbacSeedsSynced();
20393
+ } catch (err) {
20394
+ console.error("[startup] Failed to sync RBAC seed rows:", err);
20395
+ if (isRbacEnabled())
20396
+ throw err;
20397
+ }
20398
+ try {
20399
+ const { runAllSeeders } = await import("./index-rm85cfdx.js");
19356
20400
  await runAllSeeders({ scriptEmbeddingMode: "skip" });
19357
20401
  } catch (err) {
19358
20402
  console.error("[startup] Failed to seed built-in entities:", err);
@@ -19388,31 +20432,35 @@ httpServer.listen(port, async () => {
19388
20432
  initWorkflows();
19389
20433
  startScriptRunSupervisor(getMcpBaseUrl());
19390
20434
  if (hasCapability("scheduling")) {
19391
- const { startScheduler } = await import("./index-0gzmqj4k.js");
19392
- const { getExecutorRegistry: getExecutorRegistry2 } = await import("./index-5ghxn8s6.js");
20435
+ const { startScheduler } = await import("./index-9ht8gr56.js");
20436
+ const { getExecutorRegistry: getExecutorRegistry2 } = await import("./index-9fqaktjr.js");
19393
20437
  const intervalMs = Number(process.env.SCHEDULER_INTERVAL_MS) || 1e4;
19394
20438
  startScheduler(getExecutorRegistry2(), intervalMs, {
19395
20439
  runId: globalState.__runId
19396
20440
  });
19397
20441
  }
19398
20442
  if (process.env.HEARTBEAT_DISABLE !== "true") {
19399
- const { startHeartbeat } = await import("./index-nhat35em.js");
20443
+ const { startHeartbeat } = await import("./index-47em3d2n.js");
19400
20444
  const heartbeatMs = Number(process.env.HEARTBEAT_INTERVAL_MS) || 90000;
19401
20445
  startHeartbeat(heartbeatMs);
19402
20446
  }
19403
20447
  if (process.env.OAUTH_KEEPALIVE_DISABLE !== "true") {
19404
- const { startOAuthKeepalive } = await import("./keepalive-62gfj91d.js");
20448
+ const { startOAuthKeepalive } = await import("./keepalive-3cps6e3e.js");
19405
20449
  startOAuthKeepalive();
19406
20450
  }
20451
+ if (process.env.OAUTH_REFRESH_SWEEP_DISABLE !== "true") {
20452
+ const { startOAuthRefreshSweep } = await import("./oauth-refresh-sweep-sycqtbph.js");
20453
+ startOAuthRefreshSweep();
20454
+ }
19407
20455
  startMcpOAuthPendingGc();
19408
20456
  startMemoryGc();
19409
- import("./boot-reembed-p0ny05t8.js").then(({ runBootReembed }) => runBootReembed()).catch((err) => {
20457
+ import("./boot-reembed-qata34xa.js").then(({ runBootReembed }) => runBootReembed()).catch((err) => {
19410
20458
  console.error("[boot-reembed] startup backfill failed (non-fatal):", err);
19411
20459
  });
19412
- import("./boot-reembed-69agkhv6.js").then(({ runBootReembedScripts }) => runBootReembedScripts()).catch((err) => {
20460
+ import("./boot-reembed-4jpxhb25.js").then(({ runBootReembedScripts }) => runBootReembedScripts()).catch((err) => {
19413
20461
  console.error("[boot-reembed-scripts] startup backfill failed (non-fatal):", err);
19414
20462
  });
19415
- import("./boot-scrub-logs-yybrkmvr.js").then(({ runBootScrubLogs }) => runBootScrubLogs()).catch((err) => {
20463
+ import("./boot-scrub-logs-tv48nn3z.js").then(({ runBootScrubLogs }) => runBootScrubLogs()).catch((err) => {
19416
20464
  console.error("[boot-scrub-logs] startup scrub failed (non-fatal):", err);
19417
20465
  });
19418
20466
  }).on("error", (err) => {