@desplega.ai/agent-swarm 1.165.0 → 1.166.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/{acp-adapter-w6zws6me.js → acp-adapter-yzfxn8vj.js} +4 -4
  2. package/dist/{actions-jna0b3wm.js → actions-qz3m692w.js} +20 -24
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-j1xbmsqz.js} +4 -4
  4. package/dist/{app-75hm5cfa.js → app-43vrdmv8.js} +6 -7
  5. package/dist/{assistant-yzd51j33.js → assistant-ertffmka.js} +21 -25
  6. package/dist/{boot-reembed-z158xq32.js → boot-reembed-998jtpdg.js} +9 -12
  7. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-s7k7c6v2.js} +10 -12
  8. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  9. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-vvzs1zhk.js} +8 -8
  10. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-2zvmekrp.js} +9 -11
  11. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-s49wbk8t.js} +3 -3
  12. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  13. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-kkbxtw5q.js} +2618 -2600
  14. package/dist/{cli-k6xx3pve.js → cli-0nhdwxd5.js} +4 -4
  15. package/dist/{cli-n9ehc28t.js → cli-0qjcsxgz.js} +4 -4
  16. package/dist/{cli-zn2v3t8v.js → cli-3ddybbbx.js} +1 -1
  17. package/dist/{cli-5anb0ev3.js → cli-3qn4r334.js} +71 -14
  18. package/dist/{cli-1kzfvd1z.js → cli-4r1sjfc4.js} +9 -5
  19. package/dist/{cli-ba66wyhy.js → cli-66zh26sp.js} +6 -6
  20. package/dist/{cli-9xb89xca.js → cli-68r1j4sg.js} +6 -6
  21. package/dist/{cli-xnzrzets.js → cli-8kgvmkak.js} +2 -2
  22. package/dist/{cli-28smfvbn.js → cli-8t229cmr.js} +5 -5
  23. package/dist/{cli-9866bd07.js → cli-aqcey741.js} +1 -1
  24. package/dist/{cli-90v942q1.js → cli-cpfmqz3n.js} +8 -8
  25. package/dist/{cli-50eg13rj.js → cli-df56tqdw.js} +459 -282
  26. package/dist/{cli-8gpbwy09.js → cli-dw2q9f7g.js} +5 -5
  27. package/dist/{cli-0n4fasz0.js → cli-ecknp7p1.js} +15 -15
  28. package/dist/{cli-xb3c6aat.js → cli-f2n17zhp.js} +1 -1
  29. package/dist/{cli-ftry7975.js → cli-feh3dkah.js} +1 -1
  30. package/dist/{cli-amwhs8h2.js → cli-fn697bnt.js} +2 -2
  31. package/dist/{cli-prt9emct.js → cli-gt8azyet.js} +3 -3
  32. package/dist/{cli-59jn5twa.js → cli-h3x613ze.js} +2 -2
  33. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  34. package/dist/cli-j2jbr8af.js +516 -0
  35. package/dist/{cli-wfajtmf7.js → cli-jhm6hzjn.js} +593 -10
  36. package/dist/{cli-xtgtyafv.js → cli-jvj6a0cv.js} +190 -439
  37. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  38. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  39. package/dist/{cli-pjxnr1jn.js → cli-mwegs4af.js} +3 -3
  40. package/dist/{cli-5b2fjjzh.js → cli-p4ztb80h.js} +2 -2
  41. package/dist/{cli-e7t4snpa.js → cli-rggdq8wq.js} +4 -4
  42. package/dist/{cli-bdv39kgj.js → cli-rsy933yz.js} +5 -5
  43. package/dist/{cli-gh5yqzpe.js → cli-v2a8x45d.js} +2 -2
  44. package/dist/{cli-bns5c8k9.js → cli-vzc73wet.js} +43 -47
  45. package/dist/cli-wt7ccy4r.js +2654 -0
  46. package/dist/{cli-j0pdrmwg.js → cli-x7kpr72a.js} +3 -3
  47. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  48. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  49. package/dist/{cli-zvckv750.js → cli-y59h2pk9.js} +2 -2
  50. package/dist/{cli-qdv8kgz7.js → cli-yshaazs1.js} +13884 -3241
  51. package/dist/{cli-nfygw434.js → cli-ysnyhws9.js} +2 -2
  52. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  53. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  54. package/dist/cli.js +24 -20
  55. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-dw1wxqt8.js} +7 -7
  56. package/dist/{codex-login-brkxhhdg.js → codex-login-terbdsqq.js} +2 -2
  57. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-3cgb8y1g.js} +7 -7
  58. package/dist/{commands-2ybfcxkj.js → commands-mhxam6z2.js} +6 -6
  59. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-2qda3st7.js} +3 -3
  60. package/dist/{db-f4ans0jj.js → db-sjks418k.js} +8 -6
  61. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  62. package/dist/{dispatcher-pk182apd.js → dispatcher-1es759rn.js} +18 -22
  63. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-bek4cyw9.js} +3 -3
  64. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  65. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  66. package/dist/{grok-adapter-yt9pab3k.js → grok-adapter-ar9cw9jg.js} +5 -5
  67. package/dist/{handlers-g2wnr4t9.js → handlers-1gkbhktr.js} +21 -25
  68. package/dist/{hook-f7jt9vnm.js → hook-ds2zxq9v.js} +14 -16
  69. package/dist/{hook-bbdpnkb4.js → hook-vxa3tcsy.js} +8 -10
  70. package/dist/{http-w40q4w94.js → http-3h21z9md.js} +247 -164
  71. package/dist/{index-056j5kzn.js → index-9b65j1m5.js} +8 -8
  72. package/dist/{index-9bfssf54.js → index-bn6cy8ps.js} +18 -22
  73. package/dist/{index-cj1r9q83.js → index-dkfxpkmm.js} +19 -23
  74. package/dist/{index-7n70jwbb.js → index-f8xnqrye.js} +19 -21
  75. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  76. package/dist/{index-k8xc083e.js → index-n99fycq7.js} +18 -22
  77. package/dist/{keepalive-1tj10tf4.js → keepalive-3xspa9jw.js} +11 -8
  78. package/dist/{lead-st0bg7v4.js → lead-6qphc124.js} +34 -38
  79. package/dist/{maintenance-nz71zats.js → maintenance-0teem1bb.js} +7 -9
  80. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  81. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-sh9q25xp.js} +10 -7
  82. package/dist/{onboard-kgpr9mbn.js → onboard-54qmnqxe.js} +2 -2
  83. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-wjfa2wvd.js} +2 -2
  84. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ycqzp2a1.js} +2 -2
  85. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-e77e9zst.js} +4 -4
  86. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-atnkngqs.js} +7 -7
  87. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-9pjphh9q.js} +6 -6
  88. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-j78wdksm.js} +6 -6
  89. package/dist/{render-v2-tch4nefj.js → render-v2-pxjs49fx.js} +6 -7
  90. package/dist/{setup-rcm303ns.js → setup-ymej0xtx.js} +2 -2
  91. package/dist/{work-objects-ctg7t70w.js → work-objects-3nh4ymzb.js} +22 -26
  92. package/dist/{worker-5dp0xm55.js → worker-qa9fkz5s.js} +34 -38
  93. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  94. package/openapi.json +5 -2
  95. package/package.json +9 -5
  96. package/src/be/automation-preflight.ts +9 -4
  97. package/src/be/boot-scrub-sweep.ts +302 -0
  98. package/src/be/crypto/key-bootstrap.ts +1 -1
  99. package/src/be/db/approvals.ts +15 -1
  100. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  101. package/src/be/db/config-secrets.ts +31 -0
  102. package/src/be/db/runtime.ts +4 -0
  103. package/src/be/db/script-runs.ts +15 -0
  104. package/src/be/db/tasks/write.ts +1 -1
  105. package/src/be/db-queries/mcp-oauth.ts +5 -0
  106. package/src/be/db-queries/oauth.ts +77 -0
  107. package/src/be/db.ts +162 -59
  108. package/src/be/events.ts +17 -1
  109. package/src/be/memory/index-content.ts +2 -2
  110. package/src/be/memory/providers/sqlite-store.ts +63 -11
  111. package/src/be/memory/types.ts +14 -0
  112. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  113. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  114. package/src/be/scripts/db.ts +28 -4
  115. package/src/be/scripts/source-secrets.ts +37 -0
  116. package/src/be/scrub-json.ts +32 -0
  117. package/src/be/sealed-json.ts +59 -0
  118. package/src/be/secret-registry.ts +131 -0
  119. package/src/be/workflow-replay.ts +77 -0
  120. package/src/http/codex-oauth-device.ts +8 -10
  121. package/src/http/db-query-bounded.ts +3 -0
  122. package/src/http/db-query-shared.ts +107 -15
  123. package/src/http/db-query.ts +10 -2
  124. package/src/http/index.ts +23 -6
  125. package/src/http/script-runs.ts +9 -1
  126. package/src/http/scripts.ts +15 -0
  127. package/src/http/workflows.ts +3 -2
  128. package/src/oauth/mcp-wrapper.ts +1 -1
  129. package/src/providers/acp-adapter.ts +25 -11
  130. package/src/providers/acp-target-catalog.ts +35 -1
  131. package/src/providers/acp-targets.ts +69 -3
  132. package/src/providers/amp-adapter.ts +1 -1
  133. package/src/providers/cursor-adapter.ts +1 -1
  134. package/src/script-workflows/supervisor.ts +11 -2
  135. package/src/scripts-runtime/eval-harness.ts +2 -12
  136. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  137. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  138. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  139. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  140. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  141. package/src/scripts-runtime/executors/registry.ts +2 -0
  142. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  143. package/src/scripts-runtime/script-error-hints.ts +14 -0
  144. package/src/server.ts +3 -1
  145. package/src/tests/acp-adapter.test.ts +127 -2
  146. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  147. package/src/tests/check-text-columns.test.ts +145 -0
  148. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  149. package/src/tests/codex-oauth-device.test.ts +23 -37
  150. package/src/tests/db-query-denylist.test.ts +202 -0
  151. package/src/tests/db-task-read-contract.test.ts +1 -0
  152. package/src/tests/db-task-write-contract.test.ts +1 -0
  153. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  154. package/src/tests/events-scrub.test.ts +88 -0
  155. package/src/tests/fixtures/secret-corpus.ts +320 -0
  156. package/src/tests/script-connections.test.ts +5 -0
  157. package/src/tests/script-executor-conformance.test.ts +3 -0
  158. package/src/tests/script-executor-quickjs.test.ts +442 -0
  159. package/src/tests/script-executor-registry.test.ts +6 -1
  160. package/src/tests/script-runs-http.test.ts +88 -1
  161. package/src/tests/script-runs-scrub.test.ts +275 -0
  162. package/src/tests/scripts-http.test.ts +82 -1
  163. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  164. package/src/tests/sealed-key-guard.test.ts +166 -0
  165. package/src/tests/secret-canary-sweep.test.ts +958 -0
  166. package/src/tests/secret-registry.test.ts +355 -0
  167. package/src/tests/secret-rules.test.ts +230 -0
  168. package/src/tests/secret-scrubber.test.ts +3 -2
  169. package/src/tests/workflow-replay.test.ts +518 -0
  170. package/src/tests/workflow-scrub.test.ts +242 -0
  171. package/src/tools/db-query.ts +1 -1
  172. package/src/tools/inject-learning.ts +1 -1
  173. package/src/tools/workflows/get-workflow-run.ts +3 -2
  174. package/src/types.ts +1 -1
  175. package/src/utils/secret-rules.generated.ts +2055 -0
  176. package/src/utils/secret-scrubber.ts +363 -29
  177. package/src/workflows/engine.ts +39 -4
  178. package/src/workflows/recovery.ts +10 -1
  179. package/src/workflows/resume.ts +62 -3
  180. package/src/workflows/retry-poller.ts +3 -0
  181. package/dist/boot-scrub-logs-246yn076.js +0 -99
  182. package/dist/cli-3a4m1aws.js +0 -572
  183. package/dist/cli-a87b04k8.js +0 -951
  184. package/dist/cli-cvk6yzw5.js +0 -40
  185. package/dist/cli-h1spv8v4.js +0 -427
  186. package/dist/cli-jqt9hrnz.js +0 -168
  187. package/dist/cli-yhmwmv0g.js +0 -9281
  188. package/src/be/boot-scrub-logs.ts +0 -132
