From 2ee6251da4dd5da1b9bdc2b65b5be4d5188adf9f Mon Sep 17 00:00:00 2001 From: htafolla Date: Sun, 29 Mar 2026 21:41:01 -0500 Subject: [PATCH] fix: wire enforcement validators into codex-check bridge command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit codex-check only ran 3 quality-gate meta-checks and never actually analyzed code content. eval(), innerHTML, SQL injection, console.log all passed silently. Root cause: two disconnected systems — quality-gate.ts (called by bridge) vs enforcement/validators (real analyzers, never wired in). Fix: use ValidatorRegistry directly in handleCodexCheck(), bypassing RuleEnforcer.validateOperation() whose dependency chain cascades failures when validators lack full project context. SNIPPET_SAFE_RULES whitelist (10 validators that work on raw code): security-by-design, input-validation, clean-debug-logs, console-log-usage, no-duplicate-code, loop-safety, no-over-engineering, single-responsibility, error-resolution, module-system-consistency Also fixes: - Bridge positional mode now parses inline JSON payloads for hooks (node bridge.mjs hooks '{"action":"install"}' works) - 3 stale test failures (missing session_stats keys + version string) Verified: eval()→blocked, innerHTML→blocked, console.log→blocked, CJS/ESM mix→blocked, clean code→passes. 129/129 tests pass. --- src/integrations/hermes-agent/bridge.mjs | 116 ++++++++++++++++++- src/integrations/hermes-agent/test_plugin.py | 5 +- 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/integrations/hermes-agent/bridge.mjs b/src/integrations/hermes-agent/bridge.mjs index cd3f052a0..891595869 100644 --- a/src/integrations/hermes-agent/bridge.mjs +++ b/src/integrations/hermes-agent/bridge.mjs @@ -45,6 +45,7 @@ let ProcessorManager = null; let StrRayStateManager = null; let featuresConfigLoader = null; let runQualityGateWithLogging = null; +let enforcerValidators = null; let frameworkReady = false; let frameworkLoadAttempted = false; @@ -162,6 +163,25 @@ async function loadFramework(projectRoot) { } } + // RuleEnforcer — full code analysis (security, quality, architecture) + if (!enforcerValidators) { + const rePath = join(distDir, "enforcement", "index.js"); + if (existsSync(rePath)) { + try { + const reModule = await import(rePath); + // Use ValidatorRegistry directly to bypass RuleExecutor dependency chain + // which cascades failures when validators lack full project context + const ValidatorRegistry = reModule.ValidatorRegistry; + if (ValidatorRegistry) { + enforcerValidators = new ValidatorRegistry(); + // Validators are pre-registered in constructor — no registerAllValidators needed + } + } catch (e) { + logToActivity(join(projectRoot, "logs", "framework"), `ValidatorRegistry load skipped: ${e.message}`); + } + } + } + frameworkReady = !!(runQualityGateWithLogging || ProcessorManager); if (frameworkReady) return true; } catch (e) { @@ -433,21 +453,102 @@ async function handleValidate(input, projectRoot, logDir) { } async function handleCodexCheck(input, projectRoot, logDir) { - const { code, focusAreas } = input; - logToActivity(logDir, `codex-check: code_length=${code?.length || 0} focus=${focusAreas}`); + const { code, focusAreas, operation } = input; + const codeLen = code?.length || 0; + logToActivity(logDir, `codex-check: code_length=${codeLen} focus=${focusAreas} operation=${operation}`); - // Check code against debug patterns via quality gate + // Collect violations from both systems + const allViolations = []; + const allChecks = []; + let enforcerRan = false; + + // Phase 1: Quality gate (fast meta-checks + basic patterns) const qualityResult = await runQualityGateCheck( { tool: "write", args: { content: code } }, projectRoot, logDir, ); + if (qualityResult.checks) { + allChecks.push(...qualityResult.checks); + } + if (qualityResult.violations?.length) { + allViolations.push(...qualityResult.violations); + } + + // Phase 2: Enforcement validators (deep code analysis — security, quality, architecture) + // Use ValidatorRegistry directly instead of RuleEnforcer.validateOperation() + // to avoid the dependency chain cascade that blocks validators on snippet analysis. + // Only run content-analysis validators — skip project-level validators that + // require full context (docs, tests, CI, package.json) and always fail on snippets. + const SNIPPET_SAFE_RULES = new Set([ + "security-by-design", + "input-validation", + "clean-debug-logs", + "console-log-usage", + "no-duplicate-code", + "loop-safety", + "no-over-engineering", + "single-responsibility", + "error-resolution", + "module-system-consistency", + ]); + + if (enforcerValidators && codeLen > 0) { + try { + const ctx = { operation: "write", newCode: code, files: [] }; + const validators = enforcerValidators.getAllValidators(); + let enforcerViolations = 0; + + for (const v of validators) { + // Only run snippet-safe validators, or respect focus areas + if (!SNIPPET_SAFE_RULES.has(v.ruleId)) { + // Still run if focus area explicitly requests this category + if (focusAreas && focusAreas !== "all" && Array.isArray(focusAreas)) { + if (focusAreas.includes(v.category)) { + // Fall through to validate + } else { + continue; + } + } else { + continue; + } + } + + try { + const result = await v.validate(ctx); + if (!result.passed) { + enforcerViolations++; + allViolations.push({ + id: v.ruleId, + severity: v.severity || "error", + message: result.message, + suggestions: result.suggestions, + }); + allChecks.push({ id: v.ruleId, passed: false, message: result.message }); + } + } catch { + // Skip broken validators gracefully + } + } + + enforcerRan = true; + logToActivity(logDir, `codex-check: Validators ran against ${validators.length} rules (${SNIPPET_SAFE_RULES.size} snippet-safe), ${enforcerViolations} violations`); + } catch (e) { + logToActivity(logDir, `codex-check: Validator error: ${e.message}`); + } + } else if (!enforcerValidators) { + logToActivity(logDir, `codex-check: Validators not available, quality gate only`); + } + + const passed = allViolations.length === 0; + return { - passed: qualityResult.passed, - violations: qualityResult.violations, - checks: qualityResult.checks, + passed, + violations: allViolations, + checks: allChecks, focusAreas: focusAreas || "all", + enforcerRan, }; } @@ -615,6 +716,9 @@ async function main() { i++; } else if (!argv[i].startsWith("-") && !positionalCommand && KNOWN_COMMANDS.has(argv[i])) { positionalCommand = argv[i]; + } else if (!argv[i].startsWith("-") && positionalCommand && !positionalPayload) { + // Inline JSON payload after command: node bridge.mjs hooks '{"action":"install"}' + positionalPayload = argv[i]; } } diff --git a/src/integrations/hermes-agent/test_plugin.py b/src/integrations/hermes-agent/test_plugin.py index b3529bebb..c87377696 100644 --- a/src/integrations/hermes-agent/test_plugin.py +++ b/src/integrations/hermes-agent/test_plugin.py @@ -423,6 +423,9 @@ def setUp(self): "post_processor_runs": 6, "bridge_calls": 15, "bridge_errors": 0, + "subagent_dispatches": 3, + "subagent_validations": 1, + "subagent_blocks": 0, } def test_stats(self): @@ -1182,7 +1185,7 @@ def test_v2_1_log_message(self): ctx = MagicMock() with self.assertLogs("strray-hermes", level="INFO") as cm: pi.register(ctx) - self.assertTrue(any("v2.1" in m for m in cm.output)) + self.assertTrue(any("v2.2" in m for m in cm.output)) self.assertTrue(any("4 tools" in m for m in cm.output)) self.assertTrue(any("5 hooks" in m for m in cm.output))