@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,194 @@
1
+ /**
2
+ * Minimal MCP client for Streamable HTTP transport.
3
+ *
4
+ * Performs the initialize handshake, tracks MCP session IDs, parses JSON/SSE
5
+ * responses, discovers tools, and forwards tool calls. The client itself is
6
+ * transport-only: callers provide any auth headers and it never imports DB code.
7
+ */
8
+
9
+ export interface McpTool {
10
+ name: string;
11
+ description?: string;
12
+ inputSchema: Record<string, unknown>;
13
+ }
14
+
15
+ export interface McpToolCallResult {
16
+ content: Array<{ type: string; text?: string }>;
17
+ isError?: boolean;
18
+ }
19
+
20
+ export type McpJsonRpcError = {
21
+ code?: number;
22
+ message?: string;
23
+ data?: unknown;
24
+ };
25
+
26
+ export type McpToolCallEnvelope =
27
+ | { ok: true; result: McpToolCallResult }
28
+ | { ok: false; error: McpJsonRpcError | string };
29
+
30
+ function jsonRpcErrorMessage(error: McpJsonRpcError | string): string {
31
+ if (typeof error === "string") return error;
32
+ return error.message ?? JSON.stringify(error);
33
+ }
34
+
35
+ type McpHttpClientOptions = {
36
+ protocolVersion?: string;
37
+ clientInfo?: { name: string; version: string };
38
+ timeoutMs?: number;
39
+ omitEmptyAuthHeaders?: boolean;
40
+ };
41
+
42
+ export class McpHttpClient {
43
+ private sessionId: string | null = null;
44
+ private nextId = 1;
45
+ /** Additional headers merged into every request (e.g. for installed MCP servers) */
46
+ public customHeaders: Record<string, string> = {};
47
+ /**
48
+ * When true, baseUrl is used as-is for requests (external MCP servers).
49
+ * When false (default), /mcp is appended (swarm convention).
50
+ */
51
+ public useRawUrl = false;
52
+
53
+ constructor(
54
+ private baseUrl: string,
55
+ private apiKey: string,
56
+ private agentId: string,
57
+ private taskId?: string,
58
+ private options: McpHttpClientOptions = {},
59
+ ) {}
60
+
61
+ private async send(body: unknown): Promise<{ data: unknown; headers: Headers }> {
62
+ const headers: Record<string, string> = {
63
+ "Content-Type": "application/json",
64
+ Accept: "application/json, text/event-stream",
65
+ };
66
+ if (this.apiKey || !this.options.omitEmptyAuthHeaders) {
67
+ headers.Authorization = `Bearer ${this.apiKey}`;
68
+ }
69
+ if (this.agentId || !this.options.omitEmptyAuthHeaders) {
70
+ headers["X-Agent-ID"] = this.agentId;
71
+ }
72
+ Object.assign(headers, this.customHeaders);
73
+ if (this.taskId) {
74
+ headers["X-Source-Task-Id"] = this.taskId;
75
+ }
76
+ if (this.sessionId) {
77
+ headers["mcp-session-id"] = this.sessionId;
78
+ }
79
+
80
+ const controller = this.options.timeoutMs ? new AbortController() : null;
81
+ const timeout = controller
82
+ ? setTimeout(() => controller.abort(), this.options.timeoutMs)
83
+ : null;
84
+ const url = this.useRawUrl ? this.baseUrl : `${this.baseUrl}/mcp`;
85
+ try {
86
+ const res = await fetch(url, {
87
+ method: "POST",
88
+ headers,
89
+ body: JSON.stringify(body),
90
+ signal: controller?.signal,
91
+ });
92
+
93
+ if (!res.ok) {
94
+ throw new Error(`MCP request failed: ${res.status} ${res.statusText}`);
95
+ }
96
+
97
+ // Capture session ID from response.
98
+ const sid = res.headers.get("mcp-session-id");
99
+ if (sid) this.sessionId = sid;
100
+
101
+ // Handle SSE responses (extract JSON from event stream).
102
+ const contentType = res.headers.get("content-type") || "";
103
+ if (contentType.includes("text/event-stream")) {
104
+ const text = await res.text();
105
+ const dataLines = text
106
+ .split("\n")
107
+ .filter((line) => line.startsWith("data: "))
108
+ .map((line) => line.slice(6));
109
+ const lastData = dataLines[dataLines.length - 1];
110
+ return { data: lastData ? JSON.parse(lastData) : null, headers: res.headers };
111
+ }
112
+
113
+ const text = await res.text();
114
+ const data = text.trim() ? JSON.parse(text) : null;
115
+ return { data, headers: res.headers };
116
+ } catch (err) {
117
+ if (err instanceof DOMException && err.name === "AbortError") {
118
+ throw new Error(`MCP request timed out after ${this.options.timeoutMs}ms`);
119
+ }
120
+ throw err;
121
+ } finally {
122
+ if (timeout) clearTimeout(timeout);
123
+ }
124
+ }
125
+
126
+ /** Perform MCP initialize + initialized handshake */
127
+ async initialize(): Promise<void> {
128
+ const { data } = await this.send({
129
+ jsonrpc: "2.0",
130
+ id: this.nextId++,
131
+ method: "initialize",
132
+ params: {
133
+ protocolVersion: this.options.protocolVersion ?? "2025-03-26",
134
+ capabilities: {},
135
+ clientInfo: this.options.clientInfo ?? {
136
+ name: "agent-swarm-pi-mono",
137
+ version: "1.0.0",
138
+ },
139
+ },
140
+ });
141
+
142
+ if (!data || typeof data !== "object" || !("result" in (data as Record<string, unknown>))) {
143
+ throw new Error(`MCP initialize failed: ${JSON.stringify(data)}`);
144
+ }
145
+
146
+ // Send initialized notification (no response expected for notifications).
147
+ await this.send({
148
+ jsonrpc: "2.0",
149
+ method: "notifications/initialized",
150
+ });
151
+ }
152
+
153
+ /** Discover all available MCP tools */
154
+ async listTools(): Promise<McpTool[]> {
155
+ const { data } = await this.send({
156
+ jsonrpc: "2.0",
157
+ id: this.nextId++,
158
+ method: "tools/list",
159
+ params: {},
160
+ });
161
+
162
+ const result = data as { result?: { tools?: McpTool[] }; error?: McpJsonRpcError | string };
163
+ if (result?.error) {
164
+ throw new Error(`MCP tools/list failed: ${jsonRpcErrorMessage(result.error)}`);
165
+ }
166
+ return result?.result?.tools ?? [];
167
+ }
168
+
169
+ /** Call an MCP tool and preserve either the JSON-RPC result or error shape. */
170
+ async callToolRaw(name: string, args: Record<string, unknown>): Promise<McpToolCallEnvelope> {
171
+ const { data } = await this.send({
172
+ jsonrpc: "2.0",
173
+ id: this.nextId++,
174
+ method: "tools/call",
175
+ params: { name, arguments: args },
176
+ });
177
+
178
+ const response = data as {
179
+ result?: McpToolCallResult;
180
+ error?: McpJsonRpcError | string;
181
+ };
182
+ if (response?.error) return { ok: false, error: response.error };
183
+ return {
184
+ ok: true,
185
+ result: response?.result ?? { content: [{ type: "text", text: "No result" }] },
186
+ };
187
+ }
188
+
189
+ /** Call an MCP tool */
190
+ async callTool(name: string, args: Record<string, unknown>): Promise<McpToolCallResult> {
191
+ const envelope = await this.callToolRaw(name, args);
192
+ return envelope.ok ? envelope.result : { content: [{ type: "text", text: "No result" }] };
193
+ }
194
+ }
@@ -0,0 +1,16 @@
1
+ import { assertUrlSafe, publicEndpointSsrfOptions } from "./mcp-wrapper";
2
+
3
+ export const RESERVED_OAUTH_PROVIDERS = new Set(["linear", "jira"]);
4
+
5
+ export function assertOAuthProviderIsNotReserved(provider: string): void {
6
+ if (!RESERVED_OAUTH_PROVIDERS.has(provider.toLowerCase())) return;
7
+ throw new Error(
8
+ `OAuth provider "${provider}" is reserved for dedicated tracker OAuth flows. Use the Linear/Jira tracker integration flow instead.`,
9
+ );
10
+ }
11
+
12
+ export function assertOAuthAppUrlsSafe(input: { authorizeUrl: string; tokenUrl: string }): void {
13
+ const options = publicEndpointSsrfOptions();
14
+ assertUrlSafe(input.authorizeUrl, options);
15
+ assertUrlSafe(input.tokenUrl, options);
16
+ }
@@ -5,35 +5,70 @@ import {
5
5
  isTokenExpiringSoon,
6
6
  releaseOAuthRefreshLock,
7
7
  } from "../be/db-queries/oauth";
