nodal-agents 0.7.2 → 0.7.5

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 (169) hide show
  1. package/cli.js +118 -2
  2. package/migrations/0058_entity_llm_keys_context_window.sql +6 -0
  3. package/migrations/0059_agent_jobs_conversation_id.sql +10 -0
  4. package/migrations/meta/_journal.json +14 -0
  5. package/package.json +1 -1
  6. package/runner.js +1435 -571
  7. package/web/.next/BUILD_ID +1 -1
  8. package/web/.next/app-path-routes-manifest.json +9 -9
  9. package/web/.next/build-manifest.json +2 -2
  10. package/web/.next/prerender-manifest.json +3 -3
  11. package/web/.next/routes-manifest.json +32 -1
  12. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js +3 -3
  13. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js.nft.json +1 -1
  14. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page_client-reference-manifest.js +1 -1
  15. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js +2 -2
  16. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js.nft.json +1 -1
  17. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page_client-reference-manifest.js +1 -1
  18. package/web/.next/server/app/(dashboard)/agents/page.js +2 -2
  19. package/web/.next/server/app/(dashboard)/agents/page.js.nft.json +1 -1
  20. package/web/.next/server/app/(dashboard)/agents/page_client-reference-manifest.js +1 -1
  21. package/web/.next/server/app/(dashboard)/approvals/page.js +2 -2
  22. package/web/.next/server/app/(dashboard)/approvals/page.js.nft.json +1 -1
  23. package/web/.next/server/app/(dashboard)/approvals/page_client-reference-manifest.js +1 -1
  24. package/web/.next/server/app/(dashboard)/automations/page.js +2 -2
  25. package/web/.next/server/app/(dashboard)/automations/page.js.nft.json +1 -1
  26. package/web/.next/server/app/(dashboard)/automations/page_client-reference-manifest.js +1 -1
  27. package/web/.next/server/app/(dashboard)/chat/page.js +2 -2
  28. package/web/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
  29. package/web/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
  30. package/web/.next/server/app/(dashboard)/connectors/page.js +1 -1
  31. package/web/.next/server/app/(dashboard)/connectors/page.js.nft.json +1 -1
  32. package/web/.next/server/app/(dashboard)/connectors/page_client-reference-manifest.js +1 -1
  33. package/web/.next/server/app/(dashboard)/credentials/page.js +1 -1
  34. package/web/.next/server/app/(dashboard)/credentials/page.js.nft.json +1 -1
  35. package/web/.next/server/app/(dashboard)/credentials/page_client-reference-manifest.js +1 -1
  36. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js +2 -2
  37. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js.nft.json +1 -1
  38. package/web/.next/server/app/(dashboard)/jobs/[id]/page_client-reference-manifest.js +1 -1
  39. package/web/.next/server/app/(dashboard)/jobs/page.js +2 -2
  40. package/web/.next/server/app/(dashboard)/jobs/page.js.nft.json +1 -1
  41. package/web/.next/server/app/(dashboard)/jobs/page_client-reference-manifest.js +1 -1
  42. package/web/.next/server/app/(dashboard)/learned-skills/page.js +2 -2
  43. package/web/.next/server/app/(dashboard)/learned-skills/page.js.nft.json +1 -1
  44. package/web/.next/server/app/(dashboard)/learned-skills/page_client-reference-manifest.js +1 -1
  45. package/web/.next/server/app/(dashboard)/llm-providers/page.js +1 -1
  46. package/web/.next/server/app/(dashboard)/llm-providers/page.js.nft.json +1 -1
  47. package/web/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
  48. package/web/.next/server/app/(dashboard)/logs/page.js +2 -2
  49. package/web/.next/server/app/(dashboard)/logs/page.js.nft.json +1 -1
  50. package/web/.next/server/app/(dashboard)/logs/page_client-reference-manifest.js +1 -1
  51. package/web/.next/server/app/(dashboard)/mcp/page.js +2 -2
  52. package/web/.next/server/app/(dashboard)/mcp/page.js.nft.json +1 -1
  53. package/web/.next/server/app/(dashboard)/mcp/page_client-reference-manifest.js +1 -1
  54. package/web/.next/server/app/(dashboard)/memories/page.js +2 -2
  55. package/web/.next/server/app/(dashboard)/memories/page.js.nft.json +1 -1
  56. package/web/.next/server/app/(dashboard)/memories/page_client-reference-manifest.js +1 -1
  57. package/web/.next/server/app/(dashboard)/page.js +2 -2
  58. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  59. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  60. package/web/.next/server/app/(dashboard)/settings/page.js +2 -2
  61. package/web/.next/server/app/(dashboard)/settings/page.js.nft.json +1 -1
  62. package/web/.next/server/app/(dashboard)/settings/page_client-reference-manifest.js +1 -1
  63. package/web/.next/server/app/(dashboard)/settings/root-context/page.js +2 -2
  64. package/web/.next/server/app/(dashboard)/settings/root-context/page.js.nft.json +1 -1
  65. package/web/.next/server/app/(dashboard)/settings/root-context/page_client-reference-manifest.js +1 -1
  66. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js +2 -2
  67. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js.nft.json +1 -1
  68. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page_client-reference-manifest.js +1 -1
  69. package/web/.next/server/app/(dashboard)/skills/new/page.js +2 -2
  70. package/web/.next/server/app/(dashboard)/skills/new/page.js.nft.json +1 -1
  71. package/web/.next/server/app/(dashboard)/skills/new/page_client-reference-manifest.js +1 -1
  72. package/web/.next/server/app/(dashboard)/skills/page.js +2 -2
  73. package/web/.next/server/app/(dashboard)/skills/page.js.nft.json +1 -1
  74. package/web/.next/server/app/(dashboard)/skills/page_client-reference-manifest.js +1 -1
  75. package/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  76. package/web/.next/server/app/_global-error.html +1 -1
  77. package/web/.next/server/app/_global-error.rsc +2 -2
  78. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  79. package/web/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  80. package/web/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  81. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  82. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +2 -2
  83. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  84. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  85. package/web/.next/server/app/_not-found.html +1 -1
  86. package/web/.next/server/app/_not-found.rsc +3 -3
  87. package/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  88. package/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  89. package/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  90. package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  91. package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  92. package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  93. package/web/.next/server/app/api/auth/[...all]/route.js +1 -1
  94. package/web/.next/server/app/api/oauth/[provider]/callback/route.js +1 -1
  95. package/web/.next/server/app/api/oauth/[provider]/callback/route.js.nft.json +1 -1
  96. package/web/.next/server/app/api/oauth/[provider]/start/route.js +1 -1
  97. package/web/.next/server/app/login/page_client-reference-manifest.js +1 -1
  98. package/web/.next/server/app/onboarding/page.js +201 -193
  99. package/web/.next/server/app/onboarding/page.js.nft.json +1 -1
  100. package/web/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  101. package/web/.next/server/app-paths-manifest.json +9 -9
  102. package/web/.next/server/chunks/2043.js +1 -0
  103. package/web/.next/server/chunks/2046.js +1 -0
  104. package/web/.next/server/chunks/2713.js +26 -18
  105. package/web/.next/server/chunks/4479.js +1 -0
  106. package/web/.next/server/chunks/4574.js +1 -1
  107. package/web/.next/server/chunks/{8810.js → 5744.js} +5 -4
  108. package/web/.next/server/chunks/{8464.js → 7388.js} +1 -2
  109. package/web/.next/server/chunks/7560.js +1 -0
  110. package/web/.next/server/middleware-build-manifest.js +1 -1
  111. package/web/.next/server/pages/404.html +1 -1
  112. package/web/.next/server/pages/500.html +1 -1
  113. package/web/.next/server/server-reference-manifest.js +1 -1
  114. package/web/.next/server/server-reference-manifest.json +1 -1
  115. package/web/.next/static/chunks/3114-d846062bc8434b58.js +1 -0
  116. package/web/.next/static/chunks/4893-730f4c62f76c6c48.js +1 -0
  117. package/web/.next/static/chunks/8097-bad68cd78aa59a10.js +1 -0
  118. package/web/.next/static/chunks/{9836-177f6ca2f15ff703.js → 9836-a69e19544b669a71.js} +1 -1
  119. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-18973089c62eb12e.js +6 -0
  120. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-7913483b3bfd7dda.js +1 -0
  121. package/web/.next/static/chunks/app/(dashboard)/agents/page-eb7b72d4d7bfde01.js +1 -0
  122. package/web/.next/static/chunks/app/(dashboard)/approvals/{page-b3a838ecbc94a030.js → page-dc8dc54ed07bc81a.js} +1 -1
  123. package/web/.next/static/chunks/app/(dashboard)/automations/page-1034613ecd525509.js +1 -0
  124. package/web/.next/static/chunks/app/(dashboard)/chat/page-4d0e2fd060f2aafb.js +1 -0
  125. package/web/.next/static/chunks/app/(dashboard)/connectors/{page-2c6f913cc6aea4b7.js → page-8bad5bc43f197471.js} +1 -1
  126. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-116a001adade3a1e.js +1 -0
  127. package/web/.next/static/chunks/app/(dashboard)/jobs/page-0fc661a4e7588bdb.js +1 -0
  128. package/web/.next/static/chunks/app/(dashboard)/layout-9068b164d3ae4853.js +1 -0
  129. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-01f1b68b334bf442.js +1 -0
  130. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-99fd3e7b631131e8.js +1 -0
  131. package/web/.next/static/chunks/app/(dashboard)/logs/page-94babd54bdd0d1af.js +1 -0
  132. package/web/.next/static/chunks/app/(dashboard)/mcp/page-2d190bdd49b7b4b4.js +1 -0
  133. package/web/.next/static/chunks/app/(dashboard)/memories/page-ac872edd26efe323.js +1 -0
  134. package/web/.next/static/chunks/app/(dashboard)/page-8cd3f1d4d072ac25.js +1 -0
  135. package/web/.next/static/chunks/app/(dashboard)/settings/page-aed8fc039205ccd7.js +1 -0
  136. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-a2eb786f9d897813.js +1 -0
  137. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-7f87e2c66d1ceea4.js +1 -0
  138. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-54734a6e0675887a.js +1 -0
  139. package/web/.next/static/chunks/app/(dashboard)/skills/page-cae26ae45b9b11e5.js +1 -0
  140. package/web/.next/static/chunks/app/onboarding/{page-f2f04878e81fdf23.js → page-e63eee720a95baac.js} +2 -2
  141. package/web/.next/static/css/{4d808b91d7b58fce.css → 289a2e89c0d093fd.css} +1 -1
  142. package/web/.next/server/chunks/3223.js +0 -1
  143. package/web/.next/server/chunks/5515.js +0 -1
  144. package/web/.next/server/chunks/6468.js +0 -1
  145. package/web/.next/server/chunks/8410.js +0 -1
  146. package/web/.next/static/chunks/4964-c6711d3d479b6085.js +0 -1
  147. package/web/.next/static/chunks/609-a0c06f3592427d58.js +0 -1
  148. package/web/.next/static/chunks/7704-7ba1c3d45662ffc8.js +0 -1
  149. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-cfce874f4acede5e.js +0 -6
  150. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-9249c2839f551ba1.js +0 -1
  151. package/web/.next/static/chunks/app/(dashboard)/agents/page-7d784d737acedccf.js +0 -1
  152. package/web/.next/static/chunks/app/(dashboard)/automations/page-b1b548de4f52696c.js +0 -1
  153. package/web/.next/static/chunks/app/(dashboard)/chat/page-bcf6dcc73b370eb0.js +0 -1
  154. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-43f033251eddcc4b.js +0 -1
  155. package/web/.next/static/chunks/app/(dashboard)/jobs/page-1f892f950bb30403.js +0 -1
  156. package/web/.next/static/chunks/app/(dashboard)/layout-e184673151ff8a05.js +0 -1
  157. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-f873bb6f8539e47e.js +0 -1
  158. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-64adda30176f4ccb.js +0 -1
  159. package/web/.next/static/chunks/app/(dashboard)/logs/page-79cfd9141c39d8ef.js +0 -1
  160. package/web/.next/static/chunks/app/(dashboard)/mcp/page-d276d7bab25286a1.js +0 -1
  161. package/web/.next/static/chunks/app/(dashboard)/memories/page-c66720494a340503.js +0 -1
  162. package/web/.next/static/chunks/app/(dashboard)/page-3649204dd27a0ad0.js +0 -1
  163. package/web/.next/static/chunks/app/(dashboard)/settings/page-1cab404d2eccfd80.js +0 -1
  164. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-b5898c5444f88c9a.js +0 -1
  165. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-82d91fc6b6acc161.js +0 -1
  166. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-58b5ed04583ca7d4.js +0 -1
  167. package/web/.next/static/chunks/app/(dashboard)/skills/page-312bd4ae8ddc8114.js +0 -1
  168. /package/web/.next/static/{SgnP_MouhUmw0bBf-lUmD → XF1rXMlGkoyAu_GI5theh}/_buildManifest.js +0 -0
  169. /package/web/.next/static/{SgnP_MouhUmw0bBf-lUmD → XF1rXMlGkoyAu_GI5theh}/_ssgManifest.js +0 -0
package/runner.js CHANGED
@@ -294,6 +294,10 @@ var init_job = __esm({
294
294
  task: z7.string().min(1),
295
295
  original_task: z7.string().nullable(),
296
296
  chat_id: z7.string().nullable(),
297
+ // Groups jobs from the same conversational thread (migration 0059) — see
298
+ // apps/runner/src/job/conversation-id.ts for the stamping rule. Null for
299
+ // non-conversational jobs (cron/schedule/webhook with no parent).
300
+ conversation_id: z7.string().guid().nullable(),
297
301
  system_prompt: z7.string().nullable(),
298
302
  messages: z7.array(z7.record(z7.string(), z7.unknown())),
299
303
  tools_used: z7.array(z7.string()),
@@ -334,6 +338,7 @@ var init_job = __esm({
334
338
  status: JobStatusSchema.default("pending"),
335
339
  original_task: z7.string().nullable().optional(),
336
340
  chat_id: z7.string().nullable().optional(),
341
+ conversation_id: z7.string().guid().nullable().optional(),
337
342
  system_prompt: z7.string().nullable().optional(),
338
343
  request_id: z7.string().nullable().optional(),
339
344
  parent_job_id: z7.string().guid().nullable().optional()
@@ -341,6 +346,38 @@ var init_job = __esm({
341
346
  }
342
347
  });
343
348
 
349
+ // ../../packages/shared/src/entities/job-classification.ts
350
+ var CHAT_DELIVERY_TOOLS, CHAT_CONTEXT_READ_TOOLS, CHAT_ALLOWED_TOOLS;
351
+ var init_job_classification = __esm({
352
+ "../../packages/shared/src/entities/job-classification.ts"() {
353
+ "use strict";
354
+ CHAT_DELIVERY_TOOLS = [
355
+ "telegram_send_message",
356
+ "send_image",
357
+ "send_file",
358
+ "send_video",
359
+ "send_audio",
360
+ "send_voice",
361
+ "return_result",
362
+ "dashboard_publish"
363
+ ];
364
+ CHAT_CONTEXT_READ_TOOLS = [
365
+ "query_memory",
366
+ "search_history",
367
+ "skill_view",
368
+ "list_models",
369
+ "list_schedules",
370
+ "file_read",
371
+ "file_list",
372
+ "file_search"
373
+ ];
374
+ CHAT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
375
+ ...CHAT_DELIVERY_TOOLS,
376
+ ...CHAT_CONTEXT_READ_TOOLS
377
+ ]);
378
+ }
379
+ });
380
+
344
381
  // ../../packages/shared/src/entities/task.ts
345
382
  import { z as z8 } from "zod";
346
383
  var TaskDescriptionSchema, TaskTitleSchema, AgentTaskSchema, AgentTaskInsertSchema;
@@ -446,7 +483,9 @@ var init_connector = __esm({
446
483
  name: z10.string().min(1).max(120),
447
484
  slug: z10.string().min(1).max(80).regex(/^[a-z0-9-]+$/),
448
485
  base_url: z10.string().nullable(),
449
- // api_key stored encrypted in DB (pgp_sym_encrypt); raw value never returned
486
+ // audit#2026-07-07 SEC-5: this comment used to claim pgp_sym_encrypt (pgcrypto),
487
+ // which is not used anywhere. Real encryption is application-level AES-256-GCM
488
+ // via @nodal-agents/secrets before the value ever reaches the DB; raw value never returned.
450
489
  api_key: z10.string().nullable(),
451
490
  active: z10.boolean(),
452
491
  auth_type: ConnectorAuthTypeSchema,
@@ -968,7 +1007,7 @@ function parseRootGrants(raw) {
968
1007
  autonomy
969
1008
  };
970
1009
  }
971
- var AutonomyLevelSchema, RootGrantsSchema, DEFAULT_ROOT_GRANTS, INITIAL_AUTO_ROOT_GRANTS, META_TOOL_BY_GRANT;
1010
+ var AutonomyLevelSchema, RootGrantsSchema, DEFAULT_ROOT_GRANTS, INITIAL_AUTO_ROOT_GRANTS, META_TOOL_BY_GRANT, META_TOOL_NAMES;
972
1011
  var init_root_agent = __esm({
973
1012
  "../../packages/shared/src/root-agent.ts"() {
974
1013
  "use strict";
@@ -1028,6 +1067,26 @@ var init_root_agent = __esm({
1028
1067
  attachConnector: ["attach_connector", "detach_connector"],
1029
1068
  manageSchedules: ["create_schedule", "update_schedule", "toggle_schedule", "run_schedule"]
1030
1069
  };
1070
+ META_TOOL_NAMES = [
1071
+ "create_agent",
1072
+ "update_agent",
1073
+ "attach_agent",
1074
+ "detach_agent",
1075
+ "create_skill",
1076
+ "update_skill",
1077
+ "attach_skill",
1078
+ "detach_skill",
1079
+ "create_mcp",
1080
+ "attach_mcp",
1081
+ "detach_mcp",
1082
+ "create_connector",
1083
+ "attach_connector",
1084
+ "detach_connector",
1085
+ "create_schedule",
1086
+ "update_schedule",
1087
+ "toggle_schedule",
1088
+ "run_schedule"
1089
+ ];
1031
1090
  }
1032
1091
  });
1033
1092
 
@@ -1149,8 +1208,13 @@ function modelCanSeeImages(modelId) {
1149
1208
  function findModelCatalogEntry(provider, modelId) {
1150
1209
  return MODEL_CATALOG[provider]?.find((e) => e.modelId === modelId);
1151
1210
  }
1152
- function modelContextWindow(provider, modelId) {
1153
- return findModelCatalogEntry(provider, modelId)?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
1211
+ function modelContextWindow(provider, modelId, storedWindow) {
1212
+ const catalogued = findModelCatalogEntry(provider, modelId)?.contextWindow;
1213
+ if (catalogued !== void 0) return catalogued;
1214
+ if (typeof storedWindow === "number" && Number.isFinite(storedWindow) && storedWindow > 0) {
1215
+ return storedWindow;
1216
+ }
1217
+ return DEFAULT_CONTEXT_WINDOW;
1154
1218
  }
1155
1219
  function estimateModelCostUsd(provider, modelId, inputTokens, outputTokens) {
1156
1220
  const pricing = findModelCatalogEntry(provider, modelId)?.pricing;
@@ -1503,6 +1567,284 @@ var init_timezone = __esm({
1503
1567
  }
1504
1568
  });
1505
1569
 
1570
+ // ../../packages/shared/src/redact-secrets.ts
1571
+ function redactSecretsForAudit(value) {
1572
+ if (Array.isArray(value)) return value.map(redactSecretsForAudit);
1573
+ if (value !== null && typeof value === "object") {
1574
+ const out = {};
1575
+ for (const [k, v] of Object.entries(value)) {
1576
+ if (SECRET_FIELD_RE.test(k)) {
1577
+ out[k] = v == null ? v : REDACTED;
1578
+ } else if (k.toLowerCase() === "env" && v !== null && typeof v === "object" && !Array.isArray(v)) {
1579
+ const envOut = {};
1580
+ for (const ek of Object.keys(v)) envOut[ek] = REDACTED;
1581
+ out[k] = envOut;
1582
+ } else {
1583
+ out[k] = redactSecretsForAudit(v);
1584
+ }
1585
+ }
1586
+ return out;
1587
+ }
1588
+ return value;
1589
+ }
1590
+ var REDACTED, SECRET_FIELD_RE;
1591
+ var init_redact_secrets = __esm({
1592
+ "../../packages/shared/src/redact-secrets.ts"() {
1593
+ "use strict";
1594
+ REDACTED = "***";
1595
+ SECRET_FIELD_RE = /^(api[_-]?key|api[_-]?token|access[_-]?token|refresh[_-]?token|client[_-]?secret|secret|password|token|credential)$/i;
1596
+ }
1597
+ });
1598
+
1599
+ // ../../packages/shared/src/catastrophic-command.ts
1600
+ function stripQuotes(token) {
1601
+ return token.replace(/^["']|["']$/g, "");
1602
+ }
1603
+ function normalizeSlashes(s) {
1604
+ return s.replace(/\/{2,}/g, "/").replace(/\\{2,}/g, "\\");
1605
+ }
1606
+ function stripWrapperPrefix(tokens) {
1607
+ let i = 0;
1608
+ while (i < tokens.length && WRAPPER_LEADER.has((tokens[i] ?? "").toLowerCase())) {
1609
+ i += 1;
1610
+ if (i < tokens.length && /^[-/]/.test(tokens[i] ?? "")) {
1611
+ i += 1;
1612
+ }
1613
+ }
1614
+ return tokens.slice(i);
1615
+ }
1616
+ function interpreterBasename(token) {
1617
+ const t = stripQuotes(token);
1618
+ const base = t.split(/[\\/]/).pop() ?? t;
1619
+ return base.replace(/\.(exe|com)$/i, "").toLowerCase();
1620
+ }
1621
+ function skipPassthroughLeaders(tokens) {
1622
+ let i = 0;
1623
+ while (i < tokens.length && PASSTHROUGH_LEADERS.has(interpreterBasename(tokens[i] ?? ""))) {
1624
+ i += 1;
1625
+ while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i] ?? "")) i += 1;
1626
+ if (i < tokens.length && /^[-/]/.test(tokens[i] ?? "")) i += 1;
1627
+ }
1628
+ return tokens.slice(i);
1629
+ }
1630
+ function interpreterKind(name) {
1631
+ if (name === "py" || /^python[0-9.]*$/.test(name)) return "python";
1632
+ if (/^node(js)?$/.test(name)) return "node";
1633
+ if (/^perl[0-9.]*$/.test(name)) return "perl";
1634
+ if (/^ruby[0-9.]*$/.test(name)) return "ruby";
1635
+ if (/^php[0-9.]*$/.test(name)) return "php";
1636
+ if (["sh", "bash", "zsh", "ksh", "dash", "ash"].includes(name)) return "shell";
1637
+ if (name === "powershell" || name === "pwsh") return "powershell";
1638
+ return null;
1639
+ }
1640
+ function isInlineEvalFlag(kind, flag) {
1641
+ switch (kind) {
1642
+ case "python":
1643
+ return flag === "-c";
1644
+ case "node":
1645
+ return flag === "-e" || flag === "--eval" || flag === "-p" || flag === "--print";
1646
+ case "perl":
1647
+ case "ruby":
1648
+ return flag === "-e";
1649
+ case "php":
1650
+ return flag === "-r";
1651
+ case "shell":
1652
+ return flag === "-c";
1653
+ case "powershell":
1654
+ return /^-com/.test(flag) || /^-enc/.test(flag);
1655
+ default:
1656
+ return false;
1657
+ }
1658
+ }
1659
+ function hasInlineInterpreterEval(tokens) {
1660
+ const rest = skipPassthroughLeaders(tokens);
1661
+ if (rest.length === 0) return false;
1662
+ const kind = interpreterKind(interpreterBasename(rest[0] ?? ""));
1663
+ if (!kind) return false;
1664
+ return rest.slice(1).some((t) => isInlineEvalFlag(kind, stripQuotes(t).toLowerCase()));
1665
+ }
1666
+ function hasPipeIntoBareInterpreter(c) {
1667
+ const parts = c.split("|");
1668
+ for (let i = 1; i < parts.length; i++) {
1669
+ const toks = (parts[i] ?? "").trim().split(/\s+/).map(stripQuotes).filter((t) => t.length > 0);
1670
+ const rest = skipPassthroughLeaders(toks);
1671
+ if (rest.length === 0) continue;
1672
+ if (!interpreterKind(interpreterBasename(rest[0] ?? ""))) continue;
1673
+ const hasScriptArg = rest.slice(1).some((t) => !t.startsWith("-"));
1674
+ if (!hasScriptArg) return true;
1675
+ }
1676
+ return false;
1677
+ }
1678
+ function isWindowsRootOrWildcardTarget(token) {
1679
+ const t = stripQuotes(token);
1680
+ if (t === "*" || t === "\\" || t === "/") return true;
1681
+ if (/^[a-z]:\\?\*?$/i.test(t)) return true;
1682
+ if (/^%(systemdrive|systemroot|userprofile)%\\?\*?$/i.test(t)) return true;
1683
+ if (/^\$env:(systemdrive|systemroot|userprofile)\\?\*?$/i.test(t)) return true;
1684
+ return false;
1685
+ }
1686
+ function isCatastrophicCommand(cmd) {
1687
+ if (typeof cmd !== "string" || cmd.trim() === "") return false;
1688
+ const c = normalizeSlashes(cmd.trim());
1689
+ if (FORK_BOMB.test(c) || MKFS.test(c) || DD_TO_DEVICE.test(c) || POWER_STATE.test(c) || INIT_RUNLEVEL.test(c) || OVERWRITE_DEVICE.test(c) || DISKPART.test(c) || DISK_CMDLET.test(c)) {
1690
+ return true;
1691
+ }
1692
+ for (const seg of c.split(/[;&|\n\r]+/)) {
1693
+ const s = seg.trim();
1694
+ if (!s) continue;
1695
+ const tokens = s.split(/\s+/).map(stripQuotes);
1696
+ const cmdTokens = stripWrapperPrefix(tokens);
1697
+ const cmdWord = cmdTokens[0] ?? "";
1698
+ if (/^rm$/i.test(cmdWord)) {
1699
+ const recursive = /\s-\S*r/i.test(s) || /\s--recursive\b/i.test(s);
1700
+ const force = /\s-\S*f/i.test(s) || /\s--force\b/i.test(s);
1701
+ if (recursive && force) {
1702
+ if (/\s--no-preserve-root\b/i.test(s)) return true;
1703
+ if (/(\s|=)(\/|\/\*|~|~\/\*?|\$HOME\/?\*?|\*)(\s|$|"|')/.test(s)) return true;
1704
+ if (tokens.some((t) => isWindowsRootOrWildcardTarget(t))) return true;
1705
+ }
1706
+ }
1707
+ if (/^format(\.(com|exe))?$/i.test(cmdWord) && cmdTokens.slice(1).some((d) => /^[a-z]:([\\/]\*?)?$/i.test(d))) {
1708
+ return true;
1709
+ }
1710
+ if (/^(ri|remove-item|del|erase|rd|rmdir)$/i.test(cmdWord)) {
1711
+ const psRecursiveForce = /(^|\s)-r(ecurse)?\b/i.test(s) && /(^|\s)-f(orce)?\b/i.test(s);
1712
+ const cmdRecursiveForce = /\/s\b/i.test(s) && /\/q\b/i.test(s);
1713
+ if (psRecursiveForce || cmdRecursiveForce) {
1714
+ if (tokens.some((t) => isWindowsRootOrWildcardTarget(t))) return true;
1715
+ }
1716
+ }
1717
+ }
1718
+ return false;
1719
+ }
1720
+ function isInlineInterpreterEvalCommand(cmd) {
1721
+ if (typeof cmd !== "string" || cmd.trim() === "") return false;
1722
+ const c = normalizeSlashes(cmd.trim());
1723
+ if (AWK_CODE_EXEC.test(c) || hasPipeIntoBareInterpreter(c)) return true;
1724
+ for (const seg of c.split(/[;&|\n\r]+/)) {
1725
+ const s = seg.trim();
1726
+ if (!s) continue;
1727
+ const tokens = s.split(/\s+/).map(stripQuotes);
1728
+ if (hasInlineInterpreterEval(tokens)) return true;
1729
+ }
1730
+ return false;
1731
+ }
1732
+ function isDestructiveOrHeavyCommand(cmd) {
1733
+ if (typeof cmd !== "string" || cmd.trim() === "") return false;
1734
+ if (isCatastrophicCommand(cmd) || isInlineInterpreterEvalCommand(cmd)) return true;
1735
+ const c = normalizeSlashes(cmd.trim());
1736
+ return DESTRUCTIVE_PATTERNS.some((re) => re.test(c));
1737
+ }
1738
+ var FORK_BOMB, MKFS, DD_TO_DEVICE, POWER_STATE, INIT_RUNLEVEL, OVERWRITE_DEVICE, DISKPART, DISK_CMDLET, AWK_CODE_EXEC, WRAPPER_LEADER, PASSTHROUGH_LEADERS, DESTRUCTIVE_PATTERNS;
1739
+ var init_catastrophic_command = __esm({
1740
+ "../../packages/shared/src/catastrophic-command.ts"() {
1741
+ "use strict";
1742
+ FORK_BOMB = /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&\s*\}\s*;\s*:/;
1743
+ MKFS = /\bmkfs(\.\w+)?\b/i;
1744
+ DD_TO_DEVICE = /\bdd\b[\s\S]*?\bof=\/dev\/[a-z]/i;
1745
+ POWER_STATE = /\b(shutdown|reboot|halt|poweroff|stop-computer|restart-computer)\b/i;
1746
+ INIT_RUNLEVEL = /\binit\s+[06]\b/;
1747
+ OVERWRITE_DEVICE = />\s*\/dev\/(sd[a-z]|nvme\d|disk\d|hd[a-z])/i;
1748
+ DISKPART = /\bdiskpart\b/i;
1749
+ DISK_CMDLET = /\b(format-volume|clear-disk|initialize-disk)\b/i;
1750
+ AWK_CODE_EXEC = /\bawk\b[\s\S]*?(\bsystem\s*\(|\bgetline\b[^;]*\||["'][^"']*\|\s*["']?\s*(?:sh|bash|cmd))/i;
1751
+ WRAPPER_LEADER = /* @__PURE__ */ new Set([
1752
+ "cmd",
1753
+ "cmd.exe",
1754
+ "powershell",
1755
+ "powershell.exe",
1756
+ "pwsh",
1757
+ "pwsh.exe",
1758
+ "sh",
1759
+ "bash",
1760
+ "sudo"
1761
+ ]);
1762
+ PASSTHROUGH_LEADERS = /* @__PURE__ */ new Set(["sudo", "env", "cmd", "cmd.exe"]);
1763
+ DESTRUCTIVE_PATTERNS = [
1764
+ /\b(rm|rmdir|unlink|shred)\b/i,
1765
+ // delete (unix)
1766
+ /\bdel\s|\bRemove-Item\b|\brd\s+\/s/i,
1767
+ // delete (windows/ps)
1768
+ /\bfind\b[^\n]*-delete\b/i,
1769
+ // find … -delete
1770
+ /\b(pip3?|npm|pnpm|yarn|apt|apt-get|yum|dnf|brew|pacman|choco|winget|uvx|pipx|cargo|gem|conda|comfy)\b[^\n]*\binstall\b/i,
1771
+ // pkg install
1772
+ /\bgo\s+install\b|\bcomfy\b[^\n]*\bmodel\s+download\b|\bpip3?\b[^\n]*\bdownload\b/i,
1773
+ // go install / model dl
1774
+ /\bwget\b|\bgit\s+clone\b|\bcurl\b[^\n]*\s-[oO]\b|\bInvoke-WebRequest\b|\biwr\b[^\n]*-OutFile/i,
1775
+ // large download / clone
1776
+ /\b(kill|pkill|killall|taskkill)\b|\bStop-Process\b|\bStop-Service\b/i,
1777
+ // process kill
1778
+ /\bsystemctl\b|\bsc\s+(stop|delete)\b|\bservice\b[^\n]*\b(stop|restart)\b/i,
1779
+ // service control
1780
+ /\bmkfs(\.\w+)?\b|\bdd\b[^\n]*\bof=|\b(format|fdisk|parted|diskpart)\b/i,
1781
+ // disk ops
1782
+ /\bchmod\s+-\S*R|\bchown\s+-\S*R|\bicacls\b/i,
1783
+ // recursive perms/ownership
1784
+ /\bgit\s+push\b[^\n]*(--force|-f\b)|\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-\S*f|\bgit\s+branch\s+-D\b/i
1785
+ // destructive VCS
1786
+ ];
1787
+ }
1788
+ });
1789
+
1790
+ // ../../packages/shared/src/approval-impact.ts
1791
+ function commandBinaries(cmd) {
1792
+ const names = [];
1793
+ for (const segment of cmd.split(/\||&&|\|\||;/)) {
1794
+ const tokens = segment.trim().match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
1795
+ let head = "";
1796
+ for (const t of tokens) {
1797
+ if (t === "" || t === "sudo" || /^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) continue;
1798
+ head = t;
1799
+ break;
1800
+ }
1801
+ if (!head) continue;
1802
+ const base = head.replace(/^["']|["']$/g, "").split(/[\\/]/).pop();
1803
+ if (base) names.push(base.replace(/\.(exe|cmd|bat)$/i, ""));
1804
+ if (names.length >= 3) break;
1805
+ }
1806
+ return names;
1807
+ }
1808
+ function describeCommandImpact(cmd) {
1809
+ const bins = commandBinaries(cmd);
1810
+ const ran = bins.length > 0 ? `Runs \`${bins.join("` \u2192 `")}\`` : "Runs a shell command";
1811
+ if (isCatastrophicCommand(cmd)) {
1812
+ return `${ran} \u2014 MACHINE-WIDE DESTRUCTIVE: refused even if approved (hardline floor).`;
1813
+ }
1814
+ if (isInlineInterpreterEvalCommand(cmd)) {
1815
+ return `${ran} \u2014 executes arbitrary inline code through an interpreter.`;
1816
+ }
1817
+ if (isDestructiveOrHeavyCommand(cmd)) {
1818
+ return `${ran} \u2014 destructive or heavy: deletes/moves files, installs software, or changes system state.`;
1819
+ }
1820
+ return `${ran} \u2014 no destructive pattern detected (likely read/inspect).`;
1821
+ }
1822
+ function computeApprovalImpactLine(toolName, toolInput) {
1823
+ const input = toolInput ?? {};
1824
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : "?";
1825
+ switch (toolName) {
1826
+ case "file_write":
1827
+ case "file_edit":
1828
+ return `Overwrites the existing file "${str(input["path"])}" in the shared workspace.`;
1829
+ case "run_command": {
1830
+ const cmd = input["command"];
1831
+ return typeof cmd === "string" && cmd.trim().length > 0 ? describeCommandImpact(cmd) : "Runs a shell command on the host.";
1832
+ }
1833
+ case "run_skill_script":
1834
+ return `Runs script "${str(input["script"])}" from skill "${str(input["skill"])}".`;
1835
+ case "skill_file_write":
1836
+ return `Writes a file into skill "${str(input["skill"])}"'s bundle.`;
1837
+ default:
1838
+ return `${toolName}: irreversible or destructive action.`;
1839
+ }
1840
+ }
1841
+ var init_approval_impact = __esm({
1842
+ "../../packages/shared/src/approval-impact.ts"() {
1843
+ "use strict";
1844
+ init_catastrophic_command();
1845
+ }
1846
+ });
1847
+
1506
1848
  // ../../packages/shared/src/index.ts
1507
1849
  var init_src = __esm({
1508
1850
  "../../packages/shared/src/index.ts"() {
@@ -1513,6 +1855,7 @@ var init_src = __esm({
1513
1855
  init_user();
1514
1856
  init_agent();
1515
1857
  init_job();
1858
+ init_job_classification();
1516
1859
  init_task();
1517
1860
  init_credential();
1518
1861
  init_connector();
@@ -1532,6 +1875,9 @@ var init_src = __esm({
1532
1875
  init_model_catalog();
1533
1876
  init_mcp_catalog();
1534
1877
  init_timezone();
1878
+ init_redact_secrets();
1879
+ init_approval_impact();
1880
+ init_catastrophic_command();
1535
1881
  }
1536
1882
  });
1537
1883
 
@@ -1672,7 +2018,7 @@ var init_entities = __esm({
1672
2018
  });
1673
2019
 
1674
2020
  // ../../packages/db/src/schema/llm_keys.ts
1675
- import { pgTable as pgTable3, text as text3, uuid as uuid3, boolean as boolean3, timestamp as timestamp3, index as index2 } from "drizzle-orm/pg-core";
2021
+ import { pgTable as pgTable3, text as text3, uuid as uuid3, boolean as boolean3, integer, timestamp as timestamp3, index as index2 } from "drizzle-orm/pg-core";
1676
2022
  var entityLlmKeys;
1677
2023
  var init_llm_keys = __esm({
1678
2024
  "../../packages/db/src/schema/llm_keys.ts"() {
@@ -1693,6 +2039,11 @@ var init_llm_keys = __esm({
1693
2039
  apiKeyLast4: text3("api_key_last4").notNull().default(""),
1694
2040
  baseUrl: text3("base_url"),
1695
2041
  nickname: text3("nickname"),
2042
+ // Real context window (tokens) for a custom/local model the catalog can't
2043
+ // know (É-3). Auto-detected from the endpoint at save time or set by the
2044
+ // user; NULL ⇒ fall back to the catalogued value or DEFAULT_CONTEXT_WINDOW.
2045
+ // Used window-relative to trigger compaction before the model overflows.
2046
+ contextWindow: integer("context_window"),
1696
2047
  isActive: boolean3("is_active").notNull().default(true),
1697
2048
  createdAt: timestamp3("created_at", { withTimezone: true }).notNull().defaultNow(),
1698
2049
  updatedAt: timestamp3("updated_at", { withTimezone: true }).notNull().defaultNow()
@@ -1708,7 +2059,7 @@ import {
1708
2059
  text as text4,
1709
2060
  uuid as uuid4,
1710
2061
  boolean as boolean4,
1711
- integer,
2062
+ integer as integer2,
1712
2063
  bigint,
1713
2064
  timestamp as timestamp4,
1714
2065
  jsonb as jsonb2,
@@ -1758,16 +2109,16 @@ var init_agents = __esm({
1758
2109
  taskContextTemplate: text4("task_context_template"),
1759
2110
  avatarUrl: text4("avatar_url"),
1760
2111
  systemAgent: boolean4("system_agent").default(false),
1761
- maxTokensPerJob: integer("max_tokens_per_job").default(0).notNull(),
2112
+ maxTokensPerJob: integer2("max_tokens_per_job").default(0).notNull(),
1762
2113
  // Cap on characters of agent_memory injected into the system prompt per job
1763
2114
  // (Memory Sprint 2). Pure char budget — token estimation done at call site
1764
2115
  // (length/4). 1500 chars ≈ ~375 tokens, similar to Hermes' 2200+1375 split.
1765
- memoryTokenBudget: integer("memory_token_budget").default(1500).notNull(),
2116
+ memoryTokenBudget: integer2("memory_token_budget").default(1500).notNull(),
1766
2117
  // User-controlled order on the /agents page (Brique A, migration 0019).
1767
2118
  // Default 0 — ties are broken by `name ASC` in the list query. Newly
1768
2119
  // created agents land at the front of their group by default; the user
1769
2120
  // can adjust via the ↑/↓ buttons in the UI.
1770
- position: integer("position").default(0).notNull(),
2121
+ position: integer2("position").default(0).notNull(),
1771
2122
  createdAt: timestamp4("created_at", { withTimezone: true }).defaultNow(),
1772
2123
  updatedAt: timestamp4("updated_at", { withTimezone: true }).defaultNow()
1773
2124
  },
@@ -1823,9 +2174,9 @@ var init_agents = __esm({
1823
2174
  entityId: uuid4("entity_id").references(() => entities.id, { onDelete: "cascade" }),
1824
2175
  dailyTokenLimit: bigint("daily_token_limit", { mode: "number" }).default(0),
1825
2176
  monthlyTokenLimit: bigint("monthly_token_limit", { mode: "number" }).default(0),
1826
- alertThresholdPct: integer("alert_threshold_pct").default(80),
2177
+ alertThresholdPct: integer2("alert_threshold_pct").default(80),
1827
2178
  autoPause: boolean4("auto_pause").default(false),
1828
- maxJobTokens: integer("max_job_tokens").default(15e4),
2179
+ maxJobTokens: integer2("max_job_tokens").default(15e4),
1829
2180
  createdAt: timestamp4("created_at", { withTimezone: true }).defaultNow(),
1830
2181
  updatedAt: timestamp4("updated_at", { withTimezone: true }).defaultNow()
1831
2182
  },
@@ -1845,7 +2196,7 @@ import {
1845
2196
  pgTable as pgTable5,
1846
2197
  text as text5,
1847
2198
  uuid as uuid5,
1848
- integer as integer2,
2199
+ integer as integer3,
1849
2200
  real,
1850
2201
  jsonb as jsonb3,
1851
2202
  timestamp as timestamp5,
@@ -1870,6 +2221,18 @@ var init_jobs = __esm({
1870
2221
  task: text5("task").notNull(),
1871
2222
  originalTask: text5("original_task"),
1872
2223
  chatId: text5("chat_id"),
2224
+ /**
2225
+ * Groups jobs from the same conversational thread for the Jobs page UI
2226
+ * (migration 0059). Purely a reporting/grouping concern — the runtime
2227
+ * execution path (delegation, cron, task-board) never reads this column.
2228
+ * NULL for non-conversational jobs (cron/schedule/webhook with no parent).
2229
+ * Stamped at creation by apps/runner/src/job/conversation-id.ts, reusing
2230
+ * the exact idle-reset session boundary thread-history.ts already uses
2231
+ * for chat continuity — see that file for the gap rule. Delegated/
2232
+ * task-board children inherit their creator's value; nothing else
2233
+ * mutates it after insert.
2234
+ */
2235
+ conversationId: uuid5("conversation_id"),
1873
2236
  systemPrompt: text5("system_prompt"),
1874
2237
  messages: jsonb3("messages").default(sql3`'[]'::jsonb`),
1875
2238
  /**
@@ -1881,16 +2244,16 @@ var init_jobs = __esm({
1881
2244
  */
1882
2245
  searchText: text5("search_text"),
1883
2246
  toolsUsed: text5("tools_used").array().default(sql3`'{}'::text[]`),
1884
- turn: integer2("turn").default(0),
2247
+ turn: integer3("turn").default(0),
1885
2248
  result: text5("result"),
1886
2249
  error: text5("error"),
1887
- chainCount: integer2("chain_count").default(0),
2250
+ chainCount: integer3("chain_count").default(0),
1888
2251
  requestId: text5("request_id"),
1889
2252
  parentJobId: uuid5("parent_job_id"),
1890
2253
  parentRequestId: text5("parent_request_id"),
1891
- totalDurationMs: integer2("total_duration_ms").default(0),
1892
- inputTokens: integer2("input_tokens").default(0),
1893
- outputTokens: integer2("output_tokens").default(0),
2254
+ totalDurationMs: integer3("total_duration_ms").default(0),
2255
+ inputTokens: integer3("input_tokens").default(0),
2256
+ outputTokens: integer3("output_tokens").default(0),
1894
2257
  /**
1895
2258
  * Cumulative EFFECTIVE (non-cached) input tokens = Σ(inputTokens −
1896
2259
  * cachedInputTokens) per turn. The token budget (Guard 1a) measures this,
@@ -1899,7 +2262,7 @@ var init_jobs = __esm({
1899
2262
  * Persisted alongside input_tokens so the budget stays cumulative across
1900
2263
  * self-chain resumes.
1901
2264
  */
1902
- effectiveInputTokens: integer2("effective_input_tokens").default(0),
2265
+ effectiveInputTokens: integer3("effective_input_tokens").default(0),
1903
2266
  /**
1904
2267
  * Cumulative real dollar cost billed to the provider across all turns of
1905
2268
  * this job (sum of per-call costs reported by the provider). Populated only
@@ -1922,7 +2285,7 @@ var init_jobs = __esm({
1922
2285
  * identity (Anthropic, Ollama, etc.) or when the field was absent.
1923
2286
  */
1924
2287
  servedProvider: text5("served_provider"),
1925
- delegationDepth: integer2("delegation_depth").default(0),
2288
+ delegationDepth: integer3("delegation_depth").default(0),
1926
2289
  /**
1927
2290
  * The slug of the last delegated child that failed on this parent job.
1928
2291
  * Set by `resumeDelegated` when a child returns `{status:'failed'}`, cleared
@@ -1952,6 +2315,9 @@ var init_jobs = __esm({
1952
2315
  sql3`${table.createdAt} DESC`
1953
2316
  ),
1954
2317
  index4("idx_agent_jobs_parent_job_id").on(table.parentJobId),
2318
+ // Migration 0059: the Jobs page groups conversational jobs by
2319
+ // (entity_id, conversation_id) — this index serves that lookup directly.
2320
+ index4("idx_agent_jobs_entity_conversation").on(table.entityId, table.conversationId),
1955
2321
  // idx_jobs_parent (F-20, audit #2) was an exact duplicate of
1956
2322
  // idx_agent_jobs_parent_job_id above — same column, same order, both from
1957
2323
  // migration 0000. Dropped in migration 0054; kept this one (matches the
@@ -1983,7 +2349,7 @@ import {
1983
2349
  pgTable as pgTable6,
1984
2350
  text as text6,
1985
2351
  uuid as uuid6,
1986
- integer as integer3,
2352
+ integer as integer4,
1987
2353
  jsonb as jsonb4,
1988
2354
  timestamp as timestamp6,
1989
2355
  numeric,
@@ -2016,8 +2382,8 @@ var init_tasks = __esm({
2016
2382
  assignedAgentId: uuid6("assigned_agent_id").references(() => agents.id, {
2017
2383
  onDelete: "cascade"
2018
2384
  }),
2019
- inputTokens: integer3("input_tokens").default(0),
2020
- outputTokens: integer3("output_tokens").default(0),
2385
+ inputTokens: integer4("input_tokens").default(0),
2386
+ outputTokens: integer4("output_tokens").default(0),
2021
2387
  costUsd: numeric("cost_usd", { precision: 10, scale: 6 }).default("0"),
2022
2388
  dependsOn: uuid6("depends_on").array().default(sql4`'{}'::uuid[]`),
2023
2389
  context: jsonb4("context").default(sql4`'{}'::jsonb`),
@@ -2105,7 +2471,10 @@ var init_connectors = __esm({
2105
2471
  name: text8("name").notNull(),
2106
2472
  slug: text8("slug").notNull(),
2107
2473
  baseUrl: text8("base_url"),
2108
- // Encrypted with pgp_sym_encrypt in DB raw value never returned in select
2474
+ // audit#2026-07-07 SEC-5: this comment used to claim pgp_sym_encrypt (pgcrypto),
2475
+ // which is not used anywhere. Real encryption is application-level AES-256-GCM
2476
+ // via @nodal-agents/secrets before the value ever reaches this column — the DB
2477
+ // column itself just holds ciphertext as opaque text. Raw value never returned in select.
2109
2478
  apiKey: text8("api_key"),
2110
2479
  active: boolean5("active").default(true),
2111
2480
  authType: text8("auth_type").notNull().default("api_key"),
@@ -2132,7 +2501,7 @@ var init_connectors = __esm({
2132
2501
  });
2133
2502
 
2134
2503
  // ../../packages/db/src/schema/tool_calls.ts
2135
- import { pgTable as pgTable9, text as text9, uuid as uuid9, integer as integer4, jsonb as jsonb5, timestamp as timestamp9, index as index8 } from "drizzle-orm/pg-core";
2504
+ import { pgTable as pgTable9, text as text9, uuid as uuid9, integer as integer5, jsonb as jsonb5, timestamp as timestamp9, index as index8 } from "drizzle-orm/pg-core";
2136
2505
  import { sql as sql7 } from "drizzle-orm";
2137
2506
  var toolCalls;
2138
2507
  var init_tool_calls = __esm({
@@ -2149,8 +2518,8 @@ var init_tool_calls = __esm({
2149
2518
  toolName: text9("tool_name").notNull(),
2150
2519
  toolInput: jsonb5("tool_input"),
2151
2520
  toolOutput: text9("tool_output"),
2152
- durationMs: integer4("duration_ms"),
2153
- turn: integer4("turn"),
2521
+ durationMs: integer5("duration_ms"),
2522
+ turn: integer5("turn"),
2154
2523
  createdAt: timestamp9("created_at", { withTimezone: true }).defaultNow()
2155
2524
  },
2156
2525
  (table) => [
@@ -2245,7 +2614,7 @@ import {
2245
2614
  pgTable as pgTable11,
2246
2615
  text as text11,
2247
2616
  uuid as uuid11,
2248
- integer as integer5,
2617
+ integer as integer6,
2249
2618
  boolean as boolean6,
2250
2619
  timestamp as timestamp11,
2251
2620
  customType,
@@ -2288,7 +2657,7 @@ var init_memory2 = __esm({
2288
2657
  */
2289
2658
  fact: text11("fact").notNull(),
2290
2659
  category: text11("category").default("context"),
2291
- importance: integer5("importance").default(3),
2660
+ importance: integer6("importance").default(3),
2292
2661
  source: text11("source").default("agent"),
2293
2662
  skillTags: text11("skill_tags").array().default(sql9`'{}'::text[]`),
2294
2663
  memoryLayer: text11("memory_layer"),
@@ -2304,7 +2673,7 @@ var init_memory2 = __esm({
2304
2673
  */
2305
2674
  importanceLocked: boolean6("importance_locked").notNull().default(false),
2306
2675
  lastAccessedAt: timestamp11("last_accessed_at", { withTimezone: true }).defaultNow(),
2307
- accessCount: integer5("access_count").default(0),
2676
+ accessCount: integer6("access_count").default(0),
2308
2677
  createdAt: timestamp11("created_at", { withTimezone: true }).defaultNow(),
2309
2678
  updatedAt: timestamp11("updated_at", { withTimezone: true }).defaultNow()
2310
2679
  },
@@ -2332,7 +2701,7 @@ var init_memory2 = __esm({
2332
2701
  });
2333
2702
 
2334
2703
  // ../../packages/db/src/schema/webhooks.ts
2335
- import { pgTable as pgTable12, text as text12, uuid as uuid12, boolean as boolean7, integer as integer6, timestamp as timestamp12, index as index11 } from "drizzle-orm/pg-core";
2704
+ import { pgTable as pgTable12, text as text12, uuid as uuid12, boolean as boolean7, integer as integer7, timestamp as timestamp12, index as index11 } from "drizzle-orm/pg-core";
2336
2705
  var webhookTriggers;
2337
2706
  var init_webhooks = __esm({
2338
2707
  "../../packages/db/src/schema/webhooks.ts"() {
@@ -2352,7 +2721,7 @@ var init_webhooks = __esm({
2352
2721
  // default: random 32-char hex (generated in app layer for Nodal-Agents)
2353
2722
  secret: text12("secret"),
2354
2723
  lastTriggeredAt: timestamp12("last_triggered_at", { withTimezone: true }),
2355
- triggerCount: integer6("trigger_count").default(0),
2724
+ triggerCount: integer7("trigger_count").default(0),
2356
2725
  createdAt: timestamp12("created_at", { withTimezone: true }).defaultNow(),
2357
2726
  updatedAt: timestamp12("updated_at", { withTimezone: true }).defaultNow()
2358
2727
  },
@@ -2367,7 +2736,7 @@ import {
2367
2736
  text as text13,
2368
2737
  uuid as uuid13,
2369
2738
  boolean as boolean8,
2370
- integer as integer7,
2739
+ integer as integer8,
2371
2740
  jsonb as jsonb7,
2372
2741
  timestamp as timestamp13,
2373
2742
  index as index12,
@@ -2419,7 +2788,7 @@ var init_skills = __esm({
2419
2788
  }),
2420
2789
  state: text13("state").notNull().default("active"),
2421
2790
  lastUsedAt: timestamp13("last_used_at", { withTimezone: true }),
2422
- patchCount: integer7("patch_count").notNull().default(0),
2791
+ patchCount: integer8("patch_count").notNull().default(0),
2423
2792
  archivedAt: timestamp13("archived_at", { withTimezone: true }),
2424
2793
  createdAt: timestamp13("created_at", { withTimezone: true }).defaultNow(),
2425
2794
  updatedAt: timestamp13("updated_at", { withTimezone: true }).defaultNow()
@@ -2449,7 +2818,7 @@ var init_skills = __esm({
2449
2818
  id: uuid13("id").primaryKey().defaultRandom(),
2450
2819
  entityId: uuid13("entity_id").references(() => entities.id, { onDelete: "cascade" }),
2451
2820
  skillId: uuid13("skill_id").references(() => agentSkills.id, { onDelete: "cascade" }),
2452
- version: integer7("version").notNull(),
2821
+ version: integer8("version").notNull(),
2453
2822
  content: text13("content").notNull(),
2454
2823
  name: text13("name").notNull(),
2455
2824
  createdAt: timestamp13("created_at", { withTimezone: true }).defaultNow()
@@ -2664,7 +3033,7 @@ import {
2664
3033
  text as text16,
2665
3034
  uuid as uuid16,
2666
3035
  boolean as boolean11,
2667
- integer as integer8,
3036
+ integer as integer9,
2668
3037
  jsonb as jsonb9,
2669
3038
  timestamp as timestamp16,
2670
3039
  index as index15,
@@ -2686,7 +3055,7 @@ var init_misc = __esm({
2686
3055
  agentId: uuid16("agent_id").references(() => agents.id, { onDelete: "cascade" }),
2687
3056
  messages: jsonb9("messages").notNull().default(sql13`'[]'::jsonb`),
2688
3057
  status: text16("status").notNull().default("active"),
2689
- turnCount: integer8("turn_count").notNull().default(0),
3058
+ turnCount: integer9("turn_count").notNull().default(0),
2690
3059
  createdAt: timestamp16("created_at", { withTimezone: true }).notNull().defaultNow(),
2691
3060
  updatedAt: timestamp16("updated_at", { withTimezone: true }).notNull().defaultNow()
2692
3061
  },
@@ -2731,7 +3100,7 @@ var init_misc = __esm({
2731
3100
  );
2732
3101
  rateLimits = pgTable16("rate_limits", {
2733
3102
  key: text16("key").primaryKey(),
2734
- count: integer8("count").default(1),
3103
+ count: integer9("count").default(1),
2735
3104
  windowStart: timestamp16("window_start", { withTimezone: true }).defaultNow()
2736
3105
  });
2737
3106
  }
@@ -2811,7 +3180,7 @@ var init_agent_connector_assignments = __esm({
2811
3180
  });
2812
3181
 
2813
3182
  // ../../packages/db/src/schema/agent-workspaces.ts
2814
- import { pgTable as pgTable19, text as text19, uuid as uuid19, integer as integer9, timestamp as timestamp19, uniqueIndex as uniqueIndex3, index as index17 } from "drizzle-orm/pg-core";
3183
+ import { pgTable as pgTable19, text as text19, uuid as uuid19, integer as integer10, timestamp as timestamp19, uniqueIndex as uniqueIndex3, index as index17 } from "drizzle-orm/pg-core";
2815
3184
  var agentWorkspaces;
2816
3185
  var init_agent_workspaces = __esm({
2817
3186
  "../../packages/db/src/schema/agent-workspaces.ts"() {
@@ -2833,7 +3202,7 @@ var init_agent_workspaces = __esm({
2833
3202
  // Absolute filesystem path — validated to be absolute at write time.
2834
3203
  path: text19("path").notNull(),
2835
3204
  // UI display order; lower = appears first. Default 0.
2836
- position: integer9("position").default(0).notNull(),
3205
+ position: integer10("position").default(0).notNull(),
2837
3206
  createdAt: timestamp19("created_at", { withTimezone: true }).defaultNow(),
2838
3207
  updatedAt: timestamp19("updated_at", { withTimezone: true }).defaultNow()
2839
3208
  },
@@ -5307,8 +5676,8 @@ var require_lib2 = __commonJS({
5307
5676
  return this;
5308
5677
  }
5309
5678
  var p = this.constructor;
5310
- return this.then(resolve3, reject2);
5311
- function resolve3(value) {
5679
+ return this.then(resolve4, reject2);
5680
+ function resolve4(value) {
5312
5681
  function yes() {
5313
5682
  return value;
5314
5683
  }
@@ -5461,8 +5830,8 @@ var require_lib2 = __commonJS({
5461
5830
  }
5462
5831
  return out;
5463
5832
  }
5464
- Promise2.resolve = resolve2;
5465
- function resolve2(value) {
5833
+ Promise2.resolve = resolve3;
5834
+ function resolve3(value) {
5466
5835
  if (value instanceof this) {
5467
5836
  return value;
5468
5837
  }
@@ -5993,10 +6362,10 @@ var require_utils = __commonJS({
5993
6362
  var promise = external.Promise.resolve(inputData).then(function(data) {
5994
6363
  var isBlob = support.blob && (data instanceof Blob || ["[object File]", "[object Blob]"].indexOf(Object.prototype.toString.call(data)) !== -1);
5995
6364
  if (isBlob && typeof FileReader !== "undefined") {
5996
- return new external.Promise(function(resolve2, reject) {
6365
+ return new external.Promise(function(resolve3, reject) {
5997
6366
  var reader = new FileReader();
5998
6367
  reader.onload = function(e) {
5999
- resolve2(e.target.result);
6368
+ resolve3(e.target.result);
6000
6369
  };
6001
6370
  reader.onerror = function(e) {
6002
6371
  reject(e.target.error);
@@ -6551,7 +6920,7 @@ var require_StreamHelper = __commonJS({
6551
6920
  }
6552
6921
  }
6553
6922
  function accumulate(helper, updateCallback) {
6554
- return new external.Promise(function(resolve2, reject) {
6923
+ return new external.Promise(function(resolve3, reject) {
6555
6924
  var dataArray = [];
6556
6925
  var chunkType = helper._internalType, resultType = helper._outputType, mimeType = helper._mimeType;
6557
6926
  helper.on("data", function(data, meta) {
@@ -6565,7 +6934,7 @@ var require_StreamHelper = __commonJS({
6565
6934
  }).on("end", function() {
6566
6935
  try {
6567
6936
  var result = transformZipOutput(resultType, concat(chunkType, dataArray), mimeType);
6568
- resolve2(result);
6937
+ resolve3(result);
6569
6938
  } catch (e) {
6570
6939
  reject(e);
6571
6940
  }
@@ -12678,7 +13047,7 @@ var require_load = __commonJS({
12678
13047
  var Crc32Probe = require_Crc32Probe();
12679
13048
  var nodejsUtils = require_nodejsUtils();
12680
13049
  function checkEntryCRC32(zipEntry) {
12681
- return new external.Promise(function(resolve2, reject) {
13050
+ return new external.Promise(function(resolve3, reject) {
12682
13051
  var worker = zipEntry.decompressed.getContentWorker().pipe(new Crc32Probe());
12683
13052
  worker.on("error", function(e) {
12684
13053
  reject(e);
@@ -12686,7 +13055,7 @@ var require_load = __commonJS({
12686
13055
  if (worker.streamInfo.crc32 !== zipEntry.decompressed.crc32) {
12687
13056
  reject(new Error("Corrupted zip : CRC32 mismatch"));
12688
13057
  } else {
12689
- resolve2();
13058
+ resolve3();
12690
13059
  }
12691
13060
  }).resume();
12692
13061
  });
@@ -15794,7 +16163,7 @@ var require_compile = __commonJS({
15794
16163
  const schOrFunc = root.refs[ref];
15795
16164
  if (schOrFunc)
15796
16165
  return schOrFunc;
15797
- let _sch = resolve2.call(this, root, ref);
16166
+ let _sch = resolve3.call(this, root, ref);
15798
16167
  if (_sch === void 0) {
15799
16168
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
15800
16169
  const { schemaId } = this.opts;
@@ -15821,7 +16190,7 @@ var require_compile = __commonJS({
15821
16190
  function sameSchemaEnv(s1, s2) {
15822
16191
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
15823
16192
  }
15824
- function resolve2(root, ref) {
16193
+ function resolve3(root, ref) {
15825
16194
  let sch;
15826
16195
  while (typeof (sch = this.refs[ref]) == "string")
15827
16196
  ref = sch;
@@ -16452,7 +16821,7 @@ var require_fast_uri = __commonJS({
16452
16821
  }
16453
16822
  return uri;
16454
16823
  }
16455
- function resolve2(baseURI, relativeURI, options) {
16824
+ function resolve3(baseURI, relativeURI, options) {
16456
16825
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
16457
16826
  const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
16458
16827
  schemelessOptions.skipEscape = true;
@@ -16710,7 +17079,7 @@ var require_fast_uri = __commonJS({
16710
17079
  var fastUri = {
16711
17080
  SCHEMES,
16712
17081
  normalize,
16713
- resolve: resolve2,
17082
+ resolve: resolve3,
16714
17083
  resolveComponent,
16715
17084
  equal,
16716
17085
  serialize,
@@ -19793,12 +20162,12 @@ var require_isexe = __commonJS({
19793
20162
  if (typeof Promise !== "function") {
19794
20163
  throw new TypeError("callback not provided");
19795
20164
  }
19796
- return new Promise(function(resolve2, reject) {
20165
+ return new Promise(function(resolve3, reject) {
19797
20166
  isexe(path, options || {}, function(er, is) {
19798
20167
  if (er) {
19799
20168
  reject(er);
19800
20169
  } else {
19801
- resolve2(is);
20170
+ resolve3(is);
19802
20171
  }
19803
20172
  });
19804
20173
  });
@@ -19864,27 +20233,27 @@ var require_which = __commonJS({
19864
20233
  opt = {};
19865
20234
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
19866
20235
  const found = [];
19867
- const step = (i) => new Promise((resolve2, reject) => {
20236
+ const step = (i) => new Promise((resolve3, reject) => {
19868
20237
  if (i === pathEnv.length)
19869
- return opt.all && found.length ? resolve2(found) : reject(getNotFoundError(cmd));
20238
+ return opt.all && found.length ? resolve3(found) : reject(getNotFoundError(cmd));
19870
20239
  const ppRaw = pathEnv[i];
19871
20240
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
19872
20241
  const pCmd = path.join(pathPart, cmd);
19873
20242
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
19874
- resolve2(subStep(p, i, 0));
20243
+ resolve3(subStep(p, i, 0));
19875
20244
  });
19876
- const subStep = (p, i, ii) => new Promise((resolve2, reject) => {
20245
+ const subStep = (p, i, ii) => new Promise((resolve3, reject) => {
19877
20246
  if (ii === pathExt.length)
19878
- return resolve2(step(i + 1));
20247
+ return resolve3(step(i + 1));
19879
20248
  const ext = pathExt[ii];
19880
20249
  isexe(p + ext, { pathExt: pathExtExe }, (er, is) => {
19881
20250
  if (!er && is) {
19882
20251
  if (opt.all)
19883
20252
  found.push(p + ext);
19884
20253
  else
19885
- return resolve2(p + ext);
20254
+ return resolve3(p + ext);
19886
20255
  }
19887
- return resolve2(subStep(p, i, ii + 1));
20256
+ return resolve3(subStep(p, i, ii + 1));
19888
20257
  });
19889
20258
  });
19890
20259
  return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
@@ -27653,7 +28022,17 @@ var envSchema = z.object({
27653
28022
  //
27654
28023
  // Set this ONLY if you explicitly want to cap DB growth. Deleting job history is
27655
28024
  // irreversible. Start with a large value (e.g. 90) and reduce over time.
27656
- RETENTION_DAYS: z.coerce.number().default(0)
28025
+ RETENTION_DAYS: z.coerce.number().default(0),
28026
+ // ─── Approval grace window (Lot A1) ───────────────────────────────────────────
28027
+ // A gated tool normally suspends the job to `awaiting_approval` the instant it
28028
+ // fires — resuming later pays a full executeJob restart (~80-105s measured:
28029
+ // agent/skill/tool reload, thread history, system prompt). A human watching
28030
+ // the screen typically approves in 5-30s, so that whole restart is wasted. This
28031
+ // caps an in-process wait (poll approval_requests) BEFORE actually suspending:
28032
+ // if the decision lands inside the window, the runner executes it inline and
28033
+ // keeps looping — no suspend, no restart. 0 = disabled (suspend immediately,
28034
+ // the pre-existing behavior).
28035
+ NODALAI_APPROVAL_GRACE_MS: z.coerce.number().min(0).default(12e4)
27657
28036
  });
27658
28037
  var _env;
27659
28038
  function parseEnv(raw = process.env) {
@@ -27723,8 +28102,22 @@ import { eq, sql as sql16 } from "drizzle-orm";
27723
28102
  // ../../packages/secrets/src/index.ts
27724
28103
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
27725
28104
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
28105
+ import { execFileSync } from "node:child_process";
27726
28106
  import { homedir } from "node:os";
27727
28107
  import { dirname, join } from "node:path";
28108
+ function restrictFileToOwner(path) {
28109
+ try {
28110
+ chmodSync(path, 384);
28111
+ } catch {
28112
+ }
28113
+ if (process.platform !== "win32") return;
28114
+ const user = process.env["USERNAME"] || process.env["USER"];
28115
+ if (!user) return;
28116
+ try {
28117
+ execFileSync("icacls", [path, "/inheritance:r", "/grant:r", `${user}:F`], { stdio: "ignore" });
28118
+ } catch {
28119
+ }
28120
+ }
27728
28121
  var VERSION = "v1";
27729
28122
  var ALGO = "aes-256-gcm";
27730
28123
  var KEY_BYTES = 32;
@@ -27750,10 +28143,7 @@ function loadOrCreateMasterKey(path = masterKeyPath()) {
27750
28143
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
27751
28144
  const buf = randomBytes(KEY_BYTES);
27752
28145
  writeFileSync(path, buf.toString("base64"), { mode: 384 });
27753
- try {
27754
- chmodSync(path, 384);
27755
- } catch {
27756
- }
28146
+ restrictFileToOwner(path);
27757
28147
  cachedMasterKey = buf;
27758
28148
  return buf;
27759
28149
  }
@@ -27931,6 +28321,8 @@ async function getDecryptedCredentialById(db, credentialId) {
27931
28321
  // ../../packages/db/src/queries/master-key-guard.ts
27932
28322
  init_credentials();
27933
28323
  init_llm_keys();
28324
+ init_connectors();
28325
+ init_mcp();
27934
28326
 
27935
28327
  // ../../packages/db/src/repos/agents.ts
27936
28328
  init_src();
@@ -28321,6 +28713,11 @@ var ProviderConfigError = class extends Error {
28321
28713
  detail;
28322
28714
  code = "provider_config_error";
28323
28715
  };
28716
+ function isContextOverflowError(err) {
28717
+ const msg = (err instanceof Error ? err.message : String(err ?? "")).toLowerCase();
28718
+ if (!msg) return false;
28719
+ return msg.includes("context length") || msg.includes("context window") || msg.includes("context_length_exceeded") || msg.includes("maximum context") || msg.includes("exceed context") || msg.includes("exceeds context") || msg.includes("prompt is too long") || msg.includes("too many tokens") || msg.includes("reduce") && msg.includes("length") && msg.includes("token");
28720
+ }
28324
28721
 
28325
28722
  // ../../packages/llm/src/client.ts
28326
28723
  import { generateText, streamText, generateObject } from "ai";
@@ -28616,7 +29013,7 @@ function logAttempt({
28616
29013
  console.warn(`[llm-attempt-failed] ${parts.join(" ")}`);
28617
29014
  }
28618
29015
  function sleep(ms) {
28619
- return new Promise((resolve2) => setTimeout(resolve2, ms));
29016
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
28620
29017
  }
28621
29018
 
28622
29019
  // ../../packages/llm/src/tool-choice-floor.ts
@@ -29658,192 +30055,8 @@ function createToolRegistry() {
29658
30055
  };
29659
30056
  }
29660
30057
 
29661
- // ../../packages/tools/src/catastrophic-command.ts
29662
- var FORK_BOMB = /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&\s*\}\s*;\s*:/;
29663
- var MKFS = /\bmkfs(\.\w+)?\b/i;
29664
- var DD_TO_DEVICE = /\bdd\b[\s\S]*?\bof=\/dev\/[a-z]/i;
29665
- var POWER_STATE = /\b(shutdown|reboot|halt|poweroff|stop-computer|restart-computer)\b/i;
29666
- var INIT_RUNLEVEL = /\binit\s+[06]\b/;
29667
- var OVERWRITE_DEVICE = />\s*\/dev\/(sd[a-z]|nvme\d|disk\d|hd[a-z])/i;
29668
- var DISKPART = /\bdiskpart\b/i;
29669
- var DISK_CMDLET = /\b(format-volume|clear-disk|initialize-disk)\b/i;
29670
- var AWK_CODE_EXEC = /\bawk\b[\s\S]*?(\bsystem\s*\(|\bgetline\b[^;]*\||["'][^"']*\|\s*["']?\s*(?:sh|bash|cmd))/i;
29671
- function stripQuotes(token) {
29672
- return token.replace(/^["']|["']$/g, "");
29673
- }
29674
- function normalizeSlashes(s) {
29675
- return s.replace(/\/{2,}/g, "/").replace(/\\{2,}/g, "\\");
29676
- }
29677
- var WRAPPER_LEADER = /* @__PURE__ */ new Set([
29678
- "cmd",
29679
- "cmd.exe",
29680
- "powershell",
29681
- "powershell.exe",
29682
- "pwsh",
29683
- "pwsh.exe",
29684
- "sh",
29685
- "bash",
29686
- "sudo"
29687
- ]);
29688
- function stripWrapperPrefix(tokens) {
29689
- let i = 0;
29690
- while (i < tokens.length && WRAPPER_LEADER.has((tokens[i] ?? "").toLowerCase())) {
29691
- i += 1;
29692
- if (i < tokens.length && /^[-/]/.test(tokens[i] ?? "")) {
29693
- i += 1;
29694
- }
29695
- }
29696
- return tokens.slice(i);
29697
- }
29698
- var PASSTHROUGH_LEADERS = /* @__PURE__ */ new Set(["sudo", "env", "cmd", "cmd.exe"]);
29699
- function interpreterBasename(token) {
29700
- const t = stripQuotes(token);
29701
- const base = t.split(/[\\/]/).pop() ?? t;
29702
- return base.replace(/\.(exe|com)$/i, "").toLowerCase();
29703
- }
29704
- function skipPassthroughLeaders(tokens) {
29705
- let i = 0;
29706
- while (i < tokens.length && PASSTHROUGH_LEADERS.has(interpreterBasename(tokens[i] ?? ""))) {
29707
- i += 1;
29708
- while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i] ?? "")) i += 1;
29709
- if (i < tokens.length && /^[-/]/.test(tokens[i] ?? "")) i += 1;
29710
- }
29711
- return tokens.slice(i);
29712
- }
29713
- function interpreterKind(name) {
29714
- if (name === "py" || /^python[0-9.]*$/.test(name)) return "python";
29715
- if (/^node(js)?$/.test(name)) return "node";
29716
- if (/^perl[0-9.]*$/.test(name)) return "perl";
29717
- if (/^ruby[0-9.]*$/.test(name)) return "ruby";
29718
- if (/^php[0-9.]*$/.test(name)) return "php";
29719
- if (["sh", "bash", "zsh", "ksh", "dash", "ash"].includes(name)) return "shell";
29720
- if (name === "powershell" || name === "pwsh") return "powershell";
29721
- return null;
29722
- }
29723
- function isInlineEvalFlag(kind, flag) {
29724
- switch (kind) {
29725
- case "python":
29726
- return flag === "-c";
29727
- case "node":
29728
- return flag === "-e" || flag === "--eval" || flag === "-p" || flag === "--print";
29729
- case "perl":
29730
- case "ruby":
29731
- return flag === "-e";
29732
- case "php":
29733
- return flag === "-r";
29734
- case "shell":
29735
- return flag === "-c";
29736
- case "powershell":
29737
- return /^-com/.test(flag) || /^-enc/.test(flag);
29738
- default:
29739
- return false;
29740
- }
29741
- }
29742
- function hasInlineInterpreterEval(tokens) {
29743
- const rest = skipPassthroughLeaders(tokens);
29744
- if (rest.length === 0) return false;
29745
- const kind = interpreterKind(interpreterBasename(rest[0] ?? ""));
29746
- if (!kind) return false;
29747
- return rest.slice(1).some((t) => isInlineEvalFlag(kind, stripQuotes(t).toLowerCase()));
29748
- }
29749
- function hasPipeIntoBareInterpreter(c) {
29750
- const parts = c.split("|");
29751
- for (let i = 1; i < parts.length; i++) {
29752
- const toks = (parts[i] ?? "").trim().split(/\s+/).map(stripQuotes).filter((t) => t.length > 0);
29753
- const rest = skipPassthroughLeaders(toks);
29754
- if (rest.length === 0) continue;
29755
- if (!interpreterKind(interpreterBasename(rest[0] ?? ""))) continue;
29756
- const hasScriptArg = rest.slice(1).some((t) => !t.startsWith("-"));
29757
- if (!hasScriptArg) return true;
29758
- }
29759
- return false;
29760
- }
29761
- function isWindowsRootOrWildcardTarget(token) {
29762
- const t = stripQuotes(token);
29763
- if (t === "*" || t === "\\" || t === "/") return true;
29764
- if (/^[a-z]:\\?\*?$/i.test(t)) return true;
29765
- if (/^%(systemdrive|systemroot|userprofile)%\\?\*?$/i.test(t)) return true;
29766
- if (/^\$env:(systemdrive|systemroot|userprofile)\\?\*?$/i.test(t)) return true;
29767
- return false;
29768
- }
29769
- function isCatastrophicCommand(cmd) {
29770
- if (typeof cmd !== "string" || cmd.trim() === "") return false;
29771
- const c = normalizeSlashes(cmd.trim());
29772
- if (FORK_BOMB.test(c) || MKFS.test(c) || DD_TO_DEVICE.test(c) || POWER_STATE.test(c) || INIT_RUNLEVEL.test(c) || OVERWRITE_DEVICE.test(c) || DISKPART.test(c) || DISK_CMDLET.test(c) || AWK_CODE_EXEC.test(c) || hasPipeIntoBareInterpreter(c)) {
29773
- return true;
29774
- }
29775
- for (const seg of c.split(/[;&|\n\r]+/)) {
29776
- const s = seg.trim();
29777
- if (!s) continue;
29778
- const tokens = s.split(/\s+/).map(stripQuotes);
29779
- if (hasInlineInterpreterEval(tokens)) return true;
29780
- const cmdTokens = stripWrapperPrefix(tokens);
29781
- const cmdWord = cmdTokens[0] ?? "";
29782
- if (/^rm$/i.test(cmdWord)) {
29783
- const recursive = /\s-\S*r/i.test(s) || /\s--recursive\b/i.test(s);
29784
- const force = /\s-\S*f/i.test(s) || /\s--force\b/i.test(s);
29785
- if (recursive && force) {
29786
- if (/\s--no-preserve-root\b/i.test(s)) return true;
29787
- if (/(\s|=)(\/|\/\*|~|~\/\*?|\$HOME\/?\*?|\*)(\s|$|"|')/.test(s)) return true;
29788
- if (tokens.some((t) => isWindowsRootOrWildcardTarget(t))) return true;
29789
- }
29790
- }
29791
- if (/^format(\.(com|exe))?$/i.test(cmdWord) && cmdTokens.slice(1).some((d) => /^[a-z]:([\\/]\*?)?$/i.test(d))) {
29792
- return true;
29793
- }
29794
- if (/^(ri|remove-item|del|erase|rd|rmdir)$/i.test(cmdWord)) {
29795
- const psRecursiveForce = /(^|\s)-r(ecurse)?\b/i.test(s) && /(^|\s)-f(orce)?\b/i.test(s);
29796
- const cmdRecursiveForce = /\/s\b/i.test(s) && /\/q\b/i.test(s);
29797
- if (psRecursiveForce || cmdRecursiveForce) {
29798
- if (tokens.some((t) => isWindowsRootOrWildcardTarget(t))) return true;
29799
- }
29800
- }
29801
- }
29802
- return false;
29803
- }
29804
- function isInlineInterpreterEvalCommand(cmd) {
29805
- if (typeof cmd !== "string" || cmd.trim() === "") return false;
29806
- const c = normalizeSlashes(cmd.trim());
29807
- for (const seg of c.split(/[;&|\n\r]+/)) {
29808
- const s = seg.trim();
29809
- if (!s) continue;
29810
- const tokens = s.split(/\s+/).map(stripQuotes);
29811
- if (hasInlineInterpreterEval(tokens)) return true;
29812
- }
29813
- return false;
29814
- }
29815
- var DESTRUCTIVE_PATTERNS = [
29816
- /\b(rm|rmdir|unlink|shred)\b/i,
29817
- // delete (unix)
29818
- /\bdel\s|\bRemove-Item\b|\brd\s+\/s/i,
29819
- // delete (windows/ps)
29820
- /\bfind\b[^\n]*-delete\b/i,
29821
- // find … -delete
29822
- /\b(pip3?|npm|pnpm|yarn|apt|apt-get|yum|dnf|brew|pacman|choco|winget|uvx|pipx|cargo|gem|conda|comfy)\b[^\n]*\binstall\b/i,
29823
- // pkg install
29824
- /\bgo\s+install\b|\bcomfy\b[^\n]*\bmodel\s+download\b|\bpip3?\b[^\n]*\bdownload\b/i,
29825
- // go install / model dl
29826
- /\bwget\b|\bgit\s+clone\b|\bcurl\b[^\n]*\s-[oO]\b|\bInvoke-WebRequest\b|\biwr\b[^\n]*-OutFile/i,
29827
- // large download / clone
29828
- /\b(kill|pkill|killall|taskkill)\b|\bStop-Process\b|\bStop-Service\b/i,
29829
- // process kill
29830
- /\bsystemctl\b|\bsc\s+(stop|delete)\b|\bservice\b[^\n]*\b(stop|restart)\b/i,
29831
- // service control
29832
- /\bmkfs(\.\w+)?\b|\bdd\b[^\n]*\bof=|\b(format|fdisk|parted|diskpart)\b/i,
29833
- // disk ops
29834
- /\bchmod\s+-\S*R|\bchown\s+-\S*R|\bicacls\b/i,
29835
- // recursive perms/ownership
29836
- /\bgit\s+push\b[^\n]*(--force|-f\b)|\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-\S*f|\bgit\s+branch\s+-D\b/i
29837
- // destructive VCS
29838
- ];
29839
- function isDestructiveOrHeavyCommand(cmd) {
29840
- if (typeof cmd !== "string" || cmd.trim() === "") return false;
29841
- if (isCatastrophicCommand(cmd)) return true;
29842
- const c = normalizeSlashes(cmd.trim());
29843
- return DESTRUCTIVE_PATTERNS.some((re) => re.test(c));
29844
- }
29845
-
29846
30058
  // ../../packages/tools/src/execute.ts
30059
+ init_src();
29847
30060
  async function executeTool(tool, rawInput, ctx, opts) {
29848
30061
  const startMs = Date.now();
29849
30062
  const parsed = tool.inputSchema.safeParse(rawInput);
@@ -29864,12 +30077,18 @@ async function executeTool(tool, rawInput, ctx, opts) {
29864
30077
  effectiveAction = "auto_approve";
29865
30078
  } else if (opts.autonomy === "destructive_gate") {
29866
30079
  const command = String(validatedInput?.command ?? "");
30080
+ const mcpTransport = String(validatedInput?.transport ?? "");
29867
30081
  let isHeavy;
29868
30082
  if (tool.name === "run_command") isHeavy = isDestructiveOrHeavyCommand(command);
30083
+ else if (tool.name === "create_mcp" && mcpTransport === "stdio") isHeavy = true;
29869
30084
  else isHeavy = tool.riskLevel === "destructive";
29870
30085
  if (!isHeavy) effectiveAction = "auto_approve";
29871
30086
  }
29872
30087
  }
30088
+ if (!matchedRule && effectiveAction !== "block" && effectiveAction !== "require_approval" && tool.computeApproval && opts.autonomy !== "fully_autonomous") {
30089
+ const dynamic = await tool.computeApproval(validatedInput, ctx);
30090
+ if (dynamic === "require_approval") effectiveAction = "require_approval";
30091
+ }
29873
30092
  if (tool.name === "run_command" && effectiveAction !== "block" && effectiveAction !== "require_approval" && isCatastrophicCommand(String(validatedInput?.command ?? ""))) {
29874
30093
  effectiveAction = "require_approval";
29875
30094
  }
@@ -29890,6 +30109,13 @@ async function executeTool(tool, rawInput, ctx, opts) {
29890
30109
  jobId: ctx.jobId,
29891
30110
  agentId: ctx.agentId,
29892
30111
  toolName: tool.name,
30112
+ // NOUVEAU-1: unlike tool_calls, this column is NOT redacted at rest —
30113
+ // the approval re-exec path (apps/runner/src/job/execute.ts) reads it
30114
+ // back verbatim to re-run the approved call, so a redacted apiKey here
30115
+ // would store '***' as the real secret. Instead, every DISPLAY of this
30116
+ // row is redacted at load (web approvals loader + Telegram notify).
30117
+ // Encrypting the secret fields at rest here (and decrypting on re-exec)
30118
+ // is the tracked follow-up.
29893
30119
  toolInput: validatedInput,
29894
30120
  status: "pending"
29895
30121
  }).returning();
@@ -29970,7 +30196,12 @@ async function _writeToolCall(ctx, toolName, input, output, durationMs) {
29970
30196
  entityId: ctx.entityId,
29971
30197
  jobId: ctx.jobId,
29972
30198
  toolName,
29973
- toolInput: input,
30199
+ // NOUVEAU-1: the audit trail is never re-executed, so we store a
30200
+ // secret-redacted copy — create_connector/create_mcp API keys and stdio
30201
+ // env values must not sit in cleartext in tool_calls or render to the
30202
+ // logs dashboard (LogsTable). The real value already reached the tool's
30203
+ // execute() above; only this copy is masked.
30204
+ toolInput: redactSecretsForAudit(input),
29974
30205
  toolOutput: output,
29975
30206
  durationMs
29976
30207
  });
@@ -29978,6 +30209,9 @@ async function _writeToolCall(ctx, toolName, input, output, durationMs) {
29978
30209
  }
29979
30210
  }
29980
30211
 
30212
+ // ../../packages/tools/src/index.ts
30213
+ init_src();
30214
+
29981
30215
  // ../../packages/tools/src/tool-choice.ts
29982
30216
  function computeToolChoice(cfg) {
29983
30217
  const { isOrchestrator, turn, hasAdapterTools, modelSupportsForcedToolChoice = true } = cfg;
@@ -30309,7 +30543,7 @@ async function backfillEmbeddings(db, embeddingClient) {
30309
30543
  console.warn(`[memory] backfill: row ${row.id} failed (will retry next run):`, err);
30310
30544
  }
30311
30545
  if ((i + 1) % BATCH_SIZE === 0 && i + 1 < rows.length) {
30312
- await new Promise((resolve2) => setTimeout(resolve2, BATCH_PAUSE_MS));
30546
+ await new Promise((resolve3) => setTimeout(resolve3, BATCH_PAUSE_MS));
30313
30547
  }
30314
30548
  }
30315
30549
  return { candidates: rows.length, embedded };
@@ -30533,20 +30767,28 @@ async function selectMemoriesForInjection(db, opts) {
30533
30767
  );
30534
30768
  const rawQuery = opts.query ?? "";
30535
30769
  const tokens = tokenizeQuery(rawQuery);
30770
+ let packed;
30536
30771
  if (tokens.length > 0) {
30537
30772
  const tsv = sql20`"agent_memory"."search_tsv"`;
30538
- const tsq = sql20`plainto_tsquery('english', ${rawQuery})`;
30539
- const candidates2 = await db.select().from(agentMemory).where(baseConditions).orderBy(
30773
+ const tsq = sql20`to_tsquery('english', ${tokens.join(" | ")})`;
30774
+ const candidates = await db.select().from(agentMemory).where(baseConditions).orderBy(
30540
30775
  sql20`ts_rank(${tsv}, ${tsq}) DESC`,
30541
30776
  desc(agentMemory.importance),
30542
30777
  desc(agentMemory.lastAccessedAt)
30543
30778
  ).limit(MAX_CANDIDATES);
30544
- const memories2 = candidates2.map((row) => rowToMemory(row));
30545
- return packUnderBudget(memories2, opts.maxChars);
30779
+ const memories = candidates.map((row) => rowToMemory(row));
30780
+ packed = packUnderBudget(memories, opts.maxChars);
30781
+ } else {
30782
+ const candidates = await db.select().from(agentMemory).where(baseConditions).orderBy(desc(agentMemory.importance), desc(agentMemory.lastAccessedAt)).limit(MAX_CANDIDATES);
30783
+ const memories = candidates.map((row) => rowToMemory(row));
30784
+ packed = selectMemoriesUnderBudget(memories, opts.maxChars, opts.query);
30546
30785
  }
30547
- const candidates = await db.select().from(agentMemory).where(baseConditions).orderBy(desc(agentMemory.importance), desc(agentMemory.lastAccessedAt)).limit(MAX_CANDIDATES);
30548
- const memories = candidates.map((row) => rowToMemory(row));
30549
- return selectMemoriesUnderBudget(memories, opts.maxChars, opts.query);
30786
+ await touchMemories(
30787
+ db,
30788
+ packed.map((m) => m.id),
30789
+ opts.entityId
30790
+ );
30791
+ return packed;
30550
30792
  }
30551
30793
 
30552
30794
  // ../../packages/memory/src/find.ts
@@ -31067,6 +31309,28 @@ function getWorkspaceRootForDisplay(ctx, relativeOrLabeledPath) {
31067
31309
  }
31068
31310
  return list[0].path;
31069
31311
  }
31312
+ async function isPathInSharedWorkspace(ctx, resolvedPath) {
31313
+ const shared = ctx.workspaces?.find((ws) => ws.label === SHARED_WORKSPACE_LABEL);
31314
+ if (!shared) return false;
31315
+ const realShared = await realpath(shared.path).catch(() => void 0);
31316
+ if (!realShared) return false;
31317
+ const rootWithSep = realShared.endsWith(sep) ? realShared : realShared + sep;
31318
+ return resolvedPath === realShared || resolvedPath.startsWith(rootWithSep);
31319
+ }
31320
+ async function computeSharedOverwriteApproval(ctx, requestedPath) {
31321
+ let resolved;
31322
+ try {
31323
+ resolved = await resolveAndCheckPath(ctx, requestedPath);
31324
+ } catch {
31325
+ return void 0;
31326
+ }
31327
+ if (!await isPathInSharedWorkspace(ctx, resolved)) return void 0;
31328
+ const exists = await stat(resolved).then(
31329
+ () => true,
31330
+ () => false
31331
+ );
31332
+ return exists ? "require_approval" : void 0;
31333
+ }
31070
31334
 
31071
31335
  // ../../packages/tools/src/builtin/file-ops/read-lines.ts
31072
31336
  import { createReadStream } from "node:fs";
@@ -31210,13 +31474,20 @@ import { z as z28 } from "zod";
31210
31474
  var FileWriteInputSchema = z28.object({
31211
31475
  path: z28.string().min(1).describe("Path to the file, relative to the agent workspace root. Created if missing."),
31212
31476
  content: z28.string().describe("Full file contents to write. Overwrites any existing file."),
31213
- create_dirs: z28.boolean().optional().default(false).describe("If true, create missing parent directories.")
31477
+ create_dirs: z28.boolean().optional().default(false).describe("If true, create missing parent directories."),
31478
+ purpose: z28.string().optional().describe(
31479
+ "OPTIONAL. One short sentence on WHY this write is needed. Shown first on the approval card when this call requires human review (e.g. overwriting an existing file in the shared workspace) \u2014 say why in plain language, not what you already say in `path`/`content`."
31480
+ )
31214
31481
  });
31215
31482
  var fileWriteTool = {
31216
31483
  name: "file_write",
31217
31484
  description: "Write (or overwrite) a file in the agent workspace. Atomic: writes to a tempfile then renames over the target, so partial-write failures never corrupt the original. Use `file_edit` instead when you only need to change part of an existing file \u2014 it preserves lines you do not touch. Max 1 MiB per write.",
31218
31485
  inputSchema: FileWriteInputSchema,
31219
31486
  riskLevel: "write",
31487
+ // D1: gate ONLY the destructive case — overwriting a file that already
31488
+ // exists in the entity-wide SHARED workspace. A brand-new file, or a write
31489
+ // into an attached/private workspace, never gates (see computeSharedOverwriteApproval).
31490
+ computeApproval: (input, ctx) => computeSharedOverwriteApproval(ctx, input.path),
31220
31491
  execute: async (input, ctx) => {
31221
31492
  try {
31222
31493
  const bytes = Buffer.byteLength(input.content, "utf8");
@@ -31265,13 +31536,20 @@ var FileEditInputSchema = z29.object({
31265
31536
  "Exact substring to replace. Must appear in the file verbatim \u2014 whitespace and line endings count. Must be unique unless replace_all:true."
31266
31537
  ),
31267
31538
  new_string: z29.string().describe('Replacement text. Pass "" to delete the matched substring.'),
31268
- replace_all: z29.boolean().optional().default(false).describe("Replace every occurrence (default false: require exactly one match).")
31539
+ replace_all: z29.boolean().optional().default(false).describe("Replace every occurrence (default false: require exactly one match)."),
31540
+ purpose: z29.string().optional().describe(
31541
+ "OPTIONAL. One short sentence on WHY this edit is needed. Shown first on the approval card when this call requires human review (e.g. editing a file in the shared workspace) \u2014 say why in plain language, not what you already say in `path`."
31542
+ )
31269
31543
  });
31270
31544
  var fileEditTool = {
31271
31545
  name: "file_edit",
31272
31546
  description: "Edit an existing file by replacing an exact substring. Quote the chunk to change verbatim (including whitespace). Fails loud when the substring is missing (you misquoted) or matches multiple places (ambiguous \u2014 narrow the quote, or pass replace_all:true). Use this instead of file_write when you only want to change part of a file \u2014 preserves untouched lines.",
31273
31547
  inputSchema: FileEditInputSchema,
31274
31548
  riskLevel: "write",
31549
+ // D1: an edit always targets an EXISTING file (file_edit fails loud on a
31550
+ // missing one — see execute() below), so the only thing left to check is
31551
+ // whether that file lives in the shared workspace. Same gate as file_write.
31552
+ computeApproval: (input, ctx) => computeSharedOverwriteApproval(ctx, input.path),
31275
31553
  execute: async (input, ctx) => {
31276
31554
  try {
31277
31555
  const path = await resolveAndCheckPath(ctx, input.path);
@@ -31595,7 +31873,9 @@ async function validateOfficeZipBomb(buf, maxInflatedBytes = MAX_OFFICE_INFLATED
31595
31873
  nodeStream.on("error", (err) => {
31596
31874
  if (settled) return;
31597
31875
  settled = true;
31598
- reject(new Error(`Corrupt entry "${entry.name}" inside the Office archive: ${err.message}`));
31876
+ reject(
31877
+ new Error(`Corrupt entry "${entry.name}" inside the Office archive: ${err.message}`)
31878
+ );
31599
31879
  });
31600
31880
  });
31601
31881
  if (violation) return { ok: false, reason: violation };
@@ -34428,7 +34708,10 @@ async function resolveSkillId(db, entityId, slugOrName) {
34428
34708
  const [row] = await db.select({ id: agentSkills.id }).from(agentSkills).where(
34429
34709
  and4(
34430
34710
  eq5(agentSkills.entityId, entityId),
34431
- or3(eq5(agentSkills.slug, slugOrName), ilike2(agentSkills.name, escapeLikePattern(slugOrName)))
34711
+ or3(
34712
+ eq5(agentSkills.slug, slugOrName),
34713
+ ilike2(agentSkills.name, escapeLikePattern(slugOrName))
34714
+ )
34432
34715
  )
34433
34716
  ).limit(1);
34434
34717
  return row?.id ?? null;
@@ -35503,7 +35786,7 @@ var runCommandTool = {
35503
35786
  }
35504
35787
  };
35505
35788
  function runInShell(command, cwd, timeoutMs) {
35506
- return new Promise((resolve2) => {
35789
+ return new Promise((resolve3) => {
35507
35790
  const isWindows = process.platform === "win32";
35508
35791
  const child = spawn(command, {
35509
35792
  cwd,
@@ -35566,7 +35849,7 @@ function runInShell(command, cwd, timeoutMs) {
35566
35849
  settled = true;
35567
35850
  clearTimeout(timer);
35568
35851
  if (graceTimer) clearTimeout(graceTimer);
35569
- resolve2({ exitCode, stdout, stderr, timedOut, truncated, cwd });
35852
+ resolve3({ exitCode, stdout, stderr, timedOut, truncated, cwd });
35570
35853
  };
35571
35854
  const timer = setTimeout(() => {
35572
35855
  timedOut = true;
@@ -35655,7 +35938,7 @@ var runSkillScriptTool = {
35655
35938
  }
35656
35939
  };
35657
35940
  function runScript(interpreter, argv, cwd, timeoutMs, scriptLabel) {
35658
- return new Promise((resolve2) => {
35941
+ return new Promise((resolve3) => {
35659
35942
  const isWindows = process.platform === "win32";
35660
35943
  const child = spawn2(interpreter, argv, {
35661
35944
  cwd,
@@ -35715,7 +35998,7 @@ function runScript(interpreter, argv, cwd, timeoutMs, scriptLabel) {
35715
35998
  settled = true;
35716
35999
  clearTimeout(timer);
35717
36000
  if (graceTimer) clearTimeout(graceTimer);
35718
- resolve2({ exitCode, stdout, stderr, timedOut, truncated, interpreter, script: scriptLabel });
36001
+ resolve3({ exitCode, stdout, stderr, timedOut, truncated, interpreter, script: scriptLabel });
35719
36002
  };
35720
36003
  const timer = setTimeout(() => {
35721
36004
  timedOut = true;
@@ -35983,7 +36266,7 @@ ${line}` : line;
35983
36266
  return chunks;
35984
36267
  }
35985
36268
  function sleep2(ms) {
35986
- return new Promise((resolve2) => setTimeout(resolve2, ms));
36269
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
35987
36270
  }
35988
36271
  var MAX_RATE_LIMIT_RETRIES = 3;
35989
36272
  var DEFAULT_RATE_LIMIT_BACKOFF_MS = 1e3;
@@ -36461,8 +36744,11 @@ Fail conditions:
36461
36744
  err.name = "telegram_no_recipient";
36462
36745
  throw err;
36463
36746
  }
36464
- const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
36465
- const botToken = agentRows[0]?.telegramBotToken;
36747
+ let botToken = ctx.resolvedTelegramBotToken;
36748
+ if (botToken === void 0) {
36749
+ const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
36750
+ botToken = agentRows[0]?.telegramBotToken ?? void 0;
36751
+ }
36466
36752
  if (!botToken) {
36467
36753
  const err = new Error("telegram_no_bot_token");
36468
36754
  err.name = "telegram_no_bot_token";
@@ -36527,8 +36813,11 @@ Fail conditions:
36527
36813
  err.name = "no_recipient";
36528
36814
  throw err;
36529
36815
  }
36530
- const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
36531
- const botToken = agentRows[0]?.telegramBotToken;
36816
+ let botToken = ctx.resolvedTelegramBotToken;
36817
+ if (botToken === void 0) {
36818
+ const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
36819
+ botToken = agentRows[0]?.telegramBotToken ?? void 0;
36820
+ }
36532
36821
  if (!botToken) {
36533
36822
  const err = new Error("no_bot_token");
36534
36823
  err.name = "no_bot_token";
@@ -36638,8 +36927,11 @@ Fail conditions:
36638
36927
  err.name = "no_recipient";
36639
36928
  throw err;
36640
36929
  }
36641
- const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
36642
- const botToken = agentRows[0]?.telegramBotToken;
36930
+ let botToken = ctx.resolvedTelegramBotToken;
36931
+ if (botToken === void 0) {
36932
+ const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
36933
+ botToken = agentRows[0]?.telegramBotToken ?? void 0;
36934
+ }
36643
36935
  if (!botToken) {
36644
36936
  const err = new Error("no_bot_token");
36645
36937
  err.name = "no_bot_token";
@@ -36725,8 +37017,11 @@ function makeSendMediaTool(spec) {
36725
37017
  err.name = "no_recipient";
36726
37018
  throw err;
36727
37019
  }
36728
- const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
36729
- const botToken = agentRows[0]?.telegramBotToken;
37020
+ let botToken = ctx.resolvedTelegramBotToken;
37021
+ if (botToken === void 0) {
37022
+ const agentRows = await ctx.db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, ctx.agentId)).limit(1);
37023
+ botToken = agentRows[0]?.telegramBotToken ?? void 0;
37024
+ }
36730
37025
  if (!botToken) {
36731
37026
  const err = new Error("no_bot_token");
36732
37027
  err.name = "no_bot_token";
@@ -36847,6 +37142,16 @@ Size cap: 50 MB \u2192 throws \`voice_too_large\`. Other failures: \`no_recipien
36847
37142
  });
36848
37143
  }
36849
37144
 
37145
+ // ../../packages/tools/src/communication/index.ts
37146
+ var DELIVERY_TOOL_NAMES = [
37147
+ "telegram_send_message",
37148
+ "send_image",
37149
+ "send_file",
37150
+ "send_video",
37151
+ "send_audio",
37152
+ "send_voice"
37153
+ ];
37154
+
36850
37155
  // ../../packages/auth/src/types.ts
36851
37156
  var AuthError = class extends Error {
36852
37157
  code = "AUTH_REQUIRED";
@@ -36981,7 +37286,8 @@ async function healthRoute(c, deps) {
36981
37286
  await deps.db.execute(sql20`SELECT 1`);
36982
37287
  result.db = "ok";
36983
37288
  } catch (err) {
36984
- result.dbError = err instanceof Error ? err.message : String(err);
37289
+ console.error("[health] DB ping failed:", err instanceof Error ? err.message : err);
37290
+ result.dbError = "unavailable";
36985
37291
  }
36986
37292
  result.ok = result.db === "ok";
36987
37293
  return c.json(result, result.ok ? 200 : 503);
@@ -37040,14 +37346,16 @@ async function agentRoute(c, deps, runnerEnv) {
37040
37346
  ).limit(1);
37041
37347
  agentId = defaultAgentRows[0]?.id ?? null;
37042
37348
  }
37349
+ let conversationId = null;
37043
37350
  if (parentJobId) {
37044
- const [parentJob] = await deps.db.select({ entityId: agentJobs.entityId }).from(agentJobs).where(eq5(agentJobs.id, parentJobId)).limit(1);
37351
+ const [parentJob] = await deps.db.select({ entityId: agentJobs.entityId, conversationId: agentJobs.conversationId }).from(agentJobs).where(eq5(agentJobs.id, parentJobId)).limit(1);
37045
37352
  if (!parentJob) {
37046
37353
  return c.json({ error: "parent_job_not_found" }, 400);
37047
37354
  }
37048
37355
  if (entityId && parentJob.entityId !== entityId) {
37049
37356
  return c.json({ error: "parent_job_entity_mismatch" }, 403);
37050
37357
  }
37358
+ conversationId = parentJob.conversationId;
37051
37359
  }
37052
37360
  const [job] = await deps.db.insert(agentJobs).values({
37053
37361
  entityId: entityId ?? void 0,
@@ -37056,6 +37364,7 @@ async function agentRoute(c, deps, runnerEnv) {
37056
37364
  task,
37057
37365
  chatId: chatId ?? void 0,
37058
37366
  parentJobId: parentJobId ?? void 0,
37367
+ conversationId: conversationId ?? void 0,
37059
37368
  status: "pending",
37060
37369
  messages: [{ role: "user", content: task }]
37061
37370
  }).returning({ id: agentJobs.id });
@@ -38492,7 +38801,7 @@ var MAX_RATE_LIMIT_RETRIES2 = 3;
38492
38801
  var DEFAULT_RATE_LIMIT_BACKOFF_MS2 = 1e3;
38493
38802
  var MAX_RETRY_AFTER_MS2 = 6e4;
38494
38803
  function sleep3(ms) {
38495
- return new Promise((resolve2) => setTimeout(resolve2, ms));
38804
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
38496
38805
  }
38497
38806
  function createAirtableClient(getAccessToken, options = {}) {
38498
38807
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -38946,13 +39255,14 @@ function createAirtableTools(opts) {
38946
39255
 
38947
39256
  // ../../packages/adapters/google-drive/src/client.ts
38948
39257
  import { google } from "googleapis";
39258
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
38949
39259
  function createDriveClient(getAccessToken) {
38950
39260
  const auth2 = new google.auth.OAuth2();
38951
39261
  auth2.refreshHandler = async () => ({
38952
39262
  access_token: await getAccessToken(),
38953
39263
  expiry_date: Date.now() - 1
38954
39264
  });
38955
- return google.drive({ version: "v3", auth: auth2 });
39265
+ return google.drive({ version: "v3", auth: auth2, timeout: DEFAULT_TIMEOUT_MS2 });
38956
39266
  }
38957
39267
 
38958
39268
  // ../../packages/adapters/google-drive/src/tools/list-files.ts
@@ -39772,13 +40082,14 @@ function createDriveTools(opts) {
39772
40082
 
39773
40083
  // ../../packages/adapters/gmail/src/client.ts
39774
40084
  import { google as google2 } from "googleapis";
40085
+ var DEFAULT_TIMEOUT_MS3 = 3e4;
39775
40086
  function createGmailClient(getAccessToken) {
39776
40087
  const auth2 = new google2.auth.OAuth2();
39777
40088
  auth2.refreshHandler = async () => ({
39778
40089
  access_token: await getAccessToken(),
39779
40090
  expiry_date: Date.now() - 1
39780
40091
  });
39781
- return google2.gmail({ version: "v1", auth: auth2 });
40092
+ return google2.gmail({ version: "v1", auth: auth2, timeout: DEFAULT_TIMEOUT_MS3 });
39782
40093
  }
39783
40094
 
39784
40095
  // ../../packages/adapters/gmail/src/tools/messages.ts
@@ -39854,6 +40165,19 @@ async function paginateGmail(fetcher, pageSize = 100, maxItems = 100) {
39854
40165
  return items.slice(0, cappedMax);
39855
40166
  }
39856
40167
 
40168
+ // ../../packages/adapters/gmail/src/helpers/concurrency.ts
40169
+ async function mapWithConcurrency(items, limit, fn) {
40170
+ const results = new Array(items.length);
40171
+ for (let start = 0; start < items.length; start += limit) {
40172
+ const chunk = items.slice(start, start + limit);
40173
+ const chunkResults = await Promise.all(chunk.map((item, i) => fn(item, start + i)));
40174
+ for (let i = 0; i < chunkResults.length; i++) {
40175
+ results[start + i] = chunkResults[i];
40176
+ }
40177
+ }
40178
+ return results;
40179
+ }
40180
+
39857
40181
  // ../../packages/adapters/gmail/src/helpers/parse-payload.ts
39858
40182
  function decodeBase64Url(encoded) {
39859
40183
  if (!encoded) return "";
@@ -39933,6 +40257,7 @@ function parseGmailHeaders(payload) {
39933
40257
  }
39934
40258
 
39935
40259
  // ../../packages/adapters/gmail/src/helpers/rfc2822.ts
40260
+ import { randomBytes as randomBytes5 } from "node:crypto";
39936
40261
  var MIME_OVERRIDES = {
39937
40262
  ".html": "text/html",
39938
40263
  ".htm": "text/html",
@@ -39953,6 +40278,14 @@ function inferMimeType(filename) {
39953
40278
  const ext = filename.includes(".") ? "." + filename.split(".").pop().toLowerCase() : "";
39954
40279
  return MIME_OVERRIDES[ext] ?? "application/octet-stream";
39955
40280
  }
40281
+ function assertNoHeaderInjection(fieldName, value) {
40282
+ if (/[\r\n]/.test(value)) {
40283
+ throw new GmailAdapterError(
40284
+ "gmail_validation_error",
40285
+ `'${fieldName}' contains a line break (CR/LF), which is not allowed in an email header.`
40286
+ );
40287
+ }
40288
+ }
39956
40289
  function encodeHeader(value) {
39957
40290
  const hasNonAscii = [...value].some((ch) => ch.charCodeAt(0) > 127);
39958
40291
  if (!hasNonAscii) return value;
@@ -39960,10 +40293,21 @@ function encodeHeader(value) {
39960
40293
  return `=?utf-8?B?${encoded}?=`;
39961
40294
  }
39962
40295
  function makeBoundary() {
39963
- return `nodalai_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
40296
+ return `nodalai_${Date.now().toString(36)}_${randomBytes5(12).toString("hex")}`;
39964
40297
  }
39965
40298
  function buildRfc2822Message(opts) {
39966
40299
  const { to, subject, body, from, cc, bcc, replyTo, inReplyTo, references, attachments } = opts;
40300
+ assertNoHeaderInjection("to", to);
40301
+ assertNoHeaderInjection("subject", subject);
40302
+ if (from) assertNoHeaderInjection("from", from);
40303
+ if (cc) assertNoHeaderInjection("cc", cc);
40304
+ if (bcc) assertNoHeaderInjection("bcc", bcc);
40305
+ if (replyTo) assertNoHeaderInjection("replyTo", replyTo);
40306
+ if (inReplyTo) assertNoHeaderInjection("inReplyTo", inReplyTo);
40307
+ if (references) assertNoHeaderInjection("references", references);
40308
+ for (const att of attachments ?? []) {
40309
+ assertNoHeaderInjection("attachment.filename", att.filename);
40310
+ }
39967
40311
  const hasAttachments = attachments && attachments.length > 0;
39968
40312
  const isHtml = body.includes("<") && body.includes(">") && /<[a-zA-Z][^>]*>/.test(body);
39969
40313
  let rawMessage;
@@ -40039,6 +40383,7 @@ function buildRfc2822Message(opts) {
40039
40383
 
40040
40384
  // ../../packages/adapters/gmail/src/tools/messages.ts
40041
40385
  var BODY_CHAR_CAP = 1e4;
40386
+ var HYDRATE_CONCURRENCY = 15;
40042
40387
  var AttachmentItemSchema = z82.object({
40043
40388
  filename: z82.string().describe("File name shown in the email, e.g. 'report.html'."),
40044
40389
  content: z82.string().describe("File content as a string (plain text by default, or base64 if encoding is base64)."),
@@ -40122,39 +40467,39 @@ function createListMessagesTool(gmail) {
40122
40467
  Math.min(maxResults, 100),
40123
40468
  maxResults
40124
40469
  );
40125
- const detailed = await Promise.all(
40126
- messages.map(async (stub) => {
40127
- try {
40128
- const res = await gmail.users.messages.get({
40129
- userId: "me",
40130
- id: stub.id,
40131
- format: "metadata",
40132
- metadataHeaders: ["Subject", "From", "Date"]
40133
- });
40134
- const headers = parseGmailHeaders(res.data.payload ?? void 0);
40135
- return {
40136
- messageId: res.data.id ?? stub.id ?? "",
40137
- threadId: res.data.threadId ?? stub.threadId ?? "",
40138
- from: headers["from"] ?? "",
40139
- subject: headers["subject"] ?? "",
40140
- date: headers["date"] ?? "",
40141
- snippet: res.data.snippet ?? "",
40142
- labelIds: res.data.labelIds ?? []
40143
- };
40144
- } catch {
40145
- return {
40146
- messageId: stub.id ?? "",
40147
- threadId: stub.threadId ?? "",
40148
- from: "",
40149
- subject: "",
40150
- date: "",
40151
- snippet: "",
40152
- labelIds: []
40153
- };
40154
- }
40155
- })
40156
- );
40157
- return { total: detailed.length, messages: detailed };
40470
+ const detailed = await mapWithConcurrency(messages, HYDRATE_CONCURRENCY, async (stub) => {
40471
+ try {
40472
+ const res = await gmail.users.messages.get({
40473
+ userId: "me",
40474
+ id: stub.id,
40475
+ format: "metadata",
40476
+ metadataHeaders: ["Subject", "From", "Date"]
40477
+ });
40478
+ const headers = parseGmailHeaders(res.data.payload ?? void 0);
40479
+ return {
40480
+ messageId: res.data.id ?? stub.id ?? "",
40481
+ threadId: res.data.threadId ?? stub.threadId ?? "",
40482
+ from: headers["from"] ?? "",
40483
+ subject: headers["subject"] ?? "",
40484
+ date: headers["date"] ?? "",
40485
+ snippet: res.data.snippet ?? "",
40486
+ labelIds: res.data.labelIds ?? []
40487
+ };
40488
+ } catch (err) {
40489
+ return {
40490
+ messageId: stub.id ?? "",
40491
+ threadId: stub.threadId ?? "",
40492
+ from: "",
40493
+ subject: "",
40494
+ date: "",
40495
+ snippet: "",
40496
+ labelIds: [],
40497
+ error: err instanceof Error ? err.message : "gmail_get_message_failed"
40498
+ };
40499
+ }
40500
+ });
40501
+ const errors = detailed.filter((m) => m.error !== void 0).length;
40502
+ return { total: detailed.length, errors, messages: detailed };
40158
40503
  } catch (err) {
40159
40504
  throw mapGmailError(err);
40160
40505
  }
@@ -40424,6 +40769,7 @@ function createDeleteMessageTool(gmail) {
40424
40769
  // ../../packages/adapters/gmail/src/tools/threads.ts
40425
40770
  import { z as z83 } from "zod";
40426
40771
  var BODY_CHAR_CAP2 = 5e3;
40772
+ var MAX_MESSAGES_PER_THREAD = 50;
40427
40773
  var ListThreadsInput = z83.object({
40428
40774
  query: z83.string().optional().describe("Gmail search query. Examples: 'is:unread', 'from:alice@example.com label:work'."),
40429
40775
  max_results: z83.number().int().min(1).max(500).optional().describe("Max threads to return (1\u2013500, default 20)."),
@@ -40476,7 +40822,7 @@ var GetThreadInput = z83.object({
40476
40822
  function createGetThreadTool(gmail) {
40477
40823
  return {
40478
40824
  name: "gmail_get_thread",
40479
- description: "Fetch a full Gmail conversation thread with all messages, decoded bodies and headers.",
40825
+ description: `Fetch a full Gmail conversation thread with all messages, decoded bodies and headers. Capped at ${MAX_MESSAGES_PER_THREAD} messages (oldest first) \u2014 see the truncated flag.`,
40480
40826
  inputSchema: GetThreadInput,
40481
40827
  riskLevel: "read",
40482
40828
  async execute(input) {
@@ -40487,7 +40833,9 @@ function createGetThreadTool(gmail) {
40487
40833
  format: "full"
40488
40834
  });
40489
40835
  const thread = res.data;
40490
- const messages = (thread.messages ?? []).map((msg) => {
40836
+ const allMessages = thread.messages ?? [];
40837
+ const truncated = allMessages.length > MAX_MESSAGES_PER_THREAD;
40838
+ const messages = allMessages.slice(0, MAX_MESSAGES_PER_THREAD).map((msg) => {
40491
40839
  const payload = msg.payload ?? void 0;
40492
40840
  const headers = parseGmailHeaders(payload);
40493
40841
  let body = extractTextFromPayload(payload).trim();
@@ -40505,7 +40853,7 @@ function createGetThreadTool(gmail) {
40505
40853
  labelIds: msg.labelIds ?? []
40506
40854
  };
40507
40855
  });
40508
- return { threadId: thread.id ?? "", messages };
40856
+ return { threadId: thread.id ?? "", messages, truncated };
40509
40857
  } catch (err) {
40510
40858
  throw mapGmailError(err);
40511
40859
  }
@@ -40715,6 +41063,7 @@ function createDeleteLabelTool(gmail) {
40715
41063
 
40716
41064
  // ../../packages/adapters/gmail/src/tools/drafts.ts
40717
41065
  import { z as z85 } from "zod";
41066
+ var HYDRATE_CONCURRENCY2 = 15;
40718
41067
  var AttachmentItemSchema2 = z85.object({
40719
41068
  filename: z85.string().describe("File name shown in the email, e.g. 'report.html'."),
40720
41069
  content: z85.string().describe("File content as a string (plain text by default)."),
@@ -40751,36 +41100,36 @@ function createListDraftsTool(gmail) {
40751
41100
  Math.min(maxResults, 100),
40752
41101
  maxResults
40753
41102
  );
40754
- const drafts = await Promise.all(
40755
- draftStubs.map(async (stub) => {
40756
- const draftId = stub.id ?? "";
40757
- try {
40758
- const res = await gmail.users.drafts.get({
40759
- userId: "me",
40760
- id: draftId,
40761
- format: "metadata"
40762
- });
40763
- const msg = res.data.message ?? {};
40764
- const headers = parseGmailHeaders(msg.payload ?? void 0);
40765
- return {
40766
- draftId,
40767
- messageId: msg.id ?? "",
40768
- to: headers["to"] ?? "",
40769
- subject: headers["subject"] ?? "",
40770
- snippet: msg.snippet ?? ""
40771
- };
40772
- } catch {
40773
- return {
40774
- draftId,
40775
- messageId: stub.message?.id ?? "",
40776
- to: "",
40777
- subject: "",
40778
- snippet: ""
40779
- };
40780
- }
40781
- })
40782
- );
40783
- return { total: drafts.length, drafts };
41103
+ const drafts = await mapWithConcurrency(draftStubs, HYDRATE_CONCURRENCY2, async (stub) => {
41104
+ const draftId = stub.id ?? "";
41105
+ try {
41106
+ const res = await gmail.users.drafts.get({
41107
+ userId: "me",
41108
+ id: draftId,
41109
+ format: "metadata"
41110
+ });
41111
+ const msg = res.data.message ?? {};
41112
+ const headers = parseGmailHeaders(msg.payload ?? void 0);
41113
+ return {
41114
+ draftId,
41115
+ messageId: msg.id ?? "",
41116
+ to: headers["to"] ?? "",
41117
+ subject: headers["subject"] ?? "",
41118
+ snippet: msg.snippet ?? ""
41119
+ };
41120
+ } catch (err) {
41121
+ return {
41122
+ draftId,
41123
+ messageId: stub.message?.id ?? "",
41124
+ to: "",
41125
+ subject: "",
41126
+ snippet: "",
41127
+ error: err instanceof Error ? err.message : "gmail_get_draft_failed"
41128
+ };
41129
+ }
41130
+ });
41131
+ const errors = drafts.filter((d) => d.error !== void 0).length;
41132
+ return { total: drafts.length, errors, drafts };
40784
41133
  } catch (err) {
40785
41134
  throw mapGmailError(err);
40786
41135
  }
@@ -41264,13 +41613,14 @@ function createGmailTools(opts) {
41264
41613
 
41265
41614
  // ../../packages/adapters/google-calendar/src/client.ts
41266
41615
  import { google as google3 } from "googleapis";
41616
+ var DEFAULT_TIMEOUT_MS4 = 3e4;
41267
41617
  function createGoogleCalendarClient(getAccessToken) {
41268
41618
  const auth2 = new google3.auth.OAuth2();
41269
41619
  auth2.refreshHandler = async () => ({
41270
41620
  access_token: await getAccessToken(),
41271
41621
  expiry_date: Date.now() - 1
41272
41622
  });
41273
- return google3.calendar({ version: "v3", auth: auth2 });
41623
+ return google3.calendar({ version: "v3", auth: auth2, timeout: DEFAULT_TIMEOUT_MS4 });
41274
41624
  }
41275
41625
 
41276
41626
  // ../../packages/adapters/google-calendar/src/tools/events.ts
@@ -41661,13 +42011,14 @@ function createGoogleCalendarTools(opts) {
41661
42011
 
41662
42012
  // ../../packages/adapters/google-sheets/src/client.ts
41663
42013
  import { google as google4 } from "googleapis";
42014
+ var DEFAULT_TIMEOUT_MS5 = 3e4;
41664
42015
  function createSheetsClient(getAccessToken) {
41665
42016
  const auth2 = new google4.auth.OAuth2();
41666
42017
  auth2.refreshHandler = async () => ({
41667
42018
  access_token: await getAccessToken(),
41668
42019
  expiry_date: Date.now() - 1
41669
42020
  });
41670
- return google4.sheets({ version: "v4", auth: auth2 });
42021
+ return google4.sheets({ version: "v4", auth: auth2, timeout: DEFAULT_TIMEOUT_MS5 });
41671
42022
  }
41672
42023
 
41673
42024
  // ../../packages/adapters/google-sheets/src/tools/values.ts
@@ -41783,9 +42134,36 @@ function parseRange(range) {
41783
42134
  }
41784
42135
  return { sheetName, cellRange, canonical };
41785
42136
  }
42137
+ function estimateRowSpan(cellRange) {
42138
+ const parts = cellRange.split(":");
42139
+ if (parts.length > 2) return null;
42140
+ const rowOf = (part) => {
42141
+ const m = /^\$?[A-Za-z]*\$?(\d+)$/.exec(part);
42142
+ return m?.[1] !== void 0 ? Number(m[1]) : null;
42143
+ };
42144
+ const first = parts[0] ?? "";
42145
+ const second = parts[1];
42146
+ if (second === void 0) {
42147
+ const row = rowOf(first);
42148
+ return row !== null ? 1 : null;
42149
+ }
42150
+ const startRow = rowOf(first);
42151
+ const endRow = rowOf(second);
42152
+ if (startRow === null || endRow === null) return null;
42153
+ return Math.abs(endRow - startRow) + 1;
42154
+ }
41786
42155
 
41787
42156
  // ../../packages/adapters/google-sheets/src/tools/values.ts
41788
42157
  var ROW_CAP = 1e4;
42158
+ function rejectIfObviouslyTooLarge(cellRange, fullRange) {
42159
+ const span = estimateRowSpan(cellRange);
42160
+ if (span !== null && span > ROW_CAP) {
42161
+ throw new SheetsAdapterError(
42162
+ "sheets_range_too_large",
42163
+ `Range '${fullRange}' spans ${span} rows which exceeds the ${ROW_CAP}-row cap. Use a smaller range.`
42164
+ );
42165
+ }
42166
+ }
41789
42167
  var ReadRangeInput = z90.object({
41790
42168
  spreadsheet_id: z90.string().describe("The spreadsheet ID from the URL (between /d/ and /edit)."),
41791
42169
  range: z90.string().describe("A1-notation range, e.g. 'Sheet1!A1:D10' or 'A:A' for a full column."),
@@ -41801,7 +42179,8 @@ function createReadRangeTool(sheets) {
41801
42179
  riskLevel: "read",
41802
42180
  async execute(input) {
41803
42181
  try {
41804
- parseRange(input.range);
42182
+ const parsed = parseRange(input.range);
42183
+ rejectIfObviouslyTooLarge(parsed.cellRange, input.range);
41805
42184
  const res = await sheets.spreadsheets.values.get({
41806
42185
  spreadsheetId: input.spreadsheet_id,
41807
42186
  range: input.range,
@@ -41831,7 +42210,12 @@ var ReadAllInput = z90.object({
41831
42210
  function createReadAllTool(sheets) {
41832
42211
  return {
41833
42212
  name: "sheets_read_all",
41834
- description: "Read all values from a sheet tab. Returns the full 2D array. Capped at 10,000 rows \u2014 use sheets_read_range for partial reads on large sheets.",
42213
+ // audit#2026-07-07 F10: unlike sheets_read_range, this always targets the
42214
+ // whole sheet tab — there is no explicit numeric row bound to check
42215
+ // up front (estimateRowSpan has nothing to work with), so the ROW_CAP
42216
+ // check below necessarily runs AFTER the full sheet has been fetched.
42217
+ // Prefer sheets_read_range with an explicit bounded range on large sheets.
42218
+ description: "Read all values from a sheet tab. Returns the full 2D array. Capped at 10,000 rows \u2014 use sheets_read_range for partial reads on large sheets. Note: because this reads a whole tab, the cap is only enforced AFTER the full sheet is fetched \u2014 prefer sheets_read_range with an explicit row-bounded range for very large sheets.",
41835
42219
  inputSchema: ReadAllInput,
41836
42220
  riskLevel: "read",
41837
42221
  async execute(input) {
@@ -42928,13 +43312,14 @@ function createSheetsTools(opts) {
42928
43312
 
42929
43313
  // ../../packages/adapters/google-docs/src/client.ts
42930
43314
  import { google as google5 } from "googleapis";
43315
+ var DEFAULT_TIMEOUT_MS6 = 3e4;
42931
43316
  function createDocsClient(getAccessToken) {
42932
43317
  const auth2 = new google5.auth.OAuth2();
42933
43318
  auth2.refreshHandler = async () => ({
42934
43319
  access_token: await getAccessToken(),
42935
43320
  expiry_date: Date.now() - 1
42936
43321
  });
42937
- return google5.docs({ version: "v1", auth: auth2 });
43322
+ return google5.docs({ version: "v1", auth: auth2, timeout: DEFAULT_TIMEOUT_MS6 });
42938
43323
  }
42939
43324
 
42940
43325
  // ../../packages/adapters/google-docs/src/tools/lifecycle.ts
@@ -43127,6 +43512,7 @@ function buildUpdateTextStyleRequest(startIndex, endIndex, textStyle, fields) {
43127
43512
 
43128
43513
  // ../../packages/adapters/google-docs/src/tools/lifecycle.ts
43129
43514
  var CHAR_CAP3 = 1e5;
43515
+ var BODY_JSON_CHAR_CAP = 5e5;
43130
43516
  var CreateDocInput = z94.object({
43131
43517
  title: z94.string().describe("Title for the new document."),
43132
43518
  content: z94.string().optional().describe("Optional initial text content to insert into the document body.")
@@ -43188,6 +43574,13 @@ function createGetDocTool(docs) {
43188
43574
  `Document body contains ${text23.length} characters which exceeds the ${CHAR_CAP3}-character cap.`
43189
43575
  );
43190
43576
  }
43577
+ const bodyJsonLength = JSON.stringify(doc.body).length;
43578
+ if (bodyJsonLength > BODY_JSON_CHAR_CAP) {
43579
+ throw new DocsAdapterError(
43580
+ "docs_document_too_large",
43581
+ `Document body JSON is ${bodyJsonLength} characters which exceeds the ${BODY_JSON_CHAR_CAP}-character raw-JSON cap (docs_get returns the full Google Docs JSON structure, which can be much heavier than its flattened text \u2014 use docs_get_text instead).`
43582
+ );
43583
+ }
43191
43584
  }
43192
43585
  const headers = Object.entries(doc.headers ?? {}).map(([id, h]) => ({
43193
43586
  headerId: id,
@@ -44025,6 +44418,7 @@ function makeFirecrawlSearchTool(client) {
44025
44418
 
44026
44419
  // ../../packages/adapters/firecrawl/src/tools/crawl.ts
44027
44420
  import { z as z99 } from "zod";
44421
+ var MAX_CRAWL_DOCS_PER_CALL = 50;
44028
44422
  var CrawlStartInput = z99.object({
44029
44423
  url: z99.string().url().describe("The root URL to crawl."),
44030
44424
  limit: z99.number().int().min(1).max(100).optional().describe("Maximum number of pages to crawl (1\u2013100).")
@@ -44056,18 +44450,20 @@ var CrawlStatusInput = z99.object({
44056
44450
  function makeFirecrawlCrawlStatusTool(client) {
44057
44451
  return {
44058
44452
  name: "firecrawl_crawl_status",
44059
- description: 'Get the current status and partial results of a Firecrawl crawl job. Poll this until status is "completed", "failed", or "cancelled".',
44453
+ description: `Get the current status and partial results of a Firecrawl crawl job. Poll this until status is "completed", "failed", or "cancelled". Capped at ${MAX_CRAWL_DOCS_PER_CALL} documents per call \u2014 see the dataTruncated flag.`,
44060
44454
  inputSchema: CrawlStatusInput,
44061
44455
  riskLevel: "read",
44062
44456
  async execute(input) {
44063
44457
  try {
44064
44458
  const job = await client.getCrawlStatus(input.id);
44459
+ const rawData = job.data ?? [];
44460
+ const dataTruncated = rawData.length > MAX_CRAWL_DOCS_PER_CALL;
44065
44461
  return {
44066
44462
  id: job.id,
44067
44463
  status: job.status,
44068
44464
  total: job.total,
44069
44465
  completed: job.completed,
44070
- data: (job.data ?? []).map((doc) => {
44466
+ data: rawData.slice(0, MAX_CRAWL_DOCS_PER_CALL).map((doc) => {
44071
44467
  const md = capField(doc.markdown);
44072
44468
  const html = capField(doc.html);
44073
44469
  return {
@@ -44076,7 +44472,8 @@ function makeFirecrawlCrawlStatusTool(client) {
44076
44472
  ...html.content !== void 0 && { html: html.content },
44077
44473
  truncated: md.truncated || html.truncated
44078
44474
  };
44079
- })
44475
+ }),
44476
+ dataTruncated
44080
44477
  };
44081
44478
  } catch (err) {
44082
44479
  throw wrapFirecrawlError(err);
@@ -44158,6 +44555,8 @@ var WebBrowseInput = z100.object({
44158
44555
  "For a search query, how many results to return (default 3). Ignored for a single URL."
44159
44556
  )
44160
44557
  });
44558
+ var WEB_BROWSE_WAIT_SECS = 120;
44559
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["SUCCEEDED", "FAILED", "ABORTED", "TIMED-OUT"]);
44161
44560
  function sanitizeScraped(s) {
44162
44561
  return Array.from(s).filter((ch) => {
44163
44562
  const x = ch.charCodeAt(0);
@@ -44184,17 +44583,22 @@ function makeApifyWebBrowseTool(client) {
44184
44583
  riskLevel: "read",
44185
44584
  async execute(input) {
44186
44585
  try {
44187
- const run = await client.actor("apify/rag-web-browser").call({
44188
- query: input.query,
44189
- // `text` is the proven, lighter output the legacy skill used. Markdown is
44190
- // heavier and special-char-dense an unnecessary deviation that changed
44191
- // the request/response load profile. Stay on the format that always worked.
44192
- outputFormats: ["text"],
44193
- maxResults: input.maxResults ?? 3
44194
- });
44586
+ const run = await client.actor("apify/rag-web-browser").call(
44587
+ {
44588
+ query: input.query,
44589
+ // `text` is the proven, lighter output the legacy skill used. Markdown is
44590
+ // heavier and special-char-dense an unnecessary deviation that changed
44591
+ // the request/response load profile. Stay on the format that always worked.
44592
+ outputFormats: ["text"],
44593
+ maxResults: input.maxResults ?? 3
44594
+ },
44595
+ // audit#2026-07-07 F1: bound the wait — see WEB_BROWSE_WAIT_SECS above.
44596
+ { waitSecs: WEB_BROWSE_WAIT_SECS }
44597
+ );
44195
44598
  const dataset = await client.dataset(run.defaultDatasetId).listItems({ clean: true });
44196
44599
  const results = (dataset.items ?? []).map(normalizeBrowseItem).filter((r) => r.content);
44197
- return { query: input.query, count: results.length, results };
44600
+ const timedOut = !TERMINAL_RUN_STATUSES.has(run.status);
44601
+ return { query: input.query, count: results.length, results, timedOut };
44198
44602
  } catch (err) {
44199
44603
  throw wrapApifyError(err);
44200
44604
  }
@@ -44271,6 +44675,19 @@ function makeApifyGetRunTool(client) {
44271
44675
  // ../../packages/adapters/apify/src/tools/datasets.ts
44272
44676
  init_errors();
44273
44677
  import { z as z101 } from "zod";
44678
+ var ITEM_CHAR_CAP = 15e3;
44679
+ function capItem(item) {
44680
+ const json = JSON.stringify(item) ?? "";
44681
+ if (json.length <= ITEM_CHAR_CAP) return { item, truncated: false };
44682
+ return {
44683
+ item: {
44684
+ __truncated: true,
44685
+ preview: json.slice(0, ITEM_CHAR_CAP),
44686
+ originalCharCount: json.length
44687
+ },
44688
+ truncated: true
44689
+ };
44690
+ }
44274
44691
  var ListDatasetsInput = z101.object({
44275
44692
  limit: z101.number().int().min(1).max(1e3).optional().default(100).describe("Maximum number of datasets to return (default 100, max 1000)."),
44276
44693
  offset: z101.number().int().min(0).optional().default(0).describe("Number of datasets to skip (for pagination). Default 0.")
@@ -44315,7 +44732,7 @@ var GetDatasetItemsInput = z101.object({
44315
44732
  function makeApifyGetDatasetItemsTool(client) {
44316
44733
  return {
44317
44734
  name: "apify_get_dataset_items",
44318
- description: "Retrieve items from an Apify dataset. Use the datasetId returned by apify_run_actor or from apify_list_datasets. Supports limit/offset pagination (max 1000 items per call).",
44735
+ description: `Retrieve items from an Apify dataset. Use the datasetId returned by apify_run_actor or from apify_list_datasets. Supports limit/offset pagination (max 1000 items per call). Each item is capped at ${ITEM_CHAR_CAP} chars of JSON \u2014 see the truncated flag.`,
44319
44736
  inputSchema: GetDatasetItemsInput,
44320
44737
  riskLevel: "read",
44321
44738
  async execute(input) {
@@ -44324,11 +44741,13 @@ function makeApifyGetDatasetItemsTool(client) {
44324
44741
  limit: input.limit ?? 100,
44325
44742
  offset: input.offset ?? 0
44326
44743
  });
44744
+ const capped = result.items.map(capItem);
44327
44745
  return {
44328
- items: result.items,
44746
+ items: capped.map((c) => c.item),
44329
44747
  total: result.total,
44330
44748
  count: result.count,
44331
- offset: result.offset
44749
+ offset: result.offset,
44750
+ truncated: capped.some((c) => c.truncated)
44332
44751
  };
44333
44752
  } catch (err) {
44334
44753
  throw wrapApifyError(err);
@@ -44668,7 +45087,7 @@ function wrapPoyoError(err) {
44668
45087
 
44669
45088
  // ../../packages/adapters/poyo/src/client.ts
44670
45089
  var DEFAULT_BASE_URL = "https://api.poyo.ai";
44671
- var DEFAULT_TIMEOUT_MS2 = 3e4;
45090
+ var DEFAULT_TIMEOUT_MS7 = 3e4;
44672
45091
  async function fetchWithTimeout(doFetch, url2, init, timeoutMs) {
44673
45092
  try {
44674
45093
  return await doFetch(url2, { ...init, signal: AbortSignal.timeout(timeoutMs) });
@@ -44685,7 +45104,7 @@ async function fetchWithTimeout(doFetch, url2, init, timeoutMs) {
44685
45104
  function createPoyoClient(accessToken, options = {}) {
44686
45105
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
44687
45106
  const doFetch = options.fetchImpl ?? fetch;
44688
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
45107
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS7;
44689
45108
  const authHeader = { Authorization: `Bearer ${accessToken}` };
44690
45109
  return {
44691
45110
  async submit(model, input) {
@@ -44756,7 +45175,7 @@ var GenerateImageInput = z103.object({
44756
45175
  ),
44757
45176
  size: z103.string().min(1).optional().describe('Aspect ratio of the image, e.g. "1:1", "16:9", "9:16", "4:3". Default "1:1".')
44758
45177
  });
44759
- var sleep4 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
45178
+ var sleep4 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
44760
45179
  function makePoyoGenerateImageTool(client, poll = DEFAULT_POLL) {
44761
45180
  return {
44762
45181
  name: "poyo_generate_image",
@@ -44907,6 +45326,10 @@ var ADAPTER_REGISTRY = {
44907
45326
  }
44908
45327
  };
44909
45328
 
45329
+ // ../../packages/adapters/mcp/src/client.ts
45330
+ import { lookup as dnsLookupCb } from "node:dns";
45331
+ import { promisify } from "node:util";
45332
+
44910
45333
  // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
44911
45334
  import * as z3rt from "zod/v3";
44912
45335
  import * as z4mini from "zod/v4-mini";
@@ -47034,7 +47457,7 @@ var Protocol = class {
47034
47457
  return;
47035
47458
  }
47036
47459
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
47037
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
47460
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
47038
47461
  options?.signal?.throwIfAborted();
47039
47462
  }
47040
47463
  } catch (error) {
@@ -47051,7 +47474,7 @@ var Protocol = class {
47051
47474
  */
47052
47475
  request(request, resultSchema, options) {
47053
47476
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
47054
- return new Promise((resolve2, reject) => {
47477
+ return new Promise((resolve3, reject) => {
47055
47478
  const earlyReject = (error) => {
47056
47479
  reject(error);
47057
47480
  };
@@ -47129,7 +47552,7 @@ var Protocol = class {
47129
47552
  if (!parseResult.success) {
47130
47553
  reject(parseResult.error);
47131
47554
  } else {
47132
- resolve2(parseResult.data);
47555
+ resolve3(parseResult.data);
47133
47556
  }
47134
47557
  } catch (error) {
47135
47558
  reject(error);
@@ -47390,12 +47813,12 @@ var Protocol = class {
47390
47813
  }
47391
47814
  } catch {
47392
47815
  }
47393
- return new Promise((resolve2, reject) => {
47816
+ return new Promise((resolve3, reject) => {
47394
47817
  if (signal.aborted) {
47395
47818
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
47396
47819
  return;
47397
47820
  }
47398
- const timeoutId = setTimeout(resolve2, interval);
47821
+ const timeoutId = setTimeout(resolve3, interval);
47399
47822
  signal.addEventListener("abort", () => {
47400
47823
  clearTimeout(timeoutId);
47401
47824
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -48305,8 +48728,8 @@ async function random(size) {
48305
48728
  const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
48306
48729
  let result = "";
48307
48730
  while (result.length < size) {
48308
- const randomBytes5 = await getRandomValues(size - result.length);
48309
- for (const randomByte of randomBytes5) {
48731
+ const randomBytes6 = await getRandomValues(size - result.length);
48732
+ for (const randomByte of randomBytes6) {
48310
48733
  if (randomByte < evenDistCutoff) {
48311
48734
  result += mask[randomByte % mask.length];
48312
48735
  }
@@ -49802,7 +50225,7 @@ var StdioClientTransport = class {
49802
50225
  if (this._process) {
49803
50226
  throw new Error("StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.");
49804
50227
  }
49805
- return new Promise((resolve2, reject) => {
50228
+ return new Promise((resolve3, reject) => {
49806
50229
  this._process = (0, import_cross_spawn.default)(this._serverParams.command, this._serverParams.args ?? [], {
49807
50230
  // merge default env with server env because mcp server needs some env vars
49808
50231
  env: {
@@ -49819,7 +50242,7 @@ var StdioClientTransport = class {
49819
50242
  this.onerror?.(error);
49820
50243
  });
49821
50244
  this._process.on("spawn", () => {
49822
- resolve2();
50245
+ resolve3();
49823
50246
  });
49824
50247
  this._process.on("close", (_code) => {
49825
50248
  this._process = void 0;
@@ -49878,22 +50301,22 @@ var StdioClientTransport = class {
49878
50301
  if (this._process) {
49879
50302
  const processToClose = this._process;
49880
50303
  this._process = void 0;
49881
- const closePromise = new Promise((resolve2) => {
50304
+ const closePromise = new Promise((resolve3) => {
49882
50305
  processToClose.once("close", () => {
49883
- resolve2();
50306
+ resolve3();
49884
50307
  });
49885
50308
  });
49886
50309
  try {
49887
50310
  processToClose.stdin?.end();
49888
50311
  } catch {
49889
50312
  }
49890
- await Promise.race([closePromise, new Promise((resolve2) => setTimeout(resolve2, 2e3).unref())]);
50313
+ await Promise.race([closePromise, new Promise((resolve3) => setTimeout(resolve3, 2e3).unref())]);
49891
50314
  if (processToClose.exitCode === null) {
49892
50315
  try {
49893
50316
  processToClose.kill("SIGTERM");
49894
50317
  } catch {
49895
50318
  }
49896
- await Promise.race([closePromise, new Promise((resolve2) => setTimeout(resolve2, 2e3).unref())]);
50319
+ await Promise.race([closePromise, new Promise((resolve3) => setTimeout(resolve3, 2e3).unref())]);
49897
50320
  }
49898
50321
  if (processToClose.exitCode === null) {
49899
50322
  try {
@@ -49905,21 +50328,54 @@ var StdioClientTransport = class {
49905
50328
  this._readBuffer.clear();
49906
50329
  }
49907
50330
  send(message) {
49908
- return new Promise((resolve2) => {
50331
+ return new Promise((resolve3) => {
49909
50332
  if (!this._process?.stdin) {
49910
50333
  throw new Error("Not connected");
49911
50334
  }
49912
50335
  const json = serializeMessage(message);
49913
50336
  if (this._process.stdin.write(json)) {
49914
- resolve2();
50337
+ resolve3();
49915
50338
  } else {
49916
- this._process.stdin.once("drain", resolve2);
50339
+ this._process.stdin.once("drain", resolve3);
49917
50340
  }
49918
50341
  });
49919
50342
  }
49920
50343
  };
49921
50344
 
49922
50345
  // ../../packages/adapters/mcp/src/client.ts
50346
+ var dnsLookup = promisify(dnsLookupCb);
50347
+ var BLOCKED_MCP_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
50348
+ function isBlockedMcpAddress(addr) {
50349
+ const a = addr.toLowerCase();
50350
+ if (/^169\.254\./.test(a)) return true;
50351
+ if (a === "100.100.100.100") return true;
50352
+ if (a.startsWith("fe80:")) return true;
50353
+ if (a === "fd00:ec2::254") return true;
50354
+ return false;
50355
+ }
50356
+ function isIpLiteral(host) {
50357
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
50358
+ }
50359
+ async function assertMcpHttpUrlSafe(url2) {
50360
+ const hostname = url2.hostname.replace(/^\[|\]$/g, "").toLowerCase();
50361
+ if (BLOCKED_MCP_HOSTNAMES.has(hostname) || isBlockedMcpAddress(hostname)) {
50362
+ throw new Error(`MCP URL blocked: "${hostname}" is a link-local / cloud-metadata endpoint.`);
50363
+ }
50364
+ if (isIpLiteral(hostname)) return;
50365
+ let resolved;
50366
+ try {
50367
+ resolved = await dnsLookup(hostname, { all: true });
50368
+ } catch {
50369
+ return;
50370
+ }
50371
+ for (const { address } of resolved) {
50372
+ if (isBlockedMcpAddress(address)) {
50373
+ throw new Error(
50374
+ `MCP URL blocked: "${hostname}" resolves to a link-local / cloud-metadata address.`
50375
+ );
50376
+ }
50377
+ }
50378
+ }
49923
50379
  var DEFAULT_CONNECT_TIMEOUT_MS = 12e4;
49924
50380
  function resolveConnectTimeoutMs() {
49925
50381
  const raw = process.env["MCP_CONNECT_TIMEOUT_MS"];
@@ -49974,6 +50430,7 @@ async function connectMcp(opts) {
49974
50430
  const connectTimeoutMs = resolveConnectTimeoutMs();
49975
50431
  if (opts.transport === "http") {
49976
50432
  const target = buildMcpRequest(opts);
50433
+ await assertMcpHttpUrlSafe(target.url);
49977
50434
  const transport = new StreamableHTTPClientTransport(target.url, {
49978
50435
  requestInit: { headers: target.headers }
49979
50436
  });
@@ -49984,14 +50441,14 @@ async function connectMcp(opts) {
49984
50441
  () => safeCloseTransport(transport)
49985
50442
  );
49986
50443
  } else {
49987
- const baseEnv = {};
49988
- for (const [k, v] of Object.entries(process.env)) {
49989
- if (typeof v === "string") baseEnv[k] = v;
50444
+ const scrubbedEnv = {};
50445
+ for (const [k, v] of Object.entries(buildChildEnv(process.env))) {
50446
+ if (typeof v === "string") scrubbedEnv[k] = v;
49990
50447
  }
49991
50448
  const transport = new StdioClientTransport({
49992
50449
  command: opts.command,
49993
50450
  args: opts.args,
49994
- env: { ...baseEnv, ...opts.env }
50451
+ env: { ...scrubbedEnv, ...opts.env }
49995
50452
  });
49996
50453
  await withTimeout(
49997
50454
  client.connect(transport),
@@ -50082,6 +50539,22 @@ function describe(zodType, schema) {
50082
50539
 
50083
50540
  // ../../packages/adapters/mcp/src/tools.ts
50084
50541
  var MCP_CALL_TIMEOUT_MS = Number(process.env.MCP_CALL_TIMEOUT_MS) || 18e4;
50542
+ var MCP_RESULT_CHAR_CAP = Number(process.env.MCP_RESULT_CHAR_CAP) || 5e4;
50543
+ function capMcpResult(value) {
50544
+ if (typeof value === "string") {
50545
+ if (value.length <= MCP_RESULT_CHAR_CAP) return value;
50546
+ return value.slice(0, MCP_RESULT_CHAR_CAP) + `
50547
+
50548
+ [...truncated at ${MCP_RESULT_CHAR_CAP} chars \u2014 MCP tool result was larger]`;
50549
+ }
50550
+ const serialized = JSON.stringify(value) ?? "";
50551
+ if (serialized.length <= MCP_RESULT_CHAR_CAP) return value;
50552
+ return {
50553
+ truncated: true,
50554
+ originalLength: serialized.length,
50555
+ preview: serialized.slice(0, MCP_RESULT_CHAR_CAP)
50556
+ };
50557
+ }
50085
50558
  function slugToPrefix2(slug) {
50086
50559
  return slug.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
50087
50560
  }
@@ -50096,7 +50569,29 @@ function extractText(content) {
50096
50569
  (c) => typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string"
50097
50570
  ).map((c) => c.text).join("\n");
50098
50571
  }
50099
- function mcpToolToToolDefinition(client, mcpTool, slug) {
50572
+ async function callMcpTool(client, originalName, input) {
50573
+ const result = await client.callTool(
50574
+ { name: originalName, arguments: input },
50575
+ // Default result schema (CallToolResultSchema).
50576
+ void 0,
50577
+ // The MCP SDK's default per-request timeout is 60s — too short for heavy
50578
+ // tools (a Blender/KeyShot render, a long scrape). Raise it and reset the
50579
+ // clock whenever the server reports progress, so progress-streaming
50580
+ // servers can run even longer. Overridable via MCP_CALL_TIMEOUT_MS.
50581
+ { timeout: MCP_CALL_TIMEOUT_MS, resetTimeoutOnProgress: true }
50582
+ );
50583
+ if (result.isError === true) {
50584
+ const detail = extractText(result.content);
50585
+ throw new Error(`MCP tool ${originalName} failed: ${detail || "unknown error"}`);
50586
+ }
50587
+ if (result.structuredContent != null) return capMcpResult(result.structuredContent);
50588
+ const content = result.content ?? [];
50589
+ if (Array.isArray(content) && content.length > 0 && content.every((c) => c.type === "text")) {
50590
+ return capMcpResult(extractText(content));
50591
+ }
50592
+ return capMcpResult(content);
50593
+ }
50594
+ function buildMcpToolDefinition(mcpTool, slug, getClient) {
50100
50595
  const originalName = mcpTool.name;
50101
50596
  return {
50102
50597
  name: `${slugToPrefix2(slug)}__${originalName}`,
@@ -50104,38 +50599,66 @@ function mcpToolToToolDefinition(client, mcpTool, slug) {
50104
50599
  inputSchema: jsonSchemaToZod(mcpTool.inputSchema),
50105
50600
  riskLevel: riskFromAnnotations(mcpTool.annotations),
50106
50601
  async execute(input) {
50107
- const result = await client.callTool(
50108
- {
50109
- name: originalName,
50110
- arguments: input ?? {}
50111
- },
50112
- // Default result schema (CallToolResultSchema).
50113
- void 0,
50114
- // The MCP SDK's default per-request timeout is 60s — too short for heavy
50115
- // tools (a Blender/KeyShot render, a long scrape). Raise it and reset the
50116
- // clock whenever the server reports progress, so progress-streaming
50117
- // servers can run even longer. Overridable via MCP_CALL_TIMEOUT_MS.
50118
- { timeout: MCP_CALL_TIMEOUT_MS, resetTimeoutOnProgress: true }
50119
- );
50120
- if (result.isError === true) {
50121
- const detail = extractText(result.content);
50122
- throw new Error(`MCP tool ${originalName} failed: ${detail || "unknown error"}`);
50123
- }
50124
- if (result.structuredContent != null) return result.structuredContent;
50125
- const content = result.content ?? [];
50126
- if (Array.isArray(content) && content.length > 0 && content.every((c) => c.type === "text")) {
50127
- return extractText(content);
50128
- }
50129
- return content;
50602
+ const client = await getClient();
50603
+ return callMcpTool(client, originalName, input ?? {});
50130
50604
  }
50131
50605
  };
50132
50606
  }
50607
+ function mcpToolToToolDefinition(client, mcpTool, slug) {
50608
+ return buildMcpToolDefinition(mcpTool, slug, () => Promise.resolve(client));
50609
+ }
50610
+ function mcpToolToLazyToolDefinition(getClient, mcpTool, slug) {
50611
+ return buildMcpToolDefinition(mcpTool, slug, getClient);
50612
+ }
50133
50613
 
50134
50614
  // ../../packages/adapters/mcp/src/index.ts
50135
50615
  async function createMcpTools(opts) {
50136
50616
  const conn = await connectMcp(opts);
50137
50617
  const tools = conn.tools.map((t) => mcpToolToToolDefinition(conn.client, t, opts.slug));
50138
- return { tools, close: conn.close };
50618
+ return { tools, descriptors: conn.tools, close: conn.close };
50619
+ }
50620
+ function createLazyMcpTools(opts, cachedDescriptors, lazyOpts = {}) {
50621
+ let connectPromise = null;
50622
+ function ensureConnected() {
50623
+ if (!connectPromise) {
50624
+ connectPromise = connectMcp(opts).then((conn) => {
50625
+ if (lazyOpts.onConnected) {
50626
+ Promise.resolve(lazyOpts.onConnected(conn.tools)).catch((err) => {
50627
+ console.error(
50628
+ `[adapter-mcp] onConnected cache-refresh hook failed for '${opts.slug}':`,
50629
+ err instanceof Error ? err.message : err
50630
+ );
50631
+ });
50632
+ }
50633
+ return conn;
50634
+ }).catch((err) => {
50635
+ connectPromise = null;
50636
+ throw err;
50637
+ });
50638
+ }
50639
+ return connectPromise.then((conn) => conn.client);
50640
+ }
50641
+ const tools = cachedDescriptors.map(
50642
+ (d) => mcpToolToLazyToolDefinition(ensureConnected, d, opts.slug)
50643
+ );
50644
+ return {
50645
+ tools,
50646
+ descriptors: cachedDescriptors,
50647
+ close: async () => {
50648
+ const pending = connectPromise;
50649
+ if (!pending) return;
50650
+ const conn = await pending.catch(() => null);
50651
+ if (conn) await conn.close();
50652
+ }
50653
+ };
50654
+ }
50655
+
50656
+ // src/job/mcp-tool-cache.ts
50657
+ function isUsableMcpToolCache(availableTools) {
50658
+ if (!Array.isArray(availableTools) || availableTools.length === 0) return false;
50659
+ return availableTools.every(
50660
+ (t) => typeof t === "object" && t !== null && typeof t.name === "string" && t.inputSchema !== void 0
50661
+ );
50139
50662
  }
50140
50663
 
50141
50664
  // src/job/resolve-llm.ts
@@ -50174,7 +50697,10 @@ async function resolveAgentLlmClient(db, agent, onSkip) {
50174
50697
  provider: row.provider,
50175
50698
  model,
50176
50699
  apiKey: plaintextKey || void 0,
50177
- baseURL: row.baseUrl ?? void 0
50700
+ baseURL: row.baseUrl ?? void 0,
50701
+ // É-3: per-model context window for a custom/local endpoint the catalog
50702
+ // can't know — used as a fallback in modelContextWindow (catalog wins).
50703
+ contextWindow: row.contextWindow ?? void 0
50178
50704
  });
50179
50705
  }
50180
50706
  if (configs.length === 0) return { ok: false, reason: "agent_no_llm_configured" };
@@ -50563,6 +51089,83 @@ async function generateAssignTools(parentAgentId, db) {
50563
51089
  return tools;
50564
51090
  }
50565
51091
 
51092
+ // ../../packages/orchestration/src/router/tool-availability.ts
51093
+ init_src();
51094
+ var KNOWN_FIXED_TOOL_NAMES = /* @__PURE__ */ new Set([
51095
+ ...ALWAYS_ON_TOOLS,
51096
+ ...DELIVERY_TOOL_NAMES,
51097
+ ...META_TOOL_NAMES,
51098
+ "run_skill_script",
51099
+ "skill_file_write"
51100
+ ]);
51101
+ var KNOWN_CONNECTOR_TOOL_NAMES = new Set(
51102
+ Object.values(ADAPTER_REGISTRY).flatMap((entry) => entry.operations.map((o) => o.slug))
51103
+ );
51104
+ var KNOWN_TOOL_NAME_UNIVERSE = /* @__PURE__ */ new Set([
51105
+ ...KNOWN_FIXED_TOOL_NAMES,
51106
+ ...KNOWN_CONNECTOR_TOOL_NAMES
51107
+ ]);
51108
+ async function computeAgentToolNames(agentId, entityId, db, opts) {
51109
+ const names = new Set(ALWAYS_ON_TOOLS);
51110
+ const [agentRow] = await db.select({ telegramBotToken: agents.telegramBotToken }).from(agents).where(eq5(agents.id, agentId)).limit(1);
51111
+ if (opts.hasDeliveryRecipient) {
51112
+ let deliveryToken = agentRow?.telegramBotToken ?? null;
51113
+ if (!deliveryToken) {
51114
+ const [rootRow] = await db.select({ telegramBotToken: agents.telegramBotToken }).from(entities).innerJoin(agents, eq5(agents.id, entities.rootAgentId)).where(eq5(entities.id, entityId)).limit(1);
51115
+ deliveryToken = rootRow?.telegramBotToken ?? null;
51116
+ }
51117
+ if (deliveryToken) {
51118
+ for (const n of DELIVERY_TOOL_NAMES) names.add(n);
51119
+ }
51120
+ }
51121
+ const skillRows = await db.select({
51122
+ requiredBuiltins: agentSkills.requiredBuiltins,
51123
+ scriptsAuthorized: agentSkillAssignments.scriptsAuthorized,
51124
+ filesWritable: agentSkillAssignments.filesWritable
51125
+ }).from(agentSkillAssignments).innerJoin(agentSkills, eq5(agentSkills.id, agentSkillAssignments.skillId)).where(eq5(agentSkillAssignments.agentId, agentId));
51126
+ for (const r of skillRows) {
51127
+ for (const n of r.requiredBuiltins ?? []) names.add(n);
51128
+ if (r.scriptsAuthorized) names.add("run_skill_script");
51129
+ if (r.filesWritable) names.add("skill_file_write");
51130
+ }
51131
+ const connectorRows = await db.select({
51132
+ slug: connectors.slug,
51133
+ enabledOperations: agentConnectorAssignments.enabledOperations
51134
+ }).from(agentConnectorAssignments).innerJoin(connectors, eq5(connectors.id, agentConnectorAssignments.connectorId)).where(eq5(agentConnectorAssignments.agentId, agentId));
51135
+ for (const r of connectorRows) {
51136
+ const entry = ADAPTER_REGISTRY[r.slug];
51137
+ if (!entry) continue;
51138
+ const allToolNames = entry.operations.map((o) => o.slug);
51139
+ const kept = r.enabledOperations === null ? allToolNames : allToolNames.filter((n) => r.enabledOperations.includes(n));
51140
+ for (const n of kept) names.add(n);
51141
+ }
51142
+ const mcpRows = await db.select({
51143
+ serverSlug: mcpServers.slug,
51144
+ enabledTools: agentMcpServers.enabledTools,
51145
+ availableTools: mcpServers.availableTools
51146
+ }).from(agentMcpServers).innerJoin(mcpServers, eq5(mcpServers.id, agentMcpServers.mcpServerId)).where(eq5(agentMcpServers.agentId, agentId));
51147
+ for (const r of mcpRows) {
51148
+ const prefix = r.serverSlug.replace(/-/g, "_");
51149
+ const available = Array.isArray(r.availableTools) ? r.availableTools.map((t) => t && typeof t.name === "string" ? t.name : null).filter((n) => n !== null) : [];
51150
+ const enabled = Array.isArray(r.enabledTools) ? new Set(r.enabledTools.filter((n) => typeof n === "string")) : null;
51151
+ const kept = enabled === null ? available : available.filter((n) => enabled.has(n));
51152
+ for (const n of kept) names.add(`${prefix}__${n}`);
51153
+ }
51154
+ const [entityRow] = await db.select({ rootAgentId: entities.rootAgentId, rootGrants: entities.rootGrants }).from(entities).where(eq5(entities.id, entityId)).limit(1);
51155
+ if (entityRow?.rootAgentId === agentId) {
51156
+ for (const n of enabledMetaTools(parseRootGrants(entityRow.rootGrants))) names.add(n);
51157
+ }
51158
+ return names;
51159
+ }
51160
+ function findUnavailableToolMentions(briefText, availableToolNames) {
51161
+ const words = briefText.match(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g) ?? [];
51162
+ const missing = /* @__PURE__ */ new Set();
51163
+ for (const w of words) {
51164
+ if (KNOWN_TOOL_NAME_UNIVERSE.has(w) && !availableToolNames.has(w)) missing.add(w);
51165
+ }
51166
+ return [...missing];
51167
+ }
51168
+
50566
51169
  // ../../packages/orchestration/src/router/delegate.ts
50567
51170
  var TERMINAL_JOB_STATUSES = ["completed", "failed", "cancelled"];
50568
51171
  async function handleDelegation(parentJob, childSlug, toolUseId, taskInput, sideToolResults, db) {
@@ -50594,6 +51197,9 @@ ${taskInput.data}` : taskInput.task;
50594
51197
  status: "pending",
50595
51198
  parentJobId: parentJob.id,
50596
51199
  delegationDepth: childDepth,
51200
+ // Jobs page grouping (migration 0059): a delegated child is the SAME
51201
+ // conversation as its parent, not a new one.
51202
+ conversationId: parentJob.conversationId ?? void 0,
50597
51203
  messages: [{ role: "user", content: childContent }]
50598
51204
  }).returning();
50599
51205
  if (!childJob) {
@@ -50907,6 +51513,20 @@ function generateTaskTools(orchestratorAgentId, db) {
50907
51513
  const agentRows = await db.select({ id: agents.id }).from(agents).where(and4(eq5(agents.slug, input.assigned_to), eq5(agents.entityId, ctx.entityId))).limit(1);
50908
51514
  assignedAgentId = agentRows[0]?.id ?? null;
50909
51515
  }
51516
+ let warning;
51517
+ if (assignedAgentId) {
51518
+ const briefText = `${input.title} ${input.description ?? ""}`;
51519
+ const availableToolNames = await computeAgentToolNames(
51520
+ assignedAgentId,
51521
+ ctx.entityId,
51522
+ db,
51523
+ { hasDeliveryRecipient: false }
51524
+ );
51525
+ const missing = findUnavailableToolMentions(briefText, availableToolNames);
51526
+ if (missing.length > 0) {
51527
+ warning = `warning: le brief mentionne l'outil '${missing[0]}' que l'agent cible '${input.assigned_to}' n'aura pas \u2014 prescris le r\xE9sultat attendu plut\xF4t que l'outil, ou retire la mention`;
51528
+ }
51529
+ }
50910
51530
  const dependsOn = input.depends_on ?? [];
50911
51531
  const newTaskId = randomUUID2();
50912
51532
  await validateDependencies(newTaskId, dependsOn, ctx.entityId, db);
@@ -50927,7 +51547,7 @@ function generateTaskTools(orchestratorAgentId, db) {
50927
51547
  if (!task) {
50928
51548
  throw new Error("task_board_error: failed to insert task");
50929
51549
  }
50930
- return { taskId: task.id, title: task.title };
51550
+ return { taskId: task.id, title: task.title, ...warning ? { warning } : {} };
50931
51551
  }
50932
51552
  };
50933
51553
  const listTasksTool = {
@@ -51146,13 +51766,29 @@ init_src();
51146
51766
  // ../../packages/orchestration/src/agent-baseline.ts
51147
51767
  var NEEDS_FIRMER_VERIFY = /deepseek|minimax|qwen|glm|gemma|kimi|mistral|llama/i;
51148
51768
  var contentOfKind = (kind) => systemSkills.filter((s) => skillKind(s) === kind).map((s) => s.content.trim());
51149
- function buildBaselineBlock(model) {
51769
+ var MEMORY_DISCIPLINE_BLOCK = `## Memory discipline
51770
+
51771
+ ### Correct what's wrong
51772
+
51773
+ If a fact from your Persistent memory block turns out to be false in practice \u2014 a file path that doesn't exist, an invalid ID, a procedure that fails the way the memory said it wouldn't \u2014 you MUST call \`mark_memory_outdated\` on it with the reason, then \`save_memory\` the corrected fact once you have verified it. Never silently keep reusing a fact you just found to be wrong.
51774
+
51775
+ ### What's worth saving
51776
+
51777
+ A fact you save via \`save_memory\` must describe something VERIFIED \u2014 an exact path you confirmed, a real ID, a preference the user stated, a procedure that actually worked. Never save a micromanagement rule for another agent, and never save a discovery ban (e.g. "don't search for X", "don't explore Y") \u2014 every agent stays free to check things for itself when what it was given turns out to be wrong.`;
51778
+ var WORKER_DISCOVERY_BLOCK = `## Capitalize what you learn
51779
+
51780
+ When you discover something durable while working a task \u2014 the real path of a file or workflow, parameters that worked, a convention \u2014 save it via \`save_memory\` before you finish. One fact per call, short and verified.`;
51781
+ var DELEGATION_DISCIPLINE_BLOCK = `## Delegation discipline
51782
+
51783
+ When you delegate: (1) pass the PARAMETERS in the brief (paths, prompts, values) \u2014 do not do the prep work yourself that the worker can do with its own tools; (2) NEVER edit a shared or template file to encode a run's parameters \u2014 templates are immutable, values are passed as arguments; (3) only name a specific tool in a brief if you know the target agent has it \u2014 otherwise state the expected RESULT (the worker returns it via \`return_result\`) and deliver it yourself once it comes back; (4) a brief states the goal, the parameters, and the constraints \u2014 not a step-by-step procedure that forbids the worker from adapting.`;
51784
+ function buildBaselineBlock(model, opts = {}) {
51150
51785
  const parts = contentOfKind("baseline");
51151
- if (parts.length === 0) return "";
51152
- const reinforcement = NEEDS_FIRMER_VERIFY.test(model) ? "\n\n**Especially you \u2014 execution discipline:** Actually run or check your work before you say a task is done, and never write tool output you did not really get back. Be decisive: once a check passes (e.g. dependencies report ready), DO the action \u2014 do not keep re-verifying, re-listing, or running diagnostic commands. Use the tools, scripts, and exact file paths you were given (a skill loaded with skill_view ships run_skill_script and ready-made workflows/templates) instead of writing your own helper or conversion scripts, or rebuilding what already exists. Take the fewest steps that finish the task, then deliver the result with its output path." : "";
51153
- return `## How you work (always)
51786
+ const reinforcement = parts.length > 0 && NEEDS_FIRMER_VERIFY.test(model) ? "\n\n**Especially you \u2014 execution discipline:** Actually run or check your work before you say a task is done, and never write tool output you did not really get back. Be decisive: once a check passes (e.g. dependencies report ready), DO the action \u2014 do not keep re-verifying, re-listing, or running diagnostic commands. Use the tools, scripts, and exact file paths you were given (a skill loaded with skill_view ships run_skill_script and ready-made workflows/templates) instead of writing your own helper or conversion scripts, or rebuilding what already exists. Take the fewest steps that finish the task, then deliver the result with its output path." : "";
51787
+ const catalogBlock = parts.length > 0 ? `## How you work (always)
51154
51788
 
51155
- ${parts.join("\n\n")}${reinforcement}`;
51789
+ ${parts.join("\n\n")}${reinforcement}` : "";
51790
+ const roleBlock = opts.role === "orchestrator" ? DELEGATION_DISCIPLINE_BLOCK : WORKER_DISCOVERY_BLOCK;
51791
+ return [catalogBlock, MEMORY_DISCIPLINE_BLOCK, roleBlock].filter(Boolean).join("\n\n");
51156
51792
  }
51157
51793
  function buildChannelBlock(opts) {
51158
51794
  const onTelegram = opts.channel === "telegram" || opts.telegram === true;
@@ -51359,7 +51995,7 @@ ${skillIndex}` : "";
51359
51995
  }) : [];
51360
51996
  const memoryBlock = buildPersistentMemoryBlock(memoryRows);
51361
51997
  const jobContextBlock = jobContext ? buildJobContextBlock(jobContext) : "";
51362
- const baselineBlock = buildBaselineBlock(agent.model);
51998
+ const baselineBlock = buildBaselineBlock(agent.model, { role: agent.role });
51363
51999
  const channelBlock = buildChannelBlock({
51364
52000
  channel: jobContext?.origin,
51365
52001
  telegram: Boolean(jobContext?.telegramChatId)
@@ -51792,6 +52428,7 @@ import { mkdirSync as mkdirSync2 } from "node:fs";
51792
52428
  import { readFile as readFile11 } from "node:fs/promises";
51793
52429
 
51794
52430
  // src/approvals/notify.ts
52431
+ init_src();
51795
52432
  var APPROVAL_CALLBACK_PREFIX = "apr";
51796
52433
  function approvalCallbackData(approvalRequestId, decision) {
51797
52434
  return `${APPROVAL_CALLBACK_PREFIX}:${approvalRequestId}:${decision}`;
@@ -51818,7 +52455,7 @@ async function resolveTelegramDeliveryTarget(db, jobId) {
51818
52455
  return null;
51819
52456
  }
51820
52457
  function describeGatedAction(toolName, toolInput) {
51821
- const input = toolInput ?? {};
52458
+ const input = redactSecretsForAudit(toolInput ?? {});
51822
52459
  const str = (v) => typeof v === "string" ? v : JSON.stringify(v ?? null);
51823
52460
  switch (toolName) {
51824
52461
  case "run_command":
@@ -51843,15 +52480,14 @@ async function notifyApprovalCreated(deps, req) {
51843
52480
  const who = agent?.name ?? "An agent";
51844
52481
  const input = req.toolInput ?? {};
51845
52482
  const purpose = typeof input["purpose"] === "string" ? input["purpose"].trim() : "";
51846
- const impact = typeof input["impact"] === "string" ? input["impact"].trim() : "";
52483
+ const impact = computeApprovalImpactLine(req.toolName, req.toolInput);
51847
52484
  const detail = describeGatedAction(req.toolName, req.toolInput);
51848
52485
  const detailShort = detail.length > 500 ? detail.slice(0, 500) + "\n\u2026 (full detail on the dashboard)" : detail;
51849
52486
  const text23 = `\u23F3 Approval needed \u2014 ${who}
51850
52487
 
51851
- ` + (purpose ? `\u27A4 ${purpose}
51852
- ` : `${who} wants to run an action.
51853
- `) + (impact ? `\u26A0\uFE0F Impact: ${impact}
51854
- ` : "") + `
52488
+ \u27A4 ${purpose || "Purpose not specified by the agent."}
52489
+ \u26A0\uFE0F ${impact}
52490
+
51855
52491
  Details:
51856
52492
  ${detailShort}
51857
52493
 
@@ -51876,7 +52512,7 @@ import { join as join9 } from "node:path";
51876
52512
 
51877
52513
  // src/skills/install.ts
51878
52514
  import { cp, rm as rm2, mkdir as mkdir4, readFile as readFile10 } from "node:fs/promises";
51879
- import { dirname as dirname7, join as join8, basename as basename5, sep as sep5 } from "node:path";
52515
+ import { dirname as dirname7, join as join8, basename as basename5, sep as sep5, resolve as resolve2 } from "node:path";
51880
52516
 
51881
52517
  // src/skills/source.ts
51882
52518
  var SkillSourceError = class extends Error {
@@ -51887,6 +52523,11 @@ var SkillSourceError = class extends Error {
51887
52523
  };
51888
52524
  var SEGMENT = "[A-Za-z0-9._-]+";
51889
52525
  var OWNER_REPO = new RegExp(`^(${SEGMENT})/(${SEGMENT})$`);
52526
+ var SEGMENT_RE = new RegExp(`^${SEGMENT}$`);
52527
+ function isSafeSubdir(subdir) {
52528
+ if (subdir.startsWith("/") || subdir.includes("\\")) return false;
52529
+ return !subdir.split("/").includes("..");
52530
+ }
51890
52531
  var ALLOWED_HOSTS = /* @__PURE__ */ new Set([
51891
52532
  "github.com",
51892
52533
  "www.github.com",
@@ -52010,12 +52651,22 @@ function parseSkillSource(input) {
52010
52651
  });
52011
52652
  }
52012
52653
  const segs = raw.split("/").filter(Boolean);
52013
- if (segs.length >= 2 && new RegExp(`^${SEGMENT}$`).test(segs[0])) {
52654
+ if (segs.length >= 2 && SEGMENT_RE.test(segs[0])) {
52655
+ const repo = stripGitSuffix(segs[1]);
52656
+ if (!SEGMENT_RE.test(repo)) {
52657
+ throw new SkillSourceError(`Invalid repository name in "${raw}".`);
52658
+ }
52659
+ const subdir = segs.slice(2).join("/") || null;
52660
+ if (subdir !== null && !isSafeSubdir(subdir)) {
52661
+ throw new SkillSourceError(
52662
+ `Unsafe path in skill source "${raw}": must not contain ".." or an absolute/backslash path.`
52663
+ );
52664
+ }
52014
52665
  return github({
52015
52666
  owner: segs[0],
52016
- repo: stripGitSuffix(segs[1]),
52667
+ repo,
52017
52668
  ref: null,
52018
- subdir: segs.slice(2).join("/") || null,
52669
+ subdir,
52019
52670
  skillName: null,
52020
52671
  raw
52021
52672
  });
@@ -52090,7 +52741,8 @@ async function fetchAllowlisted(url2, headers) {
52090
52741
  const res = await fetch(current, { redirect: "manual", headers });
52091
52742
  if (res.status >= 300 && res.status < 400) {
52092
52743
  const location = res.headers.get("location");
52093
- if (!location) throw new SkillFetchError(`Redirect response (${res.status}) missing Location.`);
52744
+ if (!location)
52745
+ throw new SkillFetchError(`Redirect response (${res.status}) missing Location.`);
52094
52746
  current = new URL(location, current).toString();
52095
52747
  continue;
52096
52748
  }
@@ -52152,7 +52804,9 @@ async function unzipBufferTo(buf, destDir) {
52152
52804
  }
52153
52805
  if (final && !violation) {
52154
52806
  const fileData = Buffer.concat(chunks);
52155
- pendingWrites.push(mkdir3(dirname6(abs), { recursive: true }).then(() => writeFile5(abs, fileData)));
52807
+ pendingWrites.push(
52808
+ mkdir3(dirname6(abs), { recursive: true }).then(() => writeFile5(abs, fileData))
52809
+ );
52156
52810
  }
52157
52811
  };
52158
52812
  file.start();
@@ -52614,7 +53268,13 @@ function buildContent(slug, body, scripts) {
52614
53268
  async function pickManifest(extractRoot, subdir, skillName) {
52615
53269
  if (subdir) {
52616
53270
  const rel = `${subdir.replace(/\/+$/, "")}/SKILL.md`;
52617
- if (await isFile(join8(extractRoot, rel))) return rel;
53271
+ const rootResolved = resolve2(extractRoot);
53272
+ const rootWithSep = rootResolved.endsWith(sep5) ? rootResolved : rootResolved + sep5;
53273
+ const abs = resolve2(join8(extractRoot, rel));
53274
+ if (abs !== rootResolved && !abs.startsWith(rootWithSep)) {
53275
+ throw new SkillInstallError(`Unsafe skill path: "${subdir}" escapes the repository.`);
53276
+ }
53277
+ if (await isFile(abs)) return rel;
52618
53278
  throw new SkillInstallError(`No SKILL.md at "${subdir}" in the repository.`);
52619
53279
  }
52620
53280
  const manifests = await findSkillManifests(extractRoot);
@@ -53232,23 +53892,11 @@ async function resolveSearchBackend(db, agentId) {
53232
53892
  }
53233
53893
  return void 0;
53234
53894
  }
53235
- var DELIVERY_TOOL_NAMES = /* @__PURE__ */ new Set([
53236
- "telegram_send_message",
53237
- "send_image",
53238
- "send_file",
53239
- "send_video",
53240
- "send_audio",
53241
- "send_voice"
53242
- ]);
53895
+ var DELIVERY_TOOL_NAMES2 = new Set(DELIVERY_TOOL_NAMES);
53243
53896
  var DELIVERY_OR_TERMINAL_TOOL_NAMES = /* @__PURE__ */ new Set([
53244
53897
  "return_result",
53245
53898
  "dashboard_publish",
53246
- "telegram_send_message",
53247
- "send_image",
53248
- "send_file",
53249
- "send_video",
53250
- "send_audio",
53251
- "send_voice"
53899
+ ...DELIVERY_TOOL_NAMES
53252
53900
  ]);
53253
53901
  var TOOL_ONLY_DELIVERY_CHANNELS = /* @__PURE__ */ new Set(["telegram"]);
53254
53902
  function truncateForContext(value) {
@@ -53412,6 +54060,15 @@ async function maybeResumeParent(childJobId, outcome, deps, runnerEnv) {
53412
54060
  void triggerWorker(parentJobId, runnerEnv);
53413
54061
  }
53414
54062
  }
54063
+ async function reviveJobIfApprovalResolvedDuringSuspend(db, jobId, runnerEnv) {
54064
+ const openRows = await db.select({ status: approvalRequests.status }).from(approvalRequests).where(and4(eq5(approvalRequests.jobId, jobId), isNull(approvalRequests.executedAt)));
54065
+ const hasResolved = openRows.some((r) => r.status === "approved" || r.status === "rejected");
54066
+ if (!hasResolved) return;
54067
+ const flipped = await db.update(agentJobs).set({ status: "pending", updatedAt: /* @__PURE__ */ new Date() }).where(and4(eq5(agentJobs.id, jobId), eq5(agentJobs.status, "awaiting_approval"))).returning({ id: agentJobs.id });
54068
+ if (flipped.length > 0 && runnerEnv) {
54069
+ void triggerWorker(jobId, runnerEnv);
54070
+ }
54071
+ }
53415
54072
  async function runJob(jobId, deps, runnerEnv, opts) {
53416
54073
  const { db, registry } = deps;
53417
54074
  let llmClient;
@@ -53487,8 +54144,8 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53487
54144
  const sharedPath = join10(homedir3(), ".nodalai", "workspaces", job.entityId, "shared");
53488
54145
  try {
53489
54146
  mkdirSync2(sharedPath, { recursive: true });
53490
- if (!agentWorkspacesList.some((w) => w.label === "shared")) {
53491
- agentWorkspacesList.push({ label: "shared", path: sharedPath });
54147
+ if (!agentWorkspacesList.some((w) => w.label === SHARED_WORKSPACE_LABEL)) {
54148
+ agentWorkspacesList.push({ label: SHARED_WORKSPACE_LABEL, path: sharedPath });
53492
54149
  }
53493
54150
  } catch {
53494
54151
  }
@@ -53555,7 +54212,13 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53555
54212
  const fileWriteToolNames = fileWritableSkillSlugs.length > 0 ? ["skill_file_write"] : [];
53556
54213
  const fileWriteToolDefs = fileWriteToolNames.map((n) => registry.get(n)).filter((t) => t !== void 0);
53557
54214
  const capabilityTools = [];
53558
- if (agentRow.telegramBotToken && job.chatId) {
54215
+ let inheritedRootBotToken = null;
54216
+ if (!agentRow.telegramBotToken && job.parentJobId && job.chatId) {
54217
+ const [rootAgentTokenRow] = await db.select({ telegramBotToken: agents.telegramBotToken }).from(entities).innerJoin(agents, eq5(agents.id, entities.rootAgentId)).where(eq5(entities.id, job.entityId ?? "")).limit(1);
54218
+ inheritedRootBotToken = rootAgentTokenRow?.telegramBotToken ?? null;
54219
+ }
54220
+ const deliveryBotToken = agentRow.telegramBotToken ?? inheritedRootBotToken;
54221
+ if (deliveryBotToken && job.chatId) {
53559
54222
  capabilityTools.push(createTelegramSendMessageTool());
53560
54223
  capabilityTools.push(createSendImageTool());
53561
54224
  capabilityTools.push(createSendFileTool());
@@ -53645,6 +54308,7 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53645
54308
  capabilityTools.push(...filtered);
53646
54309
  }
53647
54310
  const mcpAssignments = await db.select({
54311
+ id: mcpServers.id,
53648
54312
  slug: mcpServers.slug,
53649
54313
  transport: mcpServers.transport,
53650
54314
  url: mcpServers.url,
@@ -53654,10 +54318,21 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53654
54318
  command: mcpServers.command,
53655
54319
  args: mcpServers.args,
53656
54320
  envVars: mcpServers.envVars,
54321
+ availableTools: mcpServers.availableTools,
53657
54322
  enabledTools: agentMcpServers.enabledTools
53658
54323
  }).from(agentMcpServers).innerJoin(mcpServers, eq5(mcpServers.id, agentMcpServers.mcpServerId)).where(eq5(agentMcpServers.agentId, agentRow.id));
54324
+ const refreshMcpToolCache = async (mcpServerId, liveTools) => {
54325
+ try {
54326
+ await db.update(mcpServers).set({ availableTools: liveTools }).where(eq5(mcpServers.id, mcpServerId));
54327
+ } catch (err) {
54328
+ console.error(
54329
+ `[execute] failed to refresh available_tools cache for MCP server '${mcpServerId}': ${err instanceof Error ? err.message : String(err)}`
54330
+ );
54331
+ }
54332
+ };
53659
54333
  for (const ms of mcpAssignments) {
53660
54334
  try {
54335
+ const availableTools = ms.availableTools;
53661
54336
  let toolset;
53662
54337
  if (ms.transport === "stdio") {
53663
54338
  if (!ms.command) continue;
@@ -53670,13 +54345,23 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53670
54345
  } catch {
53671
54346
  continue;
53672
54347
  }
53673
- toolset = await createMcpTools({
54348
+ const connectOpts = {
53674
54349
  transport: "stdio",
53675
54350
  slug: ms.slug,
53676
54351
  command: ms.command,
53677
54352
  args: ms.args ?? [],
53678
54353
  env: decryptedEnv
53679
- });
54354
+ };
54355
+ if (isUsableMcpToolCache(availableTools)) {
54356
+ toolset = createLazyMcpTools(connectOpts, availableTools, {
54357
+ onConnected: (liveTools) => refreshMcpToolCache(ms.id, liveTools)
54358
+ });
54359
+ } else {
54360
+ toolset = await createMcpTools(connectOpts);
54361
+ if (toolset.descriptors?.length) {
54362
+ await refreshMcpToolCache(ms.id, toolset.descriptors);
54363
+ }
54364
+ }
53680
54365
  } else {
53681
54366
  if (!ms.url || !ms.apiKey || !ms.authScheme || !ms.authParamName) continue;
53682
54367
  let decryptedKey;
@@ -53688,14 +54373,24 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53688
54373
  );
53689
54374
  continue;
53690
54375
  }
53691
- toolset = await createMcpTools({
54376
+ const connectOpts = {
53692
54377
  transport: "http",
53693
54378
  slug: ms.slug,
53694
54379
  url: ms.url,
53695
54380
  apiKey: decryptedKey,
53696
54381
  authScheme: ms.authScheme,
53697
54382
  authParamName: ms.authParamName
53698
- });
54383
+ };
54384
+ if (isUsableMcpToolCache(availableTools)) {
54385
+ toolset = createLazyMcpTools(connectOpts, availableTools, {
54386
+ onConnected: (liveTools) => refreshMcpToolCache(ms.id, liveTools)
54387
+ });
54388
+ } else {
54389
+ toolset = await createMcpTools(connectOpts);
54390
+ if (toolset.descriptors?.length) {
54391
+ await refreshMcpToolCache(ms.id, toolset.descriptors);
54392
+ }
54393
+ }
53699
54394
  }
53700
54395
  mcpClosers.push(toolset.close);
53701
54396
  const enabled = ms.enabledTools;
@@ -53744,7 +54439,13 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53744
54439
  }));
53745
54440
  const authMode = runnerEnv?.AUTH_MODE ?? process.env["AUTH_MODE"] ?? "local-trust";
53746
54441
  if (authMode !== "local-trust") {
53747
- const CODE_EXECUTION_TOOLS = ["run_command", "run_skill_script", "skill_file_write"];
54442
+ const CODE_EXECUTION_TOOLS = [
54443
+ "run_command",
54444
+ "run_skill_script",
54445
+ "skill_file_write",
54446
+ "create_mcp",
54447
+ "attach_mcp"
54448
+ ];
53748
54449
  const [yoloEntityRow] = await db.select({ lanCommandYolo: entities.lanCommandYolo }).from(entities).where(eq5(entities.id, job.entityId ?? "")).limit(1);
53749
54450
  if (!yoloEntityRow?.lanCommandYolo) {
53750
54451
  for (const codeTool of CODE_EXECUTION_TOOLS) {
@@ -53805,102 +54506,109 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53805
54506
  if (serialized.length <= MAX_TOOL_RESULT_CHARS) return { type: "json", value: json };
53806
54507
  return { type: "text", value: truncateForContext(serialized) };
53807
54508
  };
53808
- {
53809
- const pendingExecRows = await db.select().from(approvalRequests).where(and4(eq5(approvalRequests.jobId, jobId), isNull(approvalRequests.executedAt))).orderBy(approvalRequests.requestedAt);
53810
- const resolvedRows = pendingExecRows.filter(
53811
- (r) => r.status === "approved" || r.status === "rejected"
53812
- );
54509
+ const executeResolvedApprovals = async (resolvedRows, msgsIn) => {
54510
+ let msgs = msgsIn;
53813
54511
  let catastrophicRefusalMessage = null;
53814
- if (resolvedRows.length > 0) {
53815
- for (const req of resolvedRows) {
53816
- let replacementOutput;
53817
- if (req.status === "approved") {
53818
- const resumeCommand = String(
53819
- req.toolInput?.command ?? ""
53820
- );
53821
- const isCatastrophicResume = req.toolName === "run_command" && isCatastrophicCommand(resumeCommand);
53822
- if (isCatastrophicResume) {
53823
- catastrophicRefusalMessage = isInlineInterpreterEvalCommand(resumeCommand) ? "Cette commande ex\xE9cute du code en ligne via un interpr\xE9teur (python -c, node -e, sh -c, \u2026). Son contenu ne peut pas \xEAtre v\xE9rifi\xE9 automatiquement, donc elle est bloqu\xE9e par s\xE9curit\xE9 et reste refus\xE9e m\xEAme apr\xE8s approbation. Elle n'a pas \xE9t\xE9 ex\xE9cut\xE9e. Pour lancer un script, passez par un FICHIER (ex. `python mon_script.py`) plut\xF4t que par du code en ligne." : "Cette commande est jug\xE9e catastrophique (destruction machine-wide) et reste refus\xE9e m\xEAme apr\xE8s approbation, par s\xE9curit\xE9. Elle n'a pas \xE9t\xE9 ex\xE9cut\xE9e.";
53824
- replacementOutput = toResultOutput({ error: catastrophicRefusalMessage });
53825
- trace("resume_catastrophic_command_refused", { toolName: req.toolName });
54512
+ for (const req of resolvedRows) {
54513
+ let replacementOutput;
54514
+ if (req.status === "approved") {
54515
+ const resumeCommand = String(
54516
+ req.toolInput?.command ?? ""
54517
+ );
54518
+ const isCatastrophicResume = req.toolName === "run_command" && isCatastrophicCommand(resumeCommand);
54519
+ if (isCatastrophicResume) {
54520
+ catastrophicRefusalMessage = "Cette commande est jug\xE9e catastrophique (destruction machine-wide : rm -rf /, mkfs, shutdown\u2026) et reste refus\xE9e m\xEAme apr\xE8s approbation, par s\xE9curit\xE9. Elle n'a pas \xE9t\xE9 ex\xE9cut\xE9e.";
54521
+ replacementOutput = toResultOutput({ error: catastrophicRefusalMessage });
54522
+ trace("resume_catastrophic_command_refused", { toolName: req.toolName });
54523
+ } else {
54524
+ const toolDef = toolMap.get(req.toolName);
54525
+ if (!toolDef) {
54526
+ replacementOutput = toResultOutput({
54527
+ error: `approved_tool_not_found:${req.toolName}`
54528
+ });
53826
54529
  } else {
53827
- const toolDef = toolMap.get(req.toolName);
53828
- if (!toolDef) {
53829
- replacementOutput = toResultOutput({
53830
- error: `approved_tool_not_found:${req.toolName}`
53831
- });
53832
- } else {
53833
- const resumeApprovalRules = [
53834
- {
53835
- id: "resume-bypass",
53836
- entityId: job.entityId ?? "",
53837
- agentId: null,
53838
- toolName: req.toolName,
53839
- action: "auto_approve"
53840
- }
53841
- ];
53842
- const execResult = await executeTool(
53843
- toolDef,
53844
- req.toolInput,
53845
- {
53846
- jobId,
53847
- agentId: agentRow.id,
53848
- entityId: job.entityId ?? "",
53849
- db,
53850
- jobChatId: job.chatId ?? null,
53851
- embeddingClient: deps.embeddingClient,
53852
- workspaces: agentWorkspacesList,
53853
- skillStoreDir: skillStore,
53854
- assignedSkillSlugs,
53855
- scriptAuthorizedSkillSlugs,
53856
- fileWritableSkillSlugs,
53857
- provisioning: TOOL_PROVISIONING,
53858
- searchBackend
53859
- },
53860
- {
53861
- approvalRules: resumeApprovalRules,
53862
- autonomy: workspaceAutonomy,
53863
- onApprovalRequired: (req2) => notifyApprovalCreated(deps, req2)
53864
- }
53865
- );
53866
- if (execResult.outcome === "success") {
53867
- replacementOutput = toResultOutput(execResult.output);
53868
- } else if (execResult.outcome === "error") {
53869
- replacementOutput = toResultOutput({ error: execResult.error });
53870
- } else {
53871
- replacementOutput = toResultOutput({ error: "unexpected_gate_on_approved_tool" });
54530
+ const resumeApprovalRules = [
54531
+ {
54532
+ id: "resume-bypass",
54533
+ entityId: job.entityId ?? "",
54534
+ agentId: null,
54535
+ toolName: req.toolName,
54536
+ action: "auto_approve"
54537
+ }
54538
+ ];
54539
+ const execResult = await executeTool(
54540
+ toolDef,
54541
+ req.toolInput,
54542
+ {
54543
+ jobId,
54544
+ agentId: agentRow.id,
54545
+ entityId: job.entityId ?? "",
54546
+ db,
54547
+ jobChatId: job.chatId ?? null,
54548
+ resolvedTelegramBotToken: inheritedRootBotToken ?? void 0,
54549
+ embeddingClient: deps.embeddingClient,
54550
+ workspaces: agentWorkspacesList,
54551
+ skillStoreDir: skillStore,
54552
+ assignedSkillSlugs,
54553
+ scriptAuthorizedSkillSlugs,
54554
+ fileWritableSkillSlugs,
54555
+ provisioning: TOOL_PROVISIONING,
54556
+ searchBackend
54557
+ },
54558
+ {
54559
+ approvalRules: resumeApprovalRules,
54560
+ autonomy: workspaceAutonomy,
54561
+ onApprovalRequired: (r) => notifyApprovalCreated(deps, r)
53872
54562
  }
54563
+ );
54564
+ if (execResult.outcome === "success") {
54565
+ replacementOutput = toResultOutput(execResult.output);
54566
+ } else if (execResult.outcome === "error") {
54567
+ replacementOutput = toResultOutput({ error: execResult.error });
54568
+ } else {
54569
+ replacementOutput = toResultOutput({ error: "unexpected_gate_on_approved_tool" });
53873
54570
  }
53874
54571
  }
53875
- trace("resume_approved_tool_executed", { toolName: req.toolName });
53876
- } else {
53877
- const reason = req.notes ?? "no reason provided";
53878
- replacementOutput = toResultOutput(
53879
- `[REJECTED] Human reviewer rejected this action. Reason: ${reason}. Adapt your approach.`
53880
- );
53881
- trace("resume_rejected_tool_marker_replaced", { toolName: req.toolName });
53882
54572
  }
53883
- messages = messages.map((msg) => {
53884
- if (typeof msg !== "object" || msg === null || msg.role !== "tool") {
53885
- return msg;
53886
- }
53887
- const toolMsg = msg;
53888
- if (!Array.isArray(toolMsg.content)) return msg;
53889
- const updatedContent = toolMsg.content.map((block) => {
53890
- if (typeof block !== "object" || block === null || block.type !== "tool-result") {
53891
- return block;
53892
- }
53893
- const tb = block;
53894
- if (tb.toolName !== req.toolName) return block;
53895
- const outputText = tb.output.type === "text" ? tb.output.value : JSON.stringify(tb.output.value);
53896
- if (!outputText.includes("[AWAITING_APPROVAL]")) return block;
53897
- return { ...tb, output: replacementOutput };
53898
- });
53899
- return { ...toolMsg, content: updatedContent };
54573
+ trace("resume_approved_tool_executed", { toolName: req.toolName });
54574
+ } else {
54575
+ const reason = req.notes ?? "no reason provided";
54576
+ replacementOutput = toResultOutput(
54577
+ `[REJECTED] Human reviewer rejected this action. Reason: ${reason}. Adapt your approach.`
54578
+ );
54579
+ trace("resume_rejected_tool_marker_replaced", { toolName: req.toolName });
54580
+ }
54581
+ msgs = msgs.map((msg) => {
54582
+ if (typeof msg !== "object" || msg === null || msg.role !== "tool") {
54583
+ return msg;
54584
+ }
54585
+ const toolMsg = msg;
54586
+ if (!Array.isArray(toolMsg.content)) return msg;
54587
+ const updatedContent = toolMsg.content.map((block) => {
54588
+ if (typeof block !== "object" || block === null || block.type !== "tool-result") {
54589
+ return block;
54590
+ }
54591
+ const tb = block;
54592
+ if (tb.toolName !== req.toolName) return block;
54593
+ const outputText = tb.output.type === "text" ? tb.output.value : JSON.stringify(tb.output.value);
54594
+ if (!outputText.includes("[AWAITING_APPROVAL]")) return block;
54595
+ return { ...tb, output: replacementOutput };
53900
54596
  });
53901
- await db.update(approvalRequests).set({ executedAt: /* @__PURE__ */ new Date() }).where(eq5(approvalRequests.id, req.id));
53902
- }
53903
- if (catastrophicRefusalMessage !== null) {
54597
+ return { ...toolMsg, content: updatedContent };
54598
+ });
54599
+ await db.update(approvalRequests).set({ executedAt: /* @__PURE__ */ new Date() }).where(eq5(approvalRequests.id, req.id));
54600
+ }
54601
+ return { messages: msgs, catastrophicRefusalMessage };
54602
+ };
54603
+ {
54604
+ const pendingExecRows = await db.select().from(approvalRequests).where(and4(eq5(approvalRequests.jobId, jobId), isNull(approvalRequests.executedAt))).orderBy(approvalRequests.requestedAt);
54605
+ const resolvedRows = pendingExecRows.filter(
54606
+ (r) => r.status === "approved" || r.status === "rejected"
54607
+ );
54608
+ if (resolvedRows.length > 0) {
54609
+ const executed = await executeResolvedApprovals(resolvedRows, messages);
54610
+ messages = executed.messages;
54611
+ if (executed.catastrophicRefusalMessage !== null) {
53904
54612
  trace("resume_catastrophic_command_failed_job");
53905
54613
  await failJob(
53906
54614
  db,
@@ -53908,12 +54616,12 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53908
54616
  "catastrophic_command_refused",
53909
54617
  runStats(),
53910
54618
  messages,
53911
- catastrophicRefusalMessage
54619
+ executed.catastrophicRefusalMessage
53912
54620
  );
53913
54621
  return {
53914
54622
  status: "failed",
53915
54623
  error: "catastrophic_command_refused",
53916
- result: catastrophicRefusalMessage
54624
+ result: executed.catastrophicRefusalMessage
53917
54625
  };
53918
54626
  }
53919
54627
  await saveCheckpoint(db, jobId, {
@@ -53942,10 +54650,67 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53942
54650
  const MAX_TELEGRAM_REDELIVERY_NUDGES = 2;
53943
54651
  let telegramRedeliveryNudges = 0;
53944
54652
  let telegramDelivered = false;
54653
+ let deliveredViaToolName = null;
53945
54654
  const deliveryNudge = "[syst\xE8me] Tu es sur Telegram. Tu n'as pas encore livr\xE9 ta r\xE9ponse \xE0 l'utilisateur. Appelle `telegram_send_message` avec ta r\xE9ponse, PUIS `return_result`. Ne r\xE9ponds pas en texte simple \u2014 sur Telegram, seul un message envoy\xE9 via `telegram_send_message` est visible par l'utilisateur.";
54655
+ const withDeliveryNotice = (resultText) => {
54656
+ if (!job.parentJobId || !deliveredViaToolName) return resultText;
54657
+ const notice = `[livraison effectu\xE9e : ${deliveredViaToolName}${job.chatId ? ` \u2192 chat ${job.chatId}` : ""}]`;
54658
+ return resultText ? `${resultText}
54659
+
54660
+ ${notice}` : notice;
54661
+ };
53946
54662
  let approvalPending = false;
53947
54663
  const approvalNudge = "[syst\xE8me] Tu es sur Telegram et une de tes actions vient de cr\xE9er une demande d'approbation : elle attend la validation de l'utilisateur avant de s'ex\xE9cuter. Avant que le job se mette en pause, appelle `telegram_send_message` pour dire \xE0 l'utilisateur, avec tes propres mots, quelle action tu as lanc\xE9e et que tu attends son approbation (il pourra valider directement depuis Telegram via les boutons \u2705/\u274C, ou depuis le dashboard). N'appelle PAS `return_result` \u2014 la mise en pause est automatique.";
54664
+ const approvalGraceMsRaw = runnerEnv?.NODALAI_APPROVAL_GRACE_MS ?? Number(process.env["NODALAI_APPROVAL_GRACE_MS"]);
54665
+ const approvalGraceMs = Number.isFinite(approvalGraceMsRaw) && approvalGraceMsRaw >= 0 ? approvalGraceMsRaw : 12e4;
53948
54666
  const suspendForApproval = async () => {
54667
+ if (approvalGraceMs > 0) {
54668
+ const pollIntervalMs = Math.max(1, Math.min(2e3, Math.floor(approvalGraceMs / 4)));
54669
+ const deadline = Date.now() + approvalGraceMs;
54670
+ const graceHbInterval = setInterval(() => {
54671
+ void touchJob(db, jobId).catch(() => {
54672
+ });
54673
+ }, 6e4);
54674
+ try {
54675
+ while (Date.now() < deadline) {
54676
+ await new Promise((resolve3) => setTimeout(resolve3, pollIntervalMs));
54677
+ const [statusRow] = await db.select({ status: agentJobs.status }).from(agentJobs).where(eq5(agentJobs.id, jobId));
54678
+ if (statusRow?.status === "cancelled") {
54679
+ trace("grace_window_cancelled");
54680
+ await cancelJob(db, jobId, runStats(), messages);
54681
+ return { status: "cancelled" };
54682
+ }
54683
+ const openRows = await db.select().from(approvalRequests).where(
54684
+ and4(eq5(approvalRequests.jobId, jobId), isNull(approvalRequests.executedAt))
54685
+ ).orderBy(approvalRequests.requestedAt);
54686
+ if (openRows.length === 0 || openRows.some((r) => r.status === "pending")) {
54687
+ continue;
54688
+ }
54689
+ trace("grace_window_resolved_inline", { count: openRows.length });
54690
+ const executed = await executeResolvedApprovals(openRows, messages);
54691
+ messages = executed.messages;
54692
+ if (executed.catastrophicRefusalMessage !== null) {
54693
+ trace("resume_catastrophic_command_failed_job");
54694
+ await failJob(
54695
+ db,
54696
+ jobId,
54697
+ "catastrophic_command_refused",
54698
+ runStats(),
54699
+ messages,
54700
+ executed.catastrophicRefusalMessage
54701
+ );
54702
+ return {
54703
+ status: "failed",
54704
+ error: "catastrophic_command_refused",
54705
+ result: executed.catastrophicRefusalMessage
54706
+ };
54707
+ }
54708
+ return "resumed_inline";
54709
+ }
54710
+ } finally {
54711
+ clearInterval(graceHbInterval);
54712
+ }
54713
+ }
53949
54714
  await saveCheckpoint(db, jobId, {
53950
54715
  messages,
53951
54716
  turn,
@@ -53958,6 +54723,7 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53958
54723
  servedProvider
53959
54724
  });
53960
54725
  await setJobStatus(db, jobId, "awaiting_approval");
54726
+ await reviveJobIfApprovalResolvedDuringSuspend(db, jobId, runnerEnv);
53961
54727
  return { status: "awaiting_approval" };
53962
54728
  };
53963
54729
  const maxTotalTokensPerJob = (() => {
@@ -53971,7 +54737,11 @@ async function runJob(jobId, deps, runnerEnv, opts) {
53971
54737
  return Number.isFinite(n) && n > 0 ? n : DEFAULT_LIMITS.maxCostPerJobUsd;
53972
54738
  })();
53973
54739
  const compactionThreshold = (() => {
53974
- const ctxWindow = modelContextWindow(llmClient.config.provider, llmClient.config.model);
54740
+ const ctxWindow = modelContextWindow(
54741
+ llmClient.config.provider,
54742
+ llmClient.config.model,
54743
+ llmClient.config.contextWindow
54744
+ );
53975
54745
  const fracRaw = Number(process.env["LLM_COMPACTION_FRACTION"]);
53976
54746
  const frac = Number.isFinite(fracRaw) && fracRaw > 0 && fracRaw < 1 ? fracRaw : 0.7;
53977
54747
  const absRaw = Number(process.env["LLM_COMPACTION_TOKENS"]);
@@ -54167,7 +54937,7 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54167
54937
  },
54168
54938
  ...servedProvider ? { servedProvider } : {}
54169
54939
  });
54170
- const isDeliveryOnlyTurn = rawToolCalls.length > 0 && rawToolCalls.every((tc) => DELIVERY_TOOL_NAMES.has(tc.toolName));
54940
+ const isDeliveryOnlyTurn = rawToolCalls.length > 0 && rawToolCalls.every((tc) => DELIVERY_TOOL_NAMES2.has(tc.toolName));
54171
54941
  consecutiveDeliveryOnlyTurns = isDeliveryOnlyTurn ? consecutiveDeliveryOnlyTurns + 1 : 0;
54172
54942
  if (consecutiveDeliveryOnlyTurns > DEFAULT_LIMITS.maxConsecutiveDeliveryTurns) {
54173
54943
  trace("delivery_spam_guard", { turn, consecutiveDeliveryOnlyTurns });
@@ -54197,7 +54967,12 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54197
54967
  if (rawToolCalls.length === 0) {
54198
54968
  trace("no_tool_calls_branch", { turn, hasText: Boolean(response.text) });
54199
54969
  if (approvalPending) {
54200
- return await suspendForApproval();
54970
+ const suspended = await suspendForApproval();
54971
+ if (suspended === "resumed_inline") {
54972
+ approvalPending = false;
54973
+ continue;
54974
+ }
54975
+ return suspended;
54201
54976
  }
54202
54977
  const textContent = response.text ?? "";
54203
54978
  if (textContent) {
@@ -54240,7 +55015,7 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54240
55015
  runnerEnv
54241
55016
  ).catch((e) => console.warn("[reflection]", e));
54242
55017
  }
54243
- return { status: "completed", result: textContent };
55018
+ return { status: "completed", result: withDeliveryNotice(textContent) };
54244
55019
  }
54245
55020
  if (emptyTurnRetries < MAX_EMPTY_TURN_RETRIES) {
54246
55021
  emptyTurnRetries += 1;
@@ -54286,6 +55061,7 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54286
55061
  entityId: job.entityId ?? "",
54287
55062
  db,
54288
55063
  jobChatId: job.chatId ?? null,
55064
+ resolvedTelegramBotToken: inheritedRootBotToken ?? void 0,
54289
55065
  embeddingClient: deps.embeddingClient,
54290
55066
  workspaces: agentWorkspacesList,
54291
55067
  skillStoreDir: skillStore,
@@ -54451,6 +55227,7 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54451
55227
  entityId: job.entityId ?? "",
54452
55228
  db,
54453
55229
  jobChatId: job.chatId ?? null,
55230
+ resolvedTelegramBotToken: inheritedRootBotToken ?? void 0,
54454
55231
  embeddingClient: deps.embeddingClient,
54455
55232
  workspaces: agentWorkspacesList,
54456
55233
  skillStoreDir: skillStore,
@@ -54493,7 +55270,11 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54493
55270
  parentJobId: job.parentJobId ?? null,
54494
55271
  task: job.task,
54495
55272
  channel: job.channel,
54496
- chatId: job.chatId
55273
+ chatId: job.chatId,
55274
+ // Lot E (conversations) : le child délégué hérite du fil de
55275
+ // conversation du parent — sans ça, la page Jobs rattacherait
55276
+ // le sous-job à aucune conversation.
55277
+ conversationId: job.conversationId ?? null
54497
55278
  };
54498
55279
  const preAssignSideResults = toolResultBlocks.map((b) => ({
54499
55280
  type: "tool_result",
@@ -54567,8 +55348,9 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54567
55348
  awaitingApproval = true;
54568
55349
  continue;
54569
55350
  }
54570
- if (toolResult.outcome === "success" && DELIVERY_TOOL_NAMES.has(call.name)) {
55351
+ if (toolResult.outcome === "success" && DELIVERY_TOOL_NAMES2.has(call.name)) {
54571
55352
  telegramDelivered = true;
55353
+ deliveredViaToolName = call.name;
54572
55354
  }
54573
55355
  if (toolResult.outcome === "success") {
54574
55356
  unresolvedToolFailures.delete(call.name);
@@ -54605,7 +55387,13 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54605
55387
  messages = [...messages, { role: "user", content: approvalNudge }];
54606
55388
  continue;
54607
55389
  }
54608
- return await suspendForApproval();
55390
+ const suspended = await suspendForApproval();
55391
+ if (suspended === "resumed_inline") {
55392
+ awaitingApproval = false;
55393
+ approvalPending = false;
55394
+ continue;
55395
+ }
55396
+ return suspended;
54609
55397
  }
54610
55398
  const isToolErrorBlock = (block) => {
54611
55399
  if (block.toolName === "return_result") return false;
@@ -54799,7 +55587,7 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54799
55587
  trace("exit_completed_via_return_result", {
54800
55588
  propagatedResultLen: propagatedResult.length
54801
55589
  });
54802
- return { status: "completed", result: propagatedResult };
55590
+ return { status: "completed", result: withDeliveryNotice(propagatedResult) };
54803
55591
  }
54804
55592
  if (toolResultBlocks.length > 0) {
54805
55593
  messages = [...messages, { role: "tool", content: toolResultBlocks }];
@@ -54914,6 +55702,16 @@ async function runJob(jobId, deps, runnerEnv, opts) {
54914
55702
  );
54915
55703
  return { status: "failed", error: `message_structure_invalid:${err.code}` };
54916
55704
  }
55705
+ if (isContextOverflowError(err)) {
55706
+ const win = modelContextWindow(
55707
+ llmClient.config.provider,
55708
+ llmClient.config.model,
55709
+ llmClient.config.contextWindow
55710
+ );
55711
+ const code = `context_window_exceeded:${llmClient.config.model} (configured ~${win} tokens \u2014 set the model's real context window in LLM providers)`;
55712
+ await failJob(db, jobId, code, runStats(), messages);
55713
+ return { status: "failed", error: code };
55714
+ }
54917
55715
  if (err instanceof Error) {
54918
55716
  const unavailableMatch = err.message.match(
54919
55717
  /Model tried to call unavailable tool ['"`]([^'"`]+)['"`]/i
@@ -55018,6 +55816,15 @@ async function resolveApprovalDecision(deps, runnerEnv, input) {
55018
55816
  const resumed = await deps.db.update(agentJobs).set({ status: "pending", updatedAt: /* @__PURE__ */ new Date() }).where(and4(eq5(agentJobs.id, jobId), eq5(agentJobs.status, "awaiting_approval"))).returning({ id: agentJobs.id });
55019
55817
  if (resumed.length === 0) {
55020
55818
  const [current] = await deps.db.select({ status: agentJobs.status }).from(agentJobs).where(eq5(agentJobs.id, jobId)).limit(1);
55819
+ if (current?.status === "processing" || current?.status === "pending") {
55820
+ return {
55821
+ ok: true,
55822
+ jobId,
55823
+ decision: input.decision,
55824
+ chatId: job.chatId ?? null,
55825
+ resumed: "in_process"
55826
+ };
55827
+ }
55021
55828
  return { ok: false, code: "job_not_resumable", status: current?.status ?? null };
55022
55829
  }
55023
55830
  void triggerWorker(jobId, runnerEnv);
@@ -55025,7 +55832,8 @@ async function resolveApprovalDecision(deps, runnerEnv, input) {
55025
55832
  ok: true,
55026
55833
  jobId,
55027
55834
  decision: input.decision,
55028
- chatId: job.chatId ?? null
55835
+ chatId: job.chatId ?? null,
55836
+ resumed: "worker"
55029
55837
  };
55030
55838
  }
55031
55839
 
@@ -55055,7 +55863,8 @@ async function approveRoute(c, deps, runnerEnv) {
55055
55863
  if (result.code === "job_not_found") return c.json({ error: "job_not_found" }, 404);
55056
55864
  return c.json({ error: "approval_already_resolved", status: result.status }, 400);
55057
55865
  }
55058
- return c.json({ ok: true, jobId: result.jobId, status: "pending", decision }, 200);
55866
+ const status = result.resumed === "in_process" ? "processing" : "pending";
55867
+ return c.json({ ok: true, jobId: result.jobId, status, decision }, 200);
55059
55868
  }
55060
55869
 
55061
55870
  // src/cron/reset-orphans.ts
@@ -55347,13 +56156,19 @@ async function executeReadyTasks(db, deps, max2 = 5) {
55347
56156
  const runMemory = task.rootJobId ? await loadRunMemory(db, task.rootJobId, task.id, explicitDepIds) : "";
55348
56157
  const taskText = buildTaskText(task, runMemory);
55349
56158
  let childDepth = 0;
56159
+ let childConversationId = null;
55350
56160
  if (task.rootJobId) {
55351
- const [creatorRow] = await db.select({ delegationDepth: agentJobs.delegationDepth, status: agentJobs.status }).from(agentJobs).where(eq5(agentJobs.id, task.rootJobId)).limit(1);
56161
+ const [creatorRow] = await db.select({
56162
+ delegationDepth: agentJobs.delegationDepth,
56163
+ status: agentJobs.status,
56164
+ conversationId: agentJobs.conversationId
56165
+ }).from(agentJobs).where(eq5(agentJobs.id, task.rootJobId)).limit(1);
55352
56166
  if (creatorRow && CANCELLED_OR_TERMINAL.has(creatorRow.status ?? "")) {
55353
56167
  await db.update(agentTasks).set({ status: "cancelled", result: "root job cancelled", updatedAt: /* @__PURE__ */ new Date() }).where(eq5(agentTasks.id, task.id));
55354
56168
  continue;
55355
56169
  }
55356
56170
  childDepth = (creatorRow?.delegationDepth ?? 0) + 1;
56171
+ childConversationId = creatorRow?.conversationId ?? null;
55357
56172
  }
55358
56173
  const jobRows = await db.insert(agentJobs).values({
55359
56174
  entityId: task.entityId,
@@ -55362,6 +56177,7 @@ async function executeReadyTasks(db, deps, max2 = 5) {
55362
56177
  task: taskText,
55363
56178
  parentJobId: task.rootJobId ?? void 0,
55364
56179
  delegationDepth: childDepth,
56180
+ conversationId: childConversationId ?? void 0,
55365
56181
  status: "pending",
55366
56182
  messages: [{ role: "user", content: taskText }]
55367
56183
  }).returning({ id: agentJobs.id });
@@ -55592,8 +56408,15 @@ async function runScheduleTick(db, deps, max2 = 5) {
55592
56408
  }
55593
56409
 
55594
56410
  // src/cron/deliver-results.ts
56411
+ var TERMINAL_ROOT_STATUSES = ["cancelled", "completed", "failed"];
55595
56412
  async function findUndeliveredRootJobIds(db) {
55596
- const rows = await db.selectDistinct({ rootJobId: agentTasks.rootJobId }).from(agentTasks).innerJoin(agentJobs, eq5(agentJobs.id, agentTasks.rootJobId)).where(and4(isNotNull(agentTasks.rootJobId), isNull(agentJobs.completedAt)));
56413
+ const rows = await db.selectDistinct({ rootJobId: agentTasks.rootJobId }).from(agentTasks).innerJoin(agentJobs, eq5(agentJobs.id, agentTasks.rootJobId)).where(
56414
+ and4(
56415
+ isNotNull(agentTasks.rootJobId),
56416
+ isNull(agentJobs.completedAt),
56417
+ notInArray(agentJobs.status, TERMINAL_ROOT_STATUSES)
56418
+ )
56419
+ );
55597
56420
  return rows.map((r) => r.rootJobId);
55598
56421
  }
55599
56422
  async function deliverCompletedRoots(db) {
@@ -55614,6 +56437,7 @@ async function deliverCompletedRoots(db) {
55614
56437
  const rootJob = rootJobRows[0];
55615
56438
  if (!rootJob) continue;
55616
56439
  if (rootJob.completedAt !== null) continue;
56440
+ if (TERMINAL_ROOT_STATUSES.includes(rootJob.status ?? "")) continue;
55617
56441
  const complete = await checkRootJobComplete(rootJobId, db);
55618
56442
  if (!complete) continue;
55619
56443
  const taskRows = await db.select({
@@ -55636,8 +56460,10 @@ async function deliverCompletedRoots(db) {
55636
56460
  }).where(
55637
56461
  and4(
55638
56462
  eq5(agentJobs.id, rootJobId),
55639
- isNull(agentJobs.completedAt)
56463
+ isNull(agentJobs.completedAt),
55640
56464
  // gate: only one tick wins
56465
+ notInArray(agentJobs.status, TERMINAL_ROOT_STATUSES)
56466
+ // gate: never resurrect a cancelled/terminal root
55641
56467
  )
55642
56468
  ).returning({ id: agentJobs.id });
55643
56469
  if (claimed.length === 0) {
@@ -56366,10 +57192,10 @@ async function runCronTickGuarded(deps, maxTasksPerTick = 5, maxTickMs = 30 * 6e
56366
57192
  }).finally(() => {
56367
57193
  if (watchdogFired) outstandingAbandoned--;
56368
57194
  });
56369
- const watchdog = new Promise((resolve2) => {
57195
+ const watchdog = new Promise((resolve3) => {
56370
57196
  const timer = setTimeout(() => {
56371
57197
  watchdogFired = true;
56372
- resolve2("timeout");
57198
+ resolve3("timeout");
56373
57199
  }, maxTickMs);
56374
57200
  if (typeof timer.unref === "function") timer.unref();
56375
57201
  });
@@ -56593,6 +57419,10 @@ ${message}` : instruction;
56593
57419
  status: "pending",
56594
57420
  channel: "dashboard",
56595
57421
  task: instruction,
57422
+ // Jobs page grouping (migration 0059): this channel already has a
57423
+ // real conversation entity (the dashboard sidebar thread) — stamp
57424
+ // that id directly rather than re-deriving it from a gap heuristic.
57425
+ conversationId,
56596
57426
  messages: [{ role: "user", content: workerContent }]
56597
57427
  }).returning({ id: agentJobs.id });
56598
57428
  const reply = text23;
@@ -56788,6 +57618,32 @@ function startCronTicker(deps, opts = {}) {
56788
57618
  import { writeFile as writeFile6, mkdir as mkdir5, readdir as readdir8, stat as stat9, unlink as unlink4 } from "node:fs/promises";
56789
57619
  import { join as join11 } from "node:path";
56790
57620
  import { homedir as homedir4 } from "node:os";
57621
+
57622
+ // src/job/conversation-id.ts
57623
+ import { randomUUID as randomUUID3 } from "node:crypto";
57624
+ async function resolveConversationId(opts) {
57625
+ const [prior] = await opts.db.select({
57626
+ conversationId: agentJobs.conversationId,
57627
+ createdAt: agentJobs.createdAt,
57628
+ completedAt: agentJobs.completedAt
57629
+ }).from(agentJobs).where(
57630
+ and4(
57631
+ eq5(agentJobs.entityId, opts.entityId),
57632
+ eq5(agentJobs.agentId, opts.agentId),
57633
+ eq5(agentJobs.channel, opts.channel),
57634
+ eq5(agentJobs.chatId, opts.chatId)
57635
+ )
57636
+ ).orderBy(desc(agentJobs.createdAt)).limit(1);
57637
+ if (prior?.conversationId) {
57638
+ const deliveredAt = prior.completedAt ? new Date(prior.completedAt).getTime() : new Date(prior.createdAt).getTime();
57639
+ if (Date.now() - deliveredAt < IDLE_RESET_MS) {
57640
+ return prior.conversationId;
57641
+ }
57642
+ }
57643
+ return randomUUID3();
57644
+ }
57645
+
57646
+ // src/telegram/handler.ts
56791
57647
  async function handleTelegramUpdate(args) {
56792
57648
  const { update, receivingAgentId, receivingAgentEntityId, receivingAgentBotUsername, tx } = args;
56793
57649
  const message = update.message;
@@ -56889,12 +57745,20 @@ async function handleTelegramUpdate(args) {
56889
57745
  if (!taskText.trim() && hasPhoto) {
56890
57746
  taskText = "Image envoy\xE9e (sans l\xE9gende).";
56891
57747
  }
57748
+ const conversationId = await resolveConversationId({
57749
+ db: tx,
57750
+ entityId: receivingAgentEntityId,
57751
+ agentId: targetAgentId,
57752
+ channel: "telegram",
57753
+ chatId: String(chatId)
57754
+ });
56892
57755
  const [job] = await tx.insert(agentJobs).values({
56893
57756
  entityId: receivingAgentEntityId,
56894
57757
  agentId: targetAgentId,
56895
57758
  channel: "telegram",
56896
57759
  task: taskText,
56897
57760
  chatId: String(chatId),
57761
+ conversationId,
56898
57762
  status: "pending",
56899
57763
  // Text-only at insert; when there's a photo the poller upgrades this to a
56900
57764
  // multimodal [text + image] message after downloading the file.
@@ -57442,15 +58306,15 @@ async function sendAuthConfirmation(botToken, pending) {
57442
58306
  }
57443
58307
  function sleepWithAbort(ms, signal) {
57444
58308
  if (signal.aborted) return Promise.resolve();
57445
- return new Promise((resolve2) => {
58309
+ return new Promise((resolve3) => {
57446
58310
  const timer = setTimeout(() => {
57447
58311
  signal.removeEventListener("abort", onAbort);
57448
- resolve2();
58312
+ resolve3();
57449
58313
  }, ms);
57450
58314
  const onAbort = () => {
57451
58315
  clearTimeout(timer);
57452
58316
  signal.removeEventListener("abort", onAbort);
57453
- resolve2();
58317
+ resolve3();
57454
58318
  };
57455
58319
  signal.addEventListener("abort", onAbort, { once: true });
57456
58320
  });