thumbgate 1.27.19 → 1.28.0
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/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +63 -45
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bin/cli.js +52 -9
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +14 -23
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/entitlement-public-keys.json +6 -0
- package/config/gates/default.json +1 -1
- package/config/github-about.json +2 -2
- package/config/merge-quality-checks.json +3 -0
- package/package.json +233 -100
- package/public/about.html +159 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- 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 +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +12 -9
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +5 -5
- package/public/compare.html +10 -10
- package/public/dashboard.html +1 -1
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +14 -14
- package/public/index.html +81 -70
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +4 -17
- package/public/numbers.html +2 -2
- package/public/pricing.html +392 -0
- package/public/pro.html +2 -2
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-reward-model.js +13 -0
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/bayes-optimal-gate.js +6 -1
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +17 -1
- package/scripts/commercial-offer.js +3 -3
- package/scripts/dashboard-chat.js +332 -0
- 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-aggregate.js +281 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/gates-engine.js +15 -3
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/imperative-detector.js +85 -0
- package/scripts/install-shim.js +87 -0
- package/scripts/intervention-policy.js +13 -0
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/pr-manager.js +9 -22
- package/scripts/pro-local-dashboard.js +198 -0
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +137 -0
- package/scripts/risk-scorer.js +6 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +2 -2
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/thompson-sampling.js +11 -2
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/src/api/server.js +81 -10
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1707
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const {
|
|
9
|
+
getFallbackFeedbackDir,
|
|
10
|
+
getGlobalFeedbackDir,
|
|
11
|
+
getHomeDir,
|
|
12
|
+
getLegacyFeedbackDir,
|
|
13
|
+
getThumbgateFeedbackDir,
|
|
14
|
+
resolveFeedbackDir,
|
|
15
|
+
resolveProjectDir,
|
|
16
|
+
} = require('./feedback-paths');
|
|
17
|
+
const { readJsonl } = require('./fs-utils');
|
|
18
|
+
|
|
19
|
+
const FEEDBACK_LOG = 'feedback-log.jsonl';
|
|
20
|
+
const MEMORY_LOG = 'memory-log.jsonl';
|
|
21
|
+
const STATUSLINE_CACHE = 'statusline_cache.json';
|
|
22
|
+
|
|
23
|
+
function truthyDisabled(value) {
|
|
24
|
+
return value === '0' || value === 'false' || value === 'local';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldAggregateFeedback(options = {}) {
|
|
28
|
+
const env = options.env || process.env;
|
|
29
|
+
return !truthyDisabled(String(env.THUMBGATE_STATUSLINE_AGGREGATE || env.THUMBGATE_AGGREGATE_FEEDBACK || '1').toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePath(candidate) {
|
|
33
|
+
if (!candidate) return null;
|
|
34
|
+
try {
|
|
35
|
+
return path.resolve(String(candidate));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function uniquePaths(values = []) {
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
const resolved = normalizePath(value);
|
|
46
|
+
if (!resolved || seen.has(resolved)) continue;
|
|
47
|
+
seen.add(resolved);
|
|
48
|
+
out.push(resolved);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safeExists(candidate) {
|
|
54
|
+
try {
|
|
55
|
+
return Boolean(candidate && fs.existsSync(candidate));
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function immediateChildDirs(parentDir) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.readdirSync(parentDir, { withFileTypes: true })
|
|
64
|
+
.filter((entry) => entry.isDirectory())
|
|
65
|
+
.map((entry) => path.join(parentDir, entry.name));
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ancestorProjectFeedbackDirs(projectDir, options = {}) {
|
|
72
|
+
const home = normalizePath(getHomeDir(options));
|
|
73
|
+
const start = normalizePath(projectDir);
|
|
74
|
+
if (!start) return [];
|
|
75
|
+
|
|
76
|
+
const dirs = [];
|
|
77
|
+
let cursor = start;
|
|
78
|
+
while (cursor && cursor !== path.dirname(cursor)) {
|
|
79
|
+
dirs.push(
|
|
80
|
+
path.join(cursor, '.thumbgate'),
|
|
81
|
+
path.join(cursor, '.thumbgate-compat'),
|
|
82
|
+
path.join(cursor, '.claude', 'memory', 'feedback')
|
|
83
|
+
);
|
|
84
|
+
if (home && cursor === home) break;
|
|
85
|
+
const next = path.dirname(cursor);
|
|
86
|
+
if (next === cursor) break;
|
|
87
|
+
cursor = next;
|
|
88
|
+
}
|
|
89
|
+
return dirs;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function listFeedbackStoreDirs(options = {}) {
|
|
93
|
+
const env = options.env || process.env;
|
|
94
|
+
const projectDir = resolveProjectDir({ cwd: options.cwd, env, projectDir: options.projectDir });
|
|
95
|
+
const home = getHomeDir({ env });
|
|
96
|
+
const homeThumbgate = path.join(home, '.thumbgate');
|
|
97
|
+
const projectsDir = path.join(homeThumbgate, 'projects');
|
|
98
|
+
const explicitRoots = String(env.THUMBGATE_AGGREGATE_ROOTS || '')
|
|
99
|
+
.split(path.delimiter)
|
|
100
|
+
.map((value) => value.trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
const explicitFeedbackDir = options.feedbackDir || env.THUMBGATE_FEEDBACK_DIR;
|
|
103
|
+
const normalizedExplicitFeedbackDir = normalizePath(explicitFeedbackDir);
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
normalizedExplicitFeedbackDir &&
|
|
107
|
+
normalizedExplicitFeedbackDir.startsWith(normalizePath(os.tmpdir()) + path.sep) &&
|
|
108
|
+
explicitRoots.length === 0
|
|
109
|
+
) {
|
|
110
|
+
return uniquePaths([explicitFeedbackDir])
|
|
111
|
+
.filter((dir) => safeExists(path.join(dir, FEEDBACK_LOG)) || safeExists(path.join(dir, MEMORY_LOG)) || safeExists(path.join(dir, STATUSLINE_CACHE)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return uniquePaths([
|
|
115
|
+
explicitFeedbackDir,
|
|
116
|
+
resolveFeedbackDir({ projectDir, env }),
|
|
117
|
+
getThumbgateFeedbackDir({ projectDir, env }),
|
|
118
|
+
getFallbackFeedbackDir({ projectDir, env }),
|
|
119
|
+
getLegacyFeedbackDir({ projectDir, env }),
|
|
120
|
+
getGlobalFeedbackDir({ projectDir, env }),
|
|
121
|
+
homeThumbgate,
|
|
122
|
+
...immediateChildDirs(projectsDir),
|
|
123
|
+
...ancestorProjectFeedbackDirs(projectDir, { env }),
|
|
124
|
+
...explicitRoots,
|
|
125
|
+
]).filter((dir) => safeExists(path.join(dir, FEEDBACK_LOG)) || safeExists(path.join(dir, MEMORY_LOG)) || safeExists(path.join(dir, STATUSLINE_CACHE)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeSignal(signal) {
|
|
129
|
+
const raw = String(signal || '').toLowerCase();
|
|
130
|
+
if (raw === 'positive' || raw === 'up' || raw === 'thumbs_up') return 'positive';
|
|
131
|
+
if (raw === 'negative' || raw === 'down' || raw === 'thumbs_down') return 'negative';
|
|
132
|
+
return raw || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stableEntryKey(entry = {}, source = {}) {
|
|
136
|
+
const id = entry.id || entry.feedbackId || entry.sourceFeedbackId;
|
|
137
|
+
if (id) return `id:${id}`;
|
|
138
|
+
const material = JSON.stringify({
|
|
139
|
+
sourcePath: source.logPath || '',
|
|
140
|
+
sourceIndex: Number.isFinite(source.index) ? source.index : -1,
|
|
141
|
+
entry,
|
|
142
|
+
});
|
|
143
|
+
return `sha:${crypto.createHash('sha256').update(material).digest('hex')}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sortByTimestamp(entries) {
|
|
147
|
+
return entries.sort((a, b) => {
|
|
148
|
+
const at = a.timestamp ? Date.parse(a.timestamp) : 0;
|
|
149
|
+
const bt = b.timestamp ? Date.parse(b.timestamp) : 0;
|
|
150
|
+
return (Number.isFinite(at) ? at : 0) - (Number.isFinite(bt) ? bt : 0);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function collectAggregateLogEntries(fileName, options = {}) {
|
|
155
|
+
const stores = listFeedbackStoreDirs(options);
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const entries = [];
|
|
158
|
+
|
|
159
|
+
for (const dir of stores) {
|
|
160
|
+
const logPath = path.join(dir, fileName);
|
|
161
|
+
if (!safeExists(logPath)) continue;
|
|
162
|
+
const rows = readJsonl(logPath, { maxLines: 0 }) || [];
|
|
163
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
164
|
+
const rawEntry = rows[index];
|
|
165
|
+
const entry = { ...rawEntry };
|
|
166
|
+
if (entry.signal || entry.feedback) entry.signal = normalizeSignal(entry.signal || entry.feedback);
|
|
167
|
+
const key = stableEntryKey(entry, { logPath, index });
|
|
168
|
+
if (seen.has(key)) continue;
|
|
169
|
+
seen.add(key);
|
|
170
|
+
entries.push({ ...entry, sourceFeedbackDir: dir });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
entries: sortByTimestamp(entries),
|
|
176
|
+
stores,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function summarizeFeedbackEntries(entries) {
|
|
181
|
+
let totalPositive = 0;
|
|
182
|
+
let totalNegative = 0;
|
|
183
|
+
let rubricSamples = 0;
|
|
184
|
+
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (entry.signal === 'positive') totalPositive += 1;
|
|
187
|
+
if (entry.signal === 'negative') totalNegative += 1;
|
|
188
|
+
if (entry.rubric && entry.rubric.weightedScore != null) rubricSamples += 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { totalPositive, totalNegative, rubricSamples };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function createRateWindows(total, totalPositive, approvalRate) {
|
|
195
|
+
return {
|
|
196
|
+
'7d': { total: 0, positive: 0, rate: 0 },
|
|
197
|
+
'30d': { total: 0, positive: 0, rate: 0 },
|
|
198
|
+
lifetime: { total, positive: totalPositive, rate: approvalRate },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function applyEntryToRateWindow(window, entry) {
|
|
203
|
+
window.total += 1;
|
|
204
|
+
if (entry.signal === 'positive') window.positive += 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function updateRateWindows(windows, entries, now = Date.now()) {
|
|
208
|
+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
|
|
209
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
210
|
+
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const ts = entry.timestamp ? Date.parse(entry.timestamp) : Number.NaN;
|
|
213
|
+
if (!Number.isFinite(ts)) continue;
|
|
214
|
+
const age = now - ts;
|
|
215
|
+
if (age <= sevenDays) applyEntryToRateWindow(windows['7d'], entry);
|
|
216
|
+
if (age <= thirtyDays) applyEntryToRateWindow(windows['30d'], entry);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function finalizeRateWindows(windows) {
|
|
221
|
+
for (const key of ['7d', '30d']) {
|
|
222
|
+
const window = windows[key];
|
|
223
|
+
window.rate = 0;
|
|
224
|
+
if (window.total > 0) {
|
|
225
|
+
window.rate = Math.round((window.positive / window.total) * 1000) / 1000;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function trendFromRateWindows(windows) {
|
|
231
|
+
const hasTrendData = windows['7d'].total > 0 && windows['30d'].total > 0;
|
|
232
|
+
if (hasTrendData) {
|
|
233
|
+
if (windows['7d'].rate > windows['30d'].rate + 0.05) return 'improving';
|
|
234
|
+
if (windows['7d'].rate < windows['30d'].rate - 0.05) return 'degrading';
|
|
235
|
+
}
|
|
236
|
+
return 'stable';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function computeAggregateFeedbackStats(options = {}) {
|
|
240
|
+
const { entries, stores } = collectAggregateLogEntries(FEEDBACK_LOG, options);
|
|
241
|
+
const memory = collectAggregateLogEntries(MEMORY_LOG, options);
|
|
242
|
+
const { totalPositive, totalNegative, rubricSamples } = summarizeFeedbackEntries(entries);
|
|
243
|
+
const total = totalPositive + totalNegative;
|
|
244
|
+
const approvalRate = total > 0 ? Math.round((totalPositive / total) * 1000) / 1000 : 0;
|
|
245
|
+
const windows = createRateWindows(total, totalPositive, approvalRate);
|
|
246
|
+
updateRateWindows(windows, entries);
|
|
247
|
+
finalizeRateWindows(windows);
|
|
248
|
+
const trend = trendFromRateWindows(windows);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
total,
|
|
252
|
+
totalPositive,
|
|
253
|
+
totalNegative,
|
|
254
|
+
approvalRate,
|
|
255
|
+
recentRate: windows['7d'].rate || approvalRate,
|
|
256
|
+
trend,
|
|
257
|
+
windows,
|
|
258
|
+
rubric: { samples: rubricSamples || memory.entries.length, blockedPromotions: 0, failingCriteria: {} },
|
|
259
|
+
aggregate: {
|
|
260
|
+
enabled: true,
|
|
261
|
+
stores: stores.length,
|
|
262
|
+
feedbackLogPaths: stores.map((dir) => path.join(dir, FEEDBACK_LOG)),
|
|
263
|
+
memoryStores: memory.stores.length,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getAggregateStatuslineCachePath(options = {}) {
|
|
269
|
+
const env = options.env || process.env;
|
|
270
|
+
if (env.THUMBGATE_STATUSLINE_CACHE) return path.resolve(env.THUMBGATE_STATUSLINE_CACHE);
|
|
271
|
+
return path.join(getHomeDir({ env }), '.thumbgate', STATUSLINE_CACHE);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
collectAggregateLogEntries,
|
|
276
|
+
computeAggregateFeedbackStats,
|
|
277
|
+
getAggregateStatuslineCachePath,
|
|
278
|
+
listFeedbackStoreDirs,
|
|
279
|
+
normalizeSignal,
|
|
280
|
+
shouldAggregateFeedback,
|
|
281
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const TRANSPORT_KEYS = new Set([
|
|
6
|
+
'hookeventname',
|
|
7
|
+
'hook_event_name',
|
|
8
|
+
'sessionid',
|
|
9
|
+
'session_id',
|
|
10
|
+
'transcriptpath',
|
|
11
|
+
'transcript_path',
|
|
12
|
+
'timestamp',
|
|
13
|
+
'createdat',
|
|
14
|
+
'created_at',
|
|
15
|
+
'updatedat',
|
|
16
|
+
'updated_at',
|
|
17
|
+
'cwd',
|
|
18
|
+
'pid',
|
|
19
|
+
'processid',
|
|
20
|
+
'process_id',
|
|
21
|
+
'promptid',
|
|
22
|
+
'prompt_id',
|
|
23
|
+
'traceid',
|
|
24
|
+
'trace_id',
|
|
25
|
+
'requestid',
|
|
26
|
+
'request_id',
|
|
27
|
+
'installid',
|
|
28
|
+
'install_id',
|
|
29
|
+
'visitorsessionid',
|
|
30
|
+
'visitor_session_id',
|
|
31
|
+
'toolinput',
|
|
32
|
+
'tool_input',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const TRANSPORT_WORDS = new Set([
|
|
36
|
+
...TRANSPORT_KEYS,
|
|
37
|
+
'hook',
|
|
38
|
+
'event',
|
|
39
|
+
'userpromptsubmit',
|
|
40
|
+
'user_prompt_submit',
|
|
41
|
+
'pretooluse',
|
|
42
|
+
'pre_tool_use',
|
|
43
|
+
'posttooluse',
|
|
44
|
+
'post_tool_use',
|
|
45
|
+
'claude',
|
|
46
|
+
'codex',
|
|
47
|
+
'projects',
|
|
48
|
+
'redacted',
|
|
49
|
+
'tmp',
|
|
50
|
+
'private',
|
|
51
|
+
'folders',
|
|
52
|
+
'json',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function stripEphemeralText(text) {
|
|
56
|
+
if (!text || typeof text !== 'string') return '';
|
|
57
|
+
return String(text)
|
|
58
|
+
.replace(/["']?(?:hook_?event_?name|session_?id|transcript_?path|timestamp|created_?at|updated_?at|cwd|pid|process_?id|prompt_?id|trace_?id|request_?id|install_?id|visitor_?session_?id)["']?\s*[:=]\s*["']?[^"',}\]\s]+["']?/gi, ' ')
|
|
59
|
+
.replace(/\/(?:private\/)?tmp\/[^\s"',}\]]+/gi, ' ')
|
|
60
|
+
.replace(/\/var\/folders\/[^\s"',}\]]+/gi, ' ')
|
|
61
|
+
.replace(/\/Users\/[^/\s]+\/\.(?:claude|codex|thumbgate)\/[^\s"',}\]]+/gi, ' ')
|
|
62
|
+
.replace(/\/Users\/[^/\s]+\/\.config\/thumbgate\/[^\s"',}\]]+/gi, ' ')
|
|
63
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ' ')
|
|
64
|
+
.replace(/\b[0-9a-f]{24,}\b/gi, ' ')
|
|
65
|
+
.replace(/\b\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}(?:\.\d+)?z?\b/gi, ' ')
|
|
66
|
+
.replace(/\b\d{10,13}\b/g, ' ')
|
|
67
|
+
.replace(/:\d{4,5}\b/g, ':PORT');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function transportWordsOnly(text) {
|
|
71
|
+
const tokens = String(text || '')
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9_ -]/g, ' ')
|
|
74
|
+
.split(/\s+/)
|
|
75
|
+
.filter((token) => token.length >= 3);
|
|
76
|
+
if (tokens.length === 0) return true;
|
|
77
|
+
return tokens.every((token) => TRANSPORT_WORDS.has(token));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sanitizeFeedbackText(text) {
|
|
81
|
+
const stripped = stripEphemeralText(text)
|
|
82
|
+
.replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
|
|
83
|
+
.replace(/\s+/g, ' ')
|
|
84
|
+
.trim();
|
|
85
|
+
if (transportWordsOnly(stripped)) return '';
|
|
86
|
+
return stripped;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function actionFingerprint(parts) {
|
|
90
|
+
const raw = Array.isArray(parts) ? parts.join(' ') : String(parts || '');
|
|
91
|
+
const stable = sanitizeFeedbackText(raw)
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replace(/[^a-z0-9._:/ -]/g, ' ')
|
|
94
|
+
.replace(/\s+/g, ' ')
|
|
95
|
+
.trim();
|
|
96
|
+
if (!stable || transportWordsOnly(stable)) return null;
|
|
97
|
+
return crypto.createHash('sha256').update(stable).digest('hex').slice(0, 16);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
TRANSPORT_WORDS,
|
|
102
|
+
sanitizeFeedbackText,
|
|
103
|
+
actionFingerprint,
|
|
104
|
+
transportWordsOnly,
|
|
105
|
+
};
|
package/scripts/gates-engine.js
CHANGED
|
@@ -151,10 +151,22 @@ const DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN = /\b(?:rm\s+-rf|chmod\s+(?:\+x|
|
|
|
151
151
|
// far better than any single regex). Secret exfiltration and the security-vulnerability
|
|
152
152
|
// scan hard-deny on their OWN paths before this runs, so irreversible data-leak / supply
|
|
153
153
|
// chain risks stay blocked regardless of posture.
|
|
154
|
+
// Self-protective gates guard ThumbGate's own guardrails (kill/env/config/hooks). See the
|
|
155
|
+
// self-protect-binds-regardless-of-posture changeset for the full rationale.
|
|
156
|
+
function isSelfProtectGate(gateId) {
|
|
157
|
+
return typeof gateId === 'string' && gateId.startsWith('self-protect');
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
function applyEnforcementPosture(result) {
|
|
155
161
|
if (!result || (result.decision !== 'deny' && result.decision !== 'approve')) return result;
|
|
156
162
|
// Full hard enforcement opt-in: keep every deny.
|
|
157
163
|
if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') return result;
|
|
164
|
+
// Self-protection binds regardless of posture: a warning is worthless once the guardrail is
|
|
165
|
+
// gone. Owner escape: THUMBGATE_SELF_PROTECT_OVERRIDE=1 (break-glass covers .claude/settings*
|
|
166
|
+
// but not this surface, so it needs its own escape — self-lockout, 2026-07-07).
|
|
167
|
+
if (isSelfProtectGate(result.gate) && process.env.THUMBGATE_SELF_PROTECT_OVERRIDE !== '1') {
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
158
170
|
// Honor the explicit strict-knowledge-conflict opt-in for that gate.
|
|
159
171
|
if (process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1' && result.gate === 'knowledge-conflict-gate') return result;
|
|
160
172
|
// Warn-by-default: the gate still fired and is recorded; the action is allowed through
|
|
@@ -2672,12 +2684,12 @@ function buildBlockActionProCta() {
|
|
|
2672
2684
|
if (totalBlocks < 5) return null; // Too early — let them experience the product
|
|
2673
2685
|
|
|
2674
2686
|
if (totalBlocks < 25) {
|
|
2675
|
-
return '\n\n💡 Pro: keep this rule
|
|
2687
|
+
return '\n\n💡 Pro: keep this rule searchable, exportable, and visible → thumbgate.ai/go/pro';
|
|
2676
2688
|
}
|
|
2677
2689
|
if (totalBlocks < 100) {
|
|
2678
|
-
return `\n\n💡 ${totalBlocks} actions blocked. Pro
|
|
2690
|
+
return `\n\n💡 ${totalBlocks} actions blocked. Pro adds recall, dashboard proof, and exports → thumbgate.ai/go/pro ($19/mo)`;
|
|
2679
2691
|
}
|
|
2680
|
-
return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use shared hosted enforcement → thumbgate.ai/go/pro`;
|
|
2692
|
+
return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use Enterprise shared hosted enforcement → thumbgate.ai/go/pro`;
|
|
2681
2693
|
} catch (_) {
|
|
2682
2694
|
return null;
|
|
2683
2695
|
}
|