thumbgate 1.27.16 → 1.27.18
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 +85 -0
- package/.claude-plugin/plugin.json +3 -4
- package/.well-known/llms.txt +12 -33
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +30 -215
- package/adapters/chatgpt/openapi.yaml +1695 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gemini/function-declarations.json +0 -1
- package/adapters/mcp/server-stdio.js +11 -263
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +122 -1421
- package/bin/postinstall.js +27 -11
- package/config/gate-templates.json +0 -216
- package/config/gates/claim-verification.json +0 -12
- package/config/gates/default.json +2 -31
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +13 -23
- package/config/merge-quality-checks.json +1 -0
- package/config/model-candidates.json +6 -121
- package/openapi/openapi.yaml +0 -12
- package/package.json +98 -228
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +20 -72
- package/public/compare.html +8 -31
- package/public/dashboard.html +166 -930
- package/public/federal.html +2 -2
- package/public/guide.html +9 -33
- package/public/index.html +116 -495
- package/public/learn.html +18 -191
- package/public/lessons.html +10 -168
- package/public/numbers.html +7 -7
- package/public/pro.html +11 -34
- package/scripts/agent-memory-lifecycle.js +0 -211
- package/scripts/agent-readiness.js +3 -167
- package/scripts/agent-reward-model.js +1 -53
- package/scripts/auto-promote-gates.js +10 -82
- package/scripts/auto-wire-hooks.js +0 -14
- package/scripts/billing.js +1 -93
- package/scripts/build-metadata.js +10 -50
- package/scripts/cli-feedback.js +6 -18
- package/scripts/cli-schema.js +0 -90
- package/scripts/cli-telemetry.js +1 -6
- package/scripts/commercial-offer.js +2 -82
- package/scripts/context-manager.js +6 -74
- package/scripts/dashboard.js +2 -68
- package/scripts/durability/step.js +3 -3
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-dpo-pairs.js +2 -7
- package/scripts/feedback-loop.js +1 -123
- package/scripts/feedback-quality.js +0 -87
- package/scripts/filesystem-search.js +10 -35
- package/scripts/gate-stats.js +0 -89
- package/scripts/gates-engine.js +85 -1105
- package/scripts/gemini-embedding-policy.js +1 -2
- package/scripts/hook-runtime.js +14 -20
- package/scripts/hook-thumbgate-cache-updater.js +2 -18
- package/scripts/hybrid-feedback-context.js +7 -142
- package/scripts/lesson-inference.js +3 -8
- package/scripts/lesson-search.js +1 -17
- package/scripts/license.js +1 -0
- package/scripts/llm-client.js +9 -390
- package/scripts/local-model-profile.js +8 -15
- package/scripts/memory-scope-readiness.js +0 -159
- package/scripts/meta-agent-loop.js +0 -36
- package/scripts/operational-integrity.js +5 -39
- package/scripts/oss-pr-opportunity-scout.js +5 -35
- package/scripts/plausible-server-events.js +6 -9
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +1 -42
- package/scripts/rate-limiter.js +13 -64
- package/scripts/secret-scanner.js +5 -44
- package/scripts/security-scanner.js +10 -260
- package/scripts/self-distill-agent.js +1 -3
- package/scripts/seo-gsd.js +2 -916
- package/scripts/statusline-cache-path.js +2 -17
- package/scripts/statusline-local-stats.js +1 -9
- package/scripts/statusline-meta.js +2 -28
- package/scripts/statusline.sh +4 -20
- package/scripts/telemetry-analytics.js +0 -357
- package/scripts/thompson-sampling.js +10 -31
- package/scripts/thumbgate-bench.js +1 -16
- package/scripts/thumbgate-search.js +19 -85
- package/scripts/tool-registry.js +1 -169
- package/scripts/vector-store.js +0 -45
- package/scripts/workflow-sentinel.js +53 -286
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +290 -2578
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/.well-known/agentic-verify.txt +0 -1
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/config/gate-classifier-routing.json +0 -98
- package/config/post-deploy-marketing-pages.json +0 -61
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- 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-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
package/scripts/gates-engine.js
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
6
|
const crypto = require('crypto');
|
|
8
|
-
const { execFileSync } = require('child_process');
|
|
7
|
+
const { execSync, execFileSync } = require('child_process');
|
|
9
8
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
10
9
|
|
|
11
|
-
const { isProTier,
|
|
10
|
+
const { isProTier, FREE_TIER_MAX_GATES } = require('./rate-limiter');
|
|
12
11
|
const {
|
|
13
12
|
DEFAULT_BASE_BRANCH,
|
|
14
13
|
evaluateOperationalIntegrity,
|
|
@@ -20,10 +19,6 @@ const {
|
|
|
20
19
|
recordDecisionEvaluation,
|
|
21
20
|
recordDecisionOutcome,
|
|
22
21
|
} = require('./decision-journal');
|
|
23
|
-
const {
|
|
24
|
-
actionFingerprint,
|
|
25
|
-
sanitizeFeedbackText,
|
|
26
|
-
} = require('./feedback-sanitizer');
|
|
27
22
|
|
|
28
23
|
/**
|
|
29
24
|
* Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
|
|
@@ -35,12 +30,10 @@ function computeExecutableHash(command) {
|
|
|
35
30
|
const firstWord = command.trim().split(/\s+/)[0];
|
|
36
31
|
if (!firstWord) return null;
|
|
37
32
|
|
|
38
|
-
// Resolve absolute path using 'which'
|
|
39
|
-
// firstWord as an argv element, never interpolated into a command string, so
|
|
40
|
-
// a hostile `command` value cannot inject shell metacharacters here.
|
|
33
|
+
// Resolve absolute path using 'which'
|
|
41
34
|
let fullPath;
|
|
42
35
|
try {
|
|
43
|
-
fullPath =
|
|
36
|
+
fullPath = execSync(`which ${firstWord}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
44
37
|
} catch (e) {
|
|
45
38
|
// If 'which' fails, it might be an absolute path or a non-existent command
|
|
46
39
|
fullPath = path.isAbsolute(firstWord) ? firstWord : null;
|
|
@@ -58,44 +51,21 @@ const {
|
|
|
58
51
|
scanHookInput,
|
|
59
52
|
buildSafeSummary,
|
|
60
53
|
redactText,
|
|
61
|
-
isSafeSecretStorageWrite,
|
|
62
|
-
SAFE_SECRET_STORAGE_DIRS,
|
|
63
54
|
} = require('./secret-scanner');
|
|
64
55
|
const {
|
|
65
56
|
evaluateSecurityScan,
|
|
66
57
|
} = require('./security-scanner');
|
|
67
|
-
const { evaluatePlanGate } = require('./plan-gate');
|
|
68
|
-
const { getTrajectoryScore } = require('./trajectory-scorer');
|
|
69
|
-
const { evaluateSequenceState } = loadOptionalModule('./sequence-guard', () => ({
|
|
70
|
-
evaluateSequenceState: () => null,
|
|
71
|
-
}));
|
|
72
58
|
const { getAutoGatesPath } = require('./auto-promote-gates');
|
|
73
59
|
const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
|
|
74
60
|
|
|
75
61
|
const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', 'config', 'gates', 'default.json');
|
|
76
62
|
const DEFAULT_CLAIM_GATES_PATH = path.join(__dirname, '..', 'config', 'gates', 'claim-verification.json');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (process.env.CODEX_SANDBOX) {
|
|
86
|
-
return path.join(os.tmpdir(), 'thumbgate');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return path.join(process.env.HOME || os.tmpdir(), '.thumbgate');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const STATE_DIR = resolveThumbgateStateDir();
|
|
93
|
-
const STATE_PATH = path.join(STATE_DIR, 'gate-state.json');
|
|
94
|
-
const CONSTRAINTS_PATH = path.join(STATE_DIR, 'session-constraints.json');
|
|
95
|
-
const STATS_PATH = path.join(STATE_DIR, 'gate-stats.json');
|
|
96
|
-
const SESSION_ACTIONS_PATH = path.join(STATE_DIR, 'session-actions.json');
|
|
97
|
-
const CUSTOM_CLAIM_GATES_PATH = path.join(STATE_DIR, 'claim-verification.json');
|
|
98
|
-
const GOVERNANCE_STATE_PATH = path.join(STATE_DIR, 'governance-state.json');
|
|
63
|
+
const STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-state.json');
|
|
64
|
+
const CONSTRAINTS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-constraints.json');
|
|
65
|
+
const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
|
|
66
|
+
const SESSION_ACTIONS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-actions.json');
|
|
67
|
+
const CUSTOM_CLAIM_GATES_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'claim-verification.json');
|
|
68
|
+
const GOVERNANCE_STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'governance-state.json');
|
|
99
69
|
const TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
100
70
|
const SESSION_ACTION_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
101
71
|
const PROTECTED_APPROVAL_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
@@ -115,127 +85,12 @@ const DEFAULT_PROTECTED_FILE_GLOBS = [
|
|
|
115
85
|
const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
116
86
|
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;
|
|
117
87
|
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;
|
|
118
|
-
const MAX_COMMAND_SCAN_CHARS = 20000;
|
|
119
88
|
const BOOSTED_RISK_BLOCK_SCORE = 0.8;
|
|
120
89
|
const BOOSTED_RISK_MIN_EXAMPLES = 3;
|
|
121
90
|
const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
|
|
122
|
-
const HELPER_BYPASS_ACTION = 'helper_script_modified';
|
|
123
|
-
const KNOWLEDGE_ENTROPY_THRESHOLD = 0.7;
|
|
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;
|
|
132
|
-
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
// Enforcement posture (CEO decision 2026-06-04): warn-by-default.
|
|
135
|
-
// The firewall ALWAYS fires and logs every decision, but most gates WARN rather
|
|
136
|
-
// than hard-block — only TRULY CATASTROPHIC, irreversible actions hard-block:
|
|
137
|
-
// - secret exfiltration (handled on its own deny path; never downgraded)
|
|
138
|
-
// - security-vulnerability / supply-chain denies (own deny path; not downgraded)
|
|
139
|
-
// - irreversibly destructive filesystem commands (rm -rf class, mkfs, dd to disk,
|
|
140
|
-
// fork bomb) — kept as hard deny via DESTRUCTIVE_FS_PATTERN below.
|
|
141
|
-
// Everything else (memory-high-risk, workflow-sequence, off-scope, git push, deploy,
|
|
142
|
-
// approval gates) downgrades deny/approve -> warn so legitimate work is never blocked.
|
|
143
|
-
// Opt back into full hard enforcement with THUMBGATE_STRICT_ENFORCEMENT=1.
|
|
144
|
-
// Enforcement posture (CEO decision 2026-06-04): WARN + AUDIT by default.
|
|
145
|
-
// The firewall fires and LOGS every decision, but downgrades deny/approve -> warn so
|
|
146
|
-
// legitimate work is never hard-blocked. We deliberately do NOT try to hard-block
|
|
147
|
-
// arbitrary destructive commands here: a regex "catastrophic floor" is unwinnable
|
|
148
|
-
// (sudo / bash -c / find -exec / eval / base64|sh all evade it) and gives false confidence.
|
|
149
|
-
// HARD enforcement is an explicit opt-in via THUMBGATE_STRICT_ENFORCEMENT=1, which keeps
|
|
150
|
-
// the engine's FULL gate set (its high-risk-command gates catch prefixed/obfuscated forms
|
|
151
|
-
// far better than any single regex). Secret exfiltration and the security-vulnerability
|
|
152
|
-
// scan hard-deny on their OWN paths before this runs, so irreversible data-leak / supply
|
|
153
|
-
// chain risks stay blocked regardless of posture.
|
|
154
|
-
function applyEnforcementPosture(result) {
|
|
155
|
-
if (!result || (result.decision !== 'deny' && result.decision !== 'approve')) return result;
|
|
156
|
-
// Full hard enforcement opt-in: keep every deny.
|
|
157
|
-
if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') return result;
|
|
158
|
-
// Honor the explicit strict-knowledge-conflict opt-in for that gate.
|
|
159
|
-
if (process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1' && result.gate === 'knowledge-conflict-gate') return result;
|
|
160
|
-
// Warn-by-default: the gate still fired and is recorded; the action is allowed through
|
|
161
|
-
// with the warning surfaced instead of hard-blocked, so legitimate work is never blocked.
|
|
162
|
-
return {
|
|
163
|
-
...result,
|
|
164
|
-
decision: 'warn',
|
|
165
|
-
warnByDefault: true,
|
|
166
|
-
message: `${result.message}\n\n⚠️ ThumbGate is in warn-by-default mode — this was flagged and logged, not blocked. Set THUMBGATE_STRICT_ENFORCEMENT=1 to hard-block, or THUMBGATE_HOTFIX_BYPASS=1 to disable checks entirely.`,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
const BREAK_GLASS_CONDITION = 'thumbgate_break_glass';
|
|
170
|
-
const BREAK_GLASS_SETTINGS_GLOBS = [
|
|
171
|
-
'.claude/settings.local.json',
|
|
172
|
-
'.claude/settings.json',
|
|
173
|
-
'**/.claude/settings.local.json',
|
|
174
|
-
'**/.claude/settings.json',
|
|
175
|
-
'.codex/config.toml',
|
|
176
|
-
'**/.codex/config.toml',
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
function isRuntimePlanGateEnabled() {
|
|
180
|
-
return process.env.THUMBGATE_PLAN_GATE === '1' || process.env.THUMBGATE_PLAN_GATE === 'true';
|
|
181
|
-
}
|
|
182
91
|
const PR_THREAD_RESOLUTION_CLAIM_PATTERN = '(?:thread|review|comment).*?(?:resolved|verified|checked|addressed|fixed)|(?:resolved|verified|checked|addressed|fixed).*?(?:thread|review|comment)';
|
|
183
92
|
const PR_THREAD_RESOLUTION_REQUIRED_ACTIONS = ['pr_threads_checked', 'thread_resolution_verified'];
|
|
184
93
|
|
|
185
|
-
function commandScanText(command) {
|
|
186
|
-
return String(command || '').slice(0, MAX_COMMAND_SCAN_CHARS);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function commandWords(command) {
|
|
190
|
-
return commandScanText(command).toLowerCase().split(/\s+/).filter(Boolean);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function commandContainsSequence(words, sequence) {
|
|
194
|
-
if (!Array.isArray(words) || !Array.isArray(sequence) || sequence.length === 0) return false;
|
|
195
|
-
for (let i = 0; i <= words.length - sequence.length; i += 1) {
|
|
196
|
-
let matched = true;
|
|
197
|
-
for (let j = 0; j < sequence.length; j += 1) {
|
|
198
|
-
if (words[i + j] !== sequence[j]) {
|
|
199
|
-
matched = false;
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (matched) return true;
|
|
204
|
-
}
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function commandHasPostMethod(words) {
|
|
209
|
-
for (let i = 0; i < words.length; i += 1) {
|
|
210
|
-
const word = words[i];
|
|
211
|
-
if ((word === '-x' || word === '--method') && words[i + 1] === 'post') return true;
|
|
212
|
-
if (word === '--method=post' || word === '-xpost') return true;
|
|
213
|
-
}
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function isGhApiPrCreateCommand(command) {
|
|
218
|
-
const words = commandWords(command);
|
|
219
|
-
if (!commandContainsSequence(words, ['gh', 'api'])) return false;
|
|
220
|
-
const hasPullsEndpoint = words.some((word) => word === '/pulls' || word.endsWith('/pulls'));
|
|
221
|
-
if (!hasPullsEndpoint) return false;
|
|
222
|
-
const fieldFlags = new Set(['-f', '--field', '--raw-field']);
|
|
223
|
-
const hasFieldWrite = words.some((word) => (
|
|
224
|
-
fieldFlags.has(word) ||
|
|
225
|
-
word.startsWith('-f=') ||
|
|
226
|
-
word.startsWith('--field=') ||
|
|
227
|
-
word.startsWith('--raw-field=')
|
|
228
|
-
));
|
|
229
|
-
return hasFieldWrite || commandHasPostMethod(words);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function isRecursiveChmodCommand(command) {
|
|
233
|
-
const words = commandWords(command);
|
|
234
|
-
const chmodIndex = words.indexOf('chmod');
|
|
235
|
-
if (chmodIndex === -1) return false;
|
|
236
|
-
return words.slice(chmodIndex + 1).includes('-r') || words.slice(chmodIndex + 1).some((word) => word.includes('r') && word.startsWith('-'));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
94
|
// ---------------------------------------------------------------------------
|
|
240
95
|
// Config loading
|
|
241
96
|
// ---------------------------------------------------------------------------
|
|
@@ -332,27 +187,6 @@ function sanitizeGlobList(globs) {
|
|
|
332
187
|
return [...new Set(globs.map((glob) => normalizeGlob(glob)).filter(Boolean))];
|
|
333
188
|
}
|
|
334
189
|
|
|
335
|
-
// Affected files are compared as repo-relative paths (git --name-only output and
|
|
336
|
-
// inline paths are relative to the repo root). A caller who passes an ABSOLUTE
|
|
337
|
-
// allowedPath (e.g. "/Users/me/proj/src/**") therefore declares a glob that can never
|
|
338
|
-
// match — a silent no-op scope. When repoPath is known, rebase absolute globs that
|
|
339
|
-
// live under it to the repo-relative form so the scope actually applies. Globs already
|
|
340
|
-
// relative, or absolute but outside repoPath, are returned unchanged.
|
|
341
|
-
function rebaseGlobsToRepoRoot(globs, repoPath) {
|
|
342
|
-
// normalizeGlob strips both leading AND trailing slashes, so a repoPath with a
|
|
343
|
-
// trailing slash ("/Users/me/proj/") still matches the repo-relative globs.
|
|
344
|
-
const repoRel = normalizeGlob(repoPath);
|
|
345
|
-
if (!repoRel) return globs;
|
|
346
|
-
return globs.map((glob) => {
|
|
347
|
-
if (glob === repoRel) return '**';
|
|
348
|
-
if (glob.startsWith(`${repoRel}/`)) {
|
|
349
|
-
const rebased = glob.slice(repoRel.length + 1);
|
|
350
|
-
return rebased || '**';
|
|
351
|
-
}
|
|
352
|
-
return glob;
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
190
|
function globToRegExp(glob) {
|
|
357
191
|
const normalized = normalizeGlob(glob);
|
|
358
192
|
let pattern = '^';
|
|
@@ -406,9 +240,6 @@ function loadGovernanceState() {
|
|
|
406
240
|
branchGovernance: raw && raw.branchGovernance && typeof raw.branchGovernance === 'object'
|
|
407
241
|
? raw.branchGovernance
|
|
408
242
|
: null,
|
|
409
|
-
workflowContract: raw && raw.workflowContract && typeof raw.workflowContract === 'object'
|
|
410
|
-
? raw.workflowContract
|
|
411
|
-
: null,
|
|
412
243
|
};
|
|
413
244
|
const now = Date.now();
|
|
414
245
|
const activeApprovals = state.protectedApprovals.filter((entry) => {
|
|
@@ -428,7 +259,6 @@ function saveGovernanceState(state) {
|
|
|
428
259
|
taskScope: state && state.taskScope ? state.taskScope : null,
|
|
429
260
|
protectedApprovals: Array.isArray(state && state.protectedApprovals) ? state.protectedApprovals : [],
|
|
430
261
|
branchGovernance: state && state.branchGovernance ? state.branchGovernance : null,
|
|
431
|
-
workflowContract: state && state.workflowContract ? state.workflowContract : null,
|
|
432
262
|
};
|
|
433
263
|
saveJSON(module.exports.GOVERNANCE_STATE_PATH, next);
|
|
434
264
|
}
|
|
@@ -440,39 +270,33 @@ function setTaskScope(scopeInput = {}) {
|
|
|
440
270
|
taskScope: null,
|
|
441
271
|
protectedApprovals: currentState.protectedApprovals,
|
|
442
272
|
branchGovernance: currentState.branchGovernance,
|
|
443
|
-
workflowContract: null,
|
|
444
273
|
};
|
|
445
274
|
saveGovernanceState(cleared);
|
|
446
|
-
refreshLocalOnlyConstraint(cleared);
|
|
447
275
|
return null;
|
|
448
276
|
}
|
|
449
277
|
|
|
450
|
-
const
|
|
451
|
-
const allowedPaths = rebaseGlobsToRepoRoot(sanitizeGlobList(scopeInput.allowedPaths), repoPath);
|
|
278
|
+
const allowedPaths = sanitizeGlobList(scopeInput.allowedPaths);
|
|
452
279
|
if (allowedPaths.length === 0) {
|
|
453
280
|
throw new Error('allowedPaths must be a non-empty array');
|
|
454
281
|
}
|
|
455
282
|
|
|
456
|
-
const protectedPaths =
|
|
283
|
+
const protectedPaths = sanitizeGlobList(
|
|
457
284
|
Array.isArray(scopeInput.protectedPaths) && scopeInput.protectedPaths.length > 0
|
|
458
285
|
? scopeInput.protectedPaths
|
|
459
286
|
: DEFAULT_PROTECTED_FILE_GLOBS
|
|
460
|
-
)
|
|
287
|
+
);
|
|
461
288
|
const taskScope = {
|
|
462
289
|
taskId: String(scopeInput.taskId || '').trim() || null,
|
|
463
290
|
summary: String(scopeInput.summary || '').trim() || null,
|
|
464
291
|
allowedPaths,
|
|
465
292
|
protectedPaths,
|
|
466
293
|
localOnly: scopeInput.localOnly === true,
|
|
467
|
-
repoPath,
|
|
294
|
+
repoPath: String(scopeInput.repoPath || '').trim() || null,
|
|
468
295
|
createdAt: new Date().toISOString(),
|
|
469
296
|
timestamp: Date.now(),
|
|
470
297
|
};
|
|
471
298
|
const state = loadGovernanceState();
|
|
472
299
|
state.taskScope = taskScope;
|
|
473
|
-
state.workflowContract = scopeInput.workflowContract && typeof scopeInput.workflowContract === 'object'
|
|
474
|
-
? scopeInput.workflowContract
|
|
475
|
-
: null;
|
|
476
300
|
saveGovernanceState(state);
|
|
477
301
|
if (taskScope.localOnly) {
|
|
478
302
|
setConstraint('local_only', true);
|
|
@@ -508,44 +332,11 @@ function approveProtectedAction(input = {}) {
|
|
|
508
332
|
return entry;
|
|
509
333
|
}
|
|
510
334
|
|
|
511
|
-
function breakGlassEmergency(input = {}) {
|
|
512
|
-
const reason = String(input.reason || '').trim();
|
|
513
|
-
if (!reason) {
|
|
514
|
-
throw new Error('reason is required');
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const ttlMs = Math.min(clampTtlMs(input.ttlMs, TTL_MS), TTL_MS);
|
|
518
|
-
const evidence = `BREAK GLASS: ${reason}`;
|
|
519
|
-
const gates = ['pr_create_allowed', 'pr_threads_checked', BREAK_GLASS_CONDITION];
|
|
520
|
-
const satisfied = {};
|
|
521
|
-
for (const gateId of gates) {
|
|
522
|
-
satisfied[gateId] = satisfyCondition(gateId, evidence);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const approval = approveProtectedAction({
|
|
526
|
-
pathGlobs: BREAK_GLASS_SETTINGS_GLOBS,
|
|
527
|
-
reason: evidence,
|
|
528
|
-
evidence,
|
|
529
|
-
ttlMs,
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
return {
|
|
533
|
-
ok: true,
|
|
534
|
-
reason,
|
|
535
|
-
ttlMs,
|
|
536
|
-
expiresAt: new Date(Date.now() + ttlMs).toISOString(),
|
|
537
|
-
satisfied,
|
|
538
|
-
approval,
|
|
539
|
-
settingsGlobs: BREAK_GLASS_SETTINGS_GLOBS.slice(),
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
335
|
function setBranchGovernance(input = {}) {
|
|
544
336
|
if (input && input.clear === true) {
|
|
545
337
|
const state = loadGovernanceState();
|
|
546
338
|
state.branchGovernance = null;
|
|
547
339
|
saveGovernanceState(state);
|
|
548
|
-
refreshLocalOnlyConstraint(state);
|
|
549
340
|
return null;
|
|
550
341
|
}
|
|
551
342
|
|
|
@@ -596,24 +387,6 @@ function setConstraint(key, value) {
|
|
|
596
387
|
return constraints[key];
|
|
597
388
|
}
|
|
598
389
|
|
|
599
|
-
function clearConstraint(key) {
|
|
600
|
-
const constraints = loadConstraints();
|
|
601
|
-
delete constraints[key];
|
|
602
|
-
saveConstraints(constraints);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function refreshLocalOnlyConstraint(governanceState = loadGovernanceState()) {
|
|
606
|
-
const localOnlyActive = Boolean(
|
|
607
|
-
(governanceState.taskScope && governanceState.taskScope.localOnly) ||
|
|
608
|
-
(governanceState.branchGovernance && governanceState.branchGovernance.localOnly)
|
|
609
|
-
);
|
|
610
|
-
if (localOnlyActive) {
|
|
611
|
-
setConstraint('local_only', true);
|
|
612
|
-
} else {
|
|
613
|
-
clearConstraint('local_only');
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
390
|
function isConditionSatisfied(conditionId) {
|
|
618
391
|
const state = loadState();
|
|
619
392
|
const entry = state[conditionId];
|
|
@@ -653,29 +426,7 @@ function loadStats() {
|
|
|
653
426
|
|
|
654
427
|
function saveStats(stats) { saveJSON(module.exports.STATS_PATH, stats); }
|
|
655
428
|
|
|
656
|
-
function
|
|
657
|
-
if (options.actionFingerprint) return String(options.actionFingerprint);
|
|
658
|
-
const toolName = options.toolName || options.tool_name || '';
|
|
659
|
-
const toolInput = options.toolInput || options.tool_input || {};
|
|
660
|
-
const parts = [toolName];
|
|
661
|
-
if (typeof toolInput === 'string') {
|
|
662
|
-
parts.push(toolInput);
|
|
663
|
-
} else if (toolInput && typeof toolInput === 'object') {
|
|
664
|
-
parts.push(
|
|
665
|
-
toolInput.command || '',
|
|
666
|
-
toolInput.cmd || '',
|
|
667
|
-
toolInput.file_path || '',
|
|
668
|
-
toolInput.path || '',
|
|
669
|
-
toolInput.description || '',
|
|
670
|
-
toolInput.prompt || '',
|
|
671
|
-
toolInput.pattern || '',
|
|
672
|
-
);
|
|
673
|
-
if (Array.isArray(toolInput.affectedFiles)) parts.push(...toolInput.affectedFiles);
|
|
674
|
-
}
|
|
675
|
-
return actionFingerprint(parts);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function recordStat(gateId, action, gate, options = {}) {
|
|
429
|
+
function recordStat(gateId, action, gate) {
|
|
679
430
|
const stats = loadStats();
|
|
680
431
|
if (action === 'block') stats.blocked = (stats.blocked || 0) + 1;
|
|
681
432
|
else if (action === 'warn') stats.warned = (stats.warned || 0) + 1;
|
|
@@ -688,29 +439,6 @@ function recordStat(gateId, action, gate, options = {}) {
|
|
|
688
439
|
else if (action === 'warn') stats.byGate[gateId].warned += 1;
|
|
689
440
|
else if (action === 'approve') stats.byGate[gateId].pendingApproval = (stats.byGate[gateId].pendingApproval || 0) + 1;
|
|
690
441
|
else if (action === 'log') stats.byGate[gateId].logged = (stats.byGate[gateId].logged || 0) + 1;
|
|
691
|
-
|
|
692
|
-
// Track same-action recurrence within a session for first-time fix rate.
|
|
693
|
-
// Gate-only recurrence over-counts noisy gates; repeats require a stable,
|
|
694
|
-
// sanitized action fingerprint.
|
|
695
|
-
if (action === 'block' || action === 'warn') {
|
|
696
|
-
if (!stats.sessionFiredGates) stats.sessionFiredGates = {};
|
|
697
|
-
if (!stats.sessionFiredActions) stats.sessionFiredActions = {};
|
|
698
|
-
const sessionKey = `session_${Math.floor(Date.now() / SESSION_ACTION_TTL_MS)}`;
|
|
699
|
-
if (!stats.sessionFiredGates[sessionKey]) stats.sessionFiredGates[sessionKey] = {};
|
|
700
|
-
stats.sessionFiredGates[sessionKey][gateId] = true;
|
|
701
|
-
|
|
702
|
-
const fingerprint = buildGateActionFingerprint(gateId, options);
|
|
703
|
-
if (fingerprint) {
|
|
704
|
-
if (!stats.sessionFiredActions[sessionKey]) stats.sessionFiredActions[sessionKey] = {};
|
|
705
|
-
if (!stats.sessionFiredActions[sessionKey][gateId]) stats.sessionFiredActions[sessionKey][gateId] = {};
|
|
706
|
-
if (stats.sessionFiredActions[sessionKey][gateId][fingerprint]) {
|
|
707
|
-
stats.recurringBlocks = (stats.recurringBlocks || 0) + 1;
|
|
708
|
-
} else {
|
|
709
|
-
stats.sessionFiredActions[sessionKey][gateId][fingerprint] = true;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
442
|
saveStats(stats);
|
|
715
443
|
// Track lesson freshness when an auto-promoted gate fires
|
|
716
444
|
if (gate && gate.sourceLessonId) {
|
|
@@ -724,69 +452,6 @@ function recordStat(gateId, action, gate, options = {}) {
|
|
|
724
452
|
}
|
|
725
453
|
}
|
|
726
454
|
|
|
727
|
-
// ---------------------------------------------------------------------------
|
|
728
|
-
// Free-tier daily block cap
|
|
729
|
-
// ---------------------------------------------------------------------------
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Count today's gate blocks from stats. Free tier gets FREE_TIER_DAILY_BLOCKS
|
|
733
|
-
* blocks/day. After the limit, deny → warn + upgrade CTA so the action proceeds
|
|
734
|
-
* but the user sees they lost protection.
|
|
735
|
-
*/
|
|
736
|
-
function getTodayBlockCount() {
|
|
737
|
-
const stats = loadStats();
|
|
738
|
-
const today = todayKey();
|
|
739
|
-
if (!stats.dailyBlocks || !stats.dailyBlocks[today]) return 0;
|
|
740
|
-
return stats.dailyBlocks[today];
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function incrementTodayBlockCount() {
|
|
744
|
-
const stats = loadStats();
|
|
745
|
-
const today = todayKey();
|
|
746
|
-
if (!stats.dailyBlocks) stats.dailyBlocks = {};
|
|
747
|
-
// Clean old dates (keep only last 7 days to prevent unbounded growth)
|
|
748
|
-
const keys = Object.keys(stats.dailyBlocks);
|
|
749
|
-
if (keys.length > 7) {
|
|
750
|
-
keys.sort();
|
|
751
|
-
for (const k of keys.slice(0, keys.length - 7)) {
|
|
752
|
-
delete stats.dailyBlocks[k];
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
stats.dailyBlocks[today] = (stats.dailyBlocks[today] || 0) + 1;
|
|
756
|
-
saveStats(stats);
|
|
757
|
-
return stats.dailyBlocks[today];
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* If the user is free-tier and has exceeded daily block limit, downgrade
|
|
762
|
-
* a deny result to a warn with an upgrade CTA. Returns null if no cap applies.
|
|
763
|
-
*/
|
|
764
|
-
function applyDailyBlockCap(denyResult) {
|
|
765
|
-
// Pro, trial, CI, and THUMBGATE_NO_RATE_LIMIT users are uncapped
|
|
766
|
-
if (isProTier()) return null;
|
|
767
|
-
if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
|
|
768
|
-
|
|
769
|
-
const todayCount = getTodayBlockCount();
|
|
770
|
-
if (todayCount < FREE_TIER_DAILY_BLOCKS) {
|
|
771
|
-
// Under limit: allow the block, increment counter
|
|
772
|
-
incrementTodayBlockCount();
|
|
773
|
-
return null;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Over limit: downgrade deny → warn with upgrade CTA
|
|
777
|
-
const remaining = 0;
|
|
778
|
-
return {
|
|
779
|
-
decision: 'warn',
|
|
780
|
-
gate: denyResult.gate,
|
|
781
|
-
message: `⚠️ ${denyResult.message}\n\n🔓 Daily protection limit reached (${FREE_TIER_DAILY_BLOCKS}/${FREE_TIER_DAILY_BLOCKS} blocks used). This action was allowed through. Upgrade for unlimited protection: https://thumbgate.ai/go/pro`,
|
|
782
|
-
severity: denyResult.severity,
|
|
783
|
-
reasoning: (denyResult.reasoning || []).concat([
|
|
784
|
-
`Free-tier daily block limit (${FREE_TIER_DAILY_BLOCKS}) exceeded — deny downgraded to warn`,
|
|
785
|
-
]),
|
|
786
|
-
dailyBlockCapApplied: true,
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
|
|
790
455
|
// ---------------------------------------------------------------------------
|
|
791
456
|
// Reasoning chain builder
|
|
792
457
|
// ---------------------------------------------------------------------------
|
|
@@ -842,26 +507,9 @@ function toRepoRelativePath(filePath, repoRoot) {
|
|
|
842
507
|
const value = String(filePath || '').trim();
|
|
843
508
|
if (!value) return '';
|
|
844
509
|
if (repoRoot && path.isAbsolute(value)) {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
}
|
|
510
|
+
const relative = path.relative(repoRoot, value);
|
|
511
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
512
|
+
return normalizePosix(relative);
|
|
865
513
|
}
|
|
866
514
|
}
|
|
867
515
|
return normalizePosix(value);
|
|
@@ -940,7 +588,7 @@ function extractAffectedFiles(toolName, toolInput = {}) {
|
|
|
940
588
|
}
|
|
941
589
|
}
|
|
942
590
|
|
|
943
|
-
if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command)
|
|
591
|
+
if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command)) {
|
|
944
592
|
for (const filePath of getBranchDiffFiles(repoRoot)) {
|
|
945
593
|
files.add(normalizePosix(filePath));
|
|
946
594
|
}
|
|
@@ -959,7 +607,6 @@ function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
|
|
|
959
607
|
const command = String(toolInput.command || '');
|
|
960
608
|
// Original high-risk pattern (git writes, publishes, destructive ops)
|
|
961
609
|
if (HIGH_RISK_BASH_PATTERN.test(command)) return true;
|
|
962
|
-
if (isGhApiPrCreateCommand(command)) return true;
|
|
963
610
|
// Broadened: any Bash command that modifies files or has side effects.
|
|
964
611
|
// Excludes pure read/analysis commands (node --test, cat, ls, echo, etc.)
|
|
965
612
|
// to avoid false positives on benign operations.
|
|
@@ -1197,8 +844,7 @@ function getLocalOnlyScopeSources(governanceState = {}, constraints = {}) {
|
|
|
1197
844
|
|
|
1198
845
|
function isRemoteSideEffectCommand(toolName, toolInput = {}) {
|
|
1199
846
|
if (toolName !== 'Bash') return false;
|
|
1200
|
-
|
|
1201
|
-
return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(command) || isGhApiPrCreateCommand(command);
|
|
847
|
+
return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(String(toolInput.command || ''));
|
|
1202
848
|
}
|
|
1203
849
|
|
|
1204
850
|
function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governanceState = {}, constraints = {}) {
|
|
@@ -1220,127 +866,8 @@ function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governa
|
|
|
1220
866
|
};
|
|
1221
867
|
}
|
|
1222
868
|
|
|
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
|
-
|
|
1342
869
|
function recordStructuralGateBlock(toolName, toolInput, result) {
|
|
1343
|
-
recordStat(result.gate, 'block'
|
|
870
|
+
recordStat(result.gate, 'block');
|
|
1344
871
|
const auditRecord = recordAuditEvent({
|
|
1345
872
|
toolName,
|
|
1346
873
|
toolInput,
|
|
@@ -1371,25 +898,6 @@ function shouldEnforceTaskScope(gate, governanceState, toolName, toolInput = {},
|
|
|
1371
898
|
return isScopeEnforcedAction(toolName, toolInput, affectedFiles);
|
|
1372
899
|
}
|
|
1373
900
|
|
|
1374
|
-
function isAgentHookSettingsFile(filePath) {
|
|
1375
|
-
return matchesAnyGlob(filePath, BREAK_GLASS_SETTINGS_GLOBS);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function isBreakGlassSettingsBypass(gate, affectedFiles) {
|
|
1379
|
-
if (!gate || !['task-scope-edit-boundary', 'protected-file-approval-required'].includes(gate.id)) {
|
|
1380
|
-
return false;
|
|
1381
|
-
}
|
|
1382
|
-
if (!isConditionSatisfied(BREAK_GLASS_CONDITION)) return false;
|
|
1383
|
-
return Array.isArray(affectedFiles) && affectedFiles.length > 0 && affectedFiles.every(isAgentHookSettingsFile);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
function isBreakGlassSettingsRecoveryAction(toolName, toolInput = {}) {
|
|
1387
|
-
if (!EDIT_LIKE_TOOLS.has(toolName)) return false;
|
|
1388
|
-
if (!isConditionSatisfied(BREAK_GLASS_CONDITION)) return false;
|
|
1389
|
-
const affectedFiles = extractAffectedFiles(toolName, toolInput).files;
|
|
1390
|
-
return affectedFiles.length > 0 && affectedFiles.every(isAgentHookSettingsFile);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
901
|
function formatFileList(files, limit = 5) {
|
|
1394
902
|
const items = Array.isArray(files) ? files.filter(Boolean) : [];
|
|
1395
903
|
if (items.length === 0) return 'none';
|
|
@@ -1598,19 +1106,8 @@ function checkWhenClause(when, constraints) {
|
|
|
1598
1106
|
if (!when || !when.constraints) return true;
|
|
1599
1107
|
|
|
1600
1108
|
for (const [key, expectedValue] of Object.entries(when.constraints)) {
|
|
1601
|
-
|
|
1602
|
-
if (
|
|
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) {
|
|
1109
|
+
const constraint = constraints[key];
|
|
1110
|
+
if (!constraint || constraint.value !== expectedValue) {
|
|
1614
1111
|
return false;
|
|
1615
1112
|
}
|
|
1616
1113
|
}
|
|
@@ -1618,69 +1115,11 @@ function checkWhenClause(when, constraints) {
|
|
|
1618
1115
|
}
|
|
1619
1116
|
|
|
1620
1117
|
function matchGate(gate, toolName, toolInput = {}) {
|
|
1621
|
-
|
|
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
|
-
|
|
1118
|
+
const matchText = toolInput.command || toolInput.file_path || toolInput.path || '';
|
|
1657
1119
|
const affected = extractAffectedFiles(toolName, toolInput);
|
|
1658
1120
|
const affectedFiles = affected.files;
|
|
1659
1121
|
const repoRoot = affected.repoRoot;
|
|
1660
1122
|
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
|
-
}
|
|
1684
1123
|
|
|
1685
1124
|
if (Array.isArray(gate.toolNames) && gate.toolNames.length > 0 && !gate.toolNames.includes(toolName)) {
|
|
1686
1125
|
return { matched: false, matchText, affectedFiles };
|
|
@@ -1690,12 +1129,6 @@ function matchGate(gate, toolName, toolInput = {}) {
|
|
|
1690
1129
|
try {
|
|
1691
1130
|
const regex = new RegExp(gate.pattern);
|
|
1692
1131
|
if (!regex.test(matchText)) return { matched: false, matchText, affectedFiles };
|
|
1693
|
-
if (gate.id === 'permission-change-approval' && isSafeLocalCredentialHardeningCommand(toolName, toolInput)) {
|
|
1694
|
-
return { matched: false, matchText, affectedFiles };
|
|
1695
|
-
}
|
|
1696
|
-
if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
|
|
1697
|
-
return { matched: false, matchText, affectedFiles };
|
|
1698
|
-
}
|
|
1699
1132
|
} catch {
|
|
1700
1133
|
return { matched: false, matchText, affectedFiles };
|
|
1701
1134
|
}
|
|
@@ -1713,9 +1146,6 @@ function matchGate(gate, toolName, toolInput = {}) {
|
|
|
1713
1146
|
|
|
1714
1147
|
let taskScopeViolation = null;
|
|
1715
1148
|
if (gate.requireTaskScope) {
|
|
1716
|
-
if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
|
|
1717
|
-
return { matched: false, matchText, affectedFiles };
|
|
1718
|
-
}
|
|
1719
1149
|
if (!shouldEnforceTaskScope(gate, governanceState, toolName, toolInput, affectedFiles)) {
|
|
1720
1150
|
return { matched: false, matchText, affectedFiles };
|
|
1721
1151
|
}
|
|
@@ -1725,9 +1155,6 @@ function matchGate(gate, toolName, toolInput = {}) {
|
|
|
1725
1155
|
|
|
1726
1156
|
let protectedApprovalViolation = null;
|
|
1727
1157
|
if (gate.requireProtectedApproval) {
|
|
1728
|
-
if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
|
|
1729
|
-
return { matched: false, matchText, affectedFiles };
|
|
1730
|
-
}
|
|
1731
1158
|
const protectedGlobs = sanitizeGlobList(
|
|
1732
1159
|
Array.isArray(gate.protectedGlobs) && gate.protectedGlobs.length > 0
|
|
1733
1160
|
? gate.protectedGlobs
|
|
@@ -1767,40 +1194,9 @@ function matchesGate(gate, toolName, toolInput) {
|
|
|
1767
1194
|
return matchGate(gate, toolName, toolInput).matched;
|
|
1768
1195
|
}
|
|
1769
1196
|
|
|
1770
|
-
function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
|
|
1771
|
-
if (toolName !== 'Bash') return false;
|
|
1772
|
-
const command = String(toolInput.command || '').trim();
|
|
1773
|
-
if (!command || isRecursiveChmodCommand(command)) return false;
|
|
1774
|
-
if (/[;&|`$()<>*?[\]{}]/.test(command)) return false;
|
|
1775
|
-
|
|
1776
|
-
const match = command.match(/(?:^|\s)chmod\s+(?:-[fv]\s+)?0?([46]00)\s+(['"]?)(\S+)\2\s*$/i);
|
|
1777
|
-
if (!match) return false;
|
|
1778
|
-
|
|
1779
|
-
const target = match[3];
|
|
1780
|
-
if (!target || target === '/' || target === '~') return false;
|
|
1781
|
-
if (target.includes('..')) return false;
|
|
1782
|
-
|
|
1783
|
-
const normalized = target.replace(/^['"]|['"]$/g, '').toLowerCase();
|
|
1784
|
-
const looksLikeCredentialPath = /(?:^|\/)(?:\.config|\.ssh|\.gnupg|\.aws|\.gcloud|\.gemini|\.resume_secrets|\.thumbgate|secrets?|credentials?)(?:\/|$)/.test(normalized)
|
|
1785
|
-
|| /(?:key|secret|token|credential|gemini|gcloud|google|operator).*\.(?:json|pem|key|env)$/i.test(normalized)
|
|
1786
|
-
|| /\.(?:pem|key)$/i.test(normalized);
|
|
1787
|
-
|
|
1788
|
-
return looksLikeCredentialPath;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
1197
|
function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
1792
1198
|
const affected = extractAffectedFiles(toolName, toolInput);
|
|
1793
1199
|
const affectedFiles = affected.files;
|
|
1794
|
-
if (isSafeSecretStorageWrite(toolName, toolInput, process.cwd())) {
|
|
1795
|
-
return null;
|
|
1796
|
-
}
|
|
1797
|
-
// Hardening a credential file's permissions (chmod 600 on a key/secret path) is
|
|
1798
|
-
// a safety action, not a risk. The same exemption already guards the
|
|
1799
|
-
// permission-change-approval gate; without it here, `chmod 600 ~/.resume_secrets/key`
|
|
1800
|
-
// gets hard-denied by recurring-negative-memory matching — the opposite of intent.
|
|
1801
|
-
if (isSafeLocalCredentialHardeningCommand(toolName, toolInput)) {
|
|
1802
|
-
return null;
|
|
1803
|
-
}
|
|
1804
1200
|
if (!isHighRiskAction(toolName, toolInput, affectedFiles)) {
|
|
1805
1201
|
return null;
|
|
1806
1202
|
}
|
|
@@ -1814,10 +1210,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
1814
1210
|
}
|
|
1815
1211
|
|
|
1816
1212
|
const command = String(toolInput.command || '');
|
|
1817
|
-
|
|
1818
|
-
/\bgh\s+pr\s+create\b/i.test(command) || isGhApiPrCreateCommand(command)
|
|
1819
|
-
);
|
|
1820
|
-
if (isPrCreateCommand && isConditionSatisfied('pr_create_allowed')) {
|
|
1213
|
+
if (toolName === 'Bash' && /\bgh\s+pr\s+create\b/i.test(command) && isConditionSatisfied('pr_create_allowed')) {
|
|
1821
1214
|
const branchGovernanceViolation = buildBranchGovernanceViolation(
|
|
1822
1215
|
governanceState,
|
|
1823
1216
|
toolInput,
|
|
@@ -1830,10 +1223,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
1830
1223
|
}
|
|
1831
1224
|
}
|
|
1832
1225
|
|
|
1833
|
-
if (toolName === 'Bash' && (
|
|
1834
|
-
/\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) ||
|
|
1835
|
-
isGhApiPrCreateCommand(command)
|
|
1836
|
-
)) {
|
|
1226
|
+
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)) {
|
|
1837
1227
|
const branchGovernanceViolation = buildBranchGovernanceViolation(
|
|
1838
1228
|
governanceState,
|
|
1839
1229
|
toolInput,
|
|
@@ -1871,14 +1261,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
1871
1261
|
filePath: toolInput.file_path || toolInput.path || null,
|
|
1872
1262
|
affectedFiles,
|
|
1873
1263
|
});
|
|
1874
|
-
|
|
1875
|
-
let guard;
|
|
1876
|
-
if (toolInput && (toolInput.clawContext || toolInput._claw || toolInput.hybridRoute || toolInput.agentId)) {
|
|
1877
|
-
const clawCtx = toolInput.clawContext || toolInput._claw || { actionType: toolInput.actionType || 'unknown', agentId: toolInput.agentId || 'unknown', hybridRoute: toolInput.hybridRoute || 'unknown' };
|
|
1878
|
-
guard = hybrid.evaluateClawPretool ? hybrid.evaluateClawPretool(toolName, serializedInput, clawCtx) : hybrid.evaluatePretool(toolName, serializedInput);
|
|
1879
|
-
} else {
|
|
1880
|
-
guard = hybrid.evaluatePretool(toolName, serializedInput);
|
|
1881
|
-
}
|
|
1264
|
+
const guard = hybrid.evaluatePretool(toolName, serializedInput);
|
|
1882
1265
|
if (!guard || guard.mode === 'allow') {
|
|
1883
1266
|
return null;
|
|
1884
1267
|
}
|
|
@@ -2016,26 +1399,10 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
2016
1399
|
if (localOnlyRemoteSideEffectGate) {
|
|
2017
1400
|
return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
|
|
2018
1401
|
}
|
|
2019
|
-
const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
|
|
2020
|
-
if (statefulHelperBypassGate) {
|
|
2021
|
-
return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
|
|
2022
|
-
}
|
|
2023
|
-
if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
|
|
2024
|
-
recordAuditEvent({
|
|
2025
|
-
toolName,
|
|
2026
|
-
toolInput,
|
|
2027
|
-
decision: 'allow',
|
|
2028
|
-
gateId: BREAK_GLASS_CONDITION,
|
|
2029
|
-
message: 'Break-glass recovery allowed hook settings edit',
|
|
2030
|
-
severity: 'high',
|
|
2031
|
-
source: 'gates-engine',
|
|
2032
|
-
});
|
|
2033
|
-
return null;
|
|
2034
|
-
}
|
|
2035
1402
|
|
|
2036
1403
|
const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
|
|
2037
1404
|
if (pendingThreadResolutionGate) {
|
|
2038
|
-
recordStat(pendingThreadResolutionGate.gate, 'block'
|
|
1405
|
+
recordStat(pendingThreadResolutionGate.gate, 'block');
|
|
2039
1406
|
const auditRecord = recordAuditEvent({
|
|
2040
1407
|
toolName,
|
|
2041
1408
|
toolInput,
|
|
@@ -2051,7 +1418,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
2051
1418
|
|
|
2052
1419
|
const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
|
|
2053
1420
|
if (boostedRiskGuard) {
|
|
2054
|
-
recordStat(boostedRiskGuard.gate, 'block'
|
|
1421
|
+
recordStat(boostedRiskGuard.gate, 'block');
|
|
2055
1422
|
const auditRecord = recordAuditEvent({
|
|
2056
1423
|
toolName,
|
|
2057
1424
|
toolInput,
|
|
@@ -2065,23 +1432,6 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
2065
1432
|
return boostedRiskGuard;
|
|
2066
1433
|
}
|
|
2067
1434
|
|
|
2068
|
-
// Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
|
|
2069
|
-
// Keep runtime enforcement explicit so advisory planning checks do not mask
|
|
2070
|
-
// higher-priority deny/approve gates in established workflows.
|
|
2071
|
-
if (isRuntimePlanGateEnabled()) {
|
|
2072
|
-
const planGate = evaluatePlanGate(toolName, toolInput);
|
|
2073
|
-
if (planGate) {
|
|
2074
|
-
recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
|
|
2075
|
-
return planGate;
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
const trajectory = getTrajectoryScore();
|
|
2079
|
-
if (trajectory.isDrifting) {
|
|
2080
|
-
recordStat('strategic-drift', 'block', null, { toolName, toolInput });
|
|
2081
|
-
return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
1435
|
// Fast-path: feedback/recall tools skip metric gates entirely (avoids Stripe API calls)
|
|
2086
1436
|
const METRIC_SKIP_TOOLS = ['capture_feedback', 'feedback_stats', 'recall', 'feedback_summary', 'prevention_rules'];
|
|
2087
1437
|
const skipMetrics = METRIC_SKIP_TOOLS.includes(toolName);
|
|
@@ -2127,39 +1477,23 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
2127
1477
|
});
|
|
2128
1478
|
|
|
2129
1479
|
if (gate.action === 'block') {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
const cappedResult = applyDailyBlockCap(denyResult);
|
|
2133
|
-
if (cappedResult) {
|
|
2134
|
-
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
2135
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
|
|
2136
|
-
auditToFeedback(auditRecord);
|
|
2137
|
-
return cappedResult;
|
|
2138
|
-
}
|
|
2139
|
-
recordStat(gate.id, 'block', gate, { toolName, toolInput });
|
|
1480
|
+
recordStat(gate.id, 'block', gate);
|
|
1481
|
+
const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
2140
1482
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2141
1483
|
auditToFeedback(auditRecord);
|
|
2142
|
-
return
|
|
1484
|
+
return result;
|
|
2143
1485
|
}
|
|
2144
1486
|
|
|
2145
1487
|
if (gate.action === 'approve') {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
|
|
2150
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2151
|
-
auditToFeedback(auditRecord);
|
|
2152
|
-
return result;
|
|
2153
|
-
}
|
|
2154
|
-
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
2155
|
-
const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
|
|
2156
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1488
|
+
recordStat(gate.id, 'approve', gate);
|
|
1489
|
+
const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
|
|
1490
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2157
1491
|
auditToFeedback(auditRecord);
|
|
2158
1492
|
return result;
|
|
2159
1493
|
}
|
|
2160
1494
|
|
|
2161
1495
|
if (gate.action === 'log') {
|
|
2162
|
-
recordStat(gate.id, 'log', gate
|
|
1496
|
+
recordStat(gate.id, 'log', gate);
|
|
2163
1497
|
const result = { decision: 'log', gate: gate.id, message, severity: gate.severity, reasoning, logged: true };
|
|
2164
1498
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2165
1499
|
auditToFeedback(auditRecord);
|
|
@@ -2168,7 +1502,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
2168
1502
|
}
|
|
2169
1503
|
|
|
2170
1504
|
if (gate.action === 'warn') {
|
|
2171
|
-
recordStat(gate.id, 'warn', gate
|
|
1505
|
+
recordStat(gate.id, 'warn', gate);
|
|
2172
1506
|
const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
2173
1507
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2174
1508
|
auditToFeedback(auditRecord);
|
|
@@ -2176,15 +1510,14 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
2176
1510
|
}
|
|
2177
1511
|
}
|
|
2178
1512
|
|
|
2179
|
-
const
|
|
2180
|
-
const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1513
|
+
const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
|
|
2181
1514
|
governanceState,
|
|
2182
1515
|
});
|
|
2183
1516
|
const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
|
|
2184
1517
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
2185
1518
|
if (memoryGuard) {
|
|
2186
1519
|
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
2187
|
-
recordStat(enrichedMemoryGuard.gate, 'block'
|
|
1520
|
+
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
2188
1521
|
recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
|
|
2189
1522
|
const auditRecord = recordAuditEvent({
|
|
2190
1523
|
toolName,
|
|
@@ -2201,7 +1534,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
2201
1534
|
|
|
2202
1535
|
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
2203
1536
|
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
2204
|
-
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn'
|
|
1537
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
2205
1538
|
recordSentinelBlockDecision(sentinelDecision, sentinelResult);
|
|
2206
1539
|
const auditRecord = recordAuditEvent({
|
|
2207
1540
|
toolName,
|
|
@@ -2247,26 +1580,10 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2247
1580
|
if (localOnlyRemoteSideEffectGate) {
|
|
2248
1581
|
return recordStructuralGateBlock(toolName, toolInput, localOnlyRemoteSideEffectGate);
|
|
2249
1582
|
}
|
|
2250
|
-
const statefulHelperBypassGate = evaluateStatefulHelperBypassGate(toolName, toolInput);
|
|
2251
|
-
if (statefulHelperBypassGate) {
|
|
2252
|
-
return recordStructuralGateBlock(toolName, toolInput, statefulHelperBypassGate);
|
|
2253
|
-
}
|
|
2254
|
-
if (isBreakGlassSettingsRecoveryAction(toolName, toolInput)) {
|
|
2255
|
-
recordAuditEvent({
|
|
2256
|
-
toolName,
|
|
2257
|
-
toolInput,
|
|
2258
|
-
decision: 'allow',
|
|
2259
|
-
gateId: BREAK_GLASS_CONDITION,
|
|
2260
|
-
message: 'Break-glass recovery allowed hook settings edit',
|
|
2261
|
-
severity: 'high',
|
|
2262
|
-
source: 'gates-engine',
|
|
2263
|
-
});
|
|
2264
|
-
return null;
|
|
2265
|
-
}
|
|
2266
1583
|
|
|
2267
1584
|
const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
|
|
2268
1585
|
if (pendingThreadResolutionGate) {
|
|
2269
|
-
recordStat(pendingThreadResolutionGate.gate, 'block'
|
|
1586
|
+
recordStat(pendingThreadResolutionGate.gate, 'block');
|
|
2270
1587
|
const auditRecord = recordAuditEvent({
|
|
2271
1588
|
toolName,
|
|
2272
1589
|
toolInput,
|
|
@@ -2282,7 +1599,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2282
1599
|
|
|
2283
1600
|
const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
|
|
2284
1601
|
if (boostedRiskGuard) {
|
|
2285
|
-
recordStat(boostedRiskGuard.gate, 'block'
|
|
1602
|
+
recordStat(boostedRiskGuard.gate, 'block');
|
|
2286
1603
|
const auditRecord = recordAuditEvent({
|
|
2287
1604
|
toolName,
|
|
2288
1605
|
toolInput,
|
|
@@ -2296,23 +1613,6 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2296
1613
|
return boostedRiskGuard;
|
|
2297
1614
|
}
|
|
2298
1615
|
|
|
2299
|
-
// Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
|
|
2300
|
-
// Keep runtime enforcement explicit so advisory planning checks do not mask
|
|
2301
|
-
// higher-priority deny/approve gates in established workflows.
|
|
2302
|
-
if (isRuntimePlanGateEnabled()) {
|
|
2303
|
-
const planGate = evaluatePlanGate(toolName, toolInput);
|
|
2304
|
-
if (planGate) {
|
|
2305
|
-
recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
|
|
2306
|
-
return planGate;
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
const trajectory = getTrajectoryScore();
|
|
2310
|
-
if (trajectory.isDrifting) {
|
|
2311
|
-
recordStat('strategic-drift', 'block', null, { toolName, toolInput });
|
|
2312
|
-
return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
1616
|
for (const gate of config.gates) {
|
|
2317
1617
|
const matchDetails = matchGate(gate, toolName, toolInput);
|
|
2318
1618
|
if (!matchDetails.matched) continue;
|
|
@@ -2331,39 +1631,23 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2331
1631
|
const reasoning = buildReasoning(gate, toolName, toolInput, matchDetails);
|
|
2332
1632
|
|
|
2333
1633
|
if (gate.action === 'block') {
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
const cappedResult = applyDailyBlockCap(denyResult);
|
|
2337
|
-
if (cappedResult) {
|
|
2338
|
-
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
2339
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
|
|
2340
|
-
auditToFeedback(auditRecord);
|
|
2341
|
-
return cappedResult;
|
|
2342
|
-
}
|
|
2343
|
-
recordStat(gate.id, 'block', gate, { toolName, toolInput });
|
|
1634
|
+
recordStat(gate.id, 'block', gate);
|
|
1635
|
+
const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
2344
1636
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2345
1637
|
auditToFeedback(auditRecord);
|
|
2346
|
-
return
|
|
1638
|
+
return result;
|
|
2347
1639
|
}
|
|
2348
1640
|
|
|
2349
1641
|
if (gate.action === 'approve') {
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
|
|
2354
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2355
|
-
auditToFeedback(auditRecord);
|
|
2356
|
-
return result;
|
|
2357
|
-
}
|
|
2358
|
-
recordStat(gate.id, 'warn', gate, { toolName, toolInput });
|
|
2359
|
-
const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
|
|
2360
|
-
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1642
|
+
recordStat(gate.id, 'approve', gate);
|
|
1643
|
+
const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
|
|
1644
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2361
1645
|
auditToFeedback(auditRecord);
|
|
2362
1646
|
return result;
|
|
2363
1647
|
}
|
|
2364
1648
|
|
|
2365
1649
|
if (gate.action === 'log') {
|
|
2366
|
-
recordStat(gate.id, 'log', gate
|
|
1650
|
+
recordStat(gate.id, 'log', gate);
|
|
2367
1651
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2368
1652
|
auditToFeedback(auditRecord);
|
|
2369
1653
|
// 'log' action allows the tool call to proceed — continue to next gate
|
|
@@ -2371,7 +1655,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2371
1655
|
}
|
|
2372
1656
|
|
|
2373
1657
|
if (gate.action === 'warn') {
|
|
2374
|
-
recordStat(gate.id, 'warn', gate
|
|
1658
|
+
recordStat(gate.id, 'warn', gate);
|
|
2375
1659
|
const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
2376
1660
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
2377
1661
|
auditToFeedback(auditRecord);
|
|
@@ -2379,15 +1663,14 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2379
1663
|
}
|
|
2380
1664
|
}
|
|
2381
1665
|
|
|
2382
|
-
const
|
|
2383
|
-
const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1666
|
+
const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
|
|
2384
1667
|
governanceState,
|
|
2385
1668
|
});
|
|
2386
1669
|
const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
|
|
2387
1670
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
2388
1671
|
if (memoryGuard) {
|
|
2389
1672
|
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
2390
|
-
recordStat(enrichedMemoryGuard.gate, 'block'
|
|
1673
|
+
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
2391
1674
|
recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
|
|
2392
1675
|
const auditRecord = recordAuditEvent({
|
|
2393
1676
|
toolName,
|
|
@@ -2404,7 +1687,7 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2404
1687
|
|
|
2405
1688
|
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
2406
1689
|
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
2407
|
-
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn'
|
|
1690
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
2408
1691
|
recordSentinelBlockDecision(sentinelDecision, sentinelResult);
|
|
2409
1692
|
const auditRecord = recordAuditEvent({
|
|
2410
1693
|
toolName,
|
|
@@ -2424,43 +1707,14 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
2424
1707
|
return null;
|
|
2425
1708
|
}
|
|
2426
1709
|
|
|
2427
|
-
|
|
2428
|
-
// safe path, instead of a dead-end that drives agents toward brittle
|
|
2429
|
-
// workarounds (e.g. writing secrets to /tmp). The vault dirs referenced here
|
|
2430
|
-
// are the SAME constant the scanner whitelists, so the hint can never drift
|
|
2431
|
-
// from enforcement.
|
|
2432
|
-
function buildSecretRemediation(toolName = '', toolInput = {}) {
|
|
2433
|
-
const vaultDirs = (SAFE_SECRET_STORAGE_DIRS || []).map((dir) => `~/${dir}`);
|
|
2434
|
-
const primaryVault = vaultDirs[0] || '~/.resume_secrets';
|
|
2435
|
-
const vaultList = vaultDirs.join(', ') || primaryVault;
|
|
2436
|
-
|
|
2437
|
-
if (EDIT_LIKE_TOOLS && EDIT_LIKE_TOOLS.has(toolName)) {
|
|
2438
|
-
const target = toolInput.file_path || toolInput.path || toolInput.filePath || toolInput.target_path;
|
|
2439
|
-
const where = target ? ` (you targeted ${redactText(String(target))})` : '';
|
|
2440
|
-
return `To store this secret safely, write it with the Write/Edit tool to a file under ${vaultList}${where}. `
|
|
2441
|
-
+ `Those locations are whitelisted for secret storage and will NOT be blocked. `
|
|
2442
|
-
+ `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.`;
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
if (toolName === 'Bash') {
|
|
2446
|
-
return `Do not inline a live secret literal into a shell command — it leaks into shell history and process args. `
|
|
2447
|
-
+ `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.`;
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
return `Store secrets in the whitelisted vault (${vaultList}) using the Write tool rather than passing the literal through this action.`;
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
function buildSecretGuardResult(scanResult, context = {}) {
|
|
2454
|
-
const remediation = buildSecretRemediation(context.toolName, context.toolInput || {});
|
|
2455
|
-
const summary = buildSafeSummary(
|
|
2456
|
-
scanResult.findings,
|
|
2457
|
-
'Blocked because the action appears to expose secret material'
|
|
2458
|
-
);
|
|
1710
|
+
function buildSecretGuardResult(scanResult) {
|
|
2459
1711
|
return {
|
|
2460
1712
|
decision: 'deny',
|
|
2461
1713
|
gate: 'secret-exfiltration',
|
|
2462
|
-
message:
|
|
2463
|
-
|
|
1714
|
+
message: buildSafeSummary(
|
|
1715
|
+
scanResult.findings,
|
|
1716
|
+
'Blocked because the action appears to expose secret material'
|
|
1717
|
+
),
|
|
2464
1718
|
severity: 'critical',
|
|
2465
1719
|
secretScan: {
|
|
2466
1720
|
provider: scanResult.provider,
|
|
@@ -2539,15 +1793,9 @@ function evaluateSecretGuard(input = {}) {
|
|
|
2539
1793
|
if (!scanResult.detected) {
|
|
2540
1794
|
return null;
|
|
2541
1795
|
}
|
|
2542
|
-
recordStat('secret-exfiltration', 'block'
|
|
2543
|
-
toolName: input.tool_name || input.toolName || 'unknown',
|
|
2544
|
-
toolInput: input.tool_input || {},
|
|
2545
|
-
});
|
|
1796
|
+
recordStat('secret-exfiltration', 'block');
|
|
2546
1797
|
recordSecretViolation(input, scanResult);
|
|
2547
|
-
const result = buildSecretGuardResult(scanResult
|
|
2548
|
-
toolName: input.tool_name || input.toolName,
|
|
2549
|
-
toolInput: input.tool_input || {},
|
|
2550
|
-
});
|
|
1798
|
+
const result = buildSecretGuardResult(scanResult);
|
|
2551
1799
|
// Audit trail: record secret guard denial
|
|
2552
1800
|
const auditRecord = recordAuditEvent({
|
|
2553
1801
|
toolName: input.tool_name || input.toolName || 'unknown',
|
|
@@ -2563,55 +1811,15 @@ function evaluateSecretGuard(input = {}) {
|
|
|
2563
1811
|
}
|
|
2564
1812
|
|
|
2565
1813
|
// ---------------------------------------------------------------------------
|
|
2566
|
-
function isApprovalGatesEnabled() {
|
|
2567
|
-
return process.env.THUMBGATE_APPROVAL_GATES !== '0';
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
1814
|
// PreToolUse hook interface (stdin/stdout JSON)
|
|
2571
1815
|
// ---------------------------------------------------------------------------
|
|
2572
1816
|
|
|
2573
|
-
function buildPreToolUseOutput(fields = {}) {
|
|
2574
|
-
return {
|
|
2575
|
-
hookEventName: 'PreToolUse',
|
|
2576
|
-
...fields,
|
|
2577
|
-
};
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
1817
|
function buildReminderOutput(context) {
|
|
2581
|
-
return
|
|
1818
|
+
return {
|
|
2582
1819
|
additionalContext: context,
|
|
2583
1820
|
systemReminder: context,
|
|
2584
1821
|
thumbgateSystemReminder: context,
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
// ---------------------------------------------------------------------------
|
|
2589
|
-
// Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
|
|
2590
|
-
// Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
|
|
2591
|
-
// Highest-intent moment — user just saw ThumbGate save them from a mistake.
|
|
2592
|
-
// ---------------------------------------------------------------------------
|
|
2593
|
-
|
|
2594
|
-
function buildBlockActionProCta() {
|
|
2595
|
-
try {
|
|
2596
|
-
if (process.env.THUMBGATE_NO_NUDGE === '1') return null;
|
|
2597
|
-
if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
|
|
2598
|
-
if (isProTier()) return null;
|
|
2599
|
-
if (isInTrialPeriod()) return null; // Already have full access
|
|
2600
|
-
|
|
2601
|
-
const stats = loadStats();
|
|
2602
|
-
const totalBlocks = stats.blocked || 0;
|
|
2603
|
-
if (totalBlocks < 5) return null; // Too early — let them experience the product
|
|
2604
|
-
|
|
2605
|
-
if (totalBlocks < 25) {
|
|
2606
|
-
return '\n\n💡 Pro: keep this rule synced across laptops, CI, containers, and agent runtimes → thumbgate.ai/go/pro';
|
|
2607
|
-
}
|
|
2608
|
-
if (totalBlocks < 100) {
|
|
2609
|
-
return `\n\n💡 ${totalBlocks} actions blocked. Pro keeps these lessons/rules synced everywhere → thumbgate.ai/go/pro ($19/mo)`;
|
|
2610
|
-
}
|
|
2611
|
-
return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use shared hosted enforcement → thumbgate.ai/go/pro`;
|
|
2612
|
-
} catch (_) {
|
|
2613
|
-
return null;
|
|
2614
|
-
}
|
|
1822
|
+
};
|
|
2615
1823
|
}
|
|
2616
1824
|
|
|
2617
1825
|
function formatOutput(result, behavioralContext) {
|
|
@@ -2630,28 +1838,13 @@ function formatOutput(result, behavioralContext) {
|
|
|
2630
1838
|
: '';
|
|
2631
1839
|
|
|
2632
1840
|
if (result.decision === 'deny') {
|
|
2633
|
-
const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
|
|
2634
|
-
const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
|
|
2635
|
-
const proCta = buildBlockActionProCta() || '';
|
|
2636
|
-
return JSON.stringify({
|
|
2637
|
-
hookSpecificOutput: {
|
|
2638
|
-
hookEventName: 'PreToolUse',
|
|
2639
|
-
...reminder,
|
|
2640
|
-
permissionDecision: 'deny',
|
|
2641
|
-
permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}${proCta}`,
|
|
2642
|
-
},
|
|
2643
|
-
});
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
if (result.decision === 'approve') {
|
|
2647
1841
|
const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
|
|
2648
1842
|
const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
|
|
2649
1843
|
return JSON.stringify({
|
|
2650
1844
|
hookSpecificOutput: {
|
|
2651
|
-
hookEventName: 'PreToolUse',
|
|
2652
1845
|
...reminder,
|
|
2653
1846
|
permissionDecision: 'deny',
|
|
2654
|
-
permissionDecisionReason: `[GATE:${result.gate}]
|
|
1847
|
+
permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}`,
|
|
2655
1848
|
},
|
|
2656
1849
|
});
|
|
2657
1850
|
}
|
|
@@ -2661,7 +1854,6 @@ function formatOutput(result, behavioralContext) {
|
|
|
2661
1854
|
const context = `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}${extra}`;
|
|
2662
1855
|
return JSON.stringify({
|
|
2663
1856
|
hookSpecificOutput: {
|
|
2664
|
-
hookEventName: 'PreToolUse',
|
|
2665
1857
|
additionalContext: context,
|
|
2666
1858
|
...(behavioralContext ? {
|
|
2667
1859
|
systemReminder: behavioralContext,
|
|
@@ -2785,7 +1977,9 @@ function buildRecentCorrectiveActionsContext(options = {}) {
|
|
|
2785
1977
|
function buildRelevantLessonContext(toolName, toolInput) {
|
|
2786
1978
|
if (!toolName) return null;
|
|
2787
1979
|
|
|
2788
|
-
const { retrieveRelevantLessons
|
|
1980
|
+
const { retrieveRelevantLessons } = loadOptionalModule('./lesson-retrieval', () => ({
|
|
1981
|
+
retrieveRelevantLessons: () => [],
|
|
1982
|
+
}));
|
|
2789
1983
|
|
|
2790
1984
|
// Extract a searchable action context from the tool input
|
|
2791
1985
|
const actionContext = extractActionContext(toolName, toolInput);
|
|
@@ -2793,113 +1987,32 @@ function buildRelevantLessonContext(toolName, toolInput) {
|
|
|
2793
1987
|
|
|
2794
1988
|
try {
|
|
2795
1989
|
const lessons = retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
|
|
1990
|
+
// retrieveRelevantLessons already filters at relevanceScore > 0.1 internally;
|
|
1991
|
+
// any negative lesson that survives retrieval is relevant enough to surface.
|
|
1992
|
+
const negative = lessons.filter((l) => l.signal === 'negative');
|
|
1993
|
+
if (negative.length === 0) return null;
|
|
1994
|
+
|
|
1995
|
+
const formatted = negative.map((l) => {
|
|
1996
|
+
const title = (l.title || '').replace(/^MISTAKE:\s*/, '').slice(0, 140);
|
|
1997
|
+
const advice = extractAvoidanceAdvice(l.content);
|
|
1998
|
+
return advice ? ` • ${title}\n → ${advice}` : ` • ${title}`;
|
|
1999
|
+
});
|
|
2796
2000
|
|
|
2797
|
-
|
|
2798
|
-
if (entropy > KNOWLEDGE_ENTROPY_THRESHOLD) {
|
|
2799
|
-
return buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy);
|
|
2800
|
-
}
|
|
2801
|
-
return formatNegativeLessonContext(lessons);
|
|
2802
|
-
} catch {
|
|
2803
|
-
return null;
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
/**
|
|
2808
|
-
* Async counterpart of buildRelevantLessonContext: uses HYBRID (dense embeddings +
|
|
2809
|
-
* lexical) retrieval so the agent is warned about semantically-related past mistakes
|
|
2810
|
-
* even when they share no keywords with the current action. Wired into runAsync.
|
|
2811
|
-
* Degrades to the lexical result automatically when no embedder is available.
|
|
2812
|
-
*/
|
|
2813
|
-
async function buildRelevantLessonContextAsync(toolName, toolInput) {
|
|
2814
|
-
if (!toolName) return null;
|
|
2815
|
-
|
|
2816
|
-
const { retrieveRelevantLessonsAsync, retrieveRelevantLessons, calculateRetrievalEntropy } = loadOptionalModule(
|
|
2817
|
-
'./lesson-retrieval',
|
|
2818
|
-
() => ({ retrieveRelevantLessonsAsync: null, retrieveRelevantLessons: () => [], calculateRetrievalEntropy: () => 0 }),
|
|
2819
|
-
);
|
|
2820
|
-
|
|
2821
|
-
const actionContext = extractActionContext(toolName, toolInput);
|
|
2822
|
-
if (!actionContext) return null;
|
|
2823
|
-
|
|
2824
|
-
try {
|
|
2825
|
-
const lessons = retrieveRelevantLessonsAsync
|
|
2826
|
-
? await retrieveRelevantLessonsAsync(toolName, actionContext, { maxResults: 3 })
|
|
2827
|
-
: retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
|
|
2828
|
-
|
|
2829
|
-
// Knowledge Conflict Detection: if retrieved lessons have high sentiment entropy,
|
|
2830
|
-
// it indicates conflicting past evidence. Warn by default; hard-block only in
|
|
2831
|
-
// strict mode for external/destructive side-effect commands.
|
|
2832
|
-
const entropy = calculateRetrievalEntropy(lessons);
|
|
2833
|
-
if (entropy > KNOWLEDGE_ENTROPY_THRESHOLD) {
|
|
2834
|
-
return buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy);
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
return formatNegativeLessonContext(lessons);
|
|
2001
|
+
return `[ThumbGate] Past mistakes relevant to this action — read before proceeding:\n${formatted.join('\n')}`;
|
|
2838
2002
|
} catch {
|
|
2839
2003
|
return null;
|
|
2840
2004
|
}
|
|
2841
2005
|
}
|
|
2842
2006
|
|
|
2843
|
-
/**
|
|
2844
|
-
* Shared formatter: render the negative (mistake) lessons that survived retrieval
|
|
2845
|
-
* into the PreToolUse warning block. Retrieval already filters by relevance, so any
|
|
2846
|
-
* negative lesson present is relevant enough to surface.
|
|
2847
|
-
*/
|
|
2848
|
-
function formatNegativeLessonContext(lessons) {
|
|
2849
|
-
const negative = (lessons || []).filter((l) => l.signal === 'negative');
|
|
2850
|
-
if (negative.length === 0) return null;
|
|
2851
|
-
|
|
2852
|
-
const formatted = negative.map((l) => {
|
|
2853
|
-
const title = (l.title || '').replace(/^MISTAKE:\s*/, '').slice(0, 140);
|
|
2854
|
-
const advice = extractAvoidanceAdvice(l.content);
|
|
2855
|
-
return advice ? ` • ${title}\n → ${advice}` : ` • ${title}`;
|
|
2856
|
-
});
|
|
2857
|
-
|
|
2858
|
-
return `[ThumbGate] Past mistakes relevant to this action — read before proceeding:\n${formatted.join('\n')}`;
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
function isStrictKnowledgeConflictMode() {
|
|
2862
|
-
return process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1'
|
|
2863
|
-
|| process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === 'true';
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
function isKnowledgeConflictHardBlockAction(toolName, toolInput = {}) {
|
|
2867
|
-
if (!isStrictKnowledgeConflictMode()) return false;
|
|
2868
|
-
if (EDIT_LIKE_TOOLS.has(toolName)) return true;
|
|
2869
|
-
if (toolName !== 'Bash') return false;
|
|
2870
|
-
return KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN.test(String(toolInput.command || ''));
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
function buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy) {
|
|
2874
|
-
const lessonContext = formatNegativeLessonContext(lessons);
|
|
2875
|
-
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.`;
|
|
2876
|
-
|
|
2877
|
-
if (isKnowledgeConflictHardBlockAction(toolName, toolInput)) {
|
|
2878
|
-
recordStat('retrieval_entropy_high', 'block', null, { toolName, toolInput });
|
|
2879
|
-
return {
|
|
2880
|
-
decision: 'deny',
|
|
2881
|
-
gate: 'knowledge-conflict-gate',
|
|
2882
|
-
message: `✗ THUMBGATE: ${message} Strict mode is enabled for destructive or external side-effect actions; verify intent or narrow the task before proceeding.`,
|
|
2883
|
-
severity: 'high',
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
2886
|
-
|
|
2887
|
-
recordStat('retrieval_entropy_high', 'warn', null, { toolName, toolInput });
|
|
2888
|
-
return mergeContextStrings(`[ThumbGate] ${message}`, lessonContext);
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
2007
|
function extractActionContext(toolName, toolInput) {
|
|
2892
2008
|
if (!toolInput) return toolName;
|
|
2893
2009
|
const parts = [toolName];
|
|
2894
2010
|
if (toolInput.command) parts.push(String(toolInput.command).slice(0, 400));
|
|
2895
2011
|
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));
|
|
2899
2012
|
if (toolInput.description) parts.push(String(toolInput.description).slice(0, 200));
|
|
2900
2013
|
if (toolInput.prompt) parts.push(String(toolInput.prompt).slice(0, 400));
|
|
2901
2014
|
if (toolInput.pattern) parts.push(String(toolInput.pattern).slice(0, 200));
|
|
2902
|
-
return
|
|
2015
|
+
return parts.filter(Boolean).join(' ');
|
|
2903
2016
|
}
|
|
2904
2017
|
|
|
2905
2018
|
function extractAvoidanceAdvice(content) {
|
|
@@ -2928,13 +2041,6 @@ async function runAsync(input) {
|
|
|
2928
2041
|
|
|
2929
2042
|
const toolName = input.tool_name || '';
|
|
2930
2043
|
const toolInput = input.tool_input || {};
|
|
2931
|
-
const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
|
|
2932
|
-
|
|
2933
|
-
const sequenceGuard = evaluateSequenceState(toolName, toolInput);
|
|
2934
|
-
if (sequenceGuard && sequenceGuard.decision === 'deny') {
|
|
2935
|
-
return formatOutput(applyEnforcementPosture(sequenceGuard));
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
2044
|
const result = await evaluateGatesAsync(toolName, toolInput);
|
|
2939
2045
|
|
|
2940
2046
|
// Attach security warnings to allow/warn results
|
|
@@ -2947,18 +2053,11 @@ async function runAsync(input) {
|
|
|
2947
2053
|
}
|
|
2948
2054
|
}
|
|
2949
2055
|
|
|
2950
|
-
|
|
2951
|
-
const
|
|
2952
|
-
const lessonContext = safeSecretStorageWrite ? null : await buildRelevantLessonContextAsync(toolName, toolInput);
|
|
2953
|
-
|
|
2954
|
-
if (lessonContext && lessonContext.decision === "deny") {
|
|
2955
|
-
return formatOutput(applyEnforcementPosture(lessonContext));
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2056
|
+
const behavioralContext = buildBehavioralContext();
|
|
2057
|
+
const lessonContext = buildRelevantLessonContext(toolName, toolInput);
|
|
2958
2058
|
const recentContext = buildRecentCorrectiveActionsContext();
|
|
2959
2059
|
const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
|
|
2960
|
-
return formatOutput(
|
|
2961
|
-
|
|
2060
|
+
return formatOutput(result, combinedContext);
|
|
2962
2061
|
}
|
|
2963
2062
|
|
|
2964
2063
|
function run(input) {
|
|
@@ -2975,13 +2074,6 @@ function run(input) {
|
|
|
2975
2074
|
|
|
2976
2075
|
const toolName = input.tool_name || '';
|
|
2977
2076
|
const toolInput = input.tool_input || {};
|
|
2978
|
-
const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
|
|
2979
|
-
|
|
2980
|
-
const sequenceGuard = evaluateSequenceState(toolName, toolInput);
|
|
2981
|
-
if (sequenceGuard && sequenceGuard.decision === 'deny') {
|
|
2982
|
-
return formatOutput(applyEnforcementPosture(sequenceGuard));
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
2077
|
const result = evaluateGates(toolName, toolInput);
|
|
2986
2078
|
|
|
2987
2079
|
// Attach security warnings to allow/warn results
|
|
@@ -2994,18 +2086,11 @@ function run(input) {
|
|
|
2994
2086
|
}
|
|
2995
2087
|
}
|
|
2996
2088
|
|
|
2997
|
-
|
|
2998
|
-
const
|
|
2999
|
-
const lessonContext = safeSecretStorageWrite ? null : buildRelevantLessonContext(toolName, toolInput);
|
|
3000
|
-
|
|
3001
|
-
if (lessonContext && lessonContext.decision === "deny") {
|
|
3002
|
-
return formatOutput(applyEnforcementPosture(lessonContext));
|
|
3003
|
-
}
|
|
3004
|
-
|
|
2089
|
+
const behavioralContext = buildBehavioralContext();
|
|
2090
|
+
const lessonContext = buildRelevantLessonContext(toolName, toolInput);
|
|
3005
2091
|
const recentContext = buildRecentCorrectiveActionsContext();
|
|
3006
2092
|
const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
|
|
3007
|
-
return formatOutput(
|
|
3008
|
-
|
|
2093
|
+
return formatOutput(result, combinedContext);
|
|
3009
2094
|
}
|
|
3010
2095
|
|
|
3011
2096
|
// ---------------------------------------------------------------------------
|
|
@@ -3138,84 +2223,7 @@ function registerClaimGate(claimPattern, requiredActions, blockMessage) {
|
|
|
3138
2223
|
return entry;
|
|
3139
2224
|
}
|
|
3140
2225
|
|
|
3141
|
-
function
|
|
3142
|
-
if (!Array.isArray(value)) return [];
|
|
3143
|
-
return Array.from(new Set(
|
|
3144
|
-
value
|
|
3145
|
-
.map((item) => String(item || '').trim())
|
|
3146
|
-
.filter(Boolean)
|
|
3147
|
-
));
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
function normalizeGoalContract(goalContract) {
|
|
3151
|
-
if (!goalContract || typeof goalContract !== 'object' || Array.isArray(goalContract)) {
|
|
3152
|
-
return null;
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
const goal = String(goalContract.goal || '').trim();
|
|
3156
|
-
const doneWhen = normalizeStringArray(goalContract.doneWhen);
|
|
3157
|
-
const requiredActions = normalizeStringArray(goalContract.proveBy);
|
|
3158
|
-
const mustNotChange = normalizeStringArray(goalContract.mustNotChange);
|
|
3159
|
-
const handoff = {
|
|
3160
|
-
workerAgent: String(goalContract.workerAgent || '').trim() || null,
|
|
3161
|
-
reviewerAgent: String(goalContract.reviewerAgent || '').trim() || null,
|
|
3162
|
-
orchestratorAgent: String(goalContract.orchestratorAgent || '').trim() || null,
|
|
3163
|
-
};
|
|
3164
|
-
|
|
3165
|
-
const matched = Boolean(
|
|
3166
|
-
goal ||
|
|
3167
|
-
doneWhen.length > 0 ||
|
|
3168
|
-
requiredActions.length > 0 ||
|
|
3169
|
-
mustNotChange.length > 0 ||
|
|
3170
|
-
handoff.workerAgent ||
|
|
3171
|
-
handoff.reviewerAgent ||
|
|
3172
|
-
handoff.orchestratorAgent
|
|
3173
|
-
);
|
|
3174
|
-
|
|
3175
|
-
if (!matched) return null;
|
|
3176
|
-
|
|
3177
|
-
return {
|
|
3178
|
-
goal: goal || null,
|
|
3179
|
-
doneWhen,
|
|
3180
|
-
requiredActions,
|
|
3181
|
-
mustNotChange,
|
|
3182
|
-
handoff,
|
|
3183
|
-
};
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
function evaluateGoalContract(goalContract, actions = loadSessionActions()) {
|
|
3187
|
-
const normalized = normalizeGoalContract(goalContract);
|
|
3188
|
-
if (!normalized) {
|
|
3189
|
-
return {
|
|
3190
|
-
matched: false,
|
|
3191
|
-
passed: true,
|
|
3192
|
-
goal: null,
|
|
3193
|
-
doneWhen: [],
|
|
3194
|
-
requiredActions: [],
|
|
3195
|
-
missingActions: [],
|
|
3196
|
-
mustNotChange: [],
|
|
3197
|
-
handoff: {
|
|
3198
|
-
workerAgent: null,
|
|
3199
|
-
reviewerAgent: null,
|
|
3200
|
-
orchestratorAgent: null,
|
|
3201
|
-
},
|
|
3202
|
-
};
|
|
3203
|
-
}
|
|
3204
|
-
|
|
3205
|
-
const missingActions = normalized.requiredActions.filter((actionId) => !actions[actionId]);
|
|
3206
|
-
return {
|
|
3207
|
-
matched: true,
|
|
3208
|
-
passed: missingActions.length === 0,
|
|
3209
|
-
goal: normalized.goal,
|
|
3210
|
-
doneWhen: normalized.doneWhen,
|
|
3211
|
-
requiredActions: normalized.requiredActions,
|
|
3212
|
-
missingActions,
|
|
3213
|
-
mustNotChange: normalized.mustNotChange,
|
|
3214
|
-
handoff: normalized.handoff,
|
|
3215
|
-
};
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
function verifyClaimEvidence(claimText, options = {}) {
|
|
2226
|
+
function verifyClaimEvidence(claimText) {
|
|
3219
2227
|
const normalizedClaimText = String(claimText || '').trim();
|
|
3220
2228
|
if (!normalizedClaimText) {
|
|
3221
2229
|
throw new Error('claimText is required');
|
|
@@ -3243,23 +2251,9 @@ function verifyClaimEvidence(claimText, options = {}) {
|
|
|
3243
2251
|
});
|
|
3244
2252
|
}
|
|
3245
2253
|
|
|
3246
|
-
const goalContract = evaluateGoalContract(options.goalContract, actions);
|
|
3247
|
-
if (goalContract.matched) {
|
|
3248
|
-
checks.push({
|
|
3249
|
-
claim: 'goal_contract',
|
|
3250
|
-
passed: goalContract.passed,
|
|
3251
|
-
missing: goalContract.missingActions,
|
|
3252
|
-
message: goalContract.passed
|
|
3253
|
-
? 'Goal contract evidence present'
|
|
3254
|
-
: `Goal contract requires evidence: ${goalContract.missingActions.join(', ')}`,
|
|
3255
|
-
goalContract,
|
|
3256
|
-
});
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
2254
|
return {
|
|
3260
2255
|
verified: checks.every((check) => check.passed),
|
|
3261
2256
|
checks,
|
|
3262
|
-
goalContract,
|
|
3263
2257
|
};
|
|
3264
2258
|
}
|
|
3265
2259
|
|
|
@@ -3279,7 +2273,6 @@ module.exports = {
|
|
|
3279
2273
|
setTaskScope,
|
|
3280
2274
|
setBranchGovernance,
|
|
3281
2275
|
approveProtectedAction,
|
|
3282
|
-
breakGlassEmergency,
|
|
3283
2276
|
getScopeState,
|
|
3284
2277
|
getBranchGovernanceState,
|
|
3285
2278
|
isConditionSatisfied,
|
|
@@ -3296,7 +2289,6 @@ module.exports = {
|
|
|
3296
2289
|
evaluateGatesAsync,
|
|
3297
2290
|
computeExecutableHash,
|
|
3298
2291
|
formatOutput,
|
|
3299
|
-
isApprovalGatesEnabled,
|
|
3300
2292
|
run,
|
|
3301
2293
|
runAsync,
|
|
3302
2294
|
trackAction,
|
|
@@ -3305,8 +2297,6 @@ module.exports = {
|
|
|
3305
2297
|
clearSessionActions,
|
|
3306
2298
|
loadClaimGates,
|
|
3307
2299
|
registerClaimGate,
|
|
3308
|
-
normalizeGoalContract,
|
|
3309
|
-
evaluateGoalContract,
|
|
3310
2300
|
verifyClaimEvidence,
|
|
3311
2301
|
DEFAULT_CONFIG_PATH,
|
|
3312
2302
|
DEFAULT_CLAIM_GATES_PATH,
|
|
@@ -3323,7 +2313,6 @@ module.exports = {
|
|
|
3323
2313
|
buildBehavioralContext,
|
|
3324
2314
|
buildRecentCorrectiveActionsContext,
|
|
3325
2315
|
buildRelevantLessonContext,
|
|
3326
|
-
buildRelevantLessonContextAsync,
|
|
3327
2316
|
extractActionContext,
|
|
3328
2317
|
extractAvoidanceAdvice,
|
|
3329
2318
|
mergeContextStrings,
|
|
@@ -3338,16 +2327,7 @@ module.exports = {
|
|
|
3338
2327
|
getLocalOnlyScopeSources,
|
|
3339
2328
|
isRemoteSideEffectCommand,
|
|
3340
2329
|
evaluateLocalOnlyRemoteSideEffectGate,
|
|
3341
|
-
recordHelperScriptWrite,
|
|
3342
|
-
evaluateStatefulHelperBypassGate,
|
|
3343
|
-
isAgentHookSettingsFile,
|
|
3344
|
-
isBreakGlassSettingsRecoveryAction,
|
|
3345
2330
|
PR_THREAD_RESOLUTION_ACTION,
|
|
3346
|
-
HELPER_BYPASS_ACTION,
|
|
3347
|
-
buildBlockActionProCta,
|
|
3348
|
-
applyDailyBlockCap,
|
|
3349
|
-
getTodayBlockCount,
|
|
3350
|
-
incrementTodayBlockCount,
|
|
3351
2331
|
};
|
|
3352
2332
|
|
|
3353
2333
|
// ---------------------------------------------------------------------------
|