8
- import type { OAuthTokens } from "../tracker/types";
8
+ import type { OAuthApp, OAuthTokens } from "../tracker/types";
9
9
  import { type OAuthProviderConfig, refreshAccessToken } from "./wrapper";
10
10
 
11
- const refreshLocks = new Map<string, Promise<void>>();
12
- const REFRESH_LOCK_TTL_MS = 2 * 60 * 1000;
13
- const REFRESH_LOCK_WAIT_MS = 30 * 1000;
14
- const REFRESH_LOCK_POLL_MS = 250;
11
+ function parseMetadata(metadataJson: string | null | undefined): Record<string, unknown> {
12
+ try {
13
+ const parsed = JSON.parse(metadataJson || "{}");
14
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ function stringRecord(value: unknown): Record<string, string> | undefined {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
22
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, String(item)]));
23
+ }
15
24
 
16
25
  /**
17
- * Build an OAuthProviderConfig from the oauth_apps table for any provider.
26
+ * Map an oauth_apps row to a provider config. Single source of truth for the
27
+ * token-endpoint behavior knobs stored in metadata (extraParams,
28
+ * tokenAuthStyle, tokenBodyFormat) — used by ensure-token and re-exported by
29
+ * be/oauth-credential-bindings (which can't be imported from here without a
30
+ * cycle).
18
31
  */