@@ -21,6 +21,24 @@
21
21
  * API↔worker DB boundary (scripts/check-db-boundary.sh).
22
22
  */
23
23
 
24
+ import { GITLEAKS_RULES } from "./secret-rules.generated";
25
+
26
+ /**
27
+ * Version of the redaction rules below. Bump it on EVERY rule change (new
28
+ * key, suffix, regex, pass or threshold): the API's boot retro-sweep
29
+ * (src/be/boot-scrub-sweep.ts) keys its done marker on this number and
30
+ * re-scrubs stored rows once per version. v2 = the #1907 rules, swept over
31
+ * session_logs only; v3 = the first version swept across every target table;
32
+ * v4 = the API's secret registry (src/be/secret-registry.ts) registers every
33
+ * stored secret at boot, plus its base64, base64url and URL-encoded forms, and
34
+ * the known-value pass matches them all through one combined regex; v5 = pass
35
+ * 5, the vendored gitleaks rule set, plus hand-written Resend, Google OAuth,
36
+ * Discord webhook, xAI and bare Telegram shapes in pass 2; v6 = pass 5 resumes
37
+ * after the secret, not the match, so adjacent secrets that share a delimiter
38
+ * both redact, and sourcegraph-access-token drops its bare 40-hex branch.
39
+ */
40
+ export const SCRUBBER_RULES_VERSION = 6;
41
+
24
42
  /** Env-var names that are always considered secrets, even without suffix hints. */
