@lmzhen/dsh-evolution-core 0.26.26 → 0.27.1

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 (107) hide show
  1. package/README.md +16 -1
  2. package/lib/composition/index.js +3 -0
  3. package/lib/composition-D8Y3-ROd.js +604 -0
  4. package/lib/ensure-MEYMj9jn.js +222 -0
  5. package/lib/errors-Br2-uUSK.js +57 -0
  6. package/lib/http-routes-BP-S3KV-.js +149 -0
  7. package/lib/http-routes.js +2 -0
  8. package/lib/index.js +66 -850
  9. package/lib/restrictor.js +37 -0
  10. package/lib/subagent/index.js +186 -0
  11. package/lib/types/citations.js +192 -0
  12. package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +1 -1
  13. package/lib/types/composition/composer.js +681 -0
  14. package/lib/types/composition/ensure.d.ts +10 -0
  15. package/lib/types/composition/ensure.js +95 -0
  16. package/lib/types/composition/index.d.ts +5 -0
  17. package/lib/types/composition/index.js +7 -0
  18. package/lib/types/composition/preset-ids.d.ts +17 -0
  19. package/lib/types/composition/preset-ids.js +21 -0
  20. package/lib/types/composition/restricted-preset.d.ts +36 -0
  21. package/lib/types/composition/restricted-preset.js +42 -0
  22. package/lib/types/constants.js +223 -0
  23. package/lib/types/cost.js +67 -0
  24. package/lib/types/curator.js +346 -0
  25. package/lib/types/diagnostics.d.ts +26 -0
  26. package/lib/types/diagnostics.js +23 -0
  27. package/lib/types/document-body.js +91 -0
  28. package/lib/types/drift-signals.js +319 -0
  29. package/lib/types/env.js +25 -0
  30. package/lib/types/errors.d.ts +4 -1
  31. package/lib/types/errors.js +59 -0
  32. package/lib/types/events.js +16 -0
  33. package/lib/types/evidence.js +65 -0
  34. package/lib/types/evolution-events.js +479 -0
  35. package/lib/types/fold-untrusted.js +44 -0
  36. package/lib/types/frontmatter.js +623 -0
  37. package/lib/types/fuzzy-match.js +135 -0
  38. package/lib/types/gates.js +51 -0
  39. package/lib/types/guards.js +27 -0
  40. package/lib/types/health-baseline.js +63 -0
  41. package/lib/types/http-routes.d.ts +67 -35
  42. package/lib/types/http-routes.js +135 -0
  43. package/lib/types/index.d.ts +7 -2
  44. package/lib/types/index.js +101 -0
  45. package/lib/types/instance-scope.js +69 -0
  46. package/lib/types/io.js +675 -0
  47. package/lib/types/learn-prompt.js +42 -0
  48. package/lib/types/limits.js +63 -0
  49. package/lib/types/live.js +107 -0
  50. package/lib/types/long-runs.js +96 -0
  51. package/lib/types/memory-store.d.ts +0 -16
  52. package/lib/types/memory-store.js +606 -0
  53. package/lib/types/mutations.js +95 -0
  54. package/lib/types/namespace-migration.js +156 -0
  55. package/lib/types/numeric.js +47 -0
  56. package/lib/types/opt-in.js +101 -0
  57. package/lib/types/package-identity.js +47 -0
  58. package/lib/types/params.js +328 -0
  59. package/lib/types/payload.js +36 -0
  60. package/lib/types/platform-line.generated.d.ts +10 -0
  61. package/lib/types/platform-line.generated.js +12 -0
  62. package/lib/types/platform-services.d.ts +2 -2
  63. package/lib/types/platform-services.js +52 -0
  64. package/lib/types/probe.js +122 -0
  65. package/lib/types/prompts.js +409 -0
  66. package/lib/types/quality.js +308 -0
  67. package/lib/types/reachability.js +122 -0
  68. package/lib/types/redact.js +257 -0
  69. package/lib/types/reference-rewrite.js +144 -0
  70. package/lib/types/reports.js +110 -0
  71. package/lib/types/restrictor.d.ts +11 -0
  72. package/lib/types/restrictor.js +48 -0
  73. package/lib/types/review-channel.js +59 -0
  74. package/lib/types/run-registry.js +345 -0
  75. package/lib/types/scope.js +7 -0
  76. package/lib/types/serial.js +17 -0
  77. package/lib/types/session-fold.d.ts +15 -0
  78. package/lib/types/session-fold.js +28 -0
  79. package/lib/types/session-projection.js +295 -0
  80. package/lib/types/signals.js +209 -0
  81. package/lib/types/skill-discovery.js +66 -0
  82. package/lib/types/skill-health.js +134 -0
  83. package/lib/types/skill-history.js +745 -0
  84. package/lib/types/skill-lint.js +112 -0
  85. package/lib/types/skill-reads.js +67 -0
  86. package/lib/types/skill-row-cells.js +123 -0
  87. package/lib/types/skill-store.js +3528 -0
  88. package/lib/types/state-store.js +60 -0
  89. package/lib/types/store-ledger.js +117 -0
  90. package/lib/types/subagent/channel.d.ts +4 -0
  91. package/lib/types/subagent/channel.js +131 -0
  92. package/lib/types/subagent/index.d.ts +4 -0
  93. package/lib/types/subagent/index.js +5 -0
  94. package/lib/types/subagent/readiness.d.ts +6 -0
  95. package/lib/types/subagent/readiness.js +35 -0
  96. package/lib/types/subagent/types.d.ts +58 -0
  97. package/lib/types/subagent/types.js +2 -0
  98. package/lib/types/threats.js +338 -0
  99. package/lib/types/tool-dispatch.d.ts +5 -0
  100. package/lib/types/tool-dispatch.js +535 -0
  101. package/lib/types/tree-write.js +40 -0
  102. package/lib/types/unit-members.js +121 -0
  103. package/lib/types/usage.js +474 -0
  104. package/lib/types/write-authority.js +23 -0
  105. package/lib/types/write-gates.js +344 -0
  106. package/lib/types/write-inventory.js +158 -0
  107. package/package.json +39 -11
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Threat scanning for agent-authored memory and skill content.
3
+ *
4
+ * Ported as a small, dependency-free subset of Hermes Agent's
5
+ * `tools/threat_patterns.py` + hermes-claw `threats.ts`.
6
+ *
7
+ * Policy (P1-1, v19): a finding either BLOCKS the write or only REPORTS.
8
+ * Blocking is reserved for shapes with no legitimate use in stored knowledge
9
+ * (prompt-injection phrasing, credential exfiltration, the invisible-character
10
+ * smuggling core). Typography and presentation characters that are legitimate
11
+ * in ordinary prose — variation selectors (❤️), zero-width non-joiner, soft
12
+ * hyphen from PDF paste, Arabic letter mark, Mongolian vowel separator — are
13
+ * REPORT findings: they stay visible to operators and tests but never reject a
14
+ * write. Blocking them turned every emoji into a security event.
15
+ */
16
+ import { DEFAULT_THREAT_MAX_SCAN_CHARS } from "./constants.js";
17
+ import { clampedNumber } from "./numeric.js";
18
+ const FILLER = String.raw `(?:\w+\s+){0,8}`;
19
+ const PATTERNS = [
20
+ // Prompt injection / rule override.
21
+ { label: 'prompt_injection_ignore', category: 'prompt_injection', scope: 'all', regex: new RegExp(String.raw `ignore\s+${FILLER}(?:previous|above|prior|all)\s+${FILLER}instructions`, 'i') },
22
+ // P2-2 (v18): the FILLER between the quantifier and the noun closes
23
+ // "disregard all previous instructions" / "disregard any prior rules",
24
+ // which the bare form missed while its sibling `prompt_injection_ignore`
25
+ // already had it.
26
+ { label: 'disregard_rules', category: 'prompt_injection', scope: 'all', regex: new RegExp(String.raw `disregard\s+${FILLER}(?:your|all|any)\s+${FILLER}(?:instructions|rules|guidelines)`, 'i') },
27
+ { label: 'system_prompt_override', category: 'prompt_injection', scope: 'all', regex: /system\s+prompt\s+override/i },
28
+ { label: 'bypass_restrictions', category: 'prompt_injection', scope: 'all', regex: /act\s+as\s+(?:if|though)\s+(?:you\s+)?(?:have\s+no|don'?t\s+have)\s+(?:restrictions?|limits?|rules)/i },
29
+ { label: 'new_system_prompt', category: 'prompt_injection', scope: 'strict', regex: new RegExp(String.raw `new\s+${FILLER}system\s+${FILLER}prompt`, 'i') },
30
+ { label: 'forget_everything', category: 'prompt_injection', scope: 'strict', regex: new RegExp(String.raw `forget\s+${FILLER}(?:everything|all)\s+${FILLER}(?:discussed|you\s+know)`, 'i') },
31
+ // Role hijacking / jailbreak-adjacent instructions.
32
+ { label: 'role_hijack', category: 'role_hijacking', scope: 'context', regex: /you\s+are\s+now\s+(?:a|an|the|acting|playing|pretending)/i },
33
+ { label: 'fake_update', category: 'role_hijacking', scope: 'context', regex: new RegExp(String.raw `you\s+have\s+been\s+${FILLER}(?:updated|upgraded|patched)\s+to`, 'i') },
34
+ { label: 'identity_override', category: 'role_hijacking', scope: 'context', regex: /\bname\s+yourself\s+\w+/i },
35
+ { label: 'remove_filters', category: 'role_hijacking', scope: 'context', regex: /(?:respond|answer|reply)\s+without\s+(?:restrictions?|limitations?|filters?|safety)/i },
36
+ // Deception and context exfiltration.
37
+ { label: 'deception_hide', category: 'deception', scope: 'all', regex: new RegExp(String.raw `do\s+not\s+${FILLER}tell\s+${FILLER}the\s+user`, 'i') },
38
+ { label: 'leak_system_prompt', category: 'deception', scope: 'context', regex: new RegExp(String.raw `output\s+${FILLER}(?:system|initial)\s+prompt`, 'i') },
39
+ { label: 'context_exfil', category: 'exfiltration', scope: 'strict', regex: /(?:include|output|print|share)\s+(?:the\s+)?(?:conversation|chat\s+history|previous\s+messages|(?:full|entire)\s+context)/i },
40
+ // v22 (SEC-2): the gap may now cross newlines (`[\s\S]{0,512}?`, lazy for
41
+ // proximity) — the old `[^\n]{0,512}` form let a C2 instruction split its
42
+ // verb and URL across lines ("send the full report\n…\nto https://…"),
43
+ // which no in-scope pattern caught.
44
+ { label: 'send_to_url', category: 'exfiltration', scope: 'strict', regex: /(?:send|post|upload|transmit)\s+[\s\S]{0,512}?\s+(?:to|at)\s+https?:\/\//i },
45
+ // Shell-based secret exfiltration. V8-13 (0.3.47): `\b` word boundaries —
46
+ // the bare `cat`/`curl`/`wget` substrings matched inside `concat`/`scurl`
47
+ // prose (`[^\n]{0,512}` can be empty), locking EVERY write; the C2 table
48
+ // next to this one already uses `\b` — this row now matches that discipline.
49
+ // v22 (SEC-2): same cross-line gap as send_to_url.
50
+ { label: 'exfil_curl', category: 'exfiltration', scope: 'all', regex: /\bcurl\s+[\s\S]{0,512}?\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i },
51
+ { label: 'exfil_wget', category: 'exfiltration', scope: 'all', regex: /\bwget\s+[\s\S]{0,512}?\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i },
52
+ // PLAN S4.3 (2026-09-16), audit P2-14: `.env` keeps its P3-22 `(?!\w)` guard
53
+ // (`.envrc` must not trigger — `r` is a word char) and gains a second guard
54
+ // so the DOCUMENTED template variants pass. Their next char after `.env` is
55
+ // a `.` — which the first lookahead never excluded — so the legitimate
56
+ // debugging read `cat .env.example 查看变量` was hard-refused. PLAN-R2 P2-3
57
+ // (2026-09-16): the suffix's old trailing `\b` only demanded a non-word
58
+ // char, so CONTINUATIONS of the template name inherited the exemption —
59
+ // `.env.example.local` (a real-world conventional name that may hold real
60
+ // values), `.env.sample-x`, `.env.example/…`. The terminator is now
61
+ // `(?=$|[\s"'])`: the example/sample suffix must END the path segment,
62
+ // closed by end-of-line, whitespace or a quote. Per-variant behavior:
63
+ // `cat .env` → blocked (tail is EOL/space: the second
64
+ // guard's `\.` never starts)
65
+ // `cat .env.local` → blocked (`.local` is real per-machine config:
66
+ // no example/sample suffix at a
67
+ // terminator)
68
+ // `cat .env.example` → passes (dotted `example` suffix closed by
69
+ // EOL/space/quote)
70
+ // `cat .env.sample` → passes (dotted `sample` suffix, same rule)
71
+ // `cat .env.local.example` → passes (`local.` middle segment, `example`
72
+ // at a terminator)
73
+ // `cat .env.example.local` → blocked (`example` is not at a terminator
74
+ // and `local` is no example/sample)
75
+ // `cat .env.sample-x` → blocked (`-` continues the segment)
76
+ // `cat .env.example/notes` → blocked (`/` continues into another path)
77
+ // `cat .env.example..` → blocked (a dangling dot is no terminator)
78
+ // `cat .env.exampleX` → blocked (unknown suffix spelling, like
79
+ // `.samples`; `X` is a word char so
80
+ // even the first guard trips)
81
+ // `cat .envrc` → no match (first guard, plus the `\b` on `cat`)
82
+ // (A bare `(?![\w.])` would be WRONG here: it frees `.env.local`, whose next
83
+ // char is a dot — hence the suffix-spelled exemption instead. The `[\w-]`
84
+ // segments cannot eat the trailing dot, so no nested-quantifier blowup.)
85
+ { label: 'read_secrets', category: 'exfiltration', scope: 'all', regex: /\bcat\s+[^\n]{0,512}(?:\.env(?!\w)(?!\.(?:[\w-]+\.)*(?:example|sample)(?=$|[\s"']))|(?:\bcredentials\b)|\.netrc|\.pgpass|\.npmrc|\.pypirc)/i },
86
+ // Persistence / backdoor / harness-config tampering. V9-10 (0.3.51):
87
+ // `\b` word boundaries — the bare `authorized_keys` substring matched inside
88
+ // OTHER underscore-joined words (`unauthorized_keys`-style compounds);
89
+ // real references are `.ssh/authorized_keys` / `~/.ssh/authorized_keys`,
90
+ // where the word sits on its own boundary.
91
+ { label: 'ssh_backdoor', category: 'persistence', scope: 'strict', regex: /\bauthorized_keys\b/i },
92
+ { label: 'agent_config_mod', category: 'persistence', scope: 'strict', regex: /(?:update|modify|edit|write|change|append|add\s+to)\s+(?:AGENTS\.md|CLAUDE\.md|\.cursorrules|\.clinerules)/i },
93
+ { label: 'hermes_env', category: 'persistence', scope: 'strict', regex: /\$?HOME\/\.hermes|~\/\.hermes|\.hermes\/\.env|%USERPROFILE%[\\/]\.hermes/i },
94
+ // C2 / promptware vocabulary.
95
+ { label: 'c2_node_registration', category: 'c2_promptware', scope: 'context', regex: /register\s+(?:as\s+)?a?\s*node/i },
96
+ { label: 'c2_heartbeat', category: 'c2_promptware', scope: 'context', regex: /(?:heartbeats?|beacon|check[\s-]?in)\s+(?:to|with)/i },
97
+ { label: 'c2_task_pull', category: 'c2_promptware', scope: 'context', regex: /pull\s+(?:new\s+)?tasks?/i },
98
+ { label: 'known_c2_framework', category: 'c2_promptware', scope: 'context', regex: /\b(?:cobalt\s*strike|sliver|havoc|mythic|metasploit|brainworm)\b/i },
99
+ // Hardcoded secrets.
100
+ { label: 'hardcoded_secret', category: 'hardcoded_secrets', scope: 'strict', regex: /(?:api[_-]?key|token|secret|password)\s*[=:]\s*["'][a-z0-9+/=_-]{20,}["']/i },
101
+ // P3-22 (v14): a JWT is `base64url.base64url.base64url` — the dot-separated
102
+ // alphabet is outside `hardcoded_secret`'s character class, so an embedded
103
+ // token of that form went unreported. Dedicated pattern instead of widening
104
+ // the generic class (which would raise false positives on dotted identifiers).
105
+ { label: 'jwt_like_secret', category: 'hardcoded_secrets', scope: 'strict', regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/ },
106
+ // v22 (SEC-3): ssh-keygen's DEFAULT format is OPENSSH, and EC/DSA/
107
+ // ENCRYPTED/PGP blocks are equally private material — the RSA-only form
108
+ // let every other header through (redact.ts has no private-key pattern
109
+ // either, so both layers missed it).
110
+ { label: 'private_key_block', category: 'hardcoded_secrets', scope: 'all', regex: /-----BEGIN\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\s+)?PRIVATE\s+KEY(?:\s+BLOCK)?-----/ },
111
+ // v22 (SEC-5): Chinese-language injection / exfiltration. This harness's own
112
+ // conversations and stored memory are predominantly Chinese, and every
113
+ // pattern above is English-only — the classic Chinese jailbreak phrasing
114
+ // ("忽略之前所有指令…不要告诉用户…把对话记录发送到…") scored zero findings.
115
+ // Chinese has no inter-word spaces, so the English FILLER (`\w+\s+`) cannot
116
+ // apply; proximity gaps are tight `[\s\S]{0,N}` bounds instead. Blocking
117
+ // scopes mirror their English siblings.
118
+ { label: 'prompt_injection_ignore_zh', category: 'prompt_injection', scope: 'all', regex: /(?:忽略|无视|抛开)[\s\S]{0,8}(?:之前|以上|上面|先前|以前)?[\s\S]{0,8}(?:所有|全部|任何)?[\s\S]{0,4}(?:指令|规则|设定|约束)/ },
119
+ { label: 'deception_hide_zh', category: 'deception', scope: 'all', regex: /(?:不要|别|勿)(?:告诉|告知|透露)(?:给)?[\s\S]{0,6}(?:用户|任何人|主人|开发者)/ },
120
+ { label: 'system_prompt_leak_zh', category: 'deception', scope: 'context', regex: /(?:泄露|输出|打印|透露|导出)[\s\S]{0,10}系统提示|系统提示[\s\S]{0,10}(?:泄露|透露|发送|导出)/ },
121
+ { label: 'context_exfil_zh', category: 'exfiltration', scope: 'strict', regex: /(?:对话记录|聊天记录|全部上下文|完整上下文)[\s\S]{0,30}(?:发送|上传|传输|外传|泄露)[\s\S]{0,30}(?:到|至|给)/ },
122
+ { label: 'secret_exfil_zh', category: 'exfiltration', scope: 'strict', regex: /(?:密钥|凭据|口令|密码|环境变量)[\s\S]{0,30}(?:发送|上传|传输|外传|泄露)[\s\S]{0,30}(?:到|至)\s*(?:https?:\/\/|[\w.-]+\.(?:com|net|org|io|cn|dev|xyz|ru)\b)/ },
123
+ ];
124
+ // P2-2 (v18) added an invisible/format set; P1-1 (v19) splits it by intent
125
+ // after the first cut rejected ordinary prose and emoji:
126
+ // - BLOCKING: the smuggling core. Zero-width breaks/joiners with no prose
127
+ // use (ZWSP, WORD JOINER, invisible operators, deprecated format
128
+ // controls, BOM), COMBINING GRAPHEME JOINER, and the astral TAG block —
129
+ // note the TAG range must live INSIDE the character class (P2-6: the v18
130
+ // form wrote it after the class, where it matched the literal text).
131
+ // - REPORT: typography/presentation characters that are smuggling vectors
132
+ // only when abused: soft hyphen (PDF paste), Arabic letter mark, Mongolian
133
+ // vowel separator, ZWNJ (Persian/Arabic typography), variation selectors
134
+ // VS1-16 (emoji/text presentation, e.g. ❤️).
135
+ // - ZWJ (U+200D) blocks only OUTSIDE an emoji sequence: 👨‍👩‍👧 is a joined
136
+ // sequence and must pass; a ZWJ between ordinary words must not.
137
+ // Homoglyphs (e.g. Cyrillic look-alikes) remain outside this detector by design.
138
+ const INVISIBLE_CHAR_CLASS = '\\u034f\\u200b\\u2060\\u2061\\u2062\\u2063\\u2064\\u206a-\\u206f\\ufeff\\u{e0000}-\\u{e007f}';
139
+ const ZERO_WIDTH_CHARS = new RegExp(`[${INVISIBLE_CHAR_CLASS}]`, 'u');
140
+ const ZWJ_OUTSIDE_EMOJI = /(?<!\p{Extended_Pictographic})\u200d(?!\p{Extended_Pictographic})/u;
141
+ const TYPOGRAPHY_CHARS = /[\u00ad\u061c\u180e\u200c\ufe00-\ufe0f]/;
142
+ const BIDI_CHARS = /[\u202a-\u202e\u2066-\u2069]/;
143
+ // S0.4 (v37 P0-3): the splitter universe is the FULL format-control set, not the
144
+ // two hand-written groups above. Every Cf/Zl/Zp character (LRM/RLM, LS/PS, the
145
+ // interlinear annotations, the musical/tag ranges, NUL, ...) splits a payload
146
+ // phrase exactly like a zero-width space, so the reconstruction must be able to
147
+ // remove ANY of them — exempting a label may silence its finding, never blind the
148
+ // detector that finding protects. Findings stay as they are (blocking benign RTL
149
+ // typography would be a false-positive regression); the visibility half is the
150
+ // report-level `unicode_format_control` finding below. The explicit code points
151
+ // after the properties are the UNASSIGNED slots inside those same invisible
152
+ // ranges (U+2065, U+FFF0-FFF8, U+E0080-E00FF): a smuggler does not need an
153
+ // assigned character to split a word.
154
+ const FORMAT_CONTROL_CLASS = '\\p{Cf}\\p{Zl}\\p{Zp}\\u0000\\u2065\\ufff0-\\ufff8\\u{e0080}-\\u{e00ff}';
155
+ const FORMAT_CONTROL_TEST = new RegExp(`[${FORMAT_CONTROL_CLASS}]`, 'u');
156
+ /** S0.4 (v37 P0-3): a format control the three finding sets above do NOT report.
157
+ * S1-E3: the ZWJ exemption compares the real character. It used to read
158
+ * `'\\u200d'` — a 6-character literal that never equals a code point — so the
159
+ * branch was dead and every emoji ZWJ sequence (👨‍👩‍👧) raised the report-level
160
+ * `unicode_format_control` finding the class comment above promises it will not. */
161
+ function hasUnreportedFormatControl(text) {
162
+ for (const character of text) {
163
+ if (!FORMAT_CONTROL_TEST.test(character))
164
+ continue;
165
+ if (ZERO_WIDTH_CHARS.test(character) || TYPOGRAPHY_CHARS.test(character) || BIDI_CHARS.test(character))
166
+ continue;
167
+ if (character === '\u200d')
168
+ continue;
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+ const SCOPE_ORDER = { all: 1, context: 2, strict: 3 };
174
+ const NO_SCAN_OPTIONS = {};
175
+ /** Minimum window size for the full-coverage scan (V6-05, 0.3.35). With the
176
+ * proportional half-window step below, the overlap is `ceil(w/2)` — only a
177
+ * window at or above this floor keeps the overlap above the longest pattern
178
+ * span (~530 chars: `curl [^\n]{0,512} ...`), so a match straddling a window
179
+ * boundary is fully inside at least one window (E-12, 0.3.16). The clamp
180
+ * falls back to the default for a smaller caller value instead of risking a
181
+ * blind zone. */
182
+ export const PATTERN_OVERLAP = 4096;
183
+ /**
184
+ * Scan text at `scope`. Patterns are cumulative: `strict` includes all scopes.
185
+ * `options.excludeLabels` removes matching patterns without changing `scope`.
186
+ * `maxScanChars` is the WINDOW SIZE, not a total cap (E-12, 0.3.16): the whole
187
+ * text is always scanned in overlapping windows, so content beyond 65,536
188
+ * characters (skill files may run to 100,000) is no longer a blind zone.
189
+ */
190
+ export function scanThreats(text, scope = 'strict', maxScanChars = DEFAULT_THREAT_MAX_SCAN_CHARS, options = NO_SCAN_OPTIONS) {
191
+ // V4-43 self-defense: a non-finite (NaN/±Infinity) or out-of-domain window
192
+ // size would fold the window loop into an empty first window (or a NaN spin)
193
+ // and make every in-scope pattern blind. Clamp to the default so an invalid
194
+ // caller value still scans; the config layer is the first-line guard, this is
195
+ // depth. V5-27: in-repo callers pass valid/clamped values (the threat
196
+ // package pre-clamps its config-derived value), so behavior is unchanged —
197
+ // but the clamp is the guarantee for any third-party caller, not a promise
198
+ // about call sites. V6-05: the floor is now PATTERN_OVERLAP + 1 — a window
199
+ // below it cannot guarantee full coverage (see PATTERN_OVERLAP).
200
+ const windowSize = clampedNumber(maxScanChars, DEFAULT_THREAT_MAX_SCAN_CHARS, { min: PATTERN_OVERLAP + 1 });
201
+ const findings = [];
202
+ const excluded = new Set(options.excludeLabels ?? []);
203
+ // P3-22 (v14): unicode obfuscation is scope-INDEPENDENT by design — the
204
+ // smuggling core is never legitimate in stored knowledge, so it blocks even
205
+ // under `scope: 'all'`. The exemption surface still applies: a deployment
206
+ // that knows a label is benign can allowlist it like any other.
207
+ // P1-1 (v19): the typography set reports without blocking (see the class
208
+ // comment above); ZWJ is judged by context.
209
+ if ((ZERO_WIDTH_CHARS.test(text) || ZWJ_OUTSIDE_EMOJI.test(text)) && !excluded.has('unicode_zero_width')) {
210
+ findings.push({ label: 'unicode_zero_width', category: 'unicode_obfuscation', scope: 'all' });
211
+ }
212
+ if (TYPOGRAPHY_CHARS.test(text) && !excluded.has('unicode_typography')) {
213
+ findings.push({ label: 'unicode_typography', category: 'unicode_obfuscation', scope: 'all', severity: 'report' });
214
+ }
215
+ if (BIDI_CHARS.test(text) && !excluded.has('unicode_bidi_override')) {
216
+ findings.push({ label: 'unicode_bidi_override', category: 'unicode_obfuscation', scope: 'all' });
217
+ }
218
+ // S0.4 (v37 P0-3): report (never block) the format controls no set above names —
219
+ // the splitter universe already defuses the obfuscation, this only makes the
220
+ // attempt visible in the run report.
221
+ if (!excluded.has('unicode_format_control') && hasUnreportedFormatControl(text)) {
222
+ findings.push({ label: 'unicode_format_control', category: 'unicode_obfuscation', scope: 'all', severity: 'report' });
223
+ }
224
+ const normalized = text.normalize('NFKC');
225
+ // v22 (SEC-1): the report-level typography characters survive NFKC, and
226
+ // every pattern above matches literal words — one ZWNJ inside "ignore all"
227
+ // or one soft hyphen inside "do not tell the user" split every blocking
228
+ // regex while the only finding (unicode_typography, severity:'report') is
229
+ // non-blocking AND store-side consumers read the BLOCK verdict only: a
230
+ // one-character injection bypass. Patterns are scored against TWO
231
+ // reconstructed variants of the text (the original still drives the
232
+ // typography report above, so legitimate typography keeps passing):
233
+ // - SPACE variant: an inter-word splitter (ZWNJ between 忽略 words)
234
+ // becomes a word boundary ("ignore all");
235
+ // - STRIPPED variant: an intra-word splitter (soft hyphen in "n\u00ADot")
236
+ // rejoins the word — a space there would leave "n ot" unmatched.
237
+ const SPACE_SPLITTERS = /[\u00ad\u061c\u180e\u200c\ufe00-\ufe0f]/gu;
238
+ // V27 G-1 (P1): the reconstruction must cover the ZERO-WIDTH class too, not
239
+ // just the typography set. `unicode_zero_width` is the only rule that reports
240
+ // ZWSP/WJ/BOM/TAG, so exempting that single label used to be a complete
241
+ // bypass: the characters produced no finding (the guard was excluded) AND the
242
+ // pattern variants never removed them, so `ig<ZWSP>nore all previous
243
+ // instructions` matched no injection pattern at all. Exempting a label may
244
+ // silence its FINDING; it may never blind the detector that the finding
245
+ // protects. ZWJ rides along (its own report is context-sensitive) because a
246
+ // ZWJ between words splits a pattern just as effectively.
247
+ const OBFUSCATION_SPLITTERS = new RegExp(`[${FORMAT_CONTROL_CLASS}]`, 'gu');
248
+ const patternTexts = [
249
+ normalized.replace(SPACE_SPLITTERS, ' ').replace(OBFUSCATION_SPLITTERS, ' '),
250
+ normalized.replace(SPACE_SPLITTERS, '').replace(OBFUSCATION_SPLITTERS, ''),
251
+ ];
252
+ const windows = [];
253
+ for (const patternText of patternTexts) {
254
+ if (patternText.length <= windowSize) {
255
+ windows.push(patternText);
256
+ }
257
+ else {
258
+ // V6-05 (0.3.35): proportional half-window step. Overlap = `ceil(w/2)`, so
259
+ // every position is covered by ~2 windows and the scan cost is O(n × 2)
260
+ // instead of the O(n × w) cliff the old fixed-overlap step produced for
261
+ // small windows (w ≤ PATTERN_OVERLAP collapsed the step to 1). A wider
262
+ // overlap never drops a match — findings are per label/scope, not per
263
+ // location.
264
+ const step = Math.max(Math.floor(windowSize / 2), 1);
265
+ for (let start = 0; start < patternText.length; start += step) {
266
+ windows.push(patternText.slice(start, start + windowSize));
267
+ }
268
+ }
269
+ }
270
+ const seen = new Set();
271
+ for (const window of windows) {
272
+ for (const pattern of PATTERNS) {
273
+ if (SCOPE_ORDER[pattern.scope] > SCOPE_ORDER[scope])
274
+ continue;
275
+ if (excluded.has(pattern.label))
276
+ continue;
277
+ if (!pattern.regex.test(window))
278
+ continue;
279
+ const key = `${pattern.label}|${pattern.scope}`;
280
+ if (seen.has(key))
281
+ continue;
282
+ seen.add(key);
283
+ findings.push({
284
+ label: pattern.label,
285
+ category: pattern.category,
286
+ scope: pattern.scope,
287
+ });
288
+ }
289
+ }
290
+ return findings;
291
+ }
292
+ /** Blocking policy (P1-1, v19): a finding blocks unless it is explicitly
293
+ * `report`-only. Pattern findings carry no severity and therefore block as
294
+ * before. */
295
+ export function evaluateThreat(text, scope = 'strict', maxScanChars = DEFAULT_THREAT_MAX_SCAN_CHARS, options = NO_SCAN_OPTIONS) {
296
+ const findings = scanThreats(text, scope, maxScanChars, options);
297
+ return { blocked: findings.some(finding => finding.severity !== 'report'), findings };
298
+ }
299
+ /** User-facing block message for memory writes. */
300
+ export function scanMemoryThreats(text, maxScanChars = DEFAULT_THREAT_MAX_SCAN_CHARS, options = NO_SCAN_OPTIONS) {
301
+ const { blocked, findings } = evaluateThreat(text, 'strict', maxScanChars, options);
302
+ if (!blocked)
303
+ return null;
304
+ const pattern = findings.find(f => f.category !== 'unicode_obfuscation');
305
+ // WD2 (0.3.56): every user-facing block carries the exemption surface — a
306
+ // legitimate DevOps/skill phrase must end in a next step, not a dead end.
307
+ if (pattern)
308
+ return `Blocked by security scan (${pattern.label}). Rephrase without instruction-like language.${THREAT_EXEMPTION_HINT}`;
309
+ return `Blocked by security scan: invisible or potentially malicious Unicode detected.${THREAT_EXEMPTION_HINT}`;
310
+ }
311
+ /** User-facing block message for skill content writes. */
312
+ export function scanContentThreats(text, maxScanChars = DEFAULT_THREAT_MAX_SCAN_CHARS, options = NO_SCAN_OPTIONS) {
313
+ const { blocked, findings } = evaluateThreat(text, 'strict', maxScanChars, options);
314
+ if (!blocked)
315
+ return null;
316
+ // OPT-11 (2026-09): name an actual BLOCKING pattern, not `findings[0]` —
317
+ // scanThreats pushes the `severity: 'report'` unicode findings FIRST, so
318
+ // content with a soft hyphen plus a real injection phrase used to be told
319
+ // it was blocked by `unicode_typography`. Exempting that label (the hint's
320
+ // self-heal path) then did nothing — the operator only met the real label
321
+ // on a second attempt. The memory variant already filtered this way.
322
+ const pattern = findings.find(f => f.category !== 'unicode_obfuscation');
323
+ if (pattern)
324
+ return `Blocked by security scan (${pattern.label}). This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
325
+ return `Blocked by security scan: invisible or potentially malicious Unicode detected. This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
326
+ }
327
+ /**
328
+ * WD2 (0.3.56): the shared tail of every user-facing threat block — names the
329
+ * deployable self-heal path so the model (or operator) can allowlist a
330
+ * known-benign label. v20 (B-2) single source: the scan builders embed this
331
+ * constant verbatim; do NOT re-word it per call site (the three former copies
332
+ * — memory inline, content inline, and the dead `THREAT_EXEMPT_HINT` export,
333
+ * whose docblock still claimed a store-side append that memory-store/skill-store
334
+ * had already removed — had drifted apart). The evolution-threat guard channel
335
+ * returns the scan message verbatim, so the hint rides along there too.
336
+ */
337
+ export const THREAT_EXEMPTION_HINT = ' A deployment that needs a specific label can exempt it via threatExemptLabels (see the README env/dial reference).';
338
+ //# sourceMappingURL=threats.js.map
@@ -53,6 +53,11 @@ export declare const NATIVE_RESULT_EVENT = "tool/result";
53
53
  * against these literals itself (rule N11); it compares \`kind\` instead.
54
54
  */
55
55
  export declare const DISPATCH_EVENT_TYPES: readonly string[];
56
+ /**
57
+ * Model-visible session frames: the message types plus the ONE dispatch vocabulary above.
58
+ * Consumers classify through this list instead of naming event literals (rule N11).
59
+ */
60
+ export declare const MODEL_SESSION_EVENT_TYPES: readonly string[];
56
61
  /**
57
62
  * One normalized tool dispatch.
58
63
  *