19
- function getOAuthConfig(provider: string): OAuthProviderConfig | null {
20
- const app = getOAuthApp(provider);
21
- if (!app) return null;
32
+ export function oauthAppRowToProviderConfig(app: OAuthApp): OAuthProviderConfig {
33
+ const metadata = parseMetadata(app.metadata);
34
+ const extraParams =
35
+ stringRecord(metadata.extraParams) ??
36
+ (typeof metadata.actor === "string" ? { actor: metadata.actor } : undefined);
22
37
 
23
- const metadata = JSON.parse(app.metadata || "{}");
24
38
  return {
25
- provider,
39
+ provider: app.provider,
26
40
  clientId: app.clientId,
27
41
  clientSecret: app.clientSecret,
28
42
  authorizeUrl: app.authorizeUrl,
29
43
  tokenUrl: app.tokenUrl,
30
44
  redirectUri: app.redirectUri,
31
- scopes: app.scopes.split(","),
32
- extraParams: metadata.extraParams ?? (metadata.actor ? { actor: metadata.actor } : undefined),
33
- requiresRefreshTokenRotation: provider === "jira",
45
+ scopes: app.scopes
46
+ .split(",")
47
+ .map((scope) => scope.trim())
48
+ .filter(Boolean),
49
+ extraParams,
50
+ // Standard OAuth wants space-separated scopes; the wrapper's comma
51
+ // default exists only for Linear, which has its own dedicated flow.
52
+ scopeSeparator: " ",
53
+ ...(metadata.tokenAuthStyle === "basic" ? { tokenAuthStyle: "basic" as const } : {}),
54
+ ...(metadata.tokenBodyFormat === "json" ? { tokenBodyFormat: "json" as const } : {}),
55
+ requiresRefreshTokenRotation: app.provider === "jira",
34
56
  };
35
57
  }
36
58
 
59
+ const refreshLocks = new Map<string, Promise<void>>();
60
+ const REFRESH_LOCK_TTL_MS = 2 * 60 * 1000;
61
+ const REFRESH_LOCK_WAIT_MS = 30 * 1000;
62
+ const REFRESH_LOCK_POLL_MS = 250;
63
+
64
+ /**
65
+ * Build an OAuthProviderConfig from the oauth_apps table for any provider.
66
+ */
67
+ function getOAuthConfig(provider: string): OAuthProviderConfig | null {
68
+ const app = getOAuthApp(provider);
69
+ return app ? oauthAppRowToProviderConfig(app) : null;
70
+ }
71
+
37
72
  async function withProviderRefreshLock<T>(provider: string, fn: () => Promise<T>): Promise<T> {
38
73
  const previous = refreshLocks.get(provider) ?? Promise.resolve();
39
74
  let release!: () => void;
@@ -94,6 +129,26 @@ export async function ensureToken(provider: string, bufferMs?: number): Promise<
94
129
  }
95
130
  }
