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,1159 @@
1
+ """Frozen connector identity, discovery, admission, and diagnostic contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from collections.abc import Mapping
9
+ from datetime import datetime, timezone
10
+ from enum import Enum
11
+ from types import MappingProxyType
12
+ from typing import Any, ClassVar
13
+
14
+ from pydantic import (
15
+ BaseModel,
16
+ ConfigDict,
17
+ Field,
18
+ StrictInt,
19
+ StrictStr,
20
+ ValidationError,
21
+ field_serializer,
22
+ field_validator,
23
+ model_validator,
24
+ )
25
+
26
+ from millforge import IdempotencyClass, SideEffectClass, canonical_json_serialize
27
+ from millforge.compiler.diagnostics import detect_secret_candidate
28
+ from millforge.compiler.schema_validation import normalize_json_schema
29
+ from millforge.compiler.validators import (
30
+ validate_capability_id,
31
+ validate_canonical_tool_id,
32
+ validate_nonblank,
33
+ validate_sha256,
34
+ validate_tool_version,
35
+ validate_unique,
36
+ validate_utf8_size,
37
+ )
38
+ from millforge.connectors.diagnostics import (
39
+ ConnectorDiagnostic,
40
+ ConnectorDiagnosticCode,
41
+ ConnectorDiagnosticPhase,
42
+ malformed_input_diagnostic,
43
+ )
44
+ from millforge.contracts import RedactionPolicy
45
+ from millforge.tools.registry import ToolDescriptor, ToolOutputPolicy, ToolTimeoutPolicy
46
+
47
+ CONNECTOR_IDENTITY_HASH_KIND = "millforge.connector.identity.v1"
48
+ CONNECTOR_DISCOVERY_TOOL_HASH_KIND = "millforge.connector.discovery_tool.v1"
49
+ CONNECTOR_DISCOVERY_SNAPSHOT_HASH_KIND = "millforge.connector.discovery_snapshot.v1"
50
+ CONNECTOR_ADMISSION_RECORD_HASH_KIND = "millforge.connector.admission_record.v1"
51
+ CONNECTOR_DISCOVERY_SNAPSHOT_SCHEMA = "millforge.connector.discovery_snapshot"
52
+ CONNECTOR_DISCOVERY_SNAPSHOT_KIND = "connector_discovery_snapshot"
53
+ CONNECTOR_DISCOVERY_SNAPSHOT_VERSION = "1.0"
54
+ CONNECTOR_ADMISSION_MANIFEST_SCHEMA = "millforge.connector.admission_manifest"
55
+ CONNECTOR_ADMISSION_MANIFEST_KIND = "connector_admission_manifest"
56
+ CONNECTOR_ADMISSION_MANIFEST_VERSION = "1.0"
57
+
58
+ MAX_CONNECTOR_ID_UTF8 = 160
59
+ MAX_CONNECTOR_FIELD_UTF8 = 512
60
+ MAX_PROVIDER_TOOL_NAME_UTF8 = 160
61
+ MAX_PROVIDER_DESCRIPTION_UTF8 = 16_384
62
+ MAX_ADMITTED_DESCRIPTION_UTF8 = 65_536
63
+ _SECRET_REF_RE = re.compile(r"^[a-z][a-z0-9_]{0,127}$")
64
+ _SIDE_EFFECTS_REQUIRING_APPROVAL = frozenset(
65
+ item for item in SideEffectClass if item is not SideEffectClass.READ_ONLY
66
+ )
67
+
68
+ JsonValue = Any
69
+
70
+
71
+ class ConnectorTransportKind(str, Enum):
72
+ """Closed connector transport identity values."""
73
+
74
+ STDIO = "stdio"
75
+ HTTP = "http"
76
+
77
+
78
+ class ConnectorProtocol(str, Enum):
79
+ """Closed connector protocol identity values."""
80
+
81
+ MCP = "mcp"
82
+
83
+
84
+ class DescriptionPolicy(str, Enum):
85
+ """Closed description provenance policy values."""
86
+
87
+ OPERATOR_SUPPLIED = "operator_supplied"
88
+ PROVIDER_SANITIZED = "provider_sanitized"
89
+ PROVIDER_REJECTED = "provider_rejected"
90
+
91
+
92
+ class InputSchemaPolicy(str, Enum):
93
+ """Closed input schema admission policy values."""
94
+
95
+ PROVIDER_EXACT = "provider_exact"
96
+ OPERATOR_OVERLAY = "operator_overlay"
97
+
98
+
99
+ class OutputSchemaPolicy(str, Enum):
100
+ """Closed output schema admission policy values."""
101
+
102
+ PROVIDER_EXACT = "provider_exact"
103
+ OPERATOR_SUPPLIED = "operator_supplied"
104
+
105
+
106
+ class ConnectorApprovalPolicy(str, Enum):
107
+ """Closed approval policy values for admitted connector tools."""
108
+
109
+ NONE = "none"
110
+ MILLRACE_EXPLICIT = "millrace_explicit"
111
+ OPERATOR_OUT_OF_BAND = "operator_out_of_band"
112
+ FORBIDDEN = "forbidden"
113
+
114
+
115
+ class ConnectorIdentity(BaseModel):
116
+ """Immutable connector implementation identity."""
117
+
118
+ model_config = ConfigDict(
119
+ frozen=True,
120
+ extra="forbid",
121
+ arbitrary_types_allowed=True,
122
+ hide_input_in_errors=True,
123
+ use_enum_values=False,
124
+ )
125
+
126
+ connector_id: StrictStr
127
+ protocol: ConnectorProtocol
128
+ protocol_version: StrictStr
129
+ transport_kind: ConnectorTransportKind
130
+ implementation_name: StrictStr
131
+ implementation_version: StrictStr | None = None
132
+ implementation_digest: StrictStr | None = None
133
+ server_reported_name: StrictStr | None = None
134
+ server_reported_version: StrictStr | None = None
135
+ configured_secret_refs: tuple[StrictStr, ...] = Field(default_factory=tuple)
136
+ discovered_at: StrictStr
137
+
138
+ @field_validator(
139
+ "connector_id",
140
+ "protocol_version",
141
+ "implementation_name",
142
+ "implementation_version",
143
+ "implementation_digest",
144
+ "server_reported_name",
145
+ "server_reported_version",
146
+ )
147
+ @classmethod
148
+ def _identity_text_valid(cls, value: str | None, info: Any) -> str | None:
149
+ if value is None:
150
+ return None
151
+ maximum = (
152
+ MAX_CONNECTOR_ID_UTF8
153
+ if info.field_name == "connector_id"
154
+ else MAX_CONNECTOR_FIELD_UTF8
155
+ )
156
+ return _safe_connector_string(value, info.field_name, maximum)
157
+
158
+ @field_validator("implementation_digest")
159
+ @classmethod
160
+ def _implementation_digest_valid(cls, value: str | None) -> str | None:
161
+ if value is None:
162
+ return None
163
+ return validate_sha256(value, "implementation_digest")
164
+
165
+ @field_validator("discovered_at")
166
+ @classmethod
167
+ def _discovered_at_valid(cls, value: str) -> str:
168
+ return _validate_utc_timestamp(value, "discovered_at")
169
+
170
+ @field_validator("configured_secret_refs", mode="before")
171
+ @classmethod
172
+ def _secret_refs_snapshot(cls, value: Any) -> tuple[str, ...]:
173
+ values = _sorted_string_tuple(value, "configured_secret_refs")
174
+ for item in values:
175
+ if _SECRET_REF_RE.fullmatch(item) is None:
176
+ raise ValueError(
177
+ "configured_secret_refs must contain secret reference names"
178
+ )
179
+ return validate_unique(values, "configured_secret_refs")
180
+
181
+ @model_validator(mode="after")
182
+ def _identity_complete(self) -> ConnectorIdentity:
183
+ if self.implementation_version is None and self.implementation_digest is None:
184
+ raise ValueError(
185
+ "connector identity requires implementation_version or implementation_digest"
186
+ )
187
+ return self
188
+
189
+ @property
190
+ def identity_sha256(self) -> str:
191
+ return _sha256_hex(
192
+ {
193
+ "kind": CONNECTOR_IDENTITY_HASH_KIND,
194
+ "connector_id": self.connector_id,
195
+ "protocol": self.protocol.value,
196
+ "protocol_version": self.protocol_version,
197
+ "transport_kind": self.transport_kind.value,
198
+ "implementation_name": self.implementation_name,
199
+ "implementation_version": self.implementation_version,
200
+ "implementation_digest": self.implementation_digest,
201
+ "server_reported_name": self.server_reported_name,
202
+ "server_reported_version": self.server_reported_version,
203
+ "configured_secret_refs": list(self.configured_secret_refs),
204
+ }
205
+ )
206
+
207
+
208
+ class ExpectedConnectorIdentity(BaseModel):
209
+ """Exact expected identity constraints required before admission."""
210
+
211
+ model_config = ConfigDict(
212
+ frozen=True,
213
+ extra="forbid",
214
+ arbitrary_types_allowed=True,
215
+ hide_input_in_errors=True,
216
+ )
217
+
218
+ protocol: ConnectorProtocol
219
+ protocol_version: StrictStr
220
+ transport_kind: ConnectorTransportKind
221
+ implementation_name: StrictStr
222
+ implementation_version: StrictStr | None = None
223
+ implementation_digest: StrictStr | None = None
224
+
225
+ @field_validator(
226
+ "protocol_version",
227
+ "implementation_name",
228
+ "implementation_version",
229
+ "implementation_digest",
230
+ )
231
+ @classmethod
232
+ def _expected_text_valid(cls, value: str | None, info: Any) -> str | None:
233
+ if value is None:
234
+ return None
235
+ return _safe_connector_string(value, info.field_name, MAX_CONNECTOR_FIELD_UTF8)
236
+
237
+ @field_validator("implementation_digest")
238
+ @classmethod
239
+ def _expected_digest_valid(cls, value: str | None) -> str | None:
240
+ if value is None:
241
+ return None
242
+ return validate_sha256(value, "implementation_digest")
243
+
244
+ @model_validator(mode="after")
245
+ def _expected_complete(self) -> ExpectedConnectorIdentity:
246
+ if self.implementation_version is None and self.implementation_digest is None:
247
+ raise ValueError(
248
+ "expected identity requires implementation_version or implementation_digest"
249
+ )
250
+ return self
251
+
252
+ def matches(self, identity: ConnectorIdentity) -> bool:
253
+ """Return whether the supplied identity exactly satisfies this expectation."""
254
+ return (
255
+ self.protocol == identity.protocol
256
+ and self.protocol_version == identity.protocol_version
257
+ and self.transport_kind == identity.transport_kind
258
+ and self.implementation_name == identity.implementation_name
259
+ and self.implementation_version == identity.implementation_version
260
+ and self.implementation_digest == identity.implementation_digest
261
+ )
262
+
263
+
264
+ class DiscoveredProviderTool(BaseModel):
265
+ """Untrusted provider-discovered tool payload preserved for offline admission."""
266
+
267
+ model_config = ConfigDict(
268
+ frozen=True,
269
+ extra="forbid",
270
+ arbitrary_types_allowed=True,
271
+ hide_input_in_errors=True,
272
+ )
273
+
274
+ provider_tool_name: StrictStr
275
+ provider_description: StrictStr = ""
276
+ input_schema: Mapping[str, JsonValue]
277
+ output_schema: Mapping[str, JsonValue] | None = None
278
+ provider_annotations: Mapping[str, JsonValue] = Field(
279
+ default_factory=lambda: MappingProxyType({})
280
+ )
281
+
282
+ @field_validator("provider_tool_name")
283
+ @classmethod
284
+ def _provider_tool_name_valid(cls, value: str) -> str:
285
+ return _safe_connector_string(
286
+ value, "provider_tool_name", MAX_PROVIDER_TOOL_NAME_UTF8
287
+ )
288
+
289
+ @field_validator("provider_description")
290
+ @classmethod
291
+ def _provider_description_valid(cls, value: str) -> str:
292
+ validate_utf8_size(value, "provider_description", MAX_PROVIDER_DESCRIPTION_UTF8)
293
+ return value
294
+
295
+ @field_validator("input_schema", "output_schema", "provider_annotations")
296
+ @classmethod
297
+ def _raw_mapping_frozen(
298
+ cls, value: Any, info: Any
299
+ ) -> MappingProxyType[str, Any] | None:
300
+ if value is None and info.field_name == "output_schema":
301
+ return None
302
+ if not isinstance(value, Mapping):
303
+ raise ValueError(f"{info.field_name} must be a JSON object")
304
+ return _freeze_json_object(value)
305
+
306
+ @property
307
+ def input_schema_sha256(self) -> str:
308
+ return _sha256_hex(_thaw_json_value(self.input_schema))
309
+
310
+ @property
311
+ def output_schema_sha256(self) -> str | None:
312
+ if self.output_schema is None:
313
+ return None
314
+ return _sha256_hex(_thaw_json_value(self.output_schema))
315
+
316
+ @property
317
+ def provider_description_sha256(self) -> str:
318
+ return _sha256_hex(self.provider_description)
319
+
320
+ @property
321
+ def raw_tool_sha256(self) -> str:
322
+ return _sha256_hex(
323
+ {
324
+ "kind": CONNECTOR_DISCOVERY_TOOL_HASH_KIND,
325
+ "provider_tool_name": self.provider_tool_name,
326
+ "provider_description": self.provider_description,
327
+ "input_schema": _thaw_json_value(self.input_schema),
328
+ "output_schema": _thaw_json_value(self.output_schema),
329
+ "provider_annotations": _thaw_json_value(self.provider_annotations),
330
+ }
331
+ )
332
+
333
+ @field_serializer("input_schema", "output_schema", "provider_annotations")
334
+ def _serialize_mapping(self, value: Any) -> Any:
335
+ return _thaw_json_value(value)
336
+
337
+
338
+ class ConnectorDiscoverySnapshot(BaseModel):
339
+ """Immutable non-authoritative connector discovery snapshot."""
340
+
341
+ model_config = ConfigDict(
342
+ frozen=True,
343
+ extra="forbid",
344
+ arbitrary_types_allowed=True,
345
+ hide_input_in_errors=True,
346
+ )
347
+
348
+ schema_version: StrictStr = CONNECTOR_DISCOVERY_SNAPSHOT_SCHEMA
349
+ kind: StrictStr = CONNECTOR_DISCOVERY_SNAPSHOT_KIND
350
+ version: StrictStr = CONNECTOR_DISCOVERY_SNAPSHOT_VERSION
351
+ connector_identity: ConnectorIdentity
352
+ provider_tools: tuple[DiscoveredProviderTool, ...]
353
+ created_at: StrictStr
354
+ provider_metadata: Mapping[str, JsonValue] = Field(
355
+ default_factory=lambda: MappingProxyType({})
356
+ )
357
+
358
+ @field_validator("schema_version")
359
+ @classmethod
360
+ def _schema_version_valid(cls, value: str) -> str:
361
+ if value != CONNECTOR_DISCOVERY_SNAPSHOT_SCHEMA:
362
+ raise ValueError(
363
+ "schema_version must identify connector discovery snapshots"
364
+ )
365
+ return value
366
+
367
+ @field_validator("kind")
368
+ @classmethod
369
+ def _kind_valid(cls, value: str) -> str:
370
+ if value != CONNECTOR_DISCOVERY_SNAPSHOT_KIND:
371
+ raise ValueError("kind must be connector_discovery_snapshot")
372
+ return value
373
+
374
+ @field_validator("version")
375
+ @classmethod
376
+ def _version_valid(cls, value: str) -> str:
377
+ if value != CONNECTOR_DISCOVERY_SNAPSHOT_VERSION:
378
+ raise ValueError("version must be 1.0")
379
+ return value
380
+
381
+ @field_validator("created_at")
382
+ @classmethod
383
+ def _created_at_valid(cls, value: str) -> str:
384
+ return _validate_utc_timestamp(value, "created_at")
385
+
386
+ @field_validator("provider_tools", mode="before")
387
+ @classmethod
388
+ def _provider_tools_tuple(cls, value: Any) -> tuple[DiscoveredProviderTool, ...]:
389
+ if isinstance(value, str) or not isinstance(value, (list, tuple)):
390
+ raise ValueError("provider_tools must be an array")
391
+ return tuple(value)
392
+
393
+ @field_validator("provider_metadata")
394
+ @classmethod
395
+ def _metadata_frozen(cls, value: Any) -> MappingProxyType[str, Any]:
396
+ if not isinstance(value, Mapping):
397
+ raise ValueError("provider_metadata must be a JSON object")
398
+ return _freeze_json_object(value)
399
+
400
+ @property
401
+ def duplicate_provider_names(self) -> tuple[str, ...]:
402
+ counts: dict[str, int] = {}
403
+ for tool in self.provider_tools:
404
+ counts[tool.provider_tool_name] = counts.get(tool.provider_tool_name, 0) + 1
405
+ return tuple(name for name, count in sorted(counts.items()) if count > 1)
406
+
407
+ @property
408
+ def discovery_snapshot_sha256(self) -> str:
409
+ return _sha256_hex(
410
+ {
411
+ "kind": CONNECTOR_DISCOVERY_SNAPSHOT_HASH_KIND,
412
+ "schema_version": self.schema_version,
413
+ "snapshot_kind": self.kind,
414
+ "version": self.version,
415
+ "connector_identity_sha256": self.connector_identity.identity_sha256,
416
+ "created_at": self.created_at,
417
+ "provider_tools": [
418
+ {
419
+ "provider_tool_name": tool.provider_tool_name,
420
+ "raw_tool_sha256": tool.raw_tool_sha256,
421
+ }
422
+ for tool in sorted(
423
+ self.provider_tools, key=lambda item: item.provider_tool_name
424
+ )
425
+ ],
426
+ "provider_metadata": _thaw_json_value(self.provider_metadata),
427
+ }
428
+ )
429
+
430
+ @field_serializer("provider_metadata")
431
+ def _serialize_metadata(self, value: Any) -> Any:
432
+ return _thaw_json_value(value)
433
+
434
+
435
+ class ConnectorToolSelection(BaseModel):
436
+ """Operator-selected discovered tool and admitted descriptor shape."""
437
+
438
+ model_config = ConfigDict(
439
+ frozen=True,
440
+ extra="forbid",
441
+ arbitrary_types_allowed=True,
442
+ hide_input_in_errors=True,
443
+ )
444
+
445
+ provider_tool_name: StrictStr
446
+ tool_id: StrictStr
447
+ tool_version: StrictInt = Field(ge=1)
448
+ implementation_id: StrictStr
449
+ model_tool_name: StrictStr
450
+ description: StrictStr
451
+ description_policy: DescriptionPolicy
452
+ input_schema_policy: InputSchemaPolicy
453
+ output_schema_policy: OutputSchemaPolicy
454
+ input_schema: Mapping[str, JsonValue] | None = None
455
+ output_schema: Mapping[str, JsonValue] | None = None
456
+ required_capabilities: tuple[StrictStr, ...]
457
+ produced_artifact_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
458
+ side_effect_class: SideEffectClass
459
+ idempotency: IdempotencyClass
460
+ timeout_policy: ToolTimeoutPolicy
461
+ output_policy: ToolOutputPolicy
462
+ approval_policy: ConnectorApprovalPolicy
463
+ expected_raw_tool_sha256: StrictStr | None = None
464
+ expected_descriptor_sha256: StrictStr | None = None
465
+ expected_admission_record_sha256: StrictStr | None = None
466
+
467
+ @field_validator("provider_tool_name", "implementation_id", "model_tool_name")
468
+ @classmethod
469
+ def _selection_text_valid(cls, value: str, info: Any) -> str:
470
+ return _safe_connector_string(value, info.field_name, MAX_CONNECTOR_FIELD_UTF8)
471
+
472
+ @field_validator("tool_id")
473
+ @classmethod
474
+ def _tool_id_valid(cls, value: str) -> str:
475
+ return validate_canonical_tool_id(value)
476
+
477
+ @field_validator("tool_version")
478
+ @classmethod
479
+ def _tool_version_valid(cls, value: int) -> int:
480
+ return validate_tool_version(value)
481
+
482
+ @field_validator(
483
+ "expected_raw_tool_sha256",
484
+ "expected_descriptor_sha256",
485
+ "expected_admission_record_sha256",
486
+ )
487
+ @classmethod
488
+ def _expected_hash_valid(cls, value: str | None, info: Any) -> str | None:
489
+ if value is None:
490
+ return None
491
+ return validate_sha256(value, info.field_name)
492
+
493
+ @field_validator("description")
494
+ @classmethod
495
+ def _description_valid(cls, value: str) -> str:
496
+ return _safe_connector_string(
497
+ value, "description", MAX_ADMITTED_DESCRIPTION_UTF8
498
+ )
499
+
500
+ @field_validator("input_schema", "output_schema")
501
+ @classmethod
502
+ def _optional_schema_frozen(
503
+ cls, value: Any, info: Any
504
+ ) -> MappingProxyType[str, Any] | None:
505
+ if value is None:
506
+ return None
507
+ if not isinstance(value, Mapping):
508
+ raise ValueError(f"{info.field_name} must be a JSON object")
509
+ return _freeze_json_object(
510
+ normalize_json_schema(value, field_name=info.field_name)
511
+ )
512
+
513
+ @field_validator("required_capabilities", "produced_artifact_ids", mode="before")
514
+ @classmethod
515
+ def _string_tuple_snapshot(cls, value: Any, info: Any) -> tuple[str, ...]:
516
+ return _sorted_string_tuple(value, info.field_name)
517
+
518
+ @field_validator("required_capabilities")
519
+ @classmethod
520
+ def _capabilities_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
521
+ for capability_id in value:
522
+ validate_capability_id(capability_id)
523
+ return validate_unique(value, "required_capabilities")
524
+
525
+ @model_validator(mode="after")
526
+ def _selection_complete(self) -> ConnectorToolSelection:
527
+ if self.approval_policy is ConnectorApprovalPolicy.FORBIDDEN:
528
+ raise ValueError("forbidden approval policy cannot admit a tool")
529
+ if (
530
+ self.output_schema_policy is OutputSchemaPolicy.OPERATOR_SUPPLIED
531
+ and self.output_schema is None
532
+ ):
533
+ raise ValueError("operator-supplied output schema is required")
534
+ if (
535
+ self.side_effect_class in _SIDE_EFFECTS_REQUIRING_APPROVAL
536
+ and self.approval_policy
537
+ not in {
538
+ ConnectorApprovalPolicy.MILLRACE_EXPLICIT,
539
+ ConnectorApprovalPolicy.OPERATOR_OUT_OF_BAND,
540
+ }
541
+ ):
542
+ raise ValueError("side-effecting connector tools require explicit approval")
543
+ return self
544
+
545
+ @field_serializer("input_schema", "output_schema")
546
+ def _serialize_schema(self, value: Any) -> Any:
547
+ return _thaw_json_value(value)
548
+
549
+
550
+ class DeniedConnectorTool(BaseModel):
551
+ """Denied discovered tool and operator review evidence."""
552
+
553
+ model_config = ConfigDict(
554
+ frozen=True,
555
+ extra="forbid",
556
+ arbitrary_types_allowed=True,
557
+ hide_input_in_errors=True,
558
+ )
559
+
560
+ provider_tool_name: StrictStr
561
+ reason: StrictStr
562
+ approval_policy: ConnectorApprovalPolicy = ConnectorApprovalPolicy.FORBIDDEN
563
+ review_evidence: Mapping[str, JsonValue] = Field(
564
+ default_factory=lambda: MappingProxyType({})
565
+ )
566
+
567
+ @field_validator("provider_tool_name", "reason")
568
+ @classmethod
569
+ def _denial_text_valid(cls, value: str, info: Any) -> str:
570
+ return _safe_connector_string(value, info.field_name, MAX_CONNECTOR_FIELD_UTF8)
571
+
572
+ @field_validator("review_evidence")
573
+ @classmethod
574
+ def _review_evidence_frozen(cls, value: Any) -> MappingProxyType[str, Any]:
575
+ if not isinstance(value, Mapping):
576
+ raise ValueError("review_evidence must be a JSON object")
577
+ _reject_secret_values(value, path="/review_evidence")
578
+ return _freeze_json_object(value)
579
+
580
+ @field_serializer("review_evidence")
581
+ def _serialize_review_evidence(self, value: Any) -> Any:
582
+ return _thaw_json_value(value)
583
+
584
+
585
+ class ConnectorAdmissionManifest(BaseModel):
586
+ """Explicit operator admission manifest."""
587
+
588
+ model_config = ConfigDict(
589
+ frozen=True,
590
+ extra="forbid",
591
+ arbitrary_types_allowed=True,
592
+ hide_input_in_errors=True,
593
+ )
594
+
595
+ schema_version: StrictStr = CONNECTOR_ADMISSION_MANIFEST_SCHEMA
596
+ kind: StrictStr = CONNECTOR_ADMISSION_MANIFEST_KIND
597
+ version: StrictStr = CONNECTOR_ADMISSION_MANIFEST_VERSION
598
+ connector_id: StrictStr
599
+ expected_identity: ExpectedConnectorIdentity
600
+ selected_tools: tuple[ConnectorToolSelection, ...]
601
+ denied_tools: tuple[DeniedConnectorTool, ...] = Field(default_factory=tuple)
602
+ policy_metadata: Mapping[str, JsonValue] = Field(
603
+ default_factory=lambda: MappingProxyType({})
604
+ )
605
+ expected_connector_identity_sha256: StrictStr | None = None
606
+ expected_discovery_snapshot_sha256: StrictStr | None = None
607
+
608
+ @field_validator("schema_version")
609
+ @classmethod
610
+ def _schema_version_valid(cls, value: str) -> str:
611
+ if value != CONNECTOR_ADMISSION_MANIFEST_SCHEMA:
612
+ raise ValueError(
613
+ "schema_version must identify connector admission manifests"
614
+ )
615
+ return value
616
+
617
+ @field_validator("kind")
618
+ @classmethod
619
+ def _kind_valid(cls, value: str) -> str:
620
+ if value != CONNECTOR_ADMISSION_MANIFEST_KIND:
621
+ raise ValueError("kind must be connector_admission_manifest")
622
+ return value
623
+
624
+ @field_validator("version")
625
+ @classmethod
626
+ def _version_valid(cls, value: str) -> str:
627
+ if value != CONNECTOR_ADMISSION_MANIFEST_VERSION:
628
+ raise ValueError("version must be 1.0")
629
+ return value
630
+
631
+ @field_validator("connector_id")
632
+ @classmethod
633
+ def _connector_id_valid(cls, value: str) -> str:
634
+ return _safe_connector_string(value, "connector_id", MAX_CONNECTOR_ID_UTF8)
635
+
636
+ @field_validator(
637
+ "expected_connector_identity_sha256",
638
+ "expected_discovery_snapshot_sha256",
639
+ )
640
+ @classmethod
641
+ def _expected_hash_valid(cls, value: str | None, info: Any) -> str | None:
642
+ if value is None:
643
+ return None
644
+ return validate_sha256(value, info.field_name)
645
+
646
+ @field_validator("selected_tools", "denied_tools", mode="before")
647
+ @classmethod
648
+ def _tool_tuples(cls, value: Any, info: Any) -> tuple[Any, ...]:
649
+ if value is None and info.field_name == "denied_tools":
650
+ return ()
651
+ if isinstance(value, str) or not isinstance(value, (list, tuple)):
652
+ raise ValueError(f"{info.field_name} must be an array")
653
+ return tuple(value)
654
+
655
+ @field_validator("policy_metadata")
656
+ @classmethod
657
+ def _policy_metadata_frozen(cls, value: Any) -> MappingProxyType[str, Any]:
658
+ if not isinstance(value, Mapping):
659
+ raise ValueError("policy_metadata must be a JSON object")
660
+ _reject_secret_values(value, path="/policy_metadata")
661
+ return _freeze_json_object(value)
662
+
663
+ @model_validator(mode="after")
664
+ def _manifest_consistent(self) -> ConnectorAdmissionManifest:
665
+ if not self.selected_tools:
666
+ raise ValueError("selected_tools must contain at least one tool")
667
+ selected_names = tuple(item.provider_tool_name for item in self.selected_tools)
668
+ denied_names = tuple(item.provider_tool_name for item in self.denied_tools)
669
+ validate_unique(selected_names, "selected provider_tool_name")
670
+ validate_unique(denied_names, "denied provider_tool_name")
671
+ overlap = set(selected_names) & set(denied_names)
672
+ if overlap:
673
+ raise ValueError("selected and denied provider tool names must be disjoint")
674
+ tool_keys = tuple(
675
+ f"{item.tool_id}@{item.tool_version}" for item in self.selected_tools
676
+ )
677
+ validate_unique(tool_keys, "selected tool identities")
678
+ model_names = tuple(item.model_tool_name for item in self.selected_tools)
679
+ validate_unique(model_names, "selected model_tool_name")
680
+ implementation_ids = tuple(
681
+ item.implementation_id for item in self.selected_tools
682
+ )
683
+ validate_unique(implementation_ids, "selected implementation_id")
684
+ return self
685
+
686
+ @field_serializer("policy_metadata")
687
+ def _serialize_policy_metadata(self, value: Any) -> Any:
688
+ return _thaw_json_value(value)
689
+
690
+
691
+ class ConnectorAdmissionPolicy(BaseModel):
692
+ """Closed policy constraints for offline connector admission."""
693
+
694
+ model_config = ConfigDict(
695
+ frozen=True,
696
+ extra="forbid",
697
+ arbitrary_types_allowed=True,
698
+ hide_input_in_errors=True,
699
+ )
700
+
701
+ allowed_capability_ids: tuple[StrictStr, ...]
702
+ allowed_protocols: tuple[ConnectorProtocol, ...] = (ConnectorProtocol.MCP,)
703
+ allowed_transport_kinds: tuple[ConnectorTransportKind, ...] = (
704
+ ConnectorTransportKind.STDIO,
705
+ ConnectorTransportKind.HTTP,
706
+ )
707
+ max_description_utf8: StrictInt = Field(gt=0, le=MAX_ADMITTED_DESCRIPTION_UTF8)
708
+ side_effect_approval_matrix: Mapping[
709
+ SideEffectClass, tuple[ConnectorApprovalPolicy, ...]
710
+ ] = Field(default_factory=lambda: MappingProxyType(_default_approval_matrix()))
711
+
712
+ @field_validator("allowed_capability_ids", mode="before")
713
+ @classmethod
714
+ def _allowed_capabilities_snapshot(cls, value: Any) -> tuple[str, ...]:
715
+ values = _sorted_string_tuple(value, "allowed_capability_ids")
716
+ for capability_id in values:
717
+ validate_capability_id(capability_id)
718
+ return validate_unique(values, "allowed_capability_ids")
719
+
720
+ @field_validator("allowed_protocols", "allowed_transport_kinds", mode="before")
721
+ @classmethod
722
+ def _enum_tuples(cls, value: Any, info: Any) -> tuple[Any, ...]:
723
+ if isinstance(value, str) or not isinstance(value, (list, tuple)):
724
+ raise ValueError(f"{info.field_name} must be an array")
725
+ return tuple(value)
726
+
727
+ @field_validator("side_effect_approval_matrix")
728
+ @classmethod
729
+ def _matrix_frozen(
730
+ cls, value: Any
731
+ ) -> MappingProxyType[SideEffectClass, tuple[ConnectorApprovalPolicy, ...]]:
732
+ if not isinstance(value, Mapping):
733
+ raise ValueError("side_effect_approval_matrix must be an object")
734
+ matrix: dict[SideEffectClass, tuple[ConnectorApprovalPolicy, ...]] = {}
735
+ for raw_key, raw_values in value.items():
736
+ key = (
737
+ raw_key
738
+ if isinstance(raw_key, SideEffectClass)
739
+ else SideEffectClass(raw_key)
740
+ )
741
+ if isinstance(raw_values, str) or not isinstance(raw_values, (list, tuple)):
742
+ raise ValueError("side_effect_approval_matrix values must be arrays")
743
+ policies = tuple(
744
+ item
745
+ if isinstance(item, ConnectorApprovalPolicy)
746
+ else ConnectorApprovalPolicy(item)
747
+ for item in raw_values
748
+ )
749
+ if ConnectorApprovalPolicy.FORBIDDEN in policies:
750
+ raise ValueError("forbidden approval policy is denial evidence only")
751
+ if (
752
+ key in _SIDE_EFFECTS_REQUIRING_APPROVAL
753
+ and ConnectorApprovalPolicy.NONE in policies
754
+ ):
755
+ raise ValueError(
756
+ "side-effecting connector tools cannot use approval none"
757
+ )
758
+ matrix[key] = policies
759
+ return MappingProxyType(
760
+ dict(sorted(matrix.items(), key=lambda item: item[0].value))
761
+ )
762
+
763
+ @field_serializer("side_effect_approval_matrix")
764
+ def _serialize_matrix(
765
+ self,
766
+ value: MappingProxyType[SideEffectClass, tuple[ConnectorApprovalPolicy, ...]],
767
+ ) -> dict[str, list[str]]:
768
+ return {
769
+ side_effect.value: [policy.value for policy in policies]
770
+ for side_effect, policies in sorted(
771
+ value.items(), key=lambda item: item[0].value
772
+ )
773
+ }
774
+
775
+
776
+ class ConnectorAdmissionRecord(BaseModel):
777
+ """Immutable metadata tying an admitted descriptor back to discovery."""
778
+
779
+ model_config = ConfigDict(frozen=True, extra="forbid")
780
+
781
+ connector_id: StrictStr
782
+ provider_tool_name: StrictStr
783
+ connector_identity_sha256: StrictStr
784
+ discovery_snapshot_sha256: StrictStr
785
+ raw_tool_sha256: StrictStr
786
+ input_schema_sha256: StrictStr | None = None
787
+ output_schema_sha256: StrictStr | None = None
788
+ provider_description_sha256: StrictStr | None = None
789
+ descriptor_sha256: StrictStr
790
+ required_capabilities: tuple[StrictStr, ...]
791
+ side_effect_class: SideEffectClass
792
+ idempotency: IdempotencyClass
793
+ timeout_policy: ToolTimeoutPolicy
794
+ output_policy: ToolOutputPolicy
795
+ idempotency_key_policy: StrictStr | None = None
796
+ approval_policy: ConnectorApprovalPolicy
797
+
798
+ @field_validator("required_capabilities", mode="before")
799
+ @classmethod
800
+ def _capabilities_snapshot(cls, value: Any) -> tuple[str, ...]:
801
+ return _sorted_string_tuple(value, "required_capabilities")
802
+
803
+ @field_validator("required_capabilities")
804
+ @classmethod
805
+ def _capabilities_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
806
+ for capability in value:
807
+ validate_capability_id(capability)
808
+ return validate_unique(value, "required_capabilities")
809
+
810
+ @field_validator("idempotency_key_policy")
811
+ @classmethod
812
+ def _idempotency_key_policy_valid(cls, value: str | None) -> str | None:
813
+ if value is None:
814
+ return None
815
+ return validate_nonblank(value, "idempotency_key_policy")
816
+
817
+ @field_validator(
818
+ "connector_identity_sha256",
819
+ "discovery_snapshot_sha256",
820
+ "raw_tool_sha256",
821
+ "input_schema_sha256",
822
+ "output_schema_sha256",
823
+ "provider_description_sha256",
824
+ "descriptor_sha256",
825
+ )
826
+ @classmethod
827
+ def _hash_valid(cls, value: str | None, info: Any) -> str | None:
828
+ if value is None:
829
+ return None
830
+ return validate_sha256(value, info.field_name)
831
+
832
+ @property
833
+ def admission_record_sha256(self) -> str:
834
+ return _sha256_hex(
835
+ {
836
+ "kind": CONNECTOR_ADMISSION_RECORD_HASH_KIND,
837
+ "connector_id": self.connector_id,
838
+ "provider_tool_name": self.provider_tool_name,
839
+ "connector_identity_sha256": self.connector_identity_sha256,
840
+ "discovery_snapshot_sha256": self.discovery_snapshot_sha256,
841
+ "raw_tool_sha256": self.raw_tool_sha256,
842
+ "input_schema_sha256": self.input_schema_sha256,
843
+ "output_schema_sha256": self.output_schema_sha256,
844
+ "provider_description_sha256": self.provider_description_sha256,
845
+ "descriptor_sha256": self.descriptor_sha256,
846
+ "required_capabilities": list(self.required_capabilities),
847
+ "side_effect_class": self.side_effect_class.value,
848
+ "idempotency": self.idempotency.value,
849
+ "timeout_policy": self.timeout_policy.model_dump(mode="json"),
850
+ "output_policy": self.output_policy.model_dump(mode="json"),
851
+ "idempotency_key_policy": self.idempotency_key_policy,
852
+ "approval_policy": self.approval_policy.value,
853
+ }
854
+ )
855
+
856
+
857
+ class ConnectorAdmissionResult(BaseModel):
858
+ """Deterministic result container for connector admission attempts."""
859
+
860
+ model_config = ConfigDict(frozen=True, extra="forbid")
861
+
862
+ accepted: bool
863
+ descriptors: tuple[ToolDescriptor, ...] = Field(default_factory=tuple)
864
+ records: tuple[ConnectorAdmissionRecord, ...] = Field(default_factory=tuple)
865
+ diagnostics: tuple[ConnectorDiagnostic, ...] = Field(default_factory=tuple)
866
+
867
+ @model_validator(mode="after")
868
+ def _result_consistent(self) -> ConnectorAdmissionResult:
869
+ if self.accepted:
870
+ if self.diagnostics:
871
+ raise ValueError(
872
+ "accepted admission results cannot contain diagnostics"
873
+ )
874
+ if not self.descriptors:
875
+ raise ValueError("accepted admission results require descriptors")
876
+ if len(self.descriptors) != len(self.records):
877
+ raise ValueError(
878
+ "accepted admission results require one record per descriptor"
879
+ )
880
+ descriptor_hashes = tuple(
881
+ descriptor.descriptor_sha256 for descriptor in self.descriptors
882
+ )
883
+ record_hashes = tuple(record.descriptor_sha256 for record in self.records)
884
+ if descriptor_hashes != record_hashes:
885
+ raise ValueError("admission records must match descriptor hashes")
886
+ elif self.descriptors or self.records:
887
+ raise ValueError(
888
+ "rejected admission results cannot contain descriptors or records"
889
+ )
890
+ return self
891
+
892
+
893
+ class ConnectorContractValidation(BaseModel):
894
+ """Contract validation result for malformed raw inputs."""
895
+
896
+ model_config = ConfigDict(frozen=True, extra="forbid")
897
+
898
+ value: Any | None = None
899
+ diagnostics: tuple[ConnectorDiagnostic, ...] = Field(default_factory=tuple)
900
+
901
+ @property
902
+ def accepted(self) -> bool:
903
+ return self.value is not None and not self.diagnostics
904
+
905
+
906
+ class ConnectorContractModel(BaseModel):
907
+ """Base helper for diagnostic validation of raw connector mappings."""
908
+
909
+ model_config = ConfigDict(frozen=True)
910
+ _diagnostic_phase: ClassVar[ConnectorDiagnosticPhase] = (
911
+ ConnectorDiagnosticPhase.DIAGNOSTIC
912
+ )
913
+
914
+ @classmethod
915
+ def validate_contract(cls, raw: Any) -> ConnectorContractValidation:
916
+ """Validate raw input and return stable diagnostics instead of exception text."""
917
+ try:
918
+ return ConnectorContractValidation(value=cls.model_validate(raw))
919
+ except ValidationError as exc:
920
+ missing_field = _validation_missing_field(exc)
921
+ return ConnectorContractValidation(
922
+ diagnostics=(
923
+ malformed_input_diagnostic(
924
+ phase=cls._diagnostic_phase,
925
+ model_name=cls.__name__,
926
+ path=_validation_pointer(exc),
927
+ missing_field=missing_field,
928
+ code=_validation_code(exc),
929
+ ),
930
+ )
931
+ )
932
+ except Exception:
933
+ return ConnectorContractValidation(
934
+ diagnostics=(
935
+ malformed_input_diagnostic(
936
+ phase=cls._diagnostic_phase,
937
+ model_name=cls.__name__,
938
+ code=ConnectorDiagnosticCode.IDENTITY_INVALID,
939
+ ),
940
+ )
941
+ )
942
+
943
+
944
+ def _default_approval_matrix() -> dict[
945
+ SideEffectClass, tuple[ConnectorApprovalPolicy, ...]
946
+ ]:
947
+ explicit = (
948
+ ConnectorApprovalPolicy.MILLRACE_EXPLICIT,
949
+ ConnectorApprovalPolicy.OPERATOR_OUT_OF_BAND,
950
+ )
951
+ return {
952
+ SideEffectClass.READ_ONLY: (
953
+ ConnectorApprovalPolicy.NONE,
954
+ ConnectorApprovalPolicy.MILLRACE_EXPLICIT,
955
+ ConnectorApprovalPolicy.OPERATOR_OUT_OF_BAND,
956
+ ),
957
+ SideEffectClass.ARTIFACT_WRITE: explicit,
958
+ SideEffectClass.WORKSPACE_WRITE: explicit,
959
+ SideEffectClass.PROCESS_EXECUTION: explicit,
960
+ SideEffectClass.NETWORK_READ: explicit,
961
+ SideEffectClass.NETWORK_WRITE: explicit,
962
+ SideEffectClass.TERMINAL: explicit,
963
+ }
964
+
965
+
966
+ def _safe_connector_string(value: str, field_name: str, maximum: int) -> str:
967
+ validate_nonblank(value, field_name)
968
+ validate_utf8_size(value, field_name, maximum)
969
+ _reject_secret_material(value, field_name)
970
+ return value
971
+
972
+
973
+ def _validate_utc_timestamp(value: str, field_name: str) -> str:
974
+ validate_nonblank(value, field_name)
975
+ validate_utf8_size(value, field_name, MAX_CONNECTOR_FIELD_UTF8)
976
+ _reject_secret_material(value, field_name)
977
+ if not value.endswith("Z"):
978
+ raise ValueError(f"{field_name} must be an explicit UTC timestamp ending in Z")
979
+ try:
980
+ parsed = datetime.fromisoformat(value.removesuffix("Z") + "+00:00")
981
+ except ValueError as exc:
982
+ raise ValueError(f"{field_name} must be an ISO 8601 UTC timestamp") from exc
983
+ if parsed.utcoffset() != timezone.utc.utcoffset(parsed):
984
+ raise ValueError(f"{field_name} must be an explicit UTC timestamp")
985
+ return value
986
+
987
+
988
+ def _reject_secret_material(value: str, field_name: str) -> None:
989
+ if detect_secret_candidate(
990
+ field_path=f"/{field_name}",
991
+ field_name=field_name,
992
+ value=value,
993
+ policy=RedactionPolicy(),
994
+ ):
995
+ raise ValueError(f"{field_name} contains suspected secret material")
996
+
997
+
998
+ def _reject_secret_values(value: Any, *, path: str) -> None:
999
+ if isinstance(value, Mapping):
1000
+ for key, item in value.items():
1001
+ _reject_secret_values(item, path=f"{path}/{key}")
1002
+ return
1003
+ if isinstance(value, tuple | list):
1004
+ for index, item in enumerate(value):
1005
+ _reject_secret_values(item, path=f"{path}/{index}")
1006
+ return
1007
+ if isinstance(value, str):
1008
+ _reject_secret_material(value, path)
1009
+
1010
+
1011
+ def _sorted_string_tuple(value: Any, field_name: str) -> tuple[str, ...]:
1012
+ if value is None:
1013
+ return ()
1014
+ if isinstance(value, str) or not isinstance(value, (list, tuple)):
1015
+ raise ValueError(f"{field_name} must be an array")
1016
+ values = tuple(value)
1017
+ if not all(isinstance(item, str) for item in values):
1018
+ raise ValueError(f"{field_name} must contain strings")
1019
+ return tuple(sorted(values))
1020
+
1021
+
1022
+ def _freeze_json_object(value: Mapping[str, Any]) -> MappingProxyType[str, Any]:
1023
+ frozen = _freeze_json_value(dict(value), path="")
1024
+ if not isinstance(frozen, MappingProxyType):
1025
+ raise ValueError("schema must be a JSON object")
1026
+ return frozen
1027
+
1028
+
1029
+ def _freeze_json_value(value: Any, *, path: str) -> Any:
1030
+ if isinstance(value, Mapping):
1031
+ frozen_items: dict[str, Any] = {}
1032
+ for key, item in sorted(value.items(), key=lambda item: str(item[0])):
1033
+ if not isinstance(key, str):
1034
+ raise ValueError(f"{path or '/'} object keys must be strings")
1035
+ frozen_items[key] = _freeze_json_value(item, path=f"{path}/{key}")
1036
+ return MappingProxyType(frozen_items)
1037
+ if isinstance(value, tuple | list):
1038
+ return tuple(
1039
+ _freeze_json_value(item, path=f"{path}/{index}")
1040
+ for index, item in enumerate(value)
1041
+ )
1042
+ return _validate_json_scalar(value, path=path)
1043
+
1044
+
1045
+ def _thaw_json_value(value: Any, *, path: str = "") -> Any:
1046
+ if isinstance(value, Mapping):
1047
+ thawed: dict[str, Any] = {}
1048
+ for key, item in sorted(value.items(), key=lambda item: str(item[0])):
1049
+ if not isinstance(key, str):
1050
+ raise ValueError(f"{path or '/'} object keys must be strings")
1051
+ thawed[key] = _thaw_json_value(item, path=f"{path}/{key}")
1052
+ return thawed
1053
+ if isinstance(value, tuple):
1054
+ return [
1055
+ _thaw_json_value(item, path=f"{path}/{index}")
1056
+ for index, item in enumerate(value)
1057
+ ]
1058
+ if isinstance(value, list):
1059
+ return [
1060
+ _thaw_json_value(item, path=f"{path}/{index}")
1061
+ for index, item in enumerate(value)
1062
+ ]
1063
+ return _validate_json_scalar(value, path=path)
1064
+
1065
+
1066
+ def _validate_json_scalar(value: Any, *, path: str) -> str | int | float | bool | None:
1067
+ if isinstance(value, str):
1068
+ _reject_secret_json_material(value, path=path)
1069
+ json.dumps(value, allow_nan=False)
1070
+ return value
1071
+ if isinstance(value, bool):
1072
+ json.dumps(value, allow_nan=False)
1073
+ return value
1074
+ if isinstance(value, int):
1075
+ json.dumps(value, allow_nan=False)
1076
+ return value
1077
+ if isinstance(value, float):
1078
+ json.dumps(value, allow_nan=False)
1079
+ return value
1080
+ if value is None:
1081
+ json.dumps(value, allow_nan=False)
1082
+ return value
1083
+ raise ValueError(f"{path or '/'} must contain only JSON values")
1084
+
1085
+
1086
+ def _reject_secret_json_material(value: str, *, path: str) -> None:
1087
+ if "configured_secret_refs" in path:
1088
+ return
1089
+ field_name = path.rsplit("/", 1)[-1] if path else "value"
1090
+ if detect_secret_candidate(
1091
+ field_path=path or f"/{field_name}",
1092
+ field_name=field_name,
1093
+ value=value,
1094
+ policy=RedactionPolicy(),
1095
+ ):
1096
+ raise ValueError(f"{path or '/'} contains suspected secret material")
1097
+
1098
+
1099
+ def _sha256_hex(payload: Any) -> str:
1100
+ return hashlib.sha256(
1101
+ canonical_json_serialize(_thaw_json_value(payload)).encode("utf-8")
1102
+ ).hexdigest()
1103
+
1104
+
1105
+ def _validation_pointer(exc: ValidationError) -> str:
1106
+ errors = exc.errors()
1107
+ if not errors:
1108
+ return "/"
1109
+ loc = errors[0].get("loc")
1110
+ if not isinstance(loc, tuple | list) or not loc:
1111
+ return "/"
1112
+ parts = [str(part).replace("~", "~0").replace("/", "~1") for part in loc]
1113
+ return "/" + "/".join(parts)
1114
+
1115
+
1116
+ def _validation_missing_field(exc: ValidationError) -> str | None:
1117
+ errors = exc.errors()
1118
+ if not errors:
1119
+ return None
1120
+ first = errors[0]
1121
+ if first.get("type") != "missing":
1122
+ return None
1123
+ loc = first.get("loc")
1124
+ if not isinstance(loc, tuple | list) or not loc:
1125
+ return None
1126
+ return str(loc[-1])
1127
+
1128
+
1129
+ def _validation_code(exc: ValidationError) -> ConnectorDiagnosticCode:
1130
+ errors = exc.errors()
1131
+ if not errors:
1132
+ return ConnectorDiagnosticCode.IDENTITY_INVALID
1133
+ first = errors[0]
1134
+ loc = tuple(str(part) for part in first.get("loc", ()))
1135
+ message = str(first.get("msg", "")).lower()
1136
+ error_text = str(errors).lower()
1137
+ if "secret material" in error_text:
1138
+ return ConnectorDiagnosticCode.SECRET_MATERIAL
1139
+ if "forbidden approval policy cannot admit" in error_text:
1140
+ return ConnectorDiagnosticCode.FORBIDDEN_TOOL_ADMITTED
1141
+ if "selected tool identities" in error_text:
1142
+ return ConnectorDiagnosticCode.DUPLICATE_ADMITTED_TOOL
1143
+ if "selected model_tool_name" in error_text:
1144
+ return ConnectorDiagnosticCode.DUPLICATE_MODEL_TOOL_NAME
1145
+ if "selected implementation_id" in error_text:
1146
+ return ConnectorDiagnosticCode.DUPLICATE_IMPLEMENTATION_ID
1147
+ if "denied provider_tool_name" in error_text or "disjoint" in error_text:
1148
+ return ConnectorDiagnosticCode.DENIED_TOOL_INVALID
1149
+ if "output schema" in message or "output_schema" in loc:
1150
+ return ConnectorDiagnosticCode.OUTPUT_SCHEMA_UNSUPPORTED
1151
+ if "input schema" in message or "input_schema" in loc:
1152
+ return ConnectorDiagnosticCode.INPUT_SCHEMA_UNSUPPORTED
1153
+ if "required_capabilities" in loc:
1154
+ return ConnectorDiagnosticCode.CAPABILITY_MISSING
1155
+ if "approval_policy" in loc or "side-effecting connector tools" in error_text:
1156
+ return ConnectorDiagnosticCode.APPROVAL_POLICY_INVALID
1157
+ if "expected_identity" in loc or "connectoridentity" in error_text:
1158
+ return ConnectorDiagnosticCode.IDENTITY_INVALID
1159
+ return ConnectorDiagnosticCode.IDENTITY_INVALID