claude-smart 0.2.47 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""SQLite audit-event store methods.
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``_governance.py`` (the AuditEventStore bucket): the
|
|
4
|
+
three public methods (``append_audit_event``, ``list_audit_events``,
|
|
5
|
+
``gc_governance_retention``) plus the Audit-owned private
|
|
6
|
+
``_append_audit_event_with_cursor`` (called cross-bucket by the residual purge /
|
|
7
|
+
barrier completion methods, which reach it via MRO co-composition).
|
|
8
|
+
|
|
9
|
+
The residual ``SQLiteGovernanceMixin`` stays composed alongside this mixin and
|
|
10
|
+
permanently holds the shared infra (``conn``, ``_lock``, ``org_id``, ``_deps()``)
|
|
11
|
+
and the module-level helpers (``_json_dumps``, ``_row_to_audit_event``), which
|
|
12
|
+
are imported here rather than duplicated.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sqlite3
|
|
18
|
+
import threading
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
from reflexio.models.api_schema.domain.governance import AuditEvent
|
|
23
|
+
from reflexio.models.config_schema import GovernanceRetentionConfig
|
|
24
|
+
from reflexio.server.services.storage.governance_validation import (
|
|
25
|
+
_canonicalize_audit_event_for_persistence,
|
|
26
|
+
_epoch_now,
|
|
27
|
+
_is_successful_erase_event,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .._governance import _json_dumps, _row_to_audit_event
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from .._governance import _SQLiteGovernanceDeps
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuditEventStoreMixin:
|
|
37
|
+
"""SQLite audit-event store primitives."""
|
|
38
|
+
|
|
39
|
+
# Type hints for instance attributes/methods provided via MRO by the
|
|
40
|
+
# co-composed residual SQLiteGovernanceMixin / SQLiteStorageBase.
|
|
41
|
+
conn: sqlite3.Connection
|
|
42
|
+
_lock: threading.RLock
|
|
43
|
+
org_id: str
|
|
44
|
+
_deps: Callable[[], _SQLiteGovernanceDeps]
|
|
45
|
+
|
|
46
|
+
def _append_audit_event_with_cursor(
|
|
47
|
+
self, cur: sqlite3.Connection | sqlite3.Cursor, event: AuditEvent
|
|
48
|
+
) -> bool:
|
|
49
|
+
inserted = cur.execute(
|
|
50
|
+
"""INSERT OR IGNORE INTO audit_events (
|
|
51
|
+
org_id, actor_type, actor_ref, operation, entity_type, entity_id,
|
|
52
|
+
subject_ref, request_ref, idempotency_key, status, detail, created_at
|
|
53
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
54
|
+
(
|
|
55
|
+
event.org_id,
|
|
56
|
+
event.actor_type,
|
|
57
|
+
event.actor_ref,
|
|
58
|
+
event.operation,
|
|
59
|
+
event.entity_type,
|
|
60
|
+
event.entity_id,
|
|
61
|
+
event.subject_ref,
|
|
62
|
+
event.request_ref,
|
|
63
|
+
event.idempotency_key,
|
|
64
|
+
event.status,
|
|
65
|
+
_json_dumps(event.detail),
|
|
66
|
+
event.created_at,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
return inserted.rowcount > 0
|
|
70
|
+
|
|
71
|
+
def append_audit_event(self, event: AuditEvent) -> bool:
|
|
72
|
+
if _is_successful_erase_event(event):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"Successful ERASE audit rows may only be written by "
|
|
75
|
+
"complete_purge_operation_with_audit()"
|
|
76
|
+
)
|
|
77
|
+
if event.org_id != self.org_id:
|
|
78
|
+
raise ValueError("Audit event org_id must match storage org_id")
|
|
79
|
+
event = _canonicalize_audit_event_for_persistence(event)
|
|
80
|
+
with self._lock:
|
|
81
|
+
inserted = self._append_audit_event_with_cursor(self.conn, event)
|
|
82
|
+
self.conn.commit()
|
|
83
|
+
return inserted
|
|
84
|
+
|
|
85
|
+
def list_audit_events(
|
|
86
|
+
self, subject_ref: str | None = None, *, org_id: str | None = None
|
|
87
|
+
) -> list[AuditEvent]:
|
|
88
|
+
deps = self._deps()
|
|
89
|
+
if org_id is not None and org_id != self.org_id:
|
|
90
|
+
raise ValueError("Audit event org_id must match storage org_id")
|
|
91
|
+
sql = "SELECT * FROM audit_events WHERE org_id = ?"
|
|
92
|
+
params: list[Any] = [self.org_id]
|
|
93
|
+
if subject_ref is not None:
|
|
94
|
+
sql += " AND subject_ref = ?"
|
|
95
|
+
params.append(subject_ref)
|
|
96
|
+
sql += " ORDER BY created_at ASC, event_id ASC"
|
|
97
|
+
rows = deps._fetchall(sql, params)
|
|
98
|
+
return [_row_to_audit_event(row) for row in rows]
|
|
99
|
+
|
|
100
|
+
def gc_governance_retention(self, *, config: GovernanceRetentionConfig) -> int:
|
|
101
|
+
if not config.audit_events_retention_enabled:
|
|
102
|
+
return 0
|
|
103
|
+
cutoff_epoch = _epoch_now() - config.audit_events_retention_days * 24 * 60 * 60
|
|
104
|
+
with self._lock:
|
|
105
|
+
cur = self.conn.execute(
|
|
106
|
+
"""DELETE FROM audit_events
|
|
107
|
+
WHERE event_id IN (
|
|
108
|
+
SELECT event_id
|
|
109
|
+
FROM audit_events
|
|
110
|
+
WHERE org_id = ? AND created_at < ?
|
|
111
|
+
ORDER BY created_at ASC, event_id ASC
|
|
112
|
+
LIMIT ?
|
|
113
|
+
)""",
|
|
114
|
+
(
|
|
115
|
+
self.org_id,
|
|
116
|
+
cutoff_epoch,
|
|
117
|
+
config.audit_events_delete_batch_limit,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
deleted = int(cur.rowcount or 0)
|
|
121
|
+
self.conn.commit()
|
|
122
|
+
return deleted
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""SQLite governance-erase-execution store methods.
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``_governance.py`` (the GovernanceEraseExecution bucket):
|
|
4
|
+
the two public methods (``apply_governance_user_data_delete``,
|
|
5
|
+
``complete_purge_operation_with_audit``) plus the two EraseExecution-owned
|
|
6
|
+
privates (``_purge_governance_entity_content_locked``,
|
|
7
|
+
``_clear_user_data_for_governance_locked``).
|
|
8
|
+
|
|
9
|
+
``complete_purge_operation_with_audit`` is one of the two methods authorized to
|
|
10
|
+
write a successful-ERASE audit row — the ERASE-audit-idempotency block, the
|
|
11
|
+
re-read+identity-verify, and the ``status='complete'`` flip are preserved
|
|
12
|
+
byte-for-byte, committing together with the audit insert.
|
|
13
|
+
``_purge_governance_entity_content_locked`` emits the ``wasPurged`` lineage event
|
|
14
|
+
only on an actual content purge (``rowcount <= 0 -> return False`` before
|
|
15
|
+
``_append_event_stmt``); the ``_PURGE_SQL`` / ``_append_event_stmt`` symbols are
|
|
16
|
+
function-imported LOCAL-in-method (from ``.._lineage``) to avoid an import cycle.
|
|
17
|
+
|
|
18
|
+
The residual ``SQLiteGovernanceMixin`` stays composed alongside this mixin and
|
|
19
|
+
permanently holds the shared infra (``conn``, ``_lock``, ``org_id``,
|
|
20
|
+
``_deps()``), the delete-target / hide-for-rebuild / prepared-snapshot
|
|
21
|
+
validators it reaches cross-bucket
|
|
22
|
+
(``_validate_prepared_delete_target_matrix_locked``,
|
|
23
|
+
``_validate_hide_for_rebuild_targets_locked``,
|
|
24
|
+
``_prepared_owned_user_playbook_ids_locked``), and the module-level helpers
|
|
25
|
+
(``_row_to_audit_event``, ``_row_to_purge_operation``), which are imported here
|
|
26
|
+
rather than duplicated. ``_append_audit_event_with_cursor`` (AuditEventStore),
|
|
27
|
+
``get_purge_operation`` / ``_record_purge_target_locked`` (PurgeOperationStore)
|
|
28
|
+
resolve through the composed MRO.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import sqlite3
|
|
35
|
+
import threading
|
|
36
|
+
from collections.abc import Callable
|
|
37
|
+
from typing import TYPE_CHECKING, Literal
|
|
38
|
+
|
|
39
|
+
from reflexio.models.api_schema.domain.governance import (
|
|
40
|
+
AuditEvent,
|
|
41
|
+
PurgeOperation,
|
|
42
|
+
)
|
|
43
|
+
from reflexio.server.services.storage.governance_validation import (
|
|
44
|
+
_CANONICAL_DELETE_TARGET_NAMES,
|
|
45
|
+
_PREPARE_PHASE,
|
|
46
|
+
_SNAPSHOT_TARGET_NAME,
|
|
47
|
+
_canonicalize_audit_event_for_persistence,
|
|
48
|
+
_epoch_now,
|
|
49
|
+
_is_successful_erase_event,
|
|
50
|
+
_successful_erase_identity,
|
|
51
|
+
_validate_governance_purge_id,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
from .._governance import _row_to_audit_event, _row_to_purge_operation
|
|
55
|
+
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
from .._governance import _SQLiteGovernanceDeps
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GovernanceEraseExecutionMixin:
|
|
61
|
+
"""SQLite governance-erase-execution store primitives."""
|
|
62
|
+
|
|
63
|
+
# Type hints for instance attributes/methods provided via MRO by the
|
|
64
|
+
# co-composed residual SQLiteGovernanceMixin / SQLiteStorageBase.
|
|
65
|
+
conn: sqlite3.Connection
|
|
66
|
+
_lock: threading.RLock
|
|
67
|
+
org_id: str
|
|
68
|
+
_deps: Callable[[], _SQLiteGovernanceDeps]
|
|
69
|
+
|
|
70
|
+
# Cross-bucket residents reached via MRO: the delete-target / hide /
|
|
71
|
+
# prepared-snapshot validators live on the residual SQLiteGovernanceMixin.
|
|
72
|
+
_validate_prepared_delete_target_matrix_locked: Callable[[str], None]
|
|
73
|
+
_validate_hide_for_rebuild_targets_locked: Callable[[str], None]
|
|
74
|
+
_prepared_owned_user_playbook_ids_locked: Callable[[str], set[int]]
|
|
75
|
+
|
|
76
|
+
# Provided via MRO by the co-composed AuditEventStoreMixin (audit bucket)
|
|
77
|
+
# and PurgeOperationStoreMixin (purge bucket); reached here by the
|
|
78
|
+
# cross-bucket erase-execution / purge completion methods.
|
|
79
|
+
_append_audit_event_with_cursor: Callable[
|
|
80
|
+
[sqlite3.Connection | sqlite3.Cursor, AuditEvent], bool
|
|
81
|
+
]
|
|
82
|
+
get_purge_operation: Callable[[str], PurgeOperation]
|
|
83
|
+
_record_purge_target_locked: Callable[..., None]
|
|
84
|
+
|
|
85
|
+
def _purge_governance_entity_content_locked(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
entity_type: Literal["profile", "user_playbook"],
|
|
89
|
+
entity_id: str,
|
|
90
|
+
rowid: int,
|
|
91
|
+
) -> bool:
|
|
92
|
+
from .._lineage import _PURGE_SQL, _append_event_stmt
|
|
93
|
+
|
|
94
|
+
sql = _PURGE_SQL[entity_type]
|
|
95
|
+
cur = self.conn.execute(sql, (entity_id,))
|
|
96
|
+
if cur.rowcount <= 0:
|
|
97
|
+
return False
|
|
98
|
+
_append_event_stmt(
|
|
99
|
+
self.conn,
|
|
100
|
+
org_id=self.org_id,
|
|
101
|
+
entity_type=entity_type,
|
|
102
|
+
entity_id=entity_id,
|
|
103
|
+
op="purge",
|
|
104
|
+
prov="wasPurged",
|
|
105
|
+
source_ids=[],
|
|
106
|
+
actor="erasure",
|
|
107
|
+
request_id=f"purge_{entity_id}",
|
|
108
|
+
reason="content_purge",
|
|
109
|
+
)
|
|
110
|
+
if entity_type == "profile":
|
|
111
|
+
self.conn.execute(
|
|
112
|
+
"DELETE FROM profiles_fts WHERE profile_id = ?",
|
|
113
|
+
(entity_id,),
|
|
114
|
+
)
|
|
115
|
+
if self._deps()._has_sqlite_vec:
|
|
116
|
+
self.conn.execute(
|
|
117
|
+
"DELETE FROM profiles_vec WHERE rowid = ?",
|
|
118
|
+
(rowid,),
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
self.conn.execute(
|
|
122
|
+
"DELETE FROM user_playbooks_fts WHERE rowid = ?",
|
|
123
|
+
(rowid,),
|
|
124
|
+
)
|
|
125
|
+
if self._deps()._has_sqlite_vec:
|
|
126
|
+
self.conn.execute(
|
|
127
|
+
"DELETE FROM user_playbooks_vec WHERE rowid = ?",
|
|
128
|
+
(rowid,),
|
|
129
|
+
)
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def _clear_user_data_for_governance_locked(
|
|
133
|
+
self,
|
|
134
|
+
user_id: str,
|
|
135
|
+
*,
|
|
136
|
+
expected_user_playbook_ids: set[int] | None = None,
|
|
137
|
+
) -> dict[str, int]:
|
|
138
|
+
deps = self._deps()
|
|
139
|
+
interaction_ids = [
|
|
140
|
+
int(row["interaction_id"])
|
|
141
|
+
for row in self.conn.execute(
|
|
142
|
+
"SELECT interaction_id FROM interactions WHERE user_id = ?",
|
|
143
|
+
(user_id,),
|
|
144
|
+
).fetchall()
|
|
145
|
+
]
|
|
146
|
+
raw_upb_ids = [
|
|
147
|
+
int(row["user_playbook_id"])
|
|
148
|
+
for row in self.conn.execute(
|
|
149
|
+
"SELECT user_playbook_id FROM user_playbooks WHERE user_id = ?",
|
|
150
|
+
(user_id,),
|
|
151
|
+
).fetchall()
|
|
152
|
+
]
|
|
153
|
+
if (
|
|
154
|
+
expected_user_playbook_ids is not None
|
|
155
|
+
and set(raw_upb_ids) != expected_user_playbook_ids
|
|
156
|
+
):
|
|
157
|
+
raise ValueError(
|
|
158
|
+
"Current user playbooks no longer match prepared purge snapshot"
|
|
159
|
+
)
|
|
160
|
+
request_ids = [
|
|
161
|
+
str(row["request_id"])
|
|
162
|
+
for row in self.conn.execute(
|
|
163
|
+
"SELECT request_id FROM requests WHERE user_id = ?",
|
|
164
|
+
(user_id,),
|
|
165
|
+
).fetchall()
|
|
166
|
+
]
|
|
167
|
+
profile_rows = self.conn.execute(
|
|
168
|
+
"SELECT rowid, profile_id FROM profiles WHERE user_id = ?",
|
|
169
|
+
(user_id,),
|
|
170
|
+
).fetchall()
|
|
171
|
+
profile_rowid_by_id = {
|
|
172
|
+
str(row["profile_id"]): int(row["rowid"]) for row in profile_rows
|
|
173
|
+
}
|
|
174
|
+
all_profile_ids = list(profile_rowid_by_id)
|
|
175
|
+
|
|
176
|
+
purge_profile_ids, delete_profile_ids = deps._partition_purge_vs_delete(
|
|
177
|
+
"profile",
|
|
178
|
+
all_profile_ids,
|
|
179
|
+
)
|
|
180
|
+
purge_upb_str_ids, delete_upb_str_ids = deps._partition_purge_vs_delete(
|
|
181
|
+
"user_playbook",
|
|
182
|
+
[str(user_playbook_id) for user_playbook_id in raw_upb_ids],
|
|
183
|
+
)
|
|
184
|
+
purge_upb_ids = [int(entity_id) for entity_id in purge_upb_str_ids]
|
|
185
|
+
delete_upb_ids = [int(entity_id) for entity_id in delete_upb_str_ids]
|
|
186
|
+
erased_entity_ids = [
|
|
187
|
+
*request_ids,
|
|
188
|
+
*[str(interaction_id) for interaction_id in interaction_ids],
|
|
189
|
+
*all_profile_ids,
|
|
190
|
+
*[str(user_playbook_id) for user_playbook_id in raw_upb_ids],
|
|
191
|
+
]
|
|
192
|
+
if erased_entity_ids:
|
|
193
|
+
erased_entity_id_set = set(erased_entity_ids)
|
|
194
|
+
lineage_source_event_ids: list[int] = []
|
|
195
|
+
for row in self.conn.execute(
|
|
196
|
+
"SELECT event_id, source_ids FROM lineage_event WHERE org_id = ?",
|
|
197
|
+
(self.org_id,),
|
|
198
|
+
).fetchall():
|
|
199
|
+
try:
|
|
200
|
+
source_ids = json.loads(str(row["source_ids"] or "[]"))
|
|
201
|
+
except json.JSONDecodeError:
|
|
202
|
+
source_ids = []
|
|
203
|
+
if any(
|
|
204
|
+
str(source_id) in erased_entity_id_set for source_id in source_ids
|
|
205
|
+
):
|
|
206
|
+
lineage_source_event_ids.append(int(row["event_id"]))
|
|
207
|
+
deps._delete_in_chunks(
|
|
208
|
+
"lineage_event", "event_id", lineage_source_event_ids
|
|
209
|
+
)
|
|
210
|
+
placeholders = ",".join("?" for _ in erased_entity_ids)
|
|
211
|
+
self.conn.execute(
|
|
212
|
+
f"""DELETE FROM lineage_event
|
|
213
|
+
WHERE org_id = ?
|
|
214
|
+
AND (
|
|
215
|
+
request_id IN ({placeholders})
|
|
216
|
+
OR entity_id IN ({placeholders})
|
|
217
|
+
)""", # noqa: S608
|
|
218
|
+
[self.org_id, *erased_entity_ids, *erased_entity_ids],
|
|
219
|
+
)
|
|
220
|
+
delete_profile_rowids = [
|
|
221
|
+
profile_rowid_by_id[profile_id]
|
|
222
|
+
for profile_id in delete_profile_ids
|
|
223
|
+
if profile_id in profile_rowid_by_id
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
deps._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
|
|
227
|
+
deps._delete_in_chunks("user_playbooks_fts", "rowid", delete_upb_ids)
|
|
228
|
+
deps._delete_in_chunks("profiles_fts", "profile_id", delete_profile_ids)
|
|
229
|
+
if deps._has_sqlite_vec:
|
|
230
|
+
deps._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
|
|
231
|
+
deps._delete_in_chunks("user_playbooks_vec", "rowid", delete_upb_ids)
|
|
232
|
+
deps._delete_in_chunks("profiles_vec", "rowid", delete_profile_rowids)
|
|
233
|
+
|
|
234
|
+
interactions_cur = self.conn.execute(
|
|
235
|
+
"DELETE FROM interactions WHERE user_id = ?",
|
|
236
|
+
(user_id,),
|
|
237
|
+
)
|
|
238
|
+
eval_results_cur = self.conn.execute(
|
|
239
|
+
"""DELETE FROM agent_success_evaluation_result
|
|
240
|
+
WHERE user_id = ?""",
|
|
241
|
+
(user_id,),
|
|
242
|
+
)
|
|
243
|
+
requests_cur = self.conn.execute(
|
|
244
|
+
"DELETE FROM requests WHERE user_id = ?",
|
|
245
|
+
(user_id,),
|
|
246
|
+
)
|
|
247
|
+
if raw_upb_ids:
|
|
248
|
+
deps._delete_source_windows_for_user_playbook_ids(raw_upb_ids)
|
|
249
|
+
if delete_upb_ids:
|
|
250
|
+
deps._delete_in_chunks("user_playbooks", "user_playbook_id", delete_upb_ids)
|
|
251
|
+
if delete_profile_ids:
|
|
252
|
+
deps._delete_in_chunks("profiles", "profile_id", delete_profile_ids)
|
|
253
|
+
|
|
254
|
+
purged_profiles = 0
|
|
255
|
+
for profile_id in purge_profile_ids:
|
|
256
|
+
rowid = profile_rowid_by_id.get(profile_id)
|
|
257
|
+
if rowid is None:
|
|
258
|
+
continue
|
|
259
|
+
purged_profiles += int(
|
|
260
|
+
self._purge_governance_entity_content_locked(
|
|
261
|
+
entity_type="profile",
|
|
262
|
+
entity_id=profile_id,
|
|
263
|
+
rowid=rowid,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
purged_user_playbooks = 0
|
|
268
|
+
for user_playbook_id in purge_upb_ids:
|
|
269
|
+
purged_user_playbooks += int(
|
|
270
|
+
self._purge_governance_entity_content_locked(
|
|
271
|
+
entity_type="user_playbook",
|
|
272
|
+
entity_id=str(user_playbook_id),
|
|
273
|
+
rowid=user_playbook_id,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"interactions": interactions_cur.rowcount,
|
|
279
|
+
"user_playbooks": len(delete_upb_ids),
|
|
280
|
+
"profiles": len(delete_profile_ids),
|
|
281
|
+
"requests": requests_cur.rowcount,
|
|
282
|
+
"agent_success_evaluation_results": eval_results_cur.rowcount,
|
|
283
|
+
"purged_profiles": purged_profiles,
|
|
284
|
+
"purged_user_playbooks": purged_user_playbooks,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
def apply_governance_user_data_delete(
|
|
288
|
+
self, purge_id: str, user_id: str
|
|
289
|
+
) -> dict[str, int]:
|
|
290
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
291
|
+
name_map = {
|
|
292
|
+
"interactions": "interaction",
|
|
293
|
+
"user_playbooks": "user_playbook",
|
|
294
|
+
"profiles": "profile",
|
|
295
|
+
"requests": "request",
|
|
296
|
+
"agent_success_evaluation_results": "agent_success_evaluation_result",
|
|
297
|
+
"purged_profiles": "profile_purge",
|
|
298
|
+
"purged_user_playbooks": "user_playbook_purge",
|
|
299
|
+
}
|
|
300
|
+
with self._lock:
|
|
301
|
+
try:
|
|
302
|
+
self.conn.execute("BEGIN")
|
|
303
|
+
self._validate_prepared_delete_target_matrix_locked(purge_id)
|
|
304
|
+
self._validate_hide_for_rebuild_targets_locked(purge_id)
|
|
305
|
+
expected_user_playbook_ids = (
|
|
306
|
+
self._prepared_owned_user_playbook_ids_locked(purge_id)
|
|
307
|
+
)
|
|
308
|
+
counts = self._clear_user_data_for_governance_locked(
|
|
309
|
+
user_id,
|
|
310
|
+
expected_user_playbook_ids=expected_user_playbook_ids,
|
|
311
|
+
)
|
|
312
|
+
for key, value in counts.items():
|
|
313
|
+
self._record_purge_target_locked(
|
|
314
|
+
purge_id=purge_id,
|
|
315
|
+
target_name=name_map.get(key, key),
|
|
316
|
+
target_ref="all",
|
|
317
|
+
phase="delete",
|
|
318
|
+
status="complete",
|
|
319
|
+
detail={"count": int(value)},
|
|
320
|
+
deleted_count=int(value),
|
|
321
|
+
error_detail=None,
|
|
322
|
+
)
|
|
323
|
+
self.conn.commit()
|
|
324
|
+
except Exception:
|
|
325
|
+
self.conn.rollback()
|
|
326
|
+
raise
|
|
327
|
+
return counts
|
|
328
|
+
|
|
329
|
+
def complete_purge_operation_with_audit(
|
|
330
|
+
self, purge_id: str, audit_event: AuditEvent
|
|
331
|
+
) -> PurgeOperation:
|
|
332
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
333
|
+
if audit_event.org_id != self.org_id:
|
|
334
|
+
raise ValueError("Audit event org_id must match storage org_id")
|
|
335
|
+
if audit_event.idempotency_key != purge_id:
|
|
336
|
+
raise ValueError("Audit event idempotency key must match purge_id")
|
|
337
|
+
if not _is_successful_erase_event(audit_event, purge_id=purge_id):
|
|
338
|
+
raise ValueError(
|
|
339
|
+
"Completion requires a successful ERASE audit event for this purge"
|
|
340
|
+
)
|
|
341
|
+
audit_event = _canonicalize_audit_event_for_persistence(audit_event)
|
|
342
|
+
now = _epoch_now()
|
|
343
|
+
with self._lock:
|
|
344
|
+
try:
|
|
345
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
346
|
+
row = self.conn.execute(
|
|
347
|
+
"SELECT * FROM purge_operations WHERE purge_id = ? AND org_id = ?",
|
|
348
|
+
(purge_id, self.org_id),
|
|
349
|
+
).fetchone()
|
|
350
|
+
if row is None:
|
|
351
|
+
raise ValueError(f"Purge operation {purge_id!r} not found")
|
|
352
|
+
purge_operation = _row_to_purge_operation(row)
|
|
353
|
+
if purge_operation.subject_ref != audit_event.subject_ref:
|
|
354
|
+
raise ValueError(
|
|
355
|
+
"Audit event subject_ref must match purge operation subject_ref"
|
|
356
|
+
)
|
|
357
|
+
if purge_operation.request_ref != audit_event.request_ref:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
"Audit event request_ref must match purge operation request_ref"
|
|
360
|
+
)
|
|
361
|
+
barrier_row = self.conn.execute(
|
|
362
|
+
"""SELECT status FROM subject_write_barriers
|
|
363
|
+
WHERE org_id = ? AND subject_ref = ? AND purge_id = ?""",
|
|
364
|
+
(self.org_id, audit_event.subject_ref, purge_id),
|
|
365
|
+
).fetchone()
|
|
366
|
+
if barrier_row is None or barrier_row["status"] != "erasing":
|
|
367
|
+
raise ValueError("subject erasure barrier is missing")
|
|
368
|
+
snapshot = self.conn.execute(
|
|
369
|
+
"""SELECT 1 FROM purge_operation_targets
|
|
370
|
+
WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = 'all'
|
|
371
|
+
AND phase = ? AND status = 'complete'""",
|
|
372
|
+
(self.org_id, purge_id, _SNAPSHOT_TARGET_NAME, _PREPARE_PHASE),
|
|
373
|
+
).fetchone()
|
|
374
|
+
if snapshot is None:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
"Cannot complete purge without target snapshot marker"
|
|
377
|
+
)
|
|
378
|
+
delete_rows = self.conn.execute(
|
|
379
|
+
"""SELECT target_name, status FROM purge_operation_targets
|
|
380
|
+
WHERE org_id = ? AND purge_id = ? AND phase = 'delete'
|
|
381
|
+
AND target_ref = 'all'""",
|
|
382
|
+
(self.org_id, purge_id),
|
|
383
|
+
).fetchall()
|
|
384
|
+
delete_statuses = {
|
|
385
|
+
str(row["target_name"]): str(row["status"]) for row in delete_rows
|
|
386
|
+
}
|
|
387
|
+
missing_delete_targets = [
|
|
388
|
+
target_name
|
|
389
|
+
for target_name in _CANONICAL_DELETE_TARGET_NAMES
|
|
390
|
+
if delete_statuses.get(target_name) != "complete"
|
|
391
|
+
]
|
|
392
|
+
if missing_delete_targets:
|
|
393
|
+
raise ValueError(
|
|
394
|
+
"Cannot complete purge without complete delete target matrix: "
|
|
395
|
+
+ ", ".join(missing_delete_targets)
|
|
396
|
+
)
|
|
397
|
+
incomplete = self.conn.execute(
|
|
398
|
+
"""SELECT 1 FROM purge_operation_targets
|
|
399
|
+
WHERE org_id = ? AND purge_id = ? AND status != 'complete'
|
|
400
|
+
LIMIT 1""",
|
|
401
|
+
(self.org_id, purge_id),
|
|
402
|
+
).fetchone()
|
|
403
|
+
if incomplete is not None:
|
|
404
|
+
raise ValueError("Cannot complete purge with incomplete targets")
|
|
405
|
+
existing_audit_row = self.conn.execute(
|
|
406
|
+
"""SELECT * FROM audit_events
|
|
407
|
+
WHERE org_id = ? AND idempotency_key = ?""",
|
|
408
|
+
(self.org_id, purge_id),
|
|
409
|
+
).fetchone()
|
|
410
|
+
if existing_audit_row is not None:
|
|
411
|
+
existing_event = _row_to_audit_event(existing_audit_row)
|
|
412
|
+
if not _is_successful_erase_event(
|
|
413
|
+
existing_event, purge_id=purge_id
|
|
414
|
+
):
|
|
415
|
+
raise ValueError(
|
|
416
|
+
"Existing audit row for purge_id must be the matching "
|
|
417
|
+
"successful ERASE row"
|
|
418
|
+
)
|
|
419
|
+
if _successful_erase_identity(
|
|
420
|
+
existing_event
|
|
421
|
+
) != _successful_erase_identity(audit_event):
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"Existing audit row for purge_id must be the matching "
|
|
424
|
+
"successful ERASE row"
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
self._append_audit_event_with_cursor(self.conn, audit_event)
|
|
428
|
+
existing_audit_row = self.conn.execute(
|
|
429
|
+
"""SELECT * FROM audit_events
|
|
430
|
+
WHERE org_id = ? AND idempotency_key = ?""",
|
|
431
|
+
(self.org_id, purge_id),
|
|
432
|
+
).fetchone()
|
|
433
|
+
if existing_audit_row is None:
|
|
434
|
+
raise ValueError(
|
|
435
|
+
"Completion requires exactly one successful ERASE audit row "
|
|
436
|
+
"for the purge_id"
|
|
437
|
+
)
|
|
438
|
+
existing_event = _row_to_audit_event(existing_audit_row)
|
|
439
|
+
if not _is_successful_erase_event(existing_event, purge_id=purge_id):
|
|
440
|
+
raise ValueError(
|
|
441
|
+
"Completion requires exactly one matching successful ERASE "
|
|
442
|
+
"audit row for the purge_id"
|
|
443
|
+
)
|
|
444
|
+
if _successful_erase_identity(
|
|
445
|
+
existing_event
|
|
446
|
+
) != _successful_erase_identity(audit_event):
|
|
447
|
+
raise ValueError(
|
|
448
|
+
"Completion requires exactly one matching successful ERASE "
|
|
449
|
+
"audit row for the purge_id"
|
|
450
|
+
)
|
|
451
|
+
self.conn.execute(
|
|
452
|
+
"""UPDATE purge_operations
|
|
453
|
+
SET status = 'complete',
|
|
454
|
+
error_code = NULL,
|
|
455
|
+
error_detail = NULL,
|
|
456
|
+
updated_at = ?,
|
|
457
|
+
completed_at = ?
|
|
458
|
+
WHERE purge_id = ? AND org_id = ?""",
|
|
459
|
+
(now, now, purge_id, self.org_id),
|
|
460
|
+
)
|
|
461
|
+
self.conn.commit()
|
|
462
|
+
except Exception:
|
|
463
|
+
self.conn.rollback()
|
|
464
|
+
raise
|
|
465
|
+
return self.get_purge_operation(purge_id)
|