thumbgate 1.27.20 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +124 -129
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +18 -33
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +95 -78
- package/bin/postinstall.js +1 -1
- package/config/entitlement-public-keys.json +6 -0
- package/config/gates/default.json +3 -3
- package/config/github-about.json +2 -5
- package/config/merge-quality-checks.json +3 -0
- package/config/post-deploy-marketing-pages.json +1 -1
- package/hooks/hooks.json +38 -0
- package/package.json +28 -10
- package/public/about.html +1 -4
- package/public/agent-manager.html +6 -6
- package/public/agents-cost-savings.html +3 -3
- package/public/ai-malpractice-prevention.html +1 -1
- package/public/blog.html +12 -9
- package/public/chatgpt-app.html +3 -3
- package/public/codex-enterprise.html +3 -3
- package/public/codex-plugin.html +5 -5
- package/public/compare.html +10 -10
- package/public/dashboard.html +1 -1
- package/public/diagnostic.html +23 -1
- package/public/federal.html +2 -2
- package/public/guide.html +14 -14
- package/public/index.html +202 -220
- package/public/install.html +14 -14
- package/public/learn.html +4 -17
- package/public/numbers.html +2 -2
- package/public/partner-intake.html +198 -0
- package/public/pricing.html +61 -31
- package/public/pro.html +2 -2
- package/scripts/agent-reward-model.js +13 -0
- package/scripts/bayes-optimal-gate.js +6 -1
- package/scripts/billing.js +815 -137
- package/scripts/checkout-attribution-reference.js +85 -0
- package/scripts/claude-feedback-sync.js +23 -6
- package/scripts/cli-feedback.js +33 -8
- package/scripts/commercial-offer.js +3 -3
- package/scripts/entitlement.js +250 -0
- package/scripts/export-databricks-bundle.js +5 -0
- package/scripts/export-dpo-pairs.js +6 -0
- package/scripts/export-hf-dataset.js +5 -0
- package/scripts/feedback-loop.js +290 -1
- package/scripts/feedback-quality.js +25 -26
- package/scripts/feedback-sanitizer.js +151 -3
- package/scripts/gates-engine.js +134 -43
- package/scripts/hosted-config.js +9 -2
- package/scripts/imperative-detector.js +85 -0
- package/scripts/intervention-policy.js +13 -0
- package/scripts/mailer/resend-mailer.js +11 -5
- package/scripts/pr-manager.js +9 -22
- package/scripts/pro-local-dashboard.js +198 -0
- package/scripts/risk-scorer.js +6 -0
- package/scripts/secret-scanner.js +363 -68
- package/scripts/self-protection.js +21 -36
- package/scripts/seo-gsd.js +2 -2
- package/scripts/thompson-sampling.js +11 -2
- package/src/api/server.js +202 -22
- package/public/assets/brand/github-social-preview.png +0 -0
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.28.1", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.28.1", "thumbgate", "gate-check"]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -231,7 +231,7 @@ const {
|
|
|
231
231
|
finalizeSession: finalizeFeedbackSession,
|
|
232
232
|
} = require('../../scripts/feedback-session');
|
|
233
233
|
|
|
234
|
-
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.
|
|
234
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.28.1' };
|
|
235
235
|
const COMMERCE_CATEGORIES = [
|
|
236
236
|
'product_recommendation',
|
|
237
237
|
'brand_compliance',
|
|
@@ -1430,22 +1430,14 @@ function writeNdjsonResponse(id, payload, error = null) {
|
|
|
1430
1430
|
process.stdout.write(`${body}\n`);
|
|
1431
1431
|
}
|
|
1432
1432
|
|
|
1433
|
-
/**
|
|
1434
|
-
* Default staleness threshold: if a lock is older than this (ms), the holder
|
|
1435
|
-
* is considered orphaned even if its PID is still alive — it likely belongs
|
|
1436
|
-
* to a defunct Claude Code session whose process was never reaped.
|
|
1437
|
-
*/
|
|
1438
|
-
const LOCK_STALE_MS = Number(process.env.THUMBGATE_LOCK_STALE_MS) || 2 * 60 * 60 * 1000; // 2 hours
|
|
1439
|
-
|
|
1440
1433
|
/**
|
|
1441
1434
|
* Acquire a file-system lock to prevent duplicate MCP server instances.
|
|
1442
|
-
* Returns { lockFile, cleanupLock } on success
|
|
1443
|
-
* if another live server holds the lock.
|
|
1435
|
+
* Returns { lockFile, cleanupLock } on success.
|
|
1444
1436
|
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1437
|
+
* Every live stdio client needs its own MCP process, and those sessions can
|
|
1438
|
+
* legitimately run for many hours. A lock's age is therefore not evidence that
|
|
1439
|
+
* its owner is orphaned. Live owners coexist through per-session locks; only a
|
|
1440
|
+
* lock whose PID is no longer running is reclaimed.
|
|
1449
1441
|
*/
|
|
1450
1442
|
function acquireLock() {
|
|
1451
1443
|
const feedbackDir = getFeedbackPaths().FEEDBACK_DIR;
|
|
@@ -1458,26 +1450,19 @@ function acquireLock() {
|
|
|
1458
1450
|
try { process.kill(lockData.pid, 0); isRunning = true; } catch { /* process is dead */ }
|
|
1459
1451
|
|
|
1460
1452
|
if (isRunning) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
fs.writeFileSync(sessionLockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
1473
|
-
const cleanupSessionLock = () => { try { fs.unlinkSync(sessionLockFile); } catch { /* already removed */ } };
|
|
1474
|
-
process.on('exit', cleanupSessionLock);
|
|
1475
|
-
process.on('SIGTERM', () => { cleanupSessionLock(); process.exit(0); });
|
|
1476
|
-
process.on('SIGINT', () => { cleanupSessionLock(); process.exit(0); });
|
|
1477
|
-
return { lockFile: sessionLockFile, cleanupLock: cleanupSessionLock };
|
|
1478
|
-
}
|
|
1453
|
+
// Another session's MCP server is running — coexist via per-session lock.
|
|
1454
|
+
// Each client communicates via its own stdio pipe and needs its own server.
|
|
1455
|
+
// SQLite WAL mode handles concurrent access safely.
|
|
1456
|
+
process.stderr.write(`[thumbgate] Another MCP server (PID ${lockData.pid}) is running for ${feedbackDir}. Starting concurrent session.\n`);
|
|
1457
|
+
const sessionLockFile = path.join(feedbackDir, `.mcp-server-${process.pid}.lock`);
|
|
1458
|
+
fs.writeFileSync(sessionLockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
1459
|
+
const cleanupSessionLock = () => { try { fs.unlinkSync(sessionLockFile); } catch { /* already removed */ } };
|
|
1460
|
+
process.on('exit', cleanupSessionLock);
|
|
1461
|
+
process.on('SIGTERM', () => { cleanupSessionLock(); process.exit(0); });
|
|
1462
|
+
process.on('SIGINT', () => { cleanupSessionLock(); process.exit(0); });
|
|
1463
|
+
return { lockFile: sessionLockFile, cleanupLock: cleanupSessionLock };
|
|
1479
1464
|
}
|
|
1480
|
-
// Stale lock from a dead
|
|
1465
|
+
// Stale lock from a dead process — remove it.
|
|
1481
1466
|
try { fs.unlinkSync(lockFile); } catch { /* already gone */ }
|
|
1482
1467
|
process.stderr.write(`[thumbgate] Removed stale lock (PID ${lockData.pid} is no longer running).\n`);
|
|
1483
1468
|
}
|
package/bin/cli.js
CHANGED
|
@@ -134,7 +134,7 @@ function upgradeNudge() {
|
|
|
134
134
|
'\n Team rollout: start with the $499 Workflow Hardening Diagnostic\n' +
|
|
135
135
|
` ${diagnosticUrl}\n` +
|
|
136
136
|
`\n Solo side lane: Pro — ${PRO_PRICE_LABEL}\n` +
|
|
137
|
-
'
|
|
137
|
+
' Removes solo caps; adds personal recall, dashboard proof, exports, and managed adapters.\n' +
|
|
138
138
|
` ${pricingUrl}\n\n`
|
|
139
139
|
);
|
|
140
140
|
}
|
|
@@ -234,9 +234,9 @@ function proNudge(context) {
|
|
|
234
234
|
const checkoutUrl = checkoutUrlFor('cli_nudge', context || COMMAND || 'general');
|
|
235
235
|
const pricingUrl = pricingUrlFor('cli_nudge', context || COMMAND || 'general');
|
|
236
236
|
const messages = [
|
|
237
|
-
`\n 💡 Pro (${PRO_PRICE_LABEL}):
|
|
238
|
-
`\n 💡 You just taught ThumbGate something locally. Pro keeps
|
|
239
|
-
`\n 💡 ThumbGate Pro
|
|
237
|
+
`\n 💡 Pro (${PRO_PRICE_LABEL}): personal recall, dashboard proof, exports, and managed adapters.\n See pricing: ${pricingUrl}\n`,
|
|
238
|
+
`\n 💡 You just taught ThumbGate something locally. Pro keeps personal lessons searchable and exportable without free-tier caps.\n See pricing: ${pricingUrl}\n`,
|
|
239
|
+
`\n 💡 ThumbGate Pro maintains adapter coverage across Claude, Codex, Cursor, containers, and CI. ${PRO_PRICE_LABEL}.\n Start Pro: ${checkoutUrl}\n`,
|
|
240
240
|
];
|
|
241
241
|
// Rotate message daily — no Math.random (security policy)
|
|
242
242
|
const msg = messages[Math.floor(Date.now() / 86400000) % messages.length];
|
|
@@ -921,7 +921,7 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
921
921
|
console.log('Scaffold ThumbGate in the current project and wire detected agent integrations.');
|
|
922
922
|
console.log('');
|
|
923
923
|
console.log('Options:');
|
|
924
|
-
console.log(' --agent <name> Wire a specific agent: claude-code, codex, gemini, amp, cursor, cline');
|
|
924
|
+
console.log(' --agent <name> Wire a specific agent: claude-code, codex, gemini, amp, cursor, cline, opencode');
|
|
925
925
|
console.log(' --wire-hooks Wire hooks only; do not scaffold project files');
|
|
926
926
|
console.log(' --email <email> Subscribe installer to the setup guide and trial reminders');
|
|
927
927
|
console.log(' --dry-run Show hook changes without writing them');
|
|
@@ -1593,6 +1593,7 @@ function pro() {
|
|
|
1593
1593
|
trackEvent('cli_pro_view', { command: 'pro' });
|
|
1594
1594
|
const args = parseArgs(process.argv.slice(3));
|
|
1595
1595
|
const {
|
|
1596
|
+
notifyHostedProActivation,
|
|
1596
1597
|
resolveProKey,
|
|
1597
1598
|
saveLicense,
|
|
1598
1599
|
startLocalProDashboard,
|
|
@@ -1606,9 +1607,10 @@ function pro() {
|
|
|
1606
1607
|
console.log('Self-serve side lane today: Pro ($19/mo or $149/yr).');
|
|
1607
1608
|
console.log('Every licensed Pro user gets a personal local dashboard on localhost.');
|
|
1608
1609
|
console.log('\nWhat is available:');
|
|
1609
|
-
console.log(' -
|
|
1610
|
+
console.log(' - Personal recall: search lessons, rules, and proof');
|
|
1610
1611
|
console.log(' - Local Pro dashboard: your own browser dashboard for search, gates, and DPO export');
|
|
1611
|
-
console.log(' -
|
|
1612
|
+
console.log(' - Managed adapters: Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode');
|
|
1613
|
+
console.log(' - Team rollout path: Enterprise adds shared hosted lessons, org visibility, workflow proof');
|
|
1612
1614
|
console.log(' - Commercial truth doc: source of truth for traction, pricing, and proof claims');
|
|
1613
1615
|
console.log('\nLinks:');
|
|
1614
1616
|
console.log(` Buy Pro : ${PRO_CHECKOUT_URL}`);
|
|
@@ -1661,7 +1663,22 @@ function pro() {
|
|
|
1661
1663
|
console.log('\n✅ Pro license activated!');
|
|
1662
1664
|
console.log(` Key saved to: ${licensePath}`);
|
|
1663
1665
|
console.log(' Launching your personal local dashboard...\n');
|
|
1664
|
-
return
|
|
1666
|
+
return notifyHostedProActivation({
|
|
1667
|
+
key: license.key,
|
|
1668
|
+
source: 'cli_pro_activate',
|
|
1669
|
+
version: license.version,
|
|
1670
|
+
})
|
|
1671
|
+
.then((notificationResult) => {
|
|
1672
|
+
appendLocalTelemetry({
|
|
1673
|
+
eventType: 'pro_activation_alert',
|
|
1674
|
+
version: license.version,
|
|
1675
|
+
timestamp: new Date().toISOString(),
|
|
1676
|
+
notified: Boolean(notificationResult && notificationResult.notified),
|
|
1677
|
+
sent: Boolean(notificationResult && notificationResult.alert && notificationResult.alert.sent),
|
|
1678
|
+
reason: notificationResult && notificationResult.reason ? notificationResult.reason : null,
|
|
1679
|
+
});
|
|
1680
|
+
return launchDashboard(license.key, 'pro_activate');
|
|
1681
|
+
});
|
|
1665
1682
|
}
|
|
1666
1683
|
|
|
1667
1684
|
if (args.upgrade) {
|
|
@@ -2678,23 +2695,27 @@ function install() {
|
|
|
2678
2695
|
}
|
|
2679
2696
|
|
|
2680
2697
|
async function gateCheck() {
|
|
2681
|
-
// Explicit emergency escape hatch ONLY. The 2026-06-03 hotfix made this
|
|
2682
|
-
// bypass-by-default, which silently disabled ThumbGate's enforcement entirely
|
|
2683
|
-
// (the firewall approved everything). Restored 2026-06-04: enforcement runs by
|
|
2684
|
-
// default in warn-by-default posture (see gates-engine applyEnforcementPosture);
|
|
2685
|
-
// set THUMBGATE_HOTFIX_BYPASS=1 to disable all checks if a gate ever misfires.
|
|
2686
|
-
if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
|
|
2687
|
-
process.stdout.write(JSON.stringify({
|
|
2688
|
-
decision: 'approve',
|
|
2689
|
-
reason: 'hotfix-bypass-opt-in',
|
|
2690
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
|
|
2691
|
-
}) + '\n');
|
|
2692
|
-
return;
|
|
2693
|
-
}
|
|
2694
2698
|
try {
|
|
2695
2699
|
const payload = readStdinText();
|
|
2696
2700
|
const input = payload ? JSON.parse(payload) : {};
|
|
2697
2701
|
const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
|
|
2702
|
+
|
|
2703
|
+
// The operator bypass only skips advisory and strict-mode gates. Security and
|
|
2704
|
+
// self-protection floors are evaluated before the early approval path.
|
|
2705
|
+
if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
|
|
2706
|
+
const hardFloorOutput = gatesEngine.runHardFloor(input);
|
|
2707
|
+
if (hardFloorOutput) {
|
|
2708
|
+
process.stdout.write(hardFloorOutput + '\n');
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
process.stdout.write(JSON.stringify({
|
|
2712
|
+
decision: 'approve',
|
|
2713
|
+
reason: 'operator-bypass-opt-in',
|
|
2714
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
|
|
2715
|
+
}) + '\n');
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2698
2719
|
const output = await gatesEngine.runAsync(input);
|
|
2699
2720
|
process.stdout.write(output + '\n');
|
|
2700
2721
|
} catch (err) {
|
|
@@ -2732,11 +2753,18 @@ function statuslineRender() {
|
|
|
2732
2753
|
|
|
2733
2754
|
function hookAutoCapture() {
|
|
2734
2755
|
syncActiveProjectContext();
|
|
2756
|
+
const { extractPromptEnvelope } = require(path.join(PKG_ROOT, 'scripts', 'feedback-sanitizer'));
|
|
2757
|
+
const rawStdin = readStdinText();
|
|
2758
|
+
const promptEnvelope = extractPromptEnvelope(rawStdin);
|
|
2759
|
+
// stdin on a UserPromptSubmit hook is the JSON payload
|
|
2760
|
+
// {session_id, transcript_path, cwd, prompt, ...}. Persist ONLY the human
|
|
2761
|
+
// `.prompt` field — never the whole stdin object — so session metadata blobs
|
|
2762
|
+
// can't be promoted as lessons.
|
|
2735
2763
|
const prompt = process.env.CLAUDE_USER_PROMPT
|
|
2736
2764
|
|| process.env.THUMBGATE_USER_PROMPT
|
|
2737
2765
|
|| process.env.CODEX_USER_PROMPT
|
|
2738
2766
|
|| process.env.USER_PROMPT
|
|
2739
|
-
||
|
|
2767
|
+
|| promptEnvelope.prompt;
|
|
2740
2768
|
const { evaluatePromptGuard } = require(path.join(PKG_ROOT, 'scripts', 'prompt-guard'));
|
|
2741
2769
|
const { processInlineFeedback, formatCliOutput } = require(path.join(PKG_ROOT, 'scripts', 'cli-feedback'));
|
|
2742
2770
|
const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
|
|
@@ -2749,25 +2777,39 @@ function hookAutoCapture() {
|
|
|
2749
2777
|
})
|
|
2750
2778
|
);
|
|
2751
2779
|
|
|
2752
|
-
recordConversationEntry({
|
|
2753
|
-
author: 'user',
|
|
2754
|
-
text: prompt,
|
|
2755
|
-
source: 'claude_user_prompt',
|
|
2756
|
-
});
|
|
2757
|
-
|
|
2758
2780
|
const guardResult = evaluatePromptGuard(prompt);
|
|
2759
2781
|
if (guardResult) {
|
|
2782
|
+
recordConversationEntry({
|
|
2783
|
+
author: 'user',
|
|
2784
|
+
text: prompt,
|
|
2785
|
+
source: 'claude_user_prompt',
|
|
2786
|
+
});
|
|
2760
2787
|
process.stdout.write(`${JSON.stringify(guardResult)}\n`);
|
|
2761
2788
|
return;
|
|
2762
2789
|
}
|
|
2763
2790
|
|
|
2764
2791
|
const detected = detectFeedbackSignal(prompt);
|
|
2765
2792
|
if (!detected) {
|
|
2793
|
+
recordConversationEntry({
|
|
2794
|
+
author: 'user',
|
|
2795
|
+
text: prompt,
|
|
2796
|
+
source: 'claude_user_prompt',
|
|
2797
|
+
});
|
|
2766
2798
|
return;
|
|
2767
2799
|
}
|
|
2768
2800
|
|
|
2769
2801
|
const signal = detected.signal;
|
|
2770
2802
|
const conversationWindow = readRecentConversationWindow({ limit: 8 });
|
|
2803
|
+
const { buildFeedbackSourceIdentity } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
2804
|
+
const sourceEvent = buildFeedbackSourceIdentity({
|
|
2805
|
+
signal,
|
|
2806
|
+
promptText: prompt,
|
|
2807
|
+
sessionId: promptEnvelope.sessionId || process.env.CLAUDE_SESSION_ID,
|
|
2808
|
+
promptId: promptEnvelope.promptId,
|
|
2809
|
+
projectDir: promptEnvelope.projectDir || CWD,
|
|
2810
|
+
timestamp: promptEnvelope.timestamp,
|
|
2811
|
+
source: 'claude-user-prompt',
|
|
2812
|
+
});
|
|
2771
2813
|
const result = processInlineFeedback({
|
|
2772
2814
|
signal,
|
|
2773
2815
|
context: prompt,
|
|
@@ -2776,7 +2818,15 @@ function hookAutoCapture() {
|
|
|
2776
2818
|
: undefined,
|
|
2777
2819
|
whatWentWrong: signal === 'down' ? prompt : undefined,
|
|
2778
2820
|
whatWorked: signal === 'up' ? prompt : undefined,
|
|
2821
|
+
sourceEvent,
|
|
2779
2822
|
});
|
|
2823
|
+
if (!result.feedbackResult || !result.feedbackResult.duplicate) {
|
|
2824
|
+
recordConversationEntry({
|
|
2825
|
+
author: 'user',
|
|
2826
|
+
text: prompt,
|
|
2827
|
+
source: 'claude_user_prompt',
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2780
2830
|
process.stdout.write(formatCliOutput(result) + '\n');
|
|
2781
2831
|
}
|
|
2782
2832
|
|
|
@@ -3930,53 +3980,6 @@ switch (COMMAND) {
|
|
|
3930
3980
|
case 'dispatch-brief':
|
|
3931
3981
|
dispatchBrief();
|
|
3932
3982
|
break;
|
|
3933
|
-
case 'gate-check': {
|
|
3934
|
-
// PreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict
|
|
3935
|
-
// Used by: generate-pretool-hook.sh → npx thumbgate gate-check
|
|
3936
|
-
const { run: gateRun, runAsync: gateRunAsync } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
|
|
3937
|
-
const { evaluateSelfProtection } = require(path.join(PKG_ROOT, 'scripts', 'self-protection'));
|
|
3938
|
-
let stdinData = '';
|
|
3939
|
-
process.stdin.setEncoding('utf8');
|
|
3940
|
-
process.stdin.on('data', (chunk) => { stdinData += chunk; });
|
|
3941
|
-
process.stdin.on('end', async () => {
|
|
3942
|
-
try {
|
|
3943
|
-
const input = JSON.parse(stdinData);
|
|
3944
|
-
const output = await gateRunAsync(input);
|
|
3945
|
-
// Self-protection overlay (2026-07-08): the gate engine only denies edits
|
|
3946
|
-
// to ThumbGate's own governance files under strict enforcement; by default
|
|
3947
|
-
// such edits pass as a clean ALLOW. Surface them (or block in strict) so an
|
|
3948
|
-
// agent can't silently disable the firewall. Only overlays when the engine
|
|
3949
|
-
// did not already produce a hard deny.
|
|
3950
|
-
let verdict = output;
|
|
3951
|
-
try {
|
|
3952
|
-
const parsed = JSON.parse(output || '{}');
|
|
3953
|
-
const alreadyDeny = (parsed.hookSpecificOutput && parsed.hookSpecificOutput.permissionDecision === 'deny')
|
|
3954
|
-
|| parsed.decision === 'block';
|
|
3955
|
-
if (!alreadyDeny) {
|
|
3956
|
-
const sp = evaluateSelfProtection(input.tool_name, input.tool_input);
|
|
3957
|
-
if (sp && sp.action === 'block') {
|
|
3958
|
-
verdict = JSON.stringify({
|
|
3959
|
-
decision: 'block',
|
|
3960
|
-
reason: sp.message,
|
|
3961
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: sp.message },
|
|
3962
|
-
});
|
|
3963
|
-
} else if (sp && sp.action === 'warn') {
|
|
3964
|
-
verdict = JSON.stringify({
|
|
3965
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: sp.message },
|
|
3966
|
-
});
|
|
3967
|
-
}
|
|
3968
|
-
}
|
|
3969
|
-
} catch (_e) { /* non-JSON engine output: pass through unchanged */ }
|
|
3970
|
-
process.stdout.write(verdict + '\n');
|
|
3971
|
-
process.exit(0);
|
|
3972
|
-
} catch (err) {
|
|
3973
|
-
process.stderr.write(`gate-check error: ${err.message}\n`);
|
|
3974
|
-
process.stdout.write(JSON.stringify({}) + '\n');
|
|
3975
|
-
process.exit(0);
|
|
3976
|
-
}
|
|
3977
|
-
});
|
|
3978
|
-
break;
|
|
3979
|
-
}
|
|
3980
3983
|
case 'hermes-gate': {
|
|
3981
3984
|
// Nous Research Hermes Agent `pre_tool_call` shell hook.
|
|
3982
3985
|
// Hermes pipes each pending tool call as JSON to stdin and reads a decision from stdout;
|
|
@@ -3987,7 +3990,7 @@ switch (COMMAND) {
|
|
|
3987
3990
|
// Hermes `pre_tool_call` is binary (block or allow) with no warn channel, and the whole point
|
|
3988
3991
|
// of wiring it is to gate, so we run STRICT enforcement by default — otherwise ThumbGate's
|
|
3989
3992
|
// warn-by-default posture would pass every deny through and the hook would block nothing.
|
|
3990
|
-
// Opt out with THUMBGATE_HERMES_WARN_ONLY=1;
|
|
3993
|
+
// Opt out with THUMBGATE_HERMES_WARN_ONLY=1; the operator bypass also leaves ordinary gates advisory.
|
|
3991
3994
|
// Wire it in ~/.hermes/config.yaml — see adapters/hermes/config.yaml.
|
|
3992
3995
|
if (process.env.THUMBGATE_HERMES_WARN_ONLY !== '1' && process.env.THUMBGATE_HOTFIX_BYPASS !== '1') {
|
|
3993
3996
|
process.env.THUMBGATE_STRICT_ENFORCEMENT = '1';
|
|
@@ -3999,8 +4002,22 @@ switch (COMMAND) {
|
|
|
3999
4002
|
process.stdin.on('end', async () => {
|
|
4000
4003
|
try {
|
|
4001
4004
|
const payload = JSON.parse(hermesStdin);
|
|
4002
|
-
|
|
4003
|
-
const
|
|
4005
|
+
const hermesToolName = String(payload.tool_name || '');
|
|
4006
|
+
const canonicalToolNames = {
|
|
4007
|
+
terminal: 'Bash',
|
|
4008
|
+
process: 'Bash',
|
|
4009
|
+
execute_code: 'Bash',
|
|
4010
|
+
patch: 'Edit',
|
|
4011
|
+
write_file: 'Write',
|
|
4012
|
+
read_file: 'Read',
|
|
4013
|
+
};
|
|
4014
|
+
const verdict = await hermesGateRun({
|
|
4015
|
+
tool_name: canonicalToolNames[hermesToolName] || hermesToolName,
|
|
4016
|
+
tool_input: {
|
|
4017
|
+
...(payload.tool_input || {}),
|
|
4018
|
+
hermes_tool_name: hermesToolName,
|
|
4019
|
+
},
|
|
4020
|
+
});
|
|
4004
4021
|
let parsed = {};
|
|
4005
4022
|
try { parsed = JSON.parse(verdict); } catch (_e) { parsed = {}; }
|
|
4006
4023
|
const hso = parsed.hookSpecificOutput || {};
|
package/bin/postinstall.js
CHANGED
|
@@ -36,7 +36,7 @@ process.stderr.write(`
|
|
|
36
36
|
╰─────────────────────────────────────────────────────╯
|
|
37
37
|
|
|
38
38
|
Trial unlocks: unlimited rules, lesson search, DPO export,
|
|
39
|
-
|
|
39
|
+
and personal dashboard proof. After 7 days, free tier limits apply.
|
|
40
40
|
Subscribe for the 5-min setup guide + weekly tips:
|
|
41
41
|
npx thumbgate subscribe you@company.com
|
|
42
42
|
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
{
|
|
211
211
|
"id": "deny-network-egress",
|
|
212
212
|
"layer": "Cloud",
|
|
213
|
-
"pattern": "
|
|
213
|
+
"pattern": "fetch\\(|https?://(?!(?:github\\.com|registry\\.npmjs\\.org|api\\.anthropic\\.com|localhost|127\\.0\\.0\\.1|\\[::1\\]|0\\.0\\.0\\.0)(?:[:/?#]|$))|(?:curl|wget)\\s+(?:-\\S+\\s+)*(?!https?://|(?:github\\.com|registry\\.npmjs\\.org|api\\.anthropic\\.com|localhost|127\\.0\\.0\\.1|\\[::1\\]|0\\.0\\.0\\.0)(?:[:/?#]|$))[\\w.-]+\\.[a-z]{2,}",
|
|
214
214
|
"action": "warn",
|
|
215
215
|
"unless": "egress_approved",
|
|
216
216
|
"message": "Potential unauthorized network egress detected.",
|
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
"id": "self-protect-config",
|
|
343
343
|
"layer": "Execution",
|
|
344
344
|
"toolNames": ["Edit", "Write", "MultiEdit"],
|
|
345
|
-
"pattern": "(?:config/gates/|config/budget\\.json|\\.thumbgate
|
|
345
|
+
"pattern": "(?:config/gates/|config/(?:budget|enforcement|mcp-allowlists)\\.json|\\.thumbgate/config\\.json|thumbgate\\.json)",
|
|
346
346
|
"action": "block",
|
|
347
347
|
"message": "Self-protection: agent cannot modify ThumbGate configuration, gate rules, or budget settings.",
|
|
348
348
|
"severity": "critical",
|
|
@@ -372,7 +372,7 @@
|
|
|
372
372
|
"id": "self-protect-hooks-disable",
|
|
373
373
|
"layer": "Execution",
|
|
374
374
|
"toolNames": ["Edit", "Write", "Bash"],
|
|
375
|
-
"pattern": "(
|
|
375
|
+
"pattern": "(?:\\.claude/settings(?:\\.local)?\\.json|\\.codex/config\\.toml|scripts/hook-[^/]+\\.(?:js|sh))",
|
|
376
376
|
"action": "block",
|
|
377
377
|
"message": "Self-protection: agent cannot modify hook registrations.",
|
|
378
378
|
"severity": "critical",
|
package/config/github-about.json
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
"repo": "IgorGanapolsky/ThumbGate",
|
|
3
3
|
"repositoryUrl": "https://github.com/IgorGanapolsky/ThumbGate",
|
|
4
4
|
"homepageUrl": "https://thumbgate.ai",
|
|
5
|
-
"githubDescription": "
|
|
6
|
-
"metaDescription": "
|
|
5
|
+
"githubDescription": "ThumbGate Pre-Action Checks derive rules from repeated failures, flag risky tool calls, hard-block detected secret leaks, and block matches in strict mode.",
|
|
6
|
+
"metaDescription": "ThumbGate is the local pre-action governance runtime for AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode): accepted 👍 thumbs-up and 👎 thumbs-down feedback becomes history-aware local lessons; repeated failures can become prevention rules. PreToolUse checks flag and log risky tool calls, hard-block detected secret leaks, and block matching actions in strict mode. Pro is for individual operators; Enterprise rollout is scoped after intake.",
|
|
7
7
|
"topics": [
|
|
8
8
|
"thumbgate",
|
|
9
9
|
"pre-action-checks",
|
|
10
|
-
"save-llm-tokens",
|
|
11
|
-
"reduce-llm-cost",
|
|
12
|
-
"ai-cost-optimization",
|
|
13
10
|
"mcp",
|
|
14
11
|
"mcp-server",
|
|
15
12
|
"ai-agents",
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash|Edit|Write",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node",
|
|
10
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/hook-pre-tool-use.js"]
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"UserPromptSubmit": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node",
|
|
21
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/bin/cli.js", "hook-auto-capture"]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"SessionStart": [
|
|
27
|
+
{
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node",
|
|
32
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/bin/cli.js", "session-start"]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|