@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
@@ -0,0 +1,132 @@
1
+ import { getMcpServerById, getResolvedConfig } from "@/be/db";
2
+ import { McpHttpClient, type McpTool, type McpToolCallEnvelope } from "@/mcp-client/http-client";
3
+ import { ensureMcpToken } from "@/oauth/ensure-mcp-token";
4
+ import type { McpServer } from "@/types";
5
+ import { registerVolatileSecret } from "@/utils/secret-scrubber";
6
+
7
+ const MCP_PROXY_TIMEOUT_MS = 30_000;
8
+
9
+ function parseHeadersJson(value: string | null): Record<string, string> {
10
+ if (!value) return {};
11
+ try {
12
+ const parsed = JSON.parse(value) as unknown;
13
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
14
+ return Object.fromEntries(
15
+ Object.entries(parsed as Record<string, unknown>)
16
+ .filter((entry): entry is [string, string] => typeof entry[1] === "string")
17
+ .map(([key, headerValue]) => [key, headerValue]),
18
+ );
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ function resolveHeaderConfigKeys(
25
+ headerConfigKeys: string | null,
26
+ context: { agentId?: string; repoId?: string },
27
+ ): Record<string, string> {
28
+ if (!headerConfigKeys) return {};
29
+
30
+ let parsed: unknown;
31
+ try {
32
+ parsed = JSON.parse(headerConfigKeys);
33
+ } catch {
34
+ return {};
35
+ }
36
+
37
+ const configs = getResolvedConfig(context.agentId, context.repoId);
38
+ const configMap = new Map(configs.map((config) => [config.key, config.value]));
39
+ const resolved: Record<string, string> = {};
40
+
41
+ if (Array.isArray(parsed)) {
42
+ for (const item of parsed) {
43
+ if (typeof item !== "string") continue;
44
+ const value = configMap.get(item);
45
+ if (value === undefined) continue;
46
+ registerVolatileSecret(value, `mcp-header:${item}`);
47
+ resolved[item] = value;
48
+ }
49
+ return resolved;
50
+ }
51
+
52
+ if (!parsed || typeof parsed !== "object") return resolved;
53
+ for (const [headerName, configKey] of Object.entries(parsed as Record<string, unknown>)) {
54
+ if (typeof configKey !== "string") continue;
55
+ const value = configMap.get(configKey);
56
+ if (value === undefined) continue;
57
+ registerVolatileSecret(value, `mcp-header:${configKey}`);
58
+ resolved[headerName] = value;
59
+ }
60
+ return resolved;
61
+ }
62
+
63
+ async function resolveMcpHeaders(
64
+ server: McpServer,
65
+ context: { agentId?: string; repoId?: string },
66
+ ): Promise<Record<string, string>> {
67
+ const headers = {
68
+ ...parseHeadersJson(server.headers),
69
+ ...resolveHeaderConfigKeys(server.headerConfigKeys, context),
70
+ };
71
+
72
+ if (server.authMethod !== "oauth") return headers;
73
+
74
+ delete headers.Authorization;
75
+ delete headers.authorization;
76
+ const token = await ensureMcpToken(server.id);
77
+ if (!token) throw new Error("No OAuth token for this MCP server");
78
+ if (token.status !== "connected") {
79
+ throw new Error(token.lastErrorMessage ?? `OAuth status: ${token.status}`);
80
+ }
81
+
82
+ registerVolatileSecret(token.accessToken, `mcp-oauth:${server.id}`);
83
+ const rawType = token.tokenType || "Bearer";
84
+ const prefix = rawType.toLowerCase() === "bearer" ? "Bearer" : rawType;
85
+ headers.Authorization = `${prefix} ${token.accessToken}`;
86
+ return headers;
87
+ }
88
+
89
+ async function createMcpServerClient(
90
+ serverId: string,
91
+ context: { agentId?: string; repoId?: string; timeoutMs?: number } = {},
92
+ ): Promise<McpHttpClient> {
93
+ const server = getMcpServerById(serverId);
94
+ if (!server) throw new Error("MCP server not found");
95
+ if (!server.isEnabled) throw new Error("MCP server is disabled");
96
+ if (server.transport === "sse") {
97
+ throw new Error("SSE MCP servers are not supported yet — use a streamable HTTP server");
98
+ }
99
+ if (server.transport !== "http") {
100
+ throw new Error("Only streamable HTTP MCP servers are supported by script connections");
101
+ }
102
+ if (!server.url) throw new Error("MCP server URL is required");
103
+
104
+ const client = new McpHttpClient(server.url, "", "", undefined, {
105
+ clientInfo: { name: "agent-swarm-script-mcp-proxy", version: "1.0.0" },
106
+ omitEmptyAuthHeaders: true,
107
+ timeoutMs: context.timeoutMs ?? MCP_PROXY_TIMEOUT_MS,
108
+ });
109
+ client.useRawUrl = true;
110
+ client.customHeaders = await resolveMcpHeaders(server, context);
111
+ return client;
112
+ }
113
+
114
+ export async function listMcpServerTools(
115
+ serverId: string,
116
+ context: { agentId?: string; repoId?: string; timeoutMs?: number } = {},
117
+ ): Promise<McpTool[]> {
118
+ const client = await createMcpServerClient(serverId, context);
119
+ await client.initialize();
120
+ return client.listTools();
121
+ }
122
+
123
+ export async function callMcpServerTool(
124
+ serverId: string,
125
+ tool: string,
126
+ args: Record<string, unknown>,
127
+ context: { agentId?: string; repoId?: string; timeoutMs?: number } = {},
128
+ ): Promise<McpToolCallEnvelope> {
129
+ const client = await createMcpServerClient(serverId, context);
130
+ await client.initialize();
131
+ return client.callToolRaw(tool, args);
132
+ }
@@ -0,0 +1,79 @@
1
+ -- RBAC role engine storage (DES-445, increment 3).
2
+ -- Defines durable role grants for authenticated user admission. See the design
3
+ -- note at thoughts/taras/plans/2026-07-07-des-445-rbac-user-policy-admission-model.md.
4
+ --
5
+ -- Trigger fragility: trg_users_default_role depends on the users table. SQLite
6
+ -- silently drops triggers when a table is rebuilt with the create/copy/drop/
7
+ -- rename pattern, so any future users-table rebuild must recreate this trigger.
8
+ -- Phase 2's ensureRbacSeedsSynced boot path intentionally self-heals it as a
9
+ -- backstop, but migrations that rebuild users should still recreate it directly.
10
+ -- Built-in seed conflicts are surfaced by ensureRbacSeedsSynced at boot; that
11
+ -- is fatal when RBAC_ENABLED=true and logged/continued when the flag is off.
12
+
13
+ CREATE TABLE IF NOT EXISTS roles (
14
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
15
+ name TEXT NOT NULL UNIQUE,
16
+ description TEXT,
17
+ isBuiltin INTEGER NOT NULL DEFAULT 0,
18
+ grantsAll INTEGER NOT NULL DEFAULT 0,
19
+ createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ lastUpdatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
21
+ -- Future role-management API populates audit users; system writers leave NULL.
22
+ created_by TEXT REFERENCES users(id),
23
+ updated_by TEXT REFERENCES users(id)
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS role_permissions (
27
+ roleId TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
28
+ verb TEXT NOT NULL,
29
+ createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
30
+ -- Future role-management API populates audit users; system writers leave NULL.
31
+ created_by TEXT REFERENCES users(id),
32
+ updated_by TEXT REFERENCES users(id),
33
+ PRIMARY KEY (roleId, verb)
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS principal_roles (
37
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
38
+ principalType TEXT NOT NULL CHECK (principalType IN ('user','agent')),
39
+ principalId TEXT NOT NULL,
40
+ roleId TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
41
+ createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
42
+ -- Future role-management API populates audit users; system writers leave NULL.
43
+ created_by TEXT REFERENCES users(id),
44
+ updated_by TEXT REFERENCES users(id),
45
+ UNIQUE (principalType, principalId, roleId)
46
+ );
47
+ CREATE INDEX IF NOT EXISTS idx_principal_roles_principal
48
+ ON principal_roles(principalType, principalId);
49
+
50
+ -- Built-in roles: fixed ids so trigger/backfill SQL and code can reference them.
51
+ INSERT OR IGNORE INTO roles (id, name, description, isBuiltin, grantsAll) VALUES
52
+ ('rbac-role-admin', 'admin',
53
+ 'Full access including verb-less routes (legacy-equivalent default).', 1, 1),
54
+ ('rbac-role-requester', 'requester',
55
+ 'Own-task lifecycle: what legacy policy grants user principals.', 1, 0);
56
+
57
+ -- Initial snapshot of the requester verb-set; code (BUILTIN_ROLES in
58
+ -- src/be/rbac-roles.ts) is authoritative and re-syncs at boot.
59
+ INSERT OR IGNORE INTO role_permissions (roleId, verb) VALUES
60
+ ('rbac-role-requester', 'task.read.own'),
61
+ ('rbac-role-requester', 'task.cancel.own'),
62
+ ('rbac-role-requester', 'task.action.own'),
63
+ ('rbac-role-requester', 'task.fs.mutate');
64
+
65
+ -- Backfill: every existing user holds the default role.
66
+ INSERT OR IGNORE INTO principal_roles (principalType, principalId, roleId)
67
+ SELECT 'user', id, 'rbac-role-admin'
68
+ FROM users
69
+ WHERE EXISTS (SELECT 1 FROM roles WHERE id = 'rbac-role-admin');
70
+
71
+ -- Every future user row (createUser, findOrCreateUserByEmail, raw test
72
+ -- INSERTs) gets the default role atomically.
73
+ CREATE TRIGGER IF NOT EXISTS trg_users_default_role
74
+ AFTER INSERT ON users
75
+ BEGIN
76
+ INSERT OR IGNORE INTO principal_roles (principalType, principalId, roleId)
77
+ SELECT 'user', NEW.id, 'rbac-role-admin'
78
+ WHERE EXISTS (SELECT 1 FROM roles WHERE id = 'rbac-role-admin');
79
+ END;
@@ -0,0 +1,6 @@
1
+ ALTER TABLE script_credential_bindings
2
+ ADD COLUMN auth_kind TEXT NOT NULL DEFAULT 'config'
3
+ CHECK(auth_kind IN ('config', 'oauth'));
4
+
5
+ ALTER TABLE script_credential_bindings
6
+ ADD COLUMN oauth_provider TEXT;
@@ -0,0 +1,96 @@
1
+ DROP TABLE IF EXISTS script_connections_new;
2
+
3
+ CREATE TABLE script_connections_new (
4
+ id TEXT PRIMARY KEY,
5
+ slug TEXT NOT NULL,
6
+ display_name TEXT,
7
+ kind TEXT NOT NULL CHECK(kind IN ('raw', 'openapi', 'mcp', 'graphql')),
8
+ scope TEXT NOT NULL DEFAULT 'global' CHECK(scope IN ('global', 'agent', 'repo')),
9
+ scope_id TEXT,
10
+ base_url TEXT,
11
+ allowed_hosts_json TEXT NOT NULL DEFAULT '[]',
12
+ credential_binding_id TEXT REFERENCES script_credential_bindings(id) ON DELETE SET NULL,
13
+ openapi_spec_source_kind TEXT CHECK(openapi_spec_source_kind IN ('url', 'inline', 'agent_fs')),
14
+ openapi_spec_source TEXT,
15
+ openapi_spec_json TEXT,
16
+ openapi_spec_etag TEXT,
17
+ openapi_spec_fetched_at TEXT,
18
+ mcp_server_id TEXT REFERENCES mcp_servers(id) ON DELETE SET NULL,
19
+ generated_types TEXT,
20
+ generated_runtime_json TEXT,
21
+ generated_at TEXT,
22
+ generation_error TEXT,
23
+ enabled INTEGER NOT NULL DEFAULT 1,
24
+ version INTEGER NOT NULL DEFAULT 1,
25
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
27
+ created_by TEXT REFERENCES users(id),
28
+ updated_by TEXT REFERENCES users(id),
29
+ CHECK(scope = 'global' OR scope_id IS NOT NULL),
30
+ CHECK(kind != 'openapi' OR openapi_spec_json IS NOT NULL),
31
+ CHECK(kind != 'mcp' OR mcp_server_id IS NOT NULL)
32
+ );
33
+
34
+ INSERT INTO script_connections_new (
35
+ id,
36
+ slug,
37
+ display_name,
38
+ kind,
39
+ scope,
40
+ scope_id,
41
+ base_url,
42
+ allowed_hosts_json,
43
+ credential_binding_id,
44
+ openapi_spec_source_kind,
45
+ openapi_spec_source,
46
+ openapi_spec_json,
47
+ openapi_spec_etag,
48
+ openapi_spec_fetched_at,
49
+ mcp_server_id,
50
+ generated_types,
51
+ generated_runtime_json,
52
+ generated_at,
53
+ generation_error,
54
+ enabled,
55
+ version,
56
+ created_at,
57
+ updated_at,
58
+ created_by,
59
+ updated_by
60
+ )
61
+ SELECT
62
+ id,
63
+ slug,
64
+ display_name,
65
+ kind,
66
+ scope,
67
+ scope_id,
68
+ base_url,
69
+ allowed_hosts_json,
70
+ credential_binding_id,
71
+ openapi_spec_source_kind,
72
+ openapi_spec_source,
73
+ openapi_spec_json,
74
+ openapi_spec_etag,
75
+ openapi_spec_fetched_at,
76
+ mcp_server_id,
77
+ generated_types,
78
+ generated_runtime_json,
79
+ generated_at,
80
+ generation_error,
81
+ enabled,
82
+ version,
83
+ created_at,
84
+ updated_at,
85
+ created_by,
86
+ updated_by
87
+ FROM script_connections;
88
+
89
+ DROP TABLE script_connections;
90
+ ALTER TABLE script_connections_new RENAME TO script_connections;
91
+
92
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_script_connections_slug_scope
93
+ ON script_connections(slug, scope, COALESCE(scope_id, ''));
94
+
95
+ CREATE INDEX IF NOT EXISTS idx_script_connections_kind_enabled
96
+ ON script_connections(kind, enabled);
@@ -0,0 +1,9 @@
1
+ -- 113_task_routing_affinity.sql
2
+ -- Routing-affinity snapshot for interrupted/pooled tasks (routing-affinity
3
+ -- follow-up to DES-523). Nullable JSON blob: { sourceAgentId, role,
4
+ -- harnessProvider, capabilities }, always written/read whole. NULL = the
5
+ -- task is untagged and pool behavior is unchanged. See
6
+ -- `isAgentEligibleForTask` in src/be/db.ts and `RoutingAffinitySchema` in
7
+ -- src/types.ts.
8
+
9
+ ALTER TABLE agent_tasks ADD COLUMN routingAffinity TEXT;
@@ -0,0 +1,15 @@
1
+ -- 114_backfill_gpt_5_6_pricing.sql
2
+ -- Add GPT-5.6 Codex Sol/Terra/Luna pricing rows for existing deployments.
3
+ -- Fresh installs also run this after 046, so CODEX_MODEL_PRICING remains fully
4
+ -- represented at effective_from=0 without mutating already-applied migrations.
5
+
6
+ INSERT OR IGNORE INTO pricing (provider, model, token_class, effective_from, price_per_million_usd, createdAt, lastUpdatedAt) VALUES
7
+ ('codex', 'gpt-5.6-sol', 'input', 0, 5.0, 0, 0),
8
+ ('codex', 'gpt-5.6-sol', 'cached_input', 0, 0.5, 0, 0),
9
+ ('codex', 'gpt-5.6-sol', 'output', 0, 30.0, 0, 0),
10
+ ('codex', 'gpt-5.6-terra', 'input', 0, 2.5, 0, 0),
11
+ ('codex', 'gpt-5.6-terra', 'cached_input', 0, 0.25, 0, 0),
12
+ ('codex', 'gpt-5.6-terra', 'output', 0, 15.0, 0, 0),
13
+ ('codex', 'gpt-5.6-luna', 'input', 0, 1.0, 0, 0),
14
+ ('codex', 'gpt-5.6-luna', 'cached_input', 0, 0.1, 0, 0),
15
+ ('codex', 'gpt-5.6-luna', 'output', 0, 6.0, 0, 0);
@@ -50967,6 +50967,141 @@
50967
50967
  "cache_read": 1
50968
50968
  }
50969
50969
  }
50970
+ },
50971
+ "gpt-5.6-sol": {
50972
+ "id": "gpt-5.6-sol",
50973
+ "name": "GPT-5.6 Sol",
50974
+ "family": "gpt",
50975
+ "attachment": true,
50976
+ "reasoning": true,
50977
+ "reasoning_options": [
50978
+ {
50979
+ "type": "effort",
50980
+ "values": [
50981
+ "none",
50982
+ "low",
50983
+ "medium",
50984
+ "high",
50985
+ "xhigh",
50986
+ "max"
50987
+ ]
50988
+ }
50989
+ ],
50990
+ "tool_call": true,
50991
+ "structured_output": true,
50992
+ "temperature": false,
50993
+ "knowledge": "2026-02-16",
50994
+ "release_date": "2026-07-09",
50995
+ "last_updated": "2026-07-09",
50996
+ "modalities": {
50997
+ "input": [
50998
+ "text",
50999
+ "image"
51000
+ ],
51001
+ "output": [
51002
+ "text"
51003
+ ]
51004
+ },
51005
+ "open_weights": false,
51006
+ "limit": {
51007
+ "context": 1050000,
51008
+ "output": 128000
51009
+ },
51010
+ "cost": {
51011
+ "input": 5,
51012
+ "output": 30,
51013
+ "cache_read": 0.5
51014
+ }
51015
+ },
51016
+ "gpt-5.6-terra": {
51017
+ "id": "gpt-5.6-terra",
51018
+ "name": "GPT-5.6 Terra",
51019
+ "family": "gpt",
51020
+ "attachment": true,
51021
+ "reasoning": true,
51022
+ "reasoning_options": [
51023
+ {
51024
+ "type": "effort",
51025
+ "values": [
51026
+ "none",
51027
+ "low",
51028
+ "medium",
51029
+ "high",
51030
+ "xhigh",
51031
+ "max"
51032
+ ]
51033
+ }
51034
+ ],
51035
+ "tool_call": true,
51036
+ "structured_output": true,
51037
+ "temperature": false,
51038
+ "knowledge": "2026-02-16",
51039
+ "release_date": "2026-07-09",
51040
+ "last_updated": "2026-07-09",
51041
+ "modalities": {
51042
+ "input": [
51043
+ "text",
51044
+ "image"
51045
+ ],
51046
+ "output": [
51047
+ "text"
51048
+ ]
51049
+ },
51050
+ "open_weights": false,
51051
+ "limit": {
51052
+ "context": 1050000,
51053
+ "output": 128000
51054
+ },
51055
+ "cost": {
51056
+ "input": 2.5,
51057
+ "output": 15,
51058
+ "cache_read": 0.25
51059
+ }
51060
+ },
51061
+ "gpt-5.6-luna": {
51062
+ "id": "gpt-5.6-luna",
51063
+ "name": "GPT-5.6 Luna",
51064
+ "family": "gpt",
51065
+ "attachment": true,
51066
+ "reasoning": true,
51067
+ "reasoning_options": [
51068
+ {
51069
+ "type": "effort",
51070
+ "values": [
51071
+ "none",
51072
+ "low",
51073
+ "medium",
51074
+ "high",
51075
+ "xhigh",
51076
+ "max"
51077
+ ]
51078
+ }
51079
+ ],
51080
+ "tool_call": true,
51081
+ "structured_output": true,
51082
+ "temperature": false,
51083
+ "knowledge": "2026-02-16",
51084
+ "release_date": "2026-07-09",
51085
+ "last_updated": "2026-07-09",
51086
+ "modalities": {
51087
+ "input": [
51088
+ "text",
51089
+ "image"
51090
+ ],
51091
+ "output": [
51092
+ "text"
51093
+ ]
51094
+ },
51095
+ "open_weights": false,
51096
+ "limit": {
51097
+ "context": 1050000,
51098
+ "output": 128000
51099
+ },
51100
+ "cost": {
51101
+ "input": 1,
51102
+ "output": 6,
51103
+ "cache_read": 0.1
51104
+ }
50970
51105
  }
50971
51106
  }
