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,375 @@
1
+ """Immutable source-language contracts for Millforge harness files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import (
10
+ BaseModel,
11
+ ConfigDict,
12
+ Field,
13
+ StrictBool,
14
+ StrictInt,
15
+ StrictStr,
16
+ field_validator,
17
+ model_validator,
18
+ )
19
+
20
+ from millforge.compiler.validators import (
21
+ validate_argument_name,
22
+ validate_artifact_id,
23
+ validate_harness_id,
24
+ validate_harness_version,
25
+ validate_policy_id,
26
+ validate_profile_id,
27
+ validate_stage_kind_id,
28
+ validate_terminal_result,
29
+ validate_threshold,
30
+ validate_tool_reference,
31
+ validate_unique,
32
+ validate_utf8_size,
33
+ )
34
+
35
+ MAX_NODES = 512
36
+ MAX_PREREQUISITES_PER_NODE = 64
37
+ MAX_ARGUMENT_MATCHES_PER_PREREQUISITE = 32
38
+ MAX_TERMINAL_NODES = 64
39
+ MAX_DECLARED_ARTIFACTS = 512
40
+ MAX_TERMINAL_ARTIFACT_POLICIES = 64
41
+ MAX_ARTIFACTS_PER_TERMINAL = 512
42
+ MAX_STAGE_KIND_IDS = 64
43
+ MAX_SYSTEM_INSTRUCTIONS_UTF8 = 65_536
44
+
45
+
46
+ class ArgumentMatchSource(BaseModel):
47
+ """Immutable prerequisite argument equality mapping."""
48
+
49
+ model_config = ConfigDict(extra="forbid", frozen=True)
50
+
51
+ prior_argument: StrictStr
52
+ current_argument: StrictStr
53
+
54
+ @field_validator("prior_argument", "current_argument")
55
+ @classmethod
56
+ def _arguments_valid(cls, value: str) -> str:
57
+ return validate_argument_name(value)
58
+
59
+
60
+ class PrerequisiteSource(BaseModel):
61
+ """Immutable source prerequisite declaration."""
62
+
63
+ model_config = ConfigDict(extra="forbid", frozen=True)
64
+
65
+ node_id: StrictStr
66
+ argument_matches: tuple[ArgumentMatchSource, ...] = Field(default_factory=tuple)
67
+
68
+ @model_validator(mode="before")
69
+ @classmethod
70
+ def _convert_argument_mapping(cls, data: Any) -> Any:
71
+ if not isinstance(data, Mapping):
72
+ return data
73
+ values = dict(data)
74
+ raw_matches = values.get("argument_matches")
75
+ if isinstance(raw_matches, Mapping):
76
+ values["argument_matches"] = tuple(
77
+ {"prior_argument": key, "current_argument": value}
78
+ for key, value in raw_matches.items()
79
+ )
80
+ return values
81
+
82
+ @field_validator("node_id")
83
+ @classmethod
84
+ def _node_id_valid(cls, value: str) -> str:
85
+ from millforge.compiler.validators import validate_node_id
86
+
87
+ return validate_node_id(value)
88
+
89
+ @field_validator("argument_matches")
90
+ @classmethod
91
+ def _argument_matches_limited(
92
+ cls, value: tuple[ArgumentMatchSource, ...]
93
+ ) -> tuple[ArgumentMatchSource, ...]:
94
+ if len(value) > MAX_ARGUMENT_MATCHES_PER_PREREQUISITE:
95
+ raise ValueError("argument_matches may contain at most 32 entries")
96
+ validate_unique(tuple(item.prior_argument for item in value), "prior_argument")
97
+ validate_unique(
98
+ tuple(item.current_argument for item in value), "current_argument"
99
+ )
100
+ return value
101
+
102
+
103
+ class HarnessNodeSource(BaseModel):
104
+ """Immutable source graph node declaration."""
105
+
106
+ model_config = ConfigDict(extra="forbid", frozen=True)
107
+
108
+ node_id: StrictStr
109
+ tool_ref: StrictStr
110
+ required: StrictBool = False
111
+ prerequisites: tuple[PrerequisiteSource, ...] = Field(default_factory=tuple)
112
+ terminal_result: StrictStr | None = None
113
+ produces: tuple[StrictStr, ...] = Field(default_factory=tuple)
114
+
115
+ @field_validator("node_id")
116
+ @classmethod
117
+ def _node_id_valid(cls, value: str) -> str:
118
+ from millforge.compiler.validators import validate_node_id
119
+
120
+ return validate_node_id(value)
121
+
122
+ @field_validator("tool_ref")
123
+ @classmethod
124
+ def _tool_ref_valid(cls, value: str) -> str:
125
+ return validate_tool_reference(value)
126
+
127
+ @field_validator("prerequisites")
128
+ @classmethod
129
+ def _prerequisites_limited(
130
+ cls, value: tuple[PrerequisiteSource, ...]
131
+ ) -> tuple[PrerequisiteSource, ...]:
132
+ if len(value) > MAX_PREREQUISITES_PER_NODE:
133
+ raise ValueError("prerequisites may contain at most 64 entries")
134
+ return value
135
+
136
+ @field_validator("terminal_result")
137
+ @classmethod
138
+ def _terminal_result_valid(cls, value: str | None) -> str | None:
139
+ return None if value is None else validate_terminal_result(value)
140
+
141
+ @field_validator("produces")
142
+ @classmethod
143
+ def _produces_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
144
+ for item in value:
145
+ validate_artifact_id(item)
146
+ return validate_unique(value, "produces")
147
+
148
+
149
+ class HarnessGraphSource(BaseModel):
150
+ """Immutable source graph carrying node IDs from the authoring mapping."""
151
+
152
+ model_config = ConfigDict(extra="forbid", frozen=True)
153
+
154
+ nodes: tuple[HarnessNodeSource, ...] = Field(min_length=1)
155
+
156
+ @model_validator(mode="before")
157
+ @classmethod
158
+ def _convert_node_mapping(cls, data: Any) -> Any:
159
+ if not isinstance(data, Mapping):
160
+ return data
161
+ values = dict(data)
162
+ raw_nodes = values.get("nodes")
163
+ if isinstance(raw_nodes, Mapping):
164
+ converted = []
165
+ for node_id, raw_node in raw_nodes.items():
166
+ if not isinstance(raw_node, Mapping):
167
+ converted.append({"node_id": node_id})
168
+ continue
169
+ if "node_id" in raw_node:
170
+ raise ValueError(
171
+ "graph node bodies must not define node_id; the mapping key is authoritative"
172
+ )
173
+ converted.append({**dict(raw_node), "node_id": node_id})
174
+ values["nodes"] = tuple(converted)
175
+ return values
176
+
177
+ @field_validator("nodes")
178
+ @classmethod
179
+ def _nodes_valid(
180
+ cls, value: tuple[HarnessNodeSource, ...]
181
+ ) -> tuple[HarnessNodeSource, ...]:
182
+ if len(value) > MAX_NODES:
183
+ raise ValueError("nodes may contain at most 512 entries")
184
+ validate_unique(tuple(node.node_id for node in value), "node_id")
185
+ terminal_count = sum(node.terminal_result is not None for node in value)
186
+ if terminal_count > MAX_TERMINAL_NODES:
187
+ raise ValueError("nodes may contain at most 64 terminal nodes")
188
+ return value
189
+
190
+
191
+ class StageScopeSource(BaseModel):
192
+ """Immutable source stage-scope declaration."""
193
+
194
+ model_config = ConfigDict(extra="forbid", frozen=True)
195
+
196
+ stage_kind_ids: tuple[StrictStr, ...] = Field(min_length=1)
197
+
198
+ @field_validator("stage_kind_ids")
199
+ @classmethod
200
+ def _stage_kind_ids_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
201
+ if len(value) > MAX_STAGE_KIND_IDS:
202
+ raise ValueError("stage_kind_ids may contain at most 64 entries")
203
+ for item in value:
204
+ validate_stage_kind_id(item)
205
+ return validate_unique(value, "stage_kind_ids")
206
+
207
+
208
+ class PromptSource(BaseModel):
209
+ """Immutable source prompt-policy declaration."""
210
+
211
+ model_config = ConfigDict(extra="forbid", frozen=True)
212
+
213
+ policy_id: StrictStr
214
+ system_instructions: StrictStr
215
+ include_request_context: StrictBool
216
+
217
+ @field_validator("policy_id")
218
+ @classmethod
219
+ def _policy_id_valid(cls, value: str) -> str:
220
+ return validate_policy_id(value)
221
+
222
+ @field_validator("system_instructions")
223
+ @classmethod
224
+ def _system_instructions_valid(cls, value: str) -> str:
225
+ if not value.strip():
226
+ raise ValueError("system_instructions must be nonblank")
227
+ return validate_utf8_size(
228
+ value, "system_instructions", MAX_SYSTEM_INSTRUCTIONS_UTF8
229
+ )
230
+
231
+
232
+ class BudgetSource(BaseModel):
233
+ """Immutable source budget declaration with no implicit defaults."""
234
+
235
+ model_config = ConfigDict(extra="forbid", frozen=True)
236
+
237
+ max_iterations: StrictInt = Field(ge=1, le=256)
238
+ max_validation_retries: StrictInt = Field(ge=1, le=64)
239
+ max_tool_errors: StrictInt = Field(ge=1, le=64)
240
+ max_prerequisite_violations: StrictInt = Field(ge=1, le=64)
241
+ max_premature_terminal_attempts: StrictInt = Field(ge=1, le=64)
242
+
243
+
244
+ class ContextPolicySource(BaseModel):
245
+ """Immutable source context-policy declaration."""
246
+
247
+ model_config = ConfigDict(extra="forbid", frozen=True)
248
+
249
+ strategy_id: Literal["forge.tiered.v1"]
250
+ budget_tokens: StrictInt = Field(ge=256, le=1_000_000)
251
+ keep_recent_iterations: StrictInt = Field(ge=0, le=64)
252
+ phase_thresholds: tuple[float, float, float]
253
+
254
+ @field_validator("phase_thresholds", mode="before")
255
+ @classmethod
256
+ def _thresholds_scalar_valid(cls, value: Any) -> tuple[float, float, float]:
257
+ if not isinstance(value, (tuple, list)) or len(value) != 3:
258
+ raise ValueError("phase_thresholds must contain exactly three values")
259
+ thresholds = tuple(
260
+ validate_threshold(item, "phase_thresholds") for item in value
261
+ )
262
+ return (thresholds[0], thresholds[1], thresholds[2])
263
+
264
+ @field_validator("phase_thresholds")
265
+ @classmethod
266
+ def _thresholds_ordered(
267
+ cls, value: tuple[float, float, float]
268
+ ) -> tuple[float, float, float]:
269
+ if any(not math.isfinite(item) for item in value):
270
+ raise ValueError("phase_thresholds must be finite")
271
+ if tuple(sorted(value)) != value:
272
+ raise ValueError("phase_thresholds must be non-decreasing")
273
+ return value
274
+
275
+
276
+ class TerminalArtifactPolicySource(BaseModel):
277
+ """Immutable terminal-to-required-artifacts policy entry."""
278
+
279
+ model_config = ConfigDict(extra="forbid", frozen=True)
280
+
281
+ terminal_result: StrictStr
282
+ artifact_ids: tuple[StrictStr, ...] = Field(min_length=1)
283
+
284
+ @field_validator("terminal_result")
285
+ @classmethod
286
+ def _terminal_result_valid(cls, value: str) -> str:
287
+ return validate_terminal_result(value)
288
+
289
+ @field_validator("artifact_ids")
290
+ @classmethod
291
+ def _artifact_ids_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
292
+ if len(value) > MAX_ARTIFACTS_PER_TERMINAL:
293
+ raise ValueError("artifact_ids may contain at most 512 entries")
294
+ for item in value:
295
+ validate_artifact_id(item)
296
+ return validate_unique(value, "artifact_ids")
297
+
298
+
299
+ class ArtifactPolicySource(BaseModel):
300
+ """Immutable source artifact policy declaration."""
301
+
302
+ model_config = ConfigDict(extra="forbid", frozen=True)
303
+
304
+ declared_artifact_ids: tuple[StrictStr, ...] = Field(default_factory=tuple)
305
+ required_by_terminal: tuple[TerminalArtifactPolicySource, ...] = Field(
306
+ default_factory=tuple
307
+ )
308
+
309
+ @model_validator(mode="before")
310
+ @classmethod
311
+ def _convert_terminal_mapping(cls, data: Any) -> Any:
312
+ if not isinstance(data, Mapping):
313
+ return data
314
+ values = dict(data)
315
+ raw_required = values.get("required_by_terminal")
316
+ if isinstance(raw_required, Mapping):
317
+ values["required_by_terminal"] = tuple(
318
+ {"terminal_result": terminal_result, "artifact_ids": artifact_ids}
319
+ for terminal_result, artifact_ids in raw_required.items()
320
+ )
321
+ return values
322
+
323
+ @field_validator("declared_artifact_ids")
324
+ @classmethod
325
+ def _declared_artifact_ids_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
326
+ if len(value) > MAX_DECLARED_ARTIFACTS:
327
+ raise ValueError("declared_artifact_ids may contain at most 512 entries")
328
+ for item in value:
329
+ validate_artifact_id(item)
330
+ return validate_unique(value, "declared_artifact_ids")
331
+
332
+ @field_validator("required_by_terminal")
333
+ @classmethod
334
+ def _required_by_terminal_limited(
335
+ cls, value: tuple[TerminalArtifactPolicySource, ...]
336
+ ) -> tuple[TerminalArtifactPolicySource, ...]:
337
+ if len(value) > MAX_TERMINAL_ARTIFACT_POLICIES:
338
+ raise ValueError("required_by_terminal may contain at most 64 entries")
339
+ validate_unique(
340
+ tuple(item.terminal_result for item in value), "required_by_terminal"
341
+ )
342
+ return value
343
+
344
+
345
+ class HarnessSource(BaseModel):
346
+ """Immutable validated Millforge harness source contract."""
347
+
348
+ model_config = ConfigDict(extra="forbid", frozen=True)
349
+
350
+ schema_version: Literal["1.0"]
351
+ kind: Literal["millforge_harness"]
352
+ harness_id: StrictStr
353
+ harness_version: StrictInt
354
+ stage_scope: StageScopeSource
355
+ model_profile_id: StrictStr
356
+ prompt: PromptSource
357
+ budgets: BudgetSource
358
+ context: ContextPolicySource
359
+ graph: HarnessGraphSource
360
+ artifacts: ArtifactPolicySource
361
+
362
+ @field_validator("harness_id")
363
+ @classmethod
364
+ def _harness_id_valid(cls, value: str) -> str:
365
+ return validate_harness_id(value)
366
+
367
+ @field_validator("harness_version")
368
+ @classmethod
369
+ def _harness_version_valid(cls, value: int) -> int:
370
+ return validate_harness_version(value)
371
+
372
+ @field_validator("model_profile_id")
373
+ @classmethod
374
+ def _model_profile_id_valid(cls, value: str) -> str:
375
+ return validate_profile_id(value)
@@ -0,0 +1,184 @@
1
+ """Shared validators for Millforge harness source contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from dataclasses import dataclass
8
+
9
+ HARNESS_ID_MAX_LENGTH = 160
10
+ POLICY_ID_MAX_LENGTH = 160
11
+ PROFILE_ID_MAX_LENGTH = 160
12
+ CANONICAL_TOOL_ID_MAX_LENGTH = 160
13
+ CAPABILITY_ID_MAX_LENGTH = CANONICAL_TOOL_ID_MAX_LENGTH
14
+ NODE_ID_MAX_LENGTH = 64
15
+ ARTIFACT_ID_MAX_LENGTH = 128
16
+ TERMINAL_RESULT_MAX_LENGTH = 128
17
+ ARGUMENT_NAME_MAX_LENGTH = 128
18
+ REQUEST_ID_MAX_LENGTH = 160
19
+ TOOL_VERSION_MAX = 2_147_483_647
20
+ HARNESS_VERSION_MAX = 2_147_483_647
21
+
22
+ _DOTTED_ID_RE = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
23
+ _NODE_ID_RE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
24
+ _ARTIFACT_ID_RE = re.compile(r"^[a-z][a-z0-9._-]{0,127}$")
25
+ _TERMINAL_RESULT_RE = re.compile(r"^[A-Z][A-Z0-9_]{0,127}$")
26
+ _ARGUMENT_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,127}$")
27
+ _TOOL_REF_RE = re.compile(
28
+ r"^(?P<tool_id>[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*)@(?P<version>[0-9]+)$"
29
+ )
30
+ _LOWER_FIELD_KEY_RE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
31
+ _SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ToolReference:
36
+ """Parsed exact-version tool reference."""
37
+
38
+ tool_id: str
39
+ version: int
40
+
41
+
42
+ def validate_nonblank(value: str, field_name: str) -> str:
43
+ """Validate a strict string is nonblank without normalizing it."""
44
+ if not value.strip():
45
+ raise ValueError(f"{field_name} must be a non-empty string")
46
+ return value
47
+
48
+
49
+ def validate_utf8_size(value: str, field_name: str, maximum: int) -> str:
50
+ """Validate a string's UTF-8 encoded size."""
51
+ if len(value.encode("utf-8")) > maximum:
52
+ raise ValueError(f"{field_name} must be at most {maximum} UTF-8 bytes")
53
+ return value
54
+
55
+
56
+ def validate_unique(values: tuple[str, ...], field_name: str) -> tuple[str, ...]:
57
+ """Validate a tuple has no duplicate values."""
58
+ if len(set(values)) != len(values):
59
+ raise ValueError(f"{field_name} values must be unique")
60
+ return values
61
+
62
+
63
+ def validate_harness_id(value: str) -> str:
64
+ return _validate_pattern(value, "harness_id", _DOTTED_ID_RE, HARNESS_ID_MAX_LENGTH)
65
+
66
+
67
+ def validate_policy_id(value: str) -> str:
68
+ return _validate_pattern(value, "policy_id", _DOTTED_ID_RE, POLICY_ID_MAX_LENGTH)
69
+
70
+
71
+ def validate_profile_id(value: str) -> str:
72
+ return _validate_pattern(value, "profile_id", _DOTTED_ID_RE, PROFILE_ID_MAX_LENGTH)
73
+
74
+
75
+ def validate_canonical_tool_id(value: str) -> str:
76
+ return _validate_pattern(
77
+ value, "tool_id", _DOTTED_ID_RE, CANONICAL_TOOL_ID_MAX_LENGTH
78
+ )
79
+
80
+
81
+ def validate_capability_id(value: str) -> str:
82
+ return _validate_pattern(
83
+ value, "capability_id", _DOTTED_ID_RE, CAPABILITY_ID_MAX_LENGTH
84
+ )
85
+
86
+
87
+ def validate_stage_kind_id(value: str) -> str:
88
+ return _validate_pattern(
89
+ value, "stage_kind_id", _DOTTED_ID_RE, POLICY_ID_MAX_LENGTH
90
+ )
91
+
92
+
93
+ def validate_request_id(value: str) -> str:
94
+ return _validate_pattern(value, "request_id", _DOTTED_ID_RE, REQUEST_ID_MAX_LENGTH)
95
+
96
+
97
+ def validate_node_id(value: str) -> str:
98
+ return _validate_pattern(value, "node_id", _NODE_ID_RE, NODE_ID_MAX_LENGTH)
99
+
100
+
101
+ def validate_artifact_id(value: str) -> str:
102
+ return _validate_pattern(
103
+ value, "artifact_id", _ARTIFACT_ID_RE, ARTIFACT_ID_MAX_LENGTH
104
+ )
105
+
106
+
107
+ def validate_terminal_result(value: str) -> str:
108
+ return _validate_pattern(
109
+ value, "terminal_result", _TERMINAL_RESULT_RE, TERMINAL_RESULT_MAX_LENGTH
110
+ )
111
+
112
+
113
+ def validate_argument_name(value: str) -> str:
114
+ return _validate_pattern(
115
+ value, "argument_name", _ARGUMENT_NAME_RE, ARGUMENT_NAME_MAX_LENGTH
116
+ )
117
+
118
+
119
+ def validate_lower_field_key(value: str) -> str:
120
+ return _validate_pattern(value, "key", _LOWER_FIELD_KEY_RE, 64)
121
+
122
+
123
+ def validate_sha256(value: str, field_name: str) -> str:
124
+ if not _SHA256_RE.fullmatch(value):
125
+ raise ValueError(f"{field_name} must be exactly 64 lowercase hex characters")
126
+ return value
127
+
128
+
129
+ def validate_harness_version(value: int) -> int:
130
+ if value < 1 or value > HARNESS_VERSION_MAX:
131
+ raise ValueError("harness_version must be in range 1..2147483647")
132
+ return value
133
+
134
+
135
+ def validate_tool_version(value: int) -> int:
136
+ if value < 1 or value > TOOL_VERSION_MAX:
137
+ raise ValueError("tool_version must be in range 1..2147483647")
138
+ return value
139
+
140
+
141
+ def validate_tool_reference(value: str) -> str:
142
+ parse_tool_reference(value)
143
+ return value
144
+
145
+
146
+ def parse_tool_reference(value: str) -> ToolReference:
147
+ """Parse and validate ``<canonical tool ID>@<positive integer version>``."""
148
+ match = _TOOL_REF_RE.fullmatch(value)
149
+ if match is None:
150
+ raise ValueError("tool_ref must be an exact-version tool reference")
151
+ tool_id = validate_canonical_tool_id(match.group("tool_id"))
152
+ version_text = match.group("version")
153
+ if len(version_text) > 1 and version_text.startswith("0"):
154
+ raise ValueError("tool_ref version must not contain leading zeroes")
155
+ version = int(version_text)
156
+ if version < 1 or version > TOOL_VERSION_MAX:
157
+ raise ValueError("tool_ref version must be in range 1..2147483647")
158
+ return ToolReference(tool_id=tool_id, version=version)
159
+
160
+
161
+ def validate_threshold(value: object, field_name: str) -> float:
162
+ """Validate a context threshold scalar.
163
+
164
+ Integer values are accepted for explicitly float-valued thresholds, but
165
+ strings and booleans are not coerced.
166
+ """
167
+ if isinstance(value, bool) or not isinstance(value, (float, int)):
168
+ raise ValueError(f"{field_name} must be a finite number")
169
+ threshold = float(value)
170
+ if not math.isfinite(threshold):
171
+ raise ValueError(f"{field_name} must be finite")
172
+ if threshold <= 0 or threshold > 1:
173
+ raise ValueError(f"{field_name} must be in (0, 1]")
174
+ return threshold
175
+
176
+
177
+ def _validate_pattern(
178
+ value: str, field_name: str, pattern: re.Pattern[str], maximum: int
179
+ ) -> str:
180
+ if len(value) > maximum:
181
+ raise ValueError(f"{field_name} must be at most {maximum} characters")
182
+ if pattern.fullmatch(value) is None:
183
+ raise ValueError(f"{field_name} has invalid format")
184
+ return value
@@ -0,0 +1,95 @@
1
+ """Connector admission contracts for deterministic offline admission."""
2
+
3
+ from millforge.connectors.contracts import (
4
+ CONNECTOR_ADMISSION_MANIFEST_KIND,
5
+ CONNECTOR_ADMISSION_MANIFEST_SCHEMA,
6
+ CONNECTOR_ADMISSION_MANIFEST_VERSION,
7
+ CONNECTOR_ADMISSION_RECORD_HASH_KIND,
8
+ CONNECTOR_DISCOVERY_SNAPSHOT_HASH_KIND,
9
+ CONNECTOR_DISCOVERY_TOOL_HASH_KIND,
10
+ CONNECTOR_IDENTITY_HASH_KIND,
11
+ ConnectorAdmissionManifest,
12
+ ConnectorAdmissionPolicy,
13
+ ConnectorAdmissionRecord,
14
+ ConnectorAdmissionResult,
15
+ ConnectorApprovalPolicy,
16
+ ConnectorContractValidation,
17
+ ConnectorDiscoverySnapshot,
18
+ ConnectorIdentity,
19
+ ConnectorProtocol,
20
+ ConnectorToolSelection,
21
+ ConnectorTransportKind,
22
+ DeniedConnectorTool,
23
+ DescriptionPolicy,
24
+ DiscoveredProviderTool,
25
+ ExpectedConnectorIdentity,
26
+ InputSchemaPolicy,
27
+ OutputSchemaPolicy,
28
+ )
29
+ from millforge.connectors.diagnostics import (
30
+ ConnectorDiagnostic,
31
+ ConnectorDiagnosticCode,
32
+ ConnectorDiagnosticEvidence,
33
+ ConnectorDiagnosticPhase,
34
+ ConnectorDiagnosticSeverity,
35
+ connector_diagnostic,
36
+ malformed_input_diagnostic,
37
+ )
38
+ from millforge.connectors.admission import admit_connector_tools
39
+ from millforge.connectors.broker import (
40
+ ConnectorBroker,
41
+ ConnectorBrokerOutcome,
42
+ ConnectorInvocationRequest,
43
+ ConnectorProviderToolEvidence,
44
+ connector_idempotency_key,
45
+ )
46
+ from millforge.connectors.fake import DeterministicFakeConnectorBroker
47
+ from millforge.connectors.runtime import (
48
+ ConnectorAdmissionBinding,
49
+ ConnectorAdmissionSnapshot,
50
+ ConnectorAdmissionSnapshotError,
51
+ )
52
+
53
+ __all__ = [
54
+ "CONNECTOR_ADMISSION_MANIFEST_KIND",
55
+ "CONNECTOR_ADMISSION_MANIFEST_SCHEMA",
56
+ "CONNECTOR_ADMISSION_MANIFEST_VERSION",
57
+ "CONNECTOR_ADMISSION_RECORD_HASH_KIND",
58
+ "CONNECTOR_DISCOVERY_SNAPSHOT_HASH_KIND",
59
+ "CONNECTOR_DISCOVERY_TOOL_HASH_KIND",
60
+ "CONNECTOR_IDENTITY_HASH_KIND",
61
+ "ConnectorAdmissionManifest",
62
+ "ConnectorAdmissionPolicy",
63
+ "ConnectorAdmissionRecord",
64
+ "ConnectorAdmissionResult",
65
+ "ConnectorAdmissionBinding",
66
+ "ConnectorAdmissionSnapshot",
67
+ "ConnectorAdmissionSnapshotError",
68
+ "ConnectorBroker",
69
+ "ConnectorBrokerOutcome",
70
+ "ConnectorInvocationRequest",
71
+ "ConnectorProviderToolEvidence",
72
+ "ConnectorApprovalPolicy",
73
+ "ConnectorContractValidation",
74
+ "ConnectorDiagnostic",
75
+ "ConnectorDiagnosticCode",
76
+ "ConnectorDiagnosticEvidence",
77
+ "ConnectorDiagnosticPhase",
78
+ "ConnectorDiagnosticSeverity",
79
+ "ConnectorDiscoverySnapshot",
80
+ "ConnectorIdentity",
81
+ "ConnectorProtocol",
82
+ "ConnectorToolSelection",
83
+ "ConnectorTransportKind",
84
+ "DeniedConnectorTool",
85
+ "DeterministicFakeConnectorBroker",
86
+ "DescriptionPolicy",
87
+ "DiscoveredProviderTool",
88
+ "ExpectedConnectorIdentity",
89
+ "InputSchemaPolicy",
90
+ "OutputSchemaPolicy",
91
+ "admit_connector_tools",
92
+ "connector_idempotency_key",
93
+ "connector_diagnostic",
94
+ "malformed_input_diagnostic",
95
+ ]