thumbgate 1.34.3 → 1.37.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 (200) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +373 -5
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +653 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +21 -2
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +10 -1
  34. package/config/model-candidates.json +382 -24
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +401 -147
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/auto-promote-gates.js +178 -27
  89. package/scripts/billing.js +1 -1
  90. package/scripts/broker-execution-receipts.js +719 -0
  91. package/scripts/budget-aware-gates-proof.js +423 -0
  92. package/scripts/claude-feedback-sync.js +29 -3
  93. package/scripts/claw-harness-production.js +237 -0
  94. package/scripts/cli-progress.js +111 -0
  95. package/scripts/cli-schema.js +163 -1
  96. package/scripts/codex-runbook-flywheel.js +318 -0
  97. package/scripts/context-footprint.js +186 -0
  98. package/scripts/contextfs.js +143 -61
  99. package/scripts/dashboard-limits.js +27 -0
  100. package/scripts/dashboard.js +279 -9
  101. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  102. package/scripts/docker-sandbox-planner.js +18 -0
  103. package/scripts/double-blind-eval-protocol.js +252 -0
  104. package/scripts/edotenv-rl-gateway.js +259 -0
  105. package/scripts/ensure-production-search-corpus.js +162 -0
  106. package/scripts/eval-holdout.js +311 -0
  107. package/scripts/feedback-aggregate.js +21 -2
  108. package/scripts/feedback-loop.js +87 -5
  109. package/scripts/feedback-quality.js +9 -0
  110. package/scripts/file-ledger-lock.js +4 -1
  111. package/scripts/financial-control-plane.js +41 -1
  112. package/scripts/find-dormant-requires.js +118 -0
  113. package/scripts/fs-utils.js +84 -8
  114. package/scripts/gate-stats.js +2 -2
  115. package/scripts/gates-engine.js +859 -58
  116. package/scripts/generate-case-study-outreach.js +24 -15
  117. package/scripts/git-at-scale.js +628 -0
  118. package/scripts/governance-conflict-audit.js +1650 -0
  119. package/scripts/governance-difficulty-curriculum.js +328 -0
  120. package/scripts/graphrag-retrieval.js +275 -0
  121. package/scripts/gurobi-optimizer.js +324 -0
  122. package/scripts/gurobi_optimizer.py +485 -0
  123. package/scripts/harness-selector.js +82 -1
  124. package/scripts/hidden-entry-points.js +284 -0
  125. package/scripts/human-escalation.js +199 -1
  126. package/scripts/hybrid-feedback-context.js +152 -19
  127. package/scripts/intent-governed-execution.js +602 -0
  128. package/scripts/intervention-policy.js +123 -20
  129. package/scripts/jit-harness-compose.js +628 -0
  130. package/scripts/jsonl-watcher.js +10 -0
  131. package/scripts/lesson-embedding-index.js +95 -12
  132. package/scripts/lesson-retrieval.js +105 -19
  133. package/scripts/local-model-profile.js +19 -2
  134. package/scripts/mailer/resend-mailer.js +1 -1
  135. package/scripts/matryoshka-embedding.js +235 -0
  136. package/scripts/mcp-oauth.js +42 -4
  137. package/scripts/mcp-session-handles.js +1016 -0
  138. package/scripts/mcp-wiring-doctor.js +314 -0
  139. package/scripts/memory-firewall.js +115 -2
  140. package/scripts/memory-scope-readiness.js +299 -0
  141. package/scripts/memory-vs-rag-route.js +161 -0
  142. package/scripts/model-tier-router.js +148 -21
  143. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  144. package/scripts/openui-catalog-compose-honesty.js +593 -0
  145. package/scripts/operational-integrity.js +19 -1
  146. package/scripts/override-audit.js +213 -0
  147. package/scripts/package-manager-honesty-doctor.js +458 -0
  148. package/scripts/pr-manager.js +63 -1
  149. package/scripts/prove-herdr-adapter.js +52 -0
  150. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  151. package/scripts/prove-workos.js +73 -0
  152. package/scripts/provider-attestation-conformance.js +192 -0
  153. package/scripts/provider-receipt-contract.js +136 -0
  154. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  155. package/scripts/radware-threat-defense.js +280 -0
  156. package/scripts/rag-embedding-identity.js +221 -0
  157. package/scripts/rag-precision-guardrails.js +112 -2
  158. package/scripts/remote-feedback-capture.js +159 -0
  159. package/scripts/research-agent-harness.js +256 -0
  160. package/scripts/rsi-safety-hillclimb.js +200 -0
  161. package/scripts/rule-sprawl.js +188 -0
  162. package/scripts/schedule-manager.js +147 -0
  163. package/scripts/self-heal.js +8 -0
  164. package/scripts/session-lease.js +415 -0
  165. package/scripts/simatree-data-governance.js +347 -0
  166. package/scripts/slo-alert-engine.js +172 -7
  167. package/scripts/solver-parity.js +539 -0
  168. package/scripts/stealth-memory-injection-gate.js +333 -0
  169. package/scripts/switchyard-router.js +366 -0
  170. package/scripts/telemetry-analytics.js +84 -27
  171. package/scripts/temporal-decay-weighting.js +138 -0
  172. package/scripts/test-all.js +165 -0
  173. package/scripts/token-savings.js +42 -0
  174. package/scripts/tool-kpi-tracker.js +108 -5
  175. package/scripts/tool-registry.js +193 -5
  176. package/scripts/universal-claim-evaluator.js +14 -2
  177. package/scripts/vector-store.js +279 -9
  178. package/scripts/workflow-notebook.js +391 -0
  179. package/scripts/workflow-sentinel.js +111 -12
  180. package/scripts/workos-production-guard.js +260 -0
  181. package/scripts/workspace-search-route.js +515 -0
  182. package/server.json +2 -2
  183. package/src/agent-identity-boundary.js +76 -0
  184. package/src/agent-retrieval-cache.js +155 -0
  185. package/src/alert-noise-ledger.js +502 -0
  186. package/src/api/server.js +802 -185
  187. package/src/git-fast-cache.js +220 -0
  188. package/src/git-wal-sync.js +156 -0
  189. package/src/hash-anchored-edit.js +82 -0
  190. package/src/hermes-platform-protocol.js +475 -0
  191. package/src/hermes-sync-plane.js +241 -0
  192. package/src/index.js +30 -1
  193. package/src/iso42001-compliance-guard.js +97 -0
  194. package/src/latency-budget.js +244 -0
  195. package/src/mcp-writeguard.js +316 -0
  196. package/src/miminions-adapter.js +106 -0
  197. package/src/pipeline-compass.js +104 -0
  198. package/src/ppl-alert-pipeline.js +284 -0
  199. package/src/rendezvous-router.js +90 -0
  200. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Stealth memory injection gate (MemGhost / WhisperBench 2026).
