claude-smart 0.2.46 → 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.
Files changed (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,378 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import suppress
4
+ from typing import Any, Literal, TypedDict
5
+
6
+ from reflexio.models.api_schema.domain.governance import (
7
+ AuditEvent,
8
+ PurgeOperationTarget,
9
+ SubjectWriteBarrier,
10
+ UserEraseResult,
11
+ UserExportResult,
12
+ )
13
+ from reflexio.server.services.governance.config import (
14
+ get_governance_ref_secret,
15
+ governance_request_ref,
16
+ governance_subject_ref,
17
+ )
18
+ from reflexio.server.services.governance.subject_refs import stable_id
19
+
20
+ _DELETE_TARGET_NAME_TO_RESULT_KEY = {
21
+ "interaction": "interactions",
22
+ "user_playbook": "user_playbooks",
23
+ "profile": "profiles",
24
+ "request": "requests",
25
+ "agent_success_evaluation_result": "agent_success_evaluation_results",
26
+ "profile_purge": "purged_profiles",
27
+ "user_playbook_purge": "purged_user_playbooks",
28
+ }
29
+ _REQUIRED_DELETE_TARGET_NAMES = tuple(_DELETE_TARGET_NAME_TO_RESULT_KEY)
30
+ _USER_PLAYBOOK_PAGE_SIZE = 1000
31
+
32
+
33
+ class GovernanceActorContext(TypedDict):
34
+ actor_type: Literal["api_token", "jwt", "system"]
35
+ actor_ref: str | None
36
+
37
+
38
+ class GovernanceService:
39
+ def __init__(self, *, storage: Any, org_id: str, ref_secret: str) -> None:
40
+ self.storage = storage
41
+ self.org_id = org_id
42
+ self.ref_secret = ref_secret
43
+
44
+ def export_user(
45
+ self,
46
+ *,
47
+ user_id: str,
48
+ request_id: str,
49
+ actor_context: GovernanceActorContext | None = None,
50
+ ) -> UserExportResult:
51
+ self._assert_storage_ref_secret_matches()
52
+ subref = governance_subject_ref(self.org_id, user_id, self.ref_secret)
53
+ reqref = governance_request_ref(self.org_id, request_id, self.ref_secret)
54
+ export_id = stable_id("export", f"{self.org_id}:export:{subref}:{reqref}")
55
+ actor_type = actor_context["actor_type"] if actor_context else "system"
56
+ actor_ref = actor_context["actor_ref"] if actor_context else None
57
+ requests, sessions = self._load_user_requests_and_sessions(user_id)
58
+ bundle: dict[str, Any] = {
59
+ "profiles": [
60
+ profile.model_dump()
61
+ for profile in self.storage.get_user_profile(user_id)
62
+ ],
63
+ "interactions": [
64
+ interaction.model_dump()
65
+ for interaction in self.storage.get_user_interaction(user_id)
66
+ ],
67
+ "requests": [request.model_dump() for request in requests],
68
+ "sessions": sessions,
69
+ "user_playbooks": [
70
+ playbook.model_dump() for playbook in self._iter_user_playbooks(user_id)
71
+ ],
72
+ }
73
+ self.storage.append_audit_event(
74
+ AuditEvent(
75
+ org_id=self.org_id,
76
+ actor_type=actor_type,
77
+ actor_ref=actor_ref,
78
+ operation="EXPORT",
79
+ entity_type="request",
80
+ subject_ref=subref,
81
+ request_ref=reqref,
82
+ idempotency_key=export_id,
83
+ detail={"count": sum(len(items) for items in bundle.values())},
84
+ )
85
+ )
86
+ return UserExportResult(subject_ref=subref, export_id=export_id, bundle=bundle)
87
+
88
+ def erase_user(
89
+ self,
90
+ *,
91
+ user_id: str,
92
+ request_id: str,
93
+ actor_context: GovernanceActorContext | None = None,
94
+ ) -> UserEraseResult:
95
+ self._assert_storage_ref_secret_matches()
96
+ subref = governance_subject_ref(self.org_id, user_id, self.ref_secret)
97
+ reqref = governance_request_ref(self.org_id, request_id, self.ref_secret)
98
+ actor_type = actor_context["actor_type"] if actor_context else "system"
99
+ actor_ref = actor_context["actor_ref"] if actor_context else None
100
+ idempotency_key = stable_id(
101
+ "idem",
102
+ f"{self.org_id}:user_erasure:{subref}:{reqref}",
103
+ )
104
+ purge_id = stable_id("purge", idempotency_key)
105
+ purge = self.storage.begin_purge_operation(
106
+ purge_id=purge_id,
107
+ idempotency_key=idempotency_key,
108
+ operation_type="user_erasure",
109
+ scope_type="user",
110
+ subject_ref=subref,
111
+ request_ref=reqref,
112
+ )
113
+ if purge.status == "complete":
114
+ barrier = self._completed_barrier_for_retry(
115
+ subject_ref=subref, purge_id=purge_id
116
+ )
117
+ if barrier.status != "erased":
118
+ raise ValueError(
119
+ "Completed purge retry requires an erased subject barrier"
120
+ )
121
+ return UserEraseResult(
122
+ subject_ref=subref,
123
+ purge_id=purge_id,
124
+ status="complete",
125
+ deleted_counts=self._deleted_counts_from_targets(purge_id),
126
+ rebuilt_agent_playbook_ids=(
127
+ self._rebuilt_agent_playbook_ids_from_targets(purge_id)
128
+ ),
129
+ )
130
+ try:
131
+ self.storage.begin_subject_erasure_barrier(subref, purge_id)
132
+ if not self.storage.purge_targets_prepared(purge_id):
133
+ self.storage.prepare_governance_erase_targets(
134
+ purge_id,
135
+ user_id,
136
+ )
137
+
138
+ if not self._delete_targets_complete(purge_id):
139
+ self.storage.apply_governance_user_data_delete(purge_id, user_id)
140
+ deleted_counts = self._deleted_counts_from_targets(purge_id)
141
+
142
+ rebuilt_agent_playbook_ids: list[int] = []
143
+ completed = self.storage.complete_subject_erasure_barrier_after_empty_check(
144
+ purge_id,
145
+ AuditEvent(
146
+ org_id=self.org_id,
147
+ actor_type=actor_type,
148
+ actor_ref=actor_ref,
149
+ operation="ERASE",
150
+ entity_type="request",
151
+ subject_ref=subref,
152
+ request_ref=reqref,
153
+ idempotency_key=purge_id,
154
+ detail={
155
+ "deleted_counts": deleted_counts,
156
+ "rebuilt_agent_playbook_ids": rebuilt_agent_playbook_ids,
157
+ },
158
+ ),
159
+ )
160
+ except Exception as exc:
161
+ with suppress(Exception):
162
+ self.storage.fail_subject_erasure_barrier(
163
+ subref,
164
+ purge_id,
165
+ error_code="governance_erase_failed",
166
+ error_detail=type(exc).__name__,
167
+ )
168
+ with suppress(Exception):
169
+ self.storage.fail_purge_operation(
170
+ purge_id,
171
+ error_code="governance_erase_failed",
172
+ error_detail=type(exc).__name__,
173
+ )
174
+ raise
175
+ return UserEraseResult(
176
+ subject_ref=subref,
177
+ purge_id=purge_id,
178
+ status=completed.status,
179
+ deleted_counts=deleted_counts,
180
+ rebuilt_agent_playbook_ids=rebuilt_agent_playbook_ids,
181
+ )
182
+
183
+ def _assert_storage_ref_secret_matches(self) -> None:
184
+ storage_secret = get_governance_ref_secret()
185
+ if storage_secret != self.ref_secret:
186
+ raise RuntimeError(
187
+ "GovernanceService ref_secret must match REFLEXIO_GOVERNANCE_REF_SECRET "
188
+ "for governance operations"
189
+ )
190
+
191
+ def _completed_barrier_for_retry(
192
+ self, *, subject_ref: str, purge_id: str
193
+ ) -> SubjectWriteBarrier:
194
+ barrier = self.storage.get_subject_write_barrier(subject_ref)
195
+ if barrier is None or barrier.purge_id != purge_id:
196
+ raise ValueError(
197
+ "Completed purge retry requires the matching subject barrier"
198
+ )
199
+ return barrier
200
+
201
+ def _load_user_requests_and_sessions(
202
+ self, user_id: str
203
+ ) -> tuple[list[Any], list[dict[str, Any]]]:
204
+ requests: list[Any] = []
205
+ sessions_by_id: dict[str, list[str]] = {}
206
+ offset = 0
207
+ page_size = 1000
208
+
209
+ while True:
210
+ grouped_sessions = self.storage.get_sessions(
211
+ user_id=user_id,
212
+ top_k=page_size,
213
+ offset=offset,
214
+ )
215
+ returned_rows = 0
216
+ for session_id, rows in grouped_sessions.items():
217
+ returned_rows += len(rows)
218
+ request_ids = sessions_by_id.setdefault(session_id, [])
219
+ for row in rows:
220
+ if row.request is None:
221
+ continue
222
+ requests.append(row.request)
223
+ request_ids.append(row.request.request_id)
224
+ if returned_rows < page_size:
225
+ break
226
+ offset += page_size
227
+
228
+ sessions = [
229
+ {"session_id": session_id, "request_ids": request_ids}
230
+ for session_id, request_ids in sessions_by_id.items()
231
+ ]
232
+ return requests, sessions
233
+
234
+ def _iter_user_playbooks(self, user_id: str) -> list[Any]:
235
+ playbooks: list[Any] = []
236
+ offset = 0
237
+ while True:
238
+ page = self.storage.get_user_playbooks(
239
+ user_id=user_id,
240
+ limit=_USER_PLAYBOOK_PAGE_SIZE,
241
+ offset=offset,
242
+ )
243
+ playbooks.extend(page)
244
+ if len(page) < _USER_PLAYBOOK_PAGE_SIZE:
245
+ break
246
+ offset += _USER_PLAYBOOK_PAGE_SIZE
247
+ return playbooks
248
+
249
+ def _delete_targets_complete(self, purge_id: str) -> bool:
250
+ delete_targets = {
251
+ target.target_name: target
252
+ for target in self.storage.list_purge_targets(purge_id, phase="delete")
253
+ }
254
+ return all(
255
+ delete_targets.get(target_name) is not None
256
+ and delete_targets[target_name].status == "complete"
257
+ for target_name in _REQUIRED_DELETE_TARGET_NAMES
258
+ )
259
+
260
+ def _deleted_counts_from_targets(self, purge_id: str) -> dict[str, int]:
261
+ counts: dict[str, int] = {}
262
+ for target in self.storage.list_purge_targets(purge_id, phase="delete"):
263
+ result_key = _DELETE_TARGET_NAME_TO_RESULT_KEY.get(target.target_name)
264
+ if result_key is None:
265
+ continue
266
+ counts[result_key] = int(target.deleted_count)
267
+ return counts
268
+
269
+ def _rebuilt_agent_playbook_ids_from_targets(self, purge_id: str) -> list[int]:
270
+ return [
271
+ int(target.target_ref)
272
+ for target in self.storage.list_purge_targets(
273
+ purge_id,
274
+ phase="rebuild_without_erased_sources",
275
+ )
276
+ if (
277
+ target.target_name == "agent_playbook"
278
+ and target.target_ref
279
+ and target.status == "complete"
280
+ )
281
+ ]
282
+
283
+ def _rebuild_agent_playbooks(self, purge_id: str) -> list[int]:
284
+ rebuilt_ids: list[int] = []
285
+ for target in self.storage.list_purge_targets(
286
+ purge_id,
287
+ phase="rebuild_without_erased_sources",
288
+ ):
289
+ if target.target_name != "agent_playbook" or not target.target_ref:
290
+ continue
291
+ agent_playbook_id = int(target.target_ref)
292
+ if target.status == "complete":
293
+ rebuilt_ids.append(agent_playbook_id)
294
+ continue
295
+ remaining_source_windows = self._remaining_source_windows(target)
296
+ rebuild_fields = self._build_rebuilt_agent_playbook_fields(
297
+ remaining_source_windows
298
+ )
299
+ self.storage.apply_governance_agent_playbook_rebuild(
300
+ purge_id=purge_id,
301
+ agent_playbook_id=agent_playbook_id,
302
+ remaining_source_windows=remaining_source_windows,
303
+ content=rebuild_fields["content"],
304
+ trigger=rebuild_fields["trigger"],
305
+ rationale=rebuild_fields["rationale"],
306
+ blocking_issue=rebuild_fields["blocking_issue"],
307
+ expanded_terms=rebuild_fields["expanded_terms"],
308
+ tags=rebuild_fields["tags"],
309
+ )
310
+ rebuilt_ids.append(agent_playbook_id)
311
+ return rebuilt_ids
312
+
313
+ def _remaining_source_windows(
314
+ self,
315
+ target: PurgeOperationTarget,
316
+ ) -> list[dict[str, object]]:
317
+ detail = target.detail or {}
318
+ remaining = detail.get("remaining_source_windows", [])
319
+ if not isinstance(remaining, list):
320
+ raise ValueError("remaining_source_windows must be a list")
321
+ return remaining
322
+
323
+ def _build_rebuilt_agent_playbook_fields(
324
+ self,
325
+ remaining_source_windows: list[dict[str, object]],
326
+ ) -> dict[str, Any]:
327
+ user_playbook_ids: list[int] = []
328
+ for window in remaining_source_windows:
329
+ raw_user_playbook_id = window.get("user_playbook_id")
330
+ if isinstance(raw_user_playbook_id, int):
331
+ user_playbook_ids.append(raw_user_playbook_id)
332
+ playbooks_by_id = {
333
+ playbook.user_playbook_id: playbook
334
+ for playbook in self.storage.get_user_playbooks_by_ids_any_user(
335
+ user_playbook_ids
336
+ )
337
+ if playbook.user_playbook_id
338
+ }
339
+ remaining_playbooks = [
340
+ playbooks_by_id[user_playbook_id]
341
+ for user_playbook_id in user_playbook_ids
342
+ if user_playbook_id in playbooks_by_id
343
+ ]
344
+ return {
345
+ "content": self._join_non_empty_strings(
346
+ playbook.content for playbook in remaining_playbooks
347
+ ),
348
+ "trigger": self._join_non_empty_strings(
349
+ playbook.trigger for playbook in remaining_playbooks
350
+ ),
351
+ "rationale": self._join_non_empty_strings(
352
+ playbook.rationale for playbook in remaining_playbooks
353
+ ),
354
+ "blocking_issue": next(
355
+ (
356
+ playbook.blocking_issue.model_dump()
357
+ for playbook in remaining_playbooks
358
+ if playbook.blocking_issue is not None
359
+ ),
360
+ None,
361
+ ),
362
+ "expanded_terms": self._join_non_empty_strings(
363
+ playbook.expanded_terms for playbook in remaining_playbooks
364
+ ),
365
+ "tags": self._merge_tags(remaining_playbooks),
366
+ }
367
+
368
+ def _join_non_empty_strings(self, values: Any) -> str | None:
369
+ joined = "\n".join(value for value in values if value)
370
+ return joined or None
371
+
372
+ def _merge_tags(self, playbooks: list[Any]) -> list[str] | None:
373
+ merged_tags: list[str] = []
374
+ for playbook in playbooks:
375
+ for tag in playbook.tags or []:
376
+ if tag not in merged_tags:
377
+ merged_tags.append(tag)
378
+ return merged_tags or None
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+
6
+
7
+ def _hmac_ref(prefix: str, raw: str, secret: str) -> str:
8
+ if not raw:
9
+ raise ValueError("raw value must be non-empty")
10
+ if not secret:
11
+ raise ValueError("secret must be non-empty")
12
+ digest = hmac.new(secret.encode(), raw.encode(), hashlib.sha256).hexdigest()[:32]
13
+ return f"{prefix}_v1_{digest}"
14
+
15
+
16
+ def subject_ref(raw: str, secret: str) -> str:
17
+ return _hmac_ref("subref", raw, secret)
18
+
19
+
20
+ def actor_ref(raw: str, secret: str) -> str:
21
+ return _hmac_ref("actref", raw, secret)
22
+
23
+
24
+ def request_ref(raw: str, secret: str) -> str:
25
+ return _hmac_ref("reqref", raw, secret)
26
+
27
+
28
+ def stable_id(prefix: str, material: str) -> str:
29
+ if not prefix:
30
+ raise ValueError("prefix must be non-empty")
31
+ if not material:
32
+ raise ValueError("material must be non-empty")
33
+ digest = hashlib.sha256(material.encode()).hexdigest()[:32]
34
+ return f"{prefix}_{digest}"