thumbgate 1.26.7 → 1.27.2
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +2 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +20 -9
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/mcp/server-stdio.js +28 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +147 -10
- package/bin/dashboard-cli.js +7 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +60 -0
- package/config/mcp-allowlists.json +8 -7
- package/config/model-candidates.json +71 -6
- package/package.json +26 -10
- package/public/chatgpt-app.html +330 -0
- package/public/codex-plugin.html +66 -14
- package/public/dashboard.html +203 -17
- package/public/index.html +79 -4
- package/public/learn.html +70 -0
- package/public/lessons.html +129 -6
- package/public/numbers.html +2 -2
- package/public/pricing.html +20 -2
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-schema.js +26 -0
- package/scripts/dashboard-chat.js +64 -17
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/gates-engine.js +258 -61
- package/scripts/hybrid-feedback-context.js +141 -7
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plausible-domain-config.js +86 -0
- package/scripts/plausible-server-events.js +4 -2
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +28 -12
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +80 -0
- package/scripts/seo-gsd.js +53 -0
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/tool-registry.js +37 -0
- package/scripts/workflow-sentinel.js +189 -4
- package/src/api/server.js +276 -10
package/scripts/gates-engine.js
CHANGED
|
@@ -20,6 +20,10 @@ const {
|
|
|
20
20
|
recordDecisionEvaluation,
|
|
21
21
|
recordDecisionOutcome,
|
|
22
22
|
} = require('./decision-journal');
|
|
23
|
+
const {
|
|
24
|
+
actionFingerprint,
|
|
25
|
+
sanitizeFeedbackText,
|
|
26
|
+
} = require('./feedback-sanitizer');
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
|
|
@@ -54,6 +58,8 @@ const {
|
|
|
54
58
|
scanHookInput,
|
|
55
59
|
buildSafeSummary,
|
|
56
60
|
redactText,
|
|
61
|
+
isSafeSecretStorageWrite,
|
|
62
|
+
SAFE_SECRET_STORAGE_DIRS,
|
|
57
63
|
} = require('./secret-scanner');
|
|
58
64
|
const {
|
|
59
65
|
evaluateSecurityScan,
|
|
@@ -109,7 +115,7 @@ const DEFAULT_PROTECTED_FILE_GLOBS = [
|
|
|
109
115
|
const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
110
116
|
const HIGH_RISK_BASH_PATTERN = /\b(?:git\s+(?:add|commit|push)|gh\s+pr\s+(?:create|merge)|npm\s+publish|yarn\s+publish|pnpm\s+publish|rm\s+-rf)\b/i;
|
|
111
117
|
const REMOTE_SIDE_EFFECT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+(?:create|merge|close|reopen|ready|edit)\b|gh\s+release\s+(?:create|delete|edit|upload)\b|npm\s+publish\b|yarn\s+publish\b|pnpm\s+publish\b)\b/i;
|
|
112
|
-
const
|
|
118
|
+
const MAX_COMMAND_SCAN_CHARS = 20000;
|
|
113
119
|
const BOOSTED_RISK_BLOCK_SCORE = 0.8;
|
|
114
120
|
const BOOSTED_RISK_MIN_EXAMPLES = 3;
|
|
115
121
|
const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
|
|
@@ -131,6 +137,60 @@ function isRuntimePlanGateEnabled() {
|
|
|
131
137
|
const PR_THREAD_RESOLUTION_CLAIM_PATTERN = '(?:thread|review|comment).*?(?:resolved|verified|checked|addressed|fixed)|(?:resolved|verified|checked|addressed|fixed).*?(?:thread|review|comment)';
|
|
132
138
|
const PR_THREAD_RESOLUTION_REQUIRED_ACTIONS = ['pr_threads_checked', 'thread_resolution_verified'];
|
|
133
139
|
|
|
140
|
+
function commandScanText(command) {
|
|
141
|
+
return String(command || '').slice(0, MAX_COMMAND_SCAN_CHARS);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function commandWords(command) {
|
|
145
|
+
return commandScanText(command).toLowerCase().split(/\s+/).filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function commandContainsSequence(words, sequence) {
|
|
149
|
+
if (!Array.isArray(words) || !Array.isArray(sequence) || sequence.length === 0) return false;
|
|
150
|
+
for (let i = 0; i <= words.length - sequence.length; i += 1) {
|
|
151
|
+
let matched = true;
|
|
152
|
+
for (let j = 0; j < sequence.length; j += 1) {
|
|
153
|
+
if (words[i + j] !== sequence[j]) {
|
|
154
|
+
matched = false;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (matched) return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function commandHasPostMethod(words) {
|
|
164
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
165
|
+
const word = words[i];
|
|
166
|
+
if ((word === '-x' || word === '--method') && words[i + 1] === 'post') return true;
|
|
167
|
+
if (word === '--method=post' || word === '-xpost') return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isGhApiPrCreateCommand(command) {
|
|
173
|
+
const words = commandWords(command);
|
|
174
|
+
if (!commandContainsSequence(words, ['gh', 'api'])) return false;
|
|
175
|
+
const hasPullsEndpoint = words.some((word) => word === '/pulls' || word.endsWith('/pulls'));
|
|
176
|
+
if (!hasPullsEndpoint) return false;
|
|
177
|
+
const fieldFlags = new Set(['-f', '--field', '--raw-field']);
|
|
178
|
+
const hasFieldWrite = words.some((word) => (
|
|
179
|
+
fieldFlags.has(word) ||
|
|
180
|
+
word.startsWith('-f=') ||
|
|
181
|
+
word.startsWith('--field=') ||
|
|
182
|
+
word.startsWith('--raw-field=')
|
|
183
|
+
));
|
|
184
|
+
return hasFieldWrite || commandHasPostMethod(words);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isRecursiveChmodCommand(command) {
|
|
188
|
+
const words = commandWords(command);
|
|
189
|
+
const chmodIndex = words.indexOf('chmod');
|
|
190
|
+
if (chmodIndex === -1) return false;
|
|
191
|
+
return words.slice(chmodIndex + 1).includes('-r') || words.slice(chmodIndex + 1).some((word) => word.includes('r') && word.startsWith('-'));
|
|
192
|
+
}
|
|
193
|
+
|
|
134
194
|
// ---------------------------------------------------------------------------
|
|
135
195
|
// Config loading
|
|
136
196
|
// ---------------------------------------------------------------------------
|
|
@@ -227,6 +287,27 @@ function sanitizeGlobList(globs) {
|
|
|
227
287
|
return [...new Set(globs.map((glob) => normalizeGlob(glob)).filter(Boolean))];
|
|
228
288
|
}
|
|
229
289
|
|
|
290
|
+
// Affected files are compared as repo-relative paths (git --name-only output and
|
|
291
|
+
// inline paths are relative to the repo root). A caller who passes an ABSOLUTE
|
|
292
|
+
// allowedPath (e.g. "/Users/me/proj/src/**") therefore declares a glob that can never
|
|
293
|
+
// match — a silent no-op scope. When repoPath is known, rebase absolute globs that
|
|
294
|
+
// live under it to the repo-relative form so the scope actually applies. Globs already
|
|
295
|
+
// relative, or absolute but outside repoPath, are returned unchanged.
|
|
296
|
+
function rebaseGlobsToRepoRoot(globs, repoPath) {
|
|
297
|
+
// normalizeGlob strips both leading AND trailing slashes, so a repoPath with a
|
|
298
|
+
// trailing slash ("/Users/me/proj/") still matches the repo-relative globs.
|
|
299
|
+
const repoRel = normalizeGlob(repoPath);
|
|
300
|
+
if (!repoRel) return globs;
|
|
301
|
+
return globs.map((glob) => {
|
|
302
|
+
if (glob === repoRel) return '**';
|
|
303
|
+
if (glob.startsWith(`${repoRel}/`)) {
|
|
304
|
+
const rebased = glob.slice(repoRel.length + 1);
|
|
305
|
+
return rebased || '**';
|
|
306
|
+
}
|
|
307
|
+
return glob;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
230
311
|
function globToRegExp(glob) {
|
|
231
312
|
const normalized = normalizeGlob(glob);
|
|
232
313
|
let pattern = '^';
|
|
@@ -280,6 +361,9 @@ function loadGovernanceState() {
|
|
|
280
361
|
branchGovernance: raw && raw.branchGovernance && typeof raw.branchGovernance === 'object'
|
|
281
362
|
? raw.branchGovernance
|
|
282
363
|
: null,
|
|
364
|
+
workflowContract: raw && raw.workflowContract && typeof raw.workflowContract === 'object'
|
|
365
|
+
? raw.workflowContract
|
|
366
|
+
: null,
|
|
283
367
|
};
|
|
284
368
|
const now = Date.now();
|
|
285
369
|
const activeApprovals = state.protectedApprovals.filter((entry) => {
|
|
@@ -299,6 +383,7 @@ function saveGovernanceState(state) {
|
|
|
299
383
|
taskScope: state && state.taskScope ? state.taskScope : null,
|
|
300
384
|
protectedApprovals: Array.isArray(state && state.protectedApprovals) ? state.protectedApprovals : [],
|
|
301
385
|
branchGovernance: state && state.branchGovernance ? state.branchGovernance : null,
|
|
386
|
+
workflowContract: state && state.workflowContract ? state.workflowContract : null,
|
|
302
387
|
};
|
|
303
388
|
saveJSON(module.exports.GOVERNANCE_STATE_PATH, next);
|
|
304
389
|
}
|
|
@@ -310,33 +395,39 @@ function setTaskScope(scopeInput = {}) {
|
|
|
310
395
|
taskScope: null,
|
|
311
396
|
protectedApprovals: currentState.protectedApprovals,
|
|
312
397
|
branchGovernance: currentState.branchGovernance,
|
|
398
|
+
workflowContract: null,
|
|
313
399
|
};
|
|
314
400
|
saveGovernanceState(cleared);
|
|
401
|
+
refreshLocalOnlyConstraint(cleared);
|
|
315
402
|
return null;
|
|
316
403
|
}
|
|
317
404
|
|
|
318
|
-
const
|
|
405
|
+
const repoPath = String(scopeInput.repoPath || '').trim() || null;
|
|
406
|
+
const allowedPaths = rebaseGlobsToRepoRoot(sanitizeGlobList(scopeInput.allowedPaths), repoPath);
|
|
319
407
|
if (allowedPaths.length === 0) {
|
|
320
408
|
throw new Error('allowedPaths must be a non-empty array');
|
|
321
409
|
}
|
|
322
410
|
|
|
323
|
-
const protectedPaths = sanitizeGlobList(
|
|
411
|
+
const protectedPaths = rebaseGlobsToRepoRoot(sanitizeGlobList(
|
|
324
412
|
Array.isArray(scopeInput.protectedPaths) && scopeInput.protectedPaths.length > 0
|
|
325
413
|
? scopeInput.protectedPaths
|
|
326
414
|
: DEFAULT_PROTECTED_FILE_GLOBS
|
|
327
|
-
);
|
|
415
|
+
), repoPath);
|
|
328
416
|
const taskScope = {
|
|
329
417
|
taskId: String(scopeInput.taskId || '').trim() || null,
|
|
330
418
|
summary: String(scopeInput.summary || '').trim() || null,
|
|
331
419
|
allowedPaths,
|
|
332
420
|
protectedPaths,
|
|
333
421
|
localOnly: scopeInput.localOnly === true,
|
|
334
|
-
repoPath
|
|
422
|
+
repoPath,
|
|
335
423
|
createdAt: new Date().toISOString(),
|
|
336
424
|
timestamp: Date.now(),
|
|
337
425
|
};
|
|
338
426
|
const state = loadGovernanceState();
|
|
339
427
|
state.taskScope = taskScope;
|
|
428
|
+
state.workflowContract = scopeInput.workflowContract && typeof scopeInput.workflowContract === 'object'
|
|
429
|
+
? scopeInput.workflowContract
|
|
430
|
+
: null;
|
|
340
431
|
saveGovernanceState(state);
|
|
341
432
|
if (taskScope.localOnly) {
|
|
342
433
|
setConstraint('local_only', true);
|
|
@@ -409,6 +500,7 @@ function setBranchGovernance(input = {}) {
|
|
|
409
500
|
const state = loadGovernanceState();
|
|
410
501
|
state.branchGovernance = null;
|
|
411
502
|
saveGovernanceState(state);
|
|
503
|
+
refreshLocalOnlyConstraint(state);
|
|
412
504
|
return null;
|
|
413
505
|
}
|
|
414
506
|
|
|
@@ -459,6 +551,24 @@ function setConstraint(key, value) {
|
|
|
459
551
|
return constraints[key];
|
|
460
552
|
}
|
|
461
553
|
|
|
554
|
+
function clearConstraint(key) {
|
|
555
|
+
const constraints = loadConstraints();
|
|
556
|
+
delete constraints[key];
|
|
557
|
+
saveConstraints(constraints);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function refreshLocalOnlyConstraint(governanceState = loadGovernanceState()) {
|
|
561
|
+
const localOnlyActive = Boolean(
|
|
562
|
+
(governanceState.taskScope && governanceState.taskScope.localOnly) ||
|
|
563
|
+
(governanceState.branchGovernance && governanceState.branchGovernance.localOnly)
|
|
564
|
+
);
|
|
565
|
+
if (localOnlyActive) {
|
|
566
|
+
setConstraint('local_only', true);
|
|
567
|
+
} else {
|
|
568
|
+
clearConstraint('local_only');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
462
572
|
function isConditionSatisfied(conditionId) {
|
|
463
573
|
const state = loadState();
|
|
464
574
|
const entry = state[conditionId];
|
|
@@ -498,7 +608,29 @@ function loadStats() {
|
|
|
498
608
|
|
|
499
609
|
function saveStats(stats) { saveJSON(module.exports.STATS_PATH, stats); }
|
|
500
610
|
|
|
501
|
-
function
|
|
611
|
+
function buildGateActionFingerprint(gateId, options = {}) {
|
|
612
|
+
if (options.actionFingerprint) return String(options.actionFingerprint);
|
|
613
|
+
const toolName = options.toolName || options.tool_name || '';
|
|
614
|
+
const toolInput = options.toolInput || options.tool_input || {};
|
|
615
|
+
const parts = [toolName];
|
|
616
|
+
if (typeof toolInput === 'string') {
|
|
617
|
+
parts.push(toolInput);
|
|
618
|
+
} else if (toolInput && typeof toolInput === 'object') {
|
|
619
|
+
parts.push(
|
|
620
|
+
toolInput.command || '',
|
|
621
|
+
toolInput.cmd || '',
|
|
622
|
+
toolInput.file_path || '',
|
|
623
|
+
toolInput.path || '',
|
|
624
|
+
toolInput.description || '',
|
|
625
|
+
toolInput.prompt || '',
|
|
626
|
+
toolInput.pattern || '',
|
|
627
|
+
);
|
|
628
|
+
if (Array.isArray(toolInput.affectedFiles)) parts.push(...toolInput.affectedFiles);
|
|
629
|
+
}
|
|
630
|
+
return actionFingerprint(parts);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function recordStat(gateId, action, gate, options = {}) {
|
|
502
634
|
const stats = loadStats();
|
|
503
635
|
if (action === 'block') stats.blocked = (stats.blocked || 0) + 1;
|
|
504
636
|
else if (action === 'warn') stats.warned = (stats.warned || 0) + 1;
|
|
@@ -512,16 +644,25 @@ function recordStat(gateId, action, gate) {
|
|
|
512
644
|
else if (action === 'approve') stats.byGate[gateId].pendingApproval = (stats.byGate[gateId].pendingApproval || 0) + 1;
|
|
513
645
|
else if (action === 'log') stats.byGate[gateId].logged = (stats.byGate[gateId].logged || 0) + 1;
|
|
514
646
|
|
|
515
|
-
// Track
|
|
647
|
+
// Track same-action recurrence within a session for first-time fix rate.
|
|
648
|
+
// Gate-only recurrence over-counts noisy gates; repeats require a stable,
|
|
649
|
+
// sanitized action fingerprint.
|
|
516
650
|
if (action === 'block' || action === 'warn') {
|
|
517
651
|
if (!stats.sessionFiredGates) stats.sessionFiredGates = {};
|
|
652
|
+
if (!stats.sessionFiredActions) stats.sessionFiredActions = {};
|
|
518
653
|
const sessionKey = `session_${Math.floor(Date.now() / SESSION_ACTION_TTL_MS)}`;
|
|
519
654
|
if (!stats.sessionFiredGates[sessionKey]) stats.sessionFiredGates[sessionKey] = {};
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
stats.
|
|
655
|
+
stats.sessionFiredGates[sessionKey][gateId] = true;
|
|
656
|
+
|
|
657
|
+
const fingerprint = buildGateActionFingerprint(gateId, options);
|
|
658
|
+
if (fingerprint) {
|
|
659
|
+
if (!stats.sessionFiredActions[sessionKey]) stats.sessionFiredActions[sessionKey] = {};
|
|
660
|
+
if (!stats.sessionFiredActions[sessionKey][gateId]) stats.sessionFiredActions[sessionKey][gateId] = {};
|
|
661
|
+
if (stats.sessionFiredActions[sessionKey][gateId][fingerprint]) {
|
|
662
|
+
stats.recurringBlocks = (stats.recurringBlocks || 0) + 1;
|
|
663
|
+
} else {
|
|
664
|
+
stats.sessionFiredActions[sessionKey][gateId][fingerprint] = true;
|
|
665
|
+
}
|
|
525
666
|
}
|
|
526
667
|
}
|
|
527
668
|
|
|
@@ -737,7 +878,7 @@ function extractAffectedFiles(toolName, toolInput = {}) {
|
|
|
737
878
|
}
|
|
738
879
|
}
|
|
739
880
|
|
|
740
|
-
if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command) ||
|
|
881
|
+
if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command) || isGhApiPrCreateCommand(command)) {
|
|
741
882
|
for (const filePath of getBranchDiffFiles(repoRoot)) {
|
|
742
883
|
files.add(normalizePosix(filePath));
|
|
743
884
|
}
|
|
@@ -756,7 +897,7 @@ function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
|
|
|
756
897
|
const command = String(toolInput.command || '');
|
|
757
898
|
// Original high-risk pattern (git writes, publishes, destructive ops)
|
|
758
899
|
if (HIGH_RISK_BASH_PATTERN.test(command)) return true;
|
|
759
|
-
if (
|
|
900
|
+
if (isGhApiPrCreateCommand(command)) return true;
|
|
760
901
|
// Broadened: any Bash command that modifies files or has side effects.
|
|
761
902
|
// Excludes pure read/analysis commands (node --test, cat, ls, echo, etc.)
|
|
762
903
|
// to avoid false positives on benign operations.
|
|
@@ -995,7 +1136,7 @@ function getLocalOnlyScopeSources(governanceState = {}, constraints = {}) {
|
|
|
995
1136
|
function isRemoteSideEffectCommand(toolName, toolInput = {}) {
|
|
996
1137
|
if (toolName !== 'Bash') return false;
|
|
997
1138
|
const command = String(toolInput.command || '');
|
|
998
|
-
return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(command) ||
|
|
1139
|
+
return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(command) || isGhApiPrCreateCommand(command);
|
|
999
1140
|
}
|
|
1000
1141
|
|
|
1001
1142
|
function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governanceState = {}, constraints = {}) {
|
|
@@ -1018,7 +1159,7 @@ function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governa
|
|
|
1018
1159
|
}
|
|
1019
1160
|
|
|
1020
1161
|
function recordStructuralGateBlock(toolName, toolInput, result) {
|
|
1021
|
-
recordStat(result.gate, 'block');
|
|
1162
|
+
recordStat(result.gate, 'block', null, { toolName, toolInput });
|
|
1022
1163
|
const auditRecord = recordAuditEvent({
|
|
1023
1164
|
toolName,
|
|
1024
1165
|
toolInput,
|
|
@@ -1379,7 +1520,7 @@ function matchesGate(gate, toolName, toolInput) {
|
|
|
1379
1520
|
function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
|
|
1380
1521
|
if (toolName !== 'Bash') return false;
|
|
1381
1522
|
const command = String(toolInput.command || '').trim();
|
|
1382
|
-
if (!command ||
|
|
1523
|
+
if (!command || isRecursiveChmodCommand(command)) return false;
|
|
1383
1524
|
if (/[;&|`$()<>*?[\]{}]/.test(command)) return false;
|
|
1384
1525
|
|
|
1385
1526
|
const match = command.match(/(?:^|\s)chmod\s+(?:-[fv]\s+)?0?([46]00)\s+(['"]?)(\S+)\2\s*$/i);
|
|
@@ -1387,7 +1528,7 @@ function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
|
|
|
1387
1528
|
|
|
1388
1529
|
const target = match[3];
|
|
1389
1530
|
if (!target || target === '/' || target === '~') return false;
|
|
1390
|
-
if (
|
|
1531
|
+
if (target.includes('..')) return false;
|
|
1391
1532
|
|
|
1392
1533
|
const normalized = target.replace(/^['"]|['"]$/g, '').toLowerCase();
|
|
1393
1534
|
const looksLikeCredentialPath = /(?:^|\/)(?:\.config|\.ssh|\.gnupg|\.aws|\.gcloud|\.gemini|\.resume_secrets|\.thumbgate|secrets?|credentials?)(?:\/|$)/.test(normalized)
|
|
@@ -1400,6 +1541,16 @@ function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
|
|
|
1400
1541
|
function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
1401
1542
|
const affected = extractAffectedFiles(toolName, toolInput);
|
|
1402
1543
|
const affectedFiles = affected.files;
|
|
1544
|
+
if (isSafeSecretStorageWrite(toolName, toolInput, process.cwd())) {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
// Hardening a credential file's permissions (chmod 600 on a key/secret path) is
|
|
1548
|
+
// a safety action, not a risk. The same exemption already guards the
|
|
1549
|
+
// permission-change-approval gate; without it here, `chmod 600 ~/.resume_secrets/key`
|
|
1550
|
+
// gets hard-denied by recurring-negative-memory matching — the opposite of intent.
|
|
1551
|
+
if (isSafeLocalCredentialHardeningCommand(toolName, toolInput)) {
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1403
1554
|
if (!isHighRiskAction(toolName, toolInput, affectedFiles)) {
|
|
1404
1555
|
return null;
|
|
1405
1556
|
}
|
|
@@ -1414,7 +1565,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
1414
1565
|
|
|
1415
1566
|
const command = String(toolInput.command || '');
|
|
1416
1567
|
const isPrCreateCommand = toolName === 'Bash' && (
|
|
1417
|
-
/\bgh\s+pr\s+create\b/i.test(command) ||
|
|
1568
|
+
/\bgh\s+pr\s+create\b/i.test(command) || isGhApiPrCreateCommand(command)
|
|
1418
1569
|
);
|
|
1419
1570
|
if (isPrCreateCommand && isConditionSatisfied('pr_create_allowed')) {
|
|
1420
1571
|
const branchGovernanceViolation = buildBranchGovernanceViolation(
|
|
@@ -1431,7 +1582,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
1431
1582
|
|
|
1432
1583
|
if (toolName === 'Bash' && (
|
|
1433
1584
|
/\b(?:gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|git\s+tag\b|(?:npm|yarn|pnpm)\s+publish\b)\b/i.test(command) ||
|
|
1434
|
-
|
|
1585
|
+
isGhApiPrCreateCommand(command)
|
|
1435
1586
|
)) {
|
|
1436
1587
|
const branchGovernanceViolation = buildBranchGovernanceViolation(
|
|
1437
1588
|
governanceState,
|
|
@@ -1470,7 +1621,14 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
1470
1621
|
filePath: toolInput.file_path || toolInput.path || null,
|
|
1471
1622
|
affectedFiles,
|
|
1472
1623
|
});
|
|
1473
|
-
|
|
1624
|
+
// Claw/hybrid support: pass context if agent provides claw metadata (for EnterpriseClaw/OpenShell/Perplexity hybrid agents)
|
|
1625
|
+
let guard;
|
|
1626
|
+
if (toolInput && (toolInput.clawContext || toolInput._claw || toolInput.hybridRoute || toolInput.agentId)) {
|
|
1627
|
+
const clawCtx = toolInput.clawContext || toolInput._claw || { actionType: toolInput.actionType || 'unknown', agentId: toolInput.agentId || 'unknown', hybridRoute: toolInput.hybridRoute || 'unknown' };
|
|
1628
|
+
guard = hybrid.evaluateClawPretool ? hybrid.evaluateClawPretool(toolName, serializedInput, clawCtx) : hybrid.evaluatePretool(toolName, serializedInput);
|
|
1629
|
+
} else {
|
|
1630
|
+
guard = hybrid.evaluatePretool(toolName, serializedInput);
|
|
1631
|
+
}
|
|
1474
1632
|
if (!guard || guard.mode === 'allow') {
|
|
1475
1633
|
return null;
|
|
1476
1634
|
}
|
|
@@ -1623,7 +1781,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1623
1781
|
|
|
1624
1782
|
const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
|
|
1625
1783
|
if (pendingThreadResolutionGate) {
|
|
1626
|
-
recordStat(pendingThreadResolutionGate.gate, 'block');
|
|
1784
|
+
recordStat(pendingThreadResolutionGate.gate, 'block', null, { toolName, toolInput });
|
|
1627
1785
|
const auditRecord = recordAuditEvent({
|
|
1628
1786
|
toolName,
|
|
1629
1787
|
toolInput,
|
|
@@ -1639,7 +1797,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1639
1797
|
|
|
1640
1798
|
const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
|
|
1641
1799
|
if (boostedRiskGuard) {
|
|
1642
|
-
recordStat(boostedRiskGuard.gate, 'block');
|
|
1800
|
+
recordStat(boostedRiskGuard.gate, 'block', null, { toolName, toolInput });
|
|
1643
1801
|
const auditRecord = recordAuditEvent({
|
|
1644
1802
|
toolName,
|
|
1645
1803
|
toolInput,
|
|
@@ -1659,13 +1817,13 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1659
1817
|
if (isRuntimePlanGateEnabled()) {
|
|
1660
1818
|
const planGate = evaluatePlanGate(toolName, toolInput);
|
|
1661
1819
|
if (planGate) {
|
|
1662
|
-
recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn');
|
|
1820
|
+
recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
|
|
1663
1821
|
return planGate;
|
|
1664
1822
|
}
|
|
1665
1823
|
|
|
1666
1824
|
const trajectory = getTrajectoryScore();
|
|
1667
1825
|
if (trajectory.isDrifting) {
|
|
1668
|
-
recordStat('strategic-drift', 'block');
|
|
1826
|
+
recordStat('strategic-drift', 'block', null, { toolName, toolInput });
|
|
1669
1827
|
return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
|
|
1670
1828
|
}
|
|
1671
1829
|
}
|
|
@@ -1719,12 +1877,12 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1719
1877
|
// Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
|
|
1720
1878
|
const cappedResult = applyDailyBlockCap(denyResult);
|
|
1721
1879
|
if (cappedResult) {
|
|
1722
|
-
recordStat(gate.id, 'warn', gate);
|
|
1880
|
+
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
1723
1881
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
|
|
1724
1882
|
auditToFeedback(auditRecord);
|
|
1725
1883
|
return cappedResult;
|
|
1726
1884
|
}
|
|
1727
|
-
recordStat(gate.id, 'block', gate);
|
|
1885
|
+
recordStat(gate.id, 'block', gate, { toolName, toolInput });
|
|
1728
1886
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1729
1887
|
auditToFeedback(auditRecord);
|
|
1730
1888
|
return denyResult;
|
|
@@ -1733,13 +1891,13 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1733
1891
|
if (gate.action === 'approve') {
|
|
1734
1892
|
const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
|
|
1735
1893
|
if (approvalEnabled) {
|
|
1736
|
-
recordStat(gate.id, 'approve', gate);
|
|
1894
|
+
recordStat(gate.id, 'approve', gate, { toolName, toolInput });
|
|
1737
1895
|
const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
|
|
1738
1896
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1739
1897
|
auditToFeedback(auditRecord);
|
|
1740
1898
|
return result;
|
|
1741
1899
|
}
|
|
1742
|
-
recordStat(gate.id, 'warn', gate);
|
|
1900
|
+
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
1743
1901
|
const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
|
|
1744
1902
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1745
1903
|
auditToFeedback(auditRecord);
|
|
@@ -1747,7 +1905,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1747
1905
|
}
|
|
1748
1906
|
|
|
1749
1907
|
if (gate.action === 'log') {
|
|
1750
|
-
recordStat(gate.id, 'log', gate);
|
|
1908
|
+
recordStat(gate.id, 'log', gate, { toolName, toolInput });
|
|
1751
1909
|
const result = { decision: 'log', gate: gate.id, message, severity: gate.severity, reasoning, logged: true };
|
|
1752
1910
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1753
1911
|
auditToFeedback(auditRecord);
|
|
@@ -1756,7 +1914,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1756
1914
|
}
|
|
1757
1915
|
|
|
1758
1916
|
if (gate.action === 'warn') {
|
|
1759
|
-
recordStat(gate.id, 'warn', gate);
|
|
1917
|
+
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
1760
1918
|
const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1761
1919
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1762
1920
|
auditToFeedback(auditRecord);
|
|
@@ -1764,14 +1922,15 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1764
1922
|
}
|
|
1765
1923
|
}
|
|
1766
1924
|
|
|
1767
|
-
const
|
|
1925
|
+
const skipAdvisoryGuards = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
|
|
1926
|
+
const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1768
1927
|
governanceState,
|
|
1769
1928
|
});
|
|
1770
1929
|
const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
|
|
1771
1930
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1772
1931
|
if (memoryGuard) {
|
|
1773
1932
|
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
1774
|
-
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
1933
|
+
recordStat(enrichedMemoryGuard.gate, 'block', null, { toolName, toolInput });
|
|
1775
1934
|
recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
|
|
1776
1935
|
const auditRecord = recordAuditEvent({
|
|
1777
1936
|
toolName,
|
|
@@ -1788,7 +1947,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1788
1947
|
|
|
1789
1948
|
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
1790
1949
|
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
1791
|
-
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
1950
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
|
|
1792
1951
|
recordSentinelBlockDecision(sentinelDecision, sentinelResult);
|
|
1793
1952
|
const auditRecord = recordAuditEvent({
|
|
1794
1953
|
toolName,
|
|
@@ -1849,7 +2008,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1849
2008
|
|
|
1850
2009
|
const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
|
|
1851
2010
|
if (pendingThreadResolutionGate) {
|
|
1852
|
-
recordStat(pendingThreadResolutionGate.gate, 'block');
|
|
2011
|
+
recordStat(pendingThreadResolutionGate.gate, 'block', null, { toolName, toolInput });
|
|
1853
2012
|
const auditRecord = recordAuditEvent({
|
|
1854
2013
|
toolName,
|
|
1855
2014
|
toolInput,
|
|
@@ -1865,7 +2024,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1865
2024
|
|
|
1866
2025
|
const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
|
|
1867
2026
|
if (boostedRiskGuard) {
|
|
1868
|
-
recordStat(boostedRiskGuard.gate, 'block');
|
|
2027
|
+
recordStat(boostedRiskGuard.gate, 'block', null, { toolName, toolInput });
|
|
1869
2028
|
const auditRecord = recordAuditEvent({
|
|
1870
2029
|
toolName,
|
|
1871
2030
|
toolInput,
|
|
@@ -1885,13 +2044,13 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1885
2044
|
if (isRuntimePlanGateEnabled()) {
|
|
1886
2045
|
const planGate = evaluatePlanGate(toolName, toolInput);
|
|
1887
2046
|
if (planGate) {
|
|
1888
|
-
recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn');
|
|
2047
|
+
recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
|
|
1889
2048
|
return planGate;
|
|
1890
2049
|
}
|
|
1891
2050
|
|
|
1892
2051
|
const trajectory = getTrajectoryScore();
|
|
1893
2052
|
if (trajectory.isDrifting) {
|
|
1894
|
-
recordStat('strategic-drift', 'block');
|
|
2053
|
+
recordStat('strategic-drift', 'block', null, { toolName, toolInput });
|
|
1895
2054
|
return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
|
|
1896
2055
|
}
|
|
1897
2056
|
}
|
|
@@ -1918,12 +2077,12 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1918
2077
|
// Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
|
|
1919
2078
|
const cappedResult = applyDailyBlockCap(denyResult);
|
|
1920
2079
|
if (cappedResult) {
|
|
1921
|
-
recordStat(gate.id, 'warn', gate);
|
|
2080
|
+
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
1922
2081
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
|
|
1923
2082
|
auditToFeedback(auditRecord);
|
|
1924
2083
|
return cappedResult;
|
|
1925
2084
|
}
|
|
1926
|
-
recordStat(gate.id, 'block', gate);
|
|
2085
|
+
recordStat(gate.id, 'block', gate, { toolName, toolInput });
|
|
1927
2086
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1928
2087
|
auditToFeedback(auditRecord);
|
|
1929
2088
|
return denyResult;
|
|
@@ -1932,13 +2091,13 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1932
2091
|
if (gate.action === 'approve') {
|
|
1933
2092
|
const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
|
|
1934
2093
|
if (approvalEnabled) {
|
|
1935
|
-
recordStat(gate.id, 'approve', gate);
|
|
2094
|
+
recordStat(gate.id, 'approve', gate, { toolName, toolInput });
|
|
1936
2095
|
const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
|
|
1937
2096
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1938
2097
|
auditToFeedback(auditRecord);
|
|
1939
2098
|
return result;
|
|
1940
2099
|
}
|
|
1941
|
-
recordStat(gate.id, 'warn', gate);
|
|
2100
|
+
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
1942
2101
|
const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
|
|
1943
2102
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1944
2103
|
auditToFeedback(auditRecord);
|
|
@@ -1946,7 +2105,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1946
2105
|
}
|
|
1947
2106
|
|
|
1948
2107
|
if (gate.action === 'log') {
|
|
1949
|
-
recordStat(gate.id, 'log', gate);
|
|
2108
|
+
recordStat(gate.id, 'log', gate, { toolName, toolInput });
|
|
1950
2109
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1951
2110
|
auditToFeedback(auditRecord);
|
|
1952
2111
|
// 'log' action allows the tool call to proceed — continue to next gate
|
|
@@ -1954,7 +2113,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1954
2113
|
}
|
|
1955
2114
|
|
|
1956
2115
|
if (gate.action === 'warn') {
|
|
1957
|
-
recordStat(gate.id, 'warn', gate);
|
|
2116
|
+
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
1958
2117
|
const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1959
2118
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1960
2119
|
auditToFeedback(auditRecord);
|
|
@@ -1962,14 +2121,15 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1962
2121
|
}
|
|
1963
2122
|
}
|
|
1964
2123
|
|
|
1965
|
-
const
|
|
2124
|
+
const skipAdvisoryGuards = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
|
|
2125
|
+
const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1966
2126
|
governanceState,
|
|
1967
2127
|
});
|
|
1968
2128
|
const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
|
|
1969
2129
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1970
2130
|
if (memoryGuard) {
|
|
1971
2131
|
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
1972
|
-
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
2132
|
+
recordStat(enrichedMemoryGuard.gate, 'block', null, { toolName, toolInput });
|
|
1973
2133
|
recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
|
|
1974
2134
|
const auditRecord = recordAuditEvent({
|
|
1975
2135
|
toolName,
|
|
@@ -1986,7 +2146,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1986
2146
|
|
|
1987
2147
|
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
1988
2148
|
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
1989
|
-
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
2149
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
|
|
1990
2150
|
recordSentinelBlockDecision(sentinelDecision, sentinelResult);
|
|
1991
2151
|
const auditRecord = recordAuditEvent({
|
|
1992
2152
|
toolName,
|
|
@@ -2006,14 +2166,43 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2006
2166
|
return null;
|
|
2007
2167
|
}
|
|
2008
2168
|
|
|
2009
|
-
|
|
2169
|
+
// Turn a secret-exfiltration block into actionable guidance that names the
|
|
2170
|
+
// safe path, instead of a dead-end that drives agents toward brittle
|
|
2171
|
+
// workarounds (e.g. writing secrets to /tmp). The vault dirs referenced here
|
|
2172
|
+
// are the SAME constant the scanner whitelists, so the hint can never drift
|
|
2173
|
+
// from enforcement.
|
|
2174
|
+
function buildSecretRemediation(toolName = '', toolInput = {}) {
|
|
2175
|
+
const vaultDirs = (SAFE_SECRET_STORAGE_DIRS || []).map((dir) => `~/${dir}`);
|
|
2176
|
+
const primaryVault = vaultDirs[0] || '~/.resume_secrets';
|
|
2177
|
+
const vaultList = vaultDirs.join(', ') || primaryVault;
|
|
2178
|
+
|
|
2179
|
+
if (EDIT_LIKE_TOOLS && EDIT_LIKE_TOOLS.has(toolName)) {
|
|
2180
|
+
const target = toolInput.file_path || toolInput.path || toolInput.filePath || toolInput.target_path;
|
|
2181
|
+
const where = target ? ` (you targeted ${redactText(String(target))})` : '';
|
|
2182
|
+
return `To store this secret safely, write it with the Write/Edit tool to a file under ${vaultList}${where}. `
|
|
2183
|
+
+ `Those locations are whitelisted for secret storage and will NOT be blocked. `
|
|
2184
|
+
+ `Do not route around this by writing to /tmp or another path — that leaves the secret in a world-readable location and does not make it safe.`;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
if (toolName === 'Bash') {
|
|
2188
|
+
return `Do not inline a live secret literal into a shell command — it leaks into shell history and process args. `
|
|
2189
|
+
+ `Instead, store the value with the Write tool to a file under ${vaultList}, then reference it via an environment variable or by reading that file at runtime.`;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
return `Store secrets in the whitelisted vault (${vaultList}) using the Write tool rather than passing the literal through this action.`;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function buildSecretGuardResult(scanResult, context = {}) {
|
|
2196
|
+
const remediation = buildSecretRemediation(context.toolName, context.toolInput || {});
|
|
2197
|
+
const summary = buildSafeSummary(
|
|
2198
|
+
scanResult.findings,
|
|
2199
|
+
'Blocked because the action appears to expose secret material'
|
|
2200
|
+
);
|
|
2010
2201
|
return {
|
|
2011
2202
|
decision: 'deny',
|
|
2012
2203
|
gate: 'secret-exfiltration',
|
|
2013
|
-
message:
|
|
2014
|
-
|
|
2015
|
-
'Blocked because the action appears to expose secret material'
|
|
2016
|
-
),
|
|
2204
|
+
message: `${summary}. ${remediation}`,
|
|
2205
|
+
remediation,
|
|
2017
2206
|
severity: 'critical',
|
|
2018
2207
|
secretScan: {
|
|
2019
2208
|
provider: scanResult.provider,
|
|
@@ -2092,9 +2281,15 @@ function evaluateSecretGuard(input = {}) {
|
|
|
2092
2281
|
if (!scanResult.detected) {
|
|
2093
2282
|
return null;
|
|
2094
2283
|
}
|
|
2095
|
-
recordStat('secret-exfiltration', 'block'
|
|
2284
|
+
recordStat('secret-exfiltration', 'block', null, {
|
|
2285
|
+
toolName: input.tool_name || input.toolName || 'unknown',
|
|
2286
|
+
toolInput: input.tool_input || {},
|
|
2287
|
+
});
|
|
2096
2288
|
recordSecretViolation(input, scanResult);
|
|
2097
|
-
const result = buildSecretGuardResult(scanResult
|
|
2289
|
+
const result = buildSecretGuardResult(scanResult, {
|
|
2290
|
+
toolName: input.tool_name || input.toolName,
|
|
2291
|
+
toolInput: input.tool_input || {},
|
|
2292
|
+
});
|
|
2098
2293
|
// Audit trail: record secret guard denial
|
|
2099
2294
|
const auditRecord = recordAuditEvent({
|
|
2100
2295
|
toolName: input.tool_name || input.toolName || 'unknown',
|
|
@@ -2422,7 +2617,7 @@ function buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy) {
|
|
|
2422
2617
|
const message = `Knowledge conflict warning: retrieved lessons disagree for this action (entropy ${entropy}). Treat the reminders below as cautionary context, but do not stop unrelated work solely because memory is noisy.`;
|
|
2423
2618
|
|
|
2424
2619
|
if (isKnowledgeConflictHardBlockAction(toolName, toolInput)) {
|
|
2425
|
-
recordStat('retrieval_entropy_high', 'block');
|
|
2620
|
+
recordStat('retrieval_entropy_high', 'block', null, { toolName, toolInput });
|
|
2426
2621
|
return {
|
|
2427
2622
|
decision: 'deny',
|
|
2428
2623
|
gate: 'knowledge-conflict-gate',
|
|
@@ -2431,7 +2626,7 @@ function buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy) {
|
|
|
2431
2626
|
};
|
|
2432
2627
|
}
|
|
2433
2628
|
|
|
2434
|
-
recordStat('retrieval_entropy_high', 'warn');
|
|
2629
|
+
recordStat('retrieval_entropy_high', 'warn', null, { toolName, toolInput });
|
|
2435
2630
|
return mergeContextStrings(`[ThumbGate] ${message}`, lessonContext);
|
|
2436
2631
|
}
|
|
2437
2632
|
|
|
@@ -2443,7 +2638,7 @@ function extractActionContext(toolName, toolInput) {
|
|
|
2443
2638
|
if (toolInput.description) parts.push(String(toolInput.description).slice(0, 200));
|
|
2444
2639
|
if (toolInput.prompt) parts.push(String(toolInput.prompt).slice(0, 400));
|
|
2445
2640
|
if (toolInput.pattern) parts.push(String(toolInput.pattern).slice(0, 200));
|
|
2446
|
-
return parts.filter(Boolean).join(' ');
|
|
2641
|
+
return sanitizeFeedbackText(parts.filter(Boolean).join(' ')) || toolName;
|
|
2447
2642
|
}
|
|
2448
2643
|
|
|
2449
2644
|
function extractAvoidanceAdvice(content) {
|
|
@@ -2472,6 +2667,7 @@ async function runAsync(input) {
|
|
|
2472
2667
|
|
|
2473
2668
|
const toolName = input.tool_name || '';
|
|
2474
2669
|
const toolInput = input.tool_input || {};
|
|
2670
|
+
const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
|
|
2475
2671
|
|
|
2476
2672
|
const sequenceGuard = evaluateSequenceState(toolName, toolInput);
|
|
2477
2673
|
if (sequenceGuard && sequenceGuard.decision === 'deny') {
|
|
@@ -2491,8 +2687,8 @@ async function runAsync(input) {
|
|
|
2491
2687
|
}
|
|
2492
2688
|
|
|
2493
2689
|
|
|
2494
|
-
const behavioralContext = buildBehavioralContext();
|
|
2495
|
-
const lessonContext = await buildRelevantLessonContextAsync(toolName, toolInput);
|
|
2690
|
+
const behavioralContext = safeSecretStorageWrite ? null : buildBehavioralContext();
|
|
2691
|
+
const lessonContext = safeSecretStorageWrite ? null : await buildRelevantLessonContextAsync(toolName, toolInput);
|
|
2496
2692
|
|
|
2497
2693
|
if (lessonContext && lessonContext.decision === "deny") {
|
|
2498
2694
|
return formatOutput(lessonContext);
|
|
@@ -2518,6 +2714,7 @@ function run(input) {
|
|
|
2518
2714
|
|
|
2519
2715
|
const toolName = input.tool_name || '';
|
|
2520
2716
|
const toolInput = input.tool_input || {};
|
|
2717
|
+
const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
|
|
2521
2718
|
|
|
2522
2719
|
const sequenceGuard = evaluateSequenceState(toolName, toolInput);
|
|
2523
2720
|
if (sequenceGuard && sequenceGuard.decision === 'deny') {
|
|
@@ -2537,8 +2734,8 @@ function run(input) {
|
|
|
2537
2734
|
}
|
|
2538
2735
|
|
|
2539
2736
|
|
|
2540
|
-
const behavioralContext = buildBehavioralContext();
|
|
2541
|
-
const lessonContext = buildRelevantLessonContext(toolName, toolInput);
|
|
2737
|
+
const behavioralContext = safeSecretStorageWrite ? null : buildBehavioralContext();
|
|
2738
|
+
const lessonContext = safeSecretStorageWrite ? null : buildRelevantLessonContext(toolName, toolInput);
|
|
2542
2739
|
|
|
2543
2740
|
if (lessonContext && lessonContext.decision === "deny") {
|
|
2544
2741
|
return formatOutput(lessonContext);
|