claude-smart 0.2.46 → 0.2.47
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 +19 -11
- package/bin/claude-smart.js +290 -68
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -10
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/opencode/dist/server.mjs +76 -2
- package/plugin/opencode/server.mts +79 -2
- package/plugin/pyproject.toml +6 -2
- package/plugin/scripts/smart-install.sh +7 -1
- package/plugin/src/claude_smart/cli.py +210 -22
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +1 -1
- package/plugin/vendor/reflexio/reflexio/README.md +3 -1
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +23 -5
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +16 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +176 -29
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +45 -19
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +129 -111
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +14 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +5 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +262 -107
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +19 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
- 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
|
+
)
|