thumbgate 1.27.18 → 1.27.20

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 (170) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +4 -3
  9. package/.well-known/agentic-verify.txt +1 -0
  10. package/.well-known/llms.txt +33 -12
  11. package/.well-known/mcp/server-card.json +8 -8
  12. package/README.md +246 -30
  13. package/adapters/claude/.mcp.json +2 -2
  14. package/adapters/codex/config.toml +2 -2
  15. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  16. package/adapters/gemini/function-declarations.json +1 -0
  17. package/adapters/letta/README.md +41 -0
  18. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  19. package/adapters/mcp/server-stdio.js +263 -11
  20. package/adapters/opencode/opencode.json +1 -1
  21. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  22. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  23. package/bench/observability-eval-suite.json +26 -0
  24. package/bench/thumbgate-bench.json +2 -2
  25. package/bin/cli.js +1456 -122
  26. package/bin/dashboard-cli.js +7 -0
  27. package/bin/postinstall.js +11 -27
  28. package/commands/dashboard.md +15 -0
  29. package/commands/thumbgate-dashboard.md +15 -0
  30. package/config/gate-classifier-routing.json +98 -0
  31. package/config/gate-templates.json +216 -0
  32. package/config/gates/claim-verification.json +12 -0
  33. package/config/gates/default.json +31 -2
  34. package/config/github-about.json +2 -2
  35. package/config/mcp-allowlists.json +23 -13
  36. package/config/merge-quality-checks.json +0 -1
  37. package/config/model-candidates.json +121 -6
  38. package/config/post-deploy-marketing-pages.json +80 -0
  39. package/config/tessl-tiles.json +1 -3
  40. package/openapi/openapi.yaml +12 -0
  41. package/package.json +225 -100
  42. package/public/about.html +162 -0
  43. package/public/agent-manager.html +179 -0
  44. package/public/agents-cost-savings.html +153 -0
  45. package/public/ai-malpractice-prevention.html +818 -0
  46. package/public/assets/brand/github-social-preview.png +0 -0
  47. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  48. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  49. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  50. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  51. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  52. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  53. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  54. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  55. package/public/assets/brand/thumbgate-mark.svg +21 -0
  56. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  57. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  58. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  59. package/public/assets/legal-intake-control-flow.svg +66 -0
  60. package/public/blog.html +4 -4
  61. package/public/brand/thumbgate-mark.svg +19 -0
  62. package/public/brand/thumbgate-og.svg +16 -0
  63. package/public/chatgpt-app.html +330 -0
  64. package/public/codex-enterprise.html +123 -0
  65. package/public/codex-plugin.html +72 -20
  66. package/public/compare.html +31 -8
  67. package/public/dashboard.html +930 -166
  68. package/public/diagnostic.html +345 -0
  69. package/public/federal.html +2 -2
  70. package/public/guide.html +33 -13
  71. package/public/index.html +469 -111
  72. package/public/install.html +193 -0
  73. package/public/js/buyer-intent.js +672 -0
  74. package/public/learn.html +183 -18
  75. package/public/lessons.html +168 -10
  76. package/public/numbers.html +7 -7
  77. package/public/pricing.html +399 -0
  78. package/public/pro.html +34 -11
  79. package/scripts/action-receipts.js +324 -0
  80. package/scripts/activation-quickstart.js +187 -0
  81. package/scripts/agent-memory-lifecycle.js +211 -0
  82. package/scripts/agent-operations-planner.js +621 -0
  83. package/scripts/agent-readiness.js +20 -3
  84. package/scripts/agent-reward-model.js +53 -1
  85. package/scripts/ai-component-inventory.js +367 -0
  86. package/scripts/async-eval-observability.js +236 -0
  87. package/scripts/audit.js +65 -0
  88. package/scripts/auto-promote-gates.js +82 -10
  89. package/scripts/auto-wire-hooks.js +14 -0
  90. package/scripts/aws-blocks-guardrails.js +272 -0
  91. package/scripts/billing.js +93 -1
  92. package/scripts/bot-detection.js +61 -3
  93. package/scripts/build-metadata.js +50 -10
  94. package/scripts/classifier-routing.js +130 -0
  95. package/scripts/cli-feedback.js +4 -2
  96. package/scripts/cli-schema.js +97 -0
  97. package/scripts/cli-telemetry.js +6 -1
  98. package/scripts/commercial-offer.js +82 -2
  99. package/scripts/context-manager.js +74 -6
  100. package/scripts/dashboard-chat.js +332 -0
  101. package/scripts/dashboard.js +68 -2
  102. package/scripts/export-databricks-bundle.js +5 -1
  103. package/scripts/export-dpo-pairs.js +7 -2
  104. package/scripts/feedback-aggregate.js +281 -0
  105. package/scripts/feedback-loop.js +123 -1
  106. package/scripts/feedback-quality.js +87 -0
  107. package/scripts/feedback-sanitizer.js +105 -0
  108. package/scripts/filesystem-search.js +35 -10
  109. package/scripts/gate-stats.js +89 -0
  110. package/scripts/gates-engine.js +1176 -85
  111. package/scripts/gemini-embedding-policy.js +2 -1
  112. package/scripts/hook-runtime.js +20 -14
  113. package/scripts/hook-stop-anti-claim.js +301 -0
  114. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  115. package/scripts/hybrid-feedback-context.js +142 -7
  116. package/scripts/install-shim.js +87 -0
  117. package/scripts/lesson-inference.js +8 -3
  118. package/scripts/lesson-search.js +17 -1
  119. package/scripts/license.js +10 -10
  120. package/scripts/llm-client.js +169 -4
  121. package/scripts/local-model-profile.js +15 -8
  122. package/scripts/mcp-config.js +7 -1
  123. package/scripts/mcp-oauth.js +293 -0
  124. package/scripts/memory-scope-readiness.js +159 -0
  125. package/scripts/meta-agent-loop.js +36 -0
  126. package/scripts/noop-detect.js +285 -0
  127. package/scripts/operational-integrity.js +39 -5
  128. package/scripts/oss-pr-opportunity-scout.js +35 -5
  129. package/scripts/parallel-workflow-orchestrator.js +293 -0
  130. package/scripts/plan-gate.js +243 -0
  131. package/scripts/plausible-domain-config.js +99 -0
  132. package/scripts/plausible-server-events.js +9 -6
  133. package/scripts/pro-local-dashboard.js +4 -4
  134. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  135. package/scripts/published-cli.js +0 -8
  136. package/scripts/qa-scenario-planner.js +136 -0
  137. package/scripts/rate-limiter.js +64 -13
  138. package/scripts/repeat-metric.js +137 -0
  139. package/scripts/secret-fixture-tokens.js +61 -0
  140. package/scripts/secret-redaction.js +166 -0
  141. package/scripts/secret-scanner.js +44 -5
  142. package/scripts/security-scanner.js +260 -10
  143. package/scripts/self-distill-agent.js +3 -1
  144. package/scripts/self-harness-optimizer.js +141 -0
  145. package/scripts/self-healing-check.js +193 -0
  146. package/scripts/self-protection.js +90 -0
  147. package/scripts/seo-gsd.js +916 -7
  148. package/scripts/silent-failure-cluster.js +531 -0
  149. package/scripts/statusline-cache-path.js +17 -2
  150. package/scripts/statusline-cache-read.js +57 -0
  151. package/scripts/statusline-local-stats.js +9 -1
  152. package/scripts/statusline-meta.js +28 -2
  153. package/scripts/statusline.sh +20 -4
  154. package/scripts/sync-telemetry-from-prod.js +374 -0
  155. package/scripts/telemetry-analytics.js +357 -0
  156. package/scripts/thompson-sampling.js +31 -10
  157. package/scripts/thumbgate-bench.js +16 -1
  158. package/scripts/thumbgate-search.js +85 -19
  159. package/scripts/tool-contract-validator.js +76 -0
  160. package/scripts/tool-registry.js +169 -1
  161. package/scripts/trajectory-scorer.js +63 -0
  162. package/scripts/vector-store.js +45 -0
  163. package/scripts/verify-marketing-pages-deployed.js +212 -0
  164. package/scripts/visitor-journey.js +172 -0
  165. package/scripts/workflow-sentinel.js +286 -53
  166. package/scripts/workspace-evolver.js +62 -2
  167. package/src/api/server.js +2683 -319
  168. package/.claude-plugin/marketplace.json +0 -85
  169. package/adapters/chatgpt/openapi.yaml +0 -1695
  170. package/scripts/bot-detector.js +0 -50
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const { loadOptionalModule } = require("./private-core-boundary");
5
+
6
+
4
7
  /**
5
8
  * Security Scanner — OWASP-aware static analysis for PreToolUse checks.
6
9
  *
@@ -16,6 +19,10 @@
16
19
  const fs = require('fs');
17
20
  const path = require('path');
18
21
  const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
22
+ const { scanInstallCommand, detectSlopsquat } = loadOptionalModule('./slopsquat-guard', () => ({
23
+ scanInstallCommand: () => ({ detected: false, findings: [] }),
24
+ detectSlopsquat: () => null,
25
+ }));
19
26
 
20
27
  // ---------------------------------------------------------------------------
21
28
  // Vulnerability pattern definitions (OWASP Top 10 + supply chain)
@@ -30,6 +37,7 @@ const VULN_PATTERNS = [
30
37
  label: 'Command injection via unsanitized input',
31
38
  regex: /\bexec(?:Sync)?\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*(?:req\.|input|args|params|query|body|user))/g,
32
39
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
40
+ remediation: 'Avoid passing unsanitized input to child_process.exec/execSync. Use execFile or spawn with args passed as an array.',
33
41
  },
34
42
  {
35
43
  id: 'shell-interpolation',
@@ -38,6 +46,7 @@ const VULN_PATTERNS = [
38
46
  label: 'Shell command with string interpolation',
39
47
  regex: /\bexec(?:Sync)?\s*\(\s*`[^`]*\$\{[^}]*(?:req\.|input|args|params|query|body|user|process\.env)/g,
40
48
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
49
+ remediation: 'Use child_process.execFile or child_process.spawn with an array of arguments to avoid shell command interpolation.',
41
50
  },
42
51
  {
43
52
  id: 'sql-injection',
@@ -46,6 +55,7 @@ const VULN_PATTERNS = [
46
55
  label: 'Potential SQL injection via string concatenation',
47
56
  regex: /(?:query|execute|run|all|get)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*(?:req\.|input|args|params|query|body|user))/g,
48
57
  fileTypes: ['.js', '.ts', '.mjs', '.cjs', '.py'],
58
+ remediation: 'Use parameterized queries or prepared statements instead of dynamic SQL string concatenation.',
49
59
  },
50
60
  {
51
61
  id: 'eval-usage',
@@ -54,6 +64,7 @@ const VULN_PATTERNS = [
54
64
  label: 'Dynamic code execution (eval/Function constructor)',
55
65
  regex: /\b(?:eval|new\s+Function)\s*\([^)]*(?:req\.|input|args|params|query|body|user)/g,
56
66
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
67
+ remediation: 'Use JSON.parse() or a safe parser library instead of eval() or dynamic Function constructors.',
57
68
  },
58
69
 
59
70
  // XSS
@@ -64,6 +75,7 @@ const VULN_PATTERNS = [
64
75
  label: 'Potential XSS via innerHTML assignment',
65
76
  regex: /\.innerHTML\s*=\s*(?!['"]<(?:div|span|p|br|hr)\s*\/?>['"])/g,
66
77
  fileTypes: ['.js', '.ts', '.jsx', '.tsx', '.mjs'],
78
+ remediation: 'Use element.textContent or element.innerText instead of innerHTML to prevent cross-site scripting (XSS).',
67
79
  },
68
80
  {
69
81
  id: 'xss-dangerously-set',
@@ -72,6 +84,7 @@ const VULN_PATTERNS = [
72
84
  label: 'React dangerouslySetInnerHTML with dynamic content',
73
85
  regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?!['"])/g,
74
86
  fileTypes: ['.jsx', '.tsx', '.js', '.ts'],
87
+ remediation: 'Ensure dynamic content passed to dangerouslySetInnerHTML is sanitized using DOMPurify or equivalent.',
75
88
  },
76
89
 
77
90
  // Path traversal
@@ -82,6 +95,7 @@ const VULN_PATTERNS = [
82
95
  label: 'Path traversal via unsanitized user input',
83
96
  regex: /path\.(?:join|resolve)\s*\([^)]*(?:req\.|input|args|params|query|body|user)/g,
84
97
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
98
+ remediation: 'Sanitize path input using path.basename() or validate the path against an explicit list of allowed directories.',
85
99
  },
86
100
  {
87
101
  id: 'path-traversal-direct',
@@ -90,6 +104,7 @@ const VULN_PATTERNS = [
90
104
  label: 'Direct file read with user-controlled path',
91
105
  regex: /fs\.(?:readFile(?:Sync)?|createReadStream)\s*\(\s*(?:req\.|input|args|params|query|body|user)/g,
92
106
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
107
+ remediation: 'Validate input paths and restrict file system access to a sandboxed directory.',
93
108
  },
94
109
 
95
110
  // Prototype pollution
@@ -100,6 +115,7 @@ const VULN_PATTERNS = [
100
115
  label: 'Potential prototype pollution via recursive merge',
101
116
  regex: /(?:__proto__|constructor\s*\[\s*['"]prototype['"]\s*\]|Object\.assign\s*\(\s*\{\s*\})/g,
102
117
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
118
+ remediation: 'Avoid recursive object mergers without checking for __proto__ or constructor keys, or use Object.create(null).',
103
119
  },
104
120
 
105
121
  // Insecure crypto
@@ -110,6 +126,7 @@ const VULN_PATTERNS = [
110
126
  label: 'Weak hash algorithm (MD5/SHA1) for security use',
111
127
  regex: /createHash\s*\(\s*['"](?:md5|sha1)['"]\s*\)/gi,
112
128
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
129
+ remediation: 'Use SHA-256 or SHA-512 (e.g. crypto.createHash("sha256")) instead of MD5 or SHA-1 for security use cases.',
113
130
  },
114
131
  {
115
132
  id: 'hardcoded-secret',
@@ -118,6 +135,7 @@ const VULN_PATTERNS = [
118
135
  label: 'Hardcoded secret/password in source code',
119
136
  regex: /(?:password|secret|apiKey|api_key|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{12,}['"]/g,
120
137
  fileTypes: ['.js', '.ts', '.mjs', '.cjs', '.py', '.go', '.java'],
138
+ remediation: 'Move hardcoded secrets to environment variables or use a secure secrets manager.',
121
139
  },
122
140
 
123
141
  // SSRF
@@ -128,6 +146,7 @@ const VULN_PATTERNS = [
128
146
  label: 'Potential SSRF via user-controlled URL',
129
147
  regex: /(?:fetch|axios|got|request|https?\.(?:get|request))\s*\(\s*(?:`[^`]*\$\{|(?:req\.|input|args|params|query|body|user))/g,
130
148
  fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
149
+ remediation: 'Validate the destination host against a strict whitelist of allowed domains and block requests to internal IPs.',
131
150
  },
132
151
 
133
152
  // Insecure deserialization
@@ -138,6 +157,16 @@ const VULN_PATTERNS = [
138
157
  label: 'Unsafe deserialization of untrusted data',
139
158
  regex: /(?:unserialize|yaml\.load\s*\((?!.*Loader\s*=\s*yaml\.SafeLoader)|pickle\.loads?|Marshal\.load)/g,
140
159
  fileTypes: ['.js', '.ts', '.py', '.rb'],
160
+ remediation: 'Use safe parsing libraries or options (such as yaml.safeLoad) instead of unsafe deserialization/eval-like loaders.',
161
+ },
162
+ {
163
+ id: 'badhost-url-confusion',
164
+ category: 'host-header',
165
+ severity: 'high',
166
+ label: 'Potential BadHost-style host or URL confusion in AI service',
167
+ regex: /\b(?:request\.url(?:\.path)?|url_for\s*\([^)]*_external\s*=\s*True|headers\s*\[\s*['"](?:host|x-forwarded-host)['"]\s*\])/gi,
168
+ fileTypes: ['.py'],
169
+ remediation: 'Verify Host and X-Forwarded-Host headers against an approved whitelist before using them for routing or URL generation.',
141
170
  },
142
171
  ];
143
172
 
@@ -151,6 +180,7 @@ const SUPPLY_CHAIN_PATTERNS = [
151
180
  category: 'supply-chain',
152
181
  severity: 'high',
153
182
  label: 'Potentially typosquatted package name',
183
+ remediation: 'Verify spelling and authenticity of package name. If typosquatted, run: npm uninstall <package>.',
154
184
  // Common typosquat indicators: single-char substitutions of popular packages
155
185
  knownSafe: new Set([
156
186
  'express', 'lodash', 'axios', 'react', 'vue', 'angular', 'moment',
@@ -164,6 +194,7 @@ const SUPPLY_CHAIN_PATTERNS = [
164
194
  category: 'supply-chain',
165
195
  severity: 'critical',
166
196
  label: 'Suspicious install script in package.json',
197
+ remediation: 'Remove the pre/postinstall script, or run package installation with --ignore-scripts.',
167
198
  regex: /["'](?:pre|post)?install["']\s*:\s*["'](?:.*(?:curl|wget|nc\s|bash\s|sh\s|eval|exec|child_process))/g,
168
199
  },
169
200
  {
@@ -171,10 +202,79 @@ const SUPPLY_CHAIN_PATTERNS = [
171
202
  category: 'supply-chain',
172
203
  severity: 'medium',
173
204
  label: 'Wildcard or latest version in dependency',
205
+ remediation: 'Specify a concrete version constraint (e.g., "^1.0.0") instead of a wildcard or latest.',
174
206
  regex: /["'](?:dependencies|devDependencies|peerDependencies)["'][\s\S]{0,500}?["'][^"']+["']\s*:\s*["'](?:\*|latest|>=)/g,
175
207
  },
176
208
  ];
177
209
 
210
+ /**
211
+ * Simple static analysis check (reachability/usage check) to see if a package is imported.
212
+ * @param {string} pkg - The package name to search for
213
+ * @param {string} rootDir - Root directory to walk
214
+ * @returns {boolean}
215
+ */
216
+ function isPackageImported(pkg, rootDir = process.cwd()) {
217
+ const IGNORED_DIRS = new Set([
218
+ 'node_modules', '.git', 'dist', 'coverage', '.planning', '.artifacts', '.gemini', '.antigravitycli'
219
+ ]);
220
+ const FILE_EXTS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py']);
221
+
222
+ // Escape package name for regex
223
+ const escapedPkg = pkg.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
224
+ const importRegex = new RegExp(
225
+ `(?:require\\s*\\(\\s*['"]${escapedPkg}['"]\\s*\\)|from\\s*['"]${escapedPkg}['"]|import\\s*\\(\\s*['"]${escapedPkg}['"]\\s*\\)|import\\s+['"]${escapedPkg}['"])`,
226
+ 'i'
227
+ );
228
+
229
+ let found = false;
230
+ let fileCount = 0;
231
+ const maxFiles = 200; // safety limit to keep it fast (<50ms)
232
+
233
+ function walk(dir) {
234
+ if (found || fileCount >= maxFiles) return;
235
+ let files;
236
+ try {
237
+ files = fs.readdirSync(dir);
238
+ } catch {
239
+ return;
240
+ }
241
+
242
+ for (const file of files) {
243
+ if (found || fileCount >= maxFiles) return;
244
+ const fullPath = path.join(dir, file);
245
+ let stat;
246
+ try {
247
+ stat = fs.statSync(fullPath);
248
+ } catch {
249
+ continue;
250
+ }
251
+
252
+ if (stat.isDirectory()) {
253
+ if (!IGNORED_DIRS.has(file)) {
254
+ walk(fullPath);
255
+ }
256
+ } else if (stat.isFile()) {
257
+ const ext = path.extname(file).toLowerCase();
258
+ if (FILE_EXTS.has(ext)) {
259
+ fileCount++;
260
+ try {
261
+ const content = fs.readFileSync(fullPath, 'utf8');
262
+ if (importRegex.test(content)) {
263
+ found = true;
264
+ return;
265
+ }
266
+ } catch {
267
+ // ignore read errors
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ walk(rootDir);
275
+ return found;
276
+ }
277
+
178
278
  // ---------------------------------------------------------------------------
179
279
  // Core scanning functions
180
280
  // ---------------------------------------------------------------------------
@@ -212,6 +312,7 @@ function scanCode(content, filePath = '') {
212
312
  line: lineNumber,
213
313
  match: match[0].slice(0, 120),
214
314
  path: filePath,
315
+ remediation: pattern.remediation,
215
316
  });
216
317
  // Only report first match per pattern per file to avoid noise
217
318
  break;
@@ -224,6 +325,22 @@ function scanCode(content, filePath = '') {
224
325
  };
225
326
  }
226
327
 
328
+ /**
329
+ * Scan Python / AI-service code for BadHost-style URL and host-header confusion.
330
+ * This is deliberately narrow and evidence-oriented: it does not claim a CVE,
331
+ * it flags code that should prove canonical host handling before deployment.
332
+ * @param {string} content
333
+ * @param {string} filePath
334
+ * @returns {{ detected: boolean, findings: Array<Object> }}
335
+ */
336
+ function scanBadHostExposure(content, filePath = '') {
337
+ const result = scanCode(content, filePath);
338
+ return {
339
+ detected: result.findings.some((finding) => finding.id === 'badhost-url-confusion'),
340
+ findings: result.findings.filter((finding) => finding.id === 'badhost-url-confusion'),
341
+ };
342
+ }
343
+
227
344
  /**
228
345
  * Scan dependency changes in package.json mutations.
229
346
  * @param {string} oldContent - Previous package.json content (empty string if new file)
@@ -256,6 +373,10 @@ function scanDependencyChange(oldContent, newContent) {
256
373
 
257
374
  for (const [pkg, version] of Object.entries(newDeps)) {
258
375
  if (!oldDeps[pkg]) {
376
+ // Run usage check (reachability)
377
+ const reachable = isPackageImported(pkg);
378
+ const reachability = reachable ? 'imported' : 'unimported';
379
+
259
380
  // New dependency added — check for red flags
260
381
  if (version === '*' || version === 'latest' || version.startsWith('>=')) {
261
382
  findings.push({
@@ -264,6 +385,9 @@ function scanDependencyChange(oldContent, newContent) {
264
385
  severity: 'medium',
265
386
  label: `Wildcard version for new dependency: ${pkg}@${version}`,
266
387
  path: 'package.json',
388
+ remediation: `Specify a concrete version constraint (e.g., "^${version === '*' || version === 'latest' ? '1.0.0' : version}") instead of a wildcard.`,
389
+ reachable,
390
+ reachability,
267
391
  });
268
392
  }
269
393
 
@@ -275,6 +399,24 @@ function scanDependencyChange(oldContent, newContent) {
275
399
  severity: 'high',
276
400
  label: `Suspiciously short package name: "${pkg}"`,
277
401
  path: 'package.json',
402
+ remediation: 'Double-check spelling and verify this package is not a typosquatting attempt.',
403
+ reachable,
404
+ reachability,
405
+ });
406
+ }
407
+
408
+ // Tier 3: Slopsquat Guard — deterministic typosquat detection
409
+ const slopsquatFinding = detectSlopsquat(pkg, 'npm');
410
+ if (slopsquatFinding) {
411
+ findings.push({
412
+ id: slopsquatFinding.id,
413
+ category: 'supply-chain',
414
+ severity: slopsquatFinding.severity,
415
+ label: slopsquatFinding.label,
416
+ path: 'package.json',
417
+ remediation: `Verify spelling and authenticity of package "${pkg}". If typosquatted, run: npm uninstall ${pkg}.`,
418
+ reachable,
419
+ reachability,
278
420
  });
279
421
  }
280
422
  }
@@ -292,6 +434,7 @@ function scanDependencyChange(oldContent, newContent) {
292
434
  severity: 'critical',
293
435
  label: `Suspicious install script: ${name} → ${cmd.slice(0, 80)}`,
294
436
  path: 'package.json',
437
+ remediation: 'Remove the suspicious pre/postinstall script, or run package installation with --ignore-scripts.',
295
438
  });
296
439
  }
297
440
  }
@@ -313,27 +456,68 @@ function scanDependencyChange(oldContent, newContent) {
313
456
  * @param {Object} input - Hook input { tool_name, tool_input }
314
457
  * @returns {Object|null} Gate result or null if clean
315
458
  */
459
+
460
+ /**
461
+ * Evaluate slopsquat guard for a Bash command.
462
+ * @param {string} toolName
463
+ * @param {Object} toolInput
464
+ * @returns {Object|null}
465
+ */
466
+ function evaluateSlopsquatScan(toolName, toolInput) {
467
+ if (toolName !== "Bash") return null;
468
+ const command = toolInput.command || "";
469
+ if (!command) return null;
470
+
471
+ const { resolveMode, scanInstallCommand } = loadOptionalModule("./slopsquat-guard", () => ({
472
+ resolveMode: () => "block",
473
+ scanInstallCommand: () => ({ detected: false, findings: [] }),
474
+ }));
475
+
476
+ const mode = resolveMode();
477
+ if (mode === "off") return null;
478
+
479
+ const result = scanInstallCommand(command);
480
+ if (!result.detected) return null;
481
+
482
+ const hasCritical = result.findings.some(f => f.severity === "critical");
483
+ const decision = (mode === "block" && hasCritical) ? "deny" : "warn";
484
+
485
+ return {
486
+ decision,
487
+ gate: "slopsquat-guard",
488
+ message: "✗ THUMBGATE: " + result.findings[0].label,
489
+ severity: hasCritical ? "critical" : "high",
490
+ reasoning: result.findings.map(f => f.label),
491
+ };
492
+ }
493
+
316
494
  function evaluateSecurityScan(input = {}) {
317
495
  const toolName = input.tool_name || input.toolName || '';
318
496
  const toolInput = input.tool_input || {};
319
497
 
320
- // Only scan write-type operations
498
+ // Only scan write-type operations and Bash commands
321
499
  const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
322
- if (!WRITE_TOOLS.has(toolName)) {
500
+ const IS_BASH = toolName === 'Bash';
501
+
502
+ if (!WRITE_TOOLS.has(toolName) && !IS_BASH) {
323
503
  return null;
324
504
  }
325
505
 
326
506
  const filePath = toolInput.file_path || toolInput.path || '';
327
507
  const content = toolInput.content || toolInput.new_string || '';
508
+ const command = toolInput.command || '';
328
509
 
329
- if (!content) return null;
510
+ if (!content && !command) return null;
330
511
 
331
- // Tier 1: Code vulnerability scan
332
- const codeResult = scanCode(content, filePath);
512
+ // Tier 1: Code vulnerability scan (for Edits)
513
+ let codeResult = { detected: false, findings: [] };
514
+ if (content) {
515
+ codeResult = scanCode(content, filePath);
516
+ }
333
517
 
334
518
  // Tier 2: Supply chain scan for package.json changes
335
519
  let supplyChainResult = { detected: false, findings: [] };
336
- if (filePath && path.basename(filePath) === 'package.json') {
520
+ if (filePath && path.basename(filePath) === 'package.json' && content) {
337
521
  let oldContent = '';
338
522
  try {
339
523
  const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
@@ -344,7 +528,14 @@ function evaluateSecurityScan(input = {}) {
344
528
  supplyChainResult = scanDependencyChange(oldContent, content);
345
529
  }
346
530
 
347
- const allFindings = [...codeResult.findings, ...supplyChainResult.findings];
531
+ // Tier 3: Slopsquat Guard for Bash commands
532
+ let slopsquatResult = { detected: false, findings: [] };
533
+ if (IS_BASH && command) {
534
+ const slopsquatGate = evaluateSlopsquatScan(toolName, toolInput);
535
+ if (slopsquatGate) return slopsquatGate;
536
+ }
537
+
538
+ const allFindings = [...codeResult.findings, ...supplyChainResult.findings, ...slopsquatResult.findings];
348
539
  if (allFindings.length === 0) return null;
349
540
 
350
541
  // Determine overall severity
@@ -359,16 +550,18 @@ function evaluateSecurityScan(input = {}) {
359
550
  `[${f.severity.toUpperCase()}] ${f.label}${f.line ? ` (line ${f.line})` : ''}`
360
551
  ).join('; ');
361
552
 
362
- const message = `Security scan detected ${allFindings.length} issue(s) in ${filePath || 'code'}: ${summary}`;
553
+ const message = `Security scan detected ${allFindings.length} issue(s) in ${filePath || (IS_BASH ? 'command' : 'code')}: ${summary}`;
363
554
 
364
555
  const reasoning = [
365
- `Scanned ${content.length} bytes of content being written to ${filePath || 'unknown file'}`,
556
+ IS_BASH
557
+ ? `Scanned Bash command for slopsquat/typosquat risk: "${command.slice(0, 100)}..."`
558
+ : `Scanned ${content.length} bytes of content being written to ${filePath || 'unknown file'}`,
366
559
  ...allFindings.map(f => `${f.category}/${f.id}: ${f.label}${f.match ? ` — matched: ${f.match.slice(0, 60)}` : ''}`),
367
560
  ];
368
561
 
369
562
  recordAuditEvent({
370
563
  toolName,
371
- toolInput: { file_path: filePath, content_length: content.length },
564
+ toolInput: { file_path: filePath, content_length: content.length, command: IS_BASH ? command : undefined },
372
565
  decision,
373
566
  gateId,
374
567
  message,
@@ -434,15 +627,72 @@ function scanGitDiff(diffContent) {
434
627
  };
435
628
  }
436
629
 
630
+ function buildThreatDefensePlaybook(scanResult = {}, options = {}) {
631
+ const findings = Array.isArray(scanResult.findings)
632
+ ? scanResult.findings
633
+ : (scanResult.securityScan && Array.isArray(scanResult.securityScan.findings) ? scanResult.securityScan.findings : []);
634
+ const critical = findings.filter((finding) => finding.severity === 'critical');
635
+ const high = findings.filter((finding) => finding.severity === 'high');
636
+ const categories = Array.from(new Set(findings.map((finding) => finding.category).filter(Boolean)));
637
+ const hasFindings = findings.length > 0;
638
+ const hasPatchEvidence = Boolean(options.patchEvidence || options.testEvidence || options.ciEvidence);
639
+
640
+ return {
641
+ name: 'thumbgate-ai-threat-defense-playbook',
642
+ status: critical.length > 0 ? 'block' : high.length > 0 ? 'remediate' : 'monitor',
643
+ phases: [
644
+ {
645
+ id: 'prepare',
646
+ action: 'harden-foundation',
647
+ evidence: ['gate templates enabled', 'protected files configured', 'rollback path documented'],
648
+ required: true,
649
+ },
650
+ {
651
+ id: 'scan-prioritize',
652
+ action: hasFindings ? 'prioritize detected security findings by severity and exploit surface' : 'keep posture scan active',
653
+ evidence: categories.length ? categories : ['clean scan'],
654
+ required: true,
655
+ },
656
+ {
657
+ id: 'remediate',
658
+ action: hasFindings ? 'patch, run focused tests, and re-scan before allowing risky agent actions' : 'no remediation required from current scan',
659
+ evidence: hasPatchEvidence ? ['patch evidence present'] : ['patch diff', 'focused test output', 'repeat scan'],
660
+ required: hasFindings,
661
+ },
662
+ {
663
+ id: 'monitor',
664
+ action: 'record audit event and keep continuous detection enabled for future tool calls',
665
+ evidence: ['audit trail event', 'gate stats', 'review checkpoint'],
666
+ required: true,
667
+ },
668
+ ],
669
+ priority: {
670
+ critical: critical.length,
671
+ high: high.length,
672
+ total: findings.length,
673
+ categories,
674
+ },
675
+ gateDecision: critical.length > 0 ? 'deny' : high.length > 0 ? 'warn' : 'allow',
676
+ nextActions: critical.length > 0
677
+ ? ['Block the action', 'Patch the critical finding', 'Run focused tests', 'Re-scan the diff before retry']
678
+ : high.length > 0
679
+ ? ['Warn the operator', 'Create a remediation task', 'Run focused tests', 'Monitor for repeat findings']
680
+ : ['Keep continuous scan enabled', 'Review checkpoint metrics after the next session'],
681
+ };
682
+ }
683
+
437
684
  // ---------------------------------------------------------------------------
438
685
  // Exports
439
686
  // ---------------------------------------------------------------------------
440
687
 
441
688
  module.exports = {
689
+ evaluateSlopsquatScan,
442
690
  VULN_PATTERNS,
443
691
  SUPPLY_CHAIN_PATTERNS,
444
692
  scanCode,
693
+ scanBadHostExposure,
445
694
  scanDependencyChange,
446
695
  evaluateSecurityScan,
447
696
  scanGitDiff,
697
+ buildThreatDefensePlaybook,
448
698
  };
@@ -26,6 +26,7 @@ const { resolveFeedbackDir } = require('./feedback-paths');
26
26
  const { createLesson, inferStructuredLesson } = require('./lesson-inference');
27
27
  const { buildStableId } = require('./conversation-context');
28
28
  const { ensureParentDir, readJsonl } = require('./fs-utils');
29
+ const { redactSecretsDeep } = require('./secret-redaction');
29
30
 
30
31
  const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir() || '';
31
32
  const SELF_DISTILL_RUNS_PATH = path.join(HOME, '.thumbgate', 'self-distill-runs.jsonl');
@@ -383,7 +384,8 @@ async function generateLlmLessons(conversationWindow, model) {
383
384
 
384
385
  function writeRunManifest(manifest) {
385
386
  ensureParentDir(SELF_DISTILL_RUNS_PATH);
386
- fs.appendFileSync(SELF_DISTILL_RUNS_PATH, JSON.stringify(manifest) + '\n');
387
+ // Redact secrets — the manifest embeds lesson triggers/actions distilled from conversation text.
388
+ fs.appendFileSync(SELF_DISTILL_RUNS_PATH, JSON.stringify(redactSecretsDeep(manifest)) + '\n');
387
389
  }
388
390
 
389
391
  function readRunManifests() {
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Self-Harness Optimizer
4
+ *
5
+ * Implements the continuous self-improving loop from the "Self-Harness" paradigm (arXiv:2606.09498):
6
+ * 1. Weakness Mining: Reads auto-promoted gates from repeated failures (.thumbgate/auto-promoted-gates.json)
7
+ * 2. Harness Proposal: Generates technical rules and injects them directly into system prompts (AGENTS.md & GEMINI.md)
8
+ * 3. Validation: Runs quick test suites to ensure prompt updates do not introduce regressions.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+
15
+ const ROOT = path.join(__dirname, '..');
16
+ const GATES_PATH = path.join(ROOT, '.thumbgate', 'auto-promoted-gates.json');
17
+ const AGENTS_MD_PATH = path.join(ROOT, 'AGENTS.md');
18
+ const GEMINI_MD_PATH = path.join(ROOT, 'GEMINI.md');
19
+
20
+ const RULE_SECTION_HEADER = '## 🛡️ Self-Harness Prevention Rules (Auto-Generated)';
21
+
22
+ function readGates() {
23
+ if (!fs.existsSync(GATES_PATH)) return [];
24
+ try {
25
+ const data = JSON.parse(fs.readFileSync(GATES_PATH, 'utf-8'));
26
+ return Array.isArray(data.gates) ? data.gates : [];
27
+ } catch (err) {
28
+ console.error('Failed to parse auto-promoted-gates.json:', err);
29
+ return [];
30
+ }
31
+ }
32
+
33
+ function generateRuleSection(gates) {
34
+ if (gates.length === 0) {
35
+ return `${RULE_SECTION_HEADER}\n\n- No active auto-generated prevention rules at this time.\n`;
36
+ }
37
+
38
+ const lines = [
39
+ RULE_SECTION_HEADER,
40
+ '',
41
+ '> [!IMPORTANT]',
42
+ '> The following rules were automatically derived from execution failures and thumbs-down feedback.',
43
+ '> You MUST follow these constraints strictly to prevent repeated errors.',
44
+ ''
45
+ ];
46
+
47
+ gates.forEach(gate => {
48
+ // Extract rule message or pattern
49
+ const ruleText = gate.message ? gate.message.replace('Automatically blocked due to repeated failures: ', '') : `NEVER match pattern: ${gate.pattern}`;
50
+ lines.push(`- **Rule [${gate.id}]**: ${ruleText}`);
51
+ });
52
+
53
+ lines.push('');
54
+ return lines.join('\n');
55
+ }
56
+
57
+ function updatePromptFile(filePath, newSection) {
58
+ if (!fs.existsSync(filePath)) {
59
+ console.warn(`Prompt file not found: ${filePath}`);
60
+ return null;
61
+ }
62
+
63
+ const content = fs.readFileSync(filePath, 'utf-8');
64
+ const backup = content;
65
+
66
+ let newContent;
67
+ const index = content.indexOf(RULE_SECTION_HEADER);
68
+ if (index !== -1) {
69
+ // Replace existing section till the end or next major section
70
+ const before = content.substring(0, index);
71
+ const remainder = content.substring(index);
72
+
73
+ // Find next header (e.g. '## ')
74
+ const nextHeaderIndex = remainder.slice(RULE_SECTION_HEADER.length).search(/\n## /);
75
+ if (nextHeaderIndex !== -1) {
76
+ const after = remainder.substring(RULE_SECTION_HEADER.length + nextHeaderIndex + 1);
77
+ newContent = before + newSection + after;
78
+ } else {
79
+ newContent = before + newSection;
80
+ }
81
+ } else {
82
+ // Append to the end of the file
83
+ newContent = content.trim() + '\n\n' + newSection;
84
+ }
85
+
86
+ fs.writeFileSync(filePath, newContent, 'utf-8');
87
+ return backup;
88
+ }
89
+
90
+ function validateAndRollback(backups) {
91
+ console.log('Running quick validation suite...');
92
+ try {
93
+ execSync('npm run verify:quick', { cwd: ROOT, stdio: 'inherit' });
94
+ console.log('✅ Validation succeeded! No regressions detected.');
95
+
96
+ // Auto-commit prompt updates to git if validation succeeds
97
+ try {
98
+ execSync('git add AGENTS.md GEMINI.md', { cwd: ROOT, stdio: 'ignore' });
99
+ execSync('git commit -m "chore(prompt): auto-update self-harness prevention rules [auto-commit]"', { cwd: ROOT, stdio: 'ignore' });
100
+ console.log('✅ Prompt updates auto-committed to git.');
101
+ } catch (gitErr) {
102
+ console.warn('⚠️ Git commit failed or no changes to commit:', gitErr.message);
103
+ }
104
+
105
+ return true;
106
+ } catch (err) {
107
+ console.error('❌ Validation failed! Reverting prompt changes.');
108
+ for (const [filePath, backupContent] of Object.entries(backups)) {
109
+ if (backupContent) {
110
+ fs.writeFileSync(filePath, backupContent, 'utf-8');
111
+ console.log(`Reverted: ${path.basename(filePath)}`);
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+ }
117
+
118
+ function main() {
119
+ console.log('Mining weaknesses from auto-promoted gates...');
120
+ const gates = readGates();
121
+ console.log(`Found ${gates.length} active gates.`);
122
+
123
+ const newSection = generateRuleSection(gates);
124
+ const backups = {};
125
+
126
+ console.log('Updating AGENTS.md...');
127
+ backups[AGENTS_MD_PATH] = updatePromptFile(AGENTS_MD_PATH, newSection);
128
+
129
+ console.log('Updating GEMINI.md...');
130
+ backups[GEMINI_MD_PATH] = updatePromptFile(GEMINI_MD_PATH, newSection);
131
+
132
+ const success = validateAndRollback(backups);
133
+ if (!success) {
134
+ process.exit(1);
135
+ }
136
+ console.log('Self-Harness optimization complete.');
137
+ }
138
+
139
+ if (require.main === module) {
140
+ main();
141
+ }