thumbgate 0.9.14 → 1.1.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 +41 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +18 -3
- package/config/mcp-allowlists.json +11 -0
- package/openapi/openapi.yaml +105 -0
- package/package.json +7 -5
- 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 +8 -4
- 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/export-hf-dataset.js +293 -0
- 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 +48 -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,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HuggingFace Dataset Exporter
|
|
6
|
+
*
|
|
7
|
+
* Exports ThumbGate agent traces as a HuggingFace-compatible dataset in two formats:
|
|
8
|
+
*
|
|
9
|
+
* 1. Agent Traces (traces split) — raw feedback entries with tool calls, signals,
|
|
10
|
+
* context, and outcomes. Matches the "share your agent traces" initiative.
|
|
11
|
+
*
|
|
12
|
+
* 2. DPO Preferences (preferences split) — chosen/rejected preference pairs
|
|
13
|
+
* derived from error→learning memory promotion. Ready for DPO/RLHF training.
|
|
14
|
+
*
|
|
15
|
+
* Output: Parquet-compatible JSONL files + dataset_info.json (HF Dataset Card metadata).
|
|
16
|
+
*
|
|
17
|
+
* HuggingFace Datasets format:
|
|
18
|
+
* dataset_dir/
|
|
19
|
+
* dataset_info.json — metadata, features schema, splits
|
|
20
|
+
* traces.jsonl — agent trace rows
|
|
21
|
+
* preferences.jsonl — DPO preference pair rows
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
27
|
+
const { exportDpoFromMemories } = require('./export-dpo-pairs');
|
|
28
|
+
const { getProvenance } = require('./contextfs');
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function readJSONL(filePath) {
|
|
35
|
+
if (!fs.existsSync(filePath)) return [];
|
|
36
|
+
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
37
|
+
if (!raw) return [];
|
|
38
|
+
return raw
|
|
39
|
+
.split('\n')
|
|
40
|
+
.map((line) => {
|
|
41
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureDir(dirPath) {
|
|
47
|
+
if (!fs.existsSync(dirPath)) {
|
|
48
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeJSONL(filePath, rows) {
|
|
53
|
+
const content = rows.map((row) => JSON.stringify(row)).join('\n');
|
|
54
|
+
fs.writeFileSync(filePath, content ? `${content}\n` : '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// PII / path redaction
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function redactPaths(text) {
|
|
62
|
+
if (!text || typeof text !== 'string') return text || '';
|
|
63
|
+
return text
|
|
64
|
+
.replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
|
|
65
|
+
.replace(/\/home\/[^\s/]+/g, '/home/redacted')
|
|
66
|
+
.replace(/C:\\Users\\[^\s\\]+/g, 'C:\\Users\\redacted');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function redactEntry(obj) {
|
|
70
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
out[key] = redactPaths(value);
|
|
75
|
+
} else if (Array.isArray(value)) {
|
|
76
|
+
out[key] = value.map((v) => (typeof v === 'string' ? redactPaths(v) : v));
|
|
77
|
+
} else {
|
|
78
|
+
out[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Trace row builder — converts feedback-log entries to HF trace rows
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function buildTraceRow(entry, index) {
|
|
89
|
+
return {
|
|
90
|
+
trace_id: entry.id || `trace_${index}`,
|
|
91
|
+
timestamp: entry.timestamp || null,
|
|
92
|
+
signal: entry.signal || entry.feedback || 'unknown',
|
|
93
|
+
tool_name: entry.toolName || entry.actionType || 'unknown',
|
|
94
|
+
context: redactPaths(entry.context || ''),
|
|
95
|
+
what_worked: redactPaths(entry.whatWorked || ''),
|
|
96
|
+
what_went_wrong: redactPaths(entry.whatWentWrong || ''),
|
|
97
|
+
what_to_change: redactPaths(entry.whatToChange || ''),
|
|
98
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
99
|
+
failure_type: entry.failureType || null,
|
|
100
|
+
source: 'thumbgate',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Preference row builder — converts DPO pairs to HF preference rows
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
function buildPreferenceRow(pair, index) {
|
|
109
|
+
return {
|
|
110
|
+
pair_id: `pref_${index}`,
|
|
111
|
+
prompt: redactPaths(pair.prompt || ''),
|
|
112
|
+
chosen: redactPaths(pair.chosen || ''),
|
|
113
|
+
rejected: redactPaths(pair.rejected || ''),
|
|
114
|
+
match_score: pair.metadata ? pair.metadata.matchScore : null,
|
|
115
|
+
matched_keys: pair.metadata ? pair.metadata.matchedKeys || [] : [],
|
|
116
|
+
rubric_delta: pair.metadata && pair.metadata.rubric
|
|
117
|
+
? pair.metadata.rubric.weightedDelta
|
|
118
|
+
: null,
|
|
119
|
+
source: 'thumbgate',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Dataset info (HuggingFace Dataset Card metadata)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
function buildDatasetInfo({ traceCount, preferenceCount, exportedAt }) {
|
|
128
|
+
return {
|
|
129
|
+
dataset_info: {
|
|
130
|
+
description: 'Agent traces and DPO preference pairs from ThumbGate — pre-action gates for AI coding agents. Contains real-world tool call feedback, failure patterns, and learned corrections.',
|
|
131
|
+
citation: '',
|
|
132
|
+
homepage: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
133
|
+
license: 'MIT',
|
|
134
|
+
features: {
|
|
135
|
+
traces: {
|
|
136
|
+
trace_id: { dtype: 'string' },
|
|
137
|
+
timestamp: { dtype: 'string' },
|
|
138
|
+
signal: { dtype: 'string' },
|
|
139
|
+
tool_name: { dtype: 'string' },
|
|
140
|
+
context: { dtype: 'string' },
|
|
141
|
+
what_worked: { dtype: 'string' },
|
|
142
|
+
what_went_wrong: { dtype: 'string' },
|
|
143
|
+
what_to_change: { dtype: 'string' },
|
|
144
|
+
tags: { dtype: 'list', inner: { dtype: 'string' } },
|
|
145
|
+
failure_type: { dtype: 'string' },
|
|
146
|
+
source: { dtype: 'string' },
|
|
147
|
+
},
|
|
148
|
+
preferences: {
|
|
149
|
+
pair_id: { dtype: 'string' },
|
|
150
|
+
prompt: { dtype: 'string' },
|
|
151
|
+
chosen: { dtype: 'string' },
|
|
152
|
+
rejected: { dtype: 'string' },
|
|
153
|
+
match_score: { dtype: 'float32' },
|
|
154
|
+
matched_keys: { dtype: 'list', inner: { dtype: 'string' } },
|
|
155
|
+
rubric_delta: { dtype: 'float32' },
|
|
156
|
+
source: { dtype: 'string' },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
splits: {
|
|
160
|
+
traces: { num_examples: traceCount },
|
|
161
|
+
preferences: { num_examples: preferenceCount },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
exported_at: exportedAt,
|
|
165
|
+
exporter: 'thumbgate/export-hf-dataset',
|
|
166
|
+
version: '1.0.0',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Main export function
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Export ThumbGate data as a HuggingFace-compatible dataset.
|
|
176
|
+
*
|
|
177
|
+
* @param {Object} options
|
|
178
|
+
* @param {string} [options.outputDir] - Directory to write dataset files
|
|
179
|
+
* @param {string} [options.feedbackDir] - Override feedback data directory
|
|
180
|
+
* @param {boolean} [options.includeProvenance] - Include provenance events in traces
|
|
181
|
+
* @returns {Object} Export summary
|
|
182
|
+
*/
|
|
183
|
+
function exportHfDataset(options = {}) {
|
|
184
|
+
const feedbackDir = options.feedbackDir || resolveFeedbackDir();
|
|
185
|
+
const outputDir = options.outputDir || path.join(feedbackDir, 'hf-dataset');
|
|
186
|
+
const includeProvenance = options.includeProvenance !== false;
|
|
187
|
+
|
|
188
|
+
ensureDir(outputDir);
|
|
189
|
+
|
|
190
|
+
// --- Traces split ---
|
|
191
|
+
const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
|
|
192
|
+
const feedbackEntries = readJSONL(feedbackLogPath);
|
|
193
|
+
const traceRows = feedbackEntries.map((entry, i) => buildTraceRow(redactEntry(entry), i));
|
|
194
|
+
|
|
195
|
+
// Optionally append provenance events as traces
|
|
196
|
+
if (includeProvenance) {
|
|
197
|
+
try {
|
|
198
|
+
const provenanceEvents = getProvenance(200);
|
|
199
|
+
for (const evt of provenanceEvents) {
|
|
200
|
+
traceRows.push({
|
|
201
|
+
trace_id: evt.id || `prov_${traceRows.length}`,
|
|
202
|
+
timestamp: evt.timestamp || null,
|
|
203
|
+
signal: 'provenance',
|
|
204
|
+
tool_name: evt.type || 'context_assembly',
|
|
205
|
+
context: redactPaths(JSON.stringify(evt).slice(0, 500)),
|
|
206
|
+
what_worked: '',
|
|
207
|
+
what_went_wrong: '',
|
|
208
|
+
what_to_change: '',
|
|
209
|
+
tags: ['provenance'],
|
|
210
|
+
failure_type: null,
|
|
211
|
+
source: 'thumbgate',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// Provenance read failure should not break export
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
writeJSONL(path.join(outputDir, 'traces.jsonl'), traceRows);
|
|
220
|
+
|
|
221
|
+
// --- Preferences split ---
|
|
222
|
+
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
223
|
+
const memories = readJSONL(memoryLogPath);
|
|
224
|
+
let preferenceRows = [];
|
|
225
|
+
|
|
226
|
+
if (memories.length > 0) {
|
|
227
|
+
try {
|
|
228
|
+
const dpoResult = exportDpoFromMemories(memories);
|
|
229
|
+
preferenceRows = dpoResult.pairs.map((pair, i) => buildPreferenceRow(pair, i));
|
|
230
|
+
} catch {
|
|
231
|
+
// DPO export failure should not break the traces export
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
writeJSONL(path.join(outputDir, 'preferences.jsonl'), preferenceRows);
|
|
236
|
+
|
|
237
|
+
// --- Dataset info ---
|
|
238
|
+
const exportedAt = new Date().toISOString();
|
|
239
|
+
const info = buildDatasetInfo({
|
|
240
|
+
traceCount: traceRows.length,
|
|
241
|
+
preferenceCount: preferenceRows.length,
|
|
242
|
+
exportedAt,
|
|
243
|
+
});
|
|
244
|
+
fs.writeFileSync(
|
|
245
|
+
path.join(outputDir, 'dataset_info.json'),
|
|
246
|
+
JSON.stringify(info, null, 2) + '\n',
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
outputDir,
|
|
251
|
+
traceCount: traceRows.length,
|
|
252
|
+
preferenceCount: preferenceRows.length,
|
|
253
|
+
files: ['traces.jsonl', 'preferences.jsonl', 'dataset_info.json'],
|
|
254
|
+
exportedAt,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// CLI
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
function main() {
|
|
263
|
+
const args = {};
|
|
264
|
+
process.argv.slice(2).forEach((arg) => {
|
|
265
|
+
if (!arg.startsWith('--')) return;
|
|
266
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
267
|
+
args[key] = rest.length ? rest.join('=') : true;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const result = exportHfDataset({
|
|
271
|
+
outputDir: args.output || undefined,
|
|
272
|
+
includeProvenance: args.provenance !== 'false',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
console.log(`Exported HuggingFace dataset to ${result.outputDir}`);
|
|
276
|
+
console.log(` Traces: ${result.traceCount}`);
|
|
277
|
+
console.log(` Preferences: ${result.preferenceCount}`);
|
|
278
|
+
console.log(` Files: ${result.files.join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (require.main === module) {
|
|
282
|
+
main();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
exportHfDataset,
|
|
287
|
+
buildTraceRow,
|
|
288
|
+
buildPreferenceRow,
|
|
289
|
+
buildDatasetInfo,
|
|
290
|
+
redactPaths,
|
|
291
|
+
redactEntry,
|
|
292
|
+
readJSONL,
|
|
293
|
+
};
|
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"
|