Skip to content

feat(hooks)!: engine-agnostic plugin contract + semantic-PII/secrets redaction (#275) - #295

Merged
illeatmyhat merged 20 commits into
mainfrom
feat/275-readi-semantic-pii
Jul 24, 2026
Merged

feat(hooks)!: engine-agnostic plugin contract + semantic-PII/secrets redaction (#275)#295
illeatmyhat merged 20 commits into
mainfrom
feat/275-readi-semantic-pii

Conversation

@illeatmyhat

@illeatmyhat illeatmyhat commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

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

  • Engine-agnostic plugin contract (breaking). A hook plugin no longer needs to be a cpex Plugin. New altk_evolve/hooks/plugin.py (HookPlugin Protocol, 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): pii is the single deliberate raw-cpex plugin; normalizer / access_stamp / readi / secrets are native.
  • Semantic PII redaction (READI). 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.
  • Structured secrets redaction. SecretsFilterMemoryPlugin — a native plugin wrapping cpex-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).
  • Always-on seam. No enabled switch — 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) + an evolve hooks init scaffold.

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

  • Fail-closed on both axes: a configured plugin whose engine/detector is missing errors at init; a plugin that raises → MemoryPolicyViolation, nothing stored or sent.
  • No in-place payload mutation across the plugin chain (return-a-replacement contract).
  • READI offset/merge right-to-left splice correctness (multibyte-safe).
  • Full llm_pre_call egress-site coverage.
  • YAML-omitted plugin mode defaults to sequential (fail-closed) — matching the former cpex loader, so a mode-less blocking plugin can't silently degrade to non-blocking transform.

Validation

Full suite 1019 passed / 4 skipped; lockfile, linting, formatting, typing, plugins-rendered, and detect-secrets all green.

illeatmyhat and others added 5 commits July 20, 2026 11:32
[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>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The 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.

Changes

Hooks configuration and lifecycle

Layer / File(s) Summary
Hook discovery, initialization, and CLI scaffolding
altk_evolve/cli/..., altk_evolve/config/hooks.py, altk_evolve/hooks/manager.py, altk_evolve/cli/templates/*
Adds ordered configuration discovery, plugin-driven activation, fail-closed initialization, process-global manager reset behavior, and evolve hooks init scaffolding.
Lifecycle documentation and validation
altk_evolve/frontend/..., docs/guides/memory-hooks.md, tests/unit/test_hooks_*.py
Documents and tests no-plugin pass-through, discovery precedence, cleanup, overwrite protection, and configured-plugin behavior.

Native plugin and semantic PII support

Layer / File(s) Summary
Native plugin contract and integration
altk_evolve/hooks/plugin.py, altk_evolve/hooks/types.py, altk_evolve/hooks/plugins/{normalizer,access_stamp}.py, altk_evolve/hooks/manager.py
Adds engine-agnostic plugin protocols, immutable payload replacement, native shipped plugins, and adapter support for native and raw engine plugins.
READI redaction pipeline
altk_evolve/hooks/plugins/readi.py, altk_evolve/hooks/plugins/__init__.py, tests/unit/test_readi_redaction_core.py, tests/unit/test_hooks_plugins.py
Adds character-offset redaction, recursive entity/message handling, detector construction, semantic hook handlers, configuration, and missing-dependency validation.

Benchmarking and documentation

Layer / File(s) Summary
PII evaluation tooling
examples/pii_benchmark.py, pyproject.toml, tests/unit/test_readi_redaction_core.py
Adds synthetic and dataset-backed evaluation, span conversion, precision/recall/F1 and leak-rate scoring, backend selection, and benchmark dependencies.
Guides and examples
docs/guides/*.md, examples/hooks_*.yaml, examples/hooks_demo.py
Updates hook lifecycle, semantic/regex PII configuration, optional dependency instructions, and native plugin usage examples.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: vinodmut

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main changes: engine-agnostic hooks plus semantic PII and secrets redaction.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/275-readi-semantic-pii

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

illeatmyhat and others added 7 commits July 21, 2026 11:37
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
altk_evolve/hooks/manager.py (1)

242-256: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Eager validation reaches into a private cpex attribute (pm._registry).

_validate_plugin_dependencies walks pm._registry.get_all_plugins() — an underscore-prefixed, presumably-private attribute of the cpex PluginManager. If a future cpex release 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 whether cpex.framework.manager.PluginManager exposes 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1d2edc2 and 09e44c0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • altk_evolve/cli/cli.py
  • altk_evolve/cli/templates/__init__.py
  • altk_evolve/cli/templates/hooks.yaml
  • altk_evolve/config/hooks.py
  • altk_evolve/frontend/client/evolve_client.py
  • altk_evolve/hooks/__init__.py
  • altk_evolve/hooks/manager.py
  • altk_evolve/hooks/plugins/__init__.py
  • altk_evolve/hooks/plugins/pii.py
  • altk_evolve/hooks/plugins/readi.py
  • docs/guides/memory-hooks.md
  • docs/guides/pii-redaction.md
  • examples/hooks_demo.py
  • examples/hooks_plugins.yaml
  • examples/pii_benchmark.py
  • pyproject.toml
  • tests/unit/test_hooks_config_discovery.py
  • tests/unit/test_hooks_noop.py
  • tests/unit/test_hooks_plugins.py
  • tests/unit/test_hooks_seam.py
  • tests/unit/test_readi_redaction_core.py

Comment thread altk_evolve/hooks/manager.py
illeatmyhat and others added 4 commits July 22, 2026 09:59
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Add language identifiers to fenced code blocks.

Static analysis (MD040) flags these blocks as missing a language tag. Use text since they're illustrative CLI output, not executable code.

📝 Proposed fix
-```
+```text
 DELETE  4   trajectory reason=age          rule=old-sessions

and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 274627b and 7786c3f.

📒 Files selected for processing (5)
  • altk_evolve/cli/cli.py
  • altk_evolve/frontend/client/evolve_client.py
  • docs/guides/memory-hooks.md
  • docs/guides/retention.md
  • pyproject.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

illeatmyhat and others added 4 commits July 23, 2026 11:47
… + 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>
@illeatmyhat illeatmyhat changed the title feat(pii): READI semantic redaction as a hook-seam plugin (#275) feat(hooks)!: engine-agnostic plugin contract + semantic-PII/secrets redaction (#275) Jul 24, 2026
@illeatmyhat
illeatmyhat merged commit 4e7dab7 into main Jul 24, 2026
19 checks passed
illeatmyhat added a commit that referenced this pull request Jul 24, 2026
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>
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.

Compliance - PII, Retention, etc.

1 participant