thumbgate 0.9.14 → 1.0.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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +1 -0
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +105 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/forge/forge.yaml +28 -0
- package/adapters/mcp/server-stdio.js +32 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +18 -3
- package/config/mcp-allowlists.json +10 -0
- package/openapi/openapi.yaml +105 -0
- package/package.json +4 -4
- package/plugins/amp-skill/INSTALL.md +3 -4
- package/plugins/amp-skill/SKILL.md +0 -1
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-skill/INSTALL.md +1 -2
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +1 -0
- package/public/dashboard.html +1 -1
- package/public/guide.html +1 -1
- package/public/index.html +3 -3
- package/public/learn/agent-harness-pattern.html +1 -1
- package/public/learn/ai-agent-persistent-memory.html +1 -1
- package/public/learn/mcp-pre-action-gates-explained.html +1 -1
- package/public/learn/stop-ai-agent-force-push.html +1 -1
- package/public/learn/vibe-coding-safety-net.html +1 -1
- package/public/learn.html +1 -1
- package/public/lessons.html +1 -1
- package/public/pro.html +1 -1
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/async-job-runner.js +84 -24
- package/scripts/auto-wire-hooks.js +59 -1
- package/scripts/context-manager.js +330 -0
- package/scripts/dashboard.js +1 -1
- package/scripts/distribution-surfaces.js +12 -0
- package/scripts/ensure-repo-bootstrap.js +15 -14
- package/scripts/gates-engine.js +96 -10
- package/scripts/hook-auto-capture.sh +1 -1
- package/scripts/hosted-job-launcher.js +260 -0
- package/scripts/managed-dpo-export.js +91 -0
- package/scripts/obsidian-export.js +0 -1
- package/scripts/operational-integrity.js +50 -7
- package/scripts/prove-lancedb.js +62 -4
- package/scripts/publish-decision.js +16 -0
- package/scripts/self-healing-check.js +6 -1
- package/scripts/social-analytics/load-env.js +33 -2
- package/scripts/social-analytics/store.js +200 -2
- package/scripts/sync-version.js +18 -11
- package/scripts/tool-registry.js +37 -0
- package/scripts/train_from_feedback.py +0 -4
- package/scripts/workflow-sentinel.js +793 -0
- package/src/api/server.js +205 -27
- /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context Manager — Unified Context-Augmented Generation (CAG) Orchestrator
|
|
6
|
+
*
|
|
7
|
+
* Single entry point that assembles a normalized context object from:
|
|
8
|
+
* - Session state (primer / handoff)
|
|
9
|
+
* - User profile (role, preferences, agent type)
|
|
10
|
+
* - Relevant lessons (per-action retrieval)
|
|
11
|
+
* - Prevention rules / pre-tool guards
|
|
12
|
+
* - Context pack (ContextFS retrieval)
|
|
13
|
+
* - Code-graph impact (optional, for coding tasks)
|
|
14
|
+
*
|
|
15
|
+
* Implements tiered graceful degradation:
|
|
16
|
+
* Tier 1 (full) — session + lessons + rules + context pack + code-graph
|
|
17
|
+
* Tier 2 (warm) — lessons + rules + context pack (no session)
|
|
18
|
+
* Tier 3 (cold) — prevention rules + global defaults only
|
|
19
|
+
*
|
|
20
|
+
* Role-aware filtering shapes output by agent type and license tier.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
ensureContextFs,
|
|
25
|
+
constructContextPack,
|
|
26
|
+
readSessionHandoff,
|
|
27
|
+
recordProvenance,
|
|
28
|
+
} = require('./contextfs');
|
|
29
|
+
const { retrieveRelevantLessons } = require('./lesson-retrieval');
|
|
30
|
+
const { evaluatePretool } = require('./hybrid-feedback-context');
|
|
31
|
+
const { loadProfile } = require('./user-profile');
|
|
32
|
+
const {
|
|
33
|
+
analyzeCodeGraphImpact,
|
|
34
|
+
formatCodeGraphRecallSection,
|
|
35
|
+
} = require('./codegraph-context');
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Agent capability profiles — shapes what context each agent type receives
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const AGENT_PROFILES = {
|
|
42
|
+
claude: {
|
|
43
|
+
maxLessons: 8,
|
|
44
|
+
includeCodeGraph: true,
|
|
45
|
+
includeStructuredRules: true,
|
|
46
|
+
contextBudget: 10000,
|
|
47
|
+
},
|
|
48
|
+
cursor: {
|
|
49
|
+
maxLessons: 5,
|
|
50
|
+
includeCodeGraph: true,
|
|
51
|
+
includeStructuredRules: true,
|
|
52
|
+
contextBudget: 6000,
|
|
53
|
+
},
|
|
54
|
+
forgecode: {
|
|
55
|
+
maxLessons: 5,
|
|
56
|
+
includeCodeGraph: false,
|
|
57
|
+
includeStructuredRules: true,
|
|
58
|
+
contextBudget: 6000,
|
|
59
|
+
},
|
|
60
|
+
codex: {
|
|
61
|
+
maxLessons: 6,
|
|
62
|
+
includeCodeGraph: true,
|
|
63
|
+
includeStructuredRules: true,
|
|
64
|
+
contextBudget: 8000,
|
|
65
|
+
},
|
|
66
|
+
default: {
|
|
67
|
+
maxLessons: 5,
|
|
68
|
+
includeCodeGraph: false,
|
|
69
|
+
includeStructuredRules: true,
|
|
70
|
+
contextBudget: 6000,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function getAgentProfile(agentType) {
|
|
75
|
+
const key = String(agentType || 'default').toLowerCase();
|
|
76
|
+
return AGENT_PROFILES[key] || AGENT_PROFILES.default;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Tier assembly helpers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function assembleSession() {
|
|
84
|
+
try {
|
|
85
|
+
return readSessionHandoff();
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assembleLessons(query, agentProfile, options = {}) {
|
|
92
|
+
try {
|
|
93
|
+
return retrieveRelevantLessons(
|
|
94
|
+
options.toolName || '',
|
|
95
|
+
query,
|
|
96
|
+
{ maxResults: agentProfile.maxLessons, feedbackDir: options.feedbackDir },
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assembleGuards(toolName, toolInput) {
|
|
104
|
+
try {
|
|
105
|
+
return evaluatePretool(toolName || '', toolInput || {});
|
|
106
|
+
} catch {
|
|
107
|
+
return { mode: 'allow', reason: 'guard-unavailable' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assembleContextPack(query, agentProfile) {
|
|
112
|
+
try {
|
|
113
|
+
ensureContextFs();
|
|
114
|
+
return constructContextPack({
|
|
115
|
+
query,
|
|
116
|
+
maxItems: Math.min(8, Math.ceil(agentProfile.contextBudget / 1000)),
|
|
117
|
+
maxChars: agentProfile.contextBudget,
|
|
118
|
+
});
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function assembleCodeGraph(query, repoPath, agentProfile) {
|
|
125
|
+
if (!agentProfile.includeCodeGraph) return null;
|
|
126
|
+
try {
|
|
127
|
+
const impact = analyzeCodeGraphImpact({
|
|
128
|
+
intentId: null,
|
|
129
|
+
context: query,
|
|
130
|
+
repoPath,
|
|
131
|
+
});
|
|
132
|
+
return formatCodeGraphRecallSection(impact) || null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function assembleUserProfile() {
|
|
139
|
+
try {
|
|
140
|
+
const profile = loadProfile();
|
|
141
|
+
if (!profile || !profile.entries || profile.entries.length === 0) return null;
|
|
142
|
+
return {
|
|
143
|
+
entries: profile.entries,
|
|
144
|
+
charCount: profile.charCount || 0,
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Tier classification
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
function classifyTier(components) {
|
|
156
|
+
const hasSession = !!components.session;
|
|
157
|
+
const hasLessons = components.lessons && components.lessons.length > 0;
|
|
158
|
+
const hasPack = !!components.contextPack;
|
|
159
|
+
|
|
160
|
+
if (hasSession && (hasLessons || hasPack)) return 'full';
|
|
161
|
+
if (hasLessons || hasPack) return 'warm';
|
|
162
|
+
return 'cold';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Main orchestrator
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Assemble a unified context object for a given query.
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} params
|
|
173
|
+
* @param {string} params.query - Task description / context query
|
|
174
|
+
* @param {string} [params.toolName] - Current tool being invoked (for lesson retrieval)
|
|
175
|
+
* @param {Object} [params.toolInput] - Current tool input (for guard evaluation)
|
|
176
|
+
* @param {string} [params.agentType] - Agent type: claude, cursor, forgecode, codex
|
|
177
|
+
* @param {string} [params.repoPath] - Repo path for code-graph analysis
|
|
178
|
+
* @param {string} [params.feedbackDir] - Override feedback directory
|
|
179
|
+
* @returns {Object} Normalized context object
|
|
180
|
+
*/
|
|
181
|
+
function assembleUnifiedContext(params = {}) {
|
|
182
|
+
const {
|
|
183
|
+
query = '',
|
|
184
|
+
toolName,
|
|
185
|
+
toolInput,
|
|
186
|
+
agentType,
|
|
187
|
+
repoPath,
|
|
188
|
+
feedbackDir,
|
|
189
|
+
} = params;
|
|
190
|
+
|
|
191
|
+
const agentProfile = getAgentProfile(agentType);
|
|
192
|
+
|
|
193
|
+
// Assemble all components — each is fault-tolerant
|
|
194
|
+
const session = assembleSession();
|
|
195
|
+
const userProfile = assembleUserProfile();
|
|
196
|
+
const lessons = assembleLessons(query, agentProfile, { toolName, feedbackDir });
|
|
197
|
+
const guards = assembleGuards(toolName, toolInput);
|
|
198
|
+
const contextPack = assembleContextPack(query, agentProfile);
|
|
199
|
+
const codeGraph = assembleCodeGraph(query, repoPath, agentProfile);
|
|
200
|
+
|
|
201
|
+
const components = { session, userProfile, lessons, guards, contextPack, codeGraph };
|
|
202
|
+
const tier = classifyTier(components);
|
|
203
|
+
|
|
204
|
+
const result = {
|
|
205
|
+
tier,
|
|
206
|
+
agentType: agentType || 'default',
|
|
207
|
+
agentProfile: {
|
|
208
|
+
maxLessons: agentProfile.maxLessons,
|
|
209
|
+
contextBudget: agentProfile.contextBudget,
|
|
210
|
+
includeCodeGraph: agentProfile.includeCodeGraph,
|
|
211
|
+
},
|
|
212
|
+
session: session || null,
|
|
213
|
+
userProfile: userProfile || null,
|
|
214
|
+
lessons,
|
|
215
|
+
guards,
|
|
216
|
+
contextPack: contextPack ? {
|
|
217
|
+
packId: contextPack.packId,
|
|
218
|
+
itemCount: Array.isArray(contextPack.items) ? contextPack.items.length : 0,
|
|
219
|
+
items: (contextPack.items || []).slice(0, 5).map((item) => ({
|
|
220
|
+
id: item.id,
|
|
221
|
+
namespace: item.namespace,
|
|
222
|
+
title: item.title,
|
|
223
|
+
tags: item.tags || [],
|
|
224
|
+
score: item.score,
|
|
225
|
+
})),
|
|
226
|
+
visibility: contextPack.visibility || null,
|
|
227
|
+
cached: !!(contextPack.cache && contextPack.cache.hit),
|
|
228
|
+
} : null,
|
|
229
|
+
codeGraph: codeGraph || null,
|
|
230
|
+
assembledAt: new Date().toISOString(),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Record provenance for audit trail
|
|
234
|
+
try {
|
|
235
|
+
recordProvenance({
|
|
236
|
+
type: 'unified_context_assembled',
|
|
237
|
+
tier,
|
|
238
|
+
agentType: result.agentType,
|
|
239
|
+
lessonCount: lessons.length,
|
|
240
|
+
guardDecision: guards.mode || 'allow',
|
|
241
|
+
hasSession: !!session,
|
|
242
|
+
hasUserProfile: !!userProfile,
|
|
243
|
+
hasCodeGraph: !!codeGraph,
|
|
244
|
+
packId: result.contextPack ? result.contextPack.packId : null,
|
|
245
|
+
});
|
|
246
|
+
} catch {
|
|
247
|
+
// Provenance write failure must never break context assembly
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Formatting for MCP tool response
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
function formatUnifiedContext(ctx) {
|
|
258
|
+
const lines = [];
|
|
259
|
+
|
|
260
|
+
lines.push(`## Unified Context (Tier: ${ctx.tier})`);
|
|
261
|
+
lines.push(`Agent: ${ctx.agentType} | Assembled: ${ctx.assembledAt}`);
|
|
262
|
+
lines.push('');
|
|
263
|
+
|
|
264
|
+
// Session
|
|
265
|
+
if (ctx.session) {
|
|
266
|
+
lines.push('### Session');
|
|
267
|
+
if (ctx.session.lastTask) lines.push(`Last task: ${ctx.session.lastTask}`);
|
|
268
|
+
if (ctx.session.nextStep) lines.push(`Next step: ${ctx.session.nextStep}`);
|
|
269
|
+
if (ctx.session.blockers && ctx.session.blockers.length > 0) {
|
|
270
|
+
lines.push(`Blockers: ${ctx.session.blockers.join(', ')}`);
|
|
271
|
+
}
|
|
272
|
+
lines.push('');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// User profile
|
|
276
|
+
if (ctx.userProfile) {
|
|
277
|
+
lines.push('### User Profile');
|
|
278
|
+
ctx.userProfile.entries.slice(0, 3).forEach((entry) => {
|
|
279
|
+
lines.push(`- ${entry.slice(0, 120)}`);
|
|
280
|
+
});
|
|
281
|
+
lines.push('');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Guards
|
|
285
|
+
if (ctx.guards && ctx.guards.mode !== 'allow') {
|
|
286
|
+
lines.push(`### Guard: ${ctx.guards.mode.toUpperCase()}`);
|
|
287
|
+
lines.push(ctx.guards.reason || 'No reason provided');
|
|
288
|
+
lines.push('');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Lessons
|
|
292
|
+
if (ctx.lessons && ctx.lessons.length > 0) {
|
|
293
|
+
lines.push(`### Lessons (${ctx.lessons.length})`);
|
|
294
|
+
ctx.lessons.forEach((lesson) => {
|
|
295
|
+
const signal = lesson.signal === 'negative' ? '[-]' : '[+]';
|
|
296
|
+
lines.push(`${signal} ${lesson.title || lesson.id} (score: ${lesson.relevanceScore})`);
|
|
297
|
+
if (lesson.rule) {
|
|
298
|
+
lines.push(` Rule: IF ${lesson.rule.condition || '?'} THEN ${lesson.rule.action || '?'}`);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
lines.push('');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Context pack
|
|
305
|
+
if (ctx.contextPack) {
|
|
306
|
+
lines.push(`### Context Pack (${ctx.contextPack.itemCount} items)`);
|
|
307
|
+
ctx.contextPack.items.forEach((item) => {
|
|
308
|
+
lines.push(`- [${item.namespace}] ${item.title} (score: ${item.score})`);
|
|
309
|
+
});
|
|
310
|
+
if (ctx.contextPack.cached) lines.push('(cached)');
|
|
311
|
+
lines.push('');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Code graph
|
|
315
|
+
if (ctx.codeGraph) {
|
|
316
|
+
lines.push('### Code Graph Impact');
|
|
317
|
+
lines.push(ctx.codeGraph);
|
|
318
|
+
lines.push('');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return lines.join('\n');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
assembleUnifiedContext,
|
|
326
|
+
formatUnifiedContext,
|
|
327
|
+
getAgentProfile,
|
|
328
|
+
AGENT_PROFILES,
|
|
329
|
+
classifyTier,
|
|
330
|
+
};
|
package/scripts/dashboard.js
CHANGED
|
@@ -523,7 +523,7 @@ function computeInstrumentationReadiness(analytics, billing) {
|
|
|
523
523
|
const cli = telemetry.cli || {};
|
|
524
524
|
|
|
525
525
|
return {
|
|
526
|
-
plausibleConfigured:
|
|
526
|
+
plausibleConfigured: /plausible\.io\/js\/script\.js|\/js\/analytics\.js/.test(landingPage),
|
|
527
527
|
ga4Configured: Boolean(runtimeConfig.gaMeasurementId),
|
|
528
528
|
googleSearchConsoleConfigured: Boolean(runtimeConfig.googleSiteVerification),
|
|
529
529
|
softwareApplicationSchemaPresent: /"@type": "SoftwareApplication"/.test(landingPage),
|
|
@@ -6,6 +6,7 @@ const path = require('node:path');
|
|
|
6
6
|
const ROOT = path.join(__dirname, '..');
|
|
7
7
|
const PRODUCTHUNT_URL = 'https://www.producthunt.com/products/thumbgate';
|
|
8
8
|
const CLAUDE_PLUGIN_LATEST_ASSET_NAME = 'thumbgate-claude-desktop.mcpb';
|
|
9
|
+
const CLAUDE_PLUGIN_NEXT_ASSET_NAME = 'thumbgate-claude-desktop-next.mcpb';
|
|
9
10
|
|
|
10
11
|
function readJson(root, relativePath) {
|
|
11
12
|
return JSON.parse(fs.readFileSync(path.join(root, relativePath), 'utf8'));
|
|
@@ -24,6 +25,14 @@ function getClaudePluginVersionedAssetName(version = getPackageVersion(ROOT)) {
|
|
|
24
25
|
return `thumbgate-claude-desktop-v${normalized}.mcpb`;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function isPrereleaseVersion(version = getPackageVersion(ROOT)) {
|
|
29
|
+
return /^\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/.test(String(version || '').trim());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getClaudePluginChannelAssetName(version = getPackageVersion(ROOT)) {
|
|
33
|
+
return isPrereleaseVersion(version) ? CLAUDE_PLUGIN_NEXT_ASSET_NAME : CLAUDE_PLUGIN_LATEST_ASSET_NAME;
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
function getClaudePluginLatestDownloadUrl(root = ROOT) {
|
|
28
37
|
return `${getRepositoryUrl(root)}/releases/latest/download/${CLAUDE_PLUGIN_LATEST_ASSET_NAME}`;
|
|
29
38
|
}
|
|
@@ -35,10 +44,13 @@ function getClaudePluginVersionedDownloadUrl(root = ROOT, version = getPackageVe
|
|
|
35
44
|
|
|
36
45
|
module.exports = {
|
|
37
46
|
CLAUDE_PLUGIN_LATEST_ASSET_NAME,
|
|
47
|
+
CLAUDE_PLUGIN_NEXT_ASSET_NAME,
|
|
38
48
|
PRODUCTHUNT_URL,
|
|
49
|
+
getClaudePluginChannelAssetName,
|
|
39
50
|
getClaudePluginLatestDownloadUrl,
|
|
40
51
|
getClaudePluginVersionedAssetName,
|
|
41
52
|
getClaudePluginVersionedDownloadUrl,
|
|
42
53
|
getPackageVersion,
|
|
43
54
|
getRepositoryUrl,
|
|
55
|
+
isPrereleaseVersion,
|
|
44
56
|
};
|
|
@@ -5,12 +5,13 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const REPO_ROOT = path.resolve(process.argv[2] || process.cwd());
|
|
8
|
-
const
|
|
8
|
+
const THUMBGATE_ENTRY = {
|
|
9
9
|
command: 'npx',
|
|
10
10
|
args: ['-y', 'thumbgate@latest', 'serve'],
|
|
11
11
|
};
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const MCP_SERVER_KEY = 'thumbgate';
|
|
13
|
+
const LEGACY_SERVER_NAMES = ['rlhf', 'mcp-memory-gateway', 'rlhf_feedback_loop'];
|
|
14
|
+
const INFO_EXCLUDE_ENTRIES = ['.thumbgate/', '.mcp.json'];
|
|
14
15
|
|
|
15
16
|
function readJson(filePath) {
|
|
16
17
|
try {
|
|
@@ -36,11 +37,11 @@ function writeJsonIfChanged(filePath, value) {
|
|
|
36
37
|
return true;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function
|
|
40
|
+
function mergeThumbgateEntry(entry = {}) {
|
|
40
41
|
return {
|
|
41
42
|
...entry,
|
|
42
|
-
command:
|
|
43
|
-
args:
|
|
43
|
+
command: THUMBGATE_ENTRY.command,
|
|
44
|
+
args: THUMBGATE_ENTRY.args.slice(),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -49,7 +50,7 @@ function ensureMcpJson(repoRoot) {
|
|
|
49
50
|
const existing = readJson(filePath);
|
|
50
51
|
const config = existing && typeof existing === 'object' ? existing : {};
|
|
51
52
|
config.mcpServers = config.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
|
|
52
|
-
config.mcpServers
|
|
53
|
+
config.mcpServers[MCP_SERVER_KEY] = mergeThumbgateEntry(config.mcpServers[MCP_SERVER_KEY]);
|
|
53
54
|
for (const legacyName of LEGACY_SERVER_NAMES) {
|
|
54
55
|
delete config.mcpServers[legacyName];
|
|
55
56
|
}
|
|
@@ -63,13 +64,13 @@ function ensureClaudeSettings(repoRoot) {
|
|
|
63
64
|
return false;
|
|
64
65
|
}
|
|
65
66
|
const hasRelevantServer =
|
|
66
|
-
Boolean(existing.mcpServers && existing.mcpServers
|
|
67
|
+
Boolean(existing.mcpServers && existing.mcpServers[MCP_SERVER_KEY]) ||
|
|
67
68
|
LEGACY_SERVER_NAMES.some((name) => Boolean(existing.mcpServers && existing.mcpServers[name]));
|
|
68
69
|
if (!hasRelevantServer) {
|
|
69
70
|
return false;
|
|
70
71
|
}
|
|
71
72
|
existing.mcpServers = existing.mcpServers && typeof existing.mcpServers === 'object' ? existing.mcpServers : {};
|
|
72
|
-
existing.mcpServers
|
|
73
|
+
existing.mcpServers[MCP_SERVER_KEY] = mergeThumbgateEntry(existing.mcpServers[MCP_SERVER_KEY]);
|
|
73
74
|
for (const legacyName of LEGACY_SERVER_NAMES) {
|
|
74
75
|
delete existing.mcpServers[legacyName];
|
|
75
76
|
}
|
|
@@ -106,19 +107,19 @@ function ensureInfoExclude(repoRoot) {
|
|
|
106
107
|
return true;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
function
|
|
110
|
-
const
|
|
111
|
-
if (fs.existsSync(
|
|
110
|
+
function ensureThumbgateDir(repoRoot) {
|
|
111
|
+
const thumbgateDir = path.join(repoRoot, '.thumbgate');
|
|
112
|
+
if (fs.existsSync(thumbgateDir)) {
|
|
112
113
|
return false;
|
|
113
114
|
}
|
|
114
|
-
fs.mkdirSync(
|
|
115
|
+
fs.mkdirSync(thumbgateDir, { recursive: true });
|
|
115
116
|
return true;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
function main() {
|
|
119
120
|
const results = {
|
|
120
121
|
repoRoot: REPO_ROOT,
|
|
121
|
-
|
|
122
|
+
createdThumbgateDir: ensureThumbgateDir(REPO_ROOT),
|
|
122
123
|
updatedMcpJson: ensureMcpJson(REPO_ROOT),
|
|
123
124
|
updatedClaudeSettings: ensureClaudeSettings(REPO_ROOT),
|
|
124
125
|
updatedInfoExclude: ensureInfoExclude(REPO_ROOT),
|
package/scripts/gates-engine.js
CHANGED
|
@@ -11,6 +11,9 @@ const {
|
|
|
11
11
|
DEFAULT_BASE_BRANCH,
|
|
12
12
|
evaluateOperationalIntegrity,
|
|
13
13
|
} = require('./operational-integrity');
|
|
14
|
+
const {
|
|
15
|
+
evaluateWorkflowSentinel,
|
|
16
|
+
} = require('./workflow-sentinel');
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
|
|
@@ -764,6 +767,16 @@ function buildReasoning(gate, toolName, toolInput, extras = {}) {
|
|
|
764
767
|
steps.push(`Memory guard matched (${extras.memoryGuard.source}): ${extras.memoryGuard.reason}`);
|
|
765
768
|
}
|
|
766
769
|
|
|
770
|
+
if (extras.workflowSentinel) {
|
|
771
|
+
steps.push(`Workflow sentinel risk: ${extras.workflowSentinel.band} (${extras.workflowSentinel.riskScore})`);
|
|
772
|
+
if (extras.workflowSentinel.blastRadius && extras.workflowSentinel.blastRadius.summary) {
|
|
773
|
+
steps.push(`Workflow sentinel blast radius: ${extras.workflowSentinel.blastRadius.summary}`);
|
|
774
|
+
}
|
|
775
|
+
for (const remediation of (extras.workflowSentinel.remediations || []).slice(0, 3)) {
|
|
776
|
+
steps.push(`Workflow sentinel remediation: ${remediation.title} — ${remediation.action}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
767
780
|
// 5. Unless condition status
|
|
768
781
|
if (gate.unless) {
|
|
769
782
|
steps.push(`Bypassable via satisfy_gate("${gate.unless}") — not currently satisfied`);
|
|
@@ -973,6 +986,39 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
973
986
|
};
|
|
974
987
|
}
|
|
975
988
|
|
|
989
|
+
function buildSentinelGateResult(report) {
|
|
990
|
+
return {
|
|
991
|
+
decision: report.decision,
|
|
992
|
+
gate: 'workflow-sentinel',
|
|
993
|
+
message: `${report.summary} ${report.blastRadius.summary}`,
|
|
994
|
+
severity: report.decision === 'deny' ? 'critical' : 'high',
|
|
995
|
+
reasoning: Array.isArray(report.reasoning) ? report.reasoning.slice() : [],
|
|
996
|
+
sentinel: report,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function enrichResultWithSentinel(result, report) {
|
|
1001
|
+
if (!result || !report || report.decision === 'allow') {
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const next = {
|
|
1006
|
+
...result,
|
|
1007
|
+
reasoning: Array.isArray(result.reasoning) ? result.reasoning.slice() : [],
|
|
1008
|
+
sentinel: report,
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
if (report.blastRadius && report.blastRadius.summary) {
|
|
1012
|
+
next.message = `${result.message} Workflow sentinel: ${report.blastRadius.summary}`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
next.reasoning = next.reasoning.concat(
|
|
1016
|
+
Array.isArray(report.reasoning) ? report.reasoning : []
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
return next;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
976
1022
|
async function checkMetricCondition(metricCondition) {
|
|
977
1023
|
if (!metricCondition) return true;
|
|
978
1024
|
const { getBusinessMetrics } = require('./semantic-layer');
|
|
@@ -1058,20 +1104,40 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1058
1104
|
}
|
|
1059
1105
|
}
|
|
1060
1106
|
|
|
1107
|
+
const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1108
|
+
governanceState: loadGovernanceState(),
|
|
1109
|
+
});
|
|
1061
1110
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1062
1111
|
if (memoryGuard) {
|
|
1063
|
-
|
|
1112
|
+
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
1113
|
+
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
1064
1114
|
const auditRecord = recordAuditEvent({
|
|
1065
1115
|
toolName,
|
|
1066
1116
|
toolInput,
|
|
1067
1117
|
decision: 'deny',
|
|
1068
|
-
gateId:
|
|
1069
|
-
message:
|
|
1070
|
-
severity:
|
|
1118
|
+
gateId: enrichedMemoryGuard.gate,
|
|
1119
|
+
message: enrichedMemoryGuard.message,
|
|
1120
|
+
severity: enrichedMemoryGuard.severity,
|
|
1071
1121
|
source: 'gates-engine',
|
|
1072
1122
|
});
|
|
1073
1123
|
auditToFeedback(auditRecord);
|
|
1074
|
-
return
|
|
1124
|
+
return enrichedMemoryGuard;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
1128
|
+
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
1129
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
1130
|
+
const auditRecord = recordAuditEvent({
|
|
1131
|
+
toolName,
|
|
1132
|
+
toolInput,
|
|
1133
|
+
decision: sentinelResult.decision,
|
|
1134
|
+
gateId: sentinelResult.gate,
|
|
1135
|
+
message: sentinelResult.message,
|
|
1136
|
+
severity: sentinelResult.severity,
|
|
1137
|
+
source: 'workflow-sentinel',
|
|
1138
|
+
});
|
|
1139
|
+
auditToFeedback(auditRecord);
|
|
1140
|
+
return sentinelResult;
|
|
1075
1141
|
}
|
|
1076
1142
|
|
|
1077
1143
|
// Audit trail: record allow (no gate matched)
|
|
@@ -1124,20 +1190,40 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1124
1190
|
}
|
|
1125
1191
|
}
|
|
1126
1192
|
|
|
1193
|
+
const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1194
|
+
governanceState: loadGovernanceState(),
|
|
1195
|
+
});
|
|
1127
1196
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1128
1197
|
if (memoryGuard) {
|
|
1129
|
-
|
|
1198
|
+
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
1199
|
+
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
1130
1200
|
const auditRecord = recordAuditEvent({
|
|
1131
1201
|
toolName,
|
|
1132
1202
|
toolInput,
|
|
1133
1203
|
decision: 'deny',
|
|
1134
|
-
gateId:
|
|
1135
|
-
message:
|
|
1136
|
-
severity:
|
|
1204
|
+
gateId: enrichedMemoryGuard.gate,
|
|
1205
|
+
message: enrichedMemoryGuard.message,
|
|
1206
|
+
severity: enrichedMemoryGuard.severity,
|
|
1137
1207
|
source: 'gates-engine',
|
|
1138
1208
|
});
|
|
1139
1209
|
auditToFeedback(auditRecord);
|
|
1140
|
-
return
|
|
1210
|
+
return enrichedMemoryGuard;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
1214
|
+
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
1215
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
1216
|
+
const auditRecord = recordAuditEvent({
|
|
1217
|
+
toolName,
|
|
1218
|
+
toolInput,
|
|
1219
|
+
decision: sentinelResult.decision,
|
|
1220
|
+
gateId: sentinelResult.gate,
|
|
1221
|
+
message: sentinelResult.message,
|
|
1222
|
+
severity: sentinelResult.severity,
|
|
1223
|
+
source: 'workflow-sentinel',
|
|
1224
|
+
});
|
|
1225
|
+
auditToFeedback(auditRecord);
|
|
1226
|
+
return sentinelResult;
|
|
1141
1227
|
}
|
|
1142
1228
|
|
|
1143
1229
|
// Audit trail: record allow
|
|
@@ -10,7 +10,7 @@ PROMPT_GUARD="$SCRIPT_DIR/prompt-guard.js"
|
|
|
10
10
|
ACTIVE_CWD="${CLAUDE_PROJECT_DIR:-${PWD:-$(pwd)}}"
|
|
11
11
|
FEEDBACK_DIR="$(node -e "const path = require('path'); const { resolveFeedbackDir } = require(path.join(process.argv[1], 'feedback-paths.js')); process.stdout.write(resolveFeedbackDir({ cwd: process.argv[2] || process.cwd(), feedbackDir: process.env.THUMBGATE_FEEDBACK_DIR || undefined }));" "$SCRIPT_DIR" "$ACTIVE_CWD" 2>/dev/null)"
|
|
12
12
|
if [ -z "$FEEDBACK_DIR" ]; then
|
|
13
|
-
FEEDBACK_DIR="${THUMBGATE_FEEDBACK_DIR:-$ACTIVE_CWD/.
|
|
13
|
+
FEEDBACK_DIR="${THUMBGATE_FEEDBACK_DIR:-$ACTIVE_CWD/.thumbgate}"
|
|
14
14
|
fi
|
|
15
15
|
FEEDBACK_LOG="$FEEDBACK_DIR/feedback-log.jsonl"
|
|
16
16
|
MEMORY_LOG="$FEEDBACK_DIR/memory-log.jsonl"
|