feat(hooks)!: engine-agnostic plugin contract + semantic-PII/secrets redaction (#275) - #295
Conversation
[readi] pins readi-privacy (IBM READI, import name risk_assessment) plus spacy-curated-transformers, which supplies the factories READI's default en_core_web_trf pipeline needs. Minor-bounded like [hooks]/[pii] because the plugin core is written against READI's extractor and DetectionType APIs. [bench] carries datasets for examples/pii_benchmark.py --dataset only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regex redaction (cpex-pii-filter) cannot catch names: 0.13 overall span
recall on ai4privacy and 0.00 on first/last name. This adds NER-based
redaction via IBM READI on the same two hooks the regex plugin covers
(memory_pre_write + llm_pre_call), reaching 0.48 recall at precision 1.00
with names at 0.92-0.94.
Follows the seam's core/shim house style:
* Core - redact_spans / redact_text / redact_entities / redact_messages
and a SpanDetector protocol. Pure, no cpex and no READI import;
detection is injected, so the splice logic is unit-testable with a
two-line fake and no extras installed.
* Shim - ReadiSemanticPIIPlugin under the HAS_CPEX guard: parses
self._config.config, calls the core, returns a modified_payload.
Mode/on_error match the shipped regex plugin and are load-bearing:
sequential (CPEX downgrades continue_processing=False in transform/audit,
so a transform-mode redactor can never block) and on_error=fail
(fail-closed, so a crashing NER model never passes PII through).
Offsets are character offsets end to end - READI reports them natively,
unlike cpex-pii-filter's Rust byte offsets. The analyzer is built lazily
behind a construction lock so a thread-switching dispatch does not load
two copies of a multi-hundred-MB pipeline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scores any SpanDetector against a labeled gold set and reports recall, precision, F1, per-entity recall and a record-level leak rate. Synthetic templates by default; --dataset streams ai4privacy- or WikiANN-style HF corpora ([bench] extra), with --limit to keep runs practical (the full corpus is 43k rows and takes hours under transformer NER). Includes byte_spans_to_char_spans: cpex-pii-filter's Rust engine reports UTF-8 byte offsets, and scoring them as character offsets mis-places every span in a non-Latin script - it read as Japanese precision 0.31 instead of 0.99. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Core tests need no extras (detection is injected) and include the character-offset regression on multibyte text plus the byte->char conversion used by the benchmark. Shim tests stub detection so CI never downloads a transformer, and pin the sequential/fail-closed defaults. The shipped-plugin no-in-place-mutation guard is extended to the new plugin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New docs/guides/pii-redaction.md: the measured numbers, model-choice guidance (language-matched spaCy pipelines from reputable publishers over individual community uploads), cost/latency honesty about NER on every write, and limitations - READI's Presidio wrapper hardcoding language=en, the Apple Silicon MPS thread-affinity failure, and that recall is not a guarantee. hooks_plugins.yaml ships the semantic plugin commented out and ready to enable, so switching regex -> semantic is a YAML edit, not a code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR adds hook configuration discovery and CLI scaffolding, changes activation to depend on resolved plugins, introduces native hook contracts and READI semantic PII redaction, separates optional extras, and adds documentation, examples, benchmarks, and tests. ChangesHooks configuration and lifecycle
Native plugin and semantic PII support
Benchmarking and documentation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant CLI
participant HooksConfig
participant HookManager
participant ReadiSemanticPIIPlugin
participant Payload
CLI->>HooksConfig: scaffold or discover hooks.yaml
HookManager->>HooksConfig: resolve configured plugins
HookManager->>ReadiSemanticPIIPlugin: validate and initialize
Payload->>ReadiSemanticPIIPlugin: memory_pre_write or llm_pre_call
ReadiSemanticPIIPlugin-->>Payload: redacted replacement payload
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The inverted/zero-width filter ran BEFORE clamping, so a fully-out-of-range
span (e.g. (10, 20) on a 6-char string) clamped to zero width and then spliced
as a spurious INSERTION of the mask (`redact_spans("abcdef", [(10, 20)])`
returned "abcdef[REDACTED]"). Clamp first, then drop zero-width spans, so an
out-of-range span is a genuine no-op and the None-unchanged contract holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
redact_messages walked only message["content"], so PII in tool_calls[].function.arguments (and any other text-bearing field) was re-sent to the LLM every turn — a real egress leak under a semantic-only config, and a parity regression versus the regex plugin which redacts the whole payload. Walk the whole message with _redact_value, skipping a set of structural keys (role, ids, type, function name) so identity/routing fields are preserved while every free-text leaf — including the tool-call arguments JSON string — is masked. Redacting the raw arguments string is safe because redact_text only rewrites detector-flagged substrings (the PII values), leaving JSON intact. The None-means-unchanged and no-in-place-mutation contracts are preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gn metadata default Owner decision: both plugins are PII redaction; the extra name should state the METHOD, not "PII vs not-PII". READI is the more powerful, NER-based method. - Rename [pii] -> [pii-regex] (cpex-pii-filter; lighter, deterministic). - Rename [readi] -> [pii-semantic] (IBM READI NER; catches names/entities). - Keep [pii] as a backward-compatible alias for [pii-regex] so existing installs keep working with the same lightweight behaviour; it deliberately does NOT pull the heavy semantic deps. - Update every reference: both plugins' ImportError/stub install hints, plugin __init__ docstring, examples/hooks_plugins.yaml, hooks_demo.py, pii_benchmark help/hints, docs (pii-redaction.md, memory-hooks.md), test skip/match hints, and the pyproject self-references. Regenerate uv.lock. - Reframe docs: regex and semantic are two detection METHODS, and running BOTH is the recommended defence-in-depth default (regex for structured identifiers, semantic for names) rather than an either/or switch. FIX (metadata default parity): READI's redact_metadata defaulted to False while the regex plugin round-trips the whole entity through cpex-pii-filter and so redacts metadata unconditionally. Align READI's default to True (also the fail-safe default for a redactor), keep it a configurable opt-out, and document the tradeoff (metadata can hold ids/paths redaction would corrupt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The record-level leak check counts a leak only when the full PII literal survives verbatim; a partial redaction (part of a multi-token value masked, the rest left in place) no longer contains the whole literal and is not counted. Add a comment clarifying the rate is a floor, not the true leak rate — the span-level FN count is the tighter signal. No logic change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the HooksConfig.enabled master switch: the hook seam is always
live. Behavior is decided purely by which plugins resolve.
- No plugins resolved -> zero-cost no-op; cpex is not required and never
imported (preserves the deferred-import guarantee).
- Plugins configured but the engine missing -> initialize_hooks raises
ImportError ('altk-evolve[hooks]') instead of silently no-opping a
configured compliance plugin (the fail-open bug this guards against).
- A configured plugin whose detector lib is missing surfaces its
extra-naming ImportError at startup: initialize_hooks now calls an
optional startup_validate() on each plugin, which READI implements by
building its detector (validates [pii-semantic] without loading weights).
A failed init leaves the guards OFF.
Back-compat: passing enabled= is accepted and ignored via a pydantic
before-validator that pops it with a DeprecationWarning; behavior is
unchanged. Tests updated: the enable_hooks helper builds the engine via
the internal _initialize_manager so bare test-double plugins still work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffold a project-local, auto-discovered hooks config. - `evolve hooks init` writes ./evolve.hooks.yaml (--path, --force; refuses to clobber without --force) from a bundled template that ships the READI semantic PII plugin ACTIVE and the regex plugin COMMENTED OUT, both mode: sequential + on_error: fail, with switch guidance. - Post-init guidance prints the [pii-semantic] install hint, the auto-discovery note, and a platform-specific message (macOS/MPS fail-closed warning vs "works out of the box"), factored into a pure hooks_init_platform_note() for testing. - Auto-discovery (discover_hooks_config_path in config/hooks.py, injectable for tests): $EVOLVE_HOOKS_CONFIG -> ./evolve.hooks.yaml -> ~/.config/evolve/hooks.yaml; explicit plugins_yaml / code-first plugins override it; nothing found = no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- memory-hooks.md: replace the "enabled defaults False / opt-in" framing with "always live; behavior = which plugins you configure; no plugins = zero-cost no-op; configured-but-engine-missing = fail-closed error". Add the discovery search order and an `evolve hooks init` turnkey path; note enabled= is deprecated and ignored. - pii-redaction.md: rewrite the enable story to the init flow (run `evolve hooks init` -> READI active by default -> install [pii-semantic] -> optionally uncomment regex for defence-in-depth); keep the macOS/MPS caveat and model-choice guidance. - examples: drop enabled= from hooks_plugins.yaml / hooks_demo.py and point at `evolve hooks init` + auto-discovery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
altk_evolve/hooks/manager.py (1)
242-256: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winEager validation reaches into a private cpex attribute (
pm._registry).
_validate_plugin_dependencieswalkspm._registry.get_all_plugins()— an underscore-prefixed, presumably-private attribute of thecpexPluginManager. If a futurecpexrelease renames/reshapes this internal registry, hook initialization (including the fail-closed PII-dependency check this function exists for) breaks entirely rather than degrading gracefully. Worth checking whethercpex.framework.manager.PluginManagerexposes a supported public accessor for enumerating registered plugins, and using that instead if available.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@altk_evolve/hooks/manager.py` around lines 242 - 256, Update _validate_plugin_dependencies to enumerate plugins through the supported public accessor on cpex.framework.manager.PluginManager instead of reaching into pm._registry. Preserve the existing startup_validate() invocation for each registered plugin, and use a graceful fallback if no public accessor is available so hook initialization does not depend on private registry internals.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@altk_evolve/hooks/manager.py`:
- Around line 158-192: Update the yaml_path resolution in the hook
initialization flow so discovery runs only when both config.plugins_yaml and
config.plugins are unset or empty; any code-first plugins must suppress
discover_hooks_config_path(). Preserve explicit plugins_yaml precedence,
no-plugin shutdown behavior, and add a regression test covering code-first
plugins with a discoverable configuration present.
---
Nitpick comments:
In `@altk_evolve/hooks/manager.py`:
- Around line 242-256: Update _validate_plugin_dependencies to enumerate plugins
through the supported public accessor on cpex.framework.manager.PluginManager
instead of reaching into pm._registry. Preserve the existing startup_validate()
invocation for each registered plugin, and use a graceful fallback if no public
accessor is available so hook initialization does not depend on private registry
internals.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0af3bb13-bbe7-4ea5-8947-e9ce9492e0df
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (21)
altk_evolve/cli/cli.pyaltk_evolve/cli/templates/__init__.pyaltk_evolve/cli/templates/hooks.yamlaltk_evolve/config/hooks.pyaltk_evolve/frontend/client/evolve_client.pyaltk_evolve/hooks/__init__.pyaltk_evolve/hooks/manager.pyaltk_evolve/hooks/plugins/__init__.pyaltk_evolve/hooks/plugins/pii.pyaltk_evolve/hooks/plugins/readi.pydocs/guides/memory-hooks.mddocs/guides/pii-redaction.mdexamples/hooks_demo.pyexamples/hooks_plugins.yamlexamples/pii_benchmark.pypyproject.tomltests/unit/test_hooks_config_discovery.pytests/unit/test_hooks_noop.pytests/unit/test_hooks_plugins.pytests/unit/test_hooks_seam.pytests/unit/test_readi_redaction_core.py
initialize_hooks only checked plugins_yaml before auto-discovering, so a caller configuring ONLY code-first `plugins` still triggered discovery and a stray evolve.hooks.yaml / $EVOLVE_HOOKS_CONFIG / user-config file would be silently merged, contradicting the documented "explicit plugins_yaml (or any code-first plugins) overrides discovery" contract. Discover only when both plugins_yaml and plugins are unset/empty. Add a regression test asserting discovery is not consulted and the stray file's plugin is not merged when code-first plugins are set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…keep always-on hooks # Conflicts: # altk_evolve/cli/cli.py # docs/guides/memory-hooks.md
The always-on hooks redesign kept HooksConfig.enabled as a deprecated, ignored field emitting a DeprecationWarning. Remove that ceremony entirely: drop the _drop_deprecated_enabled model_validator, the warning, and the docstring/user-facing notes describing it. HooksConfig is a plain pydantic BaseModel (no model_config), so the v2 default extra="ignore" now applies: passing enabled=... is silently dropped — no error, no warning, no attribute. This matches sibling config models and keeps removal non-breaking and quiet. Replace the two DeprecationWarning tests with a single test pinning the new silent-ignore behavior. Drop the deprecation NOTE from the memory-hooks guide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under an interactive terminal, rich's bracket highlighter (distinct from markup) split `altk-evolve[pii-semantic]` / `altk-evolve[pii-regex]` in the `evolve hooks init` install hint with ANSI color codes, making the hint hard to read and breaking test_hooks_init_writes_readi_active_regex_commented under a TTY (it passed only with NO_COLOR=1). Add highlight=False (alongside the existing markup=False) to the install-hint console.print calls so the bracketed extras render literally regardless of terminal. The test now passes in the default env without forcing NO_COLOR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/guides/retention.md (1)
98-109: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd language identifiers to fenced code blocks.
Static analysis (MD040) flags these blocks as missing a language tag. Use
textsince they're illustrative CLI output, not executable code.📝 Proposed fix
-``` +```text DELETE 4 trajectory reason=age rule=old-sessionsand similarly for the block starting at line 124.
Also applies to: 124-128
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/guides/retention.md` around lines 98 - 109, Update the fenced illustrative CLI-output blocks in the retention guide, including the blocks near the current sections at lines 98 and 124, to declare the text language. Preserve their output content unchanged.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@docs/guides/retention.md`:
- Around line 98-109: Update the fenced illustrative CLI-output blocks in the
retention guide, including the blocks near the current sections at lines 98 and
124, to declare the text language. Preserve their output content unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7975ac9d-65f6-4558-802f-641bf2a0c33a
📒 Files selected for processing (5)
altk_evolve/cli/cli.pyaltk_evolve/frontend/client/evolve_client.pydocs/guides/memory-hooks.mddocs/guides/retention.mdpyproject.toml
🚧 Files skipped from review as they are similar to previous changes (4)
- altk_evolve/frontend/client/evolve_client.py
- pyproject.toml
- altk_evolve/cli/cli.py
- docs/guides/memory-hooks.md
… + adapter A hook plugin is no longer required to be a cpex `Plugin`. Introduce an engine-agnostic native contract (`altk_evolve/hooks/plugin.py`: `HookContext`, `HookPlugin` Protocol, `HookPluginBase`) that imports no cpex: plugins implement one sync method per hook over the plain frozen payloads, return `payload.replace(...)` or `None`, and raise to halt (fail-closed). The manager now owns YAML parsing itself (no longer delegated to cpex's loader) and routes every spec through `_register_spec`, which detects a cpex `Plugin` subclass (registered directly — the regex `pii` plugin stays raw cpex, proving dual support) vs a native plugin (wrapped in a cpex `Plugin` adapter). The adapter converts payloads via getattr + `_to_plain` (materializing cpex copy-on-write containers, which `model_dump()` reads as empty) so native plugins never see a cpex type. Port normalizer, access_stamp and readi to native over the same unchanged cores; drop the `HAS_CPEX` shim/stub and cpex `_default_config` helpers. All invariants preserved: fail-closed on both axes (missing detector at init; raise -> MemoryPolicyViolation, nothing stored), no in-place mutation leak, READI splice correctness, always-on semantics, no cpex import with zero plugins, backend no-bypass, full egress-site coverage. Add a third-party native plugin test (imports no cpex) covering end-to-end transform, native fail-closed, and native+raw-cpex coexistence. Update docs to lead with the native contract and scope cpex to a bounded engine section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moving YAML parsing off cpex's loader onto HookPluginSpec flipped the default for a plugin entry that omits `mode`: cpex's PluginConfig defaulted to sequential, but HookPluginSpec defaults to transform. In transform mode cpex silently downgrades a block (continue_processing=False) to a pass-through, so a mode-less blocking compliance plugin declared in evolve.hooks.yaml would fail OPEN — a posture inversion on the exact boundary the seam protects. _parse_plugins_yaml now defaults an omitted mode to sequential (restoring parity with the former cpex loader and the fail-closed-capable mode); an explicit mode is always honored. Code-first HookPluginSpec still defaults to transform (unchanged). Regression test asserts both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion Add a raw-CPEX hook plugin that reuses cpex-secrets-detection's Rust redactor to mask credentials/tokens (AWS keys, GitHub/Slack tokens, Stripe secrets, private-key blocks) on memory_pre_write and llm_pre_call — a third redaction method orthogonal to the two PII methods. Mirrors PIIFilterMemoryPlugin (guarded import → fail-closed stub naming '[secrets]', PluginConfig-driven, sequential + on_error=fail). Two forced divergences, documented in the source: - cpex-secrets-detection's SecretsDetectionPlugin subclasses mcpgateway's Plugin, not cpex's, and its hook methods are mcpgateway-bound; so the plugin also inherits cpex Plugin (for raw-cpex routing/dispatch) and drives redaction through the redactor's framework-free py_scan_container. - that Rust scanner walks dict/list containers but not pydantic models, so the memory path dumps each Entity to a dict, scans, and reloads it. Default config uses the package's real schema (enabled per-detector map + redact/redaction_text/block_on_detection/min_findings_to_block): structured detectors ON, entropy/JWT detectors OFF (they over-redact a memory corpus). Adds the [secrets] extra (cpex-secrets-detection>=0.1,<0.2), a mypy override, a commented-out scaffold block, docs, and mirrored tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The only usable surface of cpex-secrets-detection is py_scan_container — a framework-free Rust *function* (a library), not a cpex Plugin. The packaged SecretsDetectionPlugin subclasses mcpgateway's Plugin (not cpex's) and its hook methods are mcpgateway-bound, so there is no cpex plugin to reuse. That makes this the READI pattern (native plugin wrapping a detector library), not the pii pattern (reusing a genuine cpex Plugin). Rework secrets.py to a native HookPluginBase mirroring readi.py: - Drop both base classes and the dual-inheritance hack; no cpex import and no cpex_secrets_detection import at module top. The class subclasses only HookPluginBase and runs through the seam's native adapter. - Lazy build_secrets_scanner() imports py_scan_container with the same narrow ModuleNotFoundError guard as build_readi_detector (only a genuinely-absent cpex_secrets_detection → ImportError naming [secrets]). - startup_validate() builds the scanner so a missing extra fails closed at engine init, not on first write. - Sync hook methods: entities/messages arrive as list[dict] in the native contract, so pass them straight to py_scan_container (no model_dump dance) and return payload.replace(...). Block by RAISING (redact:false path), not PluginResult/PluginViolation. - Config merges a module-level DEFAULT_ENABLED/scan-config so an empty config still gets structured detectors ON, entropy/JWT OFF. Same verified keys. Tests reworked to the native shape: a routing guard (not a cpex Plugin subclass), an init-time fail-closed test (monkeypatch the scanner build to raise, register via spec, assert ImportError naming [secrets] at init), and a find_spec-guarded degradation test; e2e redaction tests now drive the native path. Docs/pyproject reclassify secrets as native; pii remains the ONE raw-cpex plugin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the pre-seam PoC demos (which imported the now-removed PIIConfig/NullRedactor/backend.redactor) with seam-native presentation files showing how to USE the merged hook seam (PR #295): - examples/pii_redaction_demo.py: regex + semantic PII plugins via HooksConfig; headline is that semantic catches the NAME regex cannot. Guards + graceful macOS/MPS fallback to regex-only. - examples/secrets_demo.py: SecretsFilterMemoryPlugin redacts a fake AWS key + GitHub token on write and on LLM egress. - DEMO.md: presentation walkthrough — evolve hooks init / HooksConfig, the runnable demos with real output, retained measured results, defence-in-depth + limitations (incl. MPS caveat). This branch is main + these demo files (presentation branch, not a history-preserving merge). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delivers the always-on memory-hook seam with an engine-agnostic plugin contract and three redaction methods (regex PII, semantic PII, structured secrets). Closes #275.
Highlights
Plugin. Newaltk_evolve/hooks/plugin.py(HookPluginProtocol,HookPluginBase,HookContext) lets a plugin implement one sync method per hook over the plain frozen payloads, importing no cpex. cpex becomes one swappable execution engine behind an adapter, and altk_evolve now owns YAML parsing. Raw cpex plugins remain supported (dual support):piiis the single deliberate raw-cpex plugin;normalizer/access_stamp/readi/secretsare native.ReadiSemanticPIIPlugin— NER-based redaction that catches names/entities a regex cannot ([pii-semantic]extra). Active by default in the scaffold; regex ([pii-regex]) composes for defence-in-depth.SecretsFilterMemoryPlugin— a native plugin wrappingcpex-secrets-detection's framework-free Rust core ([secrets]extra), same shape as READI. Structured/high-precision detectors (AWS/Google/GitHub/Slack keys, Stripe secrets, private-key blocks) on by default; entropy/JWT detectors are opt-in because they over-redact a memory corpus (legit base64/hex/hashes).enabledswitch — behavior is purely which plugins are configured. Zero plugins → zero-cost no-op, cpex never imported. Configured-but-missing-engine/detector → fail-closed hard error at init. Config auto-discovery ($EVOLVE_HOOKS_CONFIG→./evolve.hooks.yaml→~/.config/evolve/hooks.yaml) + anevolve hooks initscaffold.Hooks
memory_pre_write,memory_pre_metadata_patch,memory_pre_delete,memory_pre_namespace_delete,memory_post_read,llm_pre_call— fired at backend-layer choke points so no frontend can bypass them.Invariants held
MemoryPolicyViolation, nothing stored or sent.llm_pre_callegress-site coverage.modedefaults tosequential(fail-closed) — matching the former cpex loader, so a mode-less blocking plugin can't silently degrade to non-blockingtransform.Validation
Full suite 1019 passed / 4 skipped; lockfile, linting, formatting, typing, plugins-rendered, and detect-secrets all green.