@gcunharodrigues/wrxn 0.18.0 → 0.18.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcunharodrigues/wrxn",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "WRXN Kernel — installable AI operating system. Two profiles (project | workspace), pull-based updates, managed/seeded/state file classes.",
5
5
  "bin": {
6
6
  "wrxn": "bin/wrxn.cjs"
@@ -15,21 +15,38 @@ const path = require('path');
15
15
  // secretScan — replicated here because each self-contained install-only module imports no shared
16
16
  // kernel-lib (node stdlib only), exactly as it is duplicated across dream.cjs / sync.cjs. CASE-SENSITIVE:
17
17
  // the token shapes are case-specific. A coalesced sidecar must never harden a credential onto disk.
18
- const SECRET_PATTERNS = [
19
- /AKIA[0-9A-Z]{16}/, // AWS access key id
20
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
21
- /npm_[A-Za-z0-9]{36}/, // npm automation token
22
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
23
- /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // FULL PEM block (BEGIN…END) — MUST precede the header-only line so redaction consumes the body, not just the header
24
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header (fallback: a lone/truncated header with no END)
25
- /Bearer\s+[A-Za-z0-9\-._~+/]{16,}=*/i, // HTTP bearer / Authorization token (case-insensitive scheme)
18
+ // The CANONICAL secret-shape set (#39) — kept BYTE-IDENTICAL across every copy (dream / sync / harvest /
19
+ // memory-synth + this one), drift-pinned by adapter-drift-guard.test.cjs. Each self-contained module
20
+ // replicates the set (the install-only adapters import no shared module — node stdlib only), so the pin is
21
+ // what keeps the copies honest. CASE-SENSITIVE except where a shape carries its own /i flag.
22
+ const SECRET_PATTERNS_CANON = [
23
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
24
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
25
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
26
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
27
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
28
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
29
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
30
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
31
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
32
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
33
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
34
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
35
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
36
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
37
+ ];
38
+ // sidecar-LOCAL extras (#38) the canonical set does not subsume — broader / hooks-layer-specific, so they
39
+ // are NOT part of the drift-pinned core. Kept so sidecar never weakens (no pre-existing match is lost).
40
+ const SIDECAR_EXTRA = [
41
+ /Bearer\s+[A-Za-z0-9\-._~+/]{16,}=*/i, // case-insensitive bearer scheme, 16-char floor (broader than the canonical Bearer)
26
42
  // password=/pwd= assignment (=, :, quoted JSON/YAML). The keyword is EITHER fully quoted ("password":)
27
43
  // OR bare (password=) — never a lone trailing quote, so a path ending in "passwd" used as a JSON key
28
44
  // (e.g. "../../etc/passwd": …) is NOT misread as an assignment (it would falsely refuse a sidecar write).
29
45
  /(?:["'](?:password|passwd|pwd)["']|(?:password|passwd|pwd))\s*[:=]\s*["']?[^\s"',;)&}{]+/i,
30
- /[a-z][a-z0-9+.\-]+:\/\/[^\s:/@]+:[^\s/@]+@\S+/i, // URI connection string with inline creds (scheme://user:pass@host)
31
- /eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/, // JWT: base64url header(eyJ…).payload.signature
46
+ /[a-z][a-z0-9+.\-]{1,19}:\/\/[^\s:/@]+:[^\s/@]+@\S+/i, // URI connection string with inline creds (scheme://user:pass@host); scheme bounded {1,19} so a long letter-run fails fast — no O(n²) backtrack (#66)
47
+ /eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/, // JWT (eyJ header; shorter min than the canonical JWT)
32
48
  ];
49
+ const SECRET_PATTERNS = [...SECRET_PATTERNS_CANON, ...SIDECAR_EXTRA];
33
50
 
34
51
  function secretScan(text) {
35
52
  const s = String(text || ''); // NOT lowercased — the token shapes are case-sensitive.
@@ -65,6 +65,27 @@ function pageBody(text) {
65
65
  return end < 0 ? '' : src.slice(end + 4);
66
66
  }
67
67
 
68
+ // Fenced code blocks (``` or ~~~) hold ILLUSTRATIVE text — a [[slug]] written there as example syntax is
69
+ // not a navigable wikilink, so it must not be checked for a dead target (#28). Strip whole fenced regions
70
+ // before the scan. Line-based + anchored (`^\s{0,3}` … `{3,}`) so it stays linear — no backtracking,
71
+ // mirroring wikilinkTargets' own ReDoS discipline. A fence closes only on the SAME marker char with no
72
+ // trailing info; an unclosed fence runs to end-of-body (CommonMark), so its content stays stripped.
73
+ function stripFencedCode(body) {
74
+ const kept = [];
75
+ let fence = ''; // '`' or '~' while inside a fenced block; '' when outside
76
+ for (const line of String(body || '').split('\n')) {
77
+ const m = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
78
+ if (!fence) {
79
+ if (m) fence = m[1][0]; // open: drop the fence line, enter the block
80
+ else kept.push(line);
81
+ } else if (m && m[1][0] === fence && line.slice(m[0].length).trim() === '') {
82
+ fence = ''; // close: drop the fence line, leave the block
83
+ }
84
+ // a line inside a block (including the fences) is dropped
85
+ }
86
+ return kept.join('\n');
87
+ }
88
+
68
89
  // Wikilinks are written `[[slug]]` (Obsidian style), where slug equals a target page's `name:`. An
69
90
  // optional `|alias` or `#anchor` is stripped to the bare target slug. Returns the distinct slugs.
70
91
  // The inner class excludes BOTH `]` and `[`: a real `[[slug]]` target never contains `[`, and the
@@ -74,7 +95,8 @@ function wikilinkTargets(body) {
74
95
  const out = new Set();
75
96
  const re = /\[\[([^\]\[]+)\]\]/g;
76
97
  let m;
77
- while ((m = re.exec(String(body || '')))) {
98
+ const scannable = stripFencedCode(body); // code-block [[slug]] is illustrative, not navigable (#28)
99
+ while ((m = re.exec(scannable))) {
78
100
  const slug = m[1].split('|')[0].split('#')[0].trim();
79
101
  if (slug) out.add(slug);
80
102
  }
@@ -421,11 +421,20 @@ function negativeFilter(text) {
421
421
  // token shapes are case-specific). Evidence quotes are audit-only (never written to a page) so they
422
422
  // stay out of scope here, like the negative filters.
423
423
  const SECRET_PATTERNS = [
424
- /AKIA[0-9A-Z]{16}/, // AWS access key id
425
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
426
- /npm_[A-Za-z0-9]{36}/, // npm automation token
427
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
428
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header
424
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
425
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
426
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
427
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
428
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
429
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
430
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
431
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
432
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
433
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
434
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
435
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
436
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
437
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
429
438
  ];
430
439
 
431
440
  function secretScan(text) {
@@ -653,11 +653,20 @@ function isKebab(s) {
653
653
  // A merged survivor must never harden a session secret into recalled prose. Same patterns + case-sensitive
654
654
  // scope as dream/sync — replicated because each install-only adapter is self-contained (node stdlib only).
655
655
  const SECRET_PATTERNS = [
656
- /AKIA[0-9A-Z]{16}/, // AWS access key id
657
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
658
- /npm_[A-Za-z0-9]{36}/, // npm automation token
659
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
660
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header
656
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
657
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
658
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
659
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
660
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
661
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
662
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
663
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
664
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
665
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
666
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
667
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
668
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
669
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
661
670
  ];
662
671
 
663
672
  function secretScan(text) {
@@ -187,6 +187,51 @@ const THINK_MAX = 600;
187
187
  const TOOL_USE_MAX = 300;
188
188
  const TOOL_RESULT_MAX = 200;
189
189
 
190
+ // Hook-injected framework-context sentinels. SessionStart emits <wrxn-orientation> (which embeds the
191
+ // ENTIRE prior baton as the resume surface); UserPromptSubmit emits <synapse-rules>/<recall-surface>/
192
+ // <reference-candidate> each turn; <system-reminder> wraps harness notes. All land verbatim in the
193
+ // transcript. Left in, they out-mass a light session's real turns → the synth re-emits the embedded
194
+ // prior baton (fresh `wrote`, frozen content) — the content-channel rot of #62. Strip them BEFORE
195
+ // chunking. The blob builder is the ONE shared surface, so the claude and gemini paths both benefit.
196
+ const INJECTED_SENTINELS = ['wrxn-orientation', 'synapse-rules', 'recall-surface', 'reference-candidate', 'system-reminder'];
197
+ // ReDoS bound (#62): the phase-1 closed-block strip is ~quadratic on a pathological part (many opens, no
198
+ // closes). A real injected block (orientation embeds a ≤400-word baton) is a few KB; this cap sits far
199
+ // above that yet below the multi-second regex range, so the strip stays effectively linear.
200
+ const INJECTED_STRIP_MAX = 65536;
201
+
202
+ /**
203
+ * Strip hook-injected framework-context blocks from one transcript text part BEFORE chunking. Two phases:
204
+ * 1. every well-delimited `<tag>…</tag>` block, anywhere (multi-line via [\s\S]; nested/sequential safe);
205
+ * 2. a part-LEADING unclosed `<tag>…` (transcript truncated mid-block) → stripped to end-of-part.
206
+ * The phase-2 strip is ANCHORED to part-leading (`^\s*` — after optional whitespace, incl. the newline a
207
+ * phase-1 removal leaves) because real framework injections are always part-leading. A sentinel merely
208
+ * MENTIONED mid-prose is therefore NOT a leading match, so its trailing text survives (Finding A). PURE
209
+ * and FAIL-OPEN: any fault returns the input unchanged — a filter fault must NEVER break the blob. Node
210
+ * stdlib only. Mirrors the redactSecrets scrub discipline. (#62)
211
+ * @param {string} text
212
+ * @returns {string}
213
+ */
214
+ function stripInjectedContext(text) {
215
+ try {
216
+ let out = String(text || '');
217
+ // ReDoS guard (Finding B): skip a part far larger than any real injected block — it is ordinary content
218
+ // (a pasted file/log) with nothing part-leading to strip, so returning it as-is can never hang.
219
+ if (out.length > INJECTED_STRIP_MAX) return out;
220
+ // Phase 1 — remove every well-delimited block anywhere (low risk; sequential + nested blocks collapse).
221
+ for (const tag of INJECTED_SENTINELS) {
222
+ out = out.replace(new RegExp(`<${tag}>[\\s\\S]*?</${tag}>`, 'g'), '');
223
+ }
224
+ // Phase 2 — remove a part-LEADING unclosed remainder to end-of-part (after phase 1, any leading injected
225
+ // block left dangling by truncation is part-leading). Anchored so a mid-prose mention keeps its tail.
226
+ for (const tag of INJECTED_SENTINELS) {
227
+ out = out.replace(new RegExp(`^\\s*<${tag}>[\\s\\S]*$`), '');
228
+ }
229
+ return out;
230
+ } catch {
231
+ return String(text || ''); // a filter fault never breaks the blob
232
+ }
233
+ }
234
+
190
235
  /**
191
236
  * Build a bounded plain-text blob from a Claude Code transcript (JSONL string).
192
237
  * @param {string} jsonlText the raw transcript file contents
@@ -208,12 +253,12 @@ function buildTranscriptBlob(jsonlText) {
208
253
  const c = m.content;
209
254
  const parts = [];
210
255
  if (typeof c === 'string') {
211
- parts.push(c);
256
+ parts.push(stripInjectedContext(c)); // a SessionStart-injected orientation often arrives as string content
212
257
  } else if (Array.isArray(c)) {
213
258
  for (const p of c) {
214
259
  if (!p || typeof p !== 'object') continue;
215
260
  if (p.type === 'text') {
216
- parts.push(p.text || '');
261
+ parts.push(stripInjectedContext(p.text || '')); // UserPromptSubmit sentinels prepend the user's text part
217
262
  } else if (p.type === 'thinking') {
218
263
  parts.push('[thinking] ' + String(p.thinking || '').slice(0, THINK_MAX));
219
264
  } else if (p.type === 'tool_use') {
@@ -627,22 +672,31 @@ function stripPreamble(text) {
627
672
  // becomes the durable baton. Pattern-based (high-signal vendor token shapes, JWTs incl. Bearer
628
673
  // payloads, and `KEY/TOKEN/SECRET/PASSWORD=value` assignments); each match → `[REDACTED]`. Conservative
629
674
  // by design: it never rewrites ordinary prose, only well-known credential shapes.
630
- const REDACTIONS = [
631
- /\bAKIA[0-9A-Z]{16}\b/g, // AWS access key id
632
- /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, // GitHub PAT / OAuth / refresh / server tokens
633
- /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, // Slack tokens
634
- /\bsk-[A-Za-z0-9]{20,}\b/g, // OpenAI-style secret keys
635
- /\bAIza[0-9A-Za-z._-]{10,}\b/g, // Google / Gemini API keys
636
- /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/g, // JWTs (incl. Bearer payloads): the discriminating `eyJ…` header gates it
637
- /\bnpm_[A-Za-z0-9]{20,}\b/g, // npm publish / automation tokens (≥20 covers the 36-char granular form + variable-length CI tokens)
638
- /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, // GitHub fine-grained PATs
639
- /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g, // Stripe live/test secret keys
640
- /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g, // OpenAI project-scoped keys (underscore form not caught by sk-…)
641
- /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/g, // PEM private-key blocks
642
- /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/g, // opaque Bearer tokens (non-JWT)
643
- /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/gi, // KEY/TOKEN/SECRET = value
675
+ // The ONE canonical secret-shape set — drift-pinned byte-identical to the dream / sync / harvest copies
676
+ // by adapter-drift-guard.test.cjs. Non-global base: redaction derives a g-flagged clone below. (Dropping
677
+ // the older inline \b boundaries is deliberate and fail-safe a redactor erring broader never under-scrubs.)
678
+ const SECRET_PATTERNS = [
679
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
680
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
681
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
682
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
683
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
684
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
685
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
686
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
687
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
688
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
689
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
690
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
691
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
692
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
644
693
  ];
645
694
 
695
+ // Redaction form: every shape made global so EVERY occurrence on a line is scrubbed (not just the first).
696
+ // Each clone PRESERVES its own flags (e.g. the /i assignment shape) and only ADDS g — so detection and
697
+ // redaction never diverge. String#replace resets a global regex's lastIndex per call → reuse is safe.
698
+ const REDACTIONS = SECRET_PATTERNS.map((re) => new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g'));
699
+
646
700
  /**
647
701
  * Redact common secret shapes from `text`, replacing each match with `[REDACTED]`. PURE. Ordinary prose
648
702
  * is preserved verbatim — only credential-looking substrings are scrubbed.
@@ -969,7 +1023,7 @@ async function run(args, { invoke = defaultInvoke, out = process.stdout, err = p
969
1023
  return 2;
970
1024
  }
971
1025
  const root = rootArg || findInstallRoot(path.dirname(path.resolve(file))) || process.cwd();
972
- const blob = readTranscriptBlob(file);
1026
+ const blob = redactSecrets(readTranscriptBlob(file)); // scrub BEFORE the blob egresses to the external model — parity with runHandoff:729 / runDream:866 (#63).
973
1027
  const config = loadConfig(root);
974
1028
  const apiKey = loadEnv(root).GEMINI_API_KEY;
975
1029
  const text = await synthesize({ task, prompt, blob, config, apiKey, invoke });
@@ -264,11 +264,20 @@ async function driftFromDoor(root, { transport, timeoutMs } = {}) {
264
264
  // no shared kernel-lib import), exactly as findInstallRoot/flag/fail are duplicated across these files.
265
265
  // CASE-SENSITIVE: the token shapes are case-specific.
266
266
  const SECRET_PATTERNS = [
267
- /AKIA[0-9A-Z]{16}/, // AWS access key id
268
- /gh[pousr]_[A-Za-z0-9]{36}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_)
269
- /npm_[A-Za-z0-9]{36}/, // npm automation token
270
- /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
271
- /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private-key header
267
+ /AKIA[0-9A-Z]{16}/, // AWS access key id
268
+ /gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token (ghp_/gho_/ghu_/ghs_/ghr_); {20,} covers the 36-char + CI forms
269
+ /github_pat_[A-Za-z0-9_]{22,}/, // GitHub fine-grained PAT
270
+ /xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack token
271
+ /sk-[A-Za-z0-9]{20,}/, // OpenAI-style secret key
272
+ /sk-proj-[A-Za-z0-9_-]{20,}/, // OpenAI project-scoped key (underscore form sk-… misses)
273
+ /AIza[0-9A-Za-z._-]{10,}/, // Google / Gemini API key
274
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/, // Stripe live/test secret key
275
+ /npm_[A-Za-z0-9]{20,}/, // npm publish / automation token
276
+ /\bey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{3,}\.[A-Za-z0-9_-]{3,}\b/, // JWT (incl. Bearer payloads); the eyJ… header gates it
277
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/, // PEM block (FULL — must precede the header fallback so redaction eats the body)
278
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM header (fallback: a lone/truncated header with no END)
279
+ /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/, // opaque Bearer token (non-JWT)
280
+ /\b[A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD)\b\s*[:=]\s*\S+/i, // KEY/TOKEN/SECRET/PASSWORD = value
272
281
  ];
273
282
 
274
283
  function secretScan(text) {