50972
51107
  },
@@ -0,0 +1,35 @@
1
+ import { ensureTokenOrThrow, oauthAppRowToProviderConfig } from "@/oauth/ensure-token";
2
+ import type { OAuthProviderConfig } from "@/oauth/wrapper";
3
+ import type { OAuthApp } from "@/tracker/types";
4
+ import { getOAuthApp, getOAuthTokens, isTokenExpiringSoon } from "./db-queries/oauth";
5
+
6
+ export type OAuthBindingTokenStatus = "ok" | "expiring" | "missing";
7
+
8
+ const OAUTH_BINDING_REFRESH_BUFFER_MS = 5 * 60 * 1000;
9
+
10
+ export function oauthAppToProviderConfig(app: OAuthApp): OAuthProviderConfig {
11
+ return oauthAppRowToProviderConfig(app);
12
+ }
13
+
14
+ export function getOAuthProviderConfig(provider: string): OAuthProviderConfig | null {
15
+ const app = getOAuthApp(provider);
16
+ return app ? oauthAppRowToProviderConfig(app) : null;
17
+ }
18
+
19
+ export function getOAuthBindingTokenStatus(provider: string): OAuthBindingTokenStatus {
20
+ if (!getOAuthApp(provider)) return "missing";
21
+ if (!getOAuthTokens(provider)) return "missing";
22
+ return isTokenExpiringSoon(provider, OAUTH_BINDING_REFRESH_BUFFER_MS) ? "expiring" : "ok";
23
+ }
24
+
25
+ export async function resolveOAuthBindingToken(provider: string): Promise<string | undefined> {
26
+ if (!getOAuthApp(provider)) return undefined;
27
+ const initialTokens = getOAuthTokens(provider);
28
+ if (!initialTokens) return undefined;
29
+
30
+ if (isTokenExpiringSoon(provider, OAUTH_BINDING_REFRESH_BUFFER_MS)) {
31
+ await ensureTokenOrThrow(provider, OAUTH_BINDING_REFRESH_BUFFER_MS);
32
+ }
33
+
34
+ return getOAuthTokens(provider)?.accessToken;
35
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Background OAuth token refresh sweep.
3
+ *
4
+ * The reactive path (src/oauth/ensure-token.ts) only refreshes tokens when a
5
+ * binding is about to be USED, so rotation / inactivity-TTL refresh tokens can
6
+ * die while a connection sits idle. This sweep walks every oauth_tokens row on
7
+ * a 15-minute tick and refreshes when either:
8
+ *
9
+ * (a) the access token expires within 30 minutes, or
10
+ * (b) the row hasn't been touched in > 7 days (keep-alive for providers that
11
+ * expire refresh tokens after an inactivity window, e.g. Atlassian ~90d).
12
+ *
13
+ * Relationship to src/oauth/keepalive.ts: that job is a 12-hour alerting
14
+ * keepalive hardcoded to the tracker providers (linear/jira). This sweep is
15
+ * the generic counterpart covering every provider registered in oauth_apps
16
+ * (script-connection credential bindings included). Both funnel through the
17
+ * same per-provider locks in ensure-token, so overlap is safe.
18
+ */
19
+ import { forceRefreshTokenOrThrow } from "../oauth/ensure-token";
20
+ import { scrubSecrets } from "../utils/secret-scrubber";
21
+ import { getOAuthTokens, listOAuthTokenSweepRows } from "./db-queries/oauth";
22
+
23
+ const EXPIRY_BUFFER_MS = 30 * 60 * 1000; // (a) refresh when expiring within 30 min
24
+ const STALE_ROW_MS = 7 * 24 * 60 * 60 * 1000; // (b) keep-alive when untouched > 7 days
25
+ const SWEEP_INTERVAL_MS = 15 * 60 * 1000;
26
+ const SWEEP_STARTUP_DELAY_MS = 60 * 1000;
27
+
28
+ export type OAuthRefreshSweepResult = {
29
+ checked: number;
30
+ refreshed: number;
31
+ skipped: number;
32
+ failed: string[];
33
+ };
34
+
35
+ /**
36
+ * Run one sweep over all stored OAuth tokens. Never throws — per-provider
37
+ * failures are collected in `failed` (and logged, scrubbed) so one dead
38
+ * provider can't starve the rest.
39
+ */
40
+ export async function sweepOAuthTokenRefresh(): Promise<OAuthRefreshSweepResult> {
41
+ let checked = 0;
42
+ let refreshed = 0;
43
+ let skipped = 0;
44
+ const failed: string[] = [];
45
+
46
+ for (const row of listOAuthTokenSweepRows()) {
47
+ checked++;
48
+
49
+ if (!row.hasApp || !row.hasRefreshToken) {
50
+ skipped++;
51
+ continue;
52
+ }
53
+
54
+ const now = Date.now();
55
+ const expiringSoon = new Date(row.expiresAt).getTime() - now < EXPIRY_BUFFER_MS;
56
+ const stale = now - new Date(row.updatedAt).getTime() > STALE_ROW_MS;
57
+ if (!expiringSoon && !stale) {
58
+ skipped++;
59
+ continue;
60
+ }
61
+
62
+ const before = getOAuthTokens(row.provider);
63
+ try {
64
+ await forceRefreshTokenOrThrow(row.provider);
65
+ const after = getOAuthTokens(row.provider);
66
+ // forceRefreshTokenOrThrow stays silent on some no-op paths (row changed
67
+ // concurrently, config vanished mid-flight) — only count real refreshes.
68
+ const changed =
69
+ after !== null &&
70
+ (before === null ||
71
+ before.accessToken !== after.accessToken ||
72
+ before.expiresAt !== after.expiresAt ||
73
+ before.updatedAt !== after.updatedAt);
74
+ if (changed) {
75
+ refreshed++;
76
+ } else {
77
+ skipped++;
78
+ }
79
+ } catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ failed.push(`${row.provider}: ${message}`);
82
+ console.warn(
83
+ scrubSecrets(`[oauth-refresh-sweep] ${row.provider} refresh failed: ${message}`),
84
+ );
85
+ }
86
+ }
87
+
88
+ return { checked, refreshed, skipped, failed };
89
+ }
90
+
91
+ // ─── Periodic runner ─────────────────────────────────────────────────────────
92
+
93
+ let sweepInterval: ReturnType<typeof setInterval> | null = null;
94
+ let startupTimeout: ReturnType<typeof setTimeout> | null = null;
95
+ let inflightSweep: Promise<void> | null = null;
96
+
97
+ async function runSweepTick(trigger: "startup" | "interval"): Promise<void> {
98
+ if (inflightSweep) return inflightSweep;
99
+ inflightSweep = (async () => {
100
+ try {
101
+ const result = await sweepOAuthTokenRefresh();
102
+ // Stay quiet on no-op ticks; one concise line when something happened.
103
+ if (result.refreshed > 0 || result.failed.length > 0) {
104
+ console.log(
105
+ scrubSecrets(
106
+ `[oauth-refresh-sweep] ${trigger}: checked=${result.checked} refreshed=${result.refreshed} skipped=${result.skipped} failed=${result.failed.length}${result.failed.length > 0 ? ` (${result.failed.join("; ")})` : ""}`,
107
+ ),
108
+ );
109
+ }
110
+ } catch (err) {
111
+ // sweepOAuthTokenRefresh never throws by contract; belt and braces.
112
+ console.error("[oauth-refresh-sweep] sweep tick failed:", err);
113
+ } finally {
114
+ inflightSweep = null;
115
+ }
116
+ })();
117
+ return inflightSweep;
118
+ }
119
+
120
+ /**
121
+ * Start the periodic OAuth refresh sweep: first run ~1 minute after boot,
122
+ * then every 15 minutes. No-op when OAUTH_REFRESH_SWEEP_DISABLE=true or when
123
+ * already running.
124
+ */
125
+ export function startOAuthRefreshSweep(): void {
126
+ if (process.env.OAUTH_REFRESH_SWEEP_DISABLE === "true") {
127
+ console.log("[oauth-refresh-sweep] Disabled via OAUTH_REFRESH_SWEEP_DISABLE");
128
+ return;
129
+ }
130
+ if (sweepInterval || startupTimeout) return;
131
+
132
+ console.log(
133
+ `[oauth-refresh-sweep] Starting (interval ${Math.round(SWEEP_INTERVAL_MS / 60_000)}min, first run in ${Math.round(SWEEP_STARTUP_DELAY_MS / 1000)}s)`,
134
+ );
135
+
136
+ startupTimeout = setTimeout(() => {
137
+ startupTimeout = null;
138
+ void runSweepTick("startup");
139
+ }, SWEEP_STARTUP_DELAY_MS);
140
+ startupTimeout.unref?.();
141
+
142
+ sweepInterval = setInterval(() => {
143
+ void runSweepTick("interval");
144
+ }, SWEEP_INTERVAL_MS);
145
+ sweepInterval.unref?.();
146
+ }
147
+
148
+ /** Stop the periodic sweep and wait for any in-flight run to finish. */
149
+ export async function stopOAuthRefreshSweep(): Promise<void> {
150
+ if (startupTimeout) {
151
+ clearTimeout(startupTimeout);
152
+ startupTimeout = null;
153
+ }
154
+ if (sweepInterval) {
155
+ clearInterval(sweepInterval);
156
+ sweepInterval = null;
157
+ }
158
+ if (inflightSweep) await inflightSweep;
159
+ }