6
+ *
7
+ * Paper: "When Claws Remember but Do Not Tell: Stealthy Memory Injection in
8
+ * Persistent Personal Agents" (arXiv/HF 2607.05189).
9
+ *
10
+ * Threat: untrusted external content (email/IMAP/tool results) induces a
11
+ * persistent personal agent to write poisoned facts/preferences into durable
12
+ * memory carriers (MEMORY.md, AGENTS.md, SOUL.md, …) while staying silent in
13
+ * the user-visible response — later sessions then trust that state.
14
+ *
15
+ * ThumbGate role: pre-action deny when a write targets a durable carrier AND
16
+ * the action/content shows external untrusted provenance and/or stealth
17
+ * memory-injection language.
18
+ */
19
+
20
+ const GATE_ID = 'block-stealth-memory-injection-from-external';
21
+ const PAPER_ID = '2607.05189';
22
+ const PAPER_URL = 'https://huggingface.co/papers/2607.05189';
23
+
24
+ /** Omnipresent / high-availability carriers (paper: carrier availability). */
25
+ const DURABLE_CARRIER_PATTERNS = [
26
+ /(?:^|\/)MEMORY\.md$/i,
27
+ /(?:^|\/)AGENTS\.md$/i,
28
+ /(?:^|\/)USER\.md$/i,
29
+ /(?:^|\/)IDENTITY\.md$/i,
30
+ /(?:^|\/)SOUL\.md$/i,
31
+ /(?:^|\/)HEARTBEAT\.md$/i,
32
+ /(?:^|\/)prevention-rules\.md$/i,
33
+ /(?:^|\/)memory\/\d{4}-\d{2}-\d{2}\.md$/i,
34
+ /(?:^|\/)\.thumbgate\/prevention-rules\.md$/i,
35
+ /(?:^|\/)\.claude\/memory\/feedback\/prevention-rules\.md$/i,
36
+ /(?:^|\/)BRAIN\.md$/i,
37
+ ];
38
+
39
+ /** Episodic / retrieval-dependent paths still risky when stealth + external. */
40
+ const EPISODIC_MEMORY_PATTERNS = [
41
+ /(?:^|\/)memory\//i,
42
+ /(?:^|\/)\.claude\/projects\/.*\/memory\//i,
43
+ /(?:^|\/)mem0\//i,
44
+ ];
45
+
46
+ const STEALTH_PATTERNS = [
47
+ /\bsilent(?:ly)?\s+(?:save|store|write|record|remember|inject|add)\b/i,
48
+ /\bdo\s+not\s+(?:tell|say|mention|announce|notify|reveal|disclose)\b/i,
49
+ /\bwithout\s+(?:telling|saying|announcing|notifying|mentioning)\b/i,
50
+ /\bdo\s+not\s+(?:inform|alert)\s+the\s+user\b/i,
51
+ /\bhide\s+(?:this|the)\s+(?:from|update|memory)\b/i,
52
+ /\bstealth(?:y)?\s+memory\b/i,
53
+ /\bmemghost\b/i,
54
+ /\bwhisper\s*bench\b/i,
55
+ /\bnon[- ]?diagnostic\b/i,
56
+ /\bwithout\s+(?:user\s+)?(?:awareness|visibility|confirmation)\b/i,
57
+ ];
58
+
59
+ const EXTERNAL_PROVENANCE_PATTERNS = [
60
+ /\b(?:imap|smtp|email|inbox|mailbox|mail\s+cli)\b/i,
61
+ /\bunread\s+(?:mail|email|messages?)\b/i,
62
+ /\bcheck\s+(?:my\s+)?(?:email|inbox)\b/i,
63
+ /\bfrom\s+(?:an?\s+)?(?:email|message|sender)\b/i,
64
+ /\bexternal\s+(?:content|data|payload|source)\b/i,
65
+ /\buntrusted\s+(?:content|source|email|payload)\b/i,
66
+ /\btool_result\b/i,
67
+ /\bindirect\s+prompt\s+injection\b/i,
68
+ /\bpayload\b/i,
69
+ ];
70
+
71
+ const PREFERENCE_POISON_PATTERNS = [
72
+ /\balways\s+prefer\b/i,
73
+ /\buser\s+(?:prefers?|wants?|insists?)\b/i,
74
+ /\bpermanent(?:ly)?\s+(?:prefer|use|route|send)\b/i,
75
+ /\bupdate\s+(?:my\s+)?preferences?\b/i,
76
+ /\bfrom\s+now\s+on\b/i,
77
+ ];
78
+
79
+ const FACT_POISON_PATTERNS = [
80
+ /\bremember\s+that\b/i,
81
+ /\bfact\s*:\s*/i,
82
+ /\bstore\s+(?:as\s+)?(?:durable\s+)?(?:memory|knowledge|fact)\b/i,
83
+ /\badd\s+to\s+(?:your\s+)?(?:long[- ]?term\s+)?memory\b/i,
84
+ /\bwrite\s+(?:this\s+)?to\s+(?:MEMORY|AGENTS|SOUL|USER)\.md\b/i,
85
+ ];
86
+
87
+ const WRITE_TOOLS = new Set([
88
+ 'Write',
89
+ 'Edit',
90
+ 'write',
91
+ 'edit',
92
+ 'create_file',
93
+ 'str_replace',
94
+ 'StrReplace',
95
+ 'ApplyPatch',
96
+ 'NotebookEdit',
97
+ 'MultiEdit',
98
+ ]);
99
+
100
+ function normalizeText(value) {
101
+ if (value === undefined || value === null) return '';
102
+ return String(value);
103
+ }
104
+
105
+ function collectHaystack(toolName, toolInput = {}) {
106
+ const parts = [
107
+ toolName,
108
+ toolInput.command,
109
+ toolInput.file_path,
110
+ toolInput.path,
111
+ toolInput.filePath,
112
+ toolInput.content,
113
+ toolInput.new_string,
114
+ toolInput.old_string,
115
+ toolInput.contents,
116
+ toolInput.notes,
117
+ toolInput.context,
118
+ toolInput.prompt,
119
+ toolInput.description,
120
+ ];
121
+ if (toolInput._claw && typeof toolInput._claw === 'object') {
122
+ parts.push(JSON.stringify(toolInput._claw));
123
+ }
124
+ if (toolInput.clawContext && typeof toolInput.clawContext === 'object') {
125
+ parts.push(JSON.stringify(toolInput.clawContext));
126
+ }
127
+ if (toolInput._externalProvenance) {
128
+ parts.push(String(toolInput._externalProvenance));
129
+ }
130
+ if (toolInput.source) {
131
+ parts.push(String(toolInput.source));
132
+ }
133
+ if (Array.isArray(toolInput.tags)) {
134
+ parts.push(toolInput.tags.join(' '));
135
+ }
136
+ return parts.filter(Boolean).map(normalizeText).join('\n');
137
+ }
138
+
139
+ function extractTargetPaths(toolName, toolInput = {}) {
140
+ const paths = new Set();
141
+ for (const key of ['file_path', 'path', 'filePath', 'target_file', 'notebook_path']) {
142
+ if (toolInput[key]) paths.add(normalizeText(toolInput[key]));
143
+ }
144
+ const command = normalizeText(toolInput.command);
145
+ if (command) {
146
+ // Named durable carriers anywhere in the command (tee MEMORY.md, vim AGENTS.md).
147
+ const named = /(?:^|[\s'"=])((?:\.\/|\.\.\/|\/)?(?:[\w.@[\]-]+\/)*(?:MEMORY|AGENTS|USER|IDENTITY|SOUL|HEARTBEAT|BRAIN|prevention-rules)\.md|memory\/\d{4}-\d{2}-\d{2}\.md)/gi;
148
+ let match;
149
+ while ((match = named.exec(command)) !== null) {
150
+ paths.add(match[1]);
151
+ }
152
+ // Shell output redirects: echo ... > MEMORY.md, printf ... >> path
153
+ // Avoid matching comparison operators by requiring whitespace (or start) before `>`.
154
+ const redirect = /(?:^|[\s;|&(])>{1,2}\s*['"]?([^\s'"`;&|<>]+)/g;
155
+ while ((match = redirect.exec(command)) !== null) {
156
+ paths.add(match[1]);
157
+ }
158
+ }
159
+ return [...paths].filter(Boolean);
160
+ }
161
+
162
+ function isDurableCarrier(filePath) {
163
+ const p = normalizeText(filePath);
164
+ return DURABLE_CARRIER_PATTERNS.some((re) => re.test(p));
165
+ }
166
+
167
+ function isEpisodicMemoryPath(filePath) {
168
+ const p = normalizeText(filePath);
169
+ return EPISODIC_MEMORY_PATTERNS.some((re) => re.test(p));
170
+ }
171
+
172
+ function matchAny(text, patterns) {
173
+ const hits = [];
174
+ for (const re of patterns) {
175
+ if (re.test(text)) hits.push(re.source);
176
+ }
177
+ return hits;
178
+ }
179
+
180
+ function isWriteLikeTool(toolName, toolInput = {}) {
181
+ if (WRITE_TOOLS.has(toolName)) return true;
182
+ if (toolName === 'Bash' || toolName === 'bash' || toolName === 'Shell') {
183
+ const cmd = normalizeText(toolInput.command);
184
+ // Shell output redirects (echo/printf/cat … > file or >> file). Require a
185
+ // token boundary before `>` so we do not treat `2>1` / comparison noise only;
186
+ // still match the common MemGhost path `echo '…' > MEMORY.md`.
187
+ const hasOutputRedirect = /(?:^|[\s;|&(])>{1,2}\s*\S/.test(cmd);
188
+ return hasOutputRedirect
189
+ || /(?:\btee\b|\bcp\s+|\bmv\s+|\binstall\s+-m|\bsed\s+-i)/i.test(cmd)
190
+ || /\b(?:Write|write_file|create_file)\b/i.test(cmd);
191
+ }
192
+ return false;
193
+ }
194
+
195
+ function isExplicitlyApproved(toolInput = {}) {
196
+ if (toolInput.stealthMemoryInjectionApproved === true) return true;
197
+ if (toolInput._stealthMemoryInjectionApproved === true) return true;
198
+ if (String(process.env.THUMBGATE_ALLOW_STEALTH_MEMORY_INJECTION || '') === '1') {
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ /**
205
+ * Evaluate whether this tool call should be blocked as stealth memory injection.
206
+ * @returns {null|{decision,gate,message,severity,reasoning,signals}}
207
+ */
208
+ function evaluateStealthMemoryInjection(toolName, toolInput = {}) {
209
+ if (isExplicitlyApproved(toolInput)) return null;
210
+ if (!isWriteLikeTool(toolName, toolInput)) return null;
211
+
212
+ const targets = extractTargetPaths(toolName, toolInput);
213
+ const durableTargets = targets.filter(isDurableCarrier);
214
+ const episodicTargets = targets.filter(isEpisodicMemoryPath);
215
+ // Require a real memory destination (path or redirect target). Content that only
216
+ // *describes* MEMORY.md attacks (docs, research notes) must not false-positive.
217
+ if (durableTargets.length === 0 && episodicTargets.length === 0) {
218
+ return null;
219
+ }
220
+
221
+ const haystack = collectHaystack(toolName, toolInput);
222
+ const stealthHits = matchAny(haystack, STEALTH_PATTERNS);
223
+ const externalHits = matchAny(haystack, EXTERNAL_PROVENANCE_PATTERNS);
224
+ const preferenceHits = matchAny(haystack, PREFERENCE_POISON_PATTERNS);
225
+ const factHits = matchAny(haystack, FACT_POISON_PATTERNS);
226
+ const explicitExternalFlag = Boolean(
227
+ toolInput._externalProvenance
228
+ || toolInput.externalProvenance
229
+ || toolInput.source === 'email'
230
+ || toolInput.source === 'imap'
231
+ || toolInput.source === 'untrusted_external'
232
+ );
233
+
234
+ const hasExternal = externalHits.length > 0 || explicitExternalFlag;
235
+ const hasStealth = stealthHits.length > 0;
236
+ const hasPoisonLanguage = preferenceHits.length > 0 || factHits.length > 0;
237
+ const hasDurable = durableTargets.length > 0;
238
+
239
+ // Durable + stealth alone is enough (MemGhost core).
240
+ // Durable + external + poison language is enough (provenance risk).
241
+ // Episodic only blocks when stealth + external both present.
242
+ let shouldBlock = false;
243
+ let reasonCode = null;
244
+
245
+ if (hasDurable && hasStealth) {
246
+ shouldBlock = true;
247
+ reasonCode = 'durable_carrier_stealth_write';
248
+ } else if (hasDurable && hasExternal && hasPoisonLanguage) {
249
+ shouldBlock = true;
250
+ reasonCode = 'durable_carrier_external_poison';
251
+ } else if (episodicTargets.length > 0 && hasStealth && hasExternal) {
252
+ shouldBlock = true;
253
+ reasonCode = 'episodic_stealth_external';
254
+ }
255
+
256
+ if (!shouldBlock) return null;
257
+
258
+ const carrierClass = hasDurable ? 'durable' : 'episodic';
259
+ const message = [
260
+ 'Blocked: suspected stealth memory injection into persistent agent state.',
261
+ `Gate ${GATE_ID} (paper ${PAPER_ID}).`,
262
+ 'Untrusted external content must not silently update durable memory carriers',
263
+ '(MEMORY.md / AGENTS.md / SOUL.md / USER.md / HEARTBEAT.md / prevention-rules).',
264
+ 'Require human review, provenance tagging, or set',
265
+ 'THUMBGATE_ALLOW_STEALTH_MEMORY_INJECTION=1 only for explicit operator overrides.',
266
+ `See ${PAPER_URL}`,
267
+ ].join(' ');
268
+
269
+ return {
270
+ decision: 'deny',
271
+ gate: GATE_ID,
272
+ message,
273
+ severity: 'critical',
274
+ reasoning: [{
275
+ id: GATE_ID,
276
+ action: 'block',
277
+ layer: 'Memory',
278
+ severity: 'critical',
279
+ message,
280
+ paper: PAPER_ID,
281
+ reasonCode,
282
+ carrierClass,
283
+ }],
284
+ signals: {
285
+ paperId: PAPER_ID,
286
+ paperUrl: PAPER_URL,
287
+ reasonCode,
288
+ carrierClass,
289
+ durableTargets,
290
+ episodicTargets,
291
+ stealthHits: stealthHits.slice(0, 5),
292
+ externalHits: externalHits.slice(0, 5),
293
+ preferenceHits: preferenceHits.slice(0, 3),
294
+ factHits: factHits.slice(0, 3),
295
+ },
296
+ };
297
+ }
298
+
299
+ function main() {
300
+ const toolName = process.argv[2] || 'Write';
301
+ let toolInput = {};
302
+ const raw = process.argv[3];
303
+ if (raw) {
304
+ try {
305
+ toolInput = JSON.parse(raw);
306
+ } catch {
307
+ toolInput = { content: raw };
308
+ }
309
+ }
310
+ const result = evaluateStealthMemoryInjection(toolName, toolInput);
311
+ console.log(JSON.stringify(result, null, 2));
312
+ process.exit(result && result.decision === 'deny' ? 2 : 0);
313
+ }
314
+
315
+ if (process.argv[1]
316
+ && require('node:path').resolve(process.argv[1]) === require('node:path').resolve(__filename)) {
317
+ main();
318
+ }
319
+
320
+ module.exports = {
321
+ GATE_ID,
322
+ PAPER_ID,
323
+ PAPER_URL,
324
+ DURABLE_CARRIER_PATTERNS,
325
+ STEALTH_PATTERNS,
326
+ EXTERNAL_PROVENANCE_PATTERNS,
327
+ collectHaystack,
328
+ extractTargetPaths,
329
+ isDurableCarrier,
330
+ isEpisodicMemoryPath,
331
+ isWriteLikeTool,
332
+ evaluateStealthMemoryInjection,
333
+ };
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * NeMo Switchyard–inspired multi-model step router for ThumbGate.
6
+ *
7
+ * Steal high-ROI ideas from NVIDIA Nemotron 3.5 Lightning + NeMo Switchyard
8
+ * (2026-08-11): always-on agents need more than one model; route each step
9
+ * across a model pool for accuracy, efficiency, customization, and control.
10
+ *
11
+ * This is application-level routing + evidence, not a neural MoE. ThumbGate
12
+ * still owns PreToolUse gates and proof. Nemotron Lightning (30B MoE / 3B
13
+ * active) is the default cheap specialist for intent/gate/classify steps.
14
+ */
15
+
16
+ const DEFAULT_POOL = Object.freeze([
17
+ {
18
+ id: 'nvidia/nemotron-3.5-lightning',
19
+ model: 'nemotron-3.5-lightning',
20
+ provider: 'nvidia',
21
+ costClass: 'low',
22
+ activeParamsB: 3,
23
+ totalParamsB: 30,
24
+ roles: ['intent', 'classify', 'gate', 'route-decision', 'always-on', 'specialized'],
25
+ strengths: ['fast-inference', 'cost-efficiency', 'tool-use', 'reliability'],
26
+ },
27
+ {
28
+ id: 'alibaba/qwen3.6-flash',
29
+ model: 'qwen3.6-flash',
30
+ provider: 'model-studio',
31
+ costClass: 'low',
32
+ roles: ['triage', 'gate', 'cheap-fast'],
33
+ strengths: ['fast-inference', 'cost-efficiency', 'tool-use'],
34
+ },
35
+ {
36
+ id: 'alibaba/qwen3.8-max',
37
+ model: 'qwen3.8-max',
38
+ provider: 'model-studio',
39
+ costClass: 'medium',
40
+ roles: ['coding', 'agentic', 'long-horizon', 'high-output'],
41
+ strengths: ['agentic-coding', 'tool-use', 'long-horizon-coding', 'cost-efficiency'],
42
+ },
43
+ {
44
+ id: 'anthropic/claude-sonnet',
45
+ model: 'claude-sonnet-5-standard',
46
+ provider: 'anthropic',
47
+ costClass: 'high',
48
+ roles: ['reasoning', 'architecture', 'quality', 'review'],
49
+ strengths: ['reliability', 'long-horizon-coding', 'multi-agent'],
50
+ },
51
+ {
52
+ id: 'google/gemini-2.5-flash',
53
+ model: 'gemini-2.5-flash',
54
+ provider: 'gemini',
55
+ costClass: 'low',
56
+ roles: ['concise', 'summarize', 'extract'],
57
+ strengths: ['fast-inference', 'cost-efficiency', 'long-context'],
58
+ },
59
+ {
60
+ id: 'local/frontier',
61
+ model: 'local',
62
+ provider: 'local',
63
+ costClass: 'low',
64
+ roles: ['private', 'sensitive', 'offline'],
65
+ strengths: ['privacy', 'cost-efficiency', 'reliability'],
66
+ },
67
+ ]);
68
+
69
+ const STEP_ROLE_HINTS = Object.freeze({
70
+ intent: ['intent', 'classify', 'detect', 'route-decision'],
71
+ classify: ['classify', 'intent', 'label'],
72
+ gate: ['gate', 'pretool', 'allow', 'deny', 'check'],
73
+ code: ['code', 'coding', 'implement', 'patch', 'refactor'],
74
+ reason: ['reason', 'architecture', 'design', 'plan', 'review'],
75
+ summarize: ['summarize', 'extract', 'concise', 'compress'],
76
+ private: ['private', 'sensitive', 'pii', 'secret', 'local'],
77
+ bulk: ['bulk', 'high-volume', 'automation', 'always-on'],
78
+ });
79
+
80
+ const COST_RANK = Object.freeze({ low: 0, medium: 1, high: 2, variable: 1 });
81
+
82
+ function normalizeStep(step, index = 0) {
83
+ if (typeof step === 'string') {
84
+ return {
85
+ id: `step-${index + 1}`,
86
+ type: step,
87
+ tags: [],
88
+ riskLevel: 'medium',
89
+ };
90
+ }
91
+ const s = step && typeof step === 'object' ? step : {};
92
+ return {
93
+ id: s.id || `step-${index + 1}`,
94
+ type: String(s.type || s.role || s.name || 'unknown').toLowerCase(),
95
+ tags: Array.isArray(s.tags) ? s.tags.map((t) => String(t).toLowerCase()) : [],
96
+ riskLevel: String(s.riskLevel || 'medium').toLowerCase(),
97
+ sensitive: Boolean(s.sensitive || s.privacyRoute === 'local'),
98
+ highOutput: Boolean(s.highOutput || s.highVolume),
99
+ contextTokens: Number(s.contextTokens) || 0,
100
+ };
101
+ }
102
+
103
+ function inferStepRole(step) {
104
+ if (step.sensitive) return 'private';
105
+ const hay = `${step.type} ${(step.tags || []).join(' ')}`.toLowerCase();
106
+ for (const [role, hints] of Object.entries(STEP_ROLE_HINTS)) {
107
+ if (hints.some((h) => hay.includes(h))) return role;
108
+ }
109
+ if (step.highOutput || step.contextTokens >= 128000) return 'code';
110
+ if (step.riskLevel === 'high') return 'reason';
111
+ return 'intent';
112
+ }
113
+
114
+ function scorePoolMember(member, role, step) {
115
+ const roles = Array.isArray(member.roles) ? member.roles : [];
116
+ let score = 0;
117
+ const matchedRoles = [];
118
+
119
+ const roleMap = {
120
+ intent: ['intent', 'classify', 'gate', 'route-decision', 'always-on', 'specialized', 'triage'],
121
+ classify: ['classify', 'intent', 'label', 'specialized'],
122
+ gate: ['gate', 'pretool', 'always-on', 'specialized', 'cheap-fast'],
123
+ code: ['coding', 'agentic', 'long-horizon', 'high-output'],
124
+ reason: ['reasoning', 'architecture', 'quality', 'review'],
125
+ summarize: ['concise', 'summarize', 'extract'],
126
+ private: ['private', 'sensitive', 'offline'],
127
+ bulk: ['always-on', 'specialized', 'cheap-fast', 'triage', 'high-output'],
128
+ };
129
+
130
+ const wanted = roleMap[role] || ['specialized'];
131
+ for (const w of wanted) {
132
+ if (roles.includes(w)) {
133
+ score += 20;
134
+ matchedRoles.push(w);
135
+ }
136
+ }
137
+
138
+ // Prefer low cost for non-quality roles
139
+ const cost = COST_RANK[member.costClass] ?? 1;
140
+ if (role === 'reason') score += cost * 4; // higher cost OK for quality
141
+ else score += (2 - Math.min(cost, 2)) * 10;
142
+
143
+ // MoE Lightning bonus for always-on / gate / intent
144
+ if (member.activeParamsB && member.activeParamsB <= 4
145
+ && ['intent', 'classify', 'gate', 'bulk'].includes(role)) {
146
+ score += 12;
147
+ }
148
+
149
+ if (step.sensitive && member.provider !== 'local') {
150
+ score -= 100;
151
+ }
152
+ if (step.sensitive && member.provider === 'local') {
153
+ score += 40;
154
+ }
155
+
156
+ return { score, matchedRoles };
157
+ }
158
+
159
+ function pickModelForStep(stepInput, options = {}) {
160
+ const pool = Array.isArray(options.pool) && options.pool.length
161
+ ? options.pool
162
+ : DEFAULT_POOL;
163
+ const step = normalizeStep(stepInput, options.index || 0);
164
+ const role = options.forceRole || inferStepRole(step);
165
+
166
+ const ranked = pool.map((member) => {
167
+ const scored = scorePoolMember(member, role, step);
168
+ return {
169
+ ...member,
170
+ score: scored.score,
171
+ matchedRoles: scored.matchedRoles,
172
+ };
173
+ }).sort((a, b) => b.score - a.score || String(a.id).localeCompare(String(b.id)));
174
+
175
+ const winner = ranked[0];
176
+ return {
177
+ stepId: step.id,
178
+ stepType: step.type,
179
+ role,
180
+ modelId: winner.id,
181
+ model: winner.model,
182
+ provider: winner.provider,
183
+ costClass: winner.costClass,
184
+ score: winner.score,
185
+ reason: winner.provider === 'local' && step.sensitive
186
+ ? 'Sensitive step forced to local pool member.'
187
+ : `Switchyard step role=${role} → ${winner.id} (score ${winner.score}).`,
188
+ alternatives: ranked.slice(1, 4).map((m) => ({
189
+ modelId: m.id,
190
+ score: m.score,
191
+ provider: m.provider,
192
+ })),
193
+ evidence: {
194
+ role,
195
+ matchedRoles: winner.matchedRoles,
196
+ poolSize: pool.length,
197
+ activeParamsB: winner.activeParamsB || null,
198
+ totalParamsB: winner.totalParamsB || null,
199
+ },
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Route a multi-step agent plan across the model pool (Switchyard core).
205
+ * @param {Array|object} plan - array of steps or { steps: [] }
206
+ */
207
+ function routeAgentSteps(plan, options = {}) {
208
+ const steps = Array.isArray(plan)
209
+ ? plan
210
+ : (plan && Array.isArray(plan.steps) ? plan.steps : []);
211
+ if (steps.length === 0) {
212
+ return {
213
+ architecture: 'switchyard-multi-model',
214
+ mixtureOfExperts: false,
215
+ steps: [],
216
+ distinctModels: [],
217
+ multiModel: false,
218
+ reason: 'No steps to route.',
219
+ };
220
+ }
221
+
222
+ const routed = steps.map((step, index) => pickModelForStep(step, { ...options, index }));
223
+ const distinctModels = [...new Set(routed.map((r) => r.modelId))];
224
+ const multiModel = distinctModels.length > 1;
225
+ const complex = steps.length >= 2
226
+ || routed.some((r) => r.role === 'code' || r.role === 'reason');
227
+
228
+ return {
229
+ architecture: 'switchyard-multi-model',
230
+ mixtureOfExperts: false,
231
+ inspiredBy: 'NVIDIA NeMo Switchyard + Nemotron 3.5 Lightning',
232
+ steps: routed,
233
+ distinctModels,
234
+ multiModel,
235
+ complex,
236
+ singleModelAntiPattern: complex && !multiModel,
237
+ reason: multiModel
238
+ ? `Routed ${routed.length} steps across ${distinctModels.length} models (accuracy/efficiency/control).`
239
+ : complex
240
+ ? 'Complex plan still on one model — evaluate pool coverage or step labels.'
241
+ : `Single-model OK for simple ${routed.length}-step plan.`,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Evaluate a routing algorithm vs a single-model baseline (Switchyard "evaluate").
247
+ * Pure metrics helper — no network.
248
+ */
249
+ function evaluateRoutingAlgorithm(options = {}) {
250
+ const baseline = options.baseline || {};
251
+ const candidate = options.candidate || {};
252
+ const baselineCost = Number(baseline.costUsd);
253
+ const candidateCost = Number(candidate.costUsd);
254
+ const baselineQuality = Number(baseline.qualityScore);
255
+ const candidateQuality = Number(candidate.qualityScore);
256
+ const baselineLatency = Number(baseline.latencyMs);
257
+ const candidateLatency = Number(candidate.latencyMs);
258
+
259
+ const hasCost = Number.isFinite(baselineCost) && Number.isFinite(candidateCost);
260
+ const hasQuality = Number.isFinite(baselineQuality) && Number.isFinite(candidateQuality);
261
+ const hasLatency = Number.isFinite(baselineLatency) && Number.isFinite(candidateLatency);
262
+
263
+ if (!hasCost || !hasQuality) {
264
+ return {
265
+ pass: false,
266
+ action: 'block',
267
+ reason: 'Routing evaluation requires baseline and candidate costUsd + qualityScore evidence.',
268
+ deltas: null,
269
+ };
270
+ }
271
+
272
+ const costDeltaUsd = baselineCost - candidateCost;
273
+ const costSavingsPercent = baselineCost > 0
274
+ ? Number(((costDeltaUsd / baselineCost) * 100).toFixed(1))
275
+ : 0;
276
+ const qualityDelta = candidateQuality - baselineQuality;
277
+ const latencyDeltaMs = hasLatency ? candidateLatency - baselineLatency : null;
278
+
279
+ // Fail closed if quality drops more than allowed while claiming savings.
280
+ const maxQualityDrop = Number.isFinite(Number(options.maxQualityDrop))
281
+ ? Number(options.maxQualityDrop)
282
+ : 0.05;
283
+ const minSavingsPercent = Number.isFinite(Number(options.minSavingsPercent))
284
+ ? Number(options.minSavingsPercent)
285
+ : 10;
286
+
287
+ let pass = true;
288
+ const failures = [];
289
+ if (qualityDelta < -maxQualityDrop) {
290
+ pass = false;
291
+ failures.push(`quality drop ${qualityDelta.toFixed(3)} exceeds max ${maxQualityDrop}`);
292
+ }
293
+ if (costSavingsPercent < minSavingsPercent && options.requireSavings !== false) {
294
+ pass = false;
295
+ failures.push(`cost savings ${costSavingsPercent}% below min ${minSavingsPercent}%`);
296
+ }
297
+
298
+ return {
299
+ pass,
300
+ action: pass ? 'allow' : 'block',
301
+ reason: pass
302
+ ? `Routing beats baseline: ${costSavingsPercent}% cheaper, quality Δ ${qualityDelta.toFixed(3)}.`
303
+ : `Routing evidence failed: ${failures.join('; ')}.`,
304
+ deltas: {
305
+ costDeltaUsd: Number(costDeltaUsd.toFixed(4)),
306
+ costSavingsPercent,
307
+ qualityDelta: Number(qualityDelta.toFixed(4)),
308
+ latencyDeltaMs,
309
+ },
310
+ failures,
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Build default always-on agent step skeleton (intent → gate → act → review).
316
+ */
317
+ function buildAlwaysOnAgentPlan(options = {}) {
318
+ const sensitive = Boolean(options.sensitive);
319
+ return [
320
+ { id: 'intent', type: 'intent-classify', tags: ['always-on', 'intent'] },
321
+ { id: 'gate', type: 'pretool-gate', tags: ['gate', 'pretool'] },
322
+ {
323
+ id: 'act',
324
+ type: options.actType || 'coding-implement',
325
+ tags: options.highVolume ? ['coding', 'high-volume'] : ['coding'],
326
+ riskLevel: options.riskLevel || 'medium',
327
+ highOutput: Boolean(options.highVolume),
328
+ },
329
+ {
330
+ id: 'review',
331
+ type: 'quality-review',
332
+ tags: ['review', 'reasoning'],
333
+ riskLevel: options.riskLevel || 'medium',
334
+ sensitive,
335
+ },
336
+ ];
337
+ }
338
+
339
+ function describeNemotronLightning() {
340
+ return {
341
+ id: 'nvidia/nemotron-3.5-lightning',
342
+ name: 'NVIDIA Nemotron 3.5 Lightning',
343
+ totalParamsB: 30,
344
+ activeParamsB: 3,
345
+ architecture: 'MoE',
346
+ open: true,
347
+ useCases: [
348
+ 'always-on agent specialized steps',
349
+ 'intent / classify / gate decisions',
350
+ 'cheap routing algorithm evaluator companion',
351
+ ],
352
+ companion: 'NeMo Switchyard (multi-model step routing + evaluation)',
353
+ };
354
+ }
355
+
356
+ module.exports = {
357
+ DEFAULT_POOL,
358
+ STEP_ROLE_HINTS,
359
+ normalizeStep,
360
+ inferStepRole,
361
+ pickModelForStep,
362
+ routeAgentSteps,
363
+ evaluateRoutingAlgorithm,
364
+ buildAlwaysOnAgentPlan,
365
+ describeNemotronLightning,
366
+ };