millforge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. millforge/__init__.py +1174 -0
  2. millforge/_forge/LICENSE +21 -0
  3. millforge/_forge/PROVENANCE.json +295 -0
  4. millforge/_forge/UPDATE_POLICY.md +24 -0
  5. millforge/_forge/__init__.py +14 -0
  6. millforge/_forge/adapter.py +2232 -0
  7. millforge/_forge/base_runner.py +121 -0
  8. millforge/_forge/clients/__init__.py +10 -0
  9. millforge/_forge/clients/base.py +200 -0
  10. millforge/_forge/context/__init__.py +23 -0
  11. millforge/_forge/context/manager.py +178 -0
  12. millforge/_forge/context/strategies.py +335 -0
  13. millforge/_forge/core/__init__.py +16 -0
  14. millforge/_forge/core/inference.py +433 -0
  15. millforge/_forge/core/messages.py +119 -0
  16. millforge/_forge/core/runner.py +479 -0
  17. millforge/_forge/core/steps.py +108 -0
  18. millforge/_forge/core/workflow.py +400 -0
  19. millforge/_forge/errors.py +222 -0
  20. millforge/_forge/guardrails/__init__.py +21 -0
  21. millforge/_forge/guardrails/error_tracker.py +71 -0
  22. millforge/_forge/guardrails/guardrails.py +194 -0
  23. millforge/_forge/guardrails/nudge.py +47 -0
  24. millforge/_forge/guardrails/response_validator.py +119 -0
  25. millforge/_forge/guardrails/step_enforcer.py +183 -0
  26. millforge/_forge/prompts/__init__.py +16 -0
  27. millforge/_forge/prompts/nudges.py +95 -0
  28. millforge/_forge/prompts/templates.py +285 -0
  29. millforge/_version.py +3 -0
  30. millforge/artifacts.py +570 -0
  31. millforge/base/__init__.py +97 -0
  32. millforge/base/composition.py +402 -0
  33. millforge/base/context.py +285 -0
  34. millforge/base/harness.py +138 -0
  35. millforge/base/identity.py +465 -0
  36. millforge/base/options.py +34 -0
  37. millforge/base/platform.py +17 -0
  38. millforge/base/prompt.py +317 -0
  39. millforge/base/runner.py +546 -0
  40. millforge/compiled_plan.py +970 -0
  41. millforge/compiler/__init__.py +231 -0
  42. millforge/compiler/artifact_validation.py +257 -0
  43. millforge/compiler/canonicalization.py +169 -0
  44. millforge/compiler/capabilities.py +66 -0
  45. millforge/compiler/catalogs.py +500 -0
  46. millforge/compiler/diagnostics.py +491 -0
  47. millforge/compiler/graph.py +678 -0
  48. millforge/compiler/lowering.py +198 -0
  49. millforge/compiler/output.py +692 -0
  50. millforge/compiler/parsing.py +1424 -0
  51. millforge/compiler/requests.py +1180 -0
  52. millforge/compiler/schema_validation.py +272 -0
  53. millforge/compiler/semantic.py +490 -0
  54. millforge/compiler/service.py +448 -0
  55. millforge/compiler/source.py +375 -0
  56. millforge/compiler/validators.py +184 -0
  57. millforge/connectors/__init__.py +95 -0
  58. millforge/connectors/admission.py +801 -0
  59. millforge/connectors/broker.py +202 -0
  60. millforge/connectors/contracts.py +1159 -0
  61. millforge/connectors/diagnostics.py +189 -0
  62. millforge/connectors/fake.py +66 -0
  63. millforge/connectors/runtime.py +236 -0
  64. millforge/contracts.py +2860 -0
  65. millforge/custom_tools/__init__.py +67 -0
  66. millforge/custom_tools/compiler.py +724 -0
  67. millforge/custom_tools/contracts.py +1093 -0
  68. millforge/custom_tools/diagnostics.py +205 -0
  69. millforge/eval_artifacts.py +952 -0
  70. millforge/eval_boundary.py +2435 -0
  71. millforge/eval_fixtures/__init__.py +1 -0
  72. millforge/eval_fixtures/default_pack/__init__.py +1 -0
  73. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
  74. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
  75. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
  76. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
  77. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
  78. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
  79. millforge/eval_fixtures/default_pack/manifest.json +12 -0
  80. millforge/eval_modes.py +1282 -0
  81. millforge/eval_presets.py +1398 -0
  82. millforge/eval_reports.py +2517 -0
  83. millforge/eval_suite.py +2429 -0
  84. millforge/eval_trials.py +2632 -0
  85. millforge/eval_workflow.py +794 -0
  86. millforge/exceptions.py +122 -0
  87. millforge/model_backend.py +2098 -0
  88. millforge/protocols.py +340 -0
  89. millforge/py.typed +0 -0
  90. millforge/runtime.py +1791 -0
  91. millforge/testing/__init__.py +1089 -0
  92. millforge/tools/__init__.py +83 -0
  93. millforge/tools/builtin_runtime.py +1339 -0
  94. millforge/tools/builtins.py +773 -0
  95. millforge/tools/execution.py +1545 -0
  96. millforge/tools/path_policy.py +155 -0
  97. millforge/tools/pi_compat/PI_LICENSE +21 -0
  98. millforge/tools/pi_compat/PROVENANCE.json +55 -0
  99. millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
  100. millforge/tools/pi_compat/__init__.py +34 -0
  101. millforge/tools/pi_compat/contracts.py +49 -0
  102. millforge/tools/pi_compat/editing.py +390 -0
  103. millforge/tools/pi_compat/mutations.py +57 -0
  104. millforge/tools/pi_compat/operations.py +401 -0
  105. millforge/tools/pi_compat/paths.py +155 -0
  106. millforge/tools/pi_compat/process.py +1375 -0
  107. millforge/tools/pi_compat/search.py +738 -0
  108. millforge/tools/pi_compat/truncation.py +267 -0
  109. millforge/tools/pi_compat_catalog.py +396 -0
  110. millforge/tools/pi_compat_runtime.py +460 -0
  111. millforge/tools/registry.py +553 -0
  112. millforge/tools/results.py +533 -0
  113. millforge-0.1.0.dist-info/METADATA +844 -0
  114. millforge-0.1.0.dist-info/RECORD +116 -0
  115. millforge-0.1.0.dist-info/WHEEL +4 -0
  116. millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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()