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,553 @@
|
|
|
1
|
+
"""Immutable tool descriptor contracts and deterministic registry snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Any, ClassVar, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import (
|
|
12
|
+
BaseModel,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Field,
|
|
15
|
+
StrictBool,
|
|
16
|
+
StrictInt,
|
|
17
|
+
StrictStr,
|
|
18
|
+
field_serializer,
|
|
19
|
+
field_validator,
|
|
20
|
+
model_validator,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from millforge import IdempotencyClass, SideEffectClass, canonical_json_serialize
|
|
24
|
+
from millforge.compiler.catalogs import (
|
|
25
|
+
CatalogSnapshotMetadata,
|
|
26
|
+
MAX_CAPABILITY_ID_UTF8,
|
|
27
|
+
MAX_IMPLEMENTATION_ID_UTF8,
|
|
28
|
+
MAX_MODEL_TOOL_NAME_UTF8,
|
|
29
|
+
MAX_TOOL_DESCRIPTION_UTF8,
|
|
30
|
+
RawToolDescriptor,
|
|
31
|
+
ToolCatalogEntry,
|
|
32
|
+
ToolCatalogLookup,
|
|
33
|
+
ToolCatalogSnapshot,
|
|
34
|
+
)
|
|
35
|
+
from millforge.compiler.diagnostics import detect_secret_candidate
|
|
36
|
+
from millforge.compiler.schema_validation import validate_json_schema_subset
|
|
37
|
+
from millforge.compiler.validators import (
|
|
38
|
+
validate_artifact_id,
|
|
39
|
+
validate_capability_id,
|
|
40
|
+
validate_canonical_tool_id,
|
|
41
|
+
validate_nonblank,
|
|
42
|
+
validate_tool_version,
|
|
43
|
+
validate_unique,
|
|
44
|
+
validate_utf8_size,
|
|
45
|
+
)
|
|
46
|
+
from millforge.contracts import RedactionPolicy
|
|
47
|
+
|
|
48
|
+
DESCRIPTOR_SCHEMA_VERSION = 1
|
|
49
|
+
DESCRIPTOR_HASH_KIND = "millforge.tool_descriptor.v1"
|
|
50
|
+
SNAPSHOT_KIND = "millforge.tool_registry.snapshot.v1"
|
|
51
|
+
SNAPSHOT_ID_KIND = "millforge.tool_registry.snapshot_id.v1"
|
|
52
|
+
|
|
53
|
+
MAX_TIMEOUT_SECONDS = 86_400
|
|
54
|
+
MAX_CANCELLATION_GRACE_SECONDS = 3_600
|
|
55
|
+
MAX_OUTPUT_BYTES = 64 * 1024 * 1024
|
|
56
|
+
MAX_OUTPUT_SUMMARY_UTF8 = 65_536
|
|
57
|
+
|
|
58
|
+
_SIDE_EFFECTS_REQUIRING_CAPABILITY = frozenset(
|
|
59
|
+
item for item in SideEffectClass if item is not SideEffectClass.READ_ONLY
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
JsonValue = Any
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ToolRegistryErrorCode(str, Enum):
|
|
67
|
+
"""Stable registry error categories."""
|
|
68
|
+
|
|
69
|
+
DESCRIPTOR_INVALID = "descriptor_invalid"
|
|
70
|
+
DUPLICATE_TOOL = "duplicate_tool"
|
|
71
|
+
DUPLICATE_IMPLEMENTATION = "duplicate_implementation"
|
|
72
|
+
REGISTRY_FROZEN = "registry_frozen"
|
|
73
|
+
LOOKUP_INVALID = "lookup_invalid"
|
|
74
|
+
LOOKUP_MISSING = "lookup_missing"
|
|
75
|
+
PROJECTION_INVALID = "projection_invalid"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ToolRegistryError(ValueError):
|
|
79
|
+
"""Typed deterministic registry error with redacted evidence."""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
code: ToolRegistryErrorCode,
|
|
84
|
+
message: str,
|
|
85
|
+
*,
|
|
86
|
+
evidence: Mapping[str, str] | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
super().__init__(message)
|
|
89
|
+
self.code = code
|
|
90
|
+
self.evidence = _redacted_evidence(evidence or {})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ToolTimeoutPolicy(BaseModel):
|
|
94
|
+
"""Descriptor-owned timeout policy data for later execution stages."""
|
|
95
|
+
|
|
96
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
97
|
+
|
|
98
|
+
timeout_seconds: StrictInt = Field(ge=1, le=MAX_TIMEOUT_SECONDS)
|
|
99
|
+
cancellation_grace_seconds: StrictInt = Field(
|
|
100
|
+
ge=1, le=MAX_CANCELLATION_GRACE_SECONDS
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ToolOutputPolicy(BaseModel):
|
|
105
|
+
"""Descriptor-owned output policy data for later execution stages."""
|
|
106
|
+
|
|
107
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
108
|
+
|
|
109
|
+
max_output_bytes: StrictInt = Field(ge=1, le=MAX_OUTPUT_BYTES)
|
|
110
|
+
max_summary_utf8: StrictInt = Field(ge=1, le=MAX_OUTPUT_SUMMARY_UTF8)
|
|
111
|
+
redact_secrets: StrictBool
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ToolDescriptor(BaseModel):
|
|
115
|
+
"""Immutable registry-owned descriptor with a computed SHA-256 identity."""
|
|
116
|
+
|
|
117
|
+
model_config = ConfigDict(
|
|
118
|
+
frozen=True,
|
|
119
|
+
extra="forbid",
|
|
120
|
+
arbitrary_types_allowed=True,
|
|
121
|
+
hide_input_in_errors=True,
|
|
122
|
+
use_enum_values=False,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
schema_version: Literal[1] = 1
|
|
126
|
+
kind: Literal["millforge.tool_descriptor.v1"] = "millforge.tool_descriptor.v1"
|
|
127
|
+
tool_id: StrictStr
|
|
128
|
+
tool_version: StrictInt = Field(ge=1)
|
|
129
|
+
implementation_id: StrictStr
|
|
130
|
+
model_tool_name: StrictStr
|
|
131
|
+
description: StrictStr
|
|
132
|
+
input_schema: MappingProxyType[str, JsonValue]
|
|
133
|
+
output_schema: MappingProxyType[str, JsonValue]
|
|
134
|
+
required_capabilities: tuple[StrictStr, ...] = Field(default_factory=tuple)
|
|
135
|
+
produced_artifact_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
|
|
136
|
+
side_effect_class: SideEffectClass
|
|
137
|
+
idempotency: IdempotencyClass
|
|
138
|
+
timeout_policy: ToolTimeoutPolicy
|
|
139
|
+
output_policy: ToolOutputPolicy
|
|
140
|
+
|
|
141
|
+
_secret_policy: ClassVar[RedactionPolicy] = RedactionPolicy()
|
|
142
|
+
|
|
143
|
+
@field_validator("tool_id")
|
|
144
|
+
@classmethod
|
|
145
|
+
def _tool_id_valid(cls, value: str) -> str:
|
|
146
|
+
return validate_canonical_tool_id(value)
|
|
147
|
+
|
|
148
|
+
@field_validator("tool_version")
|
|
149
|
+
@classmethod
|
|
150
|
+
def _tool_version_valid(cls, value: int) -> int:
|
|
151
|
+
return validate_tool_version(value)
|
|
152
|
+
|
|
153
|
+
@field_validator("implementation_id")
|
|
154
|
+
@classmethod
|
|
155
|
+
def _implementation_id_valid(cls, value: str) -> str:
|
|
156
|
+
_validate_descriptor_string(
|
|
157
|
+
value, "implementation_id", MAX_IMPLEMENTATION_ID_UTF8
|
|
158
|
+
)
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
@field_validator("model_tool_name")
|
|
162
|
+
@classmethod
|
|
163
|
+
def _model_tool_name_valid(cls, value: str) -> str:
|
|
164
|
+
_validate_descriptor_string(value, "model_tool_name", MAX_MODEL_TOOL_NAME_UTF8)
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
@field_validator("description")
|
|
168
|
+
@classmethod
|
|
169
|
+
def _description_valid(cls, value: str) -> str:
|
|
170
|
+
_validate_descriptor_string(value, "description", MAX_TOOL_DESCRIPTION_UTF8)
|
|
171
|
+
return value
|
|
172
|
+
|
|
173
|
+
@field_validator("input_schema", "output_schema", mode="before")
|
|
174
|
+
@classmethod
|
|
175
|
+
def _schema_valid(cls, value: Any, info: Any) -> MappingProxyType[str, Any]:
|
|
176
|
+
_reject_runtime_values(value, path=f"/{info.field_name}")
|
|
177
|
+
_reject_secret_values(value, path=f"/{info.field_name}")
|
|
178
|
+
return validate_json_schema_subset(value, field_name=info.field_name)
|
|
179
|
+
|
|
180
|
+
@field_validator("required_capabilities", mode="before")
|
|
181
|
+
@classmethod
|
|
182
|
+
def _capabilities_snapshot(cls, value: Any) -> tuple[str, ...]:
|
|
183
|
+
return _sorted_string_tuple(value, "required_capabilities")
|
|
184
|
+
|
|
185
|
+
@field_validator("required_capabilities")
|
|
186
|
+
@classmethod
|
|
187
|
+
def _capabilities_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
188
|
+
for capability in value:
|
|
189
|
+
_validate_descriptor_string(
|
|
190
|
+
capability, "required_capabilities", MAX_CAPABILITY_ID_UTF8
|
|
191
|
+
)
|
|
192
|
+
validate_capability_id(capability)
|
|
193
|
+
return validate_unique(value, "required_capabilities")
|
|
194
|
+
|
|
195
|
+
@field_validator("produced_artifact_ids", mode="before")
|
|
196
|
+
@classmethod
|
|
197
|
+
def _produced_artifacts_snapshot(cls, value: Any) -> tuple[str, ...]:
|
|
198
|
+
return _sorted_string_tuple(value, "produced_artifact_ids")
|
|
199
|
+
|
|
200
|
+
@field_validator("produced_artifact_ids")
|
|
201
|
+
@classmethod
|
|
202
|
+
def _produced_artifacts_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
203
|
+
for artifact_id in value:
|
|
204
|
+
validate_artifact_id(artifact_id)
|
|
205
|
+
return validate_unique(value, "produced_artifact_ids")
|
|
206
|
+
|
|
207
|
+
@model_validator(mode="after")
|
|
208
|
+
def _descriptor_semantics_valid(self) -> ToolDescriptor:
|
|
209
|
+
if (
|
|
210
|
+
self.side_effect_class in _SIDE_EFFECTS_REQUIRING_CAPABILITY
|
|
211
|
+
or self.produced_artifact_ids
|
|
212
|
+
) and not self.required_capabilities:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"side-effecting or artifact-producing descriptors require capabilities"
|
|
215
|
+
)
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def descriptor_sha256(self) -> str:
|
|
220
|
+
return hashlib.sha256(
|
|
221
|
+
canonical_json_serialize(descriptor_hash_payload(self)).encode("utf-8")
|
|
222
|
+
).hexdigest()
|
|
223
|
+
|
|
224
|
+
@field_serializer("input_schema", "output_schema")
|
|
225
|
+
def _serialize_schema(self, value: MappingProxyType[str, Any]) -> Any:
|
|
226
|
+
return _thaw_json_value(value)
|
|
227
|
+
|
|
228
|
+
def to_raw_descriptor(self) -> RawToolDescriptor:
|
|
229
|
+
"""Project into the accepted compiler catalog descriptor contract."""
|
|
230
|
+
return RawToolDescriptor(
|
|
231
|
+
tool_id=self.tool_id,
|
|
232
|
+
tool_version=self.tool_version,
|
|
233
|
+
implementation_id=self.implementation_id,
|
|
234
|
+
descriptor_sha256=self.descriptor_sha256,
|
|
235
|
+
model_tool_name=self.model_tool_name,
|
|
236
|
+
description=self.description,
|
|
237
|
+
input_schema=_thaw_json_value(self.input_schema),
|
|
238
|
+
output_schema=_thaw_json_value(self.output_schema),
|
|
239
|
+
side_effect_class=self.side_effect_class,
|
|
240
|
+
idempotency=self.idempotency,
|
|
241
|
+
required_capabilities=self.required_capabilities,
|
|
242
|
+
produced_artifact_ids=self.produced_artifact_ids,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def to_catalog_entry(self) -> ToolCatalogEntry:
|
|
246
|
+
"""Project into an immutable compiler catalog entry."""
|
|
247
|
+
return ToolCatalogEntry.admit(
|
|
248
|
+
self.to_raw_descriptor(),
|
|
249
|
+
expected_tool_id=self.tool_id,
|
|
250
|
+
expected_tool_version=self.tool_version,
|
|
251
|
+
timeout_policy=self.timeout_policy,
|
|
252
|
+
output_policy=self.output_policy,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class FrozenDescriptorHashRecord(BaseModel):
|
|
257
|
+
"""Immutable descriptor identity record covered by snapshot hashing."""
|
|
258
|
+
|
|
259
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
260
|
+
|
|
261
|
+
tool_id: StrictStr
|
|
262
|
+
tool_version: StrictInt = Field(ge=1)
|
|
263
|
+
implementation_id: StrictStr
|
|
264
|
+
descriptor_sha256: StrictStr
|
|
265
|
+
|
|
266
|
+
@field_validator("tool_id")
|
|
267
|
+
@classmethod
|
|
268
|
+
def _tool_id_valid(cls, value: str) -> str:
|
|
269
|
+
return validate_canonical_tool_id(value)
|
|
270
|
+
|
|
271
|
+
@field_validator("tool_version")
|
|
272
|
+
@classmethod
|
|
273
|
+
def _tool_version_valid(cls, value: int) -> int:
|
|
274
|
+
return validate_tool_version(value)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class FrozenToolRegistrySnapshot:
|
|
278
|
+
"""Immutable exact-version lookup snapshot for semantic compilation."""
|
|
279
|
+
|
|
280
|
+
def __init__(self, descriptors: tuple[ToolDescriptor, ...]) -> None:
|
|
281
|
+
entries: dict[tuple[str, int], ToolCatalogEntry] = {}
|
|
282
|
+
records: list[FrozenDescriptorHashRecord] = []
|
|
283
|
+
for descriptor in descriptors:
|
|
284
|
+
try:
|
|
285
|
+
entry = descriptor.to_catalog_entry()
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
raise ToolRegistryError(
|
|
288
|
+
ToolRegistryErrorCode.PROJECTION_INVALID,
|
|
289
|
+
"descriptor projection failed",
|
|
290
|
+
evidence={"error_type": type(exc).__name__},
|
|
291
|
+
) from exc
|
|
292
|
+
entries[(descriptor.tool_id, descriptor.tool_version)] = entry
|
|
293
|
+
records.append(
|
|
294
|
+
FrozenDescriptorHashRecord(
|
|
295
|
+
tool_id=descriptor.tool_id,
|
|
296
|
+
tool_version=descriptor.tool_version,
|
|
297
|
+
implementation_id=entry.implementation_id,
|
|
298
|
+
descriptor_sha256=entry.descriptor_sha256,
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
self._entries = MappingProxyType(dict(sorted(entries.items())))
|
|
302
|
+
self._descriptor_hash_records = tuple(
|
|
303
|
+
sorted(records, key=_descriptor_hash_record_identity_key)
|
|
304
|
+
)
|
|
305
|
+
snapshot_payload = {
|
|
306
|
+
"schema_version": 1,
|
|
307
|
+
"kind": SNAPSHOT_KIND,
|
|
308
|
+
"descriptors": [
|
|
309
|
+
record.model_dump(mode="json")
|
|
310
|
+
for record in self._descriptor_hash_records
|
|
311
|
+
],
|
|
312
|
+
}
|
|
313
|
+
snapshot_sha256 = hashlib.sha256(
|
|
314
|
+
canonical_json_serialize(snapshot_payload).encode("utf-8")
|
|
315
|
+
).hexdigest()
|
|
316
|
+
snapshot_id_payload = {
|
|
317
|
+
"schema_version": 1,
|
|
318
|
+
"kind": SNAPSHOT_ID_KIND,
|
|
319
|
+
"snapshot_sha256": snapshot_sha256,
|
|
320
|
+
}
|
|
321
|
+
snapshot_id = hashlib.sha256(
|
|
322
|
+
canonical_json_serialize(snapshot_id_payload).encode("utf-8")
|
|
323
|
+
).hexdigest()
|
|
324
|
+
self._snapshot_sha256 = snapshot_sha256
|
|
325
|
+
self._snapshot_id = snapshot_id
|
|
326
|
+
CatalogSnapshotMetadata(
|
|
327
|
+
snapshot_id=self._snapshot_id,
|
|
328
|
+
snapshot_sha256=self._snapshot_sha256,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def snapshot_id(self) -> str:
|
|
333
|
+
return self._snapshot_id
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def snapshot_sha256(self) -> str:
|
|
337
|
+
return self._snapshot_sha256
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def descriptor_hash_records(self) -> tuple[FrozenDescriptorHashRecord, ...]:
|
|
341
|
+
return self._descriptor_hash_records
|
|
342
|
+
|
|
343
|
+
def resolve_exact(self, tool_id: str, tool_version: int) -> ToolCatalogLookup:
|
|
344
|
+
try:
|
|
345
|
+
if _looks_like_alias(tool_id):
|
|
346
|
+
raise ValueError("tool_id aliases are not supported")
|
|
347
|
+
if not isinstance(tool_version, int) or isinstance(tool_version, bool):
|
|
348
|
+
raise ValueError("tool_version must be an integer")
|
|
349
|
+
valid_tool_id = validate_canonical_tool_id(tool_id)
|
|
350
|
+
valid_tool_version = validate_tool_version(tool_version)
|
|
351
|
+
except Exception:
|
|
352
|
+
return ToolCatalogLookup.invalid(
|
|
353
|
+
error_code=ToolRegistryErrorCode.LOOKUP_INVALID.value,
|
|
354
|
+
evidence={
|
|
355
|
+
"tool_id": _safe_evidence_text(tool_id),
|
|
356
|
+
"tool_version": _safe_evidence_text(tool_version),
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
entry = self._entries.get((valid_tool_id, valid_tool_version))
|
|
360
|
+
if entry is None:
|
|
361
|
+
return ToolCatalogLookup.missing(
|
|
362
|
+
error_code=ToolRegistryErrorCode.LOOKUP_MISSING.value,
|
|
363
|
+
evidence={"tool_id": valid_tool_id, "tool_version": str(tool_version)},
|
|
364
|
+
)
|
|
365
|
+
return ToolCatalogLookup.found(entry)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class ToolRegistry:
|
|
369
|
+
"""Explicit in-process registry for immutable descriptors."""
|
|
370
|
+
|
|
371
|
+
def __init__(self) -> None:
|
|
372
|
+
self._descriptors: dict[tuple[str, int], ToolDescriptor] = {}
|
|
373
|
+
self._implementation_ids: set[str] = set()
|
|
374
|
+
self._snapshot: FrozenToolRegistrySnapshot | None = None
|
|
375
|
+
self._frozen = False
|
|
376
|
+
|
|
377
|
+
def register(self, descriptor: ToolDescriptor) -> None:
|
|
378
|
+
if self._frozen:
|
|
379
|
+
raise ToolRegistryError(
|
|
380
|
+
ToolRegistryErrorCode.REGISTRY_FROZEN,
|
|
381
|
+
"registry is frozen",
|
|
382
|
+
)
|
|
383
|
+
if not isinstance(descriptor, ToolDescriptor):
|
|
384
|
+
raise ToolRegistryError(
|
|
385
|
+
ToolRegistryErrorCode.DESCRIPTOR_INVALID,
|
|
386
|
+
"descriptor must be a ToolDescriptor",
|
|
387
|
+
)
|
|
388
|
+
key = (descriptor.tool_id, descriptor.tool_version)
|
|
389
|
+
if key in self._descriptors:
|
|
390
|
+
raise ToolRegistryError(
|
|
391
|
+
ToolRegistryErrorCode.DUPLICATE_TOOL,
|
|
392
|
+
"duplicate tool descriptor",
|
|
393
|
+
evidence={"tool_id": descriptor.tool_id},
|
|
394
|
+
)
|
|
395
|
+
if descriptor.implementation_id in self._implementation_ids:
|
|
396
|
+
raise ToolRegistryError(
|
|
397
|
+
ToolRegistryErrorCode.DUPLICATE_IMPLEMENTATION,
|
|
398
|
+
"duplicate implementation id",
|
|
399
|
+
evidence={"implementation_id": descriptor.implementation_id},
|
|
400
|
+
)
|
|
401
|
+
try:
|
|
402
|
+
descriptor.to_catalog_entry()
|
|
403
|
+
except Exception as exc:
|
|
404
|
+
raise ToolRegistryError(
|
|
405
|
+
ToolRegistryErrorCode.PROJECTION_INVALID,
|
|
406
|
+
"descriptor projection failed",
|
|
407
|
+
evidence={"error_type": type(exc).__name__},
|
|
408
|
+
) from exc
|
|
409
|
+
self._descriptors[key] = descriptor
|
|
410
|
+
self._implementation_ids.add(descriptor.implementation_id)
|
|
411
|
+
|
|
412
|
+
def freeze(self) -> FrozenToolRegistrySnapshot:
|
|
413
|
+
if self._snapshot is None:
|
|
414
|
+
descriptors = tuple(
|
|
415
|
+
descriptor
|
|
416
|
+
for _, descriptor in sorted(
|
|
417
|
+
self._descriptors.items(),
|
|
418
|
+
key=lambda item: _descriptor_identity_key(item[1]),
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
self._snapshot = FrozenToolRegistrySnapshot(descriptors)
|
|
422
|
+
self._frozen = True
|
|
423
|
+
return self._snapshot
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def descriptor_hash_payload(descriptor: ToolDescriptor) -> dict[str, Any]:
|
|
427
|
+
"""Return the deterministic descriptor payload covered by SHA-256."""
|
|
428
|
+
return {
|
|
429
|
+
"schema_version": descriptor.schema_version,
|
|
430
|
+
"kind": descriptor.kind,
|
|
431
|
+
"tool_id": descriptor.tool_id,
|
|
432
|
+
"tool_version": descriptor.tool_version,
|
|
433
|
+
"implementation_id": descriptor.implementation_id,
|
|
434
|
+
"model_tool_name": descriptor.model_tool_name,
|
|
435
|
+
"description": descriptor.description,
|
|
436
|
+
"input_schema": _thaw_json_value(descriptor.input_schema),
|
|
437
|
+
"output_schema": _thaw_json_value(descriptor.output_schema),
|
|
438
|
+
"required_capabilities": list(descriptor.required_capabilities),
|
|
439
|
+
"produced_artifact_ids": list(descriptor.produced_artifact_ids),
|
|
440
|
+
"side_effect_class": descriptor.side_effect_class.value,
|
|
441
|
+
"idempotency": descriptor.idempotency.value,
|
|
442
|
+
"timeout_policy": descriptor.timeout_policy.model_dump(mode="json"),
|
|
443
|
+
"output_policy": descriptor.output_policy.model_dump(mode="json"),
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _descriptor_identity_key(
|
|
448
|
+
descriptor: ToolDescriptor,
|
|
449
|
+
) -> tuple[str, int, str, str, str]:
|
|
450
|
+
return (
|
|
451
|
+
descriptor.tool_id,
|
|
452
|
+
descriptor.tool_version,
|
|
453
|
+
descriptor.model_tool_name,
|
|
454
|
+
descriptor.implementation_id,
|
|
455
|
+
descriptor.descriptor_sha256,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _descriptor_hash_record_identity_key(
|
|
460
|
+
record: FrozenDescriptorHashRecord,
|
|
461
|
+
) -> tuple[str, int, str, str]:
|
|
462
|
+
return (
|
|
463
|
+
record.tool_id,
|
|
464
|
+
record.tool_version,
|
|
465
|
+
record.implementation_id,
|
|
466
|
+
record.descriptor_sha256,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _sorted_string_tuple(value: Any, field_name: str) -> tuple[str, ...]:
|
|
471
|
+
if value is None:
|
|
472
|
+
return ()
|
|
473
|
+
if isinstance(value, str) or not isinstance(value, (list, tuple)):
|
|
474
|
+
raise ValueError(f"{field_name} must be an array")
|
|
475
|
+
return tuple(sorted(value))
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _validate_descriptor_string(value: str, field_name: str, maximum: int) -> None:
|
|
479
|
+
validate_nonblank(value, field_name)
|
|
480
|
+
validate_utf8_size(value, field_name, maximum)
|
|
481
|
+
if detect_secret_candidate(
|
|
482
|
+
field_path=f"/{field_name}",
|
|
483
|
+
field_name=field_name,
|
|
484
|
+
value=value,
|
|
485
|
+
policy=RedactionPolicy(),
|
|
486
|
+
):
|
|
487
|
+
raise ValueError(f"{field_name} contains suspected secret material")
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _reject_runtime_values(value: Any, *, path: str) -> None:
|
|
491
|
+
if callable(value):
|
|
492
|
+
raise ValueError(f"{path} must not contain runtime values")
|
|
493
|
+
if isinstance(value, Mapping):
|
|
494
|
+
for key, item in value.items():
|
|
495
|
+
_reject_runtime_values(item, path=f"{path}/{key}")
|
|
496
|
+
return
|
|
497
|
+
if isinstance(value, (list, tuple)):
|
|
498
|
+
for index, item in enumerate(value):
|
|
499
|
+
_reject_runtime_values(item, path=f"{path}/{index}")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _reject_secret_values(value: Any, *, path: str) -> None:
|
|
503
|
+
if isinstance(value, Mapping):
|
|
504
|
+
for key, item in value.items():
|
|
505
|
+
_reject_secret_values(item, path=f"{path}/{key}")
|
|
506
|
+
return
|
|
507
|
+
if isinstance(value, (list, tuple)):
|
|
508
|
+
for index, item in enumerate(value):
|
|
509
|
+
_reject_secret_values(item, path=f"{path}/{index}")
|
|
510
|
+
return
|
|
511
|
+
if isinstance(value, str) and detect_secret_candidate(
|
|
512
|
+
field_path="/descriptor_schema_value",
|
|
513
|
+
field_name="descriptor_schema_value",
|
|
514
|
+
value=value,
|
|
515
|
+
policy=RedactionPolicy(),
|
|
516
|
+
):
|
|
517
|
+
raise ValueError("schema contains suspected secret material")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _looks_like_alias(tool_id: Any) -> bool:
|
|
521
|
+
return isinstance(tool_id, str) and (
|
|
522
|
+
tool_id == "latest" or tool_id.endswith(".latest")
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _thaw_json_value(value: Any) -> Any:
|
|
527
|
+
if isinstance(value, Mapping):
|
|
528
|
+
return {key: _thaw_json_value(item) for key, item in value.items()}
|
|
529
|
+
if isinstance(value, tuple):
|
|
530
|
+
return [_thaw_json_value(item) for item in value]
|
|
531
|
+
return value
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _redacted_evidence(values: Mapping[str, str]) -> tuple[tuple[str, str], ...]:
|
|
535
|
+
return tuple(
|
|
536
|
+
sorted((key, _safe_evidence_text(value)) for key, value in values.items())
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _safe_evidence_text(value: Any) -> str:
|
|
541
|
+
text = str(value)
|
|
542
|
+
policy = RedactionPolicy()
|
|
543
|
+
if detect_secret_candidate(
|
|
544
|
+
field_path="/evidence",
|
|
545
|
+
field_name="evidence",
|
|
546
|
+
value=text,
|
|
547
|
+
policy=policy,
|
|
548
|
+
):
|
|
549
|
+
return policy.replacement
|
|
550
|
+
return validate_utf8_size(text, "evidence", 512)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
assert isinstance(FrozenToolRegistrySnapshot(()), ToolCatalogSnapshot)
|