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