thumbgate 1.27.17 → 1.27.18

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -221
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +99 -229
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -145
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- const AWS_BLOCKS_DEPENDENCY_PATTERN = /(^|\/)@aws-blocks\/(?:blocks|[^/\s"']+)/;
8
-
9
- function toList(value) {
10
- if (Array.isArray(value)) return value.map(String).map((entry) => entry.trim()).filter(Boolean);
11
- if (typeof value === 'string') return value.split(',').map((entry) => entry.trim()).filter(Boolean);
12
- return [];
13
- }
14
-
15
- function normalizeText(value) {
16
- if (value == null) return '';
17
- if (typeof value === 'string') return value;
18
- try {
19
- return JSON.stringify(value);
20
- } catch {
21
- return String(value);
22
- }
23
- }
24
-
25
- function readPackageJson(projectDir) {
26
- try {
27
- return JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'));
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- function packageUsesAwsBlocks(pkg) {
34
- if (!pkg || typeof pkg !== 'object') return false;
35
- const dependencyText = JSON.stringify({
36
- dependencies: pkg.dependencies || {},
37
- devDependencies: pkg.devDependencies || {},
38
- peerDependencies: pkg.peerDependencies || {},
39
- optionalDependencies: pkg.optionalDependencies || {},
40
- });
41
- const scriptsText = JSON.stringify(pkg.scripts || {});
42
- return AWS_BLOCKS_DEPENDENCY_PATTERN.test(dependencyText) || /aws-blocks|blocks-app|cdk|sandbox/i.test(scriptsText);
43
- }
44
-
45
- function detectAwsBlocksProject(projectDir = process.cwd()) {
46
- const pkg = readPackageJson(projectDir);
47
- if (packageUsesAwsBlocks(pkg)) return true;
48
- return fs.existsSync(path.join(projectDir, 'aws-blocks', 'index.ts'))
49
- || fs.existsSync(path.join(projectDir, 'aws-blocks', 'index.js'))
50
- || fs.existsSync(path.join(projectDir, 'blocks.spec.json'));
51
- }
52
-
53
- function detectAction(input = {}) {
54
- const command = normalizeText(input.command || input.args || input.shell || input.toolInput);
55
- const toolName = normalizeText(input.toolName || input.tool || input.name);
56
- const code = normalizeText(input.code || input.file || input.diff || input.body);
57
- const combined = `${toolName}\n${command}\n${code}`;
58
- const lower = combined.toLowerCase();
59
-
60
- const signals = [];
61
- const add = (id, label, severity = 'medium') => signals.push({ id, label, severity });
62
-
63
- if (/\b(cdk\s+deploy|npm\s+run\s+deploy|pnpm\s+deploy|yarn\s+deploy|sst\s+deploy|blocks?\s+deploy)\b/i.test(combined)) {
64
- add('aws-blocks-production-deploy', 'production deploy from an AWS Blocks workflow', 'high');
65
- }
66
-
67
- if (/\b(cdk\s+destroy|npm\s+run\s+destroy|pnpm\s+destroy|yarn\s+destroy|sandbox\b.*--destroy|--destroy\b|aws\s+cloudformation\s+delete-stack)\b/i.test(combined)) {
68
- add('aws-blocks-destroy', 'destroy command can remove AWS resources created from local Blocks code', 'critical');
69
- }
70
-
71
- if (/\b(drop\s+(table|database|schema|index|column)|truncate\s+table|delete\s+from\s+[\w".-]+(?:\s*;|\s*$)|update\s+[\w".-]+\s+set\b(?![\s\S]{0,160}\bwhere\b))/i.test(combined)) {
72
- add('destructive-sql-or-ddl', 'destructive or unscoped SQL mutation', 'critical');
73
- }
74
-
75
- if (/\b(aws\s+dynamodb\s+delete-table|aws\s+rds\s+delete-db-instance|aws\s+s3\s+rm\b[\s\S]*--recursive|aws\s+lambda\s+delete-function|aws\s+bedrock-agent\s+delete-|aws\s+bedrock-agent-runtime\b)/i.test(combined)) {
76
- add('destructive-aws-cli', 'destructive AWS CLI or Bedrock agent action', 'critical');
77
- }
78
-
79
- if (/\b(gcloud|aws)\b[\s\S]{0,220}\b(add-iam-policy-binding|put-role-policy|attach-role-policy|create-policy-version)\b/i.test(combined)
80
- || /\b(AdministratorAccess|roles\/owner|iam:PassRole|serviceAccountTokenCreator)\b/i.test(combined)) {
81
- add('iam-escalation', 'agent session attempts to grant broad IAM authority', 'critical');
82
- }
83
-
84
- if (/\b(new\s+Agent\s*\(|@aws-blocks\/(?:agent|blocks)|\bAgent\b[\s\S]{0,120}\btools?\b)/.test(combined)) {
85
- add('blocks-agent-tool-call', 'AWS Blocks Agent or Bedrock-style tool action needs a tool boundary', 'medium');
86
- }
87
-
88
- if (/\b(npm\s+run\s+dev|npm\s+start|pnpm\s+dev|yarn\s+dev|create-blocks-app)\b/i.test(combined)) {
89
- add('local-dev', 'local AWS Blocks development loop', 'low');
90
- }
91
-
92
- return {
93
- command,
94
- toolName,
95
- code,
96
- signals,
97
- highRisk: signals.some((signal) => ['high', 'critical'].includes(signal.severity)),
98
- localOnly: signals.some((signal) => signal.id === 'local-dev') && signals.every((signal) => signal.severity === 'low'),
99
- lower,
100
- };
101
- }
102
-
103
- function evaluateAwsBlocksAction(input = {}) {
104
- const projectDir = input.projectDir || input.cwd || process.cwd();
105
- const projectUsesAwsBlocks = Boolean(
106
- input.projectUsesAwsBlocks
107
- || input.awsBlocks
108
- || input.blocksProject
109
- || detectAwsBlocksProject(projectDir)
110
- );
111
- const evidence = new Set(toList(input.evidence || input.proof || input.receipts));
112
- const action = detectAction(input);
113
- const requiredEvidence = [];
114
-
115
- const requireEvidence = (id, label) => {
116
- if (!evidence.has(id)) requiredEvidence.push({ id, label });
117
- };
118
-
119
- if (projectUsesAwsBlocks && action.signals.some((signal) => signal.id === 'aws-blocks-production-deploy')) {
120
- requireEvidence('local-tests-pass', 'local AWS Blocks tests pass against local implementations');
121
- requireEvidence('cdk-diff-reviewed', 'CDK diff or synthesized CloudFormation change set reviewed');
122
- requireEvidence('cost-blast-radius-reviewed', 'AWS cost and resource blast radius reviewed');
123
- }
124
-
125
- if (action.signals.some((signal) => signal.id === 'aws-blocks-destroy')) {
126
- requireEvidence('resource-inventory-exported', 'resource inventory exported before destroy');
127
- requireEvidence('human-destroy-approval', 'named human approval for destroy');
128
- }
129
-
130
- if (action.signals.some((signal) => signal.id === 'destructive-sql-or-ddl')) {
131
- requireEvidence('backup-or-rollback-ready', 'backup, rollback, or restore point exists');
132
- requireEvidence('bounded-row-count-reviewed', 'row/table impact was previewed before mutation');
133
- }
134
-
135
- if (action.signals.some((signal) => signal.id === 'destructive-aws-cli')) {
136
- requireEvidence('aws-account-and-region-confirmed', 'target AWS account and region confirmed');
137
- requireEvidence('rollback-plan-attached', 'rollback or recovery plan attached');
138
- }
139
-
140
- if (action.signals.some((signal) => signal.id === 'iam-escalation')) {
141
- requireEvidence('least-privilege-review', 'least-privilege review completed');
142
- requireEvidence('security-owner-approval', 'security owner approval captured');
143
- }
144
-
145
- if (action.signals.some((signal) => signal.id === 'blocks-agent-tool-call')) {
146
- requireEvidence('agent-tool-allowlist', 'agent tool allowlist and data boundary declared');
147
- }
148
-
149
- const shouldBlock = projectUsesAwsBlocks && requiredEvidence.length > 0;
150
- const status = shouldBlock ? 'blocked' : (action.highRisk ? 'needs-review' : 'allowed');
151
-
152
- return {
153
- name: 'thumbgate-aws-blocks-guardrails',
154
- status,
155
- projectUsesAwsBlocks,
156
- signals: action.signals,
157
- requiredEvidence,
158
- enforcementBoundary: 'local AWS Blocks confidence must not automatically become production AWS authority',
159
- gates: [
160
- 'allow local AWS Blocks dev loops without AWS credentials',
161
- 'require local test proof plus CDK diff before production deploy',
162
- 'block destroy and destructive data mutations until backup, blast-radius, and human approval evidence exists',
163
- 'block IAM escalation and Bedrock/Agent tool expansion until an owner approves the tool boundary',
164
- 'write a ThumbGate receipt for every allowed high-risk cloud action',
165
- ],
166
- nextActions: shouldBlock
167
- ? requiredEvidence.map((item) => `Provide ${item.id}: ${item.label}`)
168
- : ['Record an allow receipt when this action touches real AWS resources'],
169
- };
170
- }
171
-
172
- function buildAwsBlocksHardeningOffer(input = {}) {
173
- const workflow = String(input.workflow || input.name || 'AWS Blocks backend').trim();
174
- const buyer = String(input.buyer || input.owner || 'platform owner').trim();
175
- return {
176
- name: 'thumbgate-aws-blocks-hardening-offer',
177
- status: 'ready-for-positioning',
178
- buyer,
179
- workflow,
180
- headline: 'AWS Blocks helps agents build the backend. ThumbGate stops them before unsafe cloud actions run.',
181
- offer: 'AWS Blocks Agent Safety Review',
182
- diagnosticPrice: '$499',
183
- proofPlan: [
184
- 'map one AWS Blocks local-to-cloud workflow',
185
- 'install ThumbGate against the agent running that workflow',
186
- 'add gates for deploy, destroy, data mutation, IAM, Bedrock Agent, and cost-blast-radius actions',
187
- 'produce a receipt showing the first blocked repeat and the evidence required to allow it',
188
- ],
189
- cta: 'Send one AWS Blocks workflow that is about to deploy, mutate data, or call Bedrock tools.',
190
- };
191
- }
192
-
193
- function parseArgs(argv = process.argv.slice(2)) {
194
- const args = {};
195
- for (const arg of argv) {
196
- if (arg === '--json') args.json = true;
197
- else if (arg === '--aws-blocks' || arg === '--project-uses-aws-blocks') args.projectUsesAwsBlocks = true;
198
- else if (arg.startsWith('--command=')) args.command = arg.slice('--command='.length);
199
- else if (arg.startsWith('--tool=')) args.toolName = arg.slice('--tool='.length);
200
- else if (arg.startsWith('--code=')) args.code = arg.slice('--code='.length);
201
- else if (arg.startsWith('--cwd=')) args.projectDir = arg.slice('--cwd='.length);
202
- else if (arg.startsWith('--evidence=')) args.evidence = arg.slice('--evidence='.length);
203
- else if (arg.startsWith('--workflow=')) args.workflow = arg.slice('--workflow='.length);
204
- else if (arg.startsWith('--buyer=')) args.buyer = arg.slice('--buyer='.length);
205
- else if (arg === 'offer') args.commandName = 'offer';
206
- }
207
- return args;
208
- }
209
-
210
- function runCli(args = parseArgs()) {
211
- const report = args.commandName === 'offer'
212
- ? buildAwsBlocksHardeningOffer(args)
213
- : evaluateAwsBlocksAction(args);
214
- if (args.json) console.log(JSON.stringify(report, null, 2));
215
- else {
216
- console.log(`${report.name}: ${report.status}`);
217
- for (const action of report.nextActions || report.proofPlan || []) console.log(`- ${action}`);
218
- }
219
- }
220
-
221
- if (require.main === module) runCli();
222
-
223
- module.exports = {
224
- detectAwsBlocksProject,
225
- detectAction,
226
- evaluateAwsBlocksAction,
227
- buildAwsBlocksHardeningOffer,
228
- };
@@ -1,130 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
-
7
- const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', 'config', 'gate-classifier-routing.json');
8
-
9
- function clamp01(value, fallback = 0) {
10
- const number = Number(value);
11
- if (!Number.isFinite(number)) return fallback;
12
- return Math.min(1, Math.max(0, number));
13
- }
14
-
15
- function riskRank(risk) {
16
- const normalized = String(risk || 'medium').toLowerCase();
17
- if (['critical', 'block', 'regulated'].includes(normalized)) return 4;
18
- if (['high', 'dangerous'].includes(normalized)) return 3;
19
- if (['medium', 'warn'].includes(normalized)) return 2;
20
- return 1;
21
- }
22
-
23
- function loadClassifierRoutingConfig(configPath = DEFAULT_CONFIG_PATH) {
24
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
- }
26
-
27
- function lane(config, laneName, reason, input, overrides = {}) {
28
- const laneConfig = (config.lanes && config.lanes[laneName]) || {};
29
- return {
30
- lane: laneName,
31
- reason,
32
- requiresEvidence: Boolean(laneConfig.requiresEvidence || overrides.requiresEvidence),
33
- cloudAllowed: Boolean(laneConfig.cloudAllowed),
34
- maxLatencyMs: laneConfig.maxLatencyMs,
35
- risk: String(input.risk || 'medium').toLowerCase(),
36
- ambiguity: clamp01(input.ambiguity),
37
- };
38
- }
39
-
40
- function routeClassifier(input = {}, config = loadClassifierRoutingConfig()) {
41
- const thresholds = config.thresholds || {};
42
- const labelCount = Number(input.labelCount || input.examples || 0);
43
- const latencyBudgetMs = Number(input.latencyBudgetMs || 0);
44
- const ambiguity = clamp01(input.ambiguity);
45
- const risk = riskRank(input.risk);
46
- const largeBatch = Number(input.batchRows || 0) >= Number(thresholds.largeBatchRows || 50);
47
- const privacySensitive = Boolean(input.privacySensitive || input.containsSecrets || input.customerData);
48
- const allowCloud = Boolean(input.allowCloud);
49
-
50
- if (input.hasHardRule || input.exactPolicyMatch) {
51
- return lane(config, 'deterministic', 'exact hard rule or policy match; do not spend model tokens', input);
52
- }
53
-
54
- if (input.semanticCacheHit || input.equivalentRepeat) {
55
- return lane(config, 'semantic_cache', 'semantically equivalent repeat; reuse the proven prior decision without a model call', input);
56
- }
57
-
58
- if (input.rubricFailed || input.missingEvidence || input.completionClaimWithoutProof) {
59
- return lane(config, 'rubric_gate', 'rubric or completion evidence failed; block done claims until proof exists', input);
60
- }
61
-
62
- if (input.structuredDataset && (input.missingProvenance || input.missingSources)) {
63
- return lane(config, 'rubric_gate', 'structured data claim is missing source provenance', input);
64
- }
65
-
66
- if (privacySensitive && !allowCloud && risk >= 3 && ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
67
- return lane(config, 'human_review', 'private high-risk ambiguous action; keep data local and require approval', input);
68
- }
69
-
70
- if (labelCount >= Number(thresholds.classicalMinExamples || 40) && (largeBatch || latencyBudgetMs <= Number(thresholds.lowLatencyBudgetMs || 300)) && ambiguity < Number(thresholds.mediumAmbiguity || 0.35)) {
71
- return lane(config, 'local_classical', 'enough examples and low ambiguity; use cheap local classification', input);
72
- }
73
-
74
- if (risk >= 3 && ambiguity >= Number(thresholds.highRiskAmbiguity || 0.65)) {
75
- if (allowCloud && latencyBudgetMs >= Number(thresholds.llmMinLatencyBudgetMs || 2000)) {
76
- return lane(config, 'llm_judge', 'high-risk semantic ambiguity; use a budget-capped LLM judge with evidence', input);
77
- }
78
- return lane(config, 'human_review', 'high-risk ambiguity without approved cloud/budget route', input);
79
- }
80
-
81
- if (labelCount < Number(thresholds.classicalMinExamples || 40) || ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
82
- return lane(config, 'local_semantic', 'sparse labels or fuzzy intent; use local semantic recall before any LLM', input);
83
- }
84
-
85
- return lane(config, config.defaultLane || 'local_classical', 'default local route for routine gate classification', input);
86
- }
87
-
88
- function explainClassifierRoute(input = {}, config = loadClassifierRoutingConfig()) {
89
- const decision = routeClassifier(input, config);
90
- const laneConfig = (config.lanes && config.lanes[decision.lane]) || {};
91
- return {
92
- ...decision,
93
- description: laneConfig.description || '',
94
- useFor: laneConfig.useFor || [],
95
- };
96
- }
97
-
98
- function parseArgs(argv) {
99
- const input = {};
100
- for (const arg of argv) {
101
- if (arg === '--hard-rule') input.hasHardRule = true;
102
- else if (arg === '--privacy-sensitive') input.privacySensitive = true;
103
- else if (arg === '--allow-cloud') input.allowCloud = true;
104
- else if (arg === '--customer-data') input.customerData = true;
105
- else if (arg === '--semantic-cache-hit') input.semanticCacheHit = true;
106
- else if (arg === '--equivalent-repeat') input.equivalentRepeat = true;
107
- else if (arg === '--rubric-failed') input.rubricFailed = true;
108
- else if (arg === '--missing-evidence') input.missingEvidence = true;
109
- else if (arg === '--structured-dataset') input.structuredDataset = true;
110
- else if (arg === '--missing-provenance') input.missingProvenance = true;
111
- else if (arg.startsWith('--risk=')) input.risk = arg.slice('--risk='.length);
112
- else if (arg.startsWith('--ambiguity=')) input.ambiguity = Number(arg.slice('--ambiguity='.length));
113
- else if (arg.startsWith('--labels=')) input.labelCount = Number(arg.slice('--labels='.length));
114
- else if (arg.startsWith('--latency-ms=')) input.latencyBudgetMs = Number(arg.slice('--latency-ms='.length));
115
- else if (arg.startsWith('--batch-rows=')) input.batchRows = Number(arg.slice('--batch-rows='.length));
116
- }
117
- return input;
118
- }
119
-
120
- module.exports = {
121
- DEFAULT_CONFIG_PATH,
122
- explainClassifierRoute,
123
- loadClassifierRoutingConfig,
124
- routeClassifier,
125
- };
126
-
127
- if (require.main === module) {
128
- const decision = explainClassifierRoute(parseArgs(process.argv.slice(2)));
129
- process.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
130
- }
@@ -1,332 +0,0 @@
1
- 'use strict';
2
-
3
- // scripts/dashboard-chat.js
4
- // -----------------------------------------------------------------------------
5
- // "Chat with your data" — the dashboard chat backend. Local-first RAG over
6
- // this install's ThumbGate data (lessons, raw feedback memories via LanceDB
7
- // vectors, receipts, gate stats). Retrieval is local (lesson search + optional
8
- // vector-store.searchSimilar). Generation uses your configured LLM: a local
9
- // OpenAI-compatible endpoint first, then Gemini or Perplexity when explicitly
10
- // configured.
11
- //
12
- // Dialogflow/Google is not the dashboard chatbot brain. It remains an optional
13
- // guard-adapter path for buyers who already run their own Google agent tenancy.
14
- // -----------------------------------------------------------------------------
15
-
16
- const path = require('path');
17
-
18
- const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models';
19
- const PERPLEXITY_ENDPOINT = 'https://api.perplexity.ai/chat/completions';
20
- const DEFAULT_MODEL = 'gemini-2.5-flash';
21
- const MAX_QUESTION_CHARS = 2000;
22
- const MAX_CONTEXT_LESSONS = 8;
23
-
24
- // Allowlist the model so a user-supplied `model` cannot route the call to an
25
- // arbitrary / unexpected (or more expensive) endpoint. Anything not on the list
26
- // falls back to the default.
27
- const ALLOWED_MODELS = new Set([
28
- 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro',
29
- 'gemini-2.0-flash', 'gemini-2.0-flash-lite',
30
- 'gemini-flash-latest', 'gemini-flash-lite-latest', 'gemini-pro-latest',
31
- ]);
32
-
33
- function resolveModel(requested) {
34
- const r = String(requested || '').trim();
35
- if (r && ALLOWED_MODELS.has(r)) return r;
36
- const envModel = String(process.env.THUMBGATE_GEMINI_MODEL || '').trim();
37
- if (envModel && ALLOWED_MODELS.has(envModel)) return envModel;
38
- return DEFAULT_MODEL;
39
- }
40
-
41
- function resolveApiKey(opts = {}) {
42
- let key = '';
43
- if (Object.hasOwn(opts, 'apiKey')) {
44
- key = opts.apiKey || '';
45
- } else {
46
- key = opts.apiKey || process.env.GEMINI_API_KEY || process.env.THUMBGATE_GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.PERPLEXITY_API_KEY || process.env.THUMBGATE_PERPLEXITY_API_KEY || '';
47
- }
48
- if (!key) return '';
49
- return key.trim().replace(/^["']|["']$/g, '');
50
- }
51
-
52
- function debugChatFallback(label, err) {
53
- if (process.env.THUMBGATE_DEBUG_CHAT !== '1') return;
54
- const detail = err?.message ? err.message : String(err);
55
- console.warn(`[dashboard-chat] ${label}: ${detail}`);
56
- }
57
-
58
- function loadLessonSearcher() {
59
- try {
60
- return require(path.join(__dirname, 'lesson-search')).searchLessons;
61
- } catch (err) {
62
- debugChatFallback('lesson search unavailable', err);
63
- return null;
64
- }
65
- }
66
-
67
- function lessonToContextItem(lesson) {
68
- return {
69
- id: lesson.id,
70
- signal: lesson.signal || lesson.feedback || '',
71
- title: (lesson.title || '').replace(/^(?:MISTAKE|SUCCESS):\s*/i, '').slice(0, 160),
72
- content: String(lesson.content || lesson.context || '').replace(/\s+/g, ' ').trim().slice(0, 600),
73
- tags: lesson.tags || [],
74
- source: 'lessons',
75
- };
76
- }
77
-
78
- function vectorMatchToContextItem(match, index) {
79
- return {
80
- id: match.id || `vec-${index}`,
81
- signal: match.signal || '',
82
- title: String(match.context || match.text || '').slice(0, 100),
83
- content: match.text || match.context || '',
84
- tags: match.tags ? String(match.tags).split(',').filter(Boolean) : [],
85
- source: 'lancedb-vector',
86
- };
87
- }
88
-
89
- function dedupeContextItems(items, limit = MAX_CONTEXT_LESSONS + 3) {
90
- const seen = new Set();
91
- return items.filter((item) => {
92
- if (!(item.content || item.title)) return false;
93
- const key = item.id || item.content.slice(0, 80);
94
- if (seen.has(key)) return false;
95
- seen.add(key);
96
- return true;
97
- }).slice(0, limit);
98
- }
99
-
100
- function retrieveLessonContext(question, opts = {}) {
101
- const searchLessons = loadLessonSearcher();
102
- if (!searchLessons) return [];
103
- try {
104
- const res = searchLessons(String(question || ''), {
105
- limit: MAX_CONTEXT_LESSONS,
106
- feedbackDir: opts.feedbackDir,
107
- });
108
- const rows = res?.results || res?.lessons || [];
109
- return rows.slice(0, MAX_CONTEXT_LESSONS).map(lessonToContextItem);
110
- } catch (err) {
111
- debugChatFallback('lesson retrieval failed', err);
112
- return [];
113
- }
114
- }
115
-
116
- async function retrieveVectorContext(question, opts = {}) {
117
- if (opts.useVectorSearch === false) return [];
118
- try {
119
- const vectorStore = require(path.join(__dirname, 'vector-store'));
120
- const vecResults = vectorStore.searchSimilar
121
- ? await vectorStore.searchSimilar(String(question || ''), opts.vectorLimit || 4)
122
- : [];
123
- return vecResults
124
- .filter((match) => match?.text)
125
- .map(vectorMatchToContextItem);
126
- } catch (err) {
127
- debugChatFallback('vector retrieval failed', err);
128
- return [];
129
- }
130
- }
131
-
132
- // Live numeric snapshot from gate-stats + feedback analyzer. Always injected
133
- // (~120 tokens) so the LLM can answer count/quantity questions like
134
- // "how many were blocked today" without hallucinating.
135
- function retrieveMetricsContext() {
136
- const snapshot = {};
137
- try {
138
- const gs = require(path.join(__dirname, 'gate-stats'));
139
- const s = gs.calculateStats();
140
- snapshot.gates = {
141
- total: s.totalGates,
142
- blockRules: s.blockGates,
143
- warnRules: s.warnGates,
144
- totalBlockedEvents: s.totalBlocked,
145
- totalWarnedEvents: s.totalWarned,
146
- estimatedHoursSaved: s.estimatedHoursSaved,
147
- topBlockedTrigger: s.topBlocked?.trigger || null,
148
- topBlockedOccurrences: s.topBlocked?.occurrences || 0,
149
- };
150
- } catch (err) {
151
- debugChatFallback('gate-stats unavailable', err);
152
- }
153
- try {
154
- const fl = require(path.join(__dirname, 'feedback-loop'));
155
- const a = fl.analyzeFeedback();
156
- snapshot.feedback = {
157
- total: a.total,
158
- positive: a.totalPositive,
159
- negative: a.totalNegative,
160
- approvalRate: a.approvalRate,
161
- last7d: a.windows?.['7d'],
162
- last30d: a.windows?.['30d'],
163
- trend: a.trend,
164
- };
165
- } catch (err) {
166
- debugChatFallback('feedback-loop analyze unavailable', err);
167
- }
168
- return snapshot;
169
- }
170
-
171
- // Retrieve relevant stored lessons and optional raw feedback vector matches.
172
- async function retrieveContext(question, opts = {}) {
173
- const lessons = retrieveLessonContext(question, opts);
174
- const vectors = await retrieveVectorContext(question, opts);
175
- return dedupeContextItems([...lessons, ...vectors]);
176
- }
177
-
178
- // Build a grounded RAG prompt. Pure function (testable).
179
- function buildChatPrompt(question, lessons, metrics) {
180
- const q = String(question || '').slice(0, MAX_QUESTION_CHARS).trim();
181
- const context = (lessons || []).map((l, i) => {
182
- const mark = /pos|up/i.test(l.signal) ? 'WORKED' : (/neg|down/i.test(l.signal) ? 'MISTAKE' : 'NOTE');
183
- const tags = (l.tags || []).length ? ` [tags: ${l.tags.join(', ')}]` : '';
184
- return `(${i + 1}) [${mark}] ${l.title || ''}${tags}\n ${l.content}`;
185
- }).join('\n');
186
-
187
- const metricsBlock = metrics && Object.keys(metrics).length
188
- ? `\n=== Live numeric snapshot (your data, current) ===\n${JSON.stringify(metrics, null, 2)}\n`
189
- : '';
190
-
191
- const system = [
192
- 'You are ThumbGate\'s "chat with your data" assistant. Answer the user\'s question',
193
- 'using ONLY the captured lessons and the live numeric snapshot below (this team\'s real data).',
194
- 'For count/quantity questions ("how many X", "what\'s our rate"), use the numeric snapshot.',
195
- 'For pattern/why questions, cite the lesson numbers like [1], [3].',
196
- 'If neither source contains the answer, say so plainly — do not invent facts.',
197
- ].join(' ');
198
-
199
- return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n${metricsBlock}\n=== Question ===\n${q}`;
200
- }
201
-
202
- // Parse the Gemini generateContent response into plain text. Pure (testable).
203
- function parseGeminiAnswer(body) {
204
- const parts = body?.candidates?.[0]?.content?.parts;
205
- if (!Array.isArray(parts)) return '';
206
- return parts.map((p) => (p && typeof p.text === 'string' ? p.text : '')).join('').trim();
207
- }
208
-
209
- function buildOpenAiChatPayload(prompt, model) {
210
- return JSON.stringify({
211
- model,
212
- messages: [{ role: 'user', content: prompt }],
213
- temperature: 0.2,
214
- max_tokens: 1024,
215
- });
216
- }
217
-
218
- function parseOpenAiChatAnswer(json) {
219
- return json?.choices?.[0]?.message?.content || '';
220
- }
221
-
222
- function parseModelError(json, status) {
223
- return json?.error?.message ? String(json.error.message).split('\n')[0] : `HTTP ${status}`;
224
- }
225
-
226
- function trimTrailingSlashes(value) {
227
- let text = String(value || '');
228
- while (text.endsWith('/')) {
229
- text = text.slice(0, -1);
230
- }
231
- return text;
232
- }
233
-
234
- async function callLocalOpenAiEndpoint({ endpoint, apiKey, model, prompt, fetchImpl, sources }) {
235
- const url = endpoint.includes('/chat/completions')
236
- ? endpoint
237
- : `${trimTrailingSlashes(endpoint)}/chat/completions`;
238
- const res = await fetchImpl(url, {
239
- method: 'POST',
240
- headers: {
241
- 'content-type': 'application/json',
242
- 'Authorization': `Bearer ${apiKey || 'local'}`
243
- },
244
- body: buildOpenAiChatPayload(prompt, model),
245
- });
246
- const json = await res.json().catch(() => ({}));
247
- if (!res.ok) {
248
- return { ok: false, error: 'local_llm_error', status: res.status, message: parseModelError(json, res.status), sources };
249
- }
250
- const answer = parseOpenAiChatAnswer(json);
251
- return { ok: true, answer: answer.trim() || '(no answer returned)', sources, model: json.model || model };
252
- }
253
-
254
- async function callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources }) {
255
- const res = await fetchImpl(PERPLEXITY_ENDPOINT, {
256
- method: 'POST',
257
- headers: { 'content-type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
258
- body: buildOpenAiChatPayload(prompt, 'sonar'),
259
- });
260
- const json = await res.json().catch(() => ({}));
261
- if (!res.ok) {
262
- return { ok: false, error: 'perplexity_error', status: res.status, message: parseModelError(json, res.status), sources };
263
- }
264
- const answer = parseOpenAiChatAnswer(json);
265
- return { ok: true, answer: answer.trim() || '(no answer returned)', sources, model: json.model || 'perplexity-hybrid' };
266
- }
267
-
268
- async function callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources }) {
269
- const res = await fetchImpl(`${GEMINI_ENDPOINT}/${encodeURIComponent(model)}:generateContent`, {
270
- method: 'POST',
271
- headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
272
- body: JSON.stringify({
273
- contents: [{ role: 'user', parts: [{ text: prompt }] }],
274
- generationConfig: { temperature: 0.2, maxOutputTokens: 1024 },
275
- }),
276
- });
277
- const json = await res.json().catch(() => ({}));
278
- if (!res.ok) {
279
- return { ok: false, error: 'gemini_error', status: res.status, message: parseModelError(json, res.status), sources };
280
- }
281
- const answer = parseGeminiAnswer(json);
282
- return { ok: true, answer: answer || '(no answer returned)', sources, model: json.modelVersion || model };
283
- }
284
-
285
- // Answer a question grounded in this install's lessons. Returns
286
- // { ok, answer, sources, model } or { ok:false, error, ... }.
287
- async function answerDataQuestion(question, opts = {}) {
288
- const q = String(question || '').trim();
289
- if (!q) return { ok: false, error: 'empty_question', message: 'Ask a question about your data.' };
290
- if (q.length > MAX_QUESTION_CHARS) {
291
- return { ok: false, error: 'question_too_long', message: `Question exceeds ${MAX_QUESTION_CHARS} characters.` };
292
- }
293
-
294
- const localEndpoint = opts.localEndpoint || process.env.THUMBGATE_LOCAL_LLM_ENDPOINT || '';
295
- const localModel = opts.localModel || process.env.THUMBGATE_LOCAL_LLM_MODEL || 'llama3';
296
- const apiKey = resolveApiKey(opts);
297
- const lessons = await retrieveContext(q, opts);
298
- const sources = lessons.map((l) => ({ id: l.id, title: l.title, signal: l.signal }));
299
-
300
- if (!apiKey && !localEndpoint) {
301
- return {
302
- ok: false,
303
- error: 'no_api_key',
304
- message: 'Chat is not configured. Set a valid GEMINI_API_KEY, PERPLEXITY_API_KEY, or THUMBGATE_LOCAL_LLM_ENDPOINT in the project .env.',
305
- sources,
306
- };
307
- }
308
-
309
- const model = resolveModel(opts.model);
310
- const metrics = retrieveMetricsContext();
311
- const prompt = buildChatPrompt(q, lessons, metrics);
312
- const fetchImpl = opts.fetch || globalThis.fetch;
313
- const isPerplexity = apiKey && (apiKey.startsWith('pplx-') || apiKey.includes('perplexity'));
314
-
315
- try {
316
- if (localEndpoint) return await callLocalOpenAiEndpoint({ endpoint: localEndpoint, apiKey, model: localModel, prompt, fetchImpl, sources });
317
- if (isPerplexity) return await callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources });
318
- return await callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources });
319
- } catch (err) {
320
- const safeMessage = (err && err.message) ? String(err.message).split('\n')[0].slice(0, 100) : 'An unexpected error occurred.';
321
- return { ok: false, error: 'network', message: safeMessage, sources };
322
- }
323
- }
324
-
325
- module.exports = {
326
- answerDataQuestion,
327
- buildChatPrompt,
328
- parseGeminiAnswer,
329
- retrieveContext,
330
- DEFAULT_MODEL,
331
- MAX_QUESTION_CHARS,
332
- };