25
43
  const SENSITIVE_KEY_EXACT = new Set<string>([
26
44
  "API_KEY",
@@ -452,6 +470,9 @@ function scrubKeyContext(text: string): string {
452
470
  // Leading word boundary that also matches after JSON escape sequences (\n, \t,
453
471
  // \r, etc.) where the trailing char is alphanumeric and defeats standard \b.
454
472
  const TB = String.raw`(?:(?<=\\[nrtbfu0])|(?<!\w))`;
473
+ // Rules whose token class includes `-` end on `(?!\w)`, not `\b`: a token
474
+ // ending in `-` has no word boundary after it, so `\b` backtracks and leaves
475
+ // the last character behind.
455
476
 
456
477
  const TOKEN_REGEXES: ReadonlyArray<{ name: string; re: RegExp }> = [
457
478
  // GitHub fine-grained PATs
@@ -461,34 +482,40 @@ const TOKEN_REGEXES: ReadonlyArray<{ name: string; re: RegExp }> = [
461
482
  // ACP ephemeral session tokens (base62 payload)
462
483
  { name: "acp_session_token", re: new RegExp(String.raw`${TB}aseph_[A-Za-z0-9]{20,}\b`, "g") },
463
484
  // GitLab personal access tokens
464
- { name: "gitlab_pat", re: new RegExp(String.raw`${TB}glpat-[A-Za-z0-9_-]{20,}\b`, "g") },
485
+ { name: "gitlab_pat", re: new RegExp(String.raw`${TB}glpat-[A-Za-z0-9_-]{20,}(?!\w)`, "g") },
465
486
  // Azure DevOps personal access tokens (84 chars, "AZDO" signature at offset 76)
466
487
  {
467
488
  name: "azure_devops_pat",
468
489
  re: new RegExp(String.raw`${TB}[A-Za-z0-9]{76}AZDO[A-Za-z0-9]{4}\b`, "g"),
469
490
  },
470
491
  // Anthropic API keys (must match before the generic sk- rule below)
471
- { name: "anthropic_key", re: new RegExp(String.raw`${TB}sk-ant-[A-Za-z0-9_-]{20,}\b`, "g") },
492
+ { name: "anthropic_key", re: new RegExp(String.raw`${TB}sk-ant-[A-Za-z0-9_-]{20,}(?!\w)`, "g") },
472
493
  // OpenAI project keys
473
- { name: "openai_proj_key", re: new RegExp(String.raw`${TB}sk-proj-[A-Za-z0-9_-]{20,}\b`, "g") },
494
+ {
495
+ name: "openai_proj_key",
496
+ re: new RegExp(String.raw`${TB}sk-proj-[A-Za-z0-9_-]{20,}(?!\w)`, "g"),
497
+ },
474
498
  // OpenRouter keys
475
499
  {
476
500
  name: "openrouter_key",
477
- re: new RegExp(String.raw`${TB}sk-or-(?:v1-)?[A-Za-z0-9_-]{20,}\b`, "g"),
501
+ re: new RegExp(String.raw`${TB}sk-or-(?:v1-)?[A-Za-z0-9_-]{20,}(?!\w)`, "g"),
478
502
  },
479
503
  // Generic sk- legacy OpenAI keys (must come AFTER the ant/proj/or variants)
480
504
  { name: "sk_key", re: new RegExp(String.raw`${TB}sk-[A-Za-z0-9]{20,}\b`, "g") },
481
505
  // Slack tokens
482
- { name: "slack_token", re: new RegExp(String.raw`${TB}xox[baprseo]-[A-Za-z0-9-]{10,}\b`, "g") },
506
+ {
507
+ name: "slack_token",
508
+ re: new RegExp(String.raw`${TB}xox[baprseo]-[A-Za-z0-9-]{10,}(?!\w)`, "g"),
509
+ },
483
510
  // AWS access key IDs
484
511
  { name: "aws_access_key", re: new RegExp(String.raw`${TB}AKIA[0-9A-Z]{16}\b`, "g") },
485
512
  // Google API keys
486
- { name: "google_api_key", re: new RegExp(String.raw`${TB}AIza[A-Za-z0-9_-]{35}\b`, "g") },
513
+ { name: "google_api_key", re: new RegExp(String.raw`${TB}AIza[A-Za-z0-9_-]{35}(?!\w)`, "g") },
487
514
  // JWTs (3 dot-separated base64url segments)
488
515
  {
489
516
  name: "jwt",
490
517
  re: new RegExp(
491
- String.raw`${TB}eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b`,
518
+ String.raw`${TB}eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?!\w)`,
492
519
  "g",
493
520
  ),
494
521
  },
@@ -498,22 +525,202 @@ const TOKEN_REGEXES: ReadonlyArray<{ name: string; re: RegExp }> = [
498
525
  re: new RegExp(String.raw`${TB}signoz-ingestion-key=[A-Za-z0-9._~+/-]{20,}={0,2}\b`, "g"),
499
526
  },
500
527
  // Linear OAuth tokens and API keys
501
- { name: "linear_oauth", re: new RegExp(String.raw`${TB}lin_oauth_[A-Za-z0-9_-]{10,}\b`, "g") },
502
- { name: "linear_api", re: new RegExp(String.raw`${TB}lin_api_[A-Za-z0-9_-]{10,}\b`, "g") },
528
+ {
529
+ name: "linear_oauth",
530
+ re: new RegExp(String.raw`${TB}lin_oauth_[A-Za-z0-9_-]{10,}(?!\w)`, "g"),
531
+ },
532
+ { name: "linear_api", re: new RegExp(String.raw`${TB}lin_api_[A-Za-z0-9_-]{10,}(?!\w)`, "g") },
503
533
  // npm tokens
504
- { name: "npm_token", re: new RegExp(String.raw`${TB}npm_[A-Za-z0-9_-]{20,}\b`, "g") },
534
+ { name: "npm_token", re: new RegExp(String.raw`${TB}npm_[A-Za-z0-9_-]{20,}(?!\w)`, "g") },
505
535
  // Jira API tokens (Atlassian cloud)
506
536
  {
507
537
  name: "atlassian_token",
508
- re: new RegExp(String.raw`${TB}ATATT[A-Za-z0-9_-]{20,}\b`, "g"),
538
+ re: new RegExp(String.raw`${TB}ATATT[A-Za-z0-9_=-]{20,}(?![\w=])`, "g"),
509
539
  },
510
540
  // Agent-swarm MCP user tokens (`aswt_<base62-20+>`). Schema lands in
511
541
  // migration 064; mint/revoke endpoints ship with the MCP-token plan.
512
542
  // Rule lives here now so plaintexts never leak into logs once endpoints
513
543
  // come online.
514
544
  { name: "mcp_token", re: new RegExp(String.raw`${TB}aswt_[A-Za-z0-9]{20,}\b`, "g") },
545
+ // Vendor shapes the gitleaks rule set (pass 5) has no rule for.
546
+ // Resend API keys: re_<8>_<24>, with a digit and an uppercase letter so
547
+ // snake_case identifiers never match.
548
+ {
549
+ name: "resend_key",
550
+ re: new RegExp(
551
+ String.raw`${TB}re_(?=[A-Za-z0-9_]*[0-9])(?=[A-Za-z0-9_]*[A-Z])[A-Za-z0-9]{8}_[A-Za-z0-9]{24}\b`,
552
+ "g",
553
+ ),
554
+ },
555
+ // Google OAuth access tokens and refresh tokens
556
+ { name: "google_oauth_token", re: new RegExp(String.raw`${TB}ya29\.[A-Za-z0-9_-]{20,}`, "g") },
557
+ { name: "google_refresh_token", re: /(?<![\w/])1\/\/0[A-Za-z0-9_-]{30,}/g },
558
+ // Discord webhook URLs (the token is the last path segment)
559
+ {
560
+ name: "discord_webhook",
561
+ re: /https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]{50,}/g,
562
+ },
563
+ // xAI API keys
564
+ { name: "xai_key", re: new RegExp(String.raw`${TB}xai-[A-Za-z0-9]{40,}\b`, "g") },
565
+ // Telegram bot tokens without a "telegram" keyword nearby: <bot id>:AA<33>
566
+ {
567
+ name: "telegram_bot_token",
568
+ re: new RegExp(String.raw`${TB}\d{8,10}:AA[A-Za-z0-9_-]{33}(?![A-Za-z0-9_-])`, "g"),
569
+ },
515
570
  ];
516
571
 
572
+ /**
573
+ * Pass 5: the vendored gitleaks rule set (src/utils/secret-rules.generated.ts).
574
+ * Compiled on first use. A rule runs only when the text contains one of its
575
+ * keywords, and a match counts only when its secret clears the rule's entropy
576
+ * floor and no allowlist claims it, the same gates gitleaks applies.
577
+ */
578
+ interface CompiledAllowlist {
579
+ target: "secret" | "match";
580
+ res: RegExp[];
581
+ stopwords: string[];
582
+ }
583
+
584
+ interface CompiledGitleaksRule {
585
+ id: string;
586
+ re: RegExp;
587
+ keywords: string[];
588
+ entropy?: number;
589
+ secretGroup?: number;
590
+ redactWholeMatch?: boolean;
591
+ allowlists: CompiledAllowlist[];
592
+ }
593
+
594
+ interface CompiledGitleaks {
595
+ /** Every keyword in one case-insensitive, longest-first alternation. */
596
+ keywordRe: RegExp;
597
+ /** keyword -> every keyword it contains, itself included. */
598
+ implied: Map<string, string[]>;
599
+ rules: CompiledGitleaksRule[];
600
+ global: CompiledAllowlist;
601
+ }
602
+
603
+ let gitleaks: CompiledGitleaks | null = null;
604
+
605
+ function getGitleaks(): CompiledGitleaks {
606
+ if (gitleaks) return gitleaks;
607
+ const compile = (r: { source: string; flags: string }) => new RegExp(r.source, r.flags);
608
+ const rules = GITLEAKS_RULES.rules.map((rule) => ({
609
+ id: rule.id,
610
+ re: new RegExp(rule.source, `${rule.flags}gd`),
611
+ keywords: rule.keywords,
612
+ entropy: rule.entropy,
613
+ secretGroup: rule.secretGroup,
614
+ redactWholeMatch: rule.redactWholeMatch,
615
+ allowlists: (rule.allowlists ?? []).map((list) => ({
616
+ target: list.target,
617
+ res: list.regexes.map(compile),
618
+ stopwords: list.stopwords,
619
+ })),
620
+ }));
621
+ const keywords = [...new Set(rules.flatMap((rule) => rule.keywords))].sort(
622
+ (a, b) => b.length - a.length,
623
+ );
624
+ gitleaks = {
625
+ keywordRe: new RegExp(keywords.map(escapeRegExp).join("|"), "gi"),
626
+ implied: new Map(keywords.map((k) => [k, keywords.filter((other) => k.includes(other))])),
627
+ rules,
628
+ global: {
629
+ target: "secret",
630
+ res: GITLEAKS_RULES.globalAllowlist.regexes.map(compile),
631
+ stopwords: GITLEAKS_RULES.globalAllowlist.stopwords,
632
+ },
633
+ };
634
+ return gitleaks;
635
+ }
636
+
637
+ /** Shannon entropy in bits per character, as gitleaks computes it. */
638
+ function shannonEntropy(value: string): number {
639
+ if (value.length === 0) return 0;
640
+ const counts = new Map<string, number>();
641
+ for (const ch of value) counts.set(ch, (counts.get(ch) ?? 0) + 1);
642
+ let entropy = 0;
643
+ for (const count of counts.values()) {
644
+ const p = count / value.length;
645
+ entropy -= p * Math.log2(p);
646
+ }
647
+ return entropy;
648
+ }
649
+
650
+ function allowedBy(list: CompiledAllowlist, secret: string, match: string): boolean {
651
+ const target = list.target === "match" ? match : secret;
652
+ if (list.res.some((re) => re.test(target))) return true;
653
+ if (list.stopwords.length === 0) return false;
654
+ const lower = secret.toLowerCase();
655
+ return list.stopwords.some((word) => lower.includes(word));
656
+ }
657
+
658
+ function applyGitleaksRule(text: string, rule: CompiledGitleaksRule, global: CompiledAllowlist) {
659
+ const { re } = rule;
660
+ re.lastIndex = 0;
661
+ let out = "";
662
+ let last = 0;
663
+ let changed = false;
664
+ for (let m = re.exec(text); m !== null; m = re.exec(text)) {
665
+ if (m[0].length === 0) {
666
+ re.lastIndex++;
667
+ continue;
668
+ }
669
+ // gitleaks' secret: the configured group, else the first non-empty group,
670
+ // else the whole match. Only the secret is redacted; the context stays.
671
+ let group = 0;
672
+ if (rule.secretGroup && m[rule.secretGroup]) group = rule.secretGroup;
673
+ else if (!rule.redactWholeMatch && !rule.secretGroup) {
674
+ group = m.findIndex((g, i) => i > 0 && !!g);
675
+ if (group === -1) group = 0;
676
+ }
677
+ const secret = m[group] as string;
678
+ const span = m.indices?.[group];
679
+ if (!span) continue;
680
+ // Resume at the end of the secret, not the match. Many rules consume a
681
+ // delimiter on each side, so `a b` shares one space: resuming after it
682
+ // hides the second secret's leading boundary. The next match starts at or
683
+ // after span[1], so redacted spans never overlap; the max keeps progress.
684
+ re.lastIndex = Math.max(span[1], m.index + 1);
685
+ if (rule.entropy && shannonEntropy(secret) <= rule.entropy) continue;
686
+ if (allowedBy(global, secret, m[0])) continue;
687
+ if (rule.allowlists.some((list) => allowedBy(list, secret, m[0]))) continue;
688
+ out += `${text.slice(last, span[0])}[REDACTED:gitleaks:${rule.id}]`;
689
+ last = span[1];
690
+ changed = true;
691
+ }
692
+ return changed ? out + text.slice(last) : text;
693
+ }
694
+
695
+ let gitleaksPassEnabled = true;
696
+
697
+ /** Test-only: switch pass 5 off to measure what it costs. */
698
+ export function setGitleaksPassEnabledForTesting(enabled: boolean): void {
699
+ gitleaksPassEnabled = enabled;
700
+ }
701
+
702
+ function scrubGitleaks(text: string): string {
703
+ if (!gitleaksPassEnabled) return text;
704
+ const { keywordRe, implied, rules, global } = getGitleaks();
705
+ // Visit every start position that has a keyword. The alternation is
706
+ // longest-first, so a shorter keyword starting at the same position is a
707
+ // substring of the one found, and `implied` adds it.
708
+ const present = new Set<string>();
709
+ keywordRe.lastIndex = 0;
710
+ for (let m = keywordRe.exec(text); m !== null; m = keywordRe.exec(text)) {
711
+ for (const keyword of implied.get(m[0].toLowerCase()) ?? []) present.add(keyword);
712
+ keywordRe.lastIndex = m.index + 1;
713
+ }
714
+ if (present.size === 0) return text;
715
+ let out = text;
716
+ for (const rule of rules) {
717
+ if (rule.keywords.some((keyword) => present.has(keyword))) {
718
+ out = applyGitleaksRule(out, rule, global);
719
+ }
720
+ }
721
+ return out;
722
+ }
723
+
517
724
  interface EnvValueEntry {
518
725
  value: string;
519
726
  name: string;
@@ -524,8 +731,28 @@ interface ScrubCache {
524
731
  snapshotKey: string;
525
732
  }
526
733
 
734
+ /**
735
+ * Pass 1 matcher: every known value (env entries + volatile secrets) in one
736
+ * alternation regex, longest value first, so each scrub is a single scan no
737
+ * matter how many values are registered.
738
+ */
739
+ interface KnownValueMatcher {
740
+ /** null when there is nothing to match, or the regex could not be built. */
741
+ re: RegExp | null;
742
+ /** value -> marker name. */
743
+ names: Map<string, string>;
744
+ /** Longest-first values, used only when `re` failed to build. */
745
+ fallback: string[];
746
+ /** Inputs the matcher was built from; a change on either rebuilds it. */
747
+ builtFor: ScrubCache;
748
+ builtAtGeneration: number;
749
+ }
750
+
527
751
  let cache: ScrubCache | null = null;
752
+ let matcher: KnownValueMatcher | null = null;
528
753
  const volatileSecrets = new Map<string, string>();
754
+ /** Bumped whenever `volatileSecrets` changes, so the matcher rebuilds lazily. */
755
+ let volatileGeneration = 0;
529
756
  /** Key names marked secret at runtime (swarm_config rows with isSecret=1). */
530
757
  const registeredSensitiveKeys = new Set<string>();
531
758
 
@@ -613,29 +840,77 @@ function getCache(): ScrubCache {
613
840
  return cache;
614
841
  }
615
842
 
843
+ function escapeRegExp(value: string): string {
844
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
845
+ }
846
+
847
+ function buildMatcher(envCache: ScrubCache): KnownValueMatcher {
848
+ // Volatile names first, env names second: when a value is both, the env
849
+ // name wins, as it did when the env pass ran before the volatile pass.
850
+ const names = new Map<string, string>(volatileSecrets);
851
+ for (const { value, name } of envCache.entries) names.set(value, name);
852
+
853
+ // Longest first: a regex alternation takes the first alternative that
854
+ // matches at a position, so a value that is a prefix of a longer one must
855
+ // come after it or the longer value would be cut in half.
856
+ const values = [...names.keys()].sort((a, b) => b.length - a.length);
857
+ let re: RegExp | null = null;
858
+ if (values.length > 0) {
859
+ try {
860
+ re = new RegExp(values.map(escapeRegExp).join("|"), "g");
861
+ } catch {
862
+ // Pattern too large for the engine: fall back to the per-value loop.
863
+ re = null;
864
+ }
865
+ }
866
+ return {
867
+ re,
868
+ names,
869
+ fallback: re ? [] : values,
870
+ builtFor: envCache,
871
+ builtAtGeneration: volatileGeneration,
872
+ };
873
+ }
874
+
875
+ function getMatcher(): KnownValueMatcher {
876
+ const envCache = getCache();
877
+ if (
878
+ !matcher ||
879
+ matcher.builtFor !== envCache ||
880
+ matcher.builtAtGeneration !== volatileGeneration
881
+ ) {
882
+ matcher = buildMatcher(envCache);
883
+ }
884
+ return matcher;
885
+ }
886
+
887
+ /**
888
+ * A string that has passed through `scrubSecrets`. Still a plain `string` at
889
+ * runtime; the brand lets a DB writer demand scrubbed input so `tsc` rejects a
890
+ * raw string. Only `scrubSecrets` produces it.
891
+ */
892
+ export type ScrubbedText = string & { readonly __scrubbed: true };
893
+
616
894
  /**
617
895
  * Replace known secret values in `text` with `[REDACTED:<name>]` markers.
618
896
  * Null/undefined inputs return an empty string. Empty strings pass through.
619
897
  */
620
- export function scrubSecrets(text: string | null | undefined): string {
621
- if (text == null) return "";
622
- if (text.length === 0) return text;
898
+ export function scrubSecrets(text: string | null | undefined): ScrubbedText {
899
+ if (text == null) return "" as ScrubbedText;
900
+ if (text.length === 0) return text as ScrubbedText;
623
901
 
624
902
  let out = text;
625
903
 
626
- // Pass 1: exact-match env values (preserves the env-var name in the marker
627
- // for debugging).
628
- const { entries } = getCache();
629
- for (const { value, name } of entries) {
630
- if (out.includes(value)) {
631
- // split/join is O(n) and faster than building a RegExp for every value.
632
- out = out.split(value).join(`[REDACTED:${name}]`);
633
- }
634
- }
635
-
636
- for (const [value, name] of volatileSecrets) {
637
- if (out.includes(value)) {
638
- out = out.split(value).join(`[REDACTED:${name}]`);
904
+ // Pass 1: exact-match known values, env first then volatile (registered at
905
+ // runtime, incl. every stored secret the API's secret registry loads). The
906
+ // marker keeps the env-var / source name for debugging.
907
+ const { re, names, fallback } = getMatcher();
908
+ if (re) {
909
+ re.lastIndex = 0;
910
+ out = out.replace(re, (match) => `[REDACTED:${names.get(match) ?? "secret"}]`);
911
+ } else {
912
+ for (const value of fallback) {
913
+ if (out.includes(value)) out = out.split(value).join(`[REDACTED:${names.get(value)}]`);
639
914
  }
640
915
  }
641
916
 
@@ -653,7 +928,60 @@ export function scrubSecrets(text: string | null | undefined): string {
653
928
 
654
929
  // Pass 4: credentials known by their context (JSON/YAML keys, auth headers,
655
930
  // curl flags, URL userinfo, PEM blocks).
656
- return scrubKeyContext(out);
931
+ out = scrubKeyContext(out);
932
+
933
+ // Pass 5: vendor token shapes from the gitleaks rule set, behind a keyword
934
+ // prefilter and each rule's entropy floor.
935
+ return scrubGitleaks(out) as ScrubbedText;
936
+ }
937
+
938
+ /** What `findSecretsIn` found: names and rule ids only, never the values. */
939
+ export interface SecretFindings {
940
+ /** Names of known values (env + registered secrets, every encoded form). */
941
+ knownValues: string[];
942
+ /** Ids of gitleaks rules whose pattern matched. */
943
+ gitleaksRules: string[];
944
+ }
945
+
946
+ /**
947
+ * Detect, without redacting, the two secret classes a write must refuse
948
+ * outright: a known value (pass 1, every form it was registered in) and a
949
+ * gitleaks vendor-token rule (pass 5). The key-context passes are left out on
950
+ * purpose: on code they flag `password: "…"`-shaped literals that are often
951
+ * not secrets.
952
+ */
953
+ export function findSecretsIn(text: string): SecretFindings {
954
+ const knownValues = new Set<string>();
955
+ const gitleaksRules = new Set<string>();
956
+ if (text.length === 0) return { knownValues: [], gitleaksRules: [] };
957
+
958
+ const { re, names, fallback } = getMatcher();
959
+ if (re) {
960
+ re.lastIndex = 0;
961
+ for (let m = re.exec(text); m !== null; m = re.exec(text)) {
962
+ knownValues.add(names.get(m[0]) ?? "secret");
963
+ }
964
+ } else {
965
+ for (const value of fallback) {
966
+ if (text.includes(value)) knownValues.add(names.get(value) ?? "secret");
967
+ }
968
+ }
969
+
970
+ if (gitleaksPassEnabled) {
971
+ const { keywordRe, implied, rules, global } = getGitleaks();
972
+ const present = new Set<string>();
973
+ keywordRe.lastIndex = 0;
974
+ for (let m = keywordRe.exec(text); m !== null; m = keywordRe.exec(text)) {
975
+ for (const keyword of implied.get(m[0].toLowerCase()) ?? []) present.add(keyword);
976
+ keywordRe.lastIndex = m.index + 1;
977
+ }
978
+ for (const rule of rules) {
979
+ if (!rule.keywords.some((keyword) => present.has(keyword))) continue;
980
+ if (applyGitleaksRule(text, rule, global) !== text) gitleaksRules.add(rule.id);
981
+ }
982
+ }
983
+
984
+ return { knownValues: [...knownValues].sort(), gitleaksRules: [...gitleaksRules].sort() };
657
985
  }
658
986
 
659
987
  export function scrubObject<T>(value: T, seen = new WeakSet<object>()): T {
@@ -696,7 +1024,11 @@ export function refreshSecretScrubberCache(): void {
696
1024
  */
697
1025
  export function registerVolatileSecret(value: string, name: string): void {
698
1026
  if (value.length < MIN_VALUE_LENGTH) return;
699
- for (const form of escapedForms(value)) volatileSecrets.set(form, name);
1027
+ for (const form of escapedForms(value)) {
1028
+ if (volatileSecrets.get(form) === name) continue;
1029
+ volatileSecrets.set(form, name);
1030
+ volatileGeneration++;
1031
+ }
700
1032
  }
701
1033
 
702
1034
  /**
@@ -715,6 +1047,8 @@ export function registerSensitiveKeyName(key: string): void {
715
1047
  /** Test-only: drop volatile values and runtime-registered key names. */
716
1048
  export function clearVolatileSecretsForTesting(): void {
717
1049
  volatileSecrets.clear();
1050
+ volatileGeneration++;
1051
+ matcher = null;
718
1052
  registeredSensitiveKeys.clear();
719
1053
  sensitiveKeyVerdicts.clear();
720
1054
  cache = null;
@@ -20,6 +20,7 @@ import {
20
20
  updateWorkflowRun,
21
21
  updateWorkflowRunStep,
22
22
  } from "../be/db";
23
+ import { WorkflowReplayStateError } from "../be/workflow-replay";
23
24
  import { telemetry } from "../telemetry";
24
25
  import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
25
26
  import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
@@ -226,6 +227,32 @@ const activeWalks = new Map<string, number>();
226
227
  // left behind is not mistaken for a live execution.
227
228
  const executingSteps = new Set<string>();
228
229
 
230
+ /**
231
+ * Fail closed when a run's sealed replay state cannot be opened. Resuming from
232
+ * the scrubbed copy would feed redaction markers to downstream nodes, so the
233
+ * run (and `stepId`, when one was claimed) fails with the error instead.
234
+ * Returns false, writing nothing, for any other error.
235
+ */
236
+ export async function failRunOnUnreadableReplay(
237
+ runId: string,
238
+ err: unknown,
239
+ stepId?: string,
240
+ ): Promise<boolean> {
241
+ if (!(err instanceof WorkflowReplayStateError)) return false;
242
+ const finishedAt = new Date().toISOString();
243
+ console.error(`[workflow] ${err.message}`);
244
+ if (stepId) {
245
+ await updateWorkflowRunStep(stepId, {
246
+ status: "failed",
247
+ error: err.message,
248
+ finishedAt,
249
+ nextRetryAt: null,
250
+ });
251
+ }
252
+ await updateWorkflowRun(runId, { status: "failed", error: err.message, finishedAt });
253
+ return true;
254
+ }
255
+
229
256
  export function isWorkflowRunActive(runId: string): boolean {
230
257
  return activeWalks.has(runId);
231
258
  }
@@ -267,6 +294,8 @@ export async function walkGraph(
267
294
  const release = holdWorkflowRun(runId);
268
295
  try {
269
296
  await walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId, secretKeys, options);
297
+ } catch (err) {
298
+ if (!(await failRunOnUnreadableReplay(runId, err))) throw err;
270
299
  } finally {
271
300
  release();
272
301
  }
@@ -374,10 +403,11 @@ async function walkGraphOwned(
374
403
  const results = await Promise.all(
375
404
  pendingNodes.map((node) =>
376
405
  executeStep(def, runId, ctx, node, registry, workflowId, secretKeys, options).catch(
377
- (_err): StepResult => ({
378
- outcome: "failed",
379
- successors: [],
380
- }),
406
+ async (err): Promise<StepResult> => {
407
+ // The run-status check below then stops the walk.
408
+ await failRunOnUnreadableReplay(runId, err);
409
+ return { outcome: "failed", successors: [] };
410
+ },
381
411
  ),
382
412
  ),
383
413
  );
@@ -742,6 +772,11 @@ async function executeStep(
742
772
  workflowId,
743
773
  options,
744
774
  );
775
+ } catch (err) {
776
+ if (await failRunOnUnreadableReplay(runId, err, dedup.stepId)) {
777
+ return { outcome: "failed", successors: [] };
778
+ }
779
+ throw err;
745
780
  } finally {
746
781
  executingSteps.delete(dedup.stepId);
747
782
  }
@@ -16,7 +16,12 @@ import type { WorkflowRunStep } from "../types";
16
16
  import { shapeApprovalResolution } from "./approval-resolution";
17
17
  import { loadCompletedStepRouting } from "./completed-step-routing";
18
18
  import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
19
- import { findReadyNodes, isWorkflowRunActive, walkGraph } from "./engine";
19
+ import {
20
+ failRunOnUnreadableReplay,
21
+ findReadyNodes,
22
+ isWorkflowRunActive,
23
+ walkGraph,
24
+ } from "./engine";
20
25
  import type { ExecutorRegistry } from "./executors/registry";
21
26
  import { getSecretInputKeys } from "./input";
22
27
  import { finalizeOrWait, resumeWaitState } from "./resume";
@@ -113,6 +118,7 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
113
118
  }
114
119
  recovered++;
115
120
  } catch (err) {
121
+ if (await failRunOnUnreadableReplay(runId, err)) continue;
116
122
  console.error(`[workflows] Failed to recover running run ${runId}:`, err);
117
123
  }
118
124
  }
@@ -248,6 +254,7 @@ async function recoverWaitingRuns(registry: ExecutorRegistry): Promise<number> {
248
254
  }
249
255
  recovered++;
250
256
  } catch (err) {
257
+ if (await failRunOnUnreadableReplay(stuck.runId, err)) continue;
251
258
  console.error(`[workflows] Failed to recover waiting run ${stuck.runId}:`, err);
252
259
  }
253
260
  }
@@ -349,6 +356,7 @@ async function recoverApprovalWaitingRuns(registry: ExecutorRegistry): Promise<n
349
356
  }
350
357
  recovered++;
351
358
  } catch (err) {
359
+ if (await failRunOnUnreadableReplay(stuck.runId, err)) continue;
352
360
  console.error(`[workflows] Failed to recover approval-waiting run ${stuck.runId}:`, err);
353
361
  }
354
362
  }
@@ -391,6 +399,7 @@ async function recoverWaitStates(registry: ExecutorRegistry): Promise<number> {
391
399
  await resumeWaitState(stuck.waitId, resumeStatus, payload, registry);
392
400
  recovered++;
393
401
  } catch (err) {
402
+ if (await failRunOnUnreadableReplay(stuck.runId, err)) continue;
394
403
  console.error(`[workflows] Failed to recover wait-state ${stuck.waitId}:`, err);
395
404
  }
396
405
  }