@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.
- package/dist/{acp-adapter-w6zws6me.js → acp-adapter-yzfxn8vj.js} +4 -4
- package/dist/{actions-jna0b3wm.js → actions-qz3m692w.js} +20 -24
- package/dist/{amp-adapter-0eve81pz.js → amp-adapter-j1xbmsqz.js} +4 -4
- package/dist/{app-75hm5cfa.js → app-43vrdmv8.js} +6 -7
- package/dist/{assistant-yzd51j33.js → assistant-ertffmka.js} +21 -25
- package/dist/{boot-reembed-z158xq32.js → boot-reembed-998jtpdg.js} +9 -12
- package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-s7k7c6v2.js} +10 -12
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-vvzs1zhk.js} +8 -8
- package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-2zvmekrp.js} +9 -11
- package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-s49wbk8t.js} +3 -3
- package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-kkbxtw5q.js} +2618 -2600
- package/dist/{cli-k6xx3pve.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-n9ehc28t.js → cli-0qjcsxgz.js} +4 -4
- package/dist/{cli-zn2v3t8v.js → cli-3ddybbbx.js} +1 -1
- package/dist/{cli-5anb0ev3.js → cli-3qn4r334.js} +71 -14
- package/dist/{cli-1kzfvd1z.js → cli-4r1sjfc4.js} +9 -5
- package/dist/{cli-ba66wyhy.js → cli-66zh26sp.js} +6 -6
- package/dist/{cli-9xb89xca.js → cli-68r1j4sg.js} +6 -6
- package/dist/{cli-xnzrzets.js → cli-8kgvmkak.js} +2 -2
- package/dist/{cli-28smfvbn.js → cli-8t229cmr.js} +5 -5
- package/dist/{cli-9866bd07.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-90v942q1.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-50eg13rj.js → cli-df56tqdw.js} +459 -282
- package/dist/{cli-8gpbwy09.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-0n4fasz0.js → cli-ecknp7p1.js} +15 -15
- package/dist/{cli-xb3c6aat.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-ftry7975.js → cli-feh3dkah.js} +1 -1
- package/dist/{cli-amwhs8h2.js → cli-fn697bnt.js} +2 -2
- package/dist/{cli-prt9emct.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-59jn5twa.js → cli-h3x613ze.js} +2 -2
- package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-wfajtmf7.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-xtgtyafv.js → cli-jvj6a0cv.js} +190 -439
- package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-pjxnr1jn.js → cli-mwegs4af.js} +3 -3
- package/dist/{cli-5b2fjjzh.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-e7t4snpa.js → cli-rggdq8wq.js} +4 -4
- package/dist/{cli-bdv39kgj.js → cli-rsy933yz.js} +5 -5
- package/dist/{cli-gh5yqzpe.js → cli-v2a8x45d.js} +2 -2
- package/dist/{cli-bns5c8k9.js → cli-vzc73wet.js} +43 -47
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-j0pdrmwg.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
- package/dist/{cli-zvckv750.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-qdv8kgz7.js → cli-yshaazs1.js} +13884 -3241
- package/dist/{cli-nfygw434.js → cli-ysnyhws9.js} +2 -2
- package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +24 -20
- package/dist/{codex-adapter-3qnxshje.js → codex-adapter-dw1wxqt8.js} +7 -7
- package/dist/{codex-login-brkxhhdg.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-3cgb8y1g.js} +7 -7
- package/dist/{commands-2ybfcxkj.js → commands-mhxam6z2.js} +6 -6
- package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-2qda3st7.js} +3 -3
- package/dist/{db-f4ans0jj.js → db-sjks418k.js} +8 -6
- package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-pk182apd.js → dispatcher-1es759rn.js} +18 -22
- package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-bek4cyw9.js} +3 -3
- package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
- package/dist/{grok-adapter-yt9pab3k.js → grok-adapter-ar9cw9jg.js} +5 -5
- package/dist/{handlers-g2wnr4t9.js → handlers-1gkbhktr.js} +21 -25
- package/dist/{hook-f7jt9vnm.js → hook-ds2zxq9v.js} +14 -16
- package/dist/{hook-bbdpnkb4.js → hook-vxa3tcsy.js} +8 -10
- package/dist/{http-w40q4w94.js → http-3h21z9md.js} +247 -164
- package/dist/{index-056j5kzn.js → index-9b65j1m5.js} +8 -8
- package/dist/{index-9bfssf54.js → index-bn6cy8ps.js} +18 -22
- package/dist/{index-cj1r9q83.js → index-dkfxpkmm.js} +19 -23
- package/dist/{index-7n70jwbb.js → index-f8xnqrye.js} +19 -21
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-k8xc083e.js → index-n99fycq7.js} +18 -22
- package/dist/{keepalive-1tj10tf4.js → keepalive-3xspa9jw.js} +11 -8
- package/dist/{lead-st0bg7v4.js → lead-6qphc124.js} +34 -38
- package/dist/{maintenance-nz71zats.js → maintenance-0teem1bb.js} +7 -9
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-sh9q25xp.js} +10 -7
- package/dist/{onboard-kgpr9mbn.js → onboard-54qmnqxe.js} +2 -2
- package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-wjfa2wvd.js} +2 -2
- package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-e77e9zst.js} +4 -4
- package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-atnkngqs.js} +7 -7
- package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-9pjphh9q.js} +6 -6
- package/dist/{rbac-roles-790j3ga8.js → rbac-roles-j78wdksm.js} +6 -6
- package/dist/{render-v2-tch4nefj.js → render-v2-pxjs49fx.js} +6 -7
- package/dist/{setup-rcm303ns.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-ctg7t70w.js → work-objects-3nh4ymzb.js} +22 -26
- package/dist/{worker-5dp0xm55.js → worker-qa9fkz5s.js} +34 -38
- package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +5 -2
- package/package.json +9 -5
- package/src/be/automation-preflight.ts +9 -4
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/workflow-replay.ts +77 -0
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/index.ts +23 -6
- package/src/http/script-runs.ts +9 -1
- package/src/http/scripts.ts +15 -0
- package/src/http/workflows.ts +3 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/providers/acp-adapter.ts +25 -11
- package/src/providers/acp-target-catalog.ts +35 -1
- package/src/providers/acp-targets.ts +69 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/server.ts +3 -1
- package/src/tests/acp-adapter.test.ts +127 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +88 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +3 -2
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +1 -1
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +363 -29
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/dist/boot-scrub-logs-246yn076.js +0 -99
- package/dist/cli-3a4m1aws.js +0 -572
- package/dist/cli-a87b04k8.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-h1spv8v4.js +0 -427
- package/dist/cli-jqt9hrnz.js +0 -168
- package/dist/cli-yhmwmv0g.js +0 -9281
- 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,}
|
|
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,}
|
|
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
|
-
{
|
|
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,}
|
|
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
|
-
{
|
|
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}
|
|
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,}
|
|
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
|
-
{
|
|
502
|
-
|
|
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,}
|
|
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_
|
|
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):
|
|
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
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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))
|
|
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;
|
package/src/workflows/engine.ts
CHANGED
|
@@ -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
|
-
(
|
|
378
|
-
|
|
379
|
-
|
|
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 {
|
|
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
|
}
|