thumbgate 1.27.17 → 1.27.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +85 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +33 -5
- package/adapters/chatgpt/openapi.yaml +1707 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +63 -54
- package/bin/postinstall.js +23 -14
- package/config/post-deploy-marketing-pages.json +19 -0
- package/config/tessl-tiles.json +1 -3
- package/package.json +99 -229
- package/public/guide.html +0 -4
- package/public/index.html +28 -49
- package/public/learn.html +0 -8
- package/public/numbers.html +2 -2
- package/scripts/agent-readiness.js +0 -147
- package/scripts/bot-detection.js +61 -3
- package/scripts/cli-feedback.js +4 -14
- package/scripts/cli-schema.js +7 -0
- package/scripts/durability/step.js +3 -3
- package/scripts/feedback-loop.js +10 -32
- package/scripts/gates-engine.js +72 -1
- package/scripts/hook-runtime.js +2 -2
- package/scripts/license.js +10 -9
- package/scripts/llm-client.js +44 -260
- package/scripts/mcp-config.js +7 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/seo-gsd.js +0 -5
- package/src/api/server.js +111 -35
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/bot-detector.js +0 -50
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- 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
|
-
};
|
package/scripts/bot-detector.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const BOT_PATTERNS = [
|
|
4
|
-
/bot/i, /crawl/i, /spider/i, /slurp/i, /mediapartners/i,
|
|
5
|
-
/Googlebot/i, /Bingbot/i, /DuckDuckBot/i, /Baiduspider/i,
|
|
6
|
-
/YandexBot/i, /facebookexternalhit/i, /Twitterbot/i,
|
|
7
|
-
/LinkedInBot/i, /WhatsApp/i, /Discordbot/i, /TelegramBot/i,
|
|
8
|
-
/Applebot/i, /PetalBot/i, /SemrushBot/i, /AhrefsBot/i,
|
|
9
|
-
/MJ12bot/i, /DotBot/i, /Bytespider/i,
|
|
10
|
-
/GPTBot/i, /ChatGPT/i, /Claude-SearchBot/i, /Anthropic/i, /Perplexity/i,
|
|
11
|
-
/Google-Extended/i, /CCBot/i, /cohere-ai/i,
|
|
12
|
-
/HeadlessChrome/i, /PhantomJS/i, /Puppeteer/i, /Playwright/i,
|
|
13
|
-
/python-requests/i, /node-fetch/i, /wget/i,
|
|
14
|
-
/Scrapy/i, /HttpClient/i, /Go-http-client/i,
|
|
15
|
-
/UptimeRobot/i, /Pingdom/i, /StatusCake/i,
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const OWNER_EMAILS = ['iganapolsky@gmail.com', 'ig5973700@gmail.com'];
|
|
19
|
-
|
|
20
|
-
function classifyVisitor(req) {
|
|
21
|
-
const ua = (req.headers && req.headers['user-agent']) || '';
|
|
22
|
-
const email = req.email || (req.query && req.query.email) || '';
|
|
23
|
-
|
|
24
|
-
for (const pattern of BOT_PATTERNS) {
|
|
25
|
-
if (pattern.test(ua)) {
|
|
26
|
-
return { type: 'bot', reason: `UA matches: ${pattern}`, userAgent: ua };
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (!ua || ua.length < 10) {
|
|
30
|
-
return { type: 'bot', reason: 'Empty or short user-agent', userAgent: ua };
|
|
31
|
-
}
|
|
32
|
-
for (const ownerEmail of OWNER_EMAILS) {
|
|
33
|
-
if (email && email.toLowerCase().includes(ownerEmail.toLowerCase())) {
|
|
34
|
-
return { type: 'owner', reason: `Email matches: ${ownerEmail}`, userAgent: ua };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return { type: 'real_user', reason: 'No bot pattern matched', userAgent: ua };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function shouldExcludeFromAnalytics(req) {
|
|
41
|
-
const classification = req.visitorClass || classifyVisitor(req);
|
|
42
|
-
return classification.type === 'bot';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function botFilterMiddleware(req, res, next) {
|
|
46
|
-
req.visitorClass = classifyVisitor(req);
|
|
47
|
-
next();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = { classifyVisitor, botFilterMiddleware, shouldExcludeFromAnalytics, BOT_PATTERNS, OWNER_EMAILS };
|
|
@@ -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
|
-
}
|