96
131
 
132
+ /**
133
+ * Buffer large enough that any stored expiry counts as "expiring soon",
134
+ * turning {@link ensureTokenOrThrow} into an unconditional refresh.
135
+ */
136
+ const FORCE_REFRESH_BUFFER_MS = 1000 * 60 * 60 * 24 * 365 * 100; // ~100 years
137
+
138
+ /**
139
+ * Force a refresh of the stored token regardless of remaining lifetime,
140
+ * going through the exact same in-process queue + DB refresh-lock path as
141
+ * {@link ensureTokenOrThrow}. Used by the manual refresh endpoint and the
142
+ * background refresh sweep's keep-alive path.
143
+ *
144
+ * Like {@link ensureTokenOrThrow}, stays silent (no throw) when the provider
145
+ * has no app config or no stored refresh token — callers that need those to
146
+ * be errors must pre-check.
147
+ */
148
+ export async function forceRefreshTokenOrThrow(provider: string): Promise<void> {
149
+ await ensureTokenOrThrow(provider, FORCE_REFRESH_BUFFER_MS);
150
+ }
151
+
97
152
  /**
98
153
  * Strict variant of {@link ensureToken}: throws on refresh failure of a
99
154
  * configured provider so callers (keepalive, alerting) can react.
@@ -96,6 +96,21 @@ export function assertUrlSafe(rawUrl: string, opts: SsrfGuardOptions = {}): URL
96
96
  return parsed;
97
97
  }
98
98
 
99
+ export function publicEndpointSsrfOptions(): SsrfGuardOptions {
100
+ // Fail closed: NODE_ENV is typically UNSET in the production image (Bun
101
+ // does not default it), so only an explicit development/test environment —
102
+ // or the explicit override flag — may fetch private/insecure endpoints.
103
+ // Local dev entrypoints (`bun run dev:http` / `start:http`) set
104
+ // NODE_ENV=development.
105
+ const env = process.env.NODE_ENV;
106
+ const dev =
107
+ env === "development" || env === "test" || process.env.ALLOW_PRIVATE_NETWORK_URLS === "true";
108
+ return {
109
+ allowPrivateHosts: dev,
110
+ allowInsecure: dev,
111
+ };
112
+ }
113
+
99
114
  function defaultSsrfOptions(): SsrfGuardOptions {
100
115
  return {
101
116
  allowPrivateHosts: process.env.MCP_OAUTH_ALLOW_PRIVATE_HOSTS === "true",
@@ -29,6 +29,20 @@ export interface OAuthProviderConfig {
29
29
  * pre-existing consumer of this wrapper.
30
30
  */
31
31
  scopeSeparator?: string;
32
+ /**
33
+ * How client credentials are sent to the token endpoint.
34
+ *
35
+ * - `"body"` (default): `client_id` + `client_secret` as body parameters.
36
+ * - `"basic"`: HTTP Basic `Authorization` header (RFC 6749 §2.3.1) —
37
+ * required by providers like Notion that reject body credentials.
38
+ */
39
+ tokenAuthStyle?: "body" | "basic";
40
+ /**
41
+ * Token request body encoding. `"form"` (default) sends
42
+ * `application/x-www-form-urlencoded`; `"json"` sends a JSON body
43
+ * (Notion requires JSON).
44
+ */
45
+ tokenBodyFormat?: "form" | "json";
32
46
  }
33
47
 
34
48
  interface PendingState {
@@ -97,6 +111,30 @@ export async function buildAuthorizationUrl(
97
111
  * Validates the state against our pending map, calls the token endpoint,
98
112
  * and persists tokens via storeOAuthTokens().
99
113
  */
114
+ /**
115
+ * Build headers + body for a token-endpoint request, honoring the provider's
116
+ * client-auth style (body params vs HTTP Basic) and body encoding (form vs JSON).
117
+ */
118
+ function tokenRequestInit(
119
+ config: OAuthProviderConfig,
120
+ params: Record<string, string>,
121
+ ): { headers: Record<string, string>; body: string } {
122
+ const useBasic = config.tokenAuthStyle === "basic";
123
+ const bodyParams = useBasic
124
+ ? params
125
+ : { ...params, client_id: config.clientId, client_secret: config.clientSecret };
126
+ const headers: Record<string, string> = {};
127
+ if (useBasic) {
128
+ headers.Authorization = `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64")}`;
129
+ }
130
+ if (config.tokenBodyFormat === "json") {
131
+ headers["Content-Type"] = "application/json";
132
+ return { headers, body: JSON.stringify(bodyParams) };
133
+ }
134
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
135
+ return { headers, body: new URLSearchParams(bodyParams).toString() };
136
+ }
137
+
100
138
  export async function exchangeCode(
101
139
  config: OAuthProviderConfig,
102
140
  code: string,
@@ -111,19 +149,14 @@ export async function exchangeCode(
111
149
  const { codeVerifier } = pending;
112
150
 
113
151
  // Build token request manually — Linear doesn't use standard OAuth discovery
114
- const body = new URLSearchParams({
115
- grant_type: "authorization_code",
116
- client_id: config.clientId,
117
- client_secret: config.clientSecret,
118
- redirect_uri: config.redirectUri,
119
- code,
120
- code_verifier: codeVerifier,
121
- });
122
-
123
152
  const response = await fetch(config.tokenUrl, {
124
153
  method: "POST",
125
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
126
- body: body.toString(),
154
+ ...tokenRequestInit(config, {
155
+ grant_type: "authorization_code",
156
+ redirect_uri: config.redirectUri,
157
+ code,
158
+ code_verifier: codeVerifier,
159
+ }),
127
160
  });
128
161
 
129
162
  if (!response.ok) {
@@ -167,17 +200,12 @@ export async function refreshAccessToken(
167
200
  config: OAuthProviderConfig,
168
201
  refreshToken: string,
169
202
  ): Promise<{ accessToken: string; refreshToken?: string; expiresIn?: number; scope?: string }> {
170
- const body = new URLSearchParams({
171
- grant_type: "refresh_token",
172
- client_id: config.clientId,
173
- client_secret: config.clientSecret,
174
- refresh_token: refreshToken,
175
- });
176
-
177
203
  const response = await fetch(config.tokenUrl, {
178
204
  method: "POST",
179
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
180
- body: body.toString(),
205
+ ...tokenRequestInit(config, {
206
+ grant_type: "refresh_token",
207
+ refresh_token: refreshToken,
208
+ }),
181
209
  });
182
210
 
183
211
  if (!response.ok) {
@@ -420,9 +420,9 @@ The 5 script tools (\`script-search\`, \`script-run\`, \`script-upsert\`, \`scri
420
420
  - \`agentId\` IS propagated to scripts via the \`X-Agent-ID\` header.
421
421
  - \`taskId\` is NOT propagated to scripts — there is no ambient task context. Pass \`taskId\` explicitly via \`args\` if the script needs to call \`ctx.swarm.task_storeProgress\`.
422
422
  - Use \`script-query-types\` to inspect the live \`swarm-sdk.d.ts\` before authoring a complex script.
423
- - Typed API connections: a lead-registered \`script_connections\` entry exposes a typed \`ctx.api.<slug>.<method>(...)\` client in scripts.
424
- - \`ctx.api\` clients auto-inject the configured credential at egress, so scripts calling an allow-listed external API should prefer \`ctx.api\` over hand-written \`[REDACTED:<KEY>]\` Authorization headers.
425
- - Registration is lead-only; workers should document the connection spec and hand it to the lead to register.
423
+ - Script connections: lead-registered \`script_connections\` entries expose typed clients in scripts — \`ctx.api.<slug>.<operationId>(...)\` (OpenAPI), \`ctx.api.<slug>.graphql(query, vars)\` (GraphQL), and \`ctx.mcp.<slug>.<toolName>(args)\` (MCP, proxied server-side). Introspect with \`Object.keys(ctx.api ?? {})\` / \`Object.keys(ctx.mcp ?? {})\`.
424
+ - Connection credentials (config secrets or OAuth tokens with auto-refresh) are injected at egress, so scripts calling an external API should prefer a connection over hand-written \`fetch\` + \`[REDACTED:<KEY>]\` headers — and never paste raw secrets.
425
+ - Registration is lead-only (\`script-connections\` + \`credential-bindings\` tools); workers should hand the lead the spec to register: slug, baseUrl or spec URL, allowedHosts, and auth (config key vs OAuth provider).
426
426
  `,
427
427
  variables: [],
428
428
  category: "system",
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Codex API-addressable models, verified from https://developers.openai.com/codex/models
3
- * and https://developers.openai.com/api/docs/deprecations as of 2026-05-28.
2
+ * Codex API-addressable models, verified from https://developers.openai.com/api/docs/models
3
+ * and https://developers.openai.com/api/docs/deprecations as of 2026-07-10.
4
4
  *
5
5
  * NOTE: `gpt-5.3-codex-spark` is intentionally excluded. It is a ChatGPT Pro
6
6
  * research preview and is NOT API-addressable via the Codex SDK at launch.
@@ -10,6 +10,7 @@
10
10
  * adapter so the onboarding UI and model selector can import it without
11
11
  * pulling in the SDK.
12
12
  */
13
+ import modelsDevCache from "../be/modelsdev-cache.json";
13
14
 
14
15
  /**
15
16
  * List of Codex models we know about (drives the onboarding model selector,
@@ -18,17 +19,20 @@
18
19
  * SDK, so new OpenAI models work without a code change.
19
20
  */
20
21
  export const CODEX_MODELS = [
21
- "gpt-5.5", // newest frontier coding/professional-work model, 1.05M context
22
- "gpt-5.4", // default mainline reasoning model w/ frontier coding
22
+ "gpt-5.6-sol", // frontier GPT-5.6 tier for complex reasoning/coding
23
+ "gpt-5.6-terra", // balanced GPT-5.6 tier
24
+ "gpt-5.6-luna", // fast/cheap GPT-5.6 tier for high-volume workloads
25
+ "gpt-5.5", // previous frontier coding/professional-work model
26
+ "gpt-5.4", // previous mainline reasoning model w/ frontier coding
23
27
  "gpt-5.4-mini", // faster/cheaper
24
- "gpt-5.3-codex", // coding-specialized, 1M context
28
+ "gpt-5.3-codex", // coding-specialized legacy model
25
29
  "gpt-5.2-codex", // legacy — scheduled for retirement, see openai deprecations page
26
30
  ] as const;
27
31
 
28
32
  export type CodexModel = (typeof CODEX_MODELS)[number];
29
33
 
30
34
  /** The baseline default when neither MODEL_OVERRIDE nor task.model is set. */
31
- export const CODEX_DEFAULT_MODEL: CodexModel = "gpt-5.4";
35
+ export const CODEX_DEFAULT_MODEL: CodexModel = "gpt-5.6-terra";
32
36
 
33
37
  /**
34
38
  * Map claude-style shortnames (that flow through MODEL_OVERRIDE / task.model)
@@ -36,10 +40,10 @@ export const CODEX_DEFAULT_MODEL: CodexModel = "gpt-5.4";
36
40
  * a task authored for Claude works unchanged when pointed at a Codex worker.
37
41
  */
38
42
  const CLAUDE_SHORTNAMES: Record<string, CodexModel> = {
39
- fable: "gpt-5.5",
40
- opus: "gpt-5.4",
41
- sonnet: "gpt-5.4",
42
- haiku: "gpt-5.4-mini",
43
+ fable: "gpt-5.6-sol",
44
+ opus: "gpt-5.6-sol",
45
+ sonnet: "gpt-5.6-terra",
46
+ haiku: "gpt-5.6-luna",
43
47
  };
44
48
 
45
49
  /**
@@ -56,22 +60,51 @@ export function resolveCodexModel(modelStr: string | undefined): string {
56
60
  return CLAUDE_SHORTNAMES[normalized] ?? normalized;
57
61
  }
58
62
 
59
- /**
60
- * Per-model approximate context window (tokens). The Codex SDK does not
61
- * expose these at runtime, so we maintain a static map derived from
62
- * https://developers.openai.com/codex/models. The values are used by the
63
- * `context_usage` percent calculation inside `CodexSession`.
64
- *
65
- * Update this map whenever a model's context window changes.
66
- */
67
- export const CODEX_MODEL_CONTEXT_WINDOWS: Record<CodexModel, number> = {
63
+ interface ModelsDevOpenAiModel {
64
+ limit?: {
65
+ context?: number;
66
+ };
67
+ cost?: {
68
+ input?: number;
69
+ cache_read?: number;
70
+ output?: number;
71
+ };
72
+ }
73
+
74
+ const MODELSDEV_OPENAI_MODELS =
75
+ (
76
+ modelsDevCache as {
77
+ openai?: {
78
+ models?: Record<string, ModelsDevOpenAiModel>;
79
+ };
80
+ }
81
+ ).openai?.models ?? {};
82
+
83
+ const FALLBACK_CODEX_MODEL_CONTEXT_WINDOWS: Record<CodexModel, number> = {
84
+ "gpt-5.6-sol": 1_050_000,
85
+ "gpt-5.6-terra": 1_050_000,
86
+ "gpt-5.6-luna": 1_050_000,
68
87
  "gpt-5.5": 1_050_000,
69
- "gpt-5.4": 200_000,
70
- "gpt-5.4-mini": 200_000,
71
- "gpt-5.3-codex": 1_000_000, // 1M context per plan Key Discoveries
88
+ "gpt-5.4": 1_050_000,
89
+ "gpt-5.4-mini": 400_000,
90
+ "gpt-5.3-codex": 400_000,
72
91
  "gpt-5.2-codex": 200_000,
73
92
  };
74
93
 
94
+ /**
95
+ * Per-model approximate context window (tokens). The Codex SDK does not
96
+ * expose these at runtime, so we read the vendored models.dev cache used by
97
+ * the pricing seeder and fall back only for legacy/incomplete cache entries.
98
+ * The values are used by the `context_usage` percent calculation inside
99
+ * `CodexSession`.
100
+ */
101
+ export const CODEX_MODEL_CONTEXT_WINDOWS: Record<CodexModel, number> = Object.fromEntries(
102
+ CODEX_MODELS.map((model) => [
103
+ model,
104
+ MODELSDEV_OPENAI_MODELS[model]?.limit?.context ?? FALLBACK_CODEX_MODEL_CONTEXT_WINDOWS[model],
105
+ ]),
106
+ ) as Record<CodexModel, number>;
107
+
75
108
  /**
76
109
  * Return the context window in tokens for a given Codex model. Unknown models
77
110
  * (passthrough strings) get the 200k default — keeps `context_usage` finite
@@ -82,15 +115,14 @@ export function getCodexContextWindow(model: string): number {
82
115
  }
83
116
 
84
117
  /**
85
- * Per-model pricing in USD per million tokens, sourced from
86
- * https://developers.openai.com/api/docs/pricing on 2026-05-28 (Standard tier,
87
- * short-context column long-context multipliers and Batch / Flex / Priority
88
- * tiers exist but the Codex SDK does not expose which tier was used so we
89
- * default to the headline rate).
118
+ * Per-model pricing in USD per million tokens, sourced from the vendored
119
+ * models.dev cache (`src/be/modelsdev-cache.json`). The fallback below mirrors
120
+ * that snapshot for known models and covers legacy models that models.dev no
121
+ * longer lists.
90
122
  *
91
123
  * The Codex SDK does NOT report dollar cost in `Usage`, so this map is what
92
- * powers `totalCostUsd` on the `result` event. Update whenever OpenAI changes
93
- * the pricing page or adds new models.
124
+ * powers `totalCostUsd` on the `result` event. Refresh models.dev whenever
125
+ * OpenAI changes pricing or adds new models.
94
126
  *
95
127
  * `gpt-5.2-codex` is not on the current pricing page (legacy / retired); it
96
128
  * inherits the `gpt-5.3-codex` rate as a best-effort fallback so old tasks
@@ -105,7 +137,22 @@ export interface CodexModelPricing {
105
137
  outputPerMillion: number;
106
138
  }
107
139
 
108
- export const CODEX_MODEL_PRICING: Record<CodexModel, CodexModelPricing> = {
140
+ const FALLBACK_CODEX_MODEL_PRICING: Record<CodexModel, CodexModelPricing> = {
141
+ "gpt-5.6-sol": {
142
+ inputPerMillion: 5.0,
143
+ cachedInputPerMillion: 0.5,
144
+ outputPerMillion: 30.0,
145
+ },
146
+ "gpt-5.6-terra": {
147
+ inputPerMillion: 2.5,
148
+ cachedInputPerMillion: 0.25,
149
+ outputPerMillion: 15.0,
150
+ },
151
+ "gpt-5.6-luna": {
152
+ inputPerMillion: 1.0,
153
+ cachedInputPerMillion: 0.1,
154
+ outputPerMillion: 6.0,
155
+ },
109
156
  "gpt-5.5": {
110
157
  inputPerMillion: 5.0,
111
158
  cachedInputPerMillion: 0.5,
@@ -134,6 +181,29 @@ export const CODEX_MODEL_PRICING: Record<CodexModel, CodexModelPricing> = {
134
181
  },
135
182
  };
136
183
 
184
+ function getModelsDevCodexPricing(model: CodexModel): CodexModelPricing | undefined {
185
+ const cost = MODELSDEV_OPENAI_MODELS[model]?.cost;
186
+ if (
187
+ typeof cost?.input !== "number" ||
188
+ typeof cost.cache_read !== "number" ||
189
+ typeof cost.output !== "number"
190
+ ) {
191
+ return undefined;
192
+ }
193
+ return {
194
+ inputPerMillion: cost.input,
195
+ cachedInputPerMillion: cost.cache_read,
196
+ outputPerMillion: cost.output,
197
+ };
198
+ }
199
+
200
+ export const CODEX_MODEL_PRICING: Record<CodexModel, CodexModelPricing> = Object.fromEntries(
201
+ CODEX_MODELS.map((model) => [
202
+ model,
203
+ getModelsDevCodexPricing(model) ?? FALLBACK_CODEX_MODEL_PRICING[model],
204
+ ]),
205
+ ) as Record<CodexModel, CodexModelPricing>;
206
+
137
207
  /**
138
208
  * Phase 6 — one-warning-per-process tracking so unknown models log once
139
209
  * instead of spamming the worker log on every turn.