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,801 @@
1
+ """Deterministic offline lowering from connector discovery to tool descriptors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping
7
+ from types import MappingProxyType
8
+ from typing import Any, TypeVar
9
+
10
+ from pydantic import BaseModel, ValidationError
11
+
12
+ from millforge.compiler.schema_validation import (
13
+ SchemaSubsetError,
14
+ validate_json_schema_subset,
15
+ )
16
+ from millforge.compiler.validators import validate_utf8_size
17
+ from millforge.connectors.contracts import (
18
+ ConnectorAdmissionManifest,
19
+ ConnectorAdmissionPolicy,
20
+ ConnectorAdmissionRecord,
21
+ ConnectorAdmissionResult,
22
+ ConnectorApprovalPolicy,
23
+ ConnectorDiscoverySnapshot,
24
+ ConnectorToolSelection,
25
+ DescriptionPolicy,
26
+ DiscoveredProviderTool,
27
+ InputSchemaPolicy,
28
+ OutputSchemaPolicy,
29
+ _thaw_json_value,
30
+ )
31
+ from millforge.connectors.diagnostics import (
32
+ ConnectorDiagnostic,
33
+ ConnectorDiagnosticCode,
34
+ ConnectorDiagnosticPhase,
35
+ connector_diagnostic,
36
+ malformed_input_diagnostic,
37
+ )
38
+ from millforge.tools import ToolDescriptor
39
+
40
+ _INSTRUCTION_LIKE_RE = re.compile(
41
+ r"\b(ignore|override|system prompt|developer message|previous instructions|"
42
+ r"follow these instructions|you must|do not tell)\b",
43
+ re.IGNORECASE,
44
+ )
45
+
46
+ _T = TypeVar("_T", bound=BaseModel)
47
+
48
+
49
+ def admit_connector_tools(
50
+ snapshot: ConnectorDiscoverySnapshot | Mapping[str, Any],
51
+ manifest: ConnectorAdmissionManifest | Mapping[str, Any],
52
+ policy: ConnectorAdmissionPolicy | Mapping[str, Any],
53
+ ) -> ConnectorAdmissionResult:
54
+ """Lower explicit admitted connector tools into immutable descriptors.
55
+
56
+ The service treats discovery as untrusted evidence, validates raw mappings
57
+ into connector contracts, and returns stable diagnostics instead of raw
58
+ validation or descriptor-construction exceptions.
59
+ """
60
+ validated = (
61
+ _validate_contract(
62
+ ConnectorDiscoverySnapshot,
63
+ snapshot,
64
+ phase=ConnectorDiagnosticPhase.DISCOVERY,
65
+ ),
66
+ _validate_contract(
67
+ ConnectorAdmissionManifest,
68
+ manifest,
69
+ phase=ConnectorDiagnosticPhase.MANIFEST,
70
+ ),
71
+ _validate_contract(
72
+ ConnectorAdmissionPolicy,
73
+ policy,
74
+ phase=ConnectorDiagnosticPhase.POLICY,
75
+ ),
76
+ )
77
+ diagnostics = tuple(diagnostic for _, diagnostic in validated if diagnostic)
78
+ if diagnostics:
79
+ return ConnectorAdmissionResult(accepted=False, diagnostics=diagnostics)
80
+
81
+ valid_snapshot = validated[0][0]
82
+ valid_manifest = validated[1][0]
83
+ valid_policy = validated[2][0]
84
+ if not isinstance(valid_snapshot, ConnectorDiscoverySnapshot):
85
+ raise AssertionError("snapshot validation returned unexpected contract")
86
+ if not isinstance(valid_manifest, ConnectorAdmissionManifest):
87
+ raise AssertionError("manifest validation returned unexpected contract")
88
+ if not isinstance(valid_policy, ConnectorAdmissionPolicy):
89
+ raise AssertionError("policy validation returned unexpected contract")
90
+
91
+ admission = _Admission(valid_snapshot, valid_manifest, valid_policy)
92
+ return admission.run()
93
+
94
+
95
+ class _Admission:
96
+ def __init__(
97
+ self,
98
+ snapshot: ConnectorDiscoverySnapshot,
99
+ manifest: ConnectorAdmissionManifest,
100
+ policy: ConnectorAdmissionPolicy,
101
+ ) -> None:
102
+ self.snapshot = snapshot
103
+ self.manifest = manifest
104
+ self.policy = policy
105
+ self.diagnostics: list[ConnectorDiagnostic] = []
106
+
107
+ def run(self) -> ConnectorAdmissionResult:
108
+ self._validate_global_inputs()
109
+ provider_tools = {
110
+ tool.provider_tool_name: tool for tool in self.snapshot.provider_tools
111
+ }
112
+ self._validate_denials(provider_tools)
113
+ if any(
114
+ diagnostic.code is not ConnectorDiagnosticCode.HASH_MISMATCH
115
+ for diagnostic in self.diagnostics
116
+ ):
117
+ return self._rejected()
118
+
119
+ admitted: list[tuple[ToolDescriptor, ConnectorAdmissionRecord]] = []
120
+ for selection in sorted(
121
+ self.manifest.selected_tools, key=lambda item: item.provider_tool_name
122
+ ):
123
+ provider_tool = provider_tools.get(selection.provider_tool_name)
124
+ if provider_tool is None:
125
+ self._diagnose(
126
+ ConnectorDiagnosticCode.ADMITTED_PROVIDER_TOOL_MISSING,
127
+ phase=ConnectorDiagnosticPhase.ADMISSION,
128
+ path=f"/selected_tools/{selection.provider_tool_name}",
129
+ message="Selected provider tool is not present in discovery.",
130
+ evidence={"provider_tool_name": selection.provider_tool_name},
131
+ )
132
+ continue
133
+ lowered = self._lower_selection(selection, provider_tool)
134
+ if lowered is not None:
135
+ admitted.append(lowered)
136
+
137
+ if self.diagnostics:
138
+ return self._rejected()
139
+
140
+ descriptors = tuple(descriptor for descriptor, _ in admitted)
141
+ records = tuple(record for _, record in admitted)
142
+ return ConnectorAdmissionResult(
143
+ accepted=True,
144
+ descriptors=descriptors,
145
+ records=records,
146
+ )
147
+
148
+ def _validate_global_inputs(self) -> None:
149
+ identity = self.snapshot.connector_identity
150
+ identity_sha256 = self._safe_hash(
151
+ lambda: identity.identity_sha256,
152
+ phase=ConnectorDiagnosticPhase.IDENTITY,
153
+ path="/connector_identity_sha256",
154
+ message="Connector identity evidence could not be hashed.",
155
+ evidence={"connector_id": identity.connector_id},
156
+ )
157
+ self._identity_sha256 = identity_sha256
158
+ if self.manifest.connector_id != identity.connector_id:
159
+ self._diagnose(
160
+ ConnectorDiagnosticCode.CONNECTOR_ID_MISMATCH,
161
+ phase=ConnectorDiagnosticPhase.IDENTITY,
162
+ path="/connector_id",
163
+ message="Connector ID does not match discovery identity.",
164
+ evidence={
165
+ "manifest_connector_id": self.manifest.connector_id,
166
+ "snapshot_connector_id": identity.connector_id,
167
+ },
168
+ )
169
+ if not self.manifest.expected_identity.matches(identity):
170
+ self._diagnose(
171
+ ConnectorDiagnosticCode.EXPECTED_IDENTITY_MISMATCH,
172
+ phase=ConnectorDiagnosticPhase.IDENTITY,
173
+ path="/expected_identity",
174
+ message="Connector identity does not match the admission manifest.",
175
+ evidence={"connector_id": identity.connector_id},
176
+ )
177
+ if identity_sha256 is not None:
178
+ self._check_hash(
179
+ supplied=self.manifest.expected_connector_identity_sha256,
180
+ actual=identity_sha256,
181
+ path="/expected_connector_identity_sha256",
182
+ label="connector_identity_sha256",
183
+ )
184
+ if identity.protocol not in self.policy.allowed_protocols:
185
+ self._diagnose(
186
+ ConnectorDiagnosticCode.PROTOCOL_UNSUPPORTED,
187
+ phase=ConnectorDiagnosticPhase.POLICY,
188
+ path="/allowed_protocols",
189
+ message="Connector protocol is not allowed by admission policy.",
190
+ evidence={"protocol": identity.protocol.value},
191
+ )
192
+ if identity.transport_kind not in self.policy.allowed_transport_kinds:
193
+ self._diagnose(
194
+ ConnectorDiagnosticCode.TRANSPORT_UNSUPPORTED,
195
+ phase=ConnectorDiagnosticPhase.POLICY,
196
+ path="/allowed_transport_kinds",
197
+ message="Connector transport is not allowed by admission policy.",
198
+ evidence={"transport_kind": identity.transport_kind.value},
199
+ )
200
+ for name in self.snapshot.duplicate_provider_names:
201
+ self._diagnose(
202
+ ConnectorDiagnosticCode.DISCOVERY_DUPLICATE_PROVIDER_TOOL,
203
+ phase=ConnectorDiagnosticPhase.DISCOVERY,
204
+ path="/provider_tools",
205
+ message="Discovery contains duplicate provider tool names.",
206
+ evidence={"provider_tool_name": name},
207
+ )
208
+ snapshot_sha256 = self._safe_hash(
209
+ lambda: self.snapshot.discovery_snapshot_sha256,
210
+ phase=ConnectorDiagnosticPhase.DISCOVERY,
211
+ path="/discovery_snapshot_sha256",
212
+ message="Discovery evidence could not be hashed.",
213
+ evidence={"connector_id": identity.connector_id},
214
+ )
215
+ self._discovery_snapshot_sha256 = snapshot_sha256
216
+ if snapshot_sha256 is not None:
217
+ self._check_hash(
218
+ supplied=self.manifest.expected_discovery_snapshot_sha256,
219
+ actual=snapshot_sha256,
220
+ path="/expected_discovery_snapshot_sha256",
221
+ label="discovery_snapshot_sha256",
222
+ )
223
+
224
+ def _validate_denials(
225
+ self, provider_tools: Mapping[str, DiscoveredProviderTool]
226
+ ) -> None:
227
+ for denial in self.manifest.denied_tools:
228
+ if denial.provider_tool_name not in provider_tools:
229
+ self._diagnose(
230
+ ConnectorDiagnosticCode.DENIED_TOOL_INVALID,
231
+ phase=ConnectorDiagnosticPhase.MANIFEST,
232
+ path=f"/denied_tools/{denial.provider_tool_name}",
233
+ message="Denied provider tool is not present in discovery.",
234
+ evidence={"provider_tool_name": denial.provider_tool_name},
235
+ )
236
+ if denial.approval_policy is not ConnectorApprovalPolicy.FORBIDDEN:
237
+ self._diagnose(
238
+ ConnectorDiagnosticCode.DENIED_TOOL_INVALID,
239
+ phase=ConnectorDiagnosticPhase.MANIFEST,
240
+ path=f"/denied_tools/{denial.provider_tool_name}/approval_policy",
241
+ message="Denied connector tools must use forbidden approval policy.",
242
+ evidence={"provider_tool_name": denial.provider_tool_name},
243
+ )
244
+
245
+ def _lower_selection(
246
+ self,
247
+ selection: ConnectorToolSelection,
248
+ provider_tool: DiscoveredProviderTool,
249
+ ) -> tuple[ToolDescriptor, ConnectorAdmissionRecord] | None:
250
+ raw_tool_sha256 = self._safe_hash(
251
+ lambda: provider_tool.raw_tool_sha256,
252
+ phase=ConnectorDiagnosticPhase.DISCOVERY,
253
+ path=f"/provider_tools/{provider_tool.provider_tool_name}/raw_tool_sha256",
254
+ message="Provider tool evidence could not be hashed.",
255
+ evidence={"provider_tool_name": provider_tool.provider_tool_name},
256
+ )
257
+ if raw_tool_sha256 is None:
258
+ return None
259
+ self._check_hash(
260
+ supplied=selection.expected_raw_tool_sha256,
261
+ actual=raw_tool_sha256,
262
+ path=f"/selected_tools/{selection.provider_tool_name}/expected_raw_tool_sha256",
263
+ label="raw_tool_sha256",
264
+ )
265
+ if self._has_blocking_diagnostics():
266
+ return None
267
+ description = self._admitted_description(selection, provider_tool)
268
+ input_schema = self._admitted_input_schema(selection, provider_tool)
269
+ output_schema = self._admitted_output_schema(selection, provider_tool)
270
+ self._validate_capabilities(selection)
271
+ self._validate_approval(selection)
272
+ if self._has_blocking_diagnostics():
273
+ return None
274
+ if description is None or input_schema is None or output_schema is None:
275
+ return None
276
+
277
+ try:
278
+ descriptor = ToolDescriptor(
279
+ tool_id=selection.tool_id,
280
+ tool_version=selection.tool_version,
281
+ implementation_id=selection.implementation_id,
282
+ model_tool_name=selection.model_tool_name,
283
+ description=description,
284
+ input_schema=input_schema,
285
+ output_schema=output_schema,
286
+ required_capabilities=selection.required_capabilities,
287
+ produced_artifact_ids=selection.produced_artifact_ids,
288
+ side_effect_class=selection.side_effect_class,
289
+ idempotency=selection.idempotency,
290
+ timeout_policy=selection.timeout_policy,
291
+ output_policy=selection.output_policy,
292
+ )
293
+ except Exception as exc:
294
+ self._diagnose(
295
+ ConnectorDiagnosticCode.APPROVAL_POLICY_INVALID,
296
+ phase=ConnectorDiagnosticPhase.ADMISSION,
297
+ path=f"/selected_tools/{selection.provider_tool_name}",
298
+ message="Admitted connector descriptor is invalid.",
299
+ evidence={"error_type": type(exc).__name__},
300
+ )
301
+ return None
302
+
303
+ self._check_hash(
304
+ supplied=selection.expected_descriptor_sha256,
305
+ actual=descriptor.descriptor_sha256,
306
+ path=f"/selected_tools/{selection.provider_tool_name}/expected_descriptor_sha256",
307
+ label="descriptor_sha256",
308
+ )
309
+ record = ConnectorAdmissionRecord(
310
+ connector_id=self.manifest.connector_id,
311
+ provider_tool_name=selection.provider_tool_name,
312
+ connector_identity_sha256=(
313
+ getattr(self, "_identity_sha256", None)
314
+ or self.snapshot.connector_identity.identity_sha256
315
+ ),
316
+ discovery_snapshot_sha256=(
317
+ getattr(self, "_discovery_snapshot_sha256", None)
318
+ or self.snapshot.discovery_snapshot_sha256
319
+ ),
320
+ raw_tool_sha256=raw_tool_sha256,
321
+ input_schema_sha256=provider_tool.input_schema_sha256,
322
+ output_schema_sha256=provider_tool.output_schema_sha256,
323
+ provider_description_sha256=provider_tool.provider_description_sha256,
324
+ descriptor_sha256=descriptor.descriptor_sha256,
325
+ required_capabilities=descriptor.required_capabilities,
326
+ side_effect_class=descriptor.side_effect_class,
327
+ idempotency=descriptor.idempotency,
328
+ timeout_policy=descriptor.timeout_policy,
329
+ output_policy=descriptor.output_policy,
330
+ idempotency_key_policy=_idempotency_key_policy(descriptor.idempotency),
331
+ approval_policy=selection.approval_policy,
332
+ )
333
+ self._check_hash(
334
+ supplied=selection.expected_admission_record_sha256,
335
+ actual=record.admission_record_sha256,
336
+ path=f"/selected_tools/{selection.provider_tool_name}/expected_admission_record_sha256",
337
+ label="admission_record_sha256",
338
+ )
339
+ return descriptor, record
340
+
341
+ def _admitted_description(
342
+ self,
343
+ selection: ConnectorToolSelection,
344
+ provider_tool: DiscoveredProviderTool,
345
+ ) -> str | None:
346
+ if selection.description_policy is DescriptionPolicy.OPERATOR_SUPPLIED:
347
+ return self._normalize_description(
348
+ selection.description,
349
+ path=f"/selected_tools/{selection.provider_tool_name}/description",
350
+ )
351
+ if selection.description_policy is DescriptionPolicy.PROVIDER_REJECTED:
352
+ self._diagnose(
353
+ ConnectorDiagnosticCode.DESCRIPTION_REQUIRES_OPERATOR_TEXT,
354
+ phase=ConnectorDiagnosticPhase.MANIFEST,
355
+ path=f"/selected_tools/{selection.provider_tool_name}/description_policy",
356
+ message="Rejected provider descriptions require operator-supplied text.",
357
+ evidence={"provider_tool_name": selection.provider_tool_name},
358
+ )
359
+ return None
360
+
361
+ raw_description = provider_tool.provider_description
362
+ if _description_requires_operator_text(
363
+ raw_description,
364
+ maximum=self.policy.max_description_utf8,
365
+ ):
366
+ self._diagnose(
367
+ ConnectorDiagnosticCode.DESCRIPTION_REQUIRES_OPERATOR_TEXT,
368
+ phase=ConnectorDiagnosticPhase.ADMISSION,
369
+ path=f"/provider_tools/{provider_tool.provider_tool_name}/provider_description",
370
+ message="Provider description requires operator-supplied text.",
371
+ evidence={"provider_tool_name": provider_tool.provider_tool_name},
372
+ )
373
+ return None
374
+ normalized = self._normalize_description(
375
+ raw_description,
376
+ path=f"/provider_tools/{provider_tool.provider_tool_name}/provider_description",
377
+ )
378
+ if normalized != selection.description:
379
+ self._diagnose(
380
+ ConnectorDiagnosticCode.DESCRIPTION_REQUIRES_OPERATOR_TEXT,
381
+ phase=ConnectorDiagnosticPhase.MANIFEST,
382
+ path=f"/selected_tools/{selection.provider_tool_name}/description",
383
+ message="Sanitized provider description does not match manifest text.",
384
+ evidence={"provider_tool_name": selection.provider_tool_name},
385
+ )
386
+ return None
387
+ return normalized
388
+
389
+ def _admitted_input_schema(
390
+ self,
391
+ selection: ConnectorToolSelection,
392
+ provider_tool: DiscoveredProviderTool,
393
+ ) -> MappingProxyType[str, Any] | None:
394
+ if selection.input_schema_policy is InputSchemaPolicy.OPERATOR_OVERLAY:
395
+ if selection.input_schema is None:
396
+ self._diagnose(
397
+ ConnectorDiagnosticCode.INPUT_SCHEMA_UNSUPPORTED,
398
+ phase=ConnectorDiagnosticPhase.MANIFEST,
399
+ path=f"/selected_tools/{selection.provider_tool_name}/input_schema",
400
+ message="Operator overlay input schema is required.",
401
+ evidence={"provider_tool_name": selection.provider_tool_name},
402
+ )
403
+ return None
404
+ return _schema_dict(selection.input_schema)
405
+ provider_schema = _normalize_schema_or_diagnose(
406
+ provider_tool.input_schema,
407
+ field_name="input_schema",
408
+ phase=ConnectorDiagnosticPhase.ADMISSION,
409
+ path=f"/provider_tools/{provider_tool.provider_tool_name}/input_schema",
410
+ diagnose=self._diagnose,
411
+ )
412
+ if provider_schema is not None and selection.input_schema is not None:
413
+ selected_schema = _schema_dict(selection.input_schema)
414
+ if selected_schema != provider_schema:
415
+ self._diagnose(
416
+ ConnectorDiagnosticCode.HASH_MISMATCH,
417
+ phase=ConnectorDiagnosticPhase.MANIFEST,
418
+ path=f"/selected_tools/{selection.provider_tool_name}/input_schema",
419
+ message="Manifest input schema does not match provider schema.",
420
+ evidence={"provider_tool_name": selection.provider_tool_name},
421
+ )
422
+ return None
423
+ return provider_schema
424
+
425
+ def _admitted_output_schema(
426
+ self,
427
+ selection: ConnectorToolSelection,
428
+ provider_tool: DiscoveredProviderTool,
429
+ ) -> MappingProxyType[str, Any] | None:
430
+ if selection.output_schema_policy is OutputSchemaPolicy.OPERATOR_SUPPLIED:
431
+ if selection.output_schema is None:
432
+ self._diagnose(
433
+ ConnectorDiagnosticCode.OUTPUT_SCHEMA_UNSUPPORTED,
434
+ phase=ConnectorDiagnosticPhase.MANIFEST,
435
+ path=f"/selected_tools/{selection.provider_tool_name}/output_schema",
436
+ message="Operator-supplied output schema is required.",
437
+ evidence={"provider_tool_name": selection.provider_tool_name},
438
+ )
439
+ return None
440
+ return _schema_dict(selection.output_schema)
441
+ if provider_tool.output_schema is None:
442
+ self._diagnose(
443
+ ConnectorDiagnosticCode.OUTPUT_SCHEMA_UNSUPPORTED,
444
+ phase=ConnectorDiagnosticPhase.ADMISSION,
445
+ path=f"/provider_tools/{provider_tool.provider_tool_name}/output_schema",
446
+ message="Provider output schema is required unless operator supplied.",
447
+ evidence={"provider_tool_name": provider_tool.provider_tool_name},
448
+ )
449
+ return None
450
+ provider_schema = _normalize_schema_or_diagnose(
451
+ provider_tool.output_schema,
452
+ field_name="output_schema",
453
+ phase=ConnectorDiagnosticPhase.ADMISSION,
454
+ path=f"/provider_tools/{provider_tool.provider_tool_name}/output_schema",
455
+ diagnose=self._diagnose,
456
+ )
457
+ if provider_schema is not None and selection.output_schema is not None:
458
+ selected_schema = _schema_dict(selection.output_schema)
459
+ if selected_schema != provider_schema:
460
+ self._diagnose(
461
+ ConnectorDiagnosticCode.HASH_MISMATCH,
462
+ phase=ConnectorDiagnosticPhase.MANIFEST,
463
+ path=f"/selected_tools/{selection.provider_tool_name}/output_schema",
464
+ message="Manifest output schema does not match provider schema.",
465
+ evidence={"provider_tool_name": selection.provider_tool_name},
466
+ )
467
+ return None
468
+ return provider_schema
469
+
470
+ def _validate_capabilities(self, selection: ConnectorToolSelection) -> None:
471
+ if not selection.required_capabilities:
472
+ self._diagnose(
473
+ ConnectorDiagnosticCode.CAPABILITY_MISSING,
474
+ phase=ConnectorDiagnosticPhase.POLICY,
475
+ path=f"/selected_tools/{selection.provider_tool_name}/required_capabilities",
476
+ message="Selected connector tool requires explicit capabilities.",
477
+ evidence={"provider_tool_name": selection.provider_tool_name},
478
+ )
479
+ return
480
+ allowed = set(self.policy.allowed_capability_ids)
481
+ for capability_id in selection.required_capabilities:
482
+ if capability_id not in allowed:
483
+ self._diagnose(
484
+ ConnectorDiagnosticCode.CAPABILITY_UNKNOWN,
485
+ phase=ConnectorDiagnosticPhase.POLICY,
486
+ path=f"/selected_tools/{selection.provider_tool_name}/required_capabilities",
487
+ message="Connector capability is not allowed by admission policy.",
488
+ evidence={"capability_id": capability_id},
489
+ )
490
+
491
+ def _has_blocking_diagnostics(self) -> bool:
492
+ return any(
493
+ diagnostic.code is not ConnectorDiagnosticCode.HASH_MISMATCH
494
+ for diagnostic in self.diagnostics
495
+ )
496
+
497
+ def _safe_hash(
498
+ self,
499
+ supplier: Any,
500
+ *,
501
+ phase: ConnectorDiagnosticPhase,
502
+ path: str,
503
+ message: str,
504
+ evidence: Mapping[str, Any] | None = None,
505
+ ) -> str | None:
506
+ try:
507
+ return supplier()
508
+ except Exception as exc:
509
+ code = (
510
+ ConnectorDiagnosticCode.SECRET_MATERIAL
511
+ if "secret material" in str(exc).lower()
512
+ else ConnectorDiagnosticCode.IDENTITY_INVALID
513
+ )
514
+ self._diagnose(
515
+ code,
516
+ phase=phase,
517
+ path=path,
518
+ message=(
519
+ "Discovery evidence contains suspected secret material."
520
+ if code is ConnectorDiagnosticCode.SECRET_MATERIAL
521
+ else message
522
+ ),
523
+ evidence={
524
+ **(evidence or {}),
525
+ "error_type": type(exc).__name__,
526
+ },
527
+ )
528
+ return None
529
+
530
+ def _validate_approval(self, selection: ConnectorToolSelection) -> None:
531
+ allowed = self.policy.side_effect_approval_matrix.get(
532
+ selection.side_effect_class
533
+ )
534
+ if allowed is None or selection.approval_policy not in allowed:
535
+ self._diagnose(
536
+ ConnectorDiagnosticCode.APPROVAL_POLICY_INVALID,
537
+ phase=ConnectorDiagnosticPhase.POLICY,
538
+ path=f"/selected_tools/{selection.provider_tool_name}/approval_policy",
539
+ message="Approval policy is not allowed for side-effect class.",
540
+ evidence={
541
+ "approval_policy": selection.approval_policy.value,
542
+ "side_effect_class": selection.side_effect_class.value,
543
+ },
544
+ )
545
+
546
+ def _normalize_description(self, value: str, *, path: str) -> str | None:
547
+ normalized = " ".join(value.split())
548
+ try:
549
+ validate_utf8_size(
550
+ normalized, "description", self.policy.max_description_utf8
551
+ )
552
+ except Exception:
553
+ self._diagnose(
554
+ ConnectorDiagnosticCode.DESCRIPTION_REQUIRES_OPERATOR_TEXT,
555
+ phase=ConnectorDiagnosticPhase.MANIFEST,
556
+ path=path,
557
+ message="Connector description exceeds admission policy.",
558
+ )
559
+ return None
560
+ return normalized
561
+
562
+ def _check_hash(
563
+ self,
564
+ *,
565
+ supplied: str | None,
566
+ actual: str,
567
+ path: str,
568
+ label: str,
569
+ ) -> None:
570
+ if supplied is not None and supplied != actual:
571
+ self._diagnose(
572
+ ConnectorDiagnosticCode.HASH_MISMATCH,
573
+ phase=ConnectorDiagnosticPhase.ADMISSION,
574
+ path=path,
575
+ message="Supplied connector hash does not match recomputed hash.",
576
+ evidence={"hash": label},
577
+ )
578
+
579
+ def _diagnose(
580
+ self,
581
+ code: ConnectorDiagnosticCode,
582
+ *,
583
+ phase: ConnectorDiagnosticPhase,
584
+ message: str,
585
+ location: str | None = None,
586
+ path: str | None = None,
587
+ evidence: Mapping[str, Any] | None = None,
588
+ ) -> None:
589
+ self.diagnostics.append(
590
+ connector_diagnostic(
591
+ code,
592
+ phase=phase,
593
+ message=message,
594
+ location=location,
595
+ path=path,
596
+ evidence=evidence,
597
+ )
598
+ )
599
+
600
+ def _rejected(self) -> ConnectorAdmissionResult:
601
+ return ConnectorAdmissionResult(
602
+ accepted=False,
603
+ diagnostics=tuple(
604
+ sorted(
605
+ self.diagnostics,
606
+ key=lambda item: (
607
+ item.code.value,
608
+ item.phase.value,
609
+ item.path or "",
610
+ item.location or "",
611
+ item.message,
612
+ ),
613
+ )
614
+ ),
615
+ )
616
+
617
+
618
+ def _validate_contract(
619
+ model: type[_T],
620
+ value: _T | Mapping[str, Any],
621
+ *,
622
+ phase: ConnectorDiagnosticPhase,
623
+ ) -> tuple[_T | None, ConnectorDiagnostic | None]:
624
+ if isinstance(value, model):
625
+ return value, None
626
+ try:
627
+ return model.model_validate(value), None
628
+ except ValidationError as exc:
629
+ schema_diagnostic = _schema_subset_validation_diagnostic(
630
+ exc,
631
+ model_name=model.__name__,
632
+ phase=phase,
633
+ )
634
+ if schema_diagnostic is not None:
635
+ return None, schema_diagnostic
636
+ missing_field = _missing_field(exc)
637
+ return (
638
+ None,
639
+ malformed_input_diagnostic(
640
+ phase=phase,
641
+ model_name=model.__name__,
642
+ path=_validation_pointer(exc),
643
+ missing_field=missing_field,
644
+ code=_validation_code(exc),
645
+ ),
646
+ )
647
+ except Exception:
648
+ return (
649
+ None,
650
+ malformed_input_diagnostic(
651
+ phase=phase,
652
+ model_name=model.__name__,
653
+ code=ConnectorDiagnosticCode.IDENTITY_INVALID,
654
+ ),
655
+ )
656
+
657
+
658
+ def _normalize_schema_or_diagnose(
659
+ schema: Mapping[str, Any],
660
+ *,
661
+ field_name: str,
662
+ phase: ConnectorDiagnosticPhase,
663
+ path: str,
664
+ diagnose: Any,
665
+ ) -> MappingProxyType[str, Any] | None:
666
+ try:
667
+ return validate_json_schema_subset(
668
+ _thaw_json_value(schema), field_name=field_name
669
+ )
670
+ except Exception as exc:
671
+ diagnose(
672
+ (
673
+ ConnectorDiagnosticCode.INPUT_SCHEMA_UNSUPPORTED
674
+ if field_name == "input_schema"
675
+ else ConnectorDiagnosticCode.OUTPUT_SCHEMA_UNSUPPORTED
676
+ ),
677
+ phase=phase,
678
+ path=path,
679
+ message="Connector schema is outside the accepted JSON Schema subset.",
680
+ evidence={
681
+ "error_type": type(exc).__name__,
682
+ "schema_error": str(exc),
683
+ },
684
+ )
685
+ return None
686
+
687
+
688
+ def _schema_subset_validation_diagnostic(
689
+ exc: ValidationError,
690
+ *,
691
+ model_name: str,
692
+ phase: ConnectorDiagnosticPhase,
693
+ ) -> ConnectorDiagnostic | None:
694
+ for error in exc.errors():
695
+ ctx = error.get("ctx")
696
+ if not isinstance(ctx, Mapping):
697
+ continue
698
+ schema_error = ctx.get("error")
699
+ if not isinstance(schema_error, SchemaSubsetError):
700
+ continue
701
+ loc = error.get("loc")
702
+ path = _validation_pointer_from_loc(loc)
703
+ code = (
704
+ ConnectorDiagnosticCode.OUTPUT_SCHEMA_UNSUPPORTED
705
+ if _validation_loc_has_field(loc, "output_schema")
706
+ else ConnectorDiagnosticCode.INPUT_SCHEMA_UNSUPPORTED
707
+ )
708
+ return connector_diagnostic(
709
+ code,
710
+ phase=phase,
711
+ path=path,
712
+ message="Connector schema is outside the accepted JSON Schema subset.",
713
+ evidence={
714
+ "model": model_name,
715
+ "error_type": type(schema_error).__name__,
716
+ "schema_error": str(schema_error),
717
+ },
718
+ )
719
+ return None
720
+
721
+
722
+ def _idempotency_key_policy(idempotency: Any) -> str | None:
723
+ from millforge import IdempotencyClass
724
+
725
+ if idempotency is IdempotencyClass.IDEMPOTENT_WITH_KEY:
726
+ return "call_id"
727
+ return None
728
+
729
+
730
+ def _schema_dict(schema: Mapping[str, Any]) -> MappingProxyType[str, Any]:
731
+ return validate_json_schema_subset(_thaw_json_value(schema))
732
+
733
+
734
+ def _description_requires_operator_text(value: str, *, maximum: int) -> bool:
735
+ if len(value.encode("utf-8")) > maximum:
736
+ return True
737
+ return _INSTRUCTION_LIKE_RE.search(value) is not None
738
+
739
+
740
+ def _validation_pointer(exc: ValidationError) -> str:
741
+ errors = exc.errors()
742
+ if not errors:
743
+ return "/"
744
+ loc = errors[0].get("loc")
745
+ return _validation_pointer_from_loc(loc)
746
+
747
+
748
+ def _validation_pointer_from_loc(loc: Any) -> str:
749
+ if not isinstance(loc, tuple | list) or not loc:
750
+ return "/"
751
+ parts = [str(part).replace("~", "~0").replace("/", "~1") for part in loc]
752
+ return "/" + "/".join(parts)
753
+
754
+
755
+ def _validation_loc_has_field(loc: Any, field_name: str) -> bool:
756
+ if not isinstance(loc, tuple | list):
757
+ return False
758
+ return any(str(part) == field_name for part in loc)
759
+
760
+
761
+ def _missing_field(exc: ValidationError) -> str | None:
762
+ errors = exc.errors()
763
+ if not errors or errors[0].get("type") != "missing":
764
+ return None
765
+ loc = errors[0].get("loc")
766
+ if not isinstance(loc, tuple | list) or not loc:
767
+ return None
768
+ return str(loc[-1])
769
+
770
+
771
+ def _validation_code(exc: ValidationError) -> ConnectorDiagnosticCode:
772
+ errors = exc.errors()
773
+ if not errors:
774
+ return ConnectorDiagnosticCode.IDENTITY_INVALID
775
+ first = errors[0]
776
+ loc = tuple(str(part) for part in first.get("loc", ()))
777
+ message = str(first.get("msg", "")).lower()
778
+ error_text = str(errors).lower()
779
+ if "secret material" in error_text:
780
+ return ConnectorDiagnosticCode.SECRET_MATERIAL
781
+ if "forbidden approval policy cannot admit" in error_text:
782
+ return ConnectorDiagnosticCode.FORBIDDEN_TOOL_ADMITTED
783
+ if "selected tool identities" in error_text:
784
+ return ConnectorDiagnosticCode.DUPLICATE_ADMITTED_TOOL
785
+ if "selected model_tool_name" in error_text:
786
+ return ConnectorDiagnosticCode.DUPLICATE_MODEL_TOOL_NAME
787
+ if "selected implementation_id" in error_text:
788
+ return ConnectorDiagnosticCode.DUPLICATE_IMPLEMENTATION_ID
789
+ if "denied provider_tool_name" in error_text or "disjoint" in error_text:
790
+ return ConnectorDiagnosticCode.DENIED_TOOL_INVALID
791
+ if "output schema" in message or "output_schema" in loc:
792
+ return ConnectorDiagnosticCode.OUTPUT_SCHEMA_UNSUPPORTED
793
+ if "input schema" in message or "input_schema" in loc:
794
+ return ConnectorDiagnosticCode.INPUT_SCHEMA_UNSUPPORTED
795
+ if "required_capabilities" in loc:
796
+ return ConnectorDiagnosticCode.CAPABILITY_MISSING
797
+ if "approval_policy" in loc or "side-effecting connector tools" in error_text:
798
+ return ConnectorDiagnosticCode.APPROVAL_POLICY_INVALID
799
+ if "expected_identity" in loc or "connectoridentity" in error_text:
800
+ return ConnectorDiagnosticCode.IDENTITY_INVALID
801
+ return ConnectorDiagnosticCode.IDENTITY_INVALID