millforge 0.1.0__py3-none-any.whl

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 (116) hide show
  1. millforge/__init__.py +1174 -0
  2. millforge/_forge/LICENSE +21 -0
  3. millforge/_forge/PROVENANCE.json +295 -0
  4. millforge/_forge/UPDATE_POLICY.md +24 -0
  5. millforge/_forge/__init__.py +14 -0
  6. millforge/_forge/adapter.py +2232 -0
  7. millforge/_forge/base_runner.py +121 -0
  8. millforge/_forge/clients/__init__.py +10 -0
  9. millforge/_forge/clients/base.py +200 -0
  10. millforge/_forge/context/__init__.py +23 -0
  11. millforge/_forge/context/manager.py +178 -0
  12. millforge/_forge/context/strategies.py +335 -0
  13. millforge/_forge/core/__init__.py +16 -0
  14. millforge/_forge/core/inference.py +433 -0
  15. millforge/_forge/core/messages.py +119 -0
  16. millforge/_forge/core/runner.py +479 -0
  17. millforge/_forge/core/steps.py +108 -0
  18. millforge/_forge/core/workflow.py +400 -0
  19. millforge/_forge/errors.py +222 -0
  20. millforge/_forge/guardrails/__init__.py +21 -0
  21. millforge/_forge/guardrails/error_tracker.py +71 -0
  22. millforge/_forge/guardrails/guardrails.py +194 -0
  23. millforge/_forge/guardrails/nudge.py +47 -0
  24. millforge/_forge/guardrails/response_validator.py +119 -0
  25. millforge/_forge/guardrails/step_enforcer.py +183 -0
  26. millforge/_forge/prompts/__init__.py +16 -0
  27. millforge/_forge/prompts/nudges.py +95 -0
  28. millforge/_forge/prompts/templates.py +285 -0
  29. millforge/_version.py +3 -0
  30. millforge/artifacts.py +570 -0
  31. millforge/base/__init__.py +97 -0
  32. millforge/base/composition.py +402 -0
  33. millforge/base/context.py +285 -0
  34. millforge/base/harness.py +138 -0
  35. millforge/base/identity.py +465 -0
  36. millforge/base/options.py +34 -0
  37. millforge/base/platform.py +17 -0
  38. millforge/base/prompt.py +317 -0
  39. millforge/base/runner.py +546 -0
  40. millforge/compiled_plan.py +970 -0
  41. millforge/compiler/__init__.py +231 -0
  42. millforge/compiler/artifact_validation.py +257 -0
  43. millforge/compiler/canonicalization.py +169 -0
  44. millforge/compiler/capabilities.py +66 -0
  45. millforge/compiler/catalogs.py +500 -0
  46. millforge/compiler/diagnostics.py +491 -0
  47. millforge/compiler/graph.py +678 -0
  48. millforge/compiler/lowering.py +198 -0
  49. millforge/compiler/output.py +692 -0
  50. millforge/compiler/parsing.py +1424 -0
  51. millforge/compiler/requests.py +1180 -0
  52. millforge/compiler/schema_validation.py +272 -0
  53. millforge/compiler/semantic.py +490 -0
  54. millforge/compiler/service.py +448 -0
  55. millforge/compiler/source.py +375 -0
  56. millforge/compiler/validators.py +184 -0
  57. millforge/connectors/__init__.py +95 -0
  58. millforge/connectors/admission.py +801 -0
  59. millforge/connectors/broker.py +202 -0
  60. millforge/connectors/contracts.py +1159 -0
  61. millforge/connectors/diagnostics.py +189 -0
  62. millforge/connectors/fake.py +66 -0
  63. millforge/connectors/runtime.py +236 -0
  64. millforge/contracts.py +2860 -0
  65. millforge/custom_tools/__init__.py +67 -0
  66. millforge/custom_tools/compiler.py +724 -0
  67. millforge/custom_tools/contracts.py +1093 -0
  68. millforge/custom_tools/diagnostics.py +205 -0
  69. millforge/eval_artifacts.py +952 -0
  70. millforge/eval_boundary.py +2435 -0
  71. millforge/eval_fixtures/__init__.py +1 -0
  72. millforge/eval_fixtures/default_pack/__init__.py +1 -0
  73. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
  74. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
  75. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
  76. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
  77. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
  78. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
  79. millforge/eval_fixtures/default_pack/manifest.json +12 -0
  80. millforge/eval_modes.py +1282 -0
  81. millforge/eval_presets.py +1398 -0
  82. millforge/eval_reports.py +2517 -0
  83. millforge/eval_suite.py +2429 -0
  84. millforge/eval_trials.py +2632 -0
  85. millforge/eval_workflow.py +794 -0
  86. millforge/exceptions.py +122 -0
  87. millforge/model_backend.py +2098 -0
  88. millforge/protocols.py +340 -0
  89. millforge/py.typed +0 -0
  90. millforge/runtime.py +1791 -0
  91. millforge/testing/__init__.py +1089 -0
  92. millforge/tools/__init__.py +83 -0
  93. millforge/tools/builtin_runtime.py +1339 -0
  94. millforge/tools/builtins.py +773 -0
  95. millforge/tools/execution.py +1545 -0
  96. millforge/tools/path_policy.py +155 -0
  97. millforge/tools/pi_compat/PI_LICENSE +21 -0
  98. millforge/tools/pi_compat/PROVENANCE.json +55 -0
  99. millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
  100. millforge/tools/pi_compat/__init__.py +34 -0
  101. millforge/tools/pi_compat/contracts.py +49 -0
  102. millforge/tools/pi_compat/editing.py +390 -0
  103. millforge/tools/pi_compat/mutations.py +57 -0
  104. millforge/tools/pi_compat/operations.py +401 -0
  105. millforge/tools/pi_compat/paths.py +155 -0
  106. millforge/tools/pi_compat/process.py +1375 -0
  107. millforge/tools/pi_compat/search.py +738 -0
  108. millforge/tools/pi_compat/truncation.py +267 -0
  109. millforge/tools/pi_compat_catalog.py +396 -0
  110. millforge/tools/pi_compat_runtime.py +460 -0
  111. millforge/tools/registry.py +553 -0
  112. millforge/tools/results.py +533 -0
  113. millforge-0.1.0.dist-info/METADATA +844 -0
  114. millforge-0.1.0.dist-info/RECORD +116 -0
  115. millforge-0.1.0.dist-info/WHEEL +4 -0
  116. millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,66 @@
1
+ """Deterministic capability aggregation for resolved compiler descriptors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from collections.abc import Mapping
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, StrictStr
9
+
10
+ from millforge.compiler.catalogs import ToolCatalogEntry
11
+ from millforge.compiler.diagnostics import (
12
+ CompilerDiagnostic,
13
+ CompilerPhase,
14
+ DiagnosticField,
15
+ DiagnosticSeverity,
16
+ sort_diagnostics,
17
+ )
18
+ from millforge.compiler.graph import ResolvedNodeDescriptor
19
+ from millforge.contracts import CapabilityEnvelope
20
+
21
+
22
+ class CapabilityValidationResult(BaseModel):
23
+ """Immutable result of capability aggregation and grant comparison."""
24
+
25
+ model_config = ConfigDict(extra="forbid", frozen=True)
26
+
27
+ required_capability_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
28
+ diagnostics: tuple[CompilerDiagnostic, ...] = Field(default_factory=tuple)
29
+
30
+ @property
31
+ def ok(self) -> bool:
32
+ return not self.diagnostics
33
+
34
+
35
+ def validate_capability_grants(
36
+ resolved_nodes: Mapping[str, ToolCatalogEntry | ResolvedNodeDescriptor],
37
+ envelope: CapabilityEnvelope,
38
+ ) -> CapabilityValidationResult:
39
+ """Aggregate descriptor-required capabilities and compare exact grants."""
40
+ capability_nodes: dict[str, list[str]] = defaultdict(list)
41
+ for node_id, value in resolved_nodes.items():
42
+ descriptor = (
43
+ value.descriptor if isinstance(value, ResolvedNodeDescriptor) else value
44
+ )
45
+ for capability_id in descriptor.required_capabilities:
46
+ capability_nodes[capability_id].append(node_id)
47
+
48
+ required = tuple(sorted(capability_nodes))
49
+ granted = {grant.capability_id for grant in envelope.grants}
50
+ diagnostics = [
51
+ CompilerDiagnostic(
52
+ code="MF-C001",
53
+ phase=CompilerPhase.CAPABILITY,
54
+ severity=DiagnosticSeverity.ERROR,
55
+ message=f"Required capability {capability_id!r} is not granted.",
56
+ node_id=sorted(node_ids)[0],
57
+ related_ids=tuple(sorted(node_ids)[1:]),
58
+ fields=(DiagnosticField(key="capability_id", value=capability_id),),
59
+ )
60
+ for capability_id, node_ids in sorted(capability_nodes.items())
61
+ if capability_id not in granted
62
+ ]
63
+ return CapabilityValidationResult(
64
+ required_capability_ids=required,
65
+ diagnostics=sort_diagnostics(diagnostics),
66
+ )
@@ -0,0 +1,500 @@
1
+ """Deterministic catalog snapshot contracts and entry admission."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from enum import Enum
8
+ from inspect import getattr_static
9
+ from types import MappingProxyType
10
+ from typing import Any, Protocol, runtime_checkable
11
+
12
+ from pydantic import (
13
+ BaseModel,
14
+ ConfigDict,
15
+ Field,
16
+ StrictInt,
17
+ StrictStr,
18
+ field_serializer,
19
+ field_validator,
20
+ model_validator,
21
+ )
22
+
23
+ from millforge import CompiledModelProfile, IdempotencyClass, SideEffectClass
24
+ from millforge.compiler.diagnostics import detect_secret_candidate
25
+ from millforge.compiler.schema_validation import validate_json_schema_subset
26
+ from millforge.compiler.validators import (
27
+ TOOL_VERSION_MAX,
28
+ validate_artifact_id,
29
+ validate_capability_id,
30
+ validate_canonical_tool_id,
31
+ validate_nonblank,
32
+ validate_profile_id,
33
+ validate_sha256,
34
+ validate_unique,
35
+ validate_utf8_size,
36
+ )
37
+ from millforge.contracts import RedactionPolicy
38
+
39
+ MAX_TOOL_DESCRIPTION_UTF8 = 4096
40
+ MAX_IMPLEMENTATION_ID_UTF8 = 256
41
+ MAX_MODEL_TOOL_NAME_UTF8 = 64
42
+ MAX_CAPABILITY_ID_UTF8 = 160
43
+ MF_R009 = "MF-R009"
44
+ DISCOVERY_NOT_CATALOG_CODE = "MF-C001_DISCOVERY_NOT_CATALOG"
45
+
46
+
47
+ class CatalogLookupClassification(str, Enum):
48
+ """Closed catalog lookup classifications."""
49
+
50
+ FOUND = "found"
51
+ MISSING = "missing"
52
+ INVALID = "invalid"
53
+
54
+
55
+ class CatalogMetadataError(ValueError):
56
+ """Fail-closed snapshot metadata error for semantic resolution."""
57
+
58
+ diagnostic_code = MF_R009
59
+
60
+ def __init__(self, message: str, *, evidence: Mapping[str, str] | None = None):
61
+ super().__init__(message)
62
+ self.evidence = _redacted_evidence(evidence or {})
63
+
64
+
65
+ class CatalogSnapshotMetadata(BaseModel):
66
+ """Validated immutable snapshot identity captured once per invocation."""
67
+
68
+ model_config = ConfigDict(extra="forbid", frozen=True)
69
+
70
+ snapshot_id: StrictStr
71
+ snapshot_sha256: StrictStr
72
+
73
+ @field_validator("snapshot_id", "snapshot_sha256")
74
+ @classmethod
75
+ def _snapshot_hash_valid(cls, value: str, info: Any) -> str:
76
+ return validate_sha256(value, info.field_name)
77
+
78
+
79
+ @runtime_checkable
80
+ class ToolCatalogSnapshot(Protocol):
81
+ """Synchronous exact-version tool catalog snapshot."""
82
+
83
+ @property
84
+ def snapshot_id(self) -> str: ...
85
+
86
+ @property
87
+ def snapshot_sha256(self) -> str: ...
88
+
89
+ def resolve_exact(self, tool_id: str, tool_version: int) -> ToolCatalogLookup: ...
90
+
91
+
92
+ @runtime_checkable
93
+ class ModelProfileCatalogSnapshot(Protocol):
94
+ """Synchronous exact-profile model catalog snapshot."""
95
+
96
+ @property
97
+ def snapshot_id(self) -> str: ...
98
+
99
+ @property
100
+ def snapshot_sha256(self) -> str: ...
101
+
102
+ def resolve_exact(self, profile_id: str) -> ModelProfileCatalogLookup: ...
103
+
104
+
105
+ class RawToolDescriptor(BaseModel):
106
+ """Closed raw descriptor accepted from a catalog snapshot."""
107
+
108
+ model_config = ConfigDict(extra="forbid", frozen=True)
109
+
110
+ tool_id: StrictStr
111
+ tool_version: StrictInt = Field(ge=1, le=TOOL_VERSION_MAX)
112
+ implementation_id: StrictStr
113
+ descriptor_sha256: StrictStr
114
+ model_tool_name: StrictStr
115
+ description: StrictStr
116
+ input_schema: Mapping[str, Any]
117
+ output_schema: Mapping[str, Any]
118
+ side_effect_class: SideEffectClass
119
+ idempotency: IdempotencyClass
120
+ required_capabilities: tuple[StrictStr, ...] = Field(default_factory=tuple)
121
+ produced_artifact_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
122
+
123
+ @field_validator("tool_id")
124
+ @classmethod
125
+ def _tool_id_valid(cls, value: str) -> str:
126
+ return validate_canonical_tool_id(value)
127
+
128
+ @field_validator("implementation_id")
129
+ @classmethod
130
+ def _implementation_id_valid(cls, value: str) -> str:
131
+ validate_nonblank(value, "implementation_id")
132
+ return validate_utf8_size(
133
+ value, "implementation_id", MAX_IMPLEMENTATION_ID_UTF8
134
+ )
135
+
136
+ @field_validator("descriptor_sha256")
137
+ @classmethod
138
+ def _descriptor_hash_valid(cls, value: str) -> str:
139
+ return validate_sha256(value, "descriptor_sha256")
140
+
141
+ @field_validator("model_tool_name")
142
+ @classmethod
143
+ def _model_tool_name_valid(cls, value: str) -> str:
144
+ validate_nonblank(value, "model_tool_name")
145
+ return validate_utf8_size(value, "model_tool_name", MAX_MODEL_TOOL_NAME_UTF8)
146
+
147
+ @field_validator("description")
148
+ @classmethod
149
+ def _description_valid(cls, value: str) -> str:
150
+ validate_nonblank(value, "description")
151
+ return validate_utf8_size(value, "description", MAX_TOOL_DESCRIPTION_UTF8)
152
+
153
+ @field_validator("required_capabilities")
154
+ @classmethod
155
+ def _capabilities_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
156
+ for capability in value:
157
+ validate_capability_id(capability)
158
+ return validate_unique(value, "required_capabilities")
159
+
160
+ @field_validator("produced_artifact_ids")
161
+ @classmethod
162
+ def _produced_artifacts_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
163
+ for artifact_id in value:
164
+ validate_artifact_id(artifact_id)
165
+ return validate_unique(value, "produced_artifact_ids")
166
+
167
+ @field_validator("input_schema", "output_schema")
168
+ @classmethod
169
+ def _schema_subset_valid(
170
+ cls, value: Mapping[str, Any], info: Any
171
+ ) -> Mapping[str, Any]:
172
+ _reject_runtime_values(value, path=f"/{info.field_name}")
173
+ validate_json_schema_subset(value, field_name=info.field_name)
174
+ return value
175
+
176
+
177
+ class ToolCatalogEntry(BaseModel):
178
+ """Deeply immutable compiler-facing admitted tool descriptor."""
179
+
180
+ model_config = ConfigDict(extra="forbid", frozen=True, arbitrary_types_allowed=True)
181
+
182
+ tool_id: StrictStr
183
+ tool_version: StrictInt = Field(ge=1, le=TOOL_VERSION_MAX)
184
+ implementation_id: StrictStr
185
+ descriptor_sha256: StrictStr
186
+ model_tool_name: StrictStr
187
+ description: StrictStr
188
+ input_schema: MappingProxyType[str, Any]
189
+ output_schema: MappingProxyType[str, Any]
190
+ side_effect_class: SideEffectClass
191
+ idempotency: IdempotencyClass
192
+ required_capabilities: tuple[StrictStr, ...] = Field(default_factory=tuple)
193
+ produced_artifact_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
194
+ timeout_policy: Any | None = None
195
+ output_policy: Any | None = None
196
+
197
+ @field_serializer("input_schema", "output_schema")
198
+ def _serialize_schema(self, value: MappingProxyType[str, Any]) -> Any:
199
+ return _thaw_json_value(value)
200
+
201
+ @classmethod
202
+ def admit(
203
+ cls,
204
+ descriptor: RawToolDescriptor | Mapping[str, Any],
205
+ *,
206
+ expected_tool_id: str,
207
+ expected_tool_version: int,
208
+ timeout_policy: Any | None = None,
209
+ output_policy: Any | None = None,
210
+ ) -> ToolCatalogEntry:
211
+ """Admit a raw descriptor into the immutable semantic boundary."""
212
+ expected_tool_id = validate_canonical_tool_id(expected_tool_id)
213
+ if expected_tool_version < 1 or expected_tool_version > TOOL_VERSION_MAX:
214
+ raise ValueError("expected_tool_version must be in range 1..2147483647")
215
+
216
+ raw = (
217
+ descriptor
218
+ if isinstance(descriptor, RawToolDescriptor)
219
+ else RawToolDescriptor.model_validate(descriptor)
220
+ )
221
+ if raw.tool_id != expected_tool_id:
222
+ raise ValueError("descriptor tool_id does not match requested tool_id")
223
+ if raw.tool_version != expected_tool_version:
224
+ raise ValueError(
225
+ "descriptor tool_version does not match requested tool_version"
226
+ )
227
+ _reject_runtime_values(raw.input_schema, path="/input_schema")
228
+ _reject_runtime_values(raw.output_schema, path="/output_schema")
229
+ return cls(
230
+ tool_id=raw.tool_id,
231
+ tool_version=raw.tool_version,
232
+ implementation_id=raw.implementation_id,
233
+ descriptor_sha256=raw.descriptor_sha256,
234
+ model_tool_name=raw.model_tool_name,
235
+ description=raw.description,
236
+ input_schema=validate_json_schema_subset(
237
+ raw.input_schema, field_name="input_schema"
238
+ ),
239
+ output_schema=validate_json_schema_subset(
240
+ raw.output_schema, field_name="output_schema"
241
+ ),
242
+ side_effect_class=raw.side_effect_class,
243
+ idempotency=raw.idempotency,
244
+ required_capabilities=raw.required_capabilities,
245
+ produced_artifact_ids=raw.produced_artifact_ids,
246
+ timeout_policy=timeout_policy,
247
+ output_policy=output_policy,
248
+ )
249
+
250
+
251
+ class ToolCatalogLookup(BaseModel):
252
+ """Closed exact-version tool lookup outcome."""
253
+
254
+ model_config = ConfigDict(extra="forbid", frozen=True)
255
+
256
+ classification: CatalogLookupClassification
257
+ entry: ToolCatalogEntry | None = None
258
+ error_code: StrictStr | None = None
259
+ evidence: tuple[tuple[StrictStr, StrictStr], ...] = Field(default_factory=tuple)
260
+
261
+ @field_validator("error_code")
262
+ @classmethod
263
+ def _error_code_valid(cls, value: str | None) -> str | None:
264
+ if value is None:
265
+ return None
266
+ validate_nonblank(value, "error_code")
267
+ return validate_utf8_size(value, "error_code", 128)
268
+
269
+ @model_validator(mode="after")
270
+ def _classification_shape_valid(self) -> ToolCatalogLookup:
271
+ if self.classification is CatalogLookupClassification.FOUND:
272
+ if self.entry is None:
273
+ raise ValueError("found tool lookups require an entry")
274
+ if self.error_code is not None or self.evidence:
275
+ raise ValueError("found tool lookups cannot carry errors")
276
+ elif self.entry is not None:
277
+ raise ValueError("missing or invalid tool lookups cannot carry an entry")
278
+ return self
279
+
280
+ @classmethod
281
+ def found(cls, entry: ToolCatalogEntry) -> ToolCatalogLookup:
282
+ return cls(classification=CatalogLookupClassification.FOUND, entry=entry)
283
+
284
+ @classmethod
285
+ def missing(
286
+ cls, *, error_code: str | None = None, evidence: Mapping[str, str] | None = None
287
+ ) -> ToolCatalogLookup:
288
+ return cls(
289
+ classification=CatalogLookupClassification.MISSING,
290
+ error_code=error_code,
291
+ evidence=_redacted_evidence(evidence or {}),
292
+ )
293
+
294
+ @classmethod
295
+ def invalid(
296
+ cls, *, error_code: str | None = None, evidence: Mapping[str, str] | None = None
297
+ ) -> ToolCatalogLookup:
298
+ return cls(
299
+ classification=CatalogLookupClassification.INVALID,
300
+ error_code=error_code,
301
+ evidence=_redacted_evidence(evidence or {}),
302
+ )
303
+
304
+
305
+ class ModelProfileCatalogLookup(BaseModel):
306
+ """Closed model-profile lookup outcome."""
307
+
308
+ model_config = ConfigDict(extra="forbid", frozen=True)
309
+
310
+ classification: CatalogLookupClassification
311
+ profile: CompiledModelProfile | None = None
312
+ error_code: StrictStr | None = None
313
+ evidence: tuple[tuple[StrictStr, StrictStr], ...] = Field(default_factory=tuple)
314
+
315
+ @field_validator("error_code")
316
+ @classmethod
317
+ def _error_code_valid(cls, value: str | None) -> str | None:
318
+ if value is None:
319
+ return None
320
+ validate_nonblank(value, "error_code")
321
+ return validate_utf8_size(value, "error_code", 128)
322
+
323
+ @model_validator(mode="after")
324
+ def _classification_shape_valid(self) -> ModelProfileCatalogLookup:
325
+ if self.classification is CatalogLookupClassification.FOUND:
326
+ if self.profile is None:
327
+ raise ValueError("found model-profile lookups require a profile")
328
+ if self.error_code is not None or self.evidence:
329
+ raise ValueError("found model-profile lookups cannot carry errors")
330
+ elif self.profile is not None:
331
+ raise ValueError(
332
+ "missing or invalid model-profile lookups cannot carry a profile"
333
+ )
334
+ return self
335
+
336
+ @classmethod
337
+ def found(cls, profile: CompiledModelProfile) -> ModelProfileCatalogLookup:
338
+ return cls(classification=CatalogLookupClassification.FOUND, profile=profile)
339
+
340
+ @classmethod
341
+ def missing(
342
+ cls, *, error_code: str | None = None, evidence: Mapping[str, str] | None = None
343
+ ) -> ModelProfileCatalogLookup:
344
+ return cls(
345
+ classification=CatalogLookupClassification.MISSING,
346
+ error_code=error_code,
347
+ evidence=_redacted_evidence(evidence or {}),
348
+ )
349
+
350
+ @classmethod
351
+ def invalid(
352
+ cls, *, error_code: str | None = None, evidence: Mapping[str, str] | None = None
353
+ ) -> ModelProfileCatalogLookup:
354
+ return cls(
355
+ classification=CatalogLookupClassification.INVALID,
356
+ error_code=error_code,
357
+ evidence=_redacted_evidence(evidence or {}),
358
+ )
359
+
360
+
361
+ def capture_catalog_snapshot_metadata(
362
+ snapshot: ToolCatalogSnapshot | ModelProfileCatalogSnapshot,
363
+ ) -> CatalogSnapshotMetadata:
364
+ """Read and validate snapshot metadata exactly once."""
365
+ try:
366
+ snapshot_id = snapshot.snapshot_id
367
+ snapshot_sha256 = snapshot.snapshot_sha256
368
+ except Exception as exc:
369
+ raise CatalogMetadataError(
370
+ "catalog snapshot metadata could not be read",
371
+ evidence={"error_type": type(exc).__name__},
372
+ ) from exc
373
+ try:
374
+ return CatalogSnapshotMetadata(
375
+ snapshot_id=snapshot_id,
376
+ snapshot_sha256=snapshot_sha256,
377
+ )
378
+ except ValueError as exc:
379
+ raise CatalogMetadataError(
380
+ "catalog snapshot metadata is invalid",
381
+ evidence={"error_type": type(exc).__name__},
382
+ ) from exc
383
+
384
+
385
+ def is_connector_discovery_snapshot_like(snapshot: object) -> bool:
386
+ """Return whether an object is discovery-shaped, not a semantic catalog."""
387
+ return all(
388
+ _has_static_attribute(snapshot, attribute)
389
+ for attribute in (
390
+ "connector_identity",
391
+ "provider_tools",
392
+ "discovery_snapshot_sha256",
393
+ )
394
+ )
395
+
396
+
397
+ def admit_model_profile(
398
+ raw_profile: CompiledModelProfile | Mapping[str, Any],
399
+ *,
400
+ expected_profile_id: str,
401
+ ) -> CompiledModelProfile:
402
+ """Admit only the accepted provider-neutral compiled profile contract."""
403
+ expected_profile_id = validate_profile_id(expected_profile_id)
404
+ if isinstance(raw_profile, CompiledModelProfile):
405
+ profile = raw_profile.model_copy(deep=True)
406
+ elif isinstance(raw_profile, Mapping):
407
+ _reject_forbidden_model_profile_material(raw_profile)
408
+ profile = CompiledModelProfile.model_validate(raw_profile)
409
+ else:
410
+ raise ValueError("model profile must be a CompiledModelProfile")
411
+ if profile.profile_id != expected_profile_id:
412
+ raise ValueError("model profile id does not match requested profile_id")
413
+ return profile
414
+
415
+
416
+ def _has_static_attribute(value: object, attribute: str) -> bool:
417
+ try:
418
+ getattr_static(value, attribute)
419
+ except AttributeError:
420
+ return False
421
+ return True
422
+
423
+
424
+ def _freeze_json_object(value: Mapping[str, Any]) -> MappingProxyType[str, Any]:
425
+ frozen = _freeze_json_value(dict(value), path="")
426
+ if not isinstance(frozen, MappingProxyType):
427
+ raise ValueError("schema must be a JSON object")
428
+ return frozen
429
+
430
+
431
+ def _freeze_json_value(value: Any, *, path: str) -> Any:
432
+ if isinstance(value, Mapping):
433
+ frozen_items: dict[str, Any] = {}
434
+ for key, item in value.items():
435
+ if not isinstance(key, str):
436
+ raise ValueError(f"{path or '/'} object keys must be strings")
437
+ frozen_items[key] = _freeze_json_value(item, path=f"{path}/{key}")
438
+ return MappingProxyType(frozen_items)
439
+ if isinstance(value, (list, tuple)):
440
+ return tuple(
441
+ _freeze_json_value(item, path=f"{path}/{index}")
442
+ for index, item in enumerate(value)
443
+ )
444
+ if isinstance(value, (str, int, float, bool)) or value is None:
445
+ json.dumps(value, allow_nan=False)
446
+ return value
447
+ raise ValueError(f"{path or '/'} must contain only JSON values")
448
+
449
+
450
+ def _thaw_json_value(value: Any) -> Any:
451
+ if isinstance(value, Mapping):
452
+ return {key: _thaw_json_value(item) for key, item in value.items()}
453
+ if isinstance(value, tuple):
454
+ return [_thaw_json_value(item) for item in value]
455
+ return value
456
+
457
+
458
+ def _reject_runtime_values(value: Any, *, path: str) -> None:
459
+ if callable(value):
460
+ raise ValueError(f"{path} must not contain callables")
461
+ if isinstance(value, Mapping):
462
+ for key, item in value.items():
463
+ _reject_runtime_values(item, path=f"{path}/{key}")
464
+ return
465
+ if isinstance(value, (list, tuple)):
466
+ for index, item in enumerate(value):
467
+ _reject_runtime_values(item, path=f"{path}/{index}")
468
+
469
+
470
+ def _reject_forbidden_model_profile_material(value: Mapping[str, Any]) -> None:
471
+ policy = RedactionPolicy()
472
+ for key, item in value.items():
473
+ if key != "profile_id":
474
+ raise ValueError("model profile contains runtime-only fields")
475
+ if isinstance(item, str) and detect_secret_candidate(
476
+ field_path=f"/{key}",
477
+ field_name=key,
478
+ value=item,
479
+ policy=policy,
480
+ ):
481
+ raise ValueError("model profile contains secret material")
482
+
483
+
484
+ def _redacted_evidence(values: Mapping[str, str]) -> tuple[tuple[str, str], ...]:
485
+ policy = RedactionPolicy()
486
+ redacted: list[tuple[str, str]] = []
487
+ for key in sorted(values):
488
+ value = values[key]
489
+ redacted_value = (
490
+ policy.replacement
491
+ if detect_secret_candidate(
492
+ field_path=f"/{key}",
493
+ field_name=key,
494
+ value=value,
495
+ policy=policy,
496
+ )
497
+ else value
498
+ )
499
+ redacted.append((key, validate_utf8_size(redacted_value, key, 512)))
500
+ return tuple(redacted)