feat(mcp): expose compliance and retention controls#296
Conversation
|
Warning Review limit reached
Next review available in: 13 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds MCP tools for entity administration, access recording, retention policy validation and execution, compliance status reporting, and legal-hold enforcement. The client, backend, and retention engine gain administrative scan and timestamp paths, with documentation, configuration, dependency, and unit-test updates. ChangesAdministrative MCP and retention controls
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant MCPClient
participant MCPServer
participant RetentionEngine
participant EvolveClient
participant EntityBackend
MCPClient->>MCPServer: run_retention(policy, filters)
MCPServer->>RetentionEngine: apply normalized policy
RetentionEngine->>EvolveClient: scan_entities(namespace_id, filters, limit)
EvolveClient->>EntityBackend: administrative entity search
EntityBackend-->>RetentionEngine: entity snapshot
RetentionEngine-->>MCPServer: retention outcomes and report
MCPServer-->>MCPClient: structured JSON response
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
tests/unit/test_mcp_compliance_tools.py (1)
98-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the default
record_access=Truedenial path.Both parametrized cases use
record_access=False, so the ordering issue flagged inaltk_evolve/frontend/mcp/mcp_server.py(Lines 521-534) — the access-stamping read happening before the ownership check — is untested. A case assertingclient.get_entity_by_id/client.record_accessare not called for a non-owner would lock the fix in.🤖 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 `@tests/unit/test_mcp_compliance_tools.py` around lines 98 - 111, Add a test case in test_get_entity_enforces_attributed_owner that uses the default record_access=True for a non-owner, then assert the denial response and verify client.get_entity_by_id and client.record_access are not called. Keep the existing parametrized record_access=False coverage unchanged.altk_evolve/frontend/mcp/mcp_server.py (3)
687-701: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSnapshot scan duplicates the engine's full namespace scan.
run_retentionscans up toFETCH_LIMITentities for snapshots andRetentionEngine.evaluatescans the namespace again, doubling I/O and memory on large namespaces. Consider building snapshots lazily for only the ids in the report, or having the engine expose the entities it already fetched.🤖 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/frontend/mcp/mcp_server.py` around lines 687 - 701, Eliminate the duplicate namespace scan in run_retention by reusing entities fetched by RetentionEngine.apply/evaluate when constructing snapshots. Prefer exposing the engine’s fetched entities or lazily resolving only report-referenced IDs, while preserving retention report behavior and existing scan limits.
780-797: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
healthyandretention_availableare structurally derived, not probed.
plugin["healthy"]is justmanager is not None and enabled, andretention_availableis a literalTrue, so a plugin that failed to load still reports healthy. Consider deriving per-plugin health from the manager's registered hooks (e.g.has_hooks_for) so the report can actually fail.🤖 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/frontend/mcp/mcp_server.py` around lines 780 - 797, Update the health computation in the MCP health response so each enabled plugin’s health is derived from its manager’s registered hooks using the existing hook-checking API such as has_hooks_for, rather than only checking manager existence and enabled state. Replace the literal retention_available value with the actual retention capability or probe result, and ensure failed or unregistered plugins cause the overall healthy result to be false.
583-594: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winOne backend scan per id; consider a single scan for the batch.
record_accesswith a largeentity_idslist issues N sequentialscan_entitiesround-trips. A single scan (or an id-filtered batch fetch) then an in-memory ownership partition keeps this O(1) in backend calls. Also worth short-circuiting whenallowedis empty.🤖 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/frontend/mcp/mcp_server.py` around lines 583 - 594, The entity validation loop performs one scan_entities call per unique entity ID; replace it with a single batch or ID-filtered scan, then partition the returned entities into missing, denied, and allowed using _entity_owned_by while preserving input-ID handling. In the record_access flow, skip the backend call when allowed is empty while retaining the existing timestamp behavior for non-empty updates.altk_evolve/frontend/client/evolve_client.py (1)
117-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider a named backend seam instead of reaching into
_search_entities_impl.This is the second caller of the private impl (see
consolidate_guidelines, Line 273). A documentedBaseEntityBackend.scan_entities(orsearch_entities_internal) would make the no-hook read path part of the backend contract instead of relying on a private method every backend must keep.🤖 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/frontend/client/evolve_client.py` around lines 117 - 126, The scan_entities path should use a documented backend seam rather than directly calling the private _search_entities_impl method. Add or expose a named method such as BaseEntityBackend.scan_entities (or search_entities_internal), implement it across backends as needed, and update both scan_entities and consolidate_guidelines to call that contract while preserving the no-hook read behavior.
🤖 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/frontend/mcp/mcp_server.py`:
- Around line 744-747: Validate the parsed YAML root in the plugin-loading path
before calling loaded.get, ensuring it is a mapping and treating list or scalar
roots as malformed input. Raise the same handled validation/error type used by
RetentionPolicy.from_file so get_compliance_status returns its structured
healthy: False payload instead of leaking AttributeError.
- Around line 488-496: Update the pagination flow around record_access and
next_offset to capture the original page length before per-entity re-fetching,
then advance the cursor using that pre-filter length rather than len(page).
Preserve the transformed page for returned results and access recording while
ensuring vanished entities do not cause already-consumed rows to be re-served.
- Around line 521-534: Update the entity lookup flow around _resolve_namespace
and _entity_owned_by so ownership is always determined using the non-access scan
path before any access-stamping read occurs. After confirming the entity exists
and _entity_owned_by(entity, user_id) succeeds, perform record_access when
requested; preserve the existing not-found and permission-denied responses.
In `@altk_evolve/retention/engine.py`:
- Around line 213-214: The entity scan selection in the retention engine must
not eagerly access get_all_entities when scan_entities is available. Update the
scan assignment to lazily choose the preferred scan_entities method and only
resolve get_all_entities as the fallback, preserving the existing
scan(namespace_id, limit=limit) invocation.
---
Nitpick comments:
In `@altk_evolve/frontend/client/evolve_client.py`:
- Around line 117-126: The scan_entities path should use a documented backend
seam rather than directly calling the private _search_entities_impl method. Add
or expose a named method such as BaseEntityBackend.scan_entities (or
search_entities_internal), implement it across backends as needed, and update
both scan_entities and consolidate_guidelines to call that contract while
preserving the no-hook read behavior.
In `@altk_evolve/frontend/mcp/mcp_server.py`:
- Around line 687-701: Eliminate the duplicate namespace scan in run_retention
by reusing entities fetched by RetentionEngine.apply/evaluate when constructing
snapshots. Prefer exposing the engine’s fetched entities or lazily resolving
only report-referenced IDs, while preserving retention report behavior and
existing scan limits.
- Around line 780-797: Update the health computation in the MCP health response
so each enabled plugin’s health is derived from its manager’s registered hooks
using the existing hook-checking API such as has_hooks_for, rather than only
checking manager existence and enabled state. Replace the literal
retention_available value with the actual retention capability or probe result,
and ensure failed or unregistered plugins cause the overall healthy result to be
false.
- Around line 583-594: The entity validation loop performs one scan_entities
call per unique entity ID; replace it with a single batch or ID-filtered scan,
then partition the returned entities into missing, denied, and allowed using
_entity_owned_by while preserving input-ID handling. In the record_access flow,
skip the backend call when allowed is empty while retaining the existing
timestamp behavior for non-empty updates.
In `@tests/unit/test_mcp_compliance_tools.py`:
- Around line 98-111: Add a test case in
test_get_entity_enforces_attributed_owner that uses the default
record_access=True for a non-owner, then assert the denial response and verify
client.get_entity_by_id and client.record_access are not called. Keep the
existing parametrized record_access=False coverage unchanged.
🪄 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 Plus
Run ID: 7915f270-3566-4894-b5cd-ebb096b4a53e
📒 Files selected for processing (7)
README.mdaltk_evolve/frontend/client/evolve_client.pyaltk_evolve/frontend/mcp/mcp_server.pyaltk_evolve/retention/engine.pydocs/guides/retention.mdtests/unit/test_mcp_compliance_tools.pytests/unit/test_retention.py
|
No vulnerabilities found. |
|
Review follow-up is in Addressed:
I retained per-ID ownership lookups in Validation: 966 Python tests passed (8 skipped, 279 deselected), 90 focused tests passed, pre-commit passed, and the clean UI install, 6 UI tests, and production build passed. Note: npm now reports a separate newer RSC advisory whose suggested fix is |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
altk_evolve/frontend/mcp/mcp_server.py (1)
1149-1165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse
_parse_datetimeinstead of re-implementing ISO-8601/UTC-fallback parsing.This inline
datetime.fromisoformat(created_at.replace("Z", "+00:00"))+ naive-tz-fallback logic duplicates what_parse_datetime(already used byrecord_accessandrun_retention) does, including handlingNone/empty input.♻️ Proposed refactor
- parsed_created_at = None - if created_at: - from datetime import UTC, datetime - - try: - parsed_created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00")) - except ValueError: - return json.dumps({"error": "Invalid created_at", "message": "created_at must be ISO-8601"}) - if parsed_created_at.tzinfo is None: - parsed_created_at = parsed_created_at.replace(tzinfo=UTC) + try: + parsed_created_at = _parse_datetime(created_at, field_name="created_at") + except ValueError: + return json.dumps({"error": "Invalid created_at", "message": "created_at must be ISO-8601"})🤖 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/frontend/mcp/mcp_server.py` around lines 1149 - 1165, The entity creation flow should reuse the existing _parse_datetime helper instead of duplicating ISO-8601 parsing and UTC fallback logic. Replace the inline parsed_created_at conversion in the entity creation function with _parse_datetime, preserving the existing invalid-created_at error response and handling of empty or missing values.
🤖 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/frontend/mcp/mcp_server.py`:
- Around line 806-821: Update the healthy aggregate in the MCP health response
to require hook_engine_available whenever any plugin is enabled, while
preserving the existing plugin health and retention checks. Use the existing
plugins collection and hook_engine_available value so enabled plugins report
unhealthy when the hook engine is unavailable; leave the behavior unchanged when
no plugins are enabled.
In `@altk_evolve/hooks/manager.py`:
- Around line 447-458: Preserve structured violation details in the synchronous
_invoke path: add a details field to MemoryPolicyViolation, pass
HookPolicyViolation.details when reconstructing it around the _invoke handling,
and ensure the resulting PluginViolation retains that data for
delete_entity_by_id callers. Update the regression test to assert the
LegalHoldMemoryPlugin entity_id is preserved.
---
Nitpick comments:
In `@altk_evolve/frontend/mcp/mcp_server.py`:
- Around line 1149-1165: The entity creation flow should reuse the existing
_parse_datetime helper instead of duplicating ISO-8601 parsing and UTC fallback
logic. Replace the inline parsed_created_at conversion in the entity creation
function with _parse_datetime, preserving the existing invalid-created_at error
response and handling of empty or missing values.
🪄 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 Plus
Run ID: e0aa054d-670e-4469-a035-6501c7134bac
⛔ Files ignored due to path filters (1)
altk_evolve/frontend/ui/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
altk_evolve/backend/base.pyaltk_evolve/frontend/client/evolve_client.pyaltk_evolve/frontend/mcp/mcp_server.pyaltk_evolve/frontend/ui/package.jsonaltk_evolve/hooks/manager.pyaltk_evolve/hooks/plugin.pyaltk_evolve/hooks/plugins/legal_hold.pyaltk_evolve/retention/engine.pydocs/guides/memory-hooks.mdexamples/cuga_compliance_poc_hooks.yamlexamples/hooks_plugins.yamltests/unit/test_combine_guidelines.pytests/unit/test_filesystem_backend.pytests/unit/test_hooks_seam.pytests/unit/test_mcp_compliance_tools.pytests/unit/test_mcp_server.pytests/unit/test_retention.py
| hook_coverage = {hook.value: hooks_active(hook) for hook in HookType} | ||
| hook_engine_available = engine_available() | ||
| retention_available = (callable(getattr(client, "scan_entities", None)) or callable(getattr(client, "get_all_entities", None))) and all( | ||
| callable(getattr(client, method, None)) for method in ("patch_entity_metadata", "delete_entity_by_id") | ||
| ) | ||
| healthy = client.ready() and retention_available and all(not plugin["enabled"] or plugin["healthy"] for plugin in plugins) | ||
| return _json_response( | ||
| { | ||
| "healthy": healthy, | ||
| "evolve_version": package_version, | ||
| "backend": evolve_config.backend, | ||
| "namespace_id": resolved_ns, | ||
| "retention_available": retention_available, | ||
| "hooks_enabled": manager is not None, | ||
| "hook_engine_available": hook_engine_available, | ||
| "hook_coverage": hook_coverage, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether has_hooks_for already gates on engine availability.
ast-grep outline altk_evolve/hooks/manager.py --items all
rg -n -A10 'def has_hooks_for' altk_evolve/hooks/manager.py
rg -n 'HAS_CPEX|engine_available' altk_evolve/hooks/manager.py altk_evolve/hooks/types.pyRepository: AgentToolkit/altk-evolve
Length of output: 3522
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== mcp_server health snippet =="
sed -n '780,840p' altk_evolve/frontend/mcp/mcp_server.py
echo
echo "== hooks types =="
sed -n '1,90p' altk_evolve/hooks/types.py
sed -n '200,260p' altk_evolve/hooks/types.py
echo
echo "== hook manager relevant methods =="
sed -n '100,210p' altk_evolve/hooks/manager.py
sed -n '320,360p' altk_evolve/hooks/manager.py
sed -n '300,310p' altk_evolve/hooks/manager.py
echo
echo "== health / plugin health usages =="
rg -n -C3 '"plugin_health"|plugin_health|hooks_active\(|has_hooks_for\(|engine_available\(' altk_evolve/frontend/mcp/mcp_server.py altk_evolve/hooks/manager.py altk_evolve/hooks/types.pyRepository: AgentToolkit/altk-evolve
Length of output: 23104
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== health function context =="
rg -n -C20 'def .*hook.*health|def .*health|hook_engine_available|hook_coverage|healthy = client.ready' altk_evolve/frontend/mcp/mcp_server.py
echo
echo "== hook activation gates =="
rg -n -C5 'hooks_active\(HookType\.(MEMORY_PRE_DELETE|MEMORY_PRE_WRITE|MEMORY_PRE_METADATA_PATCH|MEMORY_PRE_NAMESPACE_DELETE)\)' altk_evolve backend -g '*.py'
echo
echo "== static verifier: health expression vs engine dependency =="
python3 - <<'PY'
from pathlib import Path
import re
mcp = Path("altk_evolve/frontend/mcp/mcp_server.py").read_text()
m = re.search(r'healthy = (.+?)(?:\n return _json_response|$)', mcp, re.S)
if not m:
raise SystemExit("healthy assignment not found")
expr = " ".join(m.group(1).split())
print(expr)
print("depends_on_hook_engine_available=", "hook_engine_available" in expr)
hooks = Path("altk_evolve/hooks/manager.py").read_text()
hp = re.search(r'''def hooks_active\([^)]*\):\s*""".*?"""(?:\n\s*if not .*?\n\s*return .*?\n)?''', hooks, re.S)
hp2 = re.search(r'''def hooks_active\(hook_type: HookType\).\s*(?:""".*?""")?\s*(\s*if not.*?\n\s*return.*?\n\s*return.*?)''', hooks, re.S)
print("\\nhooks_active body:", hp2.group(1).strip() if hp2 else "not parsed")
print("hooks_active_gates_engine=", "not _plugins_enabled or _plugin_manager is None" in (hp2.group(1) if hp2 else ""))
PYRepository: AgentToolkit/altk-evolve
Length of output: 6807
Include hook_engine_available in the MCP health flag when hooks are enabled.
hooks_active() only checks _plugins_enabled/_plugin_manager and delegates to has_hooks_for(), so an enabled plugin reporting subscribed hooks still reports healthy: true even when cpex is unavailable and dispatches no-op. If any plugin is enabled, require hook_engine_available in the aggregate health before retention operations are allowed to report healthy.
🛡️ Proposed fix
- healthy = client.ready() and retention_available and all(not plugin["enabled"] or plugin["healthy"] for plugin in plugins)
+ any_plugin_enabled = any(plugin["enabled"] for plugin in plugins)
+ healthy = (
+ client.ready()
+ and retention_available
+ and (not any_plugin_enabled or hook_engine_available)
+ and all(not plugin["enabled"] or plugin["healthy"] for plugin in plugins)
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| hook_coverage = {hook.value: hooks_active(hook) for hook in HookType} | |
| hook_engine_available = engine_available() | |
| retention_available = (callable(getattr(client, "scan_entities", None)) or callable(getattr(client, "get_all_entities", None))) and all( | |
| callable(getattr(client, method, None)) for method in ("patch_entity_metadata", "delete_entity_by_id") | |
| ) | |
| healthy = client.ready() and retention_available and all(not plugin["enabled"] or plugin["healthy"] for plugin in plugins) | |
| return _json_response( | |
| { | |
| "healthy": healthy, | |
| "evolve_version": package_version, | |
| "backend": evolve_config.backend, | |
| "namespace_id": resolved_ns, | |
| "retention_available": retention_available, | |
| "hooks_enabled": manager is not None, | |
| "hook_engine_available": hook_engine_available, | |
| "hook_coverage": hook_coverage, | |
| hook_coverage = {hook.value: hooks_active(hook) for hook in HookType} | |
| hook_engine_available = engine_available() | |
| retention_available = (callable(getattr(client, "scan_entities", None)) or callable(getattr(client, "get_all_entities", None))) and all( | |
| callable(getattr(client, method, None)) for method in ("patch_entity_metadata", "delete_entity_by_id") | |
| ) | |
| any_plugin_enabled = any(plugin["enabled"] for plugin in plugins) | |
| healthy = ( | |
| client.ready() | |
| and retention_available | |
| and (not any_plugin_enabled or hook_engine_available) | |
| and all(not plugin["enabled"] or plugin["healthy"] for plugin in plugins) | |
| ) | |
| return _json_response( | |
| { | |
| "healthy": healthy, | |
| "evolve_version": package_version, | |
| "backend": evolve_config.backend, | |
| "namespace_id": resolved_ns, | |
| "retention_available": retention_available, | |
| "hooks_enabled": manager is not None, | |
| "hook_engine_available": hook_engine_available, | |
| "hook_coverage": hook_coverage, |
🤖 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/frontend/mcp/mcp_server.py` around lines 806 - 821, Update the
healthy aggregate in the MCP health response to require hook_engine_available
whenever any plugin is enabled, while preserving the existing plugin health and
retention checks. Use the existing plugins collection and hook_engine_available
value so enabled plugins report unhealthy when the hook engine is unavailable;
leave the behavior unchanged when no plugins are enabled.
| try: | ||
| out = method(plain, hook_ctx) | ||
| except HookPolicyViolation as violation: | ||
| return PluginResult( | ||
| continue_processing=False, | ||
| violation=PluginViolation( | ||
| reason=violation.reason, | ||
| description=violation.reason, | ||
| code=violation.code, | ||
| details=violation.details, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Preserve structured violation details through the public exception.
The adapter copies HookPolicyViolation.details into PluginViolation, but the synchronous _invoke path at Line 577-584 reconstructs MemoryPolicyViolation with only reason, code, and plugin name. Consequently, LegalHoldMemoryPlugin’s entity_id never reaches delete_entity_by_id callers. Add a details field to MemoryPolicyViolation, pass it through, and assert it in the regression test.
🤖 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 447 - 458, Preserve structured
violation details in the synchronous _invoke path: add a details field to
MemoryPolicyViolation, pass HookPolicyViolation.details when reconstructing it
around the _invoke handling, and ensure the resulting PluginViolation retains
that data for delete_entity_by_id callers. Update the regression test to assert
the LegalHoldMemoryPlugin entity_id is preserved.
Summary
last_accessedWhy
This supplies the Evolve-side API surface needed by the CUGA stakeholder compliance PoC and supports the eventing case discussed in #275. Evolve owns policy evaluation, hook enforcement, and referenced results; an external scheduler/eventing system can invoke runs and persist or deliver those results.
MCP tools
list_entitiesget_entitypatch_entity_metadatarecord_accessvalidate_retention_policyrun_retentionget_compliance_statusValidation
uv run --extra milvus pytest -q: 961 passed, 8 skipped, 279 deselectedNotes
run_retentionacceptsas_offor deterministic stakeholder demonstrations and returns pre-action snapshots so applied deletions remain explainable.Summary by CodeRabbit