Execution governance#14
Conversation
750f805 to
72a10d8
Compare
|
I left new comments on your fork. please have a look. |
|
Thanks, fixed the issues and updated the code accordingly. Detailed summary of the changes in my fork. |
yi-john-huang
left a comment
There was a problem hiding this comment.
Review: Execution Governance Enhancement
This PR introduces solid foundational concepts for LLM-augmented governance, and the opt-in default, lazy initialization, and graceful degradation are all the right instincts. However there are critical issues that must be addressed before merge.
Recommendation: Request Changes
🔴 Critical Issues
1. Blocking synchronous LLM call in async FastAPI request path (src/governance/middleware.py:221)
GovernanceMiddleware.evaluate() is synchronous. When enhancement is enabled, it calls create_enhanced_plan() → planner.enhance() → llm.complete(), which is a synchronous HTTP call to the Anthropic API. In FastAPI, any synchronous blocking I/O inside a request handler stalls the event loop for every other in-flight request. LLM calls routinely take 1–5 seconds — this is a serious throughput regression when enhancement.enabled=true.
Fix options:
- Wrap the LLM call in
asyncio.to_thread()/loop.run_in_executor() - Make the governance evaluation path fully async
- Run enhancement out-of-band (fire-and-forget, post-response)
2. anthropic is a hard required dependency (pyproject.toml:11)
Every deployment now installs the Anthropic SDK even when enhancement is disabled. Move it to an optional extras group:
[project.optional-dependencies]
llm = ["anthropic>=0.30.0"]3. Blocklist sanitization insufficient for a security layer (src/governance/planner.py:153)
_sanitize_for_llm redacts known field names (token, password, etc.), but any unknown sensitive field — api_token, bearer, private_key, x-api-key — passes through to the Anthropic API. For a governance/security system an allowlist of permitted fields is the correct posture, not a blocklist.
4. No JSON Schema validation of LLM output (src/governance/planner.py:274)
The schema is sent to the LLM as prompt instructions, but _build_enhanced_plan never validates the returned JSON against the schema before constructing EnhancedExecutionPlan. Malformed or adversarially crafted LLM output could produce unexpected model state.
Fix: use jsonschema.validate(enhanced_dict, schema) before calling _build_enhanced_plan.
🟠 Architectural Concerns
5. frozen=False breaks the immutability contract (src/governance/models.py:402)
Every other model in models.py uses frozen=True. EnhancedExecutionPlan introduces frozen=False because mutable ExecutionState is embedded in the plan. The better pattern: keep the plan frozen, manage ExecutionState as a separate mutable object held externally by an execution context or engine.
6. ~510 lines of dead execution layer (src/execution/)
engine.py (373 lines), executor.py (70 lines), injected_context.py (67 lines) are never called from the main application. Dead code increases review surface and maintenance burden. Defer to a follow-up PR once the execution path is actually wired in.
7. execution-plan-big.json is unused (config/execution-plan-big.json)
This 326-line file is never referenced in code and contains a personal reference ("from Yi's enforcement layer"). Remove it or move it to a docs/reference directory.
8. Fragile schema path resolution (src/governance/middleware.py:103)
Schema path is resolved by navigating Path(patterns_path).parent.parent, hard-wiring the assumption that patterns_path lives at config/intent-patterns.json. If that assumption breaks, schema resolution silently returns None and enhance() raises a RuntimeError. Pass schema_path as an explicit config key with an absolute path, or resolve relative to the project root via an env var.
🟡 Security Concerns
9. Prompt injection surface (src/governance/planner.py:40)
ENHANCE_PLAN_PROMPT embeds raw tool call arguments (which come from an upstream LLM) directly into the prompt with no isolation. A crafted tool argument like "Ignore all previous instructions and..." constitutes a prompt injection attack. Wrap the injected plan JSON in an explicit data-only block with clear delimiter instructions.
10. Silent exception swallowing in governance layer (src/governance/middleware.py:279)
except Exception as e: ... return None is fine for graceful degradation, but in a security governance layer it's important that enhancement failures are observable as metrics, not just as log lines. Consider incrementing a counter (e.g., Prometheus) so that a bug that silently disables enhancement can be detected via alerting rather than log scraping.
Minor Issues
planner.py:logger = getLogger(__name__)is between import groups — violates PEP 8engine.py,executor.py,client.pylack trailing newlinesgenerate()was reordered without implementation changes — adds diff noise- Many unit-level tests in
tests/integration/test_enhanced_plan.pybelong intests/unit/ - Duplicate section comment at line 2957 in the large test file
✅ Positives
- Enhancement is opt-in (
enabled: falsedefault) — correct safe default - Lazy LLM client initialization prevents unnecessary API key validation on startup
- Graceful degradation: enhancement failures never block the governance pipeline
SECURITY_AUDITprefix log entries for LLM API calls- Recursive sanitization implementation is solid
- Comprehensive test coverage (~2,500 lines)
|
I’ll focus exclusively on this PR for now. We need to ensure new features are merged as quickly as possible to keep our cycle time short. |
|
Thanks for the thorough review. Applied foundational fixes below. To Be Addressed in Plan Refinement PhaseWhat we have now is a starting point — security, plan structure, logging and workflow are at the first stage. The remaining issues are interconnected and need the plan paradigm to move further: Security — Current sanitization and LLM guardrailing is a first pass. Proper posture requires comprehensive allowlist review, input/output validation strategy, and audit trail beyond logging. Workflow — before making Plan Structure — Execution — Current stubs (#6) are placeholders. Proper execution layer depends on finalized plan structure. Focus now is on getting the plan content right. Once that stabilizes, we'll address async/execution/immutability concerns properly. So these issues should probably be addressed in the next iterations, simultaneously with the plan refinement. Fixed in This PRSecurity
Dependencies & Config
|
yi-john-huang
left a comment
There was a problem hiding this comment.
Follow-up Review: Execution Governance
Significant progress since the first review — thank you for the thorough set of fixes. Six of the ten original issues are now fully resolved. One critical issue remains and must be fixed before merge.
✅ Fixed Since Last Review
| Issue | Fix |
|---|---|
anthropic hard dependency |
✅ Moved to [project.optional-dependencies] llm |
| Blocklist sanitization | ✅ Replaced with two-level allowlist (ALLOWED_KEYS + SAFE_ARGUMENT_KEYS) |
| No JSON Schema validation of LLM output | ✅ jsonschema.validate() added before _build_enhanced_plan |
Fragile parent.parent schema path |
✅ Fixed: now uses Path(patterns_path).parent (co-located with patterns file) |
| Prompt injection surface | ✅ <data_block> wrapper with explicit "do not follow" instruction |
| Silent exception swallowing | ✅ Logs now include plan_id, error_type, full stack trace via exc_info=True |
| Lazy LLM client init | ✅ _get_llm() pattern; import deferred inside LLMClient.__init__() |
LLMClient hardening |
✅ Timeout, env-configurable model, clear error messages for missing dependency/key |
🔴 Critical: Blocking synchronous I/O still present
LLMClient.complete() uses the synchronous Anthropic SDK (Anthropic().messages.create()). create_enhanced_plan() is called directly from evaluate(), which runs in the FastAPI request/response cycle. FastAPI workers are async — any synchronous blocking call in the request path stalls the event loop and prevents all other requests from being served for the duration of that call.
With DEFAULT_TIMEOUT = 60 seconds, enabling enhancement under load could stall the proxy for up to a minute per request, rendering it effectively unavailable.
This issue was explicitly called out in the first review and is still not addressed.
The fix is straightforward — choose one of:
# Option A: run in thread pool (minimal change)
import asyncio
async def create_enhanced_plan(self, ...) -> EnhancedExecutionPlan | None:
return await asyncio.to_thread(self._create_enhanced_plan_sync, ...)
def _create_enhanced_plan_sync(self, ...) -> EnhancedExecutionPlan | None:
... # existing implementation# Option B: use the async Anthropic client
from anthropic import AsyncAnthropic
async def complete(self, prompt: str, temperature: float = 0) -> str:
response = await self.client.messages.create(...)
return response.content[0].textOption A is the lowest-risk change and keeps the existing implementation intact.
🟠 Remaining Architectural Concerns
EnhancedExecutionPlan.frozen=False still breaks the immutability contract
All 16 other models in models.py use frozen=True. EnhancedExecutionPlan remains the sole exception. This is a design smell that should at minimum be addressed with a clear module-level comment explaining why the contract is broken here and what invariants callers must maintain. Better: extract ExecutionState into a separate mutable container held by the engine, and freeze the plan itself.
Dead execution layer still present
src/execution/engine.py, executor.py, and injected_context.py remain in the codebase and are never called from the main application. The engine improvements (off-by-one retry fix, completed_at ordering, _should_skip stub) are good work, but they're going into code that's never exercised in production. This is maintenance debt. A # EXPERIMENTAL - not wired into main app module docstring on each file would at least make this explicit.
execution-plan-big.json — noted as intentional
Acknowledged that you're keeping this as a reference schema. If it's meant to guide future development, move it to docs/ or add a # REFERENCE ONLY - not used in production comment at the top of the file so reviewers don't have to wonder.
Minor
client.pyimportsanthropictwice with separatetry/except ImportErrorblocks in__init__andcomplete(). Since the import is cached by Python's module system, the second block incomplete()is redundant —from anthropic import APIErrorwill succeed if__init__passed. The guard incomplete()can be removed.jsonschema.ValidationErrorraised insideenhance()is caught bycreate_enhanced_plan()'s broadexcept Exceptionhandler and becomes aWARNINGlog +Nonereturn. This is intentional (best-effort enhancement) but worth a comment to that effect at theenhance()call site.
Verdict
The security improvements (allowlist sanitization, schema validation, prompt injection mitigation) are solid and well-implemented. But the event loop blocking issue is a functional correctness problem that affects any deployment that enables enhancement — it's not a future concern, it's a current bug. Fix the async issue and this PR is close to merge-ready.
|
Thanks for the follow-up review. Applied the remaining fixes: Critical fix:
Minor fixes:
Architectural items ( |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Code review (updated)Retracting my earlier "no issues found" — my automated pipeline was too lenient on confidence scoring. After cross-referencing with the other reviewer's findings, I agree with the following issues: Found 3 issues:
openclaw-secure-stack/src/governance/middleware.py Lines 226 to 234 in ff4b8b7
openclaw-secure-stack/pyproject.toml Lines 2 to 4 in ff4b8b7
openclaw-secure-stack/src/governance/planner.py Lines 401 to 404 in ff4b8b7 Additionally, I concur with the other reviewer's findings on Docker/lockfile inconsistency ( 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
|
1 asyncio.create_task — 2. Version regression — Will rebase onto master to pick up 1.5.2. 3. Silent error swallowing — Fixed. Added 4. Docker/uv.lock — Pre-existing issue, not introduced in this PR. 5 _get_llm() test failures — These tests require |
General notes / clarifications Conditional logic (_should_skip) The previous implementation was broken and half-implemented. Rather than ship incorrect branching semantics, I’ve stubbed it to return False for now. Proper conditional execution will be implemented once we finalize the conditional expression semantics and the richer plan schema. execution-plan-big.json This is intentionally kept as a landmark for the full schema vision. The smaller execution-plan.json is the one used in practice today. Enhanced plan persistence Enhanced plans are now created safely (non-blocking) but are not yet persisted. The intention is to eventually store a refined enhanced plan and issue a meaningful token once the schema and user-context embedding (paths, configs, auth, etc.) are finalized. Next steps Refining the execution plan schema and enriching it with real user/runtime context — this is where proper conditional logic will come back into play. Summary of fixes Critical (1–4) — All fixed 1 RecoveryPath / ConditionalBranch defined after use Classes moved before EnhancedExecutionPlan 2 create_enhanced_plan() discarded + no error handling Wrapped in try/except, logged failures, returns EnhancedExecutionPlan (non-blocking) 3 Schema path CWD-relative Resolved to absolute path derived from patterns_path 4 Missing src/llm/__init__.py Added High (5–9) — All fixed 5 Retry off-by-one max_attempts = 1 + max_retries 6 _should_skip() broken Stubbed to return False until semantics are defined 7 Sensitive args sent to LLM Added _sanitize_for_llm() + security/audit logging 8 Unused imports in executor.py Removed 9 LLMClient hardening Timeout, env-configurable model, error handling, dependency declared Medium — Fixed Issue Fix Duration log always “incomplete” completed_at set before _calc_total_duration() session_id=None crashes ExecutionContext initialize_state() validates and errors early isort violation Import order fixed Two schema files execution-plan-big.json kept intentionally as reference Test asserting AttributeError Updated to assert ExecutionError Tests Updated to reflect all changes
Moves the Anthropic import behind LLMClient initialization so optional LLM enhancement no longer affects import-time behavior or core governance paths. This change ensures: - Governance evaluation and execution remain functional when LLM enhancement is disabled or misconfigured - Missing dependencies or API keys fail locally and explicitly, not at module import time - Clear error messages guide correct installation and configuration Adds targeted tests covering: - Missing anthropic dependency - Missing ANTHROPIC_API_KEY - Successful lazy initialization when enhancement is enabled This reduces blast radius for optional integrations and tightens the boundary between core governance logic and external LLM providers.
Tighten failure handling around LLM-based plan enhancement. Enhancement remains best-effort and non-authoritative: failures are caught locally and never affect core governance evaluation or execution. Improvements include: - Richer warning logs with plan_id, error type, and full traceback to aid debugging and post-mortem analysis - Removal of a redundant ExecutionError definition to avoid duplicate error types - Integration test coverage asserting that enhancement failures are logged with sufficient diagnostic detail This reduces silent failure modes while preserving strict isolation between optional LLM enhancement and core execution paths.
…lidation Tighten security and determinism around LLM-based plan enhancement. This change introduces an allowlist-based sanitization strategy for all data sent to external LLMs. Only explicitly permitted structural fields and safe argument keys are allowed through; everything else is redacted. This replaces the previous sensitive-key denylist with a stricter, governance-appropriate posture. Additional improvements: - Validate LLM output against the execution-plan JSON schema before constructing an EnhancedExecutionPlan - Make schema path resolution deterministic and independent of CWD - Move LLM dependencies behind an optional dependency group - Expand tests to cover allowlist sanitization, schema validation, and schema path resolution behavior The goal is to reduce blast radius, prevent credential or PII leakage, and ensure LLM enhancement remains auditable, deterministic, and non-authoritative.
Moved jsonschema to core dependencies (it's small, reasonable)
Remove try/except ImportError in complete() - anthropic is guaranteed available since __init__ succeeded. Delete corresponding test that simulated module disappearing mid-process (impossible scenario).
- Rename agentContext to agent_context (snake_case consistency) - Add EXPERIMENTAL docstring to engine.py, injected_context.py - Clarify ValidationError handling comment in middleware.py - Update tests to use snake_case key
Move execution-plan-big.json to docs/reference/ and rename to execution-plan-reference.json. This schema is not used in production - it documents the full vision for plan structure to guide future work.
Add an explicit note explaining why EnhancedExecutionPlan remains mutable in this iteration. The plan primitive is still being refined, and execution state is embedded temporarily to avoid prematurely locking lifecycle and ownership boundaries. Once the plan schema stabilizes, execution state will be extracted into a separate mutable container owned by the execution engine and the plan model will be frozen.
Add debug logging when skipping malformed recovery_paths and conditionals. Update tests to verify logging occurs.
Add @requires_llm marker to tests that need real Anthropic client. Tests auto-skip with clear reason when key is unavailable.
c454d22 to
ad4ae41
Compare
Guard asyncio.create_task for sync callers like evaluate_governance().
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Summary
Introduces an explicit execution plan artifact in the governance layer.
The planner now produces a schema-defined
execution-plan.jsonthat represents the complete execution intent (operations, constraints, and metadata). Execution logic is explicitly separated from governance and moved into a new execution layer (stub for now).This change makes planning explicit and inspectable while keeping validation, enforcement, and runtime behavior unchanged.
Motivation
Previously, execution semantics were implicit:
This change introduces a plan-first execution boundary:
Nothing executes unless it exists in an explicit execution plan.
What Changed
Execution Plan Artifact
execution-plan.jsonas the canonical planning outputPlanner Enhancements
Execution Layer Stub
src/execution/)Middleware Refactor (Structural Only)
What Did NOT Change
This is a structural and additive change only.
Architecture (Current State)
evaluate()
→ Planner (unchanged)
→ Enhanced Plan (new)
→ Validator (unchanged)
→ Enforcement (unchanged)
→ (execution stub)
Execution consumption will be introduced incrementally in follow-up work.
Why This Matters
Breaking Changes
None.
This PR is additive and preserves all existing behavior.
Process note
This PR contains the same code as the draft discussed in
nayname#1.
Discussion and review started there; this PR mirrors that work upstream.