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,681 @@
1
+ """Pure governance validation and canonicalization helpers.
2
+
3
+ These are stateless functions and constants (no sqlite3/storage-class dependency)
4
+ that validate and canonicalize governance schema objects. Both the SQLite and
5
+ Supabase storage backends import from this module so that neither needs to reach
6
+ into a private sibling package.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from datetime import UTC, datetime
14
+ from typing import Any, NoReturn, cast, get_args
15
+
16
+ from reflexio.models.api_schema.domain import AgentPlaybookSourceWindow
17
+ from reflexio.models.api_schema.domain.enums import Status
18
+ from reflexio.models.api_schema.domain.governance import (
19
+ AuditActorType,
20
+ AuditEntityType,
21
+ AuditEvent,
22
+ AuditOperation,
23
+ AuditStatus,
24
+ PurgeOperationType,
25
+ PurgeScopeType,
26
+ PurgeTargetStatus,
27
+ )
28
+
29
+ _PREPARE_PHASE = "prepare_targets"
30
+ _SNAPSHOT_TARGET_NAME = "target_snapshot"
31
+ _CANONICAL_DELETE_TARGET_NAMES = (
32
+ "request",
33
+ "interaction",
34
+ "profile",
35
+ "user_playbook",
36
+ "agent_success_evaluation_result",
37
+ "profile_purge",
38
+ "user_playbook_purge",
39
+ )
40
+ _ALLOWED_AUDIT_ACTOR_TYPES = frozenset(get_args(AuditActorType))
41
+ _ALLOWED_AUDIT_OPERATIONS = frozenset(get_args(AuditOperation))
42
+ _ALLOWED_AUDIT_ENTITY_TYPES = frozenset(get_args(AuditEntityType))
43
+ _ALLOWED_AUDIT_STATUSES = frozenset(get_args(AuditStatus))
44
+ _ALLOWED_PURGE_OPERATION_TYPES = frozenset(get_args(PurgeOperationType))
45
+ _ALLOWED_PURGE_SCOPE_TYPES = frozenset(get_args(PurgeScopeType))
46
+ _ALLOWED_PURGE_TARGET_STATUSES = frozenset(get_args(PurgeTargetStatus))
47
+ _ALLOWED_PURGE_TARGET_NAMES = frozenset(
48
+ {
49
+ _SNAPSHOT_TARGET_NAME,
50
+ "request",
51
+ "interaction",
52
+ "profile",
53
+ "user_playbook",
54
+ "agent_success_evaluation_result",
55
+ "agent_playbook",
56
+ "profile_purge",
57
+ "user_playbook_purge",
58
+ }
59
+ )
60
+ _ALLOWED_PURGE_TARGET_PHASES = frozenset(
61
+ {
62
+ _PREPARE_PHASE,
63
+ "delete",
64
+ "hide_for_rebuild",
65
+ "rebuild_without_erased_sources",
66
+ }
67
+ )
68
+ _ALLOWED_AUDIT_DETAIL_KEYS = frozenset(
69
+ {
70
+ "agent_playbook_id",
71
+ "count",
72
+ "deleted_counts",
73
+ "deleted_count",
74
+ "rebuilt_agent_playbook_ids",
75
+ "route",
76
+ "status",
77
+ }
78
+ )
79
+ _ALLOWED_PURGE_TARGET_DETAIL_KEYS = frozenset(
80
+ {
81
+ "affected_agent_playbook_ids",
82
+ "agent_playbook_id",
83
+ "count",
84
+ "deleted_counts",
85
+ "deleted_count",
86
+ "erased_source_ids",
87
+ "owned_user_playbook_ids",
88
+ "original_source_windows",
89
+ "previous_lifecycle_status",
90
+ "prepared",
91
+ "rebuilt_agent_playbook_ids",
92
+ "remaining_source_windows",
93
+ "route",
94
+ "source_interaction_ids",
95
+ "status",
96
+ "user_playbook_id",
97
+ }
98
+ )
99
+ _DISALLOWED_DETAIL_KEYS = frozenset(
100
+ {
101
+ "content",
102
+ "email",
103
+ "prompt",
104
+ "request_id",
105
+ "request_ref",
106
+ "user_id",
107
+ }
108
+ )
109
+ _EMAIL_RE = re.compile(r"\b[^@\s]+@[^@\s]+\.[^@\s]+\b")
110
+ _REQUEST_ID_RE = re.compile(
111
+ r"\b(?:reqref_(?!v1_)|request[_-]|req[_-])[A-Za-z0-9_-]*\b",
112
+ re.IGNORECASE,
113
+ )
114
+ _TOKEN_NAME_RE = re.compile(
115
+ r"\b(?:api[-_ ]?token|token[-_ ]?name|bearer|secret[-_ ]?key)\b",
116
+ re.IGNORECASE,
117
+ )
118
+ _RAW_EXCEPTION_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)\s*:")
119
+ _SAFE_INTERNAL_ID_RE = re.compile(r"^[0-9]+$")
120
+ _USER_LIKE_TARGET_REF_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,63}$")
121
+ _CODE_SHAPED_VALUE_RE = re.compile(r"^[A-Za-z0-9]+(?:[_.:-][A-Za-z0-9]+)+$")
122
+ _IDENTIFIERISH_CODE_VALUE_RE = re.compile(
123
+ r"^(?:user|subject|actor)[_.:-][A-Za-z0-9]+(?:[_.:-][A-Za-z0-9]+)*$",
124
+ re.IGNORECASE,
125
+ )
126
+ _SAFE_ERROR_CODE_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.:-]{0,127}$")
127
+ _IDENTIFIERISH_ERROR_CODE_RE = re.compile(
128
+ r"^(?:user|subject|request|req|actor|email)[-_.:]?[A-Za-z0-9_.:-]+$",
129
+ re.IGNORECASE,
130
+ )
131
+ _ALLOWED_DETAIL_STATUS_VALUES = frozenset(
132
+ {
133
+ "archive_in_progress",
134
+ "complete",
135
+ "error",
136
+ "failed",
137
+ "ok",
138
+ "pending",
139
+ "running",
140
+ }
141
+ )
142
+ _ALLOWED_PREVIOUS_LIFECYCLE_STATUS_VALUES = frozenset(
143
+ status.value for status in Status if status.value is not None
144
+ )
145
+ _ALLOWED_DETAIL_ROUTE_VALUES = frozenset(
146
+ {
147
+ "prepare_targets",
148
+ "delete",
149
+ "hide_for_rebuild",
150
+ "rebuild_without_erased_sources",
151
+ }
152
+ )
153
+ _ALLOWED_DELETED_COUNTS_KEYS = frozenset(
154
+ {
155
+ "interactions",
156
+ "user_playbooks",
157
+ "profiles",
158
+ "requests",
159
+ "agent_success_evaluation_results",
160
+ "purged_profiles",
161
+ "purged_user_playbooks",
162
+ }
163
+ )
164
+
165
+
166
+ def _epoch_now() -> int:
167
+ return int(datetime.now(UTC).timestamp())
168
+
169
+
170
+ def _raise_governance_validation_error(field_name: str, reason: str) -> NoReturn:
171
+ raise ValueError(f"Unsafe governance {field_name}: {reason}")
172
+
173
+
174
+ def _validate_governance_string(field_name: str, value: str) -> None:
175
+ if _EMAIL_RE.search(value):
176
+ _raise_governance_validation_error(field_name, "email")
177
+ if _REQUEST_ID_RE.search(value):
178
+ _raise_governance_validation_error(field_name, "request_id")
179
+ if _TOKEN_NAME_RE.search(value):
180
+ _raise_governance_validation_error(field_name, "token")
181
+ if _RAW_EXCEPTION_RE.search(value):
182
+ _raise_governance_validation_error(field_name, "raw exception text")
183
+
184
+
185
+ def _validate_governance_prose_string(field_name: str, value: str) -> None:
186
+ _validate_governance_string(field_name, value)
187
+ lowered = value.lower()
188
+ if "prompt" in lowered or "content" in lowered:
189
+ _raise_governance_validation_error(field_name, "prompt/content")
190
+
191
+
192
+ def _validate_governance_prefixed_ref(
193
+ field_name: str, value: str | None, *, prefix: str
194
+ ) -> None:
195
+ if value is None:
196
+ return
197
+ if re.fullmatch(rf"{re.escape(prefix)}[0-9a-f]{{32}}", value) is None:
198
+ _raise_governance_validation_error(
199
+ field_name, f"must match {prefix}<32 lowercase hex chars>"
200
+ )
201
+
202
+
203
+ def _validate_governance_code_shaped(
204
+ field_name: str,
205
+ value: str,
206
+ *,
207
+ allow_minimized_ref: bool,
208
+ ) -> str:
209
+ if not value:
210
+ _raise_governance_validation_error(field_name, "required")
211
+ _validate_governance_string(field_name, value)
212
+ if allow_minimized_ref and any(
213
+ re.fullmatch(rf"{re.escape(prefix)}[0-9a-f]{{32}}", value)
214
+ for prefix in ("subref_v1_", "reqref_v1_", "actref_v1_")
215
+ ):
216
+ return value
217
+ if value.startswith(("subref_v1_", "reqref_v1_", "actref_v1_")):
218
+ _raise_governance_validation_error(field_name, "identifier")
219
+ if _IDENTIFIERISH_CODE_VALUE_RE.fullmatch(value):
220
+ _raise_governance_validation_error(field_name, "identifier")
221
+ if _SAFE_INTERNAL_ID_RE.fullmatch(value):
222
+ return value
223
+ if _CODE_SHAPED_VALUE_RE.fullmatch(value):
224
+ return value
225
+ if _USER_LIKE_TARGET_REF_RE.fullmatch(value):
226
+ _raise_governance_validation_error(field_name, "user-like identifier")
227
+ _raise_governance_validation_error(
228
+ field_name, "must be minimized, internal, or code-shaped"
229
+ )
230
+ raise AssertionError("unreachable")
231
+
232
+
233
+ def _validate_governance_idempotency_key(
234
+ field_name: str, value: str | None
235
+ ) -> str | None:
236
+ if value is None:
237
+ return None
238
+ if _SAFE_INTERNAL_ID_RE.fullmatch(value):
239
+ _raise_governance_validation_error(field_name, "numeric identifier")
240
+ return _validate_governance_code_shaped(
241
+ field_name,
242
+ value,
243
+ allow_minimized_ref=False,
244
+ )
245
+
246
+
247
+ def _validate_governance_detail_enum(
248
+ field_name: str, value: Any, *, allowed_values: frozenset[str]
249
+ ) -> str:
250
+ if not isinstance(value, str):
251
+ _raise_governance_validation_error(field_name, "expected str")
252
+ _validate_governance_prose_string(field_name, value)
253
+ if value not in allowed_values:
254
+ _raise_governance_validation_error(field_name, "must be canonical")
255
+ return value
256
+
257
+
258
+ def _validate_governance_purge_id(field_name: str, value: str) -> str:
259
+ if not value:
260
+ _raise_governance_validation_error(field_name, "required")
261
+ _validate_governance_string(field_name, value)
262
+ if value.startswith(("subref_v1_", "reqref_v1_", "actref_v1_")):
263
+ _raise_governance_validation_error(field_name, "identifier")
264
+ if not value.startswith("purge_"):
265
+ _raise_governance_validation_error(field_name, "must start with purge_")
266
+ if _CODE_SHAPED_VALUE_RE.fullmatch(value) is None:
267
+ _raise_governance_validation_error(field_name, "must be code-shaped")
268
+ suffix = value[len("purge_") :]
269
+ if _IDENTIFIERISH_CODE_VALUE_RE.fullmatch(suffix):
270
+ _raise_governance_validation_error(field_name, "identifier")
271
+ if suffix.isdecimal():
272
+ _raise_governance_validation_error(field_name, "numeric identifier")
273
+ return value
274
+
275
+
276
+ def _validate_governance_int(field_name: str, value: Any) -> None:
277
+ if isinstance(value, bool) or not isinstance(value, int):
278
+ _raise_governance_validation_error(field_name, "expected int")
279
+
280
+
281
+ def _validate_governance_nonnegative_int(field_name: str, value: Any) -> int:
282
+ _validate_governance_int(field_name, value)
283
+ if value < 0:
284
+ _raise_governance_validation_error(field_name, "must be nonnegative")
285
+ return cast(int, value)
286
+
287
+
288
+ def _validate_governance_deleted_count(value: Any) -> int:
289
+ return _validate_governance_nonnegative_int("deleted_count", value)
290
+
291
+
292
+ def _validate_governance_int_list(field_name: str, value: Any) -> list[int]:
293
+ if not isinstance(value, list):
294
+ _raise_governance_validation_error(field_name, "expected list[int]")
295
+ normalized_items: list[int] = []
296
+ for item in value:
297
+ _validate_governance_int(field_name, item)
298
+ normalized_items.append(cast(int, item))
299
+ return normalized_items
300
+
301
+
302
+ def _validate_governance_deleted_counts(field_name: str, value: Any) -> dict[str, int]:
303
+ if not isinstance(value, dict):
304
+ _raise_governance_validation_error(field_name, "expected dict[str, int]")
305
+ normalized_counts: dict[str, int] = {}
306
+ for raw_key, raw_value in value.items():
307
+ key = str(raw_key).strip().lower()
308
+ if key in normalized_counts:
309
+ _raise_governance_validation_error(field_name, f"duplicate key {key}")
310
+ if key not in _ALLOWED_DELETED_COUNTS_KEYS:
311
+ _raise_governance_validation_error(field_name, key)
312
+ normalized_counts[key] = _validate_governance_deleted_count(raw_value)
313
+ return normalized_counts
314
+
315
+
316
+ def _normalize_governance_window_item(
317
+ field_name: str, index: int, item: object
318
+ ) -> dict[str, Any]:
319
+ if not isinstance(item, dict):
320
+ _raise_governance_validation_error(
321
+ f"{field_name}[{index}]", "expected window dict"
322
+ )
323
+ window_item = cast(dict[Any, Any], item)
324
+ normalized_item: dict[str, Any] = {}
325
+ for raw_key, raw_value in window_item.items():
326
+ normalized_key = str(raw_key).strip().lower()
327
+ if normalized_key in normalized_item:
328
+ _raise_governance_validation_error(
329
+ f"{field_name}[{index}]", f"duplicate key {normalized_key}"
330
+ )
331
+ normalized_item[normalized_key] = raw_value
332
+ return normalized_item
333
+
334
+
335
+ def _validate_governance_window_list(
336
+ field_name: str, value: Any
337
+ ) -> list[dict[str, object]]:
338
+ if not isinstance(value, list):
339
+ _raise_governance_validation_error(field_name, "expected list[window]")
340
+ normalized_windows: list[dict[str, object]] = []
341
+ for index, item in enumerate(value):
342
+ normalized_item = _normalize_governance_window_item(field_name, index, item)
343
+ normalized_keys = set(normalized_item)
344
+ unexpected_keys = normalized_keys - {
345
+ "user_playbook_id",
346
+ "source_interaction_ids",
347
+ }
348
+ if unexpected_keys:
349
+ _raise_governance_validation_error(
350
+ f"{field_name}[{index}]", sorted(unexpected_keys)[0]
351
+ )
352
+ if "user_playbook_id" not in normalized_item:
353
+ _raise_governance_validation_error(
354
+ f"{field_name}[{index}].user_playbook_id", "required"
355
+ )
356
+ _validate_governance_int(
357
+ f"{field_name}[{index}].user_playbook_id",
358
+ normalized_item["user_playbook_id"],
359
+ )
360
+ canonical_item: dict[str, object] = {
361
+ "user_playbook_id": cast(int, normalized_item["user_playbook_id"])
362
+ }
363
+ if "source_interaction_ids" in normalized_item:
364
+ canonical_item["source_interaction_ids"] = _validate_governance_int_list(
365
+ f"{field_name}[{index}].source_interaction_ids",
366
+ normalized_item["source_interaction_ids"],
367
+ )
368
+ normalized_windows.append(canonical_item)
369
+ return normalized_windows
370
+
371
+
372
+ def _parse_governance_window_list(
373
+ field_name: str, value: list[dict[str, object]]
374
+ ) -> list[AgentPlaybookSourceWindow]:
375
+ windows: list[AgentPlaybookSourceWindow] = []
376
+ for normalized_item in _validate_governance_window_list(field_name, value):
377
+ user_playbook_id = cast(int, normalized_item["user_playbook_id"])
378
+ source_ids = cast(
379
+ list[int], normalized_item.get("source_interaction_ids") or []
380
+ )
381
+ windows.append(
382
+ AgentPlaybookSourceWindow(
383
+ user_playbook_id=user_playbook_id,
384
+ source_interaction_ids=[int(source_id) for source_id in source_ids],
385
+ )
386
+ )
387
+ return windows
388
+
389
+
390
+ def _canonicalize_governance_windows(
391
+ field_name: str, value: list[dict[str, object]]
392
+ ) -> list[dict[str, object]]:
393
+ return [
394
+ window.model_dump()
395
+ for window in _parse_governance_window_list(field_name, value)
396
+ ]
397
+
398
+
399
+ def _validate_governance_target_ref(
400
+ *, target_name: str, phase: str, target_ref: str
401
+ ) -> str:
402
+ if target_name == _SNAPSHOT_TARGET_NAME:
403
+ if phase != _PREPARE_PHASE:
404
+ _raise_governance_validation_error(
405
+ _SNAPSHOT_TARGET_NAME, "must use prepare_targets phase"
406
+ )
407
+ if target_ref != "all":
408
+ _raise_governance_validation_error("target_ref", "must be all")
409
+ return target_ref
410
+ if target_name in _CANONICAL_DELETE_TARGET_NAMES:
411
+ if phase != "delete":
412
+ _raise_governance_validation_error(
413
+ phase,
414
+ f"{target_name} targets must use delete phase",
415
+ )
416
+ if target_ref != "all":
417
+ _raise_governance_validation_error("target_ref", "must be all")
418
+ return target_ref
419
+ if target_name == "agent_playbook":
420
+ if phase not in {"hide_for_rebuild", "rebuild_without_erased_sources"}:
421
+ _raise_governance_validation_error(
422
+ phase,
423
+ "agent_playbook targets must use hide_for_rebuild or "
424
+ "rebuild_without_erased_sources",
425
+ )
426
+ if _SAFE_INTERNAL_ID_RE.fullmatch(target_ref):
427
+ return target_ref
428
+ _raise_governance_validation_error(
429
+ "target_ref", "must be a numeric internal id"
430
+ )
431
+ if target_ref in {"", "all"}:
432
+ return target_ref
433
+ if _SAFE_INTERNAL_ID_RE.fullmatch(target_ref):
434
+ return target_ref
435
+ for prefix in ("reqref_v1_", "subref_v1_", "actref_v1_"):
436
+ if re.fullmatch(rf"{re.escape(prefix)}[0-9a-f]{{32}}", target_ref):
437
+ return target_ref
438
+ if target_ref.startswith(prefix):
439
+ _raise_governance_validation_error(
440
+ "target_ref", f"must match {prefix}<32 lowercase hex chars>"
441
+ )
442
+ _validate_governance_string("target_ref", target_ref)
443
+ if _USER_LIKE_TARGET_REF_RE.fullmatch(target_ref):
444
+ _raise_governance_validation_error("target_ref", "user-like identifier")
445
+ _raise_governance_validation_error("target_ref", "must be minimized or internal")
446
+ raise AssertionError("unreachable")
447
+
448
+
449
+ def _validate_governance_detail_entry(
450
+ field_name: str,
451
+ key: str,
452
+ value: Any,
453
+ *,
454
+ allowed_keys: frozenset[str],
455
+ ) -> object:
456
+ if key in _DISALLOWED_DETAIL_KEYS:
457
+ _raise_governance_validation_error(field_name, key)
458
+ if key not in allowed_keys:
459
+ _raise_governance_validation_error(field_name, key)
460
+ if key in {"count", "deleted_count"}:
461
+ return _validate_governance_nonnegative_int(field_name, value)
462
+ if key in {"agent_playbook_id", "user_playbook_id"}:
463
+ _validate_governance_int(field_name, value)
464
+ return cast(int, value)
465
+ if key == "deleted_counts":
466
+ return _validate_governance_deleted_counts(field_name, value)
467
+ if key in {
468
+ "affected_agent_playbook_ids",
469
+ "erased_source_ids",
470
+ "owned_user_playbook_ids",
471
+ "rebuilt_agent_playbook_ids",
472
+ "source_interaction_ids",
473
+ }:
474
+ return _validate_governance_int_list(field_name, value)
475
+ if key in {"original_source_windows", "remaining_source_windows"}:
476
+ return _validate_governance_window_list(field_name, value)
477
+ if key == "previous_lifecycle_status":
478
+ if value is None:
479
+ return None
480
+ return _validate_governance_detail_enum(
481
+ field_name,
482
+ value,
483
+ allowed_values=_ALLOWED_PREVIOUS_LIFECYCLE_STATUS_VALUES,
484
+ )
485
+ if key == "prepared":
486
+ if not isinstance(value, bool):
487
+ _raise_governance_validation_error(field_name, "expected bool")
488
+ return value
489
+ if key == "route":
490
+ return _validate_governance_detail_enum(
491
+ field_name,
492
+ value,
493
+ allowed_values=_ALLOWED_DETAIL_ROUTE_VALUES,
494
+ )
495
+ if key == "status":
496
+ return _validate_governance_detail_enum(
497
+ field_name,
498
+ value,
499
+ allowed_values=_ALLOWED_DETAIL_STATUS_VALUES,
500
+ )
501
+ _raise_governance_validation_error(field_name, key)
502
+
503
+
504
+ def _validate_governance_detail(
505
+ field_name: str,
506
+ detail: dict[str, object] | None,
507
+ *,
508
+ allowed_keys: frozenset[str],
509
+ ) -> dict[str, object] | None:
510
+ if detail is None:
511
+ return None
512
+ if not isinstance(detail, dict):
513
+ _raise_governance_validation_error(field_name, "expected dict")
514
+ normalized_detail: dict[str, object] = {}
515
+ for key, value in detail.items():
516
+ normalized_key = str(key).strip().lower()
517
+ if normalized_key in normalized_detail:
518
+ _raise_governance_validation_error(
519
+ field_name, f"duplicate key {normalized_key}"
520
+ )
521
+ normalized_detail[normalized_key] = _validate_governance_detail_entry(
522
+ f"{field_name}.{normalized_key}",
523
+ normalized_key,
524
+ value,
525
+ allowed_keys=allowed_keys,
526
+ )
527
+ return normalized_detail
528
+
529
+
530
+ def _validate_governance_code_like(field_name: str, value: str) -> str:
531
+ if not value:
532
+ _raise_governance_validation_error(field_name, "required")
533
+ _validate_governance_string(field_name, value)
534
+ if value.startswith(("subref_v1_", "reqref_v1_", "actref_v1_")):
535
+ _raise_governance_validation_error(field_name, "identifier")
536
+ if _IDENTIFIERISH_ERROR_CODE_RE.fullmatch(value):
537
+ _raise_governance_validation_error(field_name, "identifier")
538
+ if not _SAFE_ERROR_CODE_RE.fullmatch(value):
539
+ _raise_governance_validation_error(
540
+ field_name, "must be a stable diagnostic code"
541
+ )
542
+ return value
543
+
544
+
545
+ def _validate_governance_error_detail(error_detail: str | None) -> str | None:
546
+ if error_detail is None:
547
+ return None
548
+ return _validate_governance_code_like("error_detail", error_detail)
549
+
550
+
551
+ def _validate_governance_error_code(error_code: str) -> str:
552
+ return _validate_governance_code_like("error_code", error_code)
553
+
554
+
555
+ def _validate_governance_enum(
556
+ field_name: str, value: str, *, allowed: frozenset[str]
557
+ ) -> str:
558
+ if value not in allowed:
559
+ _raise_governance_validation_error(
560
+ field_name,
561
+ f"must be one of {', '.join(sorted(allowed))}",
562
+ )
563
+ return value
564
+
565
+
566
+ def _normalize_governance_detail_for_identity(
567
+ detail: dict[str, object] | None,
568
+ ) -> str | None:
569
+ if detail is None:
570
+ return None
571
+ normalized_detail: dict[str, object] = {}
572
+ for key, value in detail.items():
573
+ normalized_key = str(key).strip().lower()
574
+ if normalized_key in {"original_source_windows", "remaining_source_windows"}:
575
+ normalized_windows = [
576
+ _normalize_governance_window_item(normalized_key, index, item)
577
+ for index, item in enumerate(cast(list[object], value))
578
+ ]
579
+ normalized_detail[normalized_key] = normalized_windows
580
+ continue
581
+ normalized_detail[normalized_key] = value
582
+ return json.dumps(normalized_detail, sort_keys=True, separators=(",", ":"))
583
+
584
+
585
+ def _validate_audit_event_for_persistence(event: AuditEvent) -> None:
586
+ _validate_governance_enum(
587
+ "actor_type",
588
+ event.actor_type,
589
+ allowed=_ALLOWED_AUDIT_ACTOR_TYPES,
590
+ )
591
+ _validate_governance_enum(
592
+ "operation",
593
+ event.operation,
594
+ allowed=_ALLOWED_AUDIT_OPERATIONS,
595
+ )
596
+ _validate_governance_enum(
597
+ "entity_type",
598
+ event.entity_type,
599
+ allowed=_ALLOWED_AUDIT_ENTITY_TYPES,
600
+ )
601
+ _validate_governance_enum(
602
+ "status",
603
+ event.status,
604
+ allowed=_ALLOWED_AUDIT_STATUSES,
605
+ )
606
+ _validate_governance_prefixed_ref("actor_ref", event.actor_ref, prefix="actref_v1_")
607
+ _validate_governance_prefixed_ref(
608
+ "subject_ref", event.subject_ref, prefix="subref_v1_"
609
+ )
610
+ if event.request_ref is None:
611
+ _raise_governance_validation_error("request_ref", "required")
612
+ _validate_governance_prefixed_ref(
613
+ "request_ref", event.request_ref, prefix="reqref_v1_"
614
+ )
615
+ if event.entity_id is not None:
616
+ _validate_governance_code_shaped(
617
+ "entity_id",
618
+ event.entity_id,
619
+ allow_minimized_ref=True,
620
+ )
621
+ _validate_governance_idempotency_key("idempotency_key", event.idempotency_key)
622
+ _validate_governance_detail(
623
+ "audit_event.detail",
624
+ event.detail,
625
+ allowed_keys=_ALLOWED_AUDIT_DETAIL_KEYS,
626
+ )
627
+
628
+
629
+ def _canonicalize_audit_event_for_persistence(event: AuditEvent) -> AuditEvent:
630
+ _validate_audit_event_for_persistence(event)
631
+ return event.model_copy(
632
+ update={
633
+ "detail": _validate_governance_detail(
634
+ "audit_event.detail",
635
+ event.detail,
636
+ allowed_keys=_ALLOWED_AUDIT_DETAIL_KEYS,
637
+ )
638
+ }
639
+ )
640
+
641
+
642
+ def _is_successful_erase_event(
643
+ event: AuditEvent, *, purge_id: str | None = None
644
+ ) -> bool:
645
+ if event.operation != "ERASE" or event.status != "ok":
646
+ return False
647
+ if purge_id is not None:
648
+ return event.idempotency_key == purge_id
649
+ return True
650
+
651
+
652
+ def _successful_erase_identity(
653
+ event: AuditEvent,
654
+ ) -> tuple[
655
+ str,
656
+ str,
657
+ str | None,
658
+ str,
659
+ str,
660
+ str | None,
661
+ str | None,
662
+ str | None,
663
+ str,
664
+ str | None,
665
+ str | None,
666
+ ]:
667
+ return (
668
+ event.org_id,
669
+ event.actor_type,
670
+ event.actor_ref,
671
+ event.operation,
672
+ event.entity_type,
673
+ event.entity_id,
674
+ event.subject_ref,
675
+ event.request_ref,
676
+ event.status,
677
+ event.idempotency_key,
678
+ _normalize_governance_detail_for_identity(
679
+ cast(dict[str, object] | None, event.detail)
680
+ ),
681
+ )