thumbgate 1.27.4 → 1.27.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +2 -1
- package/.well-known/llms.txt +6 -2
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +49 -5
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +16 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bin/cli.js +230 -6
- package/bin/postinstall.js +1 -1
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-templates.json +84 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +20 -0
- package/config/github-about.json +1 -1
- package/config/model-candidates.json +50 -0
- package/config/post-deploy-marketing-pages.json +5 -0
- package/package.json +67 -25
- package/public/agent-manager.html +41 -1
- package/public/agents-cost-savings.html +1 -1
- package/public/ai-malpractice-prevention.html +2 -1
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-mark-inline.svg +15 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +15 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +1 -1
- package/public/brand/thumbgate-mark.svg +15 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/codex-enterprise.html +1 -1
- package/public/codex-plugin.html +1 -1
- package/public/compare.html +23 -3
- package/public/dashboard.html +316 -30
- package/public/federal.html +1 -1
- package/public/guide.html +5 -4
- package/public/index.html +167 -49
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +88 -7
- package/public/lessons.html +2 -1
- package/public/numbers.html +3 -3
- package/public/pricing.html +63 -15
- package/public/pro.html +7 -7
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/auto-promote-gates.js +75 -4
- package/scripts/billing.js +12 -1
- package/scripts/build-metadata.js +24 -3
- package/scripts/cli-schema.js +42 -10
- package/scripts/dashboard-chat.js +53 -7
- package/scripts/dashboard.js +12 -17
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +121 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gates-engine.js +234 -7
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-stop-anti-claim.js +227 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +1 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/operational-integrity.js +39 -5
- package/scripts/plausible-domain-config.js +15 -2
- package/scripts/plausible-server-events.js +4 -4
- package/scripts/rate-limiter.js +12 -6
- package/scripts/secret-redaction.js +166 -0
- package/scripts/security-scanner.js +100 -0
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/seo-gsd.js +635 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +5 -2
- package/scripts/statusline.sh +13 -1
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +9 -0
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/vector-store.js +44 -0
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +862 -146
package/scripts/gates-engine.js
CHANGED
|
@@ -119,8 +119,16 @@ const MAX_COMMAND_SCAN_CHARS = 20000;
|
|
|
119
119
|
const BOOSTED_RISK_BLOCK_SCORE = 0.8;
|
|
120
120
|
const BOOSTED_RISK_MIN_EXAMPLES = 3;
|
|
121
121
|
const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
|
|
122
|
+
const HELPER_BYPASS_ACTION = 'helper_script_modified';
|
|
122
123
|
const KNOWLEDGE_ENTROPY_THRESHOLD = 0.7;
|
|
123
|
-
const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f
|
|
124
|
+
const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f[a-z]*|railway\s+(?:deploy|up)\b|gcloud\s+(?:run\s+deploy|app\s+deploy)\b|firebase\s+deploy\b|vercel\s+--prod\b|kubectl\s+(?:apply|delete)\b|terraform\s+(?:apply|destroy)\b)\b/i;
|
|
125
|
+
const HELPER_SCRIPT_FILE_PATTERN = /(?:^|\/)(?:scripts|bin|tools|tasks|\.githooks|\.github\/workflows)\/|(?:^|\/)(?:package\.json|Makefile|justfile|Taskfile\.ya?ml)$|\.(?:sh|bash|zsh|fish|js|mjs|cjs|ts|tsx|py|rb|pl|ps1|yml|yaml)$/i;
|
|
126
|
+
const PACKAGE_RUN_PATTERN = /\b(?:npm|yarn|pnpm)\s+run\s+([:@./\w-]+)\b/i;
|
|
127
|
+
const HELPER_EXEC_PATTERN = /(?:^|[;&|]\s*|\b(?:bash|sh|zsh|node|python3?|ruby|perl|tsx|ts-node)\s+)(?:(?:\.?\.?\/)?(?:scripts|bin|tools|tasks|tmp|build|dist|\.tmp|\.cache)\/[^\s;&|]+|\.\/[^\s;&|]+\.(?:sh|bash|zsh|js|mjs|cjs|ts|py|rb|pl|ps1))\b/i;
|
|
128
|
+
const HELPER_WRITE_PATTERN = /\b(?:cat|printf|echo|tee|npm\s+pkg\s+set|jq|node\s+-e|python3?\s+-c)\b[\s\S]{0,300}(?:>|--field|scripts\.|package\.json|(?:scripts|bin|tools|tasks|tmp|build|dist|\.tmp|\.cache)\/[^\s;&|]+|\.(?:sh|js|mjs|cjs|ts|py|rb|pl|ps1))\b/i;
|
|
129
|
+
const NETWORK_OR_PROCESS_BOUNDARY_PATTERN = /\b(?:curl|wget|nc|ncat|socat|ssh|scp|rsync|ftp|python3?\s+-m\s+http\.server|node\s+-e|python3?\s+-c|perl\s+-e|ruby\s+-e|bash\s+-c|sh\s+-c|osascript|open)\b/i;
|
|
130
|
+
const DOWNLOAD_EXEC_CHAIN_PATTERN = /\b(?:curl|wget)\b[\s\S]{0,400}(?:\|\s*(?:bash|sh|zsh)|&&[\s\S]{0,200}\bchmod\s+\+x\b[\s\S]{0,200}&&[\s\S]{0,120}(?:\.\/|bash|sh|node|python3?))/i;
|
|
131
|
+
const DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN = /\b(?:rm\s+-rf|chmod\s+(?:\+x|777)|chown\b|sudo\b|dd\s+if=|mkfs|git\s+reset\s+--hard|git\s+clean\s+-f[a-z]*|kubectl\s+(?:apply|delete)|terraform\s+(?:apply|destroy)|railway\s+(?:deploy|up)|gcloud\s+(?:run\s+deploy|app\s+deploy)|vercel\s+--prod|firebase\s+deploy)\b/i;
|
|
124
132
|
|
|
125
133
|
// ---------------------------------------------------------------------------
|
|
126
134
|
// Enforcement posture (CEO decision 2026-06-04): warn-by-default.
|
|
@@ -834,9 +842,26 @@ function toRepoRelativePath(filePath, repoRoot) {
|
|
|
834
842
|
const value = String(filePath || '').trim();
|
|
835
843
|
if (!value) return '';
|
|
836
844
|
if (repoRoot && path.isAbsolute(value)) {
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
845
|
+
const candidates = [[path.resolve(repoRoot), path.resolve(value)]];
|
|
846
|
+
try {
|
|
847
|
+
const rootReal = fs.realpathSync.native(repoRoot);
|
|
848
|
+
let valueReal = null;
|
|
849
|
+
try {
|
|
850
|
+
valueReal = fs.realpathSync.native(value);
|
|
851
|
+
} catch {
|
|
852
|
+
const parentReal = fs.realpathSync.native(path.dirname(value));
|
|
853
|
+
valueReal = path.join(parentReal, path.basename(value));
|
|
854
|
+
}
|
|
855
|
+
candidates.push([rootReal, valueReal]);
|
|
856
|
+
} catch {
|
|
857
|
+
// Fall back to lexical path comparison below.
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
for (const [rootCandidate, valueCandidate] of candidates) {
|
|
861
|
+
const relative = path.relative(rootCandidate, valueCandidate);
|
|
862
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
863
|
+
return normalizePosix(relative);
|
|
864
|
+
}
|
|
840
865
|
}
|
|
841
866
|
}
|
|
842
867
|
return normalizePosix(value);
|
|
@@ -1195,6 +1220,125 @@ function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governa
|
|
|
1195
1220
|
};
|
|
1196
1221
|
}
|
|
1197
1222
|
|
|
1223
|
+
function helperRiskReasons(text) {
|
|
1224
|
+
const value = String(text || '');
|
|
1225
|
+
const reasons = [];
|
|
1226
|
+
if (DOWNLOAD_EXEC_CHAIN_PATTERN.test(value)) reasons.push('download-then-execute chain');
|
|
1227
|
+
if (NETWORK_OR_PROCESS_BOUNDARY_PATTERN.test(value)) reasons.push('network/process boundary');
|
|
1228
|
+
if (DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN.test(value)) reasons.push('destructive/privileged side effect');
|
|
1229
|
+
if (/\b(?:child_process|spawn\(|exec\(|execFile\(|subprocess|ProcessBuilder)\b/i.test(value)) {
|
|
1230
|
+
reasons.push('process spawn');
|
|
1231
|
+
}
|
|
1232
|
+
return [...new Set(reasons)];
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function readPackageScript(repoRoot, scriptName) {
|
|
1236
|
+
if (!repoRoot || !scriptName) return '';
|
|
1237
|
+
try {
|
|
1238
|
+
const packagePath = path.join(repoRoot, 'package.json');
|
|
1239
|
+
if (!fs.existsSync(packagePath)) return '';
|
|
1240
|
+
const { scripts = {} } = JSON.parse(fs.readFileSync(packagePath, 'utf8')) || {};
|
|
1241
|
+
return typeof scripts[scriptName] === 'string' ? scripts[scriptName] : '';
|
|
1242
|
+
} catch {
|
|
1243
|
+
return '';
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function recordHelperScriptWrite(toolName, toolInput = {}) {
|
|
1248
|
+
if (process.env.THUMBGATE_HELPER_BYPASS_GUARD === '0') return null;
|
|
1249
|
+
|
|
1250
|
+
const affected = extractAffectedFiles(toolName, toolInput);
|
|
1251
|
+
const affectedFiles = affected.files || [];
|
|
1252
|
+
const command = String(toolInput.command || '');
|
|
1253
|
+
const actionContext = extractActionContext(toolName, toolInput);
|
|
1254
|
+
const helperFiles = affectedFiles.filter((filePath) => HELPER_SCRIPT_FILE_PATTERN.test(normalizePosix(filePath)));
|
|
1255
|
+
const packageScriptTouched = affectedFiles.some((filePath) => normalizePosix(filePath) === 'package.json') || /\bpackage\.json\b|\bscripts\./i.test(actionContext);
|
|
1256
|
+
const commandWrite = toolName === 'Bash' && HELPER_WRITE_PATTERN.test(command);
|
|
1257
|
+
|
|
1258
|
+
if (helperFiles.length === 0 && !packageScriptTouched && !commandWrite) return null;
|
|
1259
|
+
|
|
1260
|
+
const riskText = [
|
|
1261
|
+
actionContext,
|
|
1262
|
+
command,
|
|
1263
|
+
helperFiles.join(' '),
|
|
1264
|
+
packageScriptTouched ? 'package.json scripts' : '',
|
|
1265
|
+
].filter(Boolean).join('\n');
|
|
1266
|
+
const reasons = helperRiskReasons(riskText);
|
|
1267
|
+
|
|
1268
|
+
const metadata = {
|
|
1269
|
+
repoRoot: affected.repoRoot || resolveRepoRoot(toolInput) || null,
|
|
1270
|
+
helperFiles,
|
|
1271
|
+
packageScriptTouched,
|
|
1272
|
+
reasons,
|
|
1273
|
+
};
|
|
1274
|
+
trackAction(HELPER_BYPASS_ACTION, metadata);
|
|
1275
|
+
return metadata;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function evaluateStatefulHelperBypassGate(toolName, toolInput = {}) {
|
|
1279
|
+
if (process.env.THUMBGATE_HELPER_BYPASS_GUARD === '0') return null;
|
|
1280
|
+
if (toolName !== 'Bash') {
|
|
1281
|
+
recordHelperScriptWrite(toolName, toolInput);
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const command = String(toolInput.command || '').trim();
|
|
1286
|
+
if (!command) return null;
|
|
1287
|
+
|
|
1288
|
+
if (DOWNLOAD_EXEC_CHAIN_PATTERN.test(command)) {
|
|
1289
|
+
return {
|
|
1290
|
+
decision: 'deny',
|
|
1291
|
+
gate: 'stateful-helper-script-bypass',
|
|
1292
|
+
message: 'Download-then-execute chains are blocked before the real action moves below the visible tool call.',
|
|
1293
|
+
severity: 'critical',
|
|
1294
|
+
reasoning: [
|
|
1295
|
+
`Command matched download/execute chain: ${command.slice(0, 180)}`,
|
|
1296
|
+
'PreToolUse must review the whole action chain, not only the first low-risk command',
|
|
1297
|
+
],
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const writeMetadata = recordHelperScriptWrite(toolName, toolInput);
|
|
1302
|
+
const actions = listSessionActions();
|
|
1303
|
+
const recentWrite = actions[HELPER_BYPASS_ACTION];
|
|
1304
|
+
const recentMetadata = recentWrite && recentWrite.metadata && typeof recentWrite.metadata === 'object'
|
|
1305
|
+
? recentWrite.metadata
|
|
1306
|
+
: null;
|
|
1307
|
+
|
|
1308
|
+
const scriptMatch = command.match(PACKAGE_RUN_PATTERN);
|
|
1309
|
+
const scriptName = scriptMatch ? scriptMatch[1] : '';
|
|
1310
|
+
const repoRoot = resolveRepoRoot(toolInput);
|
|
1311
|
+
const packageScript = scriptName ? readPackageScript(repoRoot, scriptName) : '';
|
|
1312
|
+
const commandBoundaryReasons = helperRiskReasons(`${command}\n${packageScript}`);
|
|
1313
|
+
const executesRecentHelper = HELPER_EXEC_PATTERN.test(command);
|
|
1314
|
+
const runsRecentPackageScript = Boolean(scriptName && recentMetadata && recentMetadata.packageScriptTouched);
|
|
1315
|
+
const recentReasons = recentMetadata && Array.isArray(recentMetadata.reasons) ? recentMetadata.reasons : [];
|
|
1316
|
+
const writeRiskReasons = writeMetadata && Array.isArray(writeMetadata.reasons) ? writeMetadata.reasons : [];
|
|
1317
|
+
const correlatedReasons = [...new Set([...recentReasons, ...writeRiskReasons, ...commandBoundaryReasons])];
|
|
1318
|
+
|
|
1319
|
+
if (
|
|
1320
|
+
(executesRecentHelper || runsRecentPackageScript) &&
|
|
1321
|
+
(recentMetadata || writeMetadata) &&
|
|
1322
|
+
correlatedReasons.length > 0
|
|
1323
|
+
) {
|
|
1324
|
+
const target = scriptName ? `package script "${scriptName}"` : 'recent helper script';
|
|
1325
|
+
return {
|
|
1326
|
+
decision: 'deny',
|
|
1327
|
+
gate: 'stateful-helper-script-bypass',
|
|
1328
|
+
message: `A recently modified ${target} now crosses a risky boundary. Review it or constrain cwd, network, process, and writable paths before execution.`,
|
|
1329
|
+
severity: 'critical',
|
|
1330
|
+
reasoning: [
|
|
1331
|
+
`Recent helper/package modification: ${(recentMetadata && recentMetadata.helperFiles || []).join(', ') || (recentMetadata && recentMetadata.packageScriptTouched ? 'package.json scripts' : 'current command write')}`,
|
|
1332
|
+
`Execution command: ${command.slice(0, 180)}`,
|
|
1333
|
+
`Risk reasons: ${correlatedReasons.join(', ')}`,
|
|
1334
|
+
'This blocks the helper-script/package-script bypass class raised in external review',
|
|
1335
|
+
],
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1198
1342
|
function recordStructuralGateBlock(toolName, toolInput, result) {
|
|
1199
1343
|
recordStat(result.gate, 'block', null, { toolName, toolInput });
|
|
1200
1344
|
const auditRecord = recordAuditEvent({
|
|
@@ -1454,8 +1598,19 @@ function checkWhenClause(when, constraints) {
|
|
|
1454
1598
|
if (!when || !when.constraints) return true;
|
|
1455
1599
|
|
|
1456
1600
|
for (const [key, expectedValue] of Object.entries(when.constraints)) {
|
|
1457
|
-
|
|
1458
|
-
if (
|
|
1601
|
+
let value;
|
|
1602
|
+
if (key === 'careful_mode') {
|
|
1603
|
+
const isEnvTrue = process.env.THUMBGATE_CAREFUL_MODE === '1' ||
|
|
1604
|
+
String(process.env.THUMBGATE_CAREFUL_MODE).toLowerCase() === 'true';
|
|
1605
|
+
value = isEnvTrue || (constraints[key] && constraints[key].value === expectedValue);
|
|
1606
|
+
} else if (key === 'freeze_mode') {
|
|
1607
|
+
value = Boolean(process.env.THUMBGATE_FREEZE_PATHS) ||
|
|
1608
|
+
(constraints[key] && constraints[key].value !== false && constraints[key].value !== null && constraints[key].value !== undefined);
|
|
1609
|
+
} else {
|
|
1610
|
+
const constraint = constraints[key];
|
|
1611
|
+
value = constraint && constraint.value === expectedValue;
|
|
1612
|
+
}
|
|
1613
|
+
if (!value) {
|
|
1459
1614
|
return false;
|
|
1460
1615
|
}
|
|
1461
1616
|
}
|
|
@@ -1463,11 +1618,69 @@ function checkWhenClause(when, constraints) {
|
|
|
1463
1618
|
}
|
|
1464
1619
|
|
|
1465
1620
|
function matchGate(gate, toolName, toolInput = {}) {
|
|
1466
|
-
|
|
1621
|
+
let matchText = toolInput.command || toolInput.file_path || toolInput.path || '';
|
|
1622
|
+
|
|
1623
|
+
// Claw/hybrid support: enrich matchText with claw metadata (for EnterpriseClaw/OpenShell/Perplexity hybrid agents)
|
|
1624
|
+
const clawCtx = toolInput.clawContext || toolInput._claw || (toolInput.agentId ? {
|
|
1625
|
+
actionType: toolInput.actionType || 'unknown',
|
|
1626
|
+
agentId: toolInput.agentId || 'unknown',
|
|
1627
|
+
hybridRoute: toolInput.hybridRoute || 'unknown',
|
|
1628
|
+
screenInteraction: !!toolInput.screenInteraction,
|
|
1629
|
+
fileAccess: !!toolInput.fileAccess,
|
|
1630
|
+
} : null);
|
|
1631
|
+
|
|
1632
|
+
if (clawCtx) {
|
|
1633
|
+
const actionType = clawCtx.actionType || clawCtx.claw_action_type || 'unknown';
|
|
1634
|
+
const parts = [
|
|
1635
|
+
matchText,
|
|
1636
|
+
`claw_style: true`,
|
|
1637
|
+
`agent_identity: ${clawCtx.agentId || 'unknown'}`,
|
|
1638
|
+
`claw_action_type: ${actionType}`,
|
|
1639
|
+
`hybrid_route: ${clawCtx.hybridRoute || 'unknown'}`,
|
|
1640
|
+
];
|
|
1641
|
+
|
|
1642
|
+
if (clawCtx.screenInteraction || actionType.includes('screen')) {
|
|
1643
|
+
parts.push('screen_interaction');
|
|
1644
|
+
parts.push('interact screen');
|
|
1645
|
+
}
|
|
1646
|
+
if (clawCtx.fileAccess || actionType.includes('file') || actionType.includes('fs')) {
|
|
1647
|
+
parts.push('file_system_access');
|
|
1648
|
+
parts.push('local device file system access');
|
|
1649
|
+
}
|
|
1650
|
+
if (actionType === 'dynamic-tool-creation' || actionType.includes('create-tool') || actionType.includes('define-tool')) {
|
|
1651
|
+
parts.push('create tool');
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
matchText = parts.filter(Boolean).join(' | ');
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1467
1657
|
const affected = extractAffectedFiles(toolName, toolInput);
|
|
1468
1658
|
const affectedFiles = affected.files;
|
|
1469
1659
|
const repoRoot = affected.repoRoot;
|
|
1470
1660
|
const governanceState = loadGovernanceState();
|
|
1661
|
+
const constraints = loadConstraints();
|
|
1662
|
+
|
|
1663
|
+
if (gate.id === 'on-demand-freeze-mode' || (gate.when && gate.when.constraints && gate.when.constraints.freeze_mode)) {
|
|
1664
|
+
let freezePaths = [];
|
|
1665
|
+
if (process.env.THUMBGATE_FREEZE_PATHS) {
|
|
1666
|
+
freezePaths = process.env.THUMBGATE_FREEZE_PATHS.split(',').map(p => p.trim()).filter(Boolean);
|
|
1667
|
+
} else if (constraints.freeze_mode && typeof constraints.freeze_mode.value === 'string') {
|
|
1668
|
+
freezePaths = constraints.freeze_mode.value.split(',').map(p => p.trim()).filter(Boolean);
|
|
1669
|
+
} else if (constraints.freeze_mode && Array.isArray(constraints.freeze_mode.value)) {
|
|
1670
|
+
freezePaths = constraints.freeze_mode.value;
|
|
1671
|
+
} else if (governanceState.taskScope && Array.isArray(governanceState.taskScope.allowedPaths)) {
|
|
1672
|
+
freezePaths = governanceState.taskScope.allowedPaths;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (freezePaths.length > 0) {
|
|
1676
|
+
const outsideFiles = affectedFiles.filter((filePath) => !matchesAnyGlob(filePath, freezePaths));
|
|
1677
|
+
if (outsideFiles.length > 0) {
|
|
1678
|
+
return { matched: true, matchText, affectedFiles };
|
|
1679
|
+
} else {
|
|
1680
|
+
return { matched: false, matchText, affectedFiles };
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1471
1684
|
|
|
1472
1685
|
if (Array.isArray(gate.toolNames) && gate.toolNames.length > 0 && !gate.toolNames.includes(toolName)) {
|
|
1473
1686
|
return { matched: false, matchText, affectedFiles };
|
|
@@ -1803,6 +2016,10 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1803
2016
|
if (localOnlyRemoteSideEffectGate) {
|
|
1804
2017
|
return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
|
|
1805
2018
|
}
|
|
2019
|
+
const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
|
|
2020
|
+
if (statefulHelperBypassGate) {
|
|
2021
|
+
return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
|
|
2022
|
+
}
|
|
1806
2023
|
if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
|
|
1807
2024
|
recordAuditEvent({
|
|
1808
2025
|
toolName,
|
|
@@ -2030,6 +2247,10 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2030
2247
|
if (localOnlyRemoteSideEffectGate) {
|
|
2031
2248
|
return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
|
|
2032
2249
|
}
|
|
2250
|
+
const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
|
|
2251
|
+
if (statefulHelperBypassGate) {
|
|
2252
|
+
return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
|
|
2253
|
+
}
|
|
2033
2254
|
if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
|
|
2034
2255
|
recordAuditEvent({
|
|
2035
2256
|
toolName,
|
|
@@ -2672,6 +2893,9 @@ function extractActionContext(toolName, toolInput) {
|
|
|
2672
2893
|
const parts = [toolName];
|
|
2673
2894
|
if (toolInput.command) parts.push(String(toolInput.command).slice(0, 400));
|
|
2674
2895
|
if (toolInput.file_path) parts.push(String(toolInput.file_path));
|
|
2896
|
+
if (toolInput.content) parts.push(String(toolInput.content).slice(0, 600));
|
|
2897
|
+
if (toolInput.new_string) parts.push(String(toolInput.new_string).slice(0, 600));
|
|
2898
|
+
if (toolInput.old_string) parts.push(String(toolInput.old_string).slice(0, 240));
|
|
2675
2899
|
if (toolInput.description) parts.push(String(toolInput.description).slice(0, 200));
|
|
2676
2900
|
if (toolInput.prompt) parts.push(String(toolInput.prompt).slice(0, 400));
|
|
2677
2901
|
if (toolInput.pattern) parts.push(String(toolInput.pattern).slice(0, 200));
|
|
@@ -3114,9 +3338,12 @@ module.exports = {
|
|
|
3114
3338
|
getLocalOnlyScopeSources,
|
|
3115
3339
|
isRemoteSideEffectCommand,
|
|
3116
3340
|
evaluateLocalOnlyRemoteSideEffectGate,
|
|
3341
|
+
recordHelperScriptWrite,
|
|
3342
|
+
evaluateStatefulHelperBypassGate,
|
|
3117
3343
|
isAgentHookSettingsFile,
|
|
3118
3344
|
isBreakGlassSettingsRecoveryAction,
|
|
3119
3345
|
PR_THREAD_RESOLUTION_ACTION,
|
|
3346
|
+
HELPER_BYPASS_ACTION,
|
|
3120
3347
|
buildBlockActionProCta,
|
|
3121
3348
|
applyDailyBlockCap,
|
|
3122
3349
|
getTodayBlockCount,
|
|
@@ -122,7 +122,7 @@ function resolveGeminiEmbeddingConfig(env = process.env) {
|
|
|
122
122
|
|
|
123
123
|
return {
|
|
124
124
|
enabled,
|
|
125
|
-
provider: enabled ? 'gemini' : 'local',
|
|
125
|
+
provider: provider === 'coreai' ? 'coreai' : (enabled ? 'gemini' : 'local'),
|
|
126
126
|
model: String(env.THUMBGATE_GEMINI_EMBED_MODEL || GEMINI_EMBEDDING_2_MODEL).trim() || GEMINI_EMBEDDING_2_MODEL,
|
|
127
127
|
apiKey,
|
|
128
128
|
apiBaseUrl: trimTrailingSlashes(env.THUMBGATE_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'),
|
|
@@ -171,6 +171,7 @@ function buildGeminiEmbeddingRolloutPlan(args = {}) {
|
|
|
171
171
|
},
|
|
172
172
|
rolloutSteps: [
|
|
173
173
|
'Keep local embeddings as the default offline path.',
|
|
174
|
+
'For Apple Silicon developers, route local queries through Core AI (AOT compiled models) to bypass CPU overhead.',
|
|
174
175
|
'Enable Gemini Embedding 2 only when a Gemini API key is present.',
|
|
175
176
|
'Use task-specific query/document prefixes at index and retrieval time.',
|
|
176
177
|
'Start at 768 dimensions, then benchmark 1536 only if recall misses show up.',
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stop hook: anti-claim enforcement.
|
|
6
|
+
*
|
|
7
|
+
* Scans the assistant's most recent turn (assistant text + same-turn tool_use
|
|
8
|
+
* blocks) and blocks the "deployed / live / done / fixed / ready" claim
|
|
9
|
+
* unless that same turn included a proof tool call (curl / grep / test).
|
|
10
|
+
*
|
|
11
|
+
* Why: CLAUDE.md anti-lying directive ("Never claim fix done until
|
|
12
|
+
* committed+pushed. Never claim 'ready' without running e2e.") was
|
|
13
|
+
* aspirational, not enforced. Per CEO 2026-05-13 feedback after a session in
|
|
14
|
+
* which 5+ unverified claims slipped through, this is the harness-level
|
|
15
|
+
* enforcement that ends the recurring trust-burn pattern. ThumbGate-on-
|
|
16
|
+
* ThumbGate dogfood — we are the prevention-rule generator and a perfect
|
|
17
|
+
* customer for our own gate.
|
|
18
|
+
*
|
|
19
|
+
* Wires through .claude/settings.json Stop hooks list. Always exits 0
|
|
20
|
+
* (informational): the goal is to surface a system reminder in the next
|
|
21
|
+
* turn so the agent corrects mid-conversation rather than to hard-block
|
|
22
|
+
* the turn that already happened.
|
|
23
|
+
*
|
|
24
|
+
* Stdin: Claude Code passes the hook payload as JSON on stdin. We read
|
|
25
|
+
* `transcript_path` to locate the JSONL session log and scan the last
|
|
26
|
+
* assistant message.
|
|
27
|
+
*
|
|
28
|
+
* Stdout: any text printed is shown to the agent on the next turn.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
|
|
33
|
+
// Lie-phrase patterns. These match common "claim of completion" wording
|
|
34
|
+
// the agent emits without verification. Word-boundary anchored to avoid
|
|
35
|
+
// false positives ("ready-made", "live-streaming", etc).
|
|
36
|
+
const CLAIM_PATTERNS = [
|
|
37
|
+
/\bis\s+live\b/i,
|
|
38
|
+
/\bnow\s+live\b/i,
|
|
39
|
+
/\bgoing\s+live\b/i,
|
|
40
|
+
/\bdeployed\b(?!\s*(yet|to\s+staging|on\s+a\s+branch))/i,
|
|
41
|
+
/\b(?:is|are|it'?s)\s+(?:now\s+)?(?:fully\s+)?(?:fixed|resolved|merged|shipped)\b/i,
|
|
42
|
+
/\bproduction[-\s]ready\b/i,
|
|
43
|
+
/\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
|
|
44
|
+
/\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
|
|
45
|
+
/\b(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b.*\b(?:github|repo|repository)\b/i,
|
|
46
|
+
/\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b.*\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b/i,
|
|
47
|
+
/\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b.*\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b/i,
|
|
48
|
+
// Added 2026-06-11 after a cross-project failure analysis: these completion
|
|
49
|
+
// claims ("all green / stable / verified / race over / tests pass") were
|
|
50
|
+
// asserted without proof and slipped past the original set. The proof-gate
|
|
51
|
+
// below suppresses them whenever the SAME turn ran a verification tool, so a
|
|
52
|
+
// "verified" claim backed by a test/curl/Read stays silent.
|
|
53
|
+
/\ball\s+(?:the\s+)?(?:tests?\s+|checks?\s+)?(?:are\s+)?green\b/i,
|
|
54
|
+
/\b(?:all\s+)?(?:tests?|checks?|ci)\s+(?:are\s+)?(?:now\s+)?passing\b/i,
|
|
55
|
+
/\ball\s+(?:tests?|checks?)\s+pass(?:ed)?\b/i,
|
|
56
|
+
/\bverified\b/i,
|
|
57
|
+
/\bconfirmed\b/i,
|
|
58
|
+
/\b(?:is|are|it'?s|now)\s+stable\b/i,
|
|
59
|
+
/\ball\s+clear\b/i,
|
|
60
|
+
/\bgood\s+to\s+go\b/i,
|
|
61
|
+
/\brace\s+(?:is\s+)?over\b/i,
|
|
62
|
+
/\bno\s+longer\s+racing\b/i,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Proof-of-verification patterns. If the SAME turn included one of these
|
|
66
|
+
// tool calls or shell command tokens, the claim is considered backed and
|
|
67
|
+
// the hook stays silent.
|
|
68
|
+
const PROOF_PATTERNS = [
|
|
69
|
+
/\bcurl\b/,
|
|
70
|
+
/\bgh\s+pr\s+(?:view|checks|status)\b/,
|
|
71
|
+
/\bgh\s+run\s+view\b/,
|
|
72
|
+
/\bgh\s+api\b/,
|
|
73
|
+
/\bnode\s+--test\b/,
|
|
74
|
+
/\bnpm\s+(?:run\s+)?test\b/,
|
|
75
|
+
/\bnpm\s+pack\b/,
|
|
76
|
+
/\bjest\b/,
|
|
77
|
+
/\bmocha\b/,
|
|
78
|
+
/\bpytest\b/,
|
|
79
|
+
/\bplaywright\b/,
|
|
80
|
+
/\bgrep\b/,
|
|
81
|
+
/\bstripe\b/,
|
|
82
|
+
/\bplaid\b/,
|
|
83
|
+
/\bshopify\b/,
|
|
84
|
+
/\bsquare\b/,
|
|
85
|
+
/\bquickbooks\b/,
|
|
86
|
+
/Read\s*\(/, // Claude Code Read tool call
|
|
87
|
+
/Bash\s*\(/, // Claude Code Bash tool call
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
function readLastAssistantTurn(transcriptPath) {
|
|
91
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
|
|
92
|
+
let content;
|
|
93
|
+
try {
|
|
94
|
+
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const lines = content.trim().split('\n');
|
|
99
|
+
// Walk backwards to find the last assistant message
|
|
100
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
101
|
+
const raw = lines[i].trim();
|
|
102
|
+
if (!raw) continue;
|
|
103
|
+
let entry;
|
|
104
|
+
try {
|
|
105
|
+
entry = JSON.parse(raw);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (entry.type === 'assistant' && entry.message) {
|
|
110
|
+
return entry.message;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractText(message) {
|
|
117
|
+
if (!message || !Array.isArray(message.content)) return '';
|
|
118
|
+
return message.content
|
|
119
|
+
.filter((b) => b && typeof b.text === 'string')
|
|
120
|
+
.map((b) => b.text)
|
|
121
|
+
.join('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractToolUseSummary(message) {
|
|
125
|
+
if (!message || !Array.isArray(message.content)) return '';
|
|
126
|
+
return message.content
|
|
127
|
+
.filter((b) => b?.type === 'tool_use')
|
|
128
|
+
.map((b) => {
|
|
129
|
+
const name = b.name || 'tool';
|
|
130
|
+
let summary = '';
|
|
131
|
+
if (b.input && typeof b.input === 'object') {
|
|
132
|
+
if (typeof b.input.command === 'string') summary = b.input.command;
|
|
133
|
+
else if (typeof b.input.file_path === 'string') summary = b.input.file_path;
|
|
134
|
+
else if (typeof b.input.query === 'string') summary = b.input.query;
|
|
135
|
+
else summary = JSON.stringify(b.input).slice(0, 200);
|
|
136
|
+
}
|
|
137
|
+
return `${name}: ${summary}`;
|
|
138
|
+
})
|
|
139
|
+
.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findClaim(text) {
|
|
143
|
+
for (const p of CLAIM_PATTERNS) {
|
|
144
|
+
const m = text.match(p);
|
|
145
|
+
if (m) return m[0];
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasProof(combined) {
|
|
151
|
+
return PROOF_PATTERNS.some((p) => p.test(combined));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readStdinSync() {
|
|
155
|
+
try {
|
|
156
|
+
return fs.readFileSync(0, 'utf8');
|
|
157
|
+
} catch {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function main() {
|
|
163
|
+
const raw = readStdinSync();
|
|
164
|
+
let payload = {};
|
|
165
|
+
try {
|
|
166
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
167
|
+
} catch {
|
|
168
|
+
payload = {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
172
|
+
const message = readLastAssistantTurn(transcriptPath);
|
|
173
|
+
if (!message) return; // no transcript visible; nothing to check
|
|
174
|
+
|
|
175
|
+
const text = extractText(message);
|
|
176
|
+
const toolUseSummary = extractToolUseSummary(message);
|
|
177
|
+
const claim = findClaim(text);
|
|
178
|
+
if (!claim) return; // no completion claim made; silent
|
|
179
|
+
|
|
180
|
+
const proofText = `${text}\n${toolUseSummary}`;
|
|
181
|
+
if (hasProof(proofText)) return; // claim backed by proof in same turn
|
|
182
|
+
|
|
183
|
+
// Strict mode (THUMBGATE_STRICT_ENFORCEMENT=1): hard-block the stop. Emit a
|
|
184
|
+
// Stop-hook block decision so Claude Code does NOT end the turn — the agent
|
|
185
|
+
// must run the verification (or retract) before it can stop. Default mode
|
|
186
|
+
// stays soft (a reminder for the next turn) so we don't break existing wiring.
|
|
187
|
+
if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') {
|
|
188
|
+
const reason = `ThumbGate anti-claim gate (strict): you claimed completion ("${claim}") without a proof tool call in the same message. Run the verification (curl / grep / test / Read) and restate with the proof, or retract — do not end the turn on an unverified claim.`;
|
|
189
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Default (soft): surface a system reminder for the NEXT turn. Do not hard-block.
|
|
194
|
+
const reminder = [
|
|
195
|
+
'⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
|
|
196
|
+
` ("${claim}") without a proof tool call in the same message.`,
|
|
197
|
+
' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed /',
|
|
198
|
+
' verified / all green / stable" or commercial truth (money / tax / inventory /',
|
|
199
|
+
' permissions / customer-facing state) without curl / grep / test / source-of-truth output in the SAME turn.',
|
|
200
|
+
' If the work really is verified, re-state the claim with the proof.',
|
|
201
|
+
' If not, retract and run the verification before re-asserting.',
|
|
202
|
+
].join('\n');
|
|
203
|
+
process.stdout.write(reminder + '\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Path-resolve check instead of `require.main === module`. SonarCloud's
|
|
207
|
+
// strict type inference (rule S3403) flags the === form as always-false
|
|
208
|
+
// in CommonJS, and CLAUDE.md "Hard-Won Lessons" pins the path-based form
|
|
209
|
+
// as the portable fix (incident 2026-04-21 / PR #1115). Resolve BOTH sides
|
|
210
|
+
// so the comparison is between two normalized absolute paths.
|
|
211
|
+
const path = require('node:path');
|
|
212
|
+
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
213
|
+
try {
|
|
214
|
+
main();
|
|
215
|
+
} catch {
|
|
216
|
+
// never crash the agent
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
CLAIM_PATTERNS,
|
|
222
|
+
PROOF_PATTERNS,
|
|
223
|
+
findClaim,
|
|
224
|
+
hasProof,
|
|
225
|
+
extractText,
|
|
226
|
+
extractToolUseSummary,
|
|
227
|
+
};
|
|
@@ -6,11 +6,27 @@
|
|
|
6
6
|
* Also used directly by the CLI to refresh statusline counters after feedback capture.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
11
12
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
13
|
+
const {
|
|
14
|
+
getAggregateStatuslineCachePath,
|
|
15
|
+
shouldAggregateFeedback,
|
|
16
|
+
} = require('./feedback-aggregate');
|
|
12
17
|
|
|
13
18
|
function getCachePath() {
|
|
19
|
+
const explicitFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
|
|
20
|
+
if (shouldAggregateFeedback() && explicitFeedbackDir) {
|
|
21
|
+
try {
|
|
22
|
+
if (path.resolve(explicitFeedbackDir).startsWith(path.resolve(os.tmpdir()) + path.sep)) {
|
|
23
|
+
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (shouldAggregateFeedback()) return getAggregateStatuslineCachePath();
|
|
14
30
|
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
15
31
|
}
|
|
16
32
|
|
|
@@ -730,6 +730,7 @@ function evaluatePretool(toolName, toolInput, opts) {
|
|
|
730
730
|
|
|
731
731
|
// Slow path: build live state (also used when compiled guards are stale)
|
|
732
732
|
const state = buildHybridState({
|
|
733
|
+
feedbackDir: o.feedbackDir,
|
|
733
734
|
feedbackLogPath: o.feedbackLogPath,
|
|
734
735
|
attributedFeedbackPath: o.attributedFeedbackPath,
|
|
735
736
|
});
|
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
20
20
|
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
21
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
21
22
|
const {
|
|
22
23
|
buildStableId,
|
|
23
24
|
extractFilePaths,
|
|
@@ -137,15 +138,19 @@ function createLesson({ feedbackId, signal, inferredLesson, triggerMessage, prio
|
|
|
137
138
|
// Stable link: dashboard deep-link to this lesson
|
|
138
139
|
lesson.link = `${getLessonBaseUrl()}/lessons#${lesson.id}`;
|
|
139
140
|
|
|
141
|
+
// Redact secrets before persisting — lesson text is derived from conversation content that may
|
|
142
|
+
// have captured a pasted credential. See scripts/secret-redaction.js.
|
|
143
|
+
const redactedLesson = redactSecretsDeep(lesson);
|
|
144
|
+
|
|
140
145
|
const lessonsPath = getLessonsPath();
|
|
141
146
|
ensureParentDir(lessonsPath);
|
|
142
|
-
fs.appendFileSync(lessonsPath, JSON.stringify(
|
|
147
|
+
fs.appendFileSync(lessonsPath, JSON.stringify(redactedLesson) + '\n');
|
|
143
148
|
|
|
144
149
|
// Update recent lesson for statusbar
|
|
145
150
|
const recentPath = getRecentLessonPath();
|
|
146
|
-
fs.writeFileSync(recentPath, JSON.stringify(
|
|
151
|
+
fs.writeFileSync(recentPath, JSON.stringify(redactedLesson, null, 2) + '\n');
|
|
147
152
|
|
|
148
|
-
return
|
|
153
|
+
return redactedLesson;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
/**
|