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.
- millforge/__init__.py +1174 -0
- millforge/_forge/LICENSE +21 -0
- millforge/_forge/PROVENANCE.json +295 -0
- millforge/_forge/UPDATE_POLICY.md +24 -0
- millforge/_forge/__init__.py +14 -0
- millforge/_forge/adapter.py +2232 -0
- millforge/_forge/base_runner.py +121 -0
- millforge/_forge/clients/__init__.py +10 -0
- millforge/_forge/clients/base.py +200 -0
- millforge/_forge/context/__init__.py +23 -0
- millforge/_forge/context/manager.py +178 -0
- millforge/_forge/context/strategies.py +335 -0
- millforge/_forge/core/__init__.py +16 -0
- millforge/_forge/core/inference.py +433 -0
- millforge/_forge/core/messages.py +119 -0
- millforge/_forge/core/runner.py +479 -0
- millforge/_forge/core/steps.py +108 -0
- millforge/_forge/core/workflow.py +400 -0
- millforge/_forge/errors.py +222 -0
- millforge/_forge/guardrails/__init__.py +21 -0
- millforge/_forge/guardrails/error_tracker.py +71 -0
- millforge/_forge/guardrails/guardrails.py +194 -0
- millforge/_forge/guardrails/nudge.py +47 -0
- millforge/_forge/guardrails/response_validator.py +119 -0
- millforge/_forge/guardrails/step_enforcer.py +183 -0
- millforge/_forge/prompts/__init__.py +16 -0
- millforge/_forge/prompts/nudges.py +95 -0
- millforge/_forge/prompts/templates.py +285 -0
- millforge/_version.py +3 -0
- millforge/artifacts.py +570 -0
- millforge/base/__init__.py +97 -0
- millforge/base/composition.py +402 -0
- millforge/base/context.py +285 -0
- millforge/base/harness.py +138 -0
- millforge/base/identity.py +465 -0
- millforge/base/options.py +34 -0
- millforge/base/platform.py +17 -0
- millforge/base/prompt.py +317 -0
- millforge/base/runner.py +546 -0
- millforge/compiled_plan.py +970 -0
- millforge/compiler/__init__.py +231 -0
- millforge/compiler/artifact_validation.py +257 -0
- millforge/compiler/canonicalization.py +169 -0
- millforge/compiler/capabilities.py +66 -0
- millforge/compiler/catalogs.py +500 -0
- millforge/compiler/diagnostics.py +491 -0
- millforge/compiler/graph.py +678 -0
- millforge/compiler/lowering.py +198 -0
- millforge/compiler/output.py +692 -0
- millforge/compiler/parsing.py +1424 -0
- millforge/compiler/requests.py +1180 -0
- millforge/compiler/schema_validation.py +272 -0
- millforge/compiler/semantic.py +490 -0
- millforge/compiler/service.py +448 -0
- millforge/compiler/source.py +375 -0
- millforge/compiler/validators.py +184 -0
- millforge/connectors/__init__.py +95 -0
- millforge/connectors/admission.py +801 -0
- millforge/connectors/broker.py +202 -0
- millforge/connectors/contracts.py +1159 -0
- millforge/connectors/diagnostics.py +189 -0
- millforge/connectors/fake.py +66 -0
- millforge/connectors/runtime.py +236 -0
- millforge/contracts.py +2860 -0
- millforge/custom_tools/__init__.py +67 -0
- millforge/custom_tools/compiler.py +724 -0
- millforge/custom_tools/contracts.py +1093 -0
- millforge/custom_tools/diagnostics.py +205 -0
- millforge/eval_artifacts.py +952 -0
- millforge/eval_boundary.py +2435 -0
- millforge/eval_fixtures/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
- millforge/eval_fixtures/default_pack/manifest.json +12 -0
- millforge/eval_modes.py +1282 -0
- millforge/eval_presets.py +1398 -0
- millforge/eval_reports.py +2517 -0
- millforge/eval_suite.py +2429 -0
- millforge/eval_trials.py +2632 -0
- millforge/eval_workflow.py +794 -0
- millforge/exceptions.py +122 -0
- millforge/model_backend.py +2098 -0
- millforge/protocols.py +340 -0
- millforge/py.typed +0 -0
- millforge/runtime.py +1791 -0
- millforge/testing/__init__.py +1089 -0
- millforge/tools/__init__.py +83 -0
- millforge/tools/builtin_runtime.py +1339 -0
- millforge/tools/builtins.py +773 -0
- millforge/tools/execution.py +1545 -0
- millforge/tools/path_policy.py +155 -0
- millforge/tools/pi_compat/PI_LICENSE +21 -0
- millforge/tools/pi_compat/PROVENANCE.json +55 -0
- millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
- millforge/tools/pi_compat/__init__.py +34 -0
- millforge/tools/pi_compat/contracts.py +49 -0
- millforge/tools/pi_compat/editing.py +390 -0
- millforge/tools/pi_compat/mutations.py +57 -0
- millforge/tools/pi_compat/operations.py +401 -0
- millforge/tools/pi_compat/paths.py +155 -0
- millforge/tools/pi_compat/process.py +1375 -0
- millforge/tools/pi_compat/search.py +738 -0
- millforge/tools/pi_compat/truncation.py +267 -0
- millforge/tools/pi_compat_catalog.py +396 -0
- millforge/tools/pi_compat_runtime.py +460 -0
- millforge/tools/registry.py +553 -0
- millforge/tools/results.py +533 -0
- millforge-0.1.0.dist-info/METADATA +844 -0
- millforge-0.1.0.dist-info/RECORD +116 -0
- millforge-0.1.0.dist-info/WHEEL +4 -0
- 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)
|