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,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
|