claude-smart 0.2.47 → 0.2.48
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 +1 -1
- 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/pyproject.toml +1 -1
- package/plugin/uv.lock +1 -1
- 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/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/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 +133 -3273
- 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 +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- 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 +259 -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/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- 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/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +29 -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 +56 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +6 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- 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 +132 -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 +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +13 -7
- 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 +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -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 +33 -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/_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 +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +44 -86
- 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/_purge.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""SQLite purge-operation store methods.
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``_governance.py`` (the PurgeOperationStore bucket): the
|
|
4
|
+
seven public methods (``begin_purge_operation``, ``record_purge_target``,
|
|
5
|
+
``list_purge_targets``, ``purge_targets_prepared``,
|
|
6
|
+
``prepare_governance_erase_targets``, ``fail_purge_operation``,
|
|
7
|
+
``get_purge_operation``) plus the Purge-owned private
|
|
8
|
+
``_record_purge_target_locked`` (called cross-bucket by the residual rebuild-hide
|
|
9
|
+
/ governance-erase-execution methods, which reach it via MRO co-composition).
|
|
10
|
+
|
|
11
|
+
The residual ``SQLiteGovernanceMixin`` stays composed alongside this mixin and
|
|
12
|
+
permanently holds the shared infra (``conn``, ``_lock``, ``org_id``,
|
|
13
|
+
``_deps()``), the cross-bucket residents it calls
|
|
14
|
+
(``_owned_user_playbook_ids_locked``, ``_planned_governance_delete_counts``),
|
|
15
|
+
and the module-level helpers (``_json_dumps``, ``_row_to_purge_operation``,
|
|
16
|
+
``_row_to_purge_target``), which are imported here rather than duplicated.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import sqlite3
|
|
22
|
+
import threading
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
25
|
+
|
|
26
|
+
from reflexio.models.api_schema.domain.governance import (
|
|
27
|
+
PurgeOperation,
|
|
28
|
+
PurgeOperationTarget,
|
|
29
|
+
)
|
|
30
|
+
from reflexio.server.services.storage.governance_validation import (
|
|
31
|
+
_ALLOWED_PURGE_OPERATION_TYPES,
|
|
32
|
+
_ALLOWED_PURGE_SCOPE_TYPES,
|
|
33
|
+
_ALLOWED_PURGE_TARGET_DETAIL_KEYS,
|
|
34
|
+
_ALLOWED_PURGE_TARGET_NAMES,
|
|
35
|
+
_ALLOWED_PURGE_TARGET_PHASES,
|
|
36
|
+
_ALLOWED_PURGE_TARGET_STATUSES,
|
|
37
|
+
_PREPARE_PHASE,
|
|
38
|
+
_SNAPSHOT_TARGET_NAME,
|
|
39
|
+
_epoch_now,
|
|
40
|
+
_validate_governance_deleted_count,
|
|
41
|
+
_validate_governance_detail,
|
|
42
|
+
_validate_governance_enum,
|
|
43
|
+
_validate_governance_error_code,
|
|
44
|
+
_validate_governance_error_detail,
|
|
45
|
+
_validate_governance_idempotency_key,
|
|
46
|
+
_validate_governance_prefixed_ref,
|
|
47
|
+
_validate_governance_purge_id,
|
|
48
|
+
_validate_governance_target_ref,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from .._governance import _json_dumps, _row_to_purge_operation, _row_to_purge_target
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from .._governance import _SQLiteGovernanceDeps
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PurgeOperationStoreMixin:
|
|
58
|
+
"""SQLite purge-operation store primitives."""
|
|
59
|
+
|
|
60
|
+
# Type hints for instance attributes/methods provided via MRO by the
|
|
61
|
+
# co-composed residual SQLiteGovernanceMixin / SQLiteStorageBase.
|
|
62
|
+
conn: sqlite3.Connection
|
|
63
|
+
_lock: threading.RLock
|
|
64
|
+
org_id: str
|
|
65
|
+
_deps: Callable[[], _SQLiteGovernanceDeps]
|
|
66
|
+
_owned_user_playbook_ids_locked: Callable[[str], set[int]]
|
|
67
|
+
_planned_governance_delete_counts: Callable[[str, set[int]], dict[str, int]]
|
|
68
|
+
|
|
69
|
+
def _record_purge_target_locked(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
purge_id: str,
|
|
73
|
+
target_name: str,
|
|
74
|
+
target_ref: str,
|
|
75
|
+
phase: str,
|
|
76
|
+
status: Literal["pending", "running", "failed", "complete"],
|
|
77
|
+
detail: dict[str, object] | None,
|
|
78
|
+
deleted_count: int,
|
|
79
|
+
error_detail: str | None,
|
|
80
|
+
) -> None:
|
|
81
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
82
|
+
_validate_governance_enum(
|
|
83
|
+
"target_name",
|
|
84
|
+
target_name,
|
|
85
|
+
allowed=_ALLOWED_PURGE_TARGET_NAMES,
|
|
86
|
+
)
|
|
87
|
+
_validate_governance_enum(
|
|
88
|
+
"phase",
|
|
89
|
+
phase,
|
|
90
|
+
allowed=_ALLOWED_PURGE_TARGET_PHASES,
|
|
91
|
+
)
|
|
92
|
+
_validate_governance_enum(
|
|
93
|
+
"status",
|
|
94
|
+
status,
|
|
95
|
+
allowed=_ALLOWED_PURGE_TARGET_STATUSES,
|
|
96
|
+
)
|
|
97
|
+
detail = _validate_governance_detail(
|
|
98
|
+
"detail",
|
|
99
|
+
detail,
|
|
100
|
+
allowed_keys=_ALLOWED_PURGE_TARGET_DETAIL_KEYS,
|
|
101
|
+
)
|
|
102
|
+
error_detail = _validate_governance_error_detail(error_detail)
|
|
103
|
+
target_ref = _validate_governance_target_ref(
|
|
104
|
+
target_name=target_name,
|
|
105
|
+
phase=phase,
|
|
106
|
+
target_ref=target_ref,
|
|
107
|
+
)
|
|
108
|
+
deleted_count = _validate_governance_deleted_count(deleted_count)
|
|
109
|
+
now = _epoch_now()
|
|
110
|
+
existing = self.conn.execute(
|
|
111
|
+
"""SELECT started_at, completed_at
|
|
112
|
+
FROM purge_operation_targets
|
|
113
|
+
WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = ? AND phase = ?""",
|
|
114
|
+
(self.org_id, purge_id, target_name, target_ref, phase),
|
|
115
|
+
).fetchone()
|
|
116
|
+
started_at = existing["started_at"] if existing else None
|
|
117
|
+
completed_at = existing["completed_at"] if existing else None
|
|
118
|
+
if started_at is None and status in {"running", "failed", "complete"}:
|
|
119
|
+
started_at = now
|
|
120
|
+
if status in {"failed", "complete"}:
|
|
121
|
+
completed_at = now
|
|
122
|
+
self.conn.execute(
|
|
123
|
+
"""INSERT INTO purge_operation_targets (
|
|
124
|
+
org_id, purge_id, target_name, target_ref, phase, status, detail,
|
|
125
|
+
deleted_count, error_detail, started_at, completed_at
|
|
126
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
127
|
+
ON CONFLICT(org_id, purge_id, target_name, target_ref, phase) DO UPDATE SET
|
|
128
|
+
status = excluded.status,
|
|
129
|
+
detail = COALESCE(excluded.detail, purge_operation_targets.detail),
|
|
130
|
+
deleted_count = excluded.deleted_count,
|
|
131
|
+
error_detail = excluded.error_detail,
|
|
132
|
+
started_at = COALESCE(purge_operation_targets.started_at, excluded.started_at),
|
|
133
|
+
completed_at = excluded.completed_at""",
|
|
134
|
+
(
|
|
135
|
+
self.org_id,
|
|
136
|
+
purge_id,
|
|
137
|
+
target_name,
|
|
138
|
+
target_ref,
|
|
139
|
+
phase,
|
|
140
|
+
status,
|
|
141
|
+
_json_dumps(detail),
|
|
142
|
+
deleted_count,
|
|
143
|
+
error_detail,
|
|
144
|
+
started_at,
|
|
145
|
+
completed_at,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
self.conn.execute(
|
|
149
|
+
"""UPDATE purge_operations
|
|
150
|
+
SET status = CASE
|
|
151
|
+
WHEN status IN ('complete', 'failed') THEN status
|
|
152
|
+
WHEN ? IN ('running', 'complete') THEN 'running'
|
|
153
|
+
ELSE status
|
|
154
|
+
END,
|
|
155
|
+
updated_at = ?
|
|
156
|
+
WHERE purge_id = ? AND org_id = ?""",
|
|
157
|
+
(status, now, purge_id, self.org_id),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def begin_purge_operation(
|
|
161
|
+
self,
|
|
162
|
+
purge_id: str,
|
|
163
|
+
idempotency_key: str,
|
|
164
|
+
operation_type: Literal["user_erasure", "org_purge"],
|
|
165
|
+
scope_type: Literal["user", "org"],
|
|
166
|
+
subject_ref: str | None,
|
|
167
|
+
request_ref: str,
|
|
168
|
+
) -> PurgeOperation:
|
|
169
|
+
_validate_governance_enum(
|
|
170
|
+
"operation_type",
|
|
171
|
+
operation_type,
|
|
172
|
+
allowed=_ALLOWED_PURGE_OPERATION_TYPES,
|
|
173
|
+
)
|
|
174
|
+
_validate_governance_enum(
|
|
175
|
+
"scope_type",
|
|
176
|
+
scope_type,
|
|
177
|
+
allowed=_ALLOWED_PURGE_SCOPE_TYPES,
|
|
178
|
+
)
|
|
179
|
+
_validate_governance_prefixed_ref(
|
|
180
|
+
"subject_ref", subject_ref, prefix="subref_v1_"
|
|
181
|
+
)
|
|
182
|
+
_validate_governance_prefixed_ref(
|
|
183
|
+
"request_ref", request_ref, prefix="reqref_v1_"
|
|
184
|
+
)
|
|
185
|
+
validated_purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
186
|
+
validated_idempotency_key = cast(
|
|
187
|
+
str,
|
|
188
|
+
_validate_governance_idempotency_key("idempotency_key", idempotency_key),
|
|
189
|
+
)
|
|
190
|
+
now = _epoch_now()
|
|
191
|
+
with self._lock:
|
|
192
|
+
existing = self.conn.execute(
|
|
193
|
+
"""SELECT * FROM purge_operations
|
|
194
|
+
WHERE org_id = ? AND idempotency_key = ?""",
|
|
195
|
+
(self.org_id, validated_idempotency_key),
|
|
196
|
+
).fetchone()
|
|
197
|
+
if existing is not None:
|
|
198
|
+
existing_operation = _row_to_purge_operation(existing)
|
|
199
|
+
expected_identity = {
|
|
200
|
+
"purge_id": validated_purge_id,
|
|
201
|
+
"operation_type": operation_type,
|
|
202
|
+
"scope_type": scope_type,
|
|
203
|
+
"subject_ref": subject_ref,
|
|
204
|
+
"request_ref": request_ref,
|
|
205
|
+
}
|
|
206
|
+
for field_name, expected_value in expected_identity.items():
|
|
207
|
+
if getattr(existing_operation, field_name) != expected_value:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
"Existing purge operation for idempotency_key has "
|
|
210
|
+
f"mismatched {field_name}"
|
|
211
|
+
)
|
|
212
|
+
return _row_to_purge_operation(existing)
|
|
213
|
+
self.conn.execute(
|
|
214
|
+
"""INSERT INTO purge_operations (
|
|
215
|
+
purge_id, org_id, operation_type, scope_type, subject_ref,
|
|
216
|
+
request_ref, idempotency_key, status, created_at, updated_at
|
|
217
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)""",
|
|
218
|
+
(
|
|
219
|
+
validated_purge_id,
|
|
220
|
+
self.org_id,
|
|
221
|
+
operation_type,
|
|
222
|
+
scope_type,
|
|
223
|
+
subject_ref,
|
|
224
|
+
request_ref,
|
|
225
|
+
validated_idempotency_key,
|
|
226
|
+
now,
|
|
227
|
+
now,
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
self.conn.commit()
|
|
231
|
+
return self.get_purge_operation(validated_purge_id)
|
|
232
|
+
|
|
233
|
+
def record_purge_target(
|
|
234
|
+
self,
|
|
235
|
+
purge_id: str,
|
|
236
|
+
target_name: str,
|
|
237
|
+
phase: str,
|
|
238
|
+
status: Literal["pending", "running", "failed", "complete"],
|
|
239
|
+
target_ref: str = "",
|
|
240
|
+
detail: dict[str, object] | None = None,
|
|
241
|
+
deleted_count: int = 0,
|
|
242
|
+
error_detail: str | None = None,
|
|
243
|
+
) -> None:
|
|
244
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
245
|
+
_validate_governance_enum(
|
|
246
|
+
"target_name",
|
|
247
|
+
target_name,
|
|
248
|
+
allowed=_ALLOWED_PURGE_TARGET_NAMES,
|
|
249
|
+
)
|
|
250
|
+
_validate_governance_enum(
|
|
251
|
+
"phase",
|
|
252
|
+
phase,
|
|
253
|
+
allowed=_ALLOWED_PURGE_TARGET_PHASES,
|
|
254
|
+
)
|
|
255
|
+
_validate_governance_enum(
|
|
256
|
+
"status",
|
|
257
|
+
status,
|
|
258
|
+
allowed=_ALLOWED_PURGE_TARGET_STATUSES,
|
|
259
|
+
)
|
|
260
|
+
with self._lock:
|
|
261
|
+
self._record_purge_target_locked(
|
|
262
|
+
purge_id=purge_id,
|
|
263
|
+
target_name=target_name,
|
|
264
|
+
target_ref=target_ref,
|
|
265
|
+
phase=phase,
|
|
266
|
+
status=status,
|
|
267
|
+
detail=detail,
|
|
268
|
+
deleted_count=deleted_count,
|
|
269
|
+
error_detail=error_detail,
|
|
270
|
+
)
|
|
271
|
+
self.conn.commit()
|
|
272
|
+
|
|
273
|
+
def list_purge_targets(
|
|
274
|
+
self, purge_id: str, phase: str | None = None
|
|
275
|
+
) -> list[PurgeOperationTarget]:
|
|
276
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
277
|
+
deps = self._deps()
|
|
278
|
+
sql = "SELECT * FROM purge_operation_targets WHERE org_id = ? AND purge_id = ?"
|
|
279
|
+
params: list[Any] = [self.org_id, purge_id]
|
|
280
|
+
if phase is not None:
|
|
281
|
+
sql += " AND phase = ?"
|
|
282
|
+
params.append(phase)
|
|
283
|
+
sql += " ORDER BY phase ASC, target_name ASC, target_ref ASC"
|
|
284
|
+
rows = deps._fetchall(sql, params)
|
|
285
|
+
return [_row_to_purge_target(row) for row in rows]
|
|
286
|
+
|
|
287
|
+
def purge_targets_prepared(self, purge_id: str) -> bool:
|
|
288
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
289
|
+
row = self._deps()._fetchone(
|
|
290
|
+
"""SELECT 1 FROM purge_operation_targets
|
|
291
|
+
WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = 'all'
|
|
292
|
+
AND phase = ? AND status = 'complete'""",
|
|
293
|
+
(self.org_id, purge_id, _SNAPSHOT_TARGET_NAME, _PREPARE_PHASE),
|
|
294
|
+
)
|
|
295
|
+
return row is not None
|
|
296
|
+
|
|
297
|
+
def prepare_governance_erase_targets(
|
|
298
|
+
self,
|
|
299
|
+
purge_id: str,
|
|
300
|
+
user_id: str,
|
|
301
|
+
owned_user_playbook_ids: set[int] | None = None,
|
|
302
|
+
) -> None:
|
|
303
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
304
|
+
with self._lock:
|
|
305
|
+
if self.purge_targets_prepared(purge_id):
|
|
306
|
+
return
|
|
307
|
+
try:
|
|
308
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
309
|
+
owned_user_playbook_ids = (
|
|
310
|
+
set(owned_user_playbook_ids)
|
|
311
|
+
if owned_user_playbook_ids is not None
|
|
312
|
+
else self._owned_user_playbook_ids_locked(user_id)
|
|
313
|
+
)
|
|
314
|
+
targets = self._planned_governance_delete_counts(
|
|
315
|
+
user_id,
|
|
316
|
+
owned_user_playbook_ids,
|
|
317
|
+
)
|
|
318
|
+
for target_name, count in targets.items():
|
|
319
|
+
self._record_purge_target_locked(
|
|
320
|
+
purge_id=purge_id,
|
|
321
|
+
target_name=target_name,
|
|
322
|
+
target_ref="all",
|
|
323
|
+
phase="delete",
|
|
324
|
+
status="pending",
|
|
325
|
+
detail={"count": count},
|
|
326
|
+
deleted_count=0,
|
|
327
|
+
error_detail=None,
|
|
328
|
+
)
|
|
329
|
+
self._record_purge_target_locked(
|
|
330
|
+
purge_id=purge_id,
|
|
331
|
+
target_name=_SNAPSHOT_TARGET_NAME,
|
|
332
|
+
target_ref="all",
|
|
333
|
+
phase=_PREPARE_PHASE,
|
|
334
|
+
status="complete",
|
|
335
|
+
detail={
|
|
336
|
+
"owned_user_playbook_ids": sorted(owned_user_playbook_ids),
|
|
337
|
+
},
|
|
338
|
+
deleted_count=0,
|
|
339
|
+
error_detail=None,
|
|
340
|
+
)
|
|
341
|
+
self.conn.commit()
|
|
342
|
+
except Exception:
|
|
343
|
+
self.conn.rollback()
|
|
344
|
+
raise
|
|
345
|
+
|
|
346
|
+
def fail_purge_operation(
|
|
347
|
+
self, purge_id: str, error_code: str, error_detail: str
|
|
348
|
+
) -> PurgeOperation:
|
|
349
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
350
|
+
validated_error_code = _validate_governance_error_code(error_code)
|
|
351
|
+
validated_error_detail = _validate_governance_error_detail(error_detail)
|
|
352
|
+
now = _epoch_now()
|
|
353
|
+
with self._lock:
|
|
354
|
+
cur = self.conn.execute(
|
|
355
|
+
"""UPDATE purge_operations
|
|
356
|
+
SET status = 'failed', error_code = ?, error_detail = ?,
|
|
357
|
+
updated_at = ?, completed_at = ?
|
|
358
|
+
WHERE purge_id = ? AND org_id = ? AND status != 'complete'""",
|
|
359
|
+
(
|
|
360
|
+
validated_error_code,
|
|
361
|
+
validated_error_detail,
|
|
362
|
+
now,
|
|
363
|
+
now,
|
|
364
|
+
purge_id,
|
|
365
|
+
self.org_id,
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
if cur.rowcount == 0:
|
|
369
|
+
existing = self.conn.execute(
|
|
370
|
+
"SELECT status FROM purge_operations WHERE purge_id = ? AND org_id = ?",
|
|
371
|
+
(purge_id, self.org_id),
|
|
372
|
+
).fetchone()
|
|
373
|
+
if existing is not None and str(existing["status"]) == "complete":
|
|
374
|
+
raise ValueError("Purge operation is already complete")
|
|
375
|
+
raise ValueError(f"Purge operation {purge_id!r} not found")
|
|
376
|
+
self.conn.commit()
|
|
377
|
+
return self.get_purge_operation(purge_id)
|
|
378
|
+
|
|
379
|
+
def get_purge_operation(self, purge_id: str) -> PurgeOperation:
|
|
380
|
+
purge_id = _validate_governance_purge_id("purge_id", purge_id)
|
|
381
|
+
row = self._deps()._fetchone(
|
|
382
|
+
"SELECT * FROM purge_operations WHERE purge_id = ? AND org_id = ?",
|
|
383
|
+
(purge_id, self.org_id),
|
|
384
|
+
)
|
|
385
|
+
if row is None:
|
|
386
|
+
raise ValueError(f"Purge operation {purge_id!r} not found")
|
|
387
|
+
return _row_to_purge_operation(row)
|