thumbgate 0.9.10 → 0.9.12
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/README.md +2 -2
- package/.claude-plugin/marketplace.json +4 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +115 -312
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +4 -4
- package/adapters/mcp/server-stdio.js +61 -1
- package/adapters/opencode/opencode.json +4 -2
- package/bin/cli.js +156 -8
- package/bin/memory.sh +3 -3
- package/config/e2e-critical-flows.json +4 -0
- package/config/gates/default.json +74 -2
- package/config/github-about.json +1 -1
- package/config/mcp-allowlists.json +27 -0
- package/package.json +22 -5
- package/plugins/amp-skill/INSTALL.md +1 -0
- package/plugins/amp-skill/SKILL.md +1 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +4 -2
- package/plugins/claude-skill/INSTALL.md +1 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +4 -2
- 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/cursor-marketplace/README.md +3 -3
- package/plugins/cursor-marketplace/mcp.json +3 -1
- package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
- package/plugins/gemini-extension/INSTALL.md +3 -3
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/dashboard.html +15 -8
- package/public/index.html +125 -185
- package/public/js/buyer-intent.js +252 -0
- package/public/pro.html +1085 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/adk-consolidator.js +14 -2
- package/scripts/agent-readiness.js +3 -1
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/auto-promote-gates.js +2 -0
- package/scripts/auto-wire-hooks.js +105 -17
- package/scripts/behavioral-extraction.js +2 -6
- package/scripts/billing.js +107 -3
- package/scripts/budget-guard.js +2 -2
- package/scripts/build-metadata.js +14 -0
- package/scripts/context-engine.js +1 -0
- package/scripts/deploy-policy.js +3 -17
- package/scripts/dpo-optimizer.js +3 -6
- package/scripts/ensure-repo-bootstrap.js +129 -0
- package/scripts/export-dpo-pairs.js +2 -3
- package/scripts/export-kto-pairs.js +3 -4
- package/scripts/export-training.js +8 -6
- package/scripts/feedback-attribution.js +23 -11
- package/scripts/feedback-loop.js +40 -2
- package/scripts/feedback-to-rules.js +2 -1
- package/scripts/filesystem-search.js +3 -2
- package/scripts/gates-engine.js +760 -29
- package/scripts/generate-pretool-hook.sh +0 -0
- package/scripts/gtm-revenue-loop.js +20 -1
- package/scripts/hook-auto-capture.sh +8 -3
- package/scripts/hook-runtime.js +81 -0
- package/scripts/hook-stop-self-score.sh +3 -3
- package/scripts/hook-thumbgate-cache-updater.js +99 -38
- package/scripts/hosted-config.js +4 -16
- package/scripts/hybrid-feedback-context.js +54 -14
- package/scripts/install-mcp.js +13 -3
- package/scripts/intent-router.js +2 -2
- package/scripts/license.js +52 -14
- package/scripts/local-model-profile.js +3 -2
- package/scripts/mcp-config.js +62 -7
- package/scripts/meta-policy.js +4 -8
- package/scripts/money-watcher.js +166 -16
- package/scripts/obsidian-export.js +1 -0
- package/scripts/operational-integrity.js +480 -0
- package/scripts/post-everywhere.js +35 -12
- package/scripts/pr-manager.js +14 -11
- package/scripts/profile-router.js +2 -0
- package/scripts/prompt-dlp.js +1 -0
- package/scripts/publish-decision.js +10 -0
- package/scripts/published-cli.js +61 -0
- package/scripts/risk-scorer.js +3 -2
- package/scripts/rlhf_session_start.sh +32 -0
- package/scripts/skill-quality-tracker.js +3 -5
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
- package/scripts/social-analytics/engagement-audit.js +202 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
- package/scripts/social-analytics/install-growth-automation.js +114 -0
- package/scripts/social-analytics/load-env.js +46 -0
- package/scripts/social-analytics/poll-all.js +23 -23
- package/scripts/social-analytics/pollers/plausible.js +2 -4
- package/scripts/social-analytics/pollers/zernio.js +3 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
- package/scripts/social-analytics/publish-thumbgate-launch.js +322 -0
- package/scripts/social-analytics/publishers/reddit.js +7 -12
- package/scripts/social-analytics/publishers/zernio.js +301 -22
- package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
- package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
- package/scripts/social-analytics/sync-launch-assets.js +185 -0
- package/scripts/social-post-hourly.js +185 -0
- package/scripts/social-quality-gate.js +119 -3
- package/scripts/social-reply-monitor.js +184 -37
- package/scripts/statusline-cache-path.js +27 -0
- package/scripts/statusline-local-stats.js +16 -0
- package/scripts/statusline-meta.js +22 -0
- package/scripts/statusline.sh +40 -33
- package/scripts/sync-version.js +24 -3
- package/scripts/test-coverage.js +21 -13
- package/scripts/tool-registry.js +97 -0
- package/scripts/train_from_feedback.py +32 -9
- package/scripts/validate-feedback.js +3 -2
- package/scripts/vector-store.js +2 -3
- package/scripts/verify-obsidian-setup.sh +3 -3
- package/src/api/server.js +281 -33
package/scripts/gates-engine.js
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
-
const { execSync } = require('child_process');
|
|
7
|
+
const { execSync, execFileSync } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const { isProTier, FREE_TIER_MAX_GATES } = require('./rate-limiter');
|
|
10
|
+
const {
|
|
11
|
+
DEFAULT_BASE_BRANCH,
|
|
12
|
+
evaluateOperationalIntegrity,
|
|
13
|
+
} = require('./operational-integrity');
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
|
|
@@ -50,8 +54,25 @@ const CONSTRAINTS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'se
|
|
|
50
54
|
const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
|
|
51
55
|
const SESSION_ACTIONS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-actions.json');
|
|
52
56
|
const CUSTOM_CLAIM_GATES_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'claim-verification.json');
|
|
57
|
+
const GOVERNANCE_STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'governance-state.json');
|
|
53
58
|
const TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
54
59
|
const SESSION_ACTION_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
60
|
+
const PROTECTED_APPROVAL_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
61
|
+
const DEFAULT_PROTECTED_FILE_GLOBS = [
|
|
62
|
+
'AGENTS.md',
|
|
63
|
+
'CLAUDE.md',
|
|
64
|
+
'CLAUDE.local.md',
|
|
65
|
+
'GEMINI.md',
|
|
66
|
+
'README.md',
|
|
67
|
+
'.gitignore',
|
|
68
|
+
'.husky/**',
|
|
69
|
+
'.claude/**',
|
|
70
|
+
'skills/**',
|
|
71
|
+
'SKILL.md',
|
|
72
|
+
'config/gates/**',
|
|
73
|
+
];
|
|
74
|
+
const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
75
|
+
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;
|
|
55
76
|
|
|
56
77
|
// ---------------------------------------------------------------------------
|
|
57
78
|
// Config loading
|
|
@@ -123,6 +144,213 @@ function saveState(state) { saveJSON(module.exports.STATE_PATH, state); }
|
|
|
123
144
|
function loadConstraints() { return loadJSON(module.exports.CONSTRAINTS_PATH); }
|
|
124
145
|
function saveConstraints(constraints) { saveJSON(module.exports.CONSTRAINTS_PATH, constraints); }
|
|
125
146
|
|
|
147
|
+
function normalizePosix(filePath) {
|
|
148
|
+
return String(filePath || '')
|
|
149
|
+
.replace(/\\/g, '/')
|
|
150
|
+
.replace(/^\.\//, '')
|
|
151
|
+
.replace(/^\/+/, '')
|
|
152
|
+
.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeGlob(glob) {
|
|
156
|
+
return normalizePosix(glob).replace(/\/+$/, '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sanitizeGlobList(globs) {
|
|
160
|
+
if (!Array.isArray(globs)) return [];
|
|
161
|
+
return [...new Set(globs.map((glob) => normalizeGlob(glob)).filter(Boolean))];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function globToRegExp(glob) {
|
|
165
|
+
const normalized = normalizeGlob(glob);
|
|
166
|
+
let pattern = '^';
|
|
167
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
168
|
+
const char = normalized[i];
|
|
169
|
+
const next = normalized[i + 1];
|
|
170
|
+
if (char === '*') {
|
|
171
|
+
if (next === '*') {
|
|
172
|
+
pattern += '.*';
|
|
173
|
+
i += 1;
|
|
174
|
+
} else {
|
|
175
|
+
pattern += '[^/]*';
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if ('\\^$+?.()|{}[]'.includes(char)) {
|
|
180
|
+
pattern += `\\${char}`;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
pattern += char;
|
|
184
|
+
}
|
|
185
|
+
pattern += '$';
|
|
186
|
+
return new RegExp(pattern);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesGlob(filePath, glob) {
|
|
190
|
+
if (!glob) return false;
|
|
191
|
+
try {
|
|
192
|
+
return globToRegExp(glob).test(normalizePosix(filePath));
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function matchesAnyGlob(filePath, globs) {
|
|
199
|
+
return sanitizeGlobList(globs).some((glob) => matchesGlob(filePath, glob));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function clampTtlMs(value, fallbackMs) {
|
|
203
|
+
const fallback = Number.isFinite(fallbackMs) ? fallbackMs : PROTECTED_APPROVAL_TTL_MS;
|
|
204
|
+
const numeric = Number(value);
|
|
205
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
|
|
206
|
+
return Math.min(Math.max(numeric, 60 * 1000), 24 * 60 * 60 * 1000);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function loadGovernanceState() {
|
|
210
|
+
const raw = loadJSON(module.exports.GOVERNANCE_STATE_PATH);
|
|
211
|
+
const state = {
|
|
212
|
+
taskScope: raw && raw.taskScope && typeof raw.taskScope === 'object' ? raw.taskScope : null,
|
|
213
|
+
protectedApprovals: Array.isArray(raw && raw.protectedApprovals) ? raw.protectedApprovals : [],
|
|
214
|
+
branchGovernance: raw && raw.branchGovernance && typeof raw.branchGovernance === 'object'
|
|
215
|
+
? raw.branchGovernance
|
|
216
|
+
: null,
|
|
217
|
+
};
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const activeApprovals = state.protectedApprovals.filter((entry) => {
|
|
220
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
221
|
+
if (!entry.timestamp || !entry.expiresAt) return false;
|
|
222
|
+
return now < entry.expiresAt;
|
|
223
|
+
});
|
|
224
|
+
if (activeApprovals.length !== state.protectedApprovals.length) {
|
|
225
|
+
state.protectedApprovals = activeApprovals;
|
|
226
|
+
saveGovernanceState(state);
|
|
227
|
+
}
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function saveGovernanceState(state) {
|
|
232
|
+
const next = {
|
|
233
|
+
taskScope: state && state.taskScope ? state.taskScope : null,
|
|
234
|
+
protectedApprovals: Array.isArray(state && state.protectedApprovals) ? state.protectedApprovals : [],
|
|
235
|
+
branchGovernance: state && state.branchGovernance ? state.branchGovernance : null,
|
|
236
|
+
};
|
|
237
|
+
saveJSON(module.exports.GOVERNANCE_STATE_PATH, next);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function setTaskScope(scopeInput = {}) {
|
|
241
|
+
if (scopeInput && scopeInput.clear === true) {
|
|
242
|
+
const currentState = loadGovernanceState();
|
|
243
|
+
const cleared = {
|
|
244
|
+
taskScope: null,
|
|
245
|
+
protectedApprovals: currentState.protectedApprovals,
|
|
246
|
+
branchGovernance: currentState.branchGovernance,
|
|
247
|
+
};
|
|
248
|
+
saveGovernanceState(cleared);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const allowedPaths = sanitizeGlobList(scopeInput.allowedPaths);
|
|
253
|
+
if (allowedPaths.length === 0) {
|
|
254
|
+
throw new Error('allowedPaths must be a non-empty array');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const protectedPaths = sanitizeGlobList(
|
|
258
|
+
Array.isArray(scopeInput.protectedPaths) && scopeInput.protectedPaths.length > 0
|
|
259
|
+
? scopeInput.protectedPaths
|
|
260
|
+
: DEFAULT_PROTECTED_FILE_GLOBS
|
|
261
|
+
);
|
|
262
|
+
const taskScope = {
|
|
263
|
+
taskId: String(scopeInput.taskId || '').trim() || null,
|
|
264
|
+
summary: String(scopeInput.summary || '').trim() || null,
|
|
265
|
+
allowedPaths,
|
|
266
|
+
protectedPaths,
|
|
267
|
+
localOnly: scopeInput.localOnly === true,
|
|
268
|
+
repoPath: String(scopeInput.repoPath || '').trim() || null,
|
|
269
|
+
createdAt: new Date().toISOString(),
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
};
|
|
272
|
+
const state = loadGovernanceState();
|
|
273
|
+
state.taskScope = taskScope;
|
|
274
|
+
saveGovernanceState(state);
|
|
275
|
+
if (taskScope.localOnly) {
|
|
276
|
+
setConstraint('local_only', true);
|
|
277
|
+
}
|
|
278
|
+
return taskScope;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function approveProtectedAction(input = {}) {
|
|
282
|
+
const pathGlobs = sanitizeGlobList(input.pathGlobs);
|
|
283
|
+
if (pathGlobs.length === 0) {
|
|
284
|
+
throw new Error('pathGlobs must be a non-empty array');
|
|
285
|
+
}
|
|
286
|
+
const reason = String(input.reason || '').trim();
|
|
287
|
+
if (!reason) {
|
|
288
|
+
throw new Error('reason is required');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const ttlMs = clampTtlMs(input.ttlMs, PROTECTED_APPROVAL_TTL_MS);
|
|
292
|
+
const now = Date.now();
|
|
293
|
+
const entry = {
|
|
294
|
+
id: `approval_${now}_${Math.random().toString(36).slice(2, 8)}`,
|
|
295
|
+
pathGlobs,
|
|
296
|
+
reason,
|
|
297
|
+
evidence: String(input.evidence || '').trim() || null,
|
|
298
|
+
taskId: String(input.taskId || '').trim() || null,
|
|
299
|
+
timestamp: now,
|
|
300
|
+
expiresAt: now + ttlMs,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const state = loadGovernanceState();
|
|
304
|
+
state.protectedApprovals.push(entry);
|
|
305
|
+
saveGovernanceState(state);
|
|
306
|
+
return entry;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function setBranchGovernance(input = {}) {
|
|
310
|
+
if (input && input.clear === true) {
|
|
311
|
+
const state = loadGovernanceState();
|
|
312
|
+
state.branchGovernance = null;
|
|
313
|
+
saveGovernanceState(state);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const branchName = String(input.branchName || '').trim() || null;
|
|
318
|
+
const baseBranch = String(input.baseBranch || '').trim() || DEFAULT_BASE_BRANCH;
|
|
319
|
+
const releaseSensitiveGlobs = sanitizeGlobList(
|
|
320
|
+
Array.isArray(input.releaseSensitiveGlobs) ? input.releaseSensitiveGlobs : []
|
|
321
|
+
);
|
|
322
|
+
const governance = {
|
|
323
|
+
branchName,
|
|
324
|
+
baseBranch,
|
|
325
|
+
prRequired: input.prRequired !== false,
|
|
326
|
+
prNumber: String(input.prNumber || '').trim() || null,
|
|
327
|
+
prUrl: String(input.prUrl || '').trim() || null,
|
|
328
|
+
queueRequired: input.queueRequired === true,
|
|
329
|
+
localOnly: input.localOnly === true,
|
|
330
|
+
releaseVersion: String(input.releaseVersion || '').trim() || null,
|
|
331
|
+
releaseEvidence: String(input.releaseEvidence || '').trim() || null,
|
|
332
|
+
releaseSensitiveGlobs,
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
createdAt: new Date().toISOString(),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const state = loadGovernanceState();
|
|
338
|
+
state.branchGovernance = governance;
|
|
339
|
+
saveGovernanceState(state);
|
|
340
|
+
if (governance.localOnly) {
|
|
341
|
+
setConstraint('local_only', true);
|
|
342
|
+
}
|
|
343
|
+
return governance;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getScopeState() {
|
|
347
|
+
return loadGovernanceState();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getBranchGovernanceState() {
|
|
351
|
+
return loadGovernanceState().branchGovernance;
|
|
352
|
+
}
|
|
353
|
+
|
|
126
354
|
function setConstraint(key, value) {
|
|
127
355
|
const constraints = loadConstraints();
|
|
128
356
|
constraints[key] = {
|
|
@@ -198,6 +426,273 @@ function recordStat(gateId, action, gate) {
|
|
|
198
426
|
// Reasoning chain builder
|
|
199
427
|
// ---------------------------------------------------------------------------
|
|
200
428
|
|
|
429
|
+
function getHybridFeedbackModule() {
|
|
430
|
+
try {
|
|
431
|
+
return require('./hybrid-feedback-context');
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function safeExecFileLines(binary, args, cwd) {
|
|
438
|
+
try {
|
|
439
|
+
const output = execFileSync(binary, args, {
|
|
440
|
+
cwd,
|
|
441
|
+
encoding: 'utf8',
|
|
442
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
443
|
+
}).trim();
|
|
444
|
+
if (!output) return [];
|
|
445
|
+
return output.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
446
|
+
} catch {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function resolveRepoRoot(toolInput = {}) {
|
|
452
|
+
const candidates = [
|
|
453
|
+
toolInput.repoPath,
|
|
454
|
+
toolInput.cwd,
|
|
455
|
+
process.cwd(),
|
|
456
|
+
]
|
|
457
|
+
.filter(Boolean)
|
|
458
|
+
.map((value) => path.resolve(String(value)));
|
|
459
|
+
|
|
460
|
+
for (const cwd of candidates) {
|
|
461
|
+
try {
|
|
462
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
463
|
+
cwd,
|
|
464
|
+
encoding: 'utf8',
|
|
465
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
466
|
+
}).trim();
|
|
467
|
+
if (root) return root;
|
|
468
|
+
} catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function toRepoRelativePath(filePath, repoRoot) {
|
|
477
|
+
const value = String(filePath || '').trim();
|
|
478
|
+
if (!value) return '';
|
|
479
|
+
if (repoRoot && path.isAbsolute(value)) {
|
|
480
|
+
const relative = path.relative(repoRoot, value);
|
|
481
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
482
|
+
return normalizePosix(relative);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return normalizePosix(value);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function collectInlineAffectedFiles(toolInput = {}, repoRoot) {
|
|
489
|
+
const collected = [];
|
|
490
|
+
const arrayFields = [
|
|
491
|
+
toolInput.changed_files,
|
|
492
|
+
toolInput.changedFiles,
|
|
493
|
+
toolInput.files,
|
|
494
|
+
toolInput.file_paths,
|
|
495
|
+
toolInput.filePaths,
|
|
496
|
+
toolInput.paths,
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
for (const field of arrayFields) {
|
|
500
|
+
if (!Array.isArray(field)) continue;
|
|
501
|
+
for (const entry of field) {
|
|
502
|
+
const normalized = toRepoRelativePath(entry, repoRoot);
|
|
503
|
+
if (normalized) collected.push(normalized);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const scalarFields = [
|
|
508
|
+
toolInput.file_path,
|
|
509
|
+
toolInput.filePath,
|
|
510
|
+
toolInput.path,
|
|
511
|
+
];
|
|
512
|
+
for (const field of scalarFields) {
|
|
513
|
+
const normalized = toRepoRelativePath(field, repoRoot);
|
|
514
|
+
if (normalized) collected.push(normalized);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return [...new Set(collected)];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function getUpstreamRef(repoRoot) {
|
|
521
|
+
const upstream = safeExecFileLines('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], repoRoot)[0];
|
|
522
|
+
if (upstream) return upstream;
|
|
523
|
+
const remoteHead = safeExecFileLines('git', ['symbolic-ref', 'refs/remotes/origin/HEAD'], repoRoot)[0];
|
|
524
|
+
if (remoteHead) return remoteHead.replace(/^refs\/remotes\//, '');
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function getBranchDiffFiles(repoRoot) {
|
|
529
|
+
const upstream = getUpstreamRef(repoRoot);
|
|
530
|
+
if (upstream) {
|
|
531
|
+
return safeExecFileLines('git', ['diff', '--name-only', `${upstream}...HEAD`], repoRoot);
|
|
532
|
+
}
|
|
533
|
+
const headParent = safeExecFileLines('git', ['rev-parse', '--verify', 'HEAD~1'], repoRoot)[0];
|
|
534
|
+
if (headParent) {
|
|
535
|
+
return safeExecFileLines('git', ['diff', '--name-only', 'HEAD~1..HEAD'], repoRoot);
|
|
536
|
+
}
|
|
537
|
+
return safeExecFileLines('git', ['diff', '--name-only'], repoRoot);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function extractAffectedFiles(toolName, toolInput = {}) {
|
|
541
|
+
const repoRoot = resolveRepoRoot(toolInput);
|
|
542
|
+
const files = new Set(collectInlineAffectedFiles(toolInput, repoRoot));
|
|
543
|
+
const command = String(toolInput.command || '');
|
|
544
|
+
|
|
545
|
+
if (toolName === 'Bash' && repoRoot && command) {
|
|
546
|
+
if (/\bgit\s+commit\b/i.test(command)) {
|
|
547
|
+
for (const filePath of safeExecFileLines('git', ['diff', '--cached', '--name-only'], repoRoot)) {
|
|
548
|
+
files.add(normalizePosix(filePath));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (/\bgit\s+add\b/i.test(command)) {
|
|
553
|
+
for (const filePath of safeExecFileLines('git', ['diff', '--name-only'], repoRoot)) {
|
|
554
|
+
files.add(normalizePosix(filePath));
|
|
555
|
+
}
|
|
556
|
+
for (const filePath of safeExecFileLines('git', ['ls-files', '--others', '--exclude-standard'], repoRoot)) {
|
|
557
|
+
files.add(normalizePosix(filePath));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command)) {
|
|
562
|
+
for (const filePath of getBranchDiffFiles(repoRoot)) {
|
|
563
|
+
files.add(normalizePosix(filePath));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
repoRoot,
|
|
570
|
+
files: [...files].filter(Boolean),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
|
|
575
|
+
if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
|
|
576
|
+
if (toolName !== 'Bash') return false;
|
|
577
|
+
const command = String(toolInput.command || '');
|
|
578
|
+
return HIGH_RISK_BASH_PATTERN.test(command);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function isScopeEnforcedAction(toolName, toolInput = {}, affectedFiles = []) {
|
|
582
|
+
if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
|
|
583
|
+
if (toolName !== 'Bash') return false;
|
|
584
|
+
const command = String(toolInput.command || '');
|
|
585
|
+
if (!HIGH_RISK_BASH_PATTERN.test(command)) return false;
|
|
586
|
+
return affectedFiles.length > 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function shouldEnforceTaskScope(gate, governanceState, toolName, toolInput = {}, affectedFiles = []) {
|
|
590
|
+
if (gate.scopeMode === 'declared-only') {
|
|
591
|
+
return Boolean(governanceState && governanceState.taskScope) &&
|
|
592
|
+
EDIT_LIKE_TOOLS.has(toolName) &&
|
|
593
|
+
affectedFiles.length > 0;
|
|
594
|
+
}
|
|
595
|
+
return isScopeEnforcedAction(toolName, toolInput, affectedFiles);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function formatFileList(files, limit = 5) {
|
|
599
|
+
const items = Array.isArray(files) ? files.filter(Boolean) : [];
|
|
600
|
+
if (items.length === 0) return 'none';
|
|
601
|
+
if (items.length <= limit) return items.join(', ');
|
|
602
|
+
return `${items.slice(0, limit).join(', ')} (+${items.length - limit} more)`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function buildTaskScopeViolation(taskScope, affectedFiles) {
|
|
606
|
+
if (!Array.isArray(affectedFiles) || affectedFiles.length === 0) return null;
|
|
607
|
+
if (!taskScope || !Array.isArray(taskScope.allowedPaths) || taskScope.allowedPaths.length === 0) {
|
|
608
|
+
return {
|
|
609
|
+
reasonCode: 'missing_task_scope',
|
|
610
|
+
outsideFiles: affectedFiles.slice(),
|
|
611
|
+
allowedPaths: [],
|
|
612
|
+
summary: null,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const outsideFiles = affectedFiles.filter((filePath) => !matchesAnyGlob(filePath, taskScope.allowedPaths));
|
|
616
|
+
if (outsideFiles.length === 0) return null;
|
|
617
|
+
return {
|
|
618
|
+
reasonCode: 'outside_declared_scope',
|
|
619
|
+
outsideFiles,
|
|
620
|
+
allowedPaths: taskScope.allowedPaths.slice(),
|
|
621
|
+
summary: taskScope.summary || null,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function buildProtectedApprovalViolation(protectedGlobs, approvals, affectedFiles) {
|
|
626
|
+
const normalizedProtected = sanitizeGlobList(protectedGlobs);
|
|
627
|
+
if (normalizedProtected.length === 0 || !Array.isArray(affectedFiles) || affectedFiles.length === 0) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const protectedFiles = affectedFiles.filter((filePath) => matchesAnyGlob(filePath, normalizedProtected));
|
|
631
|
+
if (protectedFiles.length === 0) return null;
|
|
632
|
+
|
|
633
|
+
const activeApprovals = Array.isArray(approvals) ? approvals : [];
|
|
634
|
+
const missingApprovalFiles = protectedFiles.filter((filePath) => {
|
|
635
|
+
return !activeApprovals.some((entry) => matchesAnyGlob(filePath, entry.pathGlobs || []));
|
|
636
|
+
});
|
|
637
|
+
if (missingApprovalFiles.length === 0) return null;
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
protectedFiles,
|
|
641
|
+
missingApprovalFiles,
|
|
642
|
+
protectedGlobs: normalizedProtected,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function buildBranchGovernanceViolation(governanceState, toolInput = {}, affectedFiles = [], repoRoot = null, requireReleaseReadiness = false) {
|
|
647
|
+
const command = String(toolInput.command || '').trim();
|
|
648
|
+
if (!command) return null;
|
|
649
|
+
|
|
650
|
+
const integrity = evaluateOperationalIntegrity({
|
|
651
|
+
repoPath: repoRoot || (governanceState && governanceState.taskScope && governanceState.taskScope.repoPath) || process.cwd(),
|
|
652
|
+
branchGovernance: governanceState ? governanceState.branchGovernance : null,
|
|
653
|
+
changedFiles: affectedFiles,
|
|
654
|
+
command,
|
|
655
|
+
requireVersionNotBehindBase: requireReleaseReadiness,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
if (!integrity || integrity.blockers.length === 0) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
blockers: integrity.blockers,
|
|
664
|
+
currentBranch: integrity.currentBranch,
|
|
665
|
+
baseBranch: integrity.baseBranch,
|
|
666
|
+
releaseSensitiveFiles: integrity.releaseSensitiveFiles,
|
|
667
|
+
packageVersion: integrity.packageVersion,
|
|
668
|
+
baseVersion: integrity.baseVersion,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function buildGateMessage(gate, matchDetails) {
|
|
673
|
+
if (matchDetails && matchDetails.taskScopeViolation) {
|
|
674
|
+
const violation = matchDetails.taskScopeViolation;
|
|
675
|
+
if (violation.reasonCode === 'missing_task_scope') {
|
|
676
|
+
return `No task scope is declared for this high-risk action. Affected files: ${formatFileList(violation.outsideFiles)}.`;
|
|
677
|
+
}
|
|
678
|
+
return `Action touches files outside the declared task scope: ${formatFileList(violation.outsideFiles)}. Allowed paths: ${formatFileList(violation.allowedPaths)}.`;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (matchDetails && matchDetails.protectedApprovalViolation) {
|
|
682
|
+
const violation = matchDetails.protectedApprovalViolation;
|
|
683
|
+
return `Protected files require explicit approval before editing or publishing. Missing approval for: ${formatFileList(violation.missingApprovalFiles)}.`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (matchDetails && matchDetails.branchGovernanceViolation) {
|
|
687
|
+
const [firstBlocker] = matchDetails.branchGovernanceViolation.blockers || [];
|
|
688
|
+
if (firstBlocker && firstBlocker.message) {
|
|
689
|
+
return firstBlocker.message;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return gate.message;
|
|
694
|
+
}
|
|
695
|
+
|
|
201
696
|
/**
|
|
202
697
|
* Build a human-readable reasoning chain explaining WHY a gate decision was made.
|
|
203
698
|
* Returns an array of evidence steps — each a short sentence a developer can scan.
|
|
@@ -210,10 +705,14 @@ function recordStat(gateId, action, gate) {
|
|
|
210
705
|
*/
|
|
211
706
|
function buildReasoning(gate, toolName, toolInput, extras = {}) {
|
|
212
707
|
const steps = [];
|
|
213
|
-
const text = toolInput.command || toolInput.file_path || toolInput.path || '';
|
|
708
|
+
const text = extras.matchText || toolInput.command || toolInput.file_path || toolInput.path || '';
|
|
214
709
|
|
|
215
710
|
// 1. What matched
|
|
216
|
-
|
|
711
|
+
if (gate.pattern) {
|
|
712
|
+
steps.push(`Pattern /${gate.pattern}/ matched "${text.length > 80 ? text.slice(0, 80) + '…' : text}"`);
|
|
713
|
+
} else {
|
|
714
|
+
steps.push(`Structural gate ${gate.id} matched requested action on "${text.length > 80 ? text.slice(0, 80) + '…' : text}"`);
|
|
715
|
+
}
|
|
217
716
|
|
|
218
717
|
// 2. Gate identity
|
|
219
718
|
steps.push(`Gate ${gate.id} [${gate.action}] — layer: ${gate.layer || 'Execution'}, severity: ${gate.severity || 'medium'}`);
|
|
@@ -232,6 +731,39 @@ function buildReasoning(gate, toolName, toolInput, extras = {}) {
|
|
|
232
731
|
steps.push(`Active because constraint ${keys} is set`);
|
|
233
732
|
}
|
|
234
733
|
|
|
734
|
+
if (extras.affectedFiles && extras.affectedFiles.length > 0) {
|
|
735
|
+
steps.push(`Affected files: ${formatFileList(extras.affectedFiles)}`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (extras.taskScopeViolation) {
|
|
739
|
+
if (extras.taskScopeViolation.reasonCode === 'missing_task_scope') {
|
|
740
|
+
steps.push('No active task scope is declared for this high-risk action');
|
|
741
|
+
} else {
|
|
742
|
+
steps.push(`Outside declared task scope: ${formatFileList(extras.taskScopeViolation.outsideFiles)}`);
|
|
743
|
+
steps.push(`Declared scope: ${formatFileList(extras.taskScopeViolation.allowedPaths)}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (extras.protectedApprovalViolation) {
|
|
748
|
+
steps.push(`Protected files without approval: ${formatFileList(extras.protectedApprovalViolation.missingApprovalFiles)}`);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (extras.branchGovernanceViolation) {
|
|
752
|
+
if (extras.branchGovernanceViolation.currentBranch || extras.branchGovernanceViolation.baseBranch) {
|
|
753
|
+
steps.push(`Branch governance context: ${extras.branchGovernanceViolation.currentBranch || 'unknown'} -> ${extras.branchGovernanceViolation.baseBranch || 'unknown'}`);
|
|
754
|
+
}
|
|
755
|
+
if (extras.branchGovernanceViolation.releaseSensitiveFiles && extras.branchGovernanceViolation.releaseSensitiveFiles.length > 0) {
|
|
756
|
+
steps.push(`Release-sensitive files: ${formatFileList(extras.branchGovernanceViolation.releaseSensitiveFiles)}`);
|
|
757
|
+
}
|
|
758
|
+
for (const blocker of extras.branchGovernanceViolation.blockers || []) {
|
|
759
|
+
steps.push(`Branch governance blocker: ${blocker.code} — ${blocker.message}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (extras.memoryGuard && extras.memoryGuard.reason) {
|
|
764
|
+
steps.push(`Memory guard matched (${extras.memoryGuard.source}): ${extras.memoryGuard.reason}`);
|
|
765
|
+
}
|
|
766
|
+
|
|
235
767
|
// 5. Unless condition status
|
|
236
768
|
if (gate.unless) {
|
|
237
769
|
steps.push(`Bypassable via satisfy_gate("${gate.unless}") — not currently satisfied`);
|
|
@@ -269,26 +801,176 @@ function checkWhenClause(when, constraints) {
|
|
|
269
801
|
return true;
|
|
270
802
|
}
|
|
271
803
|
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
804
|
+
function matchGate(gate, toolName, toolInput = {}) {
|
|
805
|
+
const matchText = toolInput.command || toolInput.file_path || toolInput.path || '';
|
|
806
|
+
const affected = extractAffectedFiles(toolName, toolInput);
|
|
807
|
+
const affectedFiles = affected.files;
|
|
808
|
+
const repoRoot = affected.repoRoot;
|
|
809
|
+
const governanceState = loadGovernanceState();
|
|
810
|
+
|
|
811
|
+
if (Array.isArray(gate.toolNames) && gate.toolNames.length > 0 && !gate.toolNames.includes(toolName)) {
|
|
812
|
+
return { matched: false, matchText, affectedFiles };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (gate.pattern) {
|
|
816
|
+
try {
|
|
817
|
+
const regex = new RegExp(gate.pattern);
|
|
818
|
+
if (!regex.test(matchText)) return { matched: false, matchText, affectedFiles };
|
|
819
|
+
} catch {
|
|
820
|
+
return { matched: false, matchText, affectedFiles };
|
|
821
|
+
}
|
|
282
822
|
}
|
|
283
823
|
|
|
284
|
-
// 2. Check Executable Hash (New: Layer 5 Anti-Bypass)
|
|
285
|
-
// If a hash is specified, we must verify the content of the binary
|
|
286
824
|
if (gate.executable_hash && toolInput.command) {
|
|
287
825
|
const actualHash = computeExecutableHash(toolInput.command);
|
|
288
|
-
if (actualHash !== gate.executable_hash) return false;
|
|
826
|
+
if (actualHash !== gate.executable_hash) return { matched: false, matchText, affectedFiles };
|
|
289
827
|
}
|
|
290
828
|
|
|
291
|
-
|
|
829
|
+
if (Array.isArray(gate.fileGlobs) && gate.fileGlobs.length > 0) {
|
|
830
|
+
const scopedFiles = affectedFiles.filter((filePath) => matchesAnyGlob(filePath, gate.fileGlobs));
|
|
831
|
+
if (scopedFiles.length === 0) return { matched: false, matchText, affectedFiles };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
let taskScopeViolation = null;
|
|
835
|
+
if (gate.requireTaskScope) {
|
|
836
|
+
if (!shouldEnforceTaskScope(gate, governanceState, toolName, toolInput, affectedFiles)) {
|
|
837
|
+
return { matched: false, matchText, affectedFiles };
|
|
838
|
+
}
|
|
839
|
+
taskScopeViolation = buildTaskScopeViolation(governanceState.taskScope, affectedFiles);
|
|
840
|
+
if (!taskScopeViolation) return { matched: false, matchText, affectedFiles };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let protectedApprovalViolation = null;
|
|
844
|
+
if (gate.requireProtectedApproval) {
|
|
845
|
+
const protectedGlobs = sanitizeGlobList(
|
|
846
|
+
Array.isArray(gate.protectedGlobs) && gate.protectedGlobs.length > 0
|
|
847
|
+
? gate.protectedGlobs
|
|
848
|
+
: (governanceState.taskScope && governanceState.taskScope.protectedPaths) || DEFAULT_PROTECTED_FILE_GLOBS
|
|
849
|
+
);
|
|
850
|
+
protectedApprovalViolation = buildProtectedApprovalViolation(
|
|
851
|
+
protectedGlobs,
|
|
852
|
+
governanceState.protectedApprovals,
|
|
853
|
+
affectedFiles,
|
|
854
|
+
);
|
|
855
|
+
if (!protectedApprovalViolation) return { matched: false, matchText, affectedFiles };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
let branchGovernanceViolation = null;
|
|
859
|
+
if (gate.requireBranchGovernance || gate.requireReleaseReadiness) {
|
|
860
|
+
branchGovernanceViolation = buildBranchGovernanceViolation(
|
|
861
|
+
governanceState,
|
|
862
|
+
toolInput,
|
|
863
|
+
affectedFiles,
|
|
864
|
+
repoRoot,
|
|
865
|
+
gate.requireReleaseReadiness === true,
|
|
866
|
+
);
|
|
867
|
+
if (!branchGovernanceViolation) return { matched: false, matchText, affectedFiles };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
matched: true,
|
|
872
|
+
matchText,
|
|
873
|
+
affectedFiles,
|
|
874
|
+
taskScopeViolation,
|
|
875
|
+
protectedApprovalViolation,
|
|
876
|
+
branchGovernanceViolation,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function matchesGate(gate, toolName, toolInput) {
|
|
881
|
+
return matchGate(gate, toolName, toolInput).matched;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
885
|
+
const affected = extractAffectedFiles(toolName, toolInput);
|
|
886
|
+
const affectedFiles = affected.files;
|
|
887
|
+
if (!isHighRiskAction(toolName, toolInput, affectedFiles)) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
const governanceState = loadGovernanceState();
|
|
891
|
+
|
|
892
|
+
if (isScopeEnforcedAction(toolName, toolInput, affectedFiles)) {
|
|
893
|
+
const scopeViolation = buildTaskScopeViolation(governanceState.taskScope, affectedFiles);
|
|
894
|
+
if (!scopeViolation) {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const command = String(toolInput.command || '');
|
|
900
|
+
if (toolName === 'Bash' && /\bgh\s+pr\s+create\b/i.test(command) && isConditionSatisfied('pr_create_allowed')) {
|
|
901
|
+
const branchGovernanceViolation = buildBranchGovernanceViolation(
|
|
902
|
+
governanceState,
|
|
903
|
+
toolInput,
|
|
904
|
+
affectedFiles,
|
|
905
|
+
affected.repoRoot,
|
|
906
|
+
/\b(?:npm|yarn|pnpm)\s+publish\b|\bgh\s+release\s+create\b|\bgit\s+tag\b/i.test(command),
|
|
907
|
+
);
|
|
908
|
+
if (!branchGovernanceViolation) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (toolName === 'Bash' && /\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)) {
|
|
914
|
+
const branchGovernanceViolation = buildBranchGovernanceViolation(
|
|
915
|
+
governanceState,
|
|
916
|
+
toolInput,
|
|
917
|
+
affectedFiles,
|
|
918
|
+
affected.repoRoot,
|
|
919
|
+
/\b(?:npm|yarn|pnpm)\s+publish\b|\bgh\s+release\s+create\b|\bgit\s+tag\b/i.test(command),
|
|
920
|
+
);
|
|
921
|
+
if (!branchGovernanceViolation) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const protectedGlobs = sanitizeGlobList(
|
|
927
|
+
(governanceState.taskScope && governanceState.taskScope.protectedPaths) || DEFAULT_PROTECTED_FILE_GLOBS
|
|
928
|
+
);
|
|
929
|
+
if (affectedFiles.length > 0 && protectedGlobs.length > 0) {
|
|
930
|
+
const protectedApprovalViolation = buildProtectedApprovalViolation(
|
|
931
|
+
protectedGlobs,
|
|
932
|
+
governanceState.protectedApprovals,
|
|
933
|
+
affectedFiles,
|
|
934
|
+
);
|
|
935
|
+
if (!protectedApprovalViolation && affectedFiles.some((filePath) => matchesAnyGlob(filePath, protectedGlobs))) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const hybrid = getHybridFeedbackModule();
|
|
941
|
+
if (!hybrid || typeof hybrid.evaluatePretool !== 'function') {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const serializedInput = JSON.stringify({
|
|
946
|
+
toolName,
|
|
947
|
+
command: toolInput.command || null,
|
|
948
|
+
filePath: toolInput.file_path || toolInput.path || null,
|
|
949
|
+
affectedFiles,
|
|
950
|
+
});
|
|
951
|
+
const guard = hybrid.evaluatePretool(toolName, serializedInput);
|
|
952
|
+
if (!guard || guard.mode === 'allow') {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const message = `Recurring negative memory matched a high-risk action. Denied by default until scope/approval is made explicit. ${guard.reason}`;
|
|
957
|
+
return {
|
|
958
|
+
decision: 'deny',
|
|
959
|
+
gate: 'memory-high-risk-default-deny',
|
|
960
|
+
message,
|
|
961
|
+
severity: 'critical',
|
|
962
|
+
reasoning: buildReasoning({
|
|
963
|
+
id: 'memory-high-risk-default-deny',
|
|
964
|
+
action: 'block',
|
|
965
|
+
layer: 'Memory',
|
|
966
|
+
severity: 'critical',
|
|
967
|
+
message,
|
|
968
|
+
}, toolName, toolInput, {
|
|
969
|
+
matchText: toolInput.command || toolInput.file_path || toolInput.path || '',
|
|
970
|
+
affectedFiles,
|
|
971
|
+
memoryGuard: guard,
|
|
972
|
+
}),
|
|
973
|
+
};
|
|
292
974
|
}
|
|
293
975
|
|
|
294
976
|
async function checkMetricCondition(metricCondition) {
|
|
@@ -320,7 +1002,8 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
320
1002
|
const skipMetrics = METRIC_SKIP_TOOLS.includes(toolName);
|
|
321
1003
|
|
|
322
1004
|
for (const gate of config.gates) {
|
|
323
|
-
|
|
1005
|
+
const matchDetails = matchGate(gate, toolName, toolInput);
|
|
1006
|
+
if (!matchDetails.matched) continue;
|
|
324
1007
|
|
|
325
1008
|
// EvoSkill Hardening: check contextual 'when' clause
|
|
326
1009
|
if (gate.when && !checkWhenClause(gate.when, constraints)) {
|
|
@@ -352,25 +1035,45 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
352
1035
|
continue;
|
|
353
1036
|
}
|
|
354
1037
|
|
|
355
|
-
const
|
|
1038
|
+
const message = buildGateMessage(gate, matchDetails);
|
|
1039
|
+
const reasoning = buildReasoning(gate, toolName, toolInput, {
|
|
1040
|
+
metricFailed,
|
|
1041
|
+
...matchDetails,
|
|
1042
|
+
});
|
|
356
1043
|
|
|
357
1044
|
if (gate.action === 'block') {
|
|
358
1045
|
recordStat(gate.id, 'block', gate);
|
|
359
|
-
const result = { decision: 'deny', gate: gate.id, message
|
|
360
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message
|
|
1046
|
+
const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1047
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
361
1048
|
auditToFeedback(auditRecord);
|
|
362
1049
|
return result;
|
|
363
1050
|
}
|
|
364
1051
|
|
|
365
1052
|
if (gate.action === 'warn') {
|
|
366
1053
|
recordStat(gate.id, 'warn', gate);
|
|
367
|
-
const result = { decision: 'warn', gate: gate.id, message
|
|
368
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message
|
|
1054
|
+
const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1055
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
369
1056
|
auditToFeedback(auditRecord);
|
|
370
1057
|
return result;
|
|
371
1058
|
}
|
|
372
1059
|
}
|
|
373
1060
|
|
|
1061
|
+
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1062
|
+
if (memoryGuard) {
|
|
1063
|
+
recordStat(memoryGuard.gate, 'block');
|
|
1064
|
+
const auditRecord = recordAuditEvent({
|
|
1065
|
+
toolName,
|
|
1066
|
+
toolInput,
|
|
1067
|
+
decision: 'deny',
|
|
1068
|
+
gateId: memoryGuard.gate,
|
|
1069
|
+
message: memoryGuard.message,
|
|
1070
|
+
severity: memoryGuard.severity,
|
|
1071
|
+
source: 'gates-engine',
|
|
1072
|
+
});
|
|
1073
|
+
auditToFeedback(auditRecord);
|
|
1074
|
+
return memoryGuard;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
374
1077
|
// Audit trail: record allow (no gate matched)
|
|
375
1078
|
recordAuditEvent({ toolName, toolInput, decision: 'allow', source: 'gates-engine' });
|
|
376
1079
|
return null;
|
|
@@ -388,7 +1091,8 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
388
1091
|
const constraints = loadConstraints();
|
|
389
1092
|
|
|
390
1093
|
for (const gate of config.gates) {
|
|
391
|
-
|
|
1094
|
+
const matchDetails = matchGate(gate, toolName, toolInput);
|
|
1095
|
+
if (!matchDetails.matched) continue;
|
|
392
1096
|
|
|
393
1097
|
// EvoSkill Hardening: check contextual 'when' clause
|
|
394
1098
|
if (gate.when && !checkWhenClause(gate.when, constraints)) {
|
|
@@ -400,25 +1104,42 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
400
1104
|
continue;
|
|
401
1105
|
}
|
|
402
1106
|
|
|
403
|
-
const
|
|
1107
|
+
const message = buildGateMessage(gate, matchDetails);
|
|
1108
|
+
const reasoning = buildReasoning(gate, toolName, toolInput, matchDetails);
|
|
404
1109
|
|
|
405
1110
|
if (gate.action === 'block') {
|
|
406
1111
|
recordStat(gate.id, 'block', gate);
|
|
407
|
-
const result = { decision: 'deny', gate: gate.id, message
|
|
408
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message
|
|
1112
|
+
const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1113
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
409
1114
|
auditToFeedback(auditRecord);
|
|
410
1115
|
return result;
|
|
411
1116
|
}
|
|
412
1117
|
|
|
413
1118
|
if (gate.action === 'warn') {
|
|
414
1119
|
recordStat(gate.id, 'warn', gate);
|
|
415
|
-
const result = { decision: 'warn', gate: gate.id, message
|
|
416
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message
|
|
1120
|
+
const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1121
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
417
1122
|
auditToFeedback(auditRecord);
|
|
418
1123
|
return result;
|
|
419
1124
|
}
|
|
420
1125
|
}
|
|
421
1126
|
|
|
1127
|
+
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1128
|
+
if (memoryGuard) {
|
|
1129
|
+
recordStat(memoryGuard.gate, 'block');
|
|
1130
|
+
const auditRecord = recordAuditEvent({
|
|
1131
|
+
toolName,
|
|
1132
|
+
toolInput,
|
|
1133
|
+
decision: 'deny',
|
|
1134
|
+
gateId: memoryGuard.gate,
|
|
1135
|
+
message: memoryGuard.message,
|
|
1136
|
+
severity: memoryGuard.severity,
|
|
1137
|
+
source: 'gates-engine',
|
|
1138
|
+
});
|
|
1139
|
+
auditToFeedback(auditRecord);
|
|
1140
|
+
return memoryGuard;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
422
1143
|
// Audit trail: record allow
|
|
423
1144
|
recordAuditEvent({ toolName, toolInput, decision: 'allow', source: 'gates-engine' });
|
|
424
1145
|
return null;
|
|
@@ -760,6 +1481,13 @@ module.exports = {
|
|
|
760
1481
|
loadConstraints,
|
|
761
1482
|
saveConstraints,
|
|
762
1483
|
setConstraint,
|
|
1484
|
+
loadGovernanceState,
|
|
1485
|
+
saveGovernanceState,
|
|
1486
|
+
setTaskScope,
|
|
1487
|
+
setBranchGovernance,
|
|
1488
|
+
approveProtectedAction,
|
|
1489
|
+
getScopeState,
|
|
1490
|
+
getBranchGovernanceState,
|
|
763
1491
|
isConditionSatisfied,
|
|
764
1492
|
satisfyCondition,
|
|
765
1493
|
loadStats,
|
|
@@ -789,8 +1517,11 @@ module.exports = {
|
|
|
789
1517
|
STATS_PATH,
|
|
790
1518
|
SESSION_ACTIONS_PATH,
|
|
791
1519
|
CUSTOM_CLAIM_GATES_PATH,
|
|
1520
|
+
GOVERNANCE_STATE_PATH,
|
|
792
1521
|
TTL_MS,
|
|
793
1522
|
SESSION_ACTION_TTL_MS,
|
|
1523
|
+
PROTECTED_APPROVAL_TTL_MS,
|
|
1524
|
+
DEFAULT_PROTECTED_FILE_GLOBS,
|
|
794
1525
|
};
|
|
795
1526
|
|
|
796
1527
|
// ---------------------------------------------------------------------------
|