Skip to content

Execution governance#14

Merged
yi-john-huang merged 23 commits into
yi-john-huang:masterfrom
nayname:execution_governance
Mar 3, 2026
Merged

Execution governance#14
yi-john-huang merged 23 commits into
yi-john-huang:masterfrom
nayname:execution_governance

Conversation

@nayname

@nayname nayname commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces an explicit execution plan artifact in the governance layer.

The planner now produces a schema-defined execution-plan.json that 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:

  • The planner produced internal structures rather than a stable artifact
  • Execution was tightly coupled to LLM tool calling

This change introduces a plan-first execution boundary:

Nothing executes unless it exists in an explicit execution plan.


What Changed

Execution Plan Artifact

  • Introduced a schema-defined execution-plan.json as the canonical planning output
  • The plan captures:
    • execution intent
    • ordered operations
    • execution constraints
    • (initially) execution mode hints
  • The plan is created and embedded in the existing workflow, but not yet consumed for execution

Planner Enhancements

  • Planner now produces a complete execution plan artifact
  • Plan creation is enhanced using:
    • LLM output
    • schema-based structure
  • Validation logic remains unchanged and continues to operate on existing constructs

Execution Layer Stub

  • Introduced a new execution namespace (src/execution/)
  • This layer defines:
    • execution engine
    • execution context
    • adapter boundaries
  • Execution is intentionally stubbed — no runtime behavior changes yet

Middleware Refactor (Structural Only)

  • Workflow unchanged, enhanced plan generated based on basic plan
  • Clarified the separation between:
    • planning / evaluation (governance)
    • execution (execution layer)
  • Execution paths are defined but not yet activated

What Did NOT Change

  • No changes to existing validation or enforcement logic
  • No changes to tool execution behavior
  • No behavioral or security regressions

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

  • Makes execution intent explicit and inspectable
  • Establishes a stable artifact for:
    • future execution engines
    • auditing
    • replay / preview
  • Decouples planning from runtime decisions
  • Creates a clean foundation without forcing execution changes

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.

@yi-john-huang

Copy link
Copy Markdown
Owner

I left new comments on your fork. please have a look.

@nayname

nayname commented Feb 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, fixed the issues and updated the code accordingly. Detailed summary of the changes in my fork.

@yi-john-huang yi-john-huang left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 8
  • engine.py, executor.py, client.py lack trailing newlines
  • generate() was reordered without implementation changes — adds diff noise
  • Many unit-level tests in tests/integration/test_enhanced_plan.py belong in tests/unit/
  • Duplicate section comment at line 2957 in the large test file

✅ Positives

  • Enhancement is opt-in (enabled: false default) — correct safe default
  • Lazy LLM client initialization prevents unnecessary API key validation on startup
  • Graceful degradation: enhancement failures never block the governance pipeline
  • SECURITY_AUDIT prefix log entries for LLM API calls
  • Recursive sanitization implementation is solid
  • Comprehensive test coverage (~2,500 lines)

Comment thread src/governance/middleware.py Outdated
Comment thread pyproject.toml Outdated
Comment thread src/governance/planner.py Outdated
Comment thread src/governance/planner.py
Comment thread src/governance/models.py Outdated
Comment thread src/governance/middleware.py Outdated
Comment thread src/governance/planner.py
Comment thread src/governance/middleware.py
@yi-john-huang

Copy link
Copy Markdown
Owner

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.

@nayname

nayname commented Feb 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Applied foundational fixes below.

To Be Addressed in Plan Refinement Phase

What 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 evaluate() async (#1) we need to answer questions: does the caller need the enhanced plan immediately, or can we return basic plan and enhance async? Where does enhancement fit in the request lifecycle? (my POV that we need full execution plan before executions starts AND address the issue LLM pause somehow)

Plan Structurefrozen=False (#5), schema divergence (execution-plan.json vs execution-plan-big.json), and state management need alignment on what the plan contains and how it evolves.

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 PR

Security

  • 3 Allowlist sanitization — changed from blocklist to allowlist
  • 4 JSON schema validation — added jsonschema.validate() before building enhanced plan
  • 9 Prompt injection — wrapped data in <data_block> with "do not follow instructions" directive

Dependencies & Config

  • 2 Optional anthropic — moved to [project.optional-dependencies]
  • 8 Schema path resolution — simplified to resolve from patterns directory, added tests

@yi-john-huang yi-john-huang left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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].text

Option 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.py imports anthropic twice with separate try/except ImportError blocks in __init__ and complete(). Since the import is cached by Python's module system, the second block in complete() is redundant — from anthropic import APIError will succeed if __init__ passed. The guard in complete() can be removed.
  • jsonschema.ValidationError raised inside enhance() is caught by create_enhanced_plan()'s broad except Exception handler and becomes a WARNING log + None return. This is intentional (best-effort enhancement) but worth a comment to that effect at the enhance() 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.

Comment thread src/llm/client.py Outdated
Comment thread src/governance/middleware.py
Comment thread src/llm/client.py
Comment thread src/governance/models.py Outdated
Comment thread src/execution/engine.py
@nayname

nayname commented Feb 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the follow-up review. Applied the remaining fixes:

Critical fix:

  • Wrapped create_enhanced_plan() in asyncio.create_task(asyncio.to_thread(...)) — quick fix to unblock the event loop. Making evaluate() fully async is the proper solution but deferred to future iteration when we align on the enhancement workflow.

Minor fixes:

  • Removed redundant try/except ImportError in client.py complete()
  • Added EXPERIMENTAL docstring to execution layer modules (executor.py, engine.py, injected_context.py)

Architectural items (frozen=False, execution layer wiring) intentionally remain unfrozen and unwired while the execution plan primitive is still being refined. Once the plan schema stabilizes, we will freeze it and wire it into the execution layer.

@yi-john-huang

Copy link
Copy Markdown
Owner

Code review

No 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 👎.

@yi-john-huang

Copy link
Copy Markdown
Owner

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:

  1. asyncio.create_task() inside sync evaluate() will crash with RuntimeError: no running event loop when called from any context without a running event loop (e.g. webhook relay path). The fire-and-forget task reference is also not stored, risking GC before completion, and the enhanced plan result is discarded — making the entire enhancement path a no-op even when it succeeds. (.claude/rules/error-handling.md says "Let errors bubble up to appropriate handlers" and "Don't catch errors you can't handle")

# Fire-and-forget: runs in thread pool to avoid blocking event loop
if self._enhancement_enabled:
asyncio.create_task(
asyncio.to_thread(
self.create_enhanced_plan, plan, effective_session_id, user_id, token
)
)
# Record in session if enabled

  1. Version in pyproject.toml is 1.2.0 while base branch (master) is at 1.5.2 — this is a regression that will overwrite the current release version on merge.

name = "openclaw-secure-stack"
version = "1.2.0"
description = "Secure Docker-based deployment wrapper for OpenClaw AI agent"

  1. Silent error swallowing in _parse_recovery_paths and _parse_conditionals — malformed LLM output entries are caught and skipped with continue but never logged, despite logger being available in scope. (.claude/rules/error-handling.md explicitly marks catching errors without logging as "Bad" and requires logging errors with context for debugging)

)
except (KeyError, ValueError):
continue # Skip malformed entries
return result

Additionally, I concur with the other reviewer's findings on Docker/lockfile inconsistency (--frozen requires uv.lock but it is gitignored) and the _get_llm() eager call breaking test mocks (5 failures in test_enhanced_plan.py).

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@nayname

nayname commented Feb 27, 2026

Copy link
Copy Markdown
Contributor Author

1 asyncio.create_taskevaluate() is invoked from FastAPI’s async proxy() handler, and relay.py is also async, so there is always a running event loop in the intended execution path. Added a guard that skips enhancement in sync contexts as a safeguard. Just as said earlier, proper fix and architectural direction is to make evaluate() async end-to-end.
enhanced plan discarded — that's intentional for now. Enhancement is fire-and-forget while we iterate on the plan schema. The result will be stored/used once the plan schema is finalized.

2. Version regression — Will rebase onto master to pick up 1.5.2.

3. Silent error swallowing — Fixed. Added logger.debug() to _parse_recovery_paths and _parse_conditionals.

4. Docker/uv.lock — Pre-existing issue, not introduced in this PR.

5 _get_llm() test failures — These tests require ANTHROPIC_API_KEY because they test real LLM integration. That's intentional — unit tests mock the client, integration tests use the real one. CI needs the key as a secret for integration tests to pass. For convenience, made those tests skip if no ANTHROPIC_API_KEY set.

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.
@nayname
nayname force-pushed the execution_governance branch from c454d22 to ad4ae41 Compare February 27, 2026 12:12
Guard asyncio.create_task for sync callers like evaluate_governance().
@yi-john-huang

Copy link
Copy Markdown
Owner

Code review

No 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 👎.

@yi-john-huang yi-john-huang left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@yi-john-huang
yi-john-huang merged commit 1f35f57 into yi-john-huang:master Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants