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,189 @@
|
|
|
1
|
+
"""Connector admission diagnostic contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
BaseModel,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
Field,
|
|
13
|
+
StrictStr,
|
|
14
|
+
field_validator,
|
|
15
|
+
model_validator,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from millforge.compiler.diagnostics import detect_secret_candidate
|
|
19
|
+
from millforge.compiler.validators import validate_lower_field_key, validate_utf8_size
|
|
20
|
+
from millforge.contracts import RedactionPolicy, redact_diagnostic_text
|
|
21
|
+
|
|
22
|
+
MAX_CONNECTOR_DIAGNOSTIC_MESSAGE_UTF8 = 1024
|
|
23
|
+
MAX_CONNECTOR_DIAGNOSTIC_EVIDENCE_UTF8 = 512
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConnectorDiagnosticSeverity(str, Enum):
|
|
27
|
+
"""Closed connector diagnostic severity values."""
|
|
28
|
+
|
|
29
|
+
ERROR = "error"
|
|
30
|
+
WARNING = "warning"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConnectorDiagnosticPhase(str, Enum):
|
|
34
|
+
"""Closed connector diagnostic phase values."""
|
|
35
|
+
|
|
36
|
+
IDENTITY = "identity"
|
|
37
|
+
DISCOVERY = "discovery"
|
|
38
|
+
MANIFEST = "manifest"
|
|
39
|
+
POLICY = "policy"
|
|
40
|
+
ADMISSION = "admission"
|
|
41
|
+
DIAGNOSTIC = "diagnostic"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConnectorDiagnosticCode(str, Enum):
|
|
45
|
+
"""Stable connector diagnostic codes."""
|
|
46
|
+
|
|
47
|
+
DISCOVERY_NOT_CATALOG = "MF-C001_DISCOVERY_NOT_CATALOG"
|
|
48
|
+
IDENTITY_INVALID = "MF-C002_IDENTITY_INVALID"
|
|
49
|
+
SECRET_MATERIAL = "MF-C003_SECRET_MATERIAL"
|
|
50
|
+
PROTOCOL_UNSUPPORTED = "MF-C004_PROTOCOL_UNSUPPORTED"
|
|
51
|
+
TRANSPORT_UNSUPPORTED = "MF-C005_TRANSPORT_UNSUPPORTED"
|
|
52
|
+
CONNECTOR_ID_MISMATCH = "MF-C006_CONNECTOR_ID_MISMATCH"
|
|
53
|
+
EXPECTED_IDENTITY_MISMATCH = "MF-C007_EXPECTED_IDENTITY_MISMATCH"
|
|
54
|
+
DISCOVERY_DUPLICATE_PROVIDER_TOOL = "MF-C008_DISCOVERY_DUPLICATE_PROVIDER_TOOL"
|
|
55
|
+
ADMITTED_PROVIDER_TOOL_MISSING = "MF-C009_ADMITTED_PROVIDER_TOOL_MISSING"
|
|
56
|
+
DENIED_TOOL_INVALID = "MF-C010_DENIED_TOOL_INVALID"
|
|
57
|
+
DUPLICATE_ADMITTED_TOOL = "MF-C011_DUPLICATE_ADMITTED_TOOL"
|
|
58
|
+
DUPLICATE_MODEL_TOOL_NAME = "MF-C012_DUPLICATE_MODEL_TOOL_NAME"
|
|
59
|
+
DUPLICATE_IMPLEMENTATION_ID = "MF-C013_DUPLICATE_IMPLEMENTATION_ID"
|
|
60
|
+
INPUT_SCHEMA_UNSUPPORTED = "MF-C014_INPUT_SCHEMA_UNSUPPORTED"
|
|
61
|
+
OUTPUT_SCHEMA_UNSUPPORTED = "MF-C015_OUTPUT_SCHEMA_UNSUPPORTED"
|
|
62
|
+
DESCRIPTION_REQUIRES_OPERATOR_TEXT = "MF-C016_DESCRIPTION_REQUIRES_OPERATOR_TEXT"
|
|
63
|
+
CAPABILITY_MISSING = "MF-C017_CAPABILITY_MISSING"
|
|
64
|
+
CAPABILITY_UNKNOWN = "MF-C018_CAPABILITY_UNKNOWN"
|
|
65
|
+
APPROVAL_POLICY_INVALID = "MF-C019_APPROVAL_POLICY_INVALID"
|
|
66
|
+
FORBIDDEN_TOOL_ADMITTED = "MF-C020_FORBIDDEN_TOOL_ADMITTED"
|
|
67
|
+
HASH_MISMATCH = "MF-C021_HASH_MISMATCH"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ConnectorDiagnosticEvidence(BaseModel):
|
|
71
|
+
"""Immutable redacted scalar evidence for connector diagnostics."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
74
|
+
|
|
75
|
+
key: StrictStr
|
|
76
|
+
value: StrictStr
|
|
77
|
+
|
|
78
|
+
@field_validator("key")
|
|
79
|
+
@classmethod
|
|
80
|
+
def _key_valid(cls, value: str) -> str:
|
|
81
|
+
return validate_lower_field_key(value)
|
|
82
|
+
|
|
83
|
+
@field_validator("value")
|
|
84
|
+
@classmethod
|
|
85
|
+
def _value_redacted(cls, value: str) -> str:
|
|
86
|
+
return redact_connector_text(
|
|
87
|
+
value, maximum=MAX_CONNECTOR_DIAGNOSTIC_EVIDENCE_UTF8
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ConnectorDiagnostic(BaseModel):
|
|
92
|
+
"""Stable connector diagnostic with redacted evidence."""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
95
|
+
|
|
96
|
+
code: ConnectorDiagnosticCode
|
|
97
|
+
severity: ConnectorDiagnosticSeverity
|
|
98
|
+
phase: ConnectorDiagnosticPhase
|
|
99
|
+
location: StrictStr | None = None
|
|
100
|
+
path: StrictStr | None = None
|
|
101
|
+
message: StrictStr
|
|
102
|
+
evidence: tuple[ConnectorDiagnosticEvidence, ...] = Field(default_factory=tuple)
|
|
103
|
+
|
|
104
|
+
@field_validator("location", "path")
|
|
105
|
+
@classmethod
|
|
106
|
+
def _optional_location_valid(cls, value: str | None) -> str | None:
|
|
107
|
+
if value is None:
|
|
108
|
+
return None
|
|
109
|
+
return validate_utf8_size(value, "diagnostic location", 256)
|
|
110
|
+
|
|
111
|
+
@field_validator("message")
|
|
112
|
+
@classmethod
|
|
113
|
+
def _message_redacted(cls, value: str) -> str:
|
|
114
|
+
return redact_connector_text(
|
|
115
|
+
value, maximum=MAX_CONNECTOR_DIAGNOSTIC_MESSAGE_UTF8
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@model_validator(mode="after")
|
|
119
|
+
def _location_or_path_required(self) -> ConnectorDiagnostic:
|
|
120
|
+
if self.location is None and self.path is None:
|
|
121
|
+
raise ValueError("diagnostic requires location or path")
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def redact_connector_text(value: Any, *, maximum: int) -> str:
|
|
126
|
+
"""Return bounded diagnostic text with secret-looking material redacted."""
|
|
127
|
+
text = str(value)
|
|
128
|
+
policy = RedactionPolicy()
|
|
129
|
+
if detect_secret_candidate(
|
|
130
|
+
field_path="/connector_diagnostic",
|
|
131
|
+
field_name="connector_diagnostic",
|
|
132
|
+
value=text,
|
|
133
|
+
policy=policy,
|
|
134
|
+
):
|
|
135
|
+
return policy.replacement
|
|
136
|
+
return validate_utf8_size(
|
|
137
|
+
redact_diagnostic_text(text, policy=policy), "diagnostic", maximum
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def connector_diagnostic(
|
|
142
|
+
code: ConnectorDiagnosticCode,
|
|
143
|
+
*,
|
|
144
|
+
phase: ConnectorDiagnosticPhase,
|
|
145
|
+
message: str,
|
|
146
|
+
location: str | None = None,
|
|
147
|
+
path: str | None = None,
|
|
148
|
+
severity: ConnectorDiagnosticSeverity = ConnectorDiagnosticSeverity.ERROR,
|
|
149
|
+
evidence: Mapping[str, Any] | None = None,
|
|
150
|
+
) -> ConnectorDiagnostic:
|
|
151
|
+
"""Build a deterministic connector diagnostic."""
|
|
152
|
+
fields = tuple(
|
|
153
|
+
ConnectorDiagnosticEvidence(key=key, value=str(value))
|
|
154
|
+
for key, value in sorted(
|
|
155
|
+
(evidence or {}).items(), key=lambda item: str(item[0])
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
return ConnectorDiagnostic(
|
|
159
|
+
code=code,
|
|
160
|
+
severity=severity,
|
|
161
|
+
phase=phase,
|
|
162
|
+
location=location,
|
|
163
|
+
path=path,
|
|
164
|
+
message=message,
|
|
165
|
+
evidence=fields,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def malformed_input_diagnostic(
|
|
170
|
+
*,
|
|
171
|
+
phase: ConnectorDiagnosticPhase,
|
|
172
|
+
model_name: str,
|
|
173
|
+
path: str = "/",
|
|
174
|
+
missing_field: str | None = None,
|
|
175
|
+
code: ConnectorDiagnosticCode = ConnectorDiagnosticCode.IDENTITY_INVALID,
|
|
176
|
+
) -> ConnectorDiagnostic:
|
|
177
|
+
"""Represent malformed raw input without exposing validator exception text."""
|
|
178
|
+
evidence: dict[str, str] = {"model": model_name}
|
|
179
|
+
message = "Connector input is malformed."
|
|
180
|
+
if missing_field is not None:
|
|
181
|
+
message = "Connector input is missing a required field."
|
|
182
|
+
evidence["field"] = missing_field
|
|
183
|
+
return connector_diagnostic(
|
|
184
|
+
code,
|
|
185
|
+
phase=phase,
|
|
186
|
+
path=path,
|
|
187
|
+
message=message,
|
|
188
|
+
evidence=evidence,
|
|
189
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Deterministic offline connector broker for runtime-boundary tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
|
|
7
|
+
from millforge.connectors.broker import (
|
|
8
|
+
ConnectorBrokerOutcome,
|
|
9
|
+
ConnectorInvocationRequest,
|
|
10
|
+
ConnectorProviderToolEvidence,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
FakeConnectorHandler = Callable[[ConnectorInvocationRequest], ConnectorBrokerOutcome]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeterministicFakeConnectorBroker:
|
|
17
|
+
"""In-memory connector broker keyed by connector ID and provider tool name."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
handlers: Mapping[
|
|
22
|
+
tuple[str, str], ConnectorBrokerOutcome | FakeConnectorHandler
|
|
23
|
+
]
|
|
24
|
+
| None = None,
|
|
25
|
+
*,
|
|
26
|
+
provider_evidence: Mapping[tuple[str, str], ConnectorProviderToolEvidence]
|
|
27
|
+
| None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._handlers = dict(handlers or {})
|
|
30
|
+
self._provider_evidence = dict(provider_evidence or {})
|
|
31
|
+
self._requests: list[ConnectorInvocationRequest] = []
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def requests(self) -> tuple[ConnectorInvocationRequest, ...]:
|
|
35
|
+
"""Return captured broker requests in invocation order."""
|
|
36
|
+
return tuple(self._requests)
|
|
37
|
+
|
|
38
|
+
def register(
|
|
39
|
+
self,
|
|
40
|
+
connector_id: str,
|
|
41
|
+
provider_tool_name: str,
|
|
42
|
+
outcome: ConnectorBrokerOutcome | FakeConnectorHandler,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Register a deterministic outcome or handler for a scoped provider tool."""
|
|
45
|
+
key = (connector_id, provider_tool_name)
|
|
46
|
+
if key in self._handlers:
|
|
47
|
+
raise ValueError("duplicate fake connector handler")
|
|
48
|
+
self._handlers[key] = outcome
|
|
49
|
+
|
|
50
|
+
def has_provider_tool(self, connector_id: str, provider_tool_name: str) -> bool:
|
|
51
|
+
return (connector_id, provider_tool_name) in self._handlers
|
|
52
|
+
|
|
53
|
+
def provider_tool_evidence(
|
|
54
|
+
self,
|
|
55
|
+
connector_id: str,
|
|
56
|
+
provider_tool_name: str,
|
|
57
|
+
) -> ConnectorProviderToolEvidence | None:
|
|
58
|
+
return self._provider_evidence.get((connector_id, provider_tool_name))
|
|
59
|
+
|
|
60
|
+
def invoke(self, request: ConnectorInvocationRequest) -> ConnectorBrokerOutcome:
|
|
61
|
+
key = (request.connector_id, request.provider_tool_name)
|
|
62
|
+
handler = self._handlers[key]
|
|
63
|
+
self._requests.append(request)
|
|
64
|
+
if isinstance(handler, ConnectorBrokerOutcome):
|
|
65
|
+
return handler
|
|
66
|
+
return handler(request)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Runtime-owned connector admission snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator
|
|
10
|
+
|
|
11
|
+
from millforge import IdempotencyClass, SideEffectClass, canonical_json_serialize
|
|
12
|
+
from millforge.compiler.catalogs import ToolCatalogSnapshot
|
|
13
|
+
from millforge.compiler.validators import validate_canonical_tool_id, validate_sha256
|
|
14
|
+
from millforge.connectors.contracts import (
|
|
15
|
+
ConnectorAdmissionRecord,
|
|
16
|
+
ConnectorApprovalPolicy,
|
|
17
|
+
)
|
|
18
|
+
from millforge.tools.registry import ToolOutputPolicy, ToolTimeoutPolicy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConnectorAdmissionSnapshotError(ValueError):
|
|
22
|
+
"""Fail-closed connector admission snapshot construction error."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConnectorAdmissionBinding(BaseModel):
|
|
26
|
+
"""Frozen runtime binding from a compiled connector descriptor to admission."""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
29
|
+
|
|
30
|
+
tool_id: StrictStr
|
|
31
|
+
tool_version: StrictInt = Field(ge=1)
|
|
32
|
+
descriptor_sha256: StrictStr
|
|
33
|
+
connector_id: StrictStr
|
|
34
|
+
provider_tool_name: StrictStr
|
|
35
|
+
connector_identity_sha256: StrictStr
|
|
36
|
+
discovery_snapshot_sha256: StrictStr
|
|
37
|
+
raw_tool_sha256: StrictStr
|
|
38
|
+
input_schema_sha256: StrictStr | None = None
|
|
39
|
+
output_schema_sha256: StrictStr | None = None
|
|
40
|
+
provider_description_sha256: StrictStr | None = None
|
|
41
|
+
required_capabilities: tuple[StrictStr, ...]
|
|
42
|
+
side_effect_class: SideEffectClass
|
|
43
|
+
idempotency: IdempotencyClass
|
|
44
|
+
timeout_policy: ToolTimeoutPolicy
|
|
45
|
+
output_policy: ToolOutputPolicy
|
|
46
|
+
idempotency_key_policy: StrictStr | None = None
|
|
47
|
+
approval_policy: ConnectorApprovalPolicy
|
|
48
|
+
admission_record_sha256: StrictStr
|
|
49
|
+
|
|
50
|
+
@field_validator("tool_id")
|
|
51
|
+
@classmethod
|
|
52
|
+
def _tool_id_valid(cls, value: str) -> str:
|
|
53
|
+
return validate_canonical_tool_id(value)
|
|
54
|
+
|
|
55
|
+
@field_validator(
|
|
56
|
+
"descriptor_sha256",
|
|
57
|
+
"connector_identity_sha256",
|
|
58
|
+
"discovery_snapshot_sha256",
|
|
59
|
+
"raw_tool_sha256",
|
|
60
|
+
"input_schema_sha256",
|
|
61
|
+
"output_schema_sha256",
|
|
62
|
+
"provider_description_sha256",
|
|
63
|
+
"admission_record_sha256",
|
|
64
|
+
)
|
|
65
|
+
@classmethod
|
|
66
|
+
def _hash_valid(cls, value: str | None, info: object) -> str | None:
|
|
67
|
+
if value is None:
|
|
68
|
+
return None
|
|
69
|
+
field_name = getattr(info, "field_name", "sha256")
|
|
70
|
+
return validate_sha256(value, str(field_name))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ConnectorAdmissionSnapshot:
|
|
74
|
+
"""Deep-frozen runtime snapshot of accepted connector admission records."""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
records: Iterable[ConnectorAdmissionRecord],
|
|
80
|
+
descriptor_snapshot: ToolCatalogSnapshot,
|
|
81
|
+
) -> None:
|
|
82
|
+
descriptor_keys_by_hash = _descriptor_keys_by_hash(descriptor_snapshot)
|
|
83
|
+
bindings: dict[tuple[str, int, str], ConnectorAdmissionBinding] = {}
|
|
84
|
+
for source_record in records:
|
|
85
|
+
record = ConnectorAdmissionRecord.model_validate(
|
|
86
|
+
source_record.model_dump(mode="json")
|
|
87
|
+
)
|
|
88
|
+
key_prefix = descriptor_keys_by_hash.get(record.descriptor_sha256)
|
|
89
|
+
if key_prefix is None:
|
|
90
|
+
raise ConnectorAdmissionSnapshotError(
|
|
91
|
+
"connector admission record is stale for descriptor snapshot"
|
|
92
|
+
)
|
|
93
|
+
tool_id, tool_version = key_prefix
|
|
94
|
+
if not is_connector_tool_id(tool_id):
|
|
95
|
+
raise ConnectorAdmissionSnapshotError(
|
|
96
|
+
"connector admission record targets a non-connector descriptor"
|
|
97
|
+
)
|
|
98
|
+
key = (tool_id, tool_version, record.descriptor_sha256)
|
|
99
|
+
if key in bindings:
|
|
100
|
+
raise ConnectorAdmissionSnapshotError(
|
|
101
|
+
"duplicate connector admission record"
|
|
102
|
+
)
|
|
103
|
+
descriptor_entry = descriptor_snapshot.resolve_exact(
|
|
104
|
+
tool_id, tool_version
|
|
105
|
+
).entry
|
|
106
|
+
if descriptor_entry is None:
|
|
107
|
+
raise ConnectorAdmissionSnapshotError(
|
|
108
|
+
"connector admission record is stale for descriptor snapshot"
|
|
109
|
+
)
|
|
110
|
+
_validate_record_descriptor_consistency(record, descriptor_entry)
|
|
111
|
+
bindings[key] = ConnectorAdmissionBinding(
|
|
112
|
+
tool_id=tool_id,
|
|
113
|
+
tool_version=tool_version,
|
|
114
|
+
descriptor_sha256=record.descriptor_sha256,
|
|
115
|
+
connector_id=record.connector_id,
|
|
116
|
+
provider_tool_name=record.provider_tool_name,
|
|
117
|
+
connector_identity_sha256=record.connector_identity_sha256,
|
|
118
|
+
discovery_snapshot_sha256=record.discovery_snapshot_sha256,
|
|
119
|
+
raw_tool_sha256=record.raw_tool_sha256,
|
|
120
|
+
input_schema_sha256=record.input_schema_sha256,
|
|
121
|
+
output_schema_sha256=record.output_schema_sha256,
|
|
122
|
+
provider_description_sha256=record.provider_description_sha256,
|
|
123
|
+
required_capabilities=record.required_capabilities,
|
|
124
|
+
side_effect_class=record.side_effect_class,
|
|
125
|
+
idempotency=record.idempotency,
|
|
126
|
+
timeout_policy=record.timeout_policy,
|
|
127
|
+
output_policy=record.output_policy,
|
|
128
|
+
idempotency_key_policy=record.idempotency_key_policy,
|
|
129
|
+
approval_policy=record.approval_policy,
|
|
130
|
+
admission_record_sha256=record.admission_record_sha256,
|
|
131
|
+
)
|
|
132
|
+
self._bindings = MappingProxyType(dict(sorted(bindings.items())))
|
|
133
|
+
self._snapshot_sha256 = _snapshot_sha256(self._bindings.values())
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def snapshot_sha256(self) -> str:
|
|
137
|
+
"""Return a deterministic hash of accepted connector admission bindings."""
|
|
138
|
+
return self._snapshot_sha256
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def bindings(self) -> tuple[ConnectorAdmissionBinding, ...]:
|
|
142
|
+
"""Return frozen connector admission bindings in deterministic order."""
|
|
143
|
+
return tuple(self._bindings.values())
|
|
144
|
+
|
|
145
|
+
def resolve(
|
|
146
|
+
self, tool_id: str, tool_version: int, descriptor_sha256: str
|
|
147
|
+
) -> ConnectorAdmissionBinding | None:
|
|
148
|
+
"""Resolve an exact compiled connector descriptor binding."""
|
|
149
|
+
return self._bindings.get((tool_id, tool_version, descriptor_sha256))
|
|
150
|
+
|
|
151
|
+
def require(
|
|
152
|
+
self, tool_id: str, tool_version: int, descriptor_sha256: str
|
|
153
|
+
) -> ConnectorAdmissionBinding:
|
|
154
|
+
"""Resolve an exact connector binding or fail construction."""
|
|
155
|
+
binding = self.resolve(tool_id, tool_version, descriptor_sha256)
|
|
156
|
+
if binding is None:
|
|
157
|
+
raise ConnectorAdmissionSnapshotError(
|
|
158
|
+
"compiled connector descriptor is missing admission record"
|
|
159
|
+
)
|
|
160
|
+
return binding
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_connector_tool_id(tool_id: str) -> bool:
|
|
164
|
+
"""Return whether a tool id belongs to the connector descriptor namespace."""
|
|
165
|
+
return tool_id.startswith("connector.")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _descriptor_keys_by_hash(
|
|
169
|
+
descriptor_snapshot: ToolCatalogSnapshot,
|
|
170
|
+
) -> dict[str, tuple[str, int]]:
|
|
171
|
+
records = getattr(descriptor_snapshot, "descriptor_hash_records", None)
|
|
172
|
+
if records is None:
|
|
173
|
+
raise ConnectorAdmissionSnapshotError(
|
|
174
|
+
"descriptor snapshot does not expose descriptor hash records"
|
|
175
|
+
)
|
|
176
|
+
keys_by_hash: dict[str, tuple[str, int]] = {}
|
|
177
|
+
for record in records:
|
|
178
|
+
descriptor_sha256 = record.descriptor_sha256
|
|
179
|
+
key = (record.tool_id, record.tool_version)
|
|
180
|
+
if descriptor_sha256 in keys_by_hash:
|
|
181
|
+
raise ConnectorAdmissionSnapshotError("duplicate descriptor hash record")
|
|
182
|
+
keys_by_hash[descriptor_sha256] = key
|
|
183
|
+
return keys_by_hash
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _validate_record_descriptor_consistency(
|
|
187
|
+
record: ConnectorAdmissionRecord, descriptor_entry: object
|
|
188
|
+
) -> None:
|
|
189
|
+
expected = {
|
|
190
|
+
"descriptor_sha256": getattr(descriptor_entry, "descriptor_sha256", None),
|
|
191
|
+
"required_capabilities": tuple(
|
|
192
|
+
getattr(descriptor_entry, "required_capabilities", ())
|
|
193
|
+
),
|
|
194
|
+
"side_effect_class": getattr(descriptor_entry, "side_effect_class", None),
|
|
195
|
+
"idempotency": getattr(descriptor_entry, "idempotency", None),
|
|
196
|
+
"timeout_policy": getattr(descriptor_entry, "timeout_policy", None),
|
|
197
|
+
"output_policy": getattr(descriptor_entry, "output_policy", None),
|
|
198
|
+
"idempotency_key_policy": (
|
|
199
|
+
"call_id"
|
|
200
|
+
if getattr(descriptor_entry, "idempotency", None)
|
|
201
|
+
is IdempotencyClass.IDEMPOTENT_WITH_KEY
|
|
202
|
+
else None
|
|
203
|
+
),
|
|
204
|
+
}
|
|
205
|
+
actual = {
|
|
206
|
+
"descriptor_sha256": record.descriptor_sha256,
|
|
207
|
+
"required_capabilities": record.required_capabilities,
|
|
208
|
+
"side_effect_class": record.side_effect_class,
|
|
209
|
+
"idempotency": record.idempotency,
|
|
210
|
+
"timeout_policy": record.timeout_policy,
|
|
211
|
+
"output_policy": record.output_policy,
|
|
212
|
+
"idempotency_key_policy": record.idempotency_key_policy,
|
|
213
|
+
}
|
|
214
|
+
for field, expected_value in expected.items():
|
|
215
|
+
if actual[field] != expected_value:
|
|
216
|
+
raise ConnectorAdmissionSnapshotError(
|
|
217
|
+
f"connector admission record is descriptor-inconsistent: {field}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _snapshot_sha256(bindings: Iterable[ConnectorAdmissionBinding]) -> str:
|
|
222
|
+
payload = {
|
|
223
|
+
"kind": "millforge.connector.admission_snapshot.v1",
|
|
224
|
+
"bindings": [
|
|
225
|
+
binding.model_dump(mode="json")
|
|
226
|
+
for binding in sorted(
|
|
227
|
+
bindings,
|
|
228
|
+
key=lambda item: (
|
|
229
|
+
item.tool_id,
|
|
230
|
+
item.tool_version,
|
|
231
|
+
item.descriptor_sha256,
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
],
|
|
235
|
+
}
|
|
236
|
+
return hashlib.sha256(canonical_json_serialize(payload).encode("utf-8")).hexdigest()
|