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,1339 @@
|
|
|
1
|
+
"""Explicit runtime implementations for accepted built-in tool bindings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from millforge import (
|
|
13
|
+
ArtifactRef,
|
|
14
|
+
SideEffectCertainty,
|
|
15
|
+
ToolExecutionStatus,
|
|
16
|
+
)
|
|
17
|
+
from millforge.compiled_plan import CompiledHarnessPlan
|
|
18
|
+
from millforge.contracts import (
|
|
19
|
+
SideEffectRecord,
|
|
20
|
+
TerminalIntent,
|
|
21
|
+
ToolExecutionContext,
|
|
22
|
+
ToolExecutionResult,
|
|
23
|
+
ValidatedToolCall,
|
|
24
|
+
)
|
|
25
|
+
from millforge.tools.builtins import (
|
|
26
|
+
create_builtin_tool_snapshot,
|
|
27
|
+
iter_builtin_tool_descriptors,
|
|
28
|
+
)
|
|
29
|
+
from millforge.tools.execution import (
|
|
30
|
+
CompiledToolBindingExecutor,
|
|
31
|
+
RuntimeToolRegistry,
|
|
32
|
+
create_tool_executor,
|
|
33
|
+
)
|
|
34
|
+
from millforge.tools.path_policy import (
|
|
35
|
+
PathPolicyError,
|
|
36
|
+
atomic_write_contained,
|
|
37
|
+
canonical_sha256_bytes,
|
|
38
|
+
logical_from_resolved,
|
|
39
|
+
resolve_existing_contained,
|
|
40
|
+
resolve_write_contained,
|
|
41
|
+
validate_logical_path,
|
|
42
|
+
)
|
|
43
|
+
from millforge.tools.results import (
|
|
44
|
+
ToolExecutionErrorCode,
|
|
45
|
+
canonical_sha256,
|
|
46
|
+
make_denial_result,
|
|
47
|
+
make_tool_result,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_DEFAULT_MAX_BYTES = 32_768
|
|
51
|
+
_MAX_BYTES = 1_048_576
|
|
52
|
+
_DEFAULT_MAX_RESULTS = 200
|
|
53
|
+
_MAX_RESULTS = 1_000
|
|
54
|
+
_ARTIFACT_FILENAMES = {
|
|
55
|
+
"plan": "plan.md",
|
|
56
|
+
"patch_summary": "patch_summary.md",
|
|
57
|
+
"test_results": "test_results.md",
|
|
58
|
+
"workspace_diff": "workspace_diff.md",
|
|
59
|
+
"checker_verdict": "checker_verdict.md",
|
|
60
|
+
"arbiter_verdict": "arbiter_verdict.md",
|
|
61
|
+
}
|
|
62
|
+
_ARTIFACT_CONTENT_TYPE = "text/markdown"
|
|
63
|
+
_READ_TOOL_ARTIFACT_ID = {
|
|
64
|
+
"builtin.artifact.read_plan": "plan",
|
|
65
|
+
"builtin.artifact.read_patch_summary": "patch_summary",
|
|
66
|
+
"builtin.artifact.read_test_results": "test_results",
|
|
67
|
+
"builtin.artifact.read_workspace_diff": "workspace_diff",
|
|
68
|
+
"builtin.artifact.read_checker_verdict": "checker_verdict",
|
|
69
|
+
}
|
|
70
|
+
_WRITE_TOOL_CONTENT_FIELD = {
|
|
71
|
+
"builtin.artifact.write_plan": ("plan", "plan"),
|
|
72
|
+
"builtin.artifact.write_patch_summary": ("patch_summary", "summary"),
|
|
73
|
+
"builtin.artifact.write_test_results": ("test_results", "results"),
|
|
74
|
+
"builtin.artifact.write_checker_verdict": ("checker_verdict", "verdict"),
|
|
75
|
+
"builtin.artifact.write_arbiter_verdict": ("arbiter_verdict", "verdict"),
|
|
76
|
+
}
|
|
77
|
+
_SHELL_TEST_PROFILES = {
|
|
78
|
+
"tool-boundary": (
|
|
79
|
+
"python",
|
|
80
|
+
"-m",
|
|
81
|
+
"pytest",
|
|
82
|
+
"tests/test_tool_execution_boundary.py",
|
|
83
|
+
"tests/test_builtin_tool_catalog.py",
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
_SHELL_STATIC_CHECK_PROFILES = {
|
|
87
|
+
"tool-boundary-ruff": (
|
|
88
|
+
"python",
|
|
89
|
+
"-m",
|
|
90
|
+
"ruff",
|
|
91
|
+
"check",
|
|
92
|
+
"src/millforge/tools",
|
|
93
|
+
"src/millforge/contracts.py",
|
|
94
|
+
"src/millforge/compiled_plan.py",
|
|
95
|
+
"tests/test_tool_execution_boundary.py",
|
|
96
|
+
),
|
|
97
|
+
"tool-boundary-mypy": (
|
|
98
|
+
"python",
|
|
99
|
+
"-m",
|
|
100
|
+
"mypy",
|
|
101
|
+
"src/millforge/tools",
|
|
102
|
+
"src/millforge/contracts.py",
|
|
103
|
+
"src/millforge/compiled_plan.py",
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
_SHELL_ALLOWED_SELECTORS = {None, "", "all"}
|
|
107
|
+
_SHELL_ENV_ALLOWLIST = (
|
|
108
|
+
"PATH",
|
|
109
|
+
"HOME",
|
|
110
|
+
"TMPDIR",
|
|
111
|
+
"TMP",
|
|
112
|
+
"TEMP",
|
|
113
|
+
"SYSTEMROOT",
|
|
114
|
+
"WINDIR",
|
|
115
|
+
)
|
|
116
|
+
_SHELL_DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
117
|
+
_SHELL_MAX_TIMEOUT_SECONDS = 1800.0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
BuiltinImplementation = Callable[
|
|
121
|
+
[ValidatedToolCall, ToolExecutionContext], ToolExecutionResult
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_builtin_runtime_registry() -> RuntimeToolRegistry:
|
|
126
|
+
"""Create the explicit source-owned built-in implementation registry."""
|
|
127
|
+
registry = RuntimeToolRegistry()
|
|
128
|
+
implementations: dict[str, BuiltinImplementation] = {
|
|
129
|
+
"builtin.request.inspect": _request_inspect,
|
|
130
|
+
"builtin.request.read_requirements": _request_requirements,
|
|
131
|
+
"builtin.workspace.list_files": _workspace_list_files,
|
|
132
|
+
"builtin.workspace.read_file": _workspace_read_file,
|
|
133
|
+
"builtin.workspace.search_text": _workspace_search_text,
|
|
134
|
+
"builtin.workspace.write_file": _workspace_write_file,
|
|
135
|
+
"builtin.workspace.apply_patch": _workspace_apply_patch,
|
|
136
|
+
"builtin.workspace.read_diff": _workspace_read_diff,
|
|
137
|
+
"builtin.shell.run_tests": _shell_run_profile,
|
|
138
|
+
"builtin.shell.run_static_check": _shell_run_profile,
|
|
139
|
+
"builtin.artifact.read": _artifact_read,
|
|
140
|
+
"builtin.artifact.read_plan": _artifact_read,
|
|
141
|
+
"builtin.artifact.read_patch_summary": _artifact_read,
|
|
142
|
+
"builtin.artifact.read_test_results": _artifact_read,
|
|
143
|
+
"builtin.artifact.read_workspace_diff": _artifact_read,
|
|
144
|
+
"builtin.artifact.read_checker_verdict": _artifact_read,
|
|
145
|
+
"builtin.artifact.write_plan": _artifact_write,
|
|
146
|
+
"builtin.artifact.write_patch_summary": _artifact_write,
|
|
147
|
+
"builtin.artifact.write_test_results": _artifact_write,
|
|
148
|
+
"builtin.artifact.write_workspace_diff": _artifact_write,
|
|
149
|
+
"builtin.artifact.write_checker_verdict": _artifact_write,
|
|
150
|
+
"builtin.artifact.write_arbiter_verdict": _artifact_write,
|
|
151
|
+
"builtin.artifact.write_verdict": _artifact_write,
|
|
152
|
+
"builtin.terminal.submit": _terminal_intent,
|
|
153
|
+
"builtin.terminal.reject": _terminal_intent,
|
|
154
|
+
"builtin.terminal.escalate": _terminal_intent,
|
|
155
|
+
}
|
|
156
|
+
for descriptor in iter_builtin_tool_descriptors():
|
|
157
|
+
implementation = implementations.get(descriptor.tool_id, _not_implemented)
|
|
158
|
+
registry.register(descriptor.implementation_id, implementation)
|
|
159
|
+
return registry
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_builtin_tool_executor(
|
|
163
|
+
plan: CompiledHarnessPlan,
|
|
164
|
+
) -> CompiledToolBindingExecutor:
|
|
165
|
+
"""Create a built-in executor admitted by the compiled plan and 04B snapshot."""
|
|
166
|
+
return create_tool_executor(
|
|
167
|
+
plan=plan,
|
|
168
|
+
descriptor_snapshot=create_builtin_tool_snapshot(),
|
|
169
|
+
runtime_registry=create_builtin_runtime_registry(),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def validate_builtin_pre_entry_policy(
|
|
174
|
+
call: ValidatedToolCall,
|
|
175
|
+
context: ToolExecutionContext,
|
|
176
|
+
*,
|
|
177
|
+
input_sha256: str | None = None,
|
|
178
|
+
) -> ToolExecutionResult | None:
|
|
179
|
+
"""Return a denial result when built-in side-effect policy rejects a call.
|
|
180
|
+
|
|
181
|
+
The validator intentionally performs no writes, process launches, terminal
|
|
182
|
+
handoff, queue/status mutation, or network access. Production built-in
|
|
183
|
+
implementations keep their internal checks as a second line of defense.
|
|
184
|
+
"""
|
|
185
|
+
input_digest = input_sha256 or canonical_sha256(call.arguments)
|
|
186
|
+
try:
|
|
187
|
+
error = _builtin_policy_error(call, context)
|
|
188
|
+
except (OSError, PathPolicyError, ValueError) as exc:
|
|
189
|
+
error = _policy_error(_policy_error_code(call), str(exc))
|
|
190
|
+
if error is None:
|
|
191
|
+
return None
|
|
192
|
+
descriptor = _descriptor_for_call(call)
|
|
193
|
+
return make_denial_result(
|
|
194
|
+
call_id=call.call_id,
|
|
195
|
+
code=error.code,
|
|
196
|
+
summary=error.summary,
|
|
197
|
+
evidence={
|
|
198
|
+
"tool_id": call.binding.tool_id,
|
|
199
|
+
"node_id": call.node_id,
|
|
200
|
+
"policy": error.policy,
|
|
201
|
+
},
|
|
202
|
+
side_effect_class=descriptor.side_effect_class,
|
|
203
|
+
idempotency=descriptor.idempotency,
|
|
204
|
+
input_sha256=input_digest,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _request_inspect(
|
|
209
|
+
call: ValidatedToolCall,
|
|
210
|
+
context: ToolExecutionContext,
|
|
211
|
+
) -> ToolExecutionResult:
|
|
212
|
+
artifact_refs = [ref.artifact_id for ref in context.input_artifacts]
|
|
213
|
+
work_item = context.work_item_id or ""
|
|
214
|
+
objective = work_item[:512]
|
|
215
|
+
output = {
|
|
216
|
+
"status": "success",
|
|
217
|
+
"summary": "request context inspected",
|
|
218
|
+
"request_id": context.request_id,
|
|
219
|
+
"stage_id": context.stage.stage_kind_id,
|
|
220
|
+
"objective": objective,
|
|
221
|
+
"artifact_refs": artifact_refs[:_DEFAULT_MAX_RESULTS],
|
|
222
|
+
"truncated": len(artifact_refs) > _DEFAULT_MAX_RESULTS,
|
|
223
|
+
}
|
|
224
|
+
return _success(call, output)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _request_requirements(
|
|
228
|
+
call: ValidatedToolCall,
|
|
229
|
+
context: ToolExecutionContext,
|
|
230
|
+
) -> ToolExecutionResult:
|
|
231
|
+
requirements = [
|
|
232
|
+
f"capability:{grant.capability_id}"
|
|
233
|
+
for grant in context.capability_envelope.grants
|
|
234
|
+
]
|
|
235
|
+
policy = context.compiled_artifact_policy
|
|
236
|
+
if policy is not None:
|
|
237
|
+
requirements.extend(
|
|
238
|
+
f"artifact:{artifact_id}" for artifact_id in policy.declared_artifact_ids
|
|
239
|
+
)
|
|
240
|
+
for item in policy.required_by_terminal:
|
|
241
|
+
joined = ",".join(item.artifact_ids)
|
|
242
|
+
requirements.append(f"terminal:{item.terminal_result}:artifacts:{joined}")
|
|
243
|
+
artifact_refs = [ref.artifact_id for ref in context.input_artifacts]
|
|
244
|
+
truncated = (
|
|
245
|
+
len(requirements) > _MAX_RESULTS or len(artifact_refs) > _DEFAULT_MAX_RESULTS
|
|
246
|
+
)
|
|
247
|
+
output = {
|
|
248
|
+
"status": "success",
|
|
249
|
+
"summary": "request requirements inspected",
|
|
250
|
+
"requirements": requirements[:_MAX_RESULTS],
|
|
251
|
+
"artifact_refs": artifact_refs[:_DEFAULT_MAX_RESULTS],
|
|
252
|
+
"truncated": truncated,
|
|
253
|
+
}
|
|
254
|
+
return _success(call, output)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _workspace_list_files(
|
|
258
|
+
call: ValidatedToolCall,
|
|
259
|
+
context: ToolExecutionContext,
|
|
260
|
+
) -> ToolExecutionResult:
|
|
261
|
+
root = _workspace_root(context)
|
|
262
|
+
if root is None:
|
|
263
|
+
return _denied(
|
|
264
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
265
|
+
)
|
|
266
|
+
max_results = _bounded_int(
|
|
267
|
+
call.arguments.get("max_results"), _DEFAULT_MAX_RESULTS, _MAX_RESULTS
|
|
268
|
+
)
|
|
269
|
+
try:
|
|
270
|
+
base = resolve_existing_contained(
|
|
271
|
+
root, str(call.arguments["root"]), allow_dot=True
|
|
272
|
+
)
|
|
273
|
+
pattern = str(call.arguments.get("glob") or "*")
|
|
274
|
+
paths: list[str] = []
|
|
275
|
+
truncated = False
|
|
276
|
+
for item in sorted(base.rglob("*")):
|
|
277
|
+
if not item.is_file():
|
|
278
|
+
continue
|
|
279
|
+
try:
|
|
280
|
+
logical = logical_from_resolved(root, item)
|
|
281
|
+
except PathPolicyError:
|
|
282
|
+
continue
|
|
283
|
+
if fnmatch.fnmatch(logical, pattern) or fnmatch.fnmatch(item.name, pattern):
|
|
284
|
+
paths.append(logical)
|
|
285
|
+
if len(paths) >= max_results:
|
|
286
|
+
truncated = True
|
|
287
|
+
break
|
|
288
|
+
except (OSError, PathPolicyError) as exc:
|
|
289
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
290
|
+
return _success(
|
|
291
|
+
call,
|
|
292
|
+
{
|
|
293
|
+
"status": "success",
|
|
294
|
+
"summary": f"listed {len(paths)} workspace file(s)",
|
|
295
|
+
"paths": paths,
|
|
296
|
+
"truncated": truncated,
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _workspace_read_file(
|
|
302
|
+
call: ValidatedToolCall,
|
|
303
|
+
context: ToolExecutionContext,
|
|
304
|
+
) -> ToolExecutionResult:
|
|
305
|
+
root = _workspace_root(context)
|
|
306
|
+
if root is None:
|
|
307
|
+
return _denied(
|
|
308
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
309
|
+
)
|
|
310
|
+
max_bytes = _bounded_int(
|
|
311
|
+
call.arguments.get("max_bytes"), _DEFAULT_MAX_BYTES, _MAX_BYTES
|
|
312
|
+
)
|
|
313
|
+
try:
|
|
314
|
+
target = resolve_existing_contained(root, str(call.arguments["path"]))
|
|
315
|
+
if not target.is_file():
|
|
316
|
+
return _denied(
|
|
317
|
+
call, ToolExecutionErrorCode.NOT_FOUND, "workspace file not found"
|
|
318
|
+
)
|
|
319
|
+
data = target.read_bytes()
|
|
320
|
+
except (OSError, PathPolicyError) as exc:
|
|
321
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
322
|
+
content, truncated = _decode_bounded(data, max_bytes)
|
|
323
|
+
return _success(
|
|
324
|
+
call,
|
|
325
|
+
{
|
|
326
|
+
"status": "success",
|
|
327
|
+
"summary": "workspace file read",
|
|
328
|
+
"content": content,
|
|
329
|
+
"truncated": truncated,
|
|
330
|
+
"artifact_refs": [],
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _workspace_search_text(
|
|
336
|
+
call: ValidatedToolCall,
|
|
337
|
+
context: ToolExecutionContext,
|
|
338
|
+
) -> ToolExecutionResult:
|
|
339
|
+
root = _workspace_root(context)
|
|
340
|
+
if root is None:
|
|
341
|
+
return _denied(
|
|
342
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
343
|
+
)
|
|
344
|
+
query = str(call.arguments["query"])
|
|
345
|
+
max_results = _bounded_int(
|
|
346
|
+
call.arguments.get("max_results"), _DEFAULT_MAX_RESULTS, _MAX_RESULTS
|
|
347
|
+
)
|
|
348
|
+
try:
|
|
349
|
+
base = resolve_existing_contained(
|
|
350
|
+
root, str(call.arguments.get("root") or "."), allow_dot=True
|
|
351
|
+
)
|
|
352
|
+
pattern = str(call.arguments.get("glob") or "*")
|
|
353
|
+
matches: list[dict[str, Any]] = []
|
|
354
|
+
truncated = False
|
|
355
|
+
for item in sorted(base.rglob("*")):
|
|
356
|
+
if not item.is_file():
|
|
357
|
+
continue
|
|
358
|
+
try:
|
|
359
|
+
logical = logical_from_resolved(root, item)
|
|
360
|
+
except PathPolicyError:
|
|
361
|
+
continue
|
|
362
|
+
if not (
|
|
363
|
+
fnmatch.fnmatch(logical, pattern) or fnmatch.fnmatch(item.name, pattern)
|
|
364
|
+
):
|
|
365
|
+
continue
|
|
366
|
+
try:
|
|
367
|
+
lines = item.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
368
|
+
except OSError:
|
|
369
|
+
continue
|
|
370
|
+
for line_number, line in enumerate(lines, start=1):
|
|
371
|
+
if query in line:
|
|
372
|
+
matches.append(
|
|
373
|
+
{
|
|
374
|
+
"path": logical,
|
|
375
|
+
"line": line_number,
|
|
376
|
+
"snippet": line[:512],
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
if len(matches) >= max_results:
|
|
380
|
+
truncated = True
|
|
381
|
+
raise StopIteration
|
|
382
|
+
except StopIteration:
|
|
383
|
+
pass
|
|
384
|
+
except (OSError, PathPolicyError) as exc:
|
|
385
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
386
|
+
return _success(
|
|
387
|
+
call,
|
|
388
|
+
{
|
|
389
|
+
"status": "success",
|
|
390
|
+
"summary": f"found {len(matches)} match(es)",
|
|
391
|
+
"matches": matches,
|
|
392
|
+
"truncated": truncated,
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _workspace_write_file(
|
|
398
|
+
call: ValidatedToolCall,
|
|
399
|
+
context: ToolExecutionContext,
|
|
400
|
+
) -> ToolExecutionResult:
|
|
401
|
+
root = _workspace_root(context)
|
|
402
|
+
if root is None:
|
|
403
|
+
return _denied(
|
|
404
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
405
|
+
)
|
|
406
|
+
path = str(call.arguments["path"])
|
|
407
|
+
content = str(call.arguments["content"]).encode("utf-8")
|
|
408
|
+
try:
|
|
409
|
+
target = resolve_write_contained(root, path)
|
|
410
|
+
expected = call.arguments.get("expected_sha256")
|
|
411
|
+
if expected is not None and target.exists():
|
|
412
|
+
actual = canonical_sha256_bytes(target.read_bytes())
|
|
413
|
+
if actual != expected:
|
|
414
|
+
return _denied(
|
|
415
|
+
call,
|
|
416
|
+
ToolExecutionErrorCode.CONFLICT,
|
|
417
|
+
"expected_sha256 does not match current file",
|
|
418
|
+
)
|
|
419
|
+
digest = atomic_write_contained(root, path, content)
|
|
420
|
+
except (OSError, PathPolicyError) as exc:
|
|
421
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
422
|
+
return _success(
|
|
423
|
+
call,
|
|
424
|
+
{
|
|
425
|
+
"status": "success",
|
|
426
|
+
"summary": "workspace file written atomically",
|
|
427
|
+
"path": validate_logical_path(path).as_posix(),
|
|
428
|
+
"content_sha256": digest,
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _workspace_apply_patch(
|
|
434
|
+
call: ValidatedToolCall,
|
|
435
|
+
context: ToolExecutionContext,
|
|
436
|
+
) -> ToolExecutionResult:
|
|
437
|
+
root = _workspace_root(context)
|
|
438
|
+
if root is None:
|
|
439
|
+
return _denied(
|
|
440
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
441
|
+
)
|
|
442
|
+
patch = str(call.arguments["patch"])
|
|
443
|
+
try:
|
|
444
|
+
changed_paths = _changed_paths_from_patch(root, patch)
|
|
445
|
+
before = _git_diff(root, changed_paths, _MAX_BYTES)
|
|
446
|
+
expected = call.arguments.get("expected_base_sha256")
|
|
447
|
+
if expected is not None and canonical_sha256(before) != expected:
|
|
448
|
+
return _denied(
|
|
449
|
+
call,
|
|
450
|
+
ToolExecutionErrorCode.CONFLICT,
|
|
451
|
+
"expected_base_sha256 does not match current diff",
|
|
452
|
+
)
|
|
453
|
+
subprocess.run(
|
|
454
|
+
["git", "apply", "--whitespace=nowarn", "-"],
|
|
455
|
+
cwd=root,
|
|
456
|
+
input=patch.encode("utf-8"),
|
|
457
|
+
stdout=subprocess.PIPE,
|
|
458
|
+
stderr=subprocess.PIPE,
|
|
459
|
+
check=True,
|
|
460
|
+
)
|
|
461
|
+
diff = _git_diff(root, changed_paths, _MAX_BYTES)
|
|
462
|
+
except subprocess.CalledProcessError as exc:
|
|
463
|
+
detail = exc.stderr.decode("utf-8", errors="replace")[:512]
|
|
464
|
+
return _denied(
|
|
465
|
+
call, ToolExecutionErrorCode.CONFLICT, detail or "patch rejected"
|
|
466
|
+
)
|
|
467
|
+
except (OSError, PathPolicyError, ValueError) as exc:
|
|
468
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
469
|
+
return _success(
|
|
470
|
+
call,
|
|
471
|
+
{
|
|
472
|
+
"status": "success",
|
|
473
|
+
"summary": f"applied patch touching {len(changed_paths)} path(s)",
|
|
474
|
+
"changed_paths": changed_paths,
|
|
475
|
+
"diff_sha256": canonical_sha256(diff),
|
|
476
|
+
},
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _workspace_read_diff(
|
|
481
|
+
call: ValidatedToolCall,
|
|
482
|
+
context: ToolExecutionContext,
|
|
483
|
+
) -> ToolExecutionResult:
|
|
484
|
+
root = _workspace_root(context)
|
|
485
|
+
if root is None:
|
|
486
|
+
return _denied(
|
|
487
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
488
|
+
)
|
|
489
|
+
max_bytes = _bounded_int(
|
|
490
|
+
call.arguments.get("max_bytes"), _DEFAULT_MAX_BYTES, _MAX_BYTES
|
|
491
|
+
)
|
|
492
|
+
try:
|
|
493
|
+
paths = [
|
|
494
|
+
validate_logical_path(str(item)).as_posix()
|
|
495
|
+
for item in call.arguments.get("paths", [])
|
|
496
|
+
]
|
|
497
|
+
diff = _git_diff(root, paths, max_bytes)
|
|
498
|
+
except (OSError, PathPolicyError) as exc:
|
|
499
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
500
|
+
encoded = diff.encode("utf-8")
|
|
501
|
+
truncated = len(encoded) > max_bytes
|
|
502
|
+
if truncated:
|
|
503
|
+
diff = encoded[:max_bytes].decode("utf-8", errors="ignore") + "[truncated]"
|
|
504
|
+
return _success(
|
|
505
|
+
call,
|
|
506
|
+
{
|
|
507
|
+
"status": "success",
|
|
508
|
+
"summary": "workspace diff read",
|
|
509
|
+
"diff": diff,
|
|
510
|
+
"truncated": truncated,
|
|
511
|
+
"diff_sha256": canonical_sha256(diff),
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _artifact_read(
|
|
517
|
+
call: ValidatedToolCall,
|
|
518
|
+
context: ToolExecutionContext,
|
|
519
|
+
) -> ToolExecutionResult:
|
|
520
|
+
artifact_id = _read_artifact_id(call)
|
|
521
|
+
max_bytes = _bounded_int(
|
|
522
|
+
call.arguments.get("max_bytes"), _DEFAULT_MAX_BYTES, _MAX_BYTES
|
|
523
|
+
)
|
|
524
|
+
error = _artifact_policy_error(context, artifact_id, produced_ids=None)
|
|
525
|
+
if error is not None:
|
|
526
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, error)
|
|
527
|
+
ref = _artifact_ref(artifact_id)
|
|
528
|
+
try:
|
|
529
|
+
path = _artifact_logical_path(artifact_id)
|
|
530
|
+
root = _artifact_root(context)
|
|
531
|
+
if root is None:
|
|
532
|
+
return _denied(
|
|
533
|
+
call,
|
|
534
|
+
ToolExecutionErrorCode.POLICY_DENIED,
|
|
535
|
+
"artifact root is unavailable",
|
|
536
|
+
)
|
|
537
|
+
target = resolve_existing_contained(root, path)
|
|
538
|
+
data = target.read_bytes()
|
|
539
|
+
except (OSError, PathPolicyError) as exc:
|
|
540
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
541
|
+
content, truncated = _decode_bounded(data, max_bytes)
|
|
542
|
+
return _success(
|
|
543
|
+
call,
|
|
544
|
+
{
|
|
545
|
+
"status": "success",
|
|
546
|
+
"summary": "artifact read",
|
|
547
|
+
"artifact_id": artifact_id,
|
|
548
|
+
"content": content,
|
|
549
|
+
"content_sha256": canonical_sha256_bytes(data),
|
|
550
|
+
"truncated": truncated,
|
|
551
|
+
},
|
|
552
|
+
artifact_refs=(ref,),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _artifact_write(
|
|
557
|
+
call: ValidatedToolCall,
|
|
558
|
+
context: ToolExecutionContext,
|
|
559
|
+
) -> ToolExecutionResult:
|
|
560
|
+
root = _artifact_root(context)
|
|
561
|
+
if root is None:
|
|
562
|
+
return _denied(
|
|
563
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "artifact root is unavailable"
|
|
564
|
+
)
|
|
565
|
+
if call.binding.tool_id == "builtin.artifact.write_verdict":
|
|
566
|
+
artifact_id = str(call.arguments["artifact_id"])
|
|
567
|
+
content = str(call.arguments["verdict"])
|
|
568
|
+
elif call.binding.tool_id == "builtin.artifact.write_workspace_diff":
|
|
569
|
+
artifact_id = "workspace_diff"
|
|
570
|
+
diff_result = _workspace_read_diff(_workspace_read_diff_call(call), context)
|
|
571
|
+
if diff_result.status is not ToolExecutionStatus.SUCCESS:
|
|
572
|
+
return _denied(
|
|
573
|
+
call,
|
|
574
|
+
ToolExecutionErrorCode(diff_result.error_code)
|
|
575
|
+
if diff_result.error_code is not None
|
|
576
|
+
else ToolExecutionErrorCode.POLICY_DENIED,
|
|
577
|
+
diff_result.summary,
|
|
578
|
+
)
|
|
579
|
+
if not isinstance(diff_result.structured_data, dict):
|
|
580
|
+
return _denied(
|
|
581
|
+
call,
|
|
582
|
+
ToolExecutionErrorCode.IMPLEMENTATION_ERROR,
|
|
583
|
+
"workspace diff runtime returned invalid structured data",
|
|
584
|
+
)
|
|
585
|
+
content = str(diff_result.structured_data["diff"])
|
|
586
|
+
else:
|
|
587
|
+
artifact_id, field = _WRITE_TOOL_CONTENT_FIELD[call.binding.tool_id]
|
|
588
|
+
content = str(call.arguments[field])
|
|
589
|
+
error = _artifact_policy_error(
|
|
590
|
+
context, artifact_id, produced_ids=call_node_produced_ids(call)
|
|
591
|
+
)
|
|
592
|
+
if error is not None:
|
|
593
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, error)
|
|
594
|
+
ref = _artifact_ref(artifact_id)
|
|
595
|
+
try:
|
|
596
|
+
digest = atomic_write_contained(
|
|
597
|
+
root, _artifact_logical_path(artifact_id), content.encode("utf-8")
|
|
598
|
+
)
|
|
599
|
+
except (OSError, PathPolicyError) as exc:
|
|
600
|
+
return _denied(call, ToolExecutionErrorCode.POLICY_DENIED, str(exc))
|
|
601
|
+
return _success(
|
|
602
|
+
call,
|
|
603
|
+
{
|
|
604
|
+
"status": "success",
|
|
605
|
+
"summary": "artifact written atomically",
|
|
606
|
+
"artifact_id": artifact_id,
|
|
607
|
+
"content_sha256": digest,
|
|
608
|
+
},
|
|
609
|
+
artifact_refs=(ref,),
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _shell_run_profile(
|
|
614
|
+
call: ValidatedToolCall,
|
|
615
|
+
context: ToolExecutionContext,
|
|
616
|
+
) -> ToolExecutionResult:
|
|
617
|
+
root = _workspace_root(context)
|
|
618
|
+
if root is None:
|
|
619
|
+
return _denied(
|
|
620
|
+
call, ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
621
|
+
)
|
|
622
|
+
if not root.is_dir():
|
|
623
|
+
return _denied(
|
|
624
|
+
call,
|
|
625
|
+
ToolExecutionErrorCode.POLICY_DENIED,
|
|
626
|
+
"workspace root is not a directory",
|
|
627
|
+
)
|
|
628
|
+
selector = call.arguments.get("selector")
|
|
629
|
+
if selector not in _SHELL_ALLOWED_SELECTORS:
|
|
630
|
+
return _denied(
|
|
631
|
+
call,
|
|
632
|
+
ToolExecutionErrorCode.INVALID_ARGUMENTS,
|
|
633
|
+
"selector is not an approved shell profile selector",
|
|
634
|
+
)
|
|
635
|
+
profile = str(call.arguments["profile"])
|
|
636
|
+
command = _shell_profile_command(call.binding.tool_id, profile)
|
|
637
|
+
if command is None:
|
|
638
|
+
return _denied(
|
|
639
|
+
call,
|
|
640
|
+
ToolExecutionErrorCode.INVALID_ARGUMENTS,
|
|
641
|
+
"profile is not an approved shell profile",
|
|
642
|
+
)
|
|
643
|
+
max_bytes = _bounded_int(
|
|
644
|
+
call.arguments.get("max_output_bytes"), _DEFAULT_MAX_BYTES, _MAX_BYTES
|
|
645
|
+
)
|
|
646
|
+
timeout_seconds = min(
|
|
647
|
+
context.timeout.timeout_seconds,
|
|
648
|
+
_descriptor_for_call(call).timeout_policy.timeout_seconds,
|
|
649
|
+
_SHELL_MAX_TIMEOUT_SECONDS,
|
|
650
|
+
)
|
|
651
|
+
if timeout_seconds <= 0:
|
|
652
|
+
timeout_seconds = _SHELL_DEFAULT_TIMEOUT_SECONDS
|
|
653
|
+
env = _shell_environment()
|
|
654
|
+
try:
|
|
655
|
+
completed = subprocess.run(
|
|
656
|
+
list(command),
|
|
657
|
+
cwd=root,
|
|
658
|
+
env=env,
|
|
659
|
+
stdin=subprocess.DEVNULL,
|
|
660
|
+
stdout=subprocess.PIPE,
|
|
661
|
+
stderr=subprocess.PIPE,
|
|
662
|
+
shell=False,
|
|
663
|
+
check=False,
|
|
664
|
+
timeout=timeout_seconds,
|
|
665
|
+
)
|
|
666
|
+
except subprocess.TimeoutExpired as exc:
|
|
667
|
+
output, truncated = _join_process_output(exc.stdout, exc.stderr, max_bytes)
|
|
668
|
+
return _process_failure(
|
|
669
|
+
call,
|
|
670
|
+
ToolExecutionStatus.TIMED_OUT,
|
|
671
|
+
ToolExecutionErrorCode.TIMEOUT,
|
|
672
|
+
f"shell profile timed out: {output}",
|
|
673
|
+
-1,
|
|
674
|
+
truncated,
|
|
675
|
+
SideEffectCertainty.COMPLETION_UNKNOWN,
|
|
676
|
+
)
|
|
677
|
+
except OSError as exc:
|
|
678
|
+
return _process_failure(
|
|
679
|
+
call,
|
|
680
|
+
ToolExecutionStatus.HARD_FAILURE,
|
|
681
|
+
ToolExecutionErrorCode.IMPLEMENTATION_ERROR,
|
|
682
|
+
f"shell profile failed to start: {type(exc).__name__}",
|
|
683
|
+
-1,
|
|
684
|
+
False,
|
|
685
|
+
SideEffectCertainty.NOT_ATTEMPTED,
|
|
686
|
+
)
|
|
687
|
+
output, truncated = _join_process_output(
|
|
688
|
+
completed.stdout, completed.stderr, max_bytes
|
|
689
|
+
)
|
|
690
|
+
if completed.returncode == 0:
|
|
691
|
+
return _success(
|
|
692
|
+
call,
|
|
693
|
+
{
|
|
694
|
+
"status": "success",
|
|
695
|
+
"summary": f"shell profile completed successfully: {output}",
|
|
696
|
+
"exit_code": completed.returncode,
|
|
697
|
+
"artifact_refs": [],
|
|
698
|
+
"truncated": truncated,
|
|
699
|
+
},
|
|
700
|
+
)
|
|
701
|
+
return _process_failure(
|
|
702
|
+
call,
|
|
703
|
+
ToolExecutionStatus.SOFT_FAILURE,
|
|
704
|
+
ToolExecutionErrorCode.POLICY_DENIED,
|
|
705
|
+
f"shell profile failed with exit code {completed.returncode}: {output}",
|
|
706
|
+
completed.returncode,
|
|
707
|
+
truncated,
|
|
708
|
+
SideEffectCertainty.CONFIRMED_COMPLETE,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _terminal_intent(
|
|
713
|
+
call: ValidatedToolCall,
|
|
714
|
+
context: ToolExecutionContext,
|
|
715
|
+
) -> ToolExecutionResult:
|
|
716
|
+
terminal_result = str(call.arguments["terminal_result"])
|
|
717
|
+
try:
|
|
718
|
+
missing = _missing_required_terminal_artifacts(call, context, terminal_result)
|
|
719
|
+
if missing:
|
|
720
|
+
return _denied(
|
|
721
|
+
call,
|
|
722
|
+
ToolExecutionErrorCode.TERMINAL_INTENT_INVALID,
|
|
723
|
+
"terminal intent is missing required terminal artifacts",
|
|
724
|
+
)
|
|
725
|
+
summary = str(call.arguments["summary"])
|
|
726
|
+
artifact_refs = _terminal_artifact_refs(call, context)
|
|
727
|
+
TerminalIntent(
|
|
728
|
+
request_id=context.request_id,
|
|
729
|
+
run_id=context.run_id,
|
|
730
|
+
stage=context.stage,
|
|
731
|
+
terminal_node_id=call.node_id,
|
|
732
|
+
terminal_result=terminal_result,
|
|
733
|
+
disposition=_terminal_disposition(call),
|
|
734
|
+
summary=summary,
|
|
735
|
+
artifact_refs=artifact_refs,
|
|
736
|
+
)
|
|
737
|
+
except (PathPolicyError, ValueError) as exc:
|
|
738
|
+
return _denied(call, ToolExecutionErrorCode.TERMINAL_INTENT_INVALID, str(exc))
|
|
739
|
+
return _success(
|
|
740
|
+
call,
|
|
741
|
+
{
|
|
742
|
+
"status": "success",
|
|
743
|
+
"summary": "terminal intent validated",
|
|
744
|
+
"terminal_result": terminal_result,
|
|
745
|
+
},
|
|
746
|
+
artifact_refs=artifact_refs,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def call_node_produced_ids(call: ValidatedToolCall) -> tuple[str, ...]:
|
|
751
|
+
"""Return produced IDs admitted by descriptor binding for artifact write tools."""
|
|
752
|
+
for descriptor in iter_builtin_tool_descriptors():
|
|
753
|
+
if descriptor.tool_id == call.binding.tool_id:
|
|
754
|
+
return descriptor.produced_artifact_ids
|
|
755
|
+
return ()
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _not_implemented(
|
|
759
|
+
call: ValidatedToolCall,
|
|
760
|
+
_context: ToolExecutionContext,
|
|
761
|
+
) -> ToolExecutionResult:
|
|
762
|
+
return _result(
|
|
763
|
+
call,
|
|
764
|
+
ToolExecutionStatus.SOFT_FAILURE,
|
|
765
|
+
ToolExecutionErrorCode.IMPLEMENTATION_ERROR,
|
|
766
|
+
"built-in runtime implementation is not wired in this process",
|
|
767
|
+
{
|
|
768
|
+
"status": "soft_failure",
|
|
769
|
+
"summary": "built-in runtime implementation is not wired in this process",
|
|
770
|
+
},
|
|
771
|
+
SideEffectCertainty.NOT_ATTEMPTED,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _success(
|
|
776
|
+
call: ValidatedToolCall,
|
|
777
|
+
output: dict[str, Any],
|
|
778
|
+
*,
|
|
779
|
+
artifact_refs: tuple[ArtifactRef, ...] = (),
|
|
780
|
+
) -> ToolExecutionResult:
|
|
781
|
+
return _result(
|
|
782
|
+
call,
|
|
783
|
+
ToolExecutionStatus.SUCCESS,
|
|
784
|
+
None,
|
|
785
|
+
str(output["summary"]),
|
|
786
|
+
output,
|
|
787
|
+
SideEffectCertainty.CONFIRMED_COMPLETE,
|
|
788
|
+
artifact_refs=artifact_refs,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _denied(
|
|
793
|
+
call: ValidatedToolCall,
|
|
794
|
+
code: ToolExecutionErrorCode,
|
|
795
|
+
summary: str,
|
|
796
|
+
) -> ToolExecutionResult:
|
|
797
|
+
output = _failure_output(call, summary)
|
|
798
|
+
return _result(
|
|
799
|
+
call,
|
|
800
|
+
ToolExecutionStatus.NOT_EXECUTED,
|
|
801
|
+
code,
|
|
802
|
+
summary,
|
|
803
|
+
output,
|
|
804
|
+
SideEffectCertainty.NOT_ATTEMPTED,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _result(
|
|
809
|
+
call: ValidatedToolCall,
|
|
810
|
+
status: ToolExecutionStatus,
|
|
811
|
+
code: ToolExecutionErrorCode | None,
|
|
812
|
+
summary: str,
|
|
813
|
+
output: dict[str, Any],
|
|
814
|
+
certainty: SideEffectCertainty,
|
|
815
|
+
*,
|
|
816
|
+
artifact_refs: tuple[ArtifactRef, ...] = (),
|
|
817
|
+
side_effect_record: SideEffectRecord | None = None,
|
|
818
|
+
) -> ToolExecutionResult:
|
|
819
|
+
descriptor = _descriptor_for_call(call)
|
|
820
|
+
return make_tool_result(
|
|
821
|
+
call_id=call.call_id,
|
|
822
|
+
status=status,
|
|
823
|
+
code=code,
|
|
824
|
+
summary=summary,
|
|
825
|
+
structured_data=output,
|
|
826
|
+
side_effect_class=descriptor.side_effect_class,
|
|
827
|
+
idempotency=descriptor.idempotency,
|
|
828
|
+
side_effect_certainty=certainty,
|
|
829
|
+
input_sha256=canonical_sha256(call.arguments),
|
|
830
|
+
artifact_refs=artifact_refs,
|
|
831
|
+
side_effect_record=side_effect_record,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _descriptor_for_call(call: ValidatedToolCall) -> Any:
|
|
836
|
+
for descriptor in iter_builtin_tool_descriptors():
|
|
837
|
+
if descriptor.implementation_id == call.binding.implementation_id:
|
|
838
|
+
return descriptor
|
|
839
|
+
raise KeyError(call.binding.implementation_id)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _failure_output(call: ValidatedToolCall, summary: str) -> dict[str, Any]:
|
|
843
|
+
common = {"status": "soft_failure", "summary": summary}
|
|
844
|
+
tool_id = call.binding.tool_id
|
|
845
|
+
if tool_id == "builtin.request.inspect":
|
|
846
|
+
return {
|
|
847
|
+
**common,
|
|
848
|
+
"request_id": "",
|
|
849
|
+
"stage_id": "",
|
|
850
|
+
"objective": "",
|
|
851
|
+
"artifact_refs": [],
|
|
852
|
+
"truncated": False,
|
|
853
|
+
}
|
|
854
|
+
if tool_id == "builtin.request.read_requirements":
|
|
855
|
+
return {**common, "requirements": [], "artifact_refs": [], "truncated": False}
|
|
856
|
+
if tool_id == "builtin.workspace.list_files":
|
|
857
|
+
return {**common, "paths": [], "truncated": False}
|
|
858
|
+
if tool_id == "builtin.workspace.read_file":
|
|
859
|
+
return {**common, "content": "", "truncated": False, "artifact_refs": []}
|
|
860
|
+
if tool_id == "builtin.workspace.search_text":
|
|
861
|
+
return {**common, "matches": [], "truncated": False}
|
|
862
|
+
if tool_id == "builtin.workspace.write_file":
|
|
863
|
+
return {**common, "path": "", "content_sha256": "0" * 64}
|
|
864
|
+
if tool_id == "builtin.workspace.apply_patch":
|
|
865
|
+
return {**common, "changed_paths": [], "diff_sha256": "0" * 64}
|
|
866
|
+
if tool_id == "builtin.workspace.read_diff":
|
|
867
|
+
return {**common, "diff": "", "truncated": False, "diff_sha256": "0" * 64}
|
|
868
|
+
if tool_id.startswith("builtin.shell."):
|
|
869
|
+
return {**common, "exit_code": -1, "artifact_refs": [], "truncated": False}
|
|
870
|
+
if tool_id == "builtin.artifact.read" or tool_id in _READ_TOOL_ARTIFACT_ID:
|
|
871
|
+
return {
|
|
872
|
+
**common,
|
|
873
|
+
"artifact_id": _failure_artifact_id(call),
|
|
874
|
+
"content": "",
|
|
875
|
+
"content_sha256": "0" * 64,
|
|
876
|
+
"truncated": False,
|
|
877
|
+
}
|
|
878
|
+
if tool_id.startswith("builtin.artifact.write_"):
|
|
879
|
+
artifact_id = str(call.arguments.get("artifact_id") or "")
|
|
880
|
+
if not artifact_id:
|
|
881
|
+
artifact_id = _WRITE_TOOL_CONTENT_FIELD.get(tool_id, ("", ""))[0]
|
|
882
|
+
return {**common, "artifact_id": artifact_id, "content_sha256": "0" * 64}
|
|
883
|
+
if tool_id.startswith("builtin.terminal."):
|
|
884
|
+
return {
|
|
885
|
+
**common,
|
|
886
|
+
"terminal_result": str(call.arguments.get("terminal_result", "")),
|
|
887
|
+
}
|
|
888
|
+
return common
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
class _BuiltinPolicyError:
|
|
892
|
+
def __init__(
|
|
893
|
+
self,
|
|
894
|
+
code: ToolExecutionErrorCode,
|
|
895
|
+
summary: str,
|
|
896
|
+
policy: str,
|
|
897
|
+
) -> None:
|
|
898
|
+
self.code = code
|
|
899
|
+
self.summary = summary
|
|
900
|
+
self.policy = policy
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _policy_error(
|
|
904
|
+
code: ToolExecutionErrorCode,
|
|
905
|
+
summary: str,
|
|
906
|
+
*,
|
|
907
|
+
policy: str = "built-in side-effect policy",
|
|
908
|
+
) -> _BuiltinPolicyError:
|
|
909
|
+
return _BuiltinPolicyError(code, summary, policy)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _policy_error_code(call: ValidatedToolCall) -> ToolExecutionErrorCode:
|
|
913
|
+
return (
|
|
914
|
+
ToolExecutionErrorCode.TERMINAL_INTENT_INVALID
|
|
915
|
+
if call.binding.tool_id.startswith("builtin.terminal.")
|
|
916
|
+
else ToolExecutionErrorCode.POLICY_DENIED
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _builtin_policy_error(
|
|
921
|
+
call: ValidatedToolCall,
|
|
922
|
+
context: ToolExecutionContext,
|
|
923
|
+
) -> _BuiltinPolicyError | None:
|
|
924
|
+
tool_id = call.binding.tool_id
|
|
925
|
+
if tool_id.startswith("builtin.workspace."):
|
|
926
|
+
return _workspace_policy_error(call, context)
|
|
927
|
+
if tool_id.startswith("builtin.artifact."):
|
|
928
|
+
return _artifact_pre_entry_policy_error(call, context)
|
|
929
|
+
if tool_id.startswith("builtin.shell."):
|
|
930
|
+
return _shell_policy_error(call, context)
|
|
931
|
+
if tool_id.startswith("builtin.terminal."):
|
|
932
|
+
return _terminal_policy_error(call, context)
|
|
933
|
+
return None
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _workspace_policy_error(
|
|
937
|
+
call: ValidatedToolCall,
|
|
938
|
+
context: ToolExecutionContext,
|
|
939
|
+
) -> _BuiltinPolicyError | None:
|
|
940
|
+
root = _workspace_root(context)
|
|
941
|
+
if root is None:
|
|
942
|
+
return _policy_error(
|
|
943
|
+
ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
944
|
+
)
|
|
945
|
+
tool_id = call.binding.tool_id
|
|
946
|
+
if tool_id == "builtin.workspace.list_files":
|
|
947
|
+
resolve_existing_contained(root, str(call.arguments["root"]), allow_dot=True)
|
|
948
|
+
elif tool_id == "builtin.workspace.read_file":
|
|
949
|
+
target = resolve_existing_contained(root, str(call.arguments["path"]))
|
|
950
|
+
if not target.is_file():
|
|
951
|
+
return _policy_error(
|
|
952
|
+
ToolExecutionErrorCode.NOT_FOUND, "workspace file not found"
|
|
953
|
+
)
|
|
954
|
+
elif tool_id == "builtin.workspace.search_text":
|
|
955
|
+
resolve_existing_contained(
|
|
956
|
+
root, str(call.arguments.get("root") or "."), allow_dot=True
|
|
957
|
+
)
|
|
958
|
+
elif tool_id == "builtin.workspace.write_file":
|
|
959
|
+
target = resolve_write_contained(root, str(call.arguments["path"]))
|
|
960
|
+
expected = call.arguments.get("expected_sha256")
|
|
961
|
+
if expected is not None and target.exists():
|
|
962
|
+
actual = canonical_sha256_bytes(target.read_bytes())
|
|
963
|
+
if actual != expected:
|
|
964
|
+
return _policy_error(
|
|
965
|
+
ToolExecutionErrorCode.CONFLICT,
|
|
966
|
+
"expected_sha256 does not match current file",
|
|
967
|
+
)
|
|
968
|
+
elif tool_id == "builtin.workspace.apply_patch":
|
|
969
|
+
_changed_paths_from_patch(root, str(call.arguments["patch"]))
|
|
970
|
+
elif tool_id == "builtin.workspace.read_diff":
|
|
971
|
+
if not root.is_dir():
|
|
972
|
+
return _policy_error(
|
|
973
|
+
ToolExecutionErrorCode.POLICY_DENIED,
|
|
974
|
+
"workspace root is unavailable",
|
|
975
|
+
)
|
|
976
|
+
for item in call.arguments.get("paths", []):
|
|
977
|
+
validate_logical_path(str(item))
|
|
978
|
+
return None
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _artifact_pre_entry_policy_error(
|
|
982
|
+
call: ValidatedToolCall,
|
|
983
|
+
context: ToolExecutionContext,
|
|
984
|
+
) -> _BuiltinPolicyError | None:
|
|
985
|
+
root = _artifact_root(context)
|
|
986
|
+
if root is None:
|
|
987
|
+
return _policy_error(
|
|
988
|
+
ToolExecutionErrorCode.POLICY_DENIED, "artifact root is unavailable"
|
|
989
|
+
)
|
|
990
|
+
root.resolve(strict=True)
|
|
991
|
+
if call.binding.tool_id == "builtin.artifact.read" or (
|
|
992
|
+
call.binding.tool_id in _READ_TOOL_ARTIFACT_ID
|
|
993
|
+
):
|
|
994
|
+
artifact_id = _read_artifact_id(call)
|
|
995
|
+
error = _artifact_policy_error(context, artifact_id, produced_ids=None)
|
|
996
|
+
if error is not None:
|
|
997
|
+
return _policy_error(ToolExecutionErrorCode.POLICY_DENIED, error)
|
|
998
|
+
resolve_existing_contained(root, _artifact_logical_path(artifact_id))
|
|
999
|
+
return None
|
|
1000
|
+
if call.binding.tool_id == "builtin.artifact.write_verdict":
|
|
1001
|
+
artifact_id = str(call.arguments["artifact_id"])
|
|
1002
|
+
elif call.binding.tool_id == "builtin.artifact.write_workspace_diff":
|
|
1003
|
+
artifact_id = "workspace_diff"
|
|
1004
|
+
diff_error = _workspace_policy_error(_workspace_read_diff_call(call), context)
|
|
1005
|
+
if diff_error is not None:
|
|
1006
|
+
return diff_error
|
|
1007
|
+
else:
|
|
1008
|
+
artifact_id = _WRITE_TOOL_CONTENT_FIELD[call.binding.tool_id][0]
|
|
1009
|
+
error = _artifact_policy_error(
|
|
1010
|
+
context, artifact_id, produced_ids=call_node_produced_ids(call)
|
|
1011
|
+
)
|
|
1012
|
+
if error is not None:
|
|
1013
|
+
return _policy_error(ToolExecutionErrorCode.POLICY_DENIED, error)
|
|
1014
|
+
resolve_write_contained(root, _artifact_logical_path(artifact_id))
|
|
1015
|
+
return None
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def _shell_policy_error(
|
|
1019
|
+
call: ValidatedToolCall,
|
|
1020
|
+
context: ToolExecutionContext,
|
|
1021
|
+
) -> _BuiltinPolicyError | None:
|
|
1022
|
+
root = _workspace_root(context)
|
|
1023
|
+
if root is None:
|
|
1024
|
+
return _policy_error(
|
|
1025
|
+
ToolExecutionErrorCode.POLICY_DENIED, "workspace root is unavailable"
|
|
1026
|
+
)
|
|
1027
|
+
if not root.is_dir():
|
|
1028
|
+
return _policy_error(
|
|
1029
|
+
ToolExecutionErrorCode.POLICY_DENIED,
|
|
1030
|
+
"workspace root is not a directory",
|
|
1031
|
+
)
|
|
1032
|
+
selector = call.arguments.get("selector")
|
|
1033
|
+
if selector not in _SHELL_ALLOWED_SELECTORS:
|
|
1034
|
+
return _policy_error(
|
|
1035
|
+
ToolExecutionErrorCode.INVALID_ARGUMENTS,
|
|
1036
|
+
"selector is not an approved shell profile selector",
|
|
1037
|
+
)
|
|
1038
|
+
profile = str(call.arguments["profile"])
|
|
1039
|
+
if _shell_profile_command(call.binding.tool_id, profile) is None:
|
|
1040
|
+
return _policy_error(
|
|
1041
|
+
ToolExecutionErrorCode.INVALID_ARGUMENTS,
|
|
1042
|
+
"profile is not an approved shell profile",
|
|
1043
|
+
)
|
|
1044
|
+
timeout_seconds = min(
|
|
1045
|
+
context.timeout.timeout_seconds,
|
|
1046
|
+
_descriptor_for_call(call).timeout_policy.timeout_seconds,
|
|
1047
|
+
_SHELL_MAX_TIMEOUT_SECONDS,
|
|
1048
|
+
)
|
|
1049
|
+
if timeout_seconds <= 0:
|
|
1050
|
+
return _policy_error(
|
|
1051
|
+
ToolExecutionErrorCode.TIMEOUT, "shell timeout policy is exhausted"
|
|
1052
|
+
)
|
|
1053
|
+
return None
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _terminal_policy_error(
|
|
1057
|
+
call: ValidatedToolCall,
|
|
1058
|
+
context: ToolExecutionContext,
|
|
1059
|
+
) -> _BuiltinPolicyError | None:
|
|
1060
|
+
terminal_result = str(call.arguments["terminal_result"])
|
|
1061
|
+
missing = _missing_required_terminal_artifacts(call, context, terminal_result)
|
|
1062
|
+
if missing:
|
|
1063
|
+
return _policy_error(
|
|
1064
|
+
ToolExecutionErrorCode.TERMINAL_INTENT_INVALID,
|
|
1065
|
+
"terminal intent is missing required terminal artifacts",
|
|
1066
|
+
policy="terminal artifact policy",
|
|
1067
|
+
)
|
|
1068
|
+
_terminal_artifact_refs(call, context)
|
|
1069
|
+
TerminalIntent(
|
|
1070
|
+
request_id=context.request_id,
|
|
1071
|
+
run_id=context.run_id,
|
|
1072
|
+
stage=context.stage,
|
|
1073
|
+
terminal_node_id=call.node_id,
|
|
1074
|
+
terminal_result=terminal_result,
|
|
1075
|
+
disposition=_terminal_disposition(call),
|
|
1076
|
+
summary=str(call.arguments["summary"]),
|
|
1077
|
+
artifact_refs=_terminal_artifact_refs(call, context),
|
|
1078
|
+
)
|
|
1079
|
+
return None
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _shell_profile_command(tool_id: str, profile: str) -> tuple[str, ...] | None:
|
|
1083
|
+
profiles = (
|
|
1084
|
+
_SHELL_TEST_PROFILES
|
|
1085
|
+
if tool_id == "builtin.shell.run_tests"
|
|
1086
|
+
else _SHELL_STATIC_CHECK_PROFILES
|
|
1087
|
+
)
|
|
1088
|
+
return profiles.get(profile)
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _shell_environment() -> dict[str, str]:
|
|
1092
|
+
env = {
|
|
1093
|
+
key: value
|
|
1094
|
+
for key in _SHELL_ENV_ALLOWLIST
|
|
1095
|
+
if (value := os.environ.get(key)) is not None
|
|
1096
|
+
}
|
|
1097
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
1098
|
+
env["NO_COLOR"] = "1"
|
|
1099
|
+
env.setdefault("LC_ALL", "C.UTF-8")
|
|
1100
|
+
return env
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _join_process_output(stdout: Any, stderr: Any, max_bytes: int) -> tuple[str, bool]:
|
|
1104
|
+
out = stdout or b""
|
|
1105
|
+
err = stderr or b""
|
|
1106
|
+
if isinstance(out, str):
|
|
1107
|
+
out = out.encode("utf-8", errors="replace")
|
|
1108
|
+
if isinstance(err, str):
|
|
1109
|
+
err = err.encode("utf-8", errors="replace")
|
|
1110
|
+
data = out + (b"\n" if out and err else b"") + err
|
|
1111
|
+
content, truncated = _decode_bounded(data, max_bytes)
|
|
1112
|
+
return content.strip() or "[no output]", truncated
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _process_failure(
|
|
1116
|
+
call: ValidatedToolCall,
|
|
1117
|
+
status: ToolExecutionStatus,
|
|
1118
|
+
code: ToolExecutionErrorCode,
|
|
1119
|
+
summary: str,
|
|
1120
|
+
exit_code: int,
|
|
1121
|
+
truncated: bool,
|
|
1122
|
+
certainty: SideEffectCertainty,
|
|
1123
|
+
) -> ToolExecutionResult:
|
|
1124
|
+
side_effect_record = None
|
|
1125
|
+
if certainty is SideEffectCertainty.COMPLETION_UNKNOWN:
|
|
1126
|
+
side_effect_record = SideEffectRecord(
|
|
1127
|
+
certainty=certainty,
|
|
1128
|
+
detail_code=ToolExecutionErrorCode.AMBIGUOUS_SIDE_EFFECT.value,
|
|
1129
|
+
summary="process completion could not be proven after timeout",
|
|
1130
|
+
retry_allowed=False,
|
|
1131
|
+
)
|
|
1132
|
+
return _result(
|
|
1133
|
+
call,
|
|
1134
|
+
status,
|
|
1135
|
+
code,
|
|
1136
|
+
summary,
|
|
1137
|
+
{
|
|
1138
|
+
"status": "soft_failure",
|
|
1139
|
+
"summary": summary,
|
|
1140
|
+
"exit_code": exit_code,
|
|
1141
|
+
"artifact_refs": [],
|
|
1142
|
+
"truncated": truncated,
|
|
1143
|
+
},
|
|
1144
|
+
certainty,
|
|
1145
|
+
side_effect_record=side_effect_record,
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def _terminal_disposition(
|
|
1150
|
+
call: ValidatedToolCall,
|
|
1151
|
+
) -> Literal["success", "blocked", "rejected", "escalated"]:
|
|
1152
|
+
if call.binding.tool_id == "builtin.terminal.reject":
|
|
1153
|
+
return "rejected"
|
|
1154
|
+
if call.binding.tool_id == "builtin.terminal.escalate":
|
|
1155
|
+
return "escalated"
|
|
1156
|
+
terminal_result = str(call.arguments["terminal_result"])
|
|
1157
|
+
return (
|
|
1158
|
+
"blocked"
|
|
1159
|
+
if terminal_result.endswith("_BLOCKED") or terminal_result == "BLOCKED"
|
|
1160
|
+
else "success"
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def _missing_required_terminal_artifacts(
|
|
1165
|
+
call: ValidatedToolCall,
|
|
1166
|
+
context: ToolExecutionContext,
|
|
1167
|
+
terminal_result: str,
|
|
1168
|
+
) -> list[str]:
|
|
1169
|
+
policy = context.compiled_artifact_policy
|
|
1170
|
+
if policy is None:
|
|
1171
|
+
return []
|
|
1172
|
+
provided = set(_terminal_artifact_ids(call, context))
|
|
1173
|
+
required = {
|
|
1174
|
+
artifact_id
|
|
1175
|
+
for requirement in policy.required_by_terminal
|
|
1176
|
+
if requirement.terminal_result == terminal_result
|
|
1177
|
+
for artifact_id in requirement.artifact_ids
|
|
1178
|
+
}
|
|
1179
|
+
return sorted(required - provided)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def _terminal_artifact_ids(
|
|
1183
|
+
call: ValidatedToolCall,
|
|
1184
|
+
context: ToolExecutionContext,
|
|
1185
|
+
) -> tuple[str, ...]:
|
|
1186
|
+
explicit = call.arguments.get("artifact_refs")
|
|
1187
|
+
if explicit is None:
|
|
1188
|
+
return tuple(ref.artifact_id for ref in context.input_artifacts)
|
|
1189
|
+
refs_by_id = {ref.artifact_id: ref for ref in context.input_artifacts}
|
|
1190
|
+
ids: list[str] = []
|
|
1191
|
+
for item in explicit:
|
|
1192
|
+
artifact_id = str(item)
|
|
1193
|
+
if artifact_id not in refs_by_id:
|
|
1194
|
+
raise ValueError(
|
|
1195
|
+
f"terminal artifact_ref {artifact_id!r} is not provided by the runtime"
|
|
1196
|
+
)
|
|
1197
|
+
ids.append(artifact_id)
|
|
1198
|
+
return tuple(ids)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _terminal_artifact_refs(
|
|
1202
|
+
call: ValidatedToolCall,
|
|
1203
|
+
context: ToolExecutionContext,
|
|
1204
|
+
) -> tuple[ArtifactRef, ...]:
|
|
1205
|
+
ids = _terminal_artifact_ids(call, context)
|
|
1206
|
+
refs_by_id = {ref.artifact_id: ref for ref in context.input_artifacts}
|
|
1207
|
+
return tuple(refs_by_id[artifact_id] for artifact_id in ids)
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def _workspace_root(context: ToolExecutionContext) -> Path | None:
|
|
1211
|
+
return None if context.workspace_root is None else Path(context.workspace_root)
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _artifact_root(context: ToolExecutionContext) -> Path | None:
|
|
1215
|
+
return None if context.artifact_root is None else Path(context.artifact_root)
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def _artifact_policy_error(
|
|
1219
|
+
context: ToolExecutionContext,
|
|
1220
|
+
artifact_id: str,
|
|
1221
|
+
*,
|
|
1222
|
+
produced_ids: tuple[str, ...] | None,
|
|
1223
|
+
) -> str | None:
|
|
1224
|
+
policy = context.compiled_artifact_policy
|
|
1225
|
+
if policy is None:
|
|
1226
|
+
return "compiled artifact policy is unavailable"
|
|
1227
|
+
if artifact_id not in policy.declared_artifact_ids:
|
|
1228
|
+
return "artifact_id is not declared by the compiled artifact policy"
|
|
1229
|
+
if produced_ids is not None and artifact_id not in produced_ids:
|
|
1230
|
+
return "artifact_id is not produced by this compiled tool binding"
|
|
1231
|
+
if artifact_id not in _ARTIFACT_FILENAMES:
|
|
1232
|
+
return "artifact_id has no runtime-managed artifact path"
|
|
1233
|
+
return None
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def _read_artifact_id(call: ValidatedToolCall) -> str:
|
|
1237
|
+
artifact_id = _READ_TOOL_ARTIFACT_ID.get(call.binding.tool_id)
|
|
1238
|
+
if artifact_id is not None:
|
|
1239
|
+
return artifact_id
|
|
1240
|
+
return str(call.arguments["artifact_id"])
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def _failure_artifact_id(call: ValidatedToolCall) -> str:
|
|
1244
|
+
artifact_id = _READ_TOOL_ARTIFACT_ID.get(call.binding.tool_id)
|
|
1245
|
+
if artifact_id is not None:
|
|
1246
|
+
return artifact_id
|
|
1247
|
+
return str(call.arguments.get("artifact_id", ""))
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def _workspace_read_diff_call(call: ValidatedToolCall) -> ValidatedToolCall:
|
|
1251
|
+
descriptor = _descriptor_for_tool_id("builtin.workspace.read_diff")
|
|
1252
|
+
binding = call.binding.model_copy(
|
|
1253
|
+
update={
|
|
1254
|
+
"tool_id": descriptor.tool_id,
|
|
1255
|
+
"tool_version": descriptor.tool_version,
|
|
1256
|
+
"descriptor_sha256": descriptor.descriptor_sha256,
|
|
1257
|
+
"implementation_id": descriptor.implementation_id,
|
|
1258
|
+
}
|
|
1259
|
+
)
|
|
1260
|
+
return call.model_copy(update={"binding": binding, "arguments": {}})
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _artifact_logical_path(artifact_id: str) -> str:
|
|
1264
|
+
try:
|
|
1265
|
+
return _ARTIFACT_FILENAMES[artifact_id]
|
|
1266
|
+
except KeyError as exc:
|
|
1267
|
+
raise PathPolicyError(
|
|
1268
|
+
"artifact_id has no runtime-managed artifact path"
|
|
1269
|
+
) from exc
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def _artifact_ref(artifact_id: str) -> ArtifactRef:
|
|
1273
|
+
return ArtifactRef(
|
|
1274
|
+
artifact_id=artifact_id,
|
|
1275
|
+
path=Path("millforge") / _artifact_logical_path(artifact_id),
|
|
1276
|
+
content_type=_ARTIFACT_CONTENT_TYPE,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def _descriptor_for_tool_id(tool_id: str) -> Any:
|
|
1281
|
+
for descriptor in iter_builtin_tool_descriptors():
|
|
1282
|
+
if descriptor.tool_id == tool_id:
|
|
1283
|
+
return descriptor
|
|
1284
|
+
raise KeyError(tool_id)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def _bounded_int(value: Any, default: int, maximum: int) -> int:
|
|
1288
|
+
if value is None:
|
|
1289
|
+
return default
|
|
1290
|
+
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
|
1291
|
+
return default
|
|
1292
|
+
return min(value, maximum)
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _decode_bounded(data: bytes, max_bytes: int) -> tuple[str, bool]:
|
|
1296
|
+
truncated = len(data) > max_bytes
|
|
1297
|
+
if truncated:
|
|
1298
|
+
data = data[:max_bytes]
|
|
1299
|
+
content = data.decode("utf-8", errors="replace")
|
|
1300
|
+
if truncated:
|
|
1301
|
+
content += "[truncated]"
|
|
1302
|
+
return content, truncated
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _changed_paths_from_patch(root: Path, patch: str) -> list[str]:
|
|
1306
|
+
changed: set[str] = set()
|
|
1307
|
+
for line in patch.splitlines():
|
|
1308
|
+
if not (
|
|
1309
|
+
line.startswith("--- ")
|
|
1310
|
+
or line.startswith("+++ ")
|
|
1311
|
+
or line.startswith("rename from ")
|
|
1312
|
+
or line.startswith("rename to ")
|
|
1313
|
+
):
|
|
1314
|
+
continue
|
|
1315
|
+
raw = line.split(maxsplit=1)[1].strip()
|
|
1316
|
+
if raw == "/dev/null":
|
|
1317
|
+
continue
|
|
1318
|
+
if raw.startswith(("a/", "b/")):
|
|
1319
|
+
raw = raw[2:]
|
|
1320
|
+
logical = validate_logical_path(raw).as_posix()
|
|
1321
|
+
resolve_write_contained(root, logical)
|
|
1322
|
+
changed.add(logical)
|
|
1323
|
+
if not changed:
|
|
1324
|
+
raise ValueError("patch does not declare any changed paths")
|
|
1325
|
+
return sorted(changed)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def _git_diff(root: Path, paths: list[str], max_bytes: int) -> str:
|
|
1329
|
+
command = ["git", "diff", "--"]
|
|
1330
|
+
command.extend(paths)
|
|
1331
|
+
completed = subprocess.run(
|
|
1332
|
+
command,
|
|
1333
|
+
cwd=root,
|
|
1334
|
+
stdout=subprocess.PIPE,
|
|
1335
|
+
stderr=subprocess.PIPE,
|
|
1336
|
+
check=False,
|
|
1337
|
+
)
|
|
1338
|
+
data = completed.stdout[: max_bytes + 1]
|
|
1339
|
+
return data.decode("utf-8", errors="replace")
|