agentic-runner 2.6.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.
- agentic_runner/__init__.py +12 -0
- agentic_runner/activities.py +4918 -0
- agentic_runner/callback.py +342 -0
- agentic_runner/child_watcher.py +66 -0
- agentic_runner/cli.py +416 -0
- agentic_runner/config.py +105 -0
- agentic_runner/credentials.py +252 -0
- agentic_runner/device_login_activities.py +79 -0
- agentic_runner/egress.py +243 -0
- agentic_runner/heartbeat_link.py +249 -0
- agentic_runner/hooks.py +455 -0
- agentic_runner/host_store.py +295 -0
- agentic_runner/integrations/__init__.py +0 -0
- agentic_runner/integrations/git/__init__.py +1 -0
- agentic_runner/integrations/git/contracts.py +198 -0
- agentic_runner/integrations/git/evidence.py +442 -0
- agentic_runner/integrations/git/fake_workspace.py +339 -0
- agentic_runner/integrations/git/workspace.py +921 -0
- agentic_runner/integrations/github/__init__.py +53 -0
- agentic_runner/integrations/github/auth.py +171 -0
- agentic_runner/integrations/github/fake_client.py +494 -0
- agentic_runner/integrations/github/gh_client.py +944 -0
- agentic_runner/lifecycle.py +48 -0
- agentic_runner/llm_proxy.py +937 -0
- agentic_runner/mcp.py +342 -0
- agentic_runner/message_store.py +341 -0
- agentic_runner/py.typed +0 -0
- agentic_runner/recipient_key_secret.py +134 -0
- agentic_runner/registration.py +363 -0
- agentic_runner/runtime/__init__.py +0 -0
- agentic_runner/runtime/verifier_command.py +344 -0
- agentic_runner/sealed_box.py +509 -0
- agentic_runner/service.py +1068 -0
- agentic_runner/tiny_http.py +133 -0
- agentic_runner/triage_activities.py +113 -0
- agentic_runner/user_sources.py +546 -0
- agentic_runner/workers/__init__.py +1 -0
- agentic_runner/workers/_runtime_support.py +388 -0
- agentic_runner/workers/agent_runtime.py +93 -0
- agentic_runner/workers/claude_runtime.py +226 -0
- agentic_runner/workers/codex_runtime.py +311 -0
- agentic_runner/workers/command_policy.py +250 -0
- agentic_runner/workers/contract_device_login.py +211 -0
- agentic_runner/workers/contract_isolation.py +500 -0
- agentic_runner/workers/fastapi_client.py +396 -0
- agentic_runner/workers/harness_usage.py +65 -0
- agentic_runner/workers/mcp_config.py +111 -0
- agentic_runner/workers/settings.py +314 -0
- agentic_runner/workstation.py +687 -0
- agentic_runner-2.6.0.dist-info/METADATA +49 -0
- agentic_runner-2.6.0.dist-info/RECORD +54 -0
- agentic_runner-2.6.0.dist-info/WHEEL +4 -0
- agentic_runner-2.6.0.dist-info/entry_points.txt +2 -0
- agentic_runner-2.6.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import selectors
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
from agentic_runner.integrations.git.workspace import (
|
|
12
|
+
require_runner_owned_git_dir,
|
|
13
|
+
resolve_workspace_path,
|
|
14
|
+
)
|
|
15
|
+
from agentic_runner_contracts.redaction import redact_secret_like_text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class GitEvidence:
|
|
20
|
+
"""Bounded, redacted git status and diff evidence for worker handoff."""
|
|
21
|
+
|
|
22
|
+
workspace_path: Path
|
|
23
|
+
status: str
|
|
24
|
+
diff: str
|
|
25
|
+
stderr: str
|
|
26
|
+
status_returncode: int
|
|
27
|
+
diff_returncode: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_MAX_UNTRACKED_FILE_BYTES: Final = 65_536
|
|
31
|
+
_PIPE_READ_CHUNK_BYTES: Final = 8192
|
|
32
|
+
_TRUNCATION_MARKER = "\n[TRUNCATED]\n"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def collect_git_evidence(
|
|
36
|
+
*,
|
|
37
|
+
workspace_path: Path,
|
|
38
|
+
workspace_root: Path,
|
|
39
|
+
output_limit_bytes: int,
|
|
40
|
+
command_timeout_seconds: int = 10,
|
|
41
|
+
) -> GitEvidence:
|
|
42
|
+
"""Collect bounded, redacted status and diff from a workspace under its root."""
|
|
43
|
+
|
|
44
|
+
if output_limit_bytes <= 0:
|
|
45
|
+
raise ValueError("output_limit_bytes must be positive")
|
|
46
|
+
if command_timeout_seconds <= 0:
|
|
47
|
+
raise ValueError("command_timeout_seconds must be positive")
|
|
48
|
+
|
|
49
|
+
resolved_workspace = resolve_workspace_path(workspace_root, workspace_path)
|
|
50
|
+
# The other seam that runs the Runner's git over a tree the Contract owns, with the
|
|
51
|
+
# same `safe.directory` suppression — `status`/`diff` honour `core.fsmonitor` and
|
|
52
|
+
# `filter.*.clean` just as `commit` does.
|
|
53
|
+
require_runner_owned_git_dir(resolved_workspace)
|
|
54
|
+
status = _run_git_evidence_command(
|
|
55
|
+
["status", "--short", "--untracked-files=all", "--porcelain=v1"],
|
|
56
|
+
cwd=resolved_workspace,
|
|
57
|
+
output_limit_bytes=output_limit_bytes,
|
|
58
|
+
command_timeout_seconds=command_timeout_seconds,
|
|
59
|
+
)
|
|
60
|
+
diff = _run_git_evidence_command(
|
|
61
|
+
[
|
|
62
|
+
"diff",
|
|
63
|
+
"--no-ext-diff",
|
|
64
|
+
"--no-textconv",
|
|
65
|
+
"--no-color",
|
|
66
|
+
"--src-prefix=a/",
|
|
67
|
+
"--dst-prefix=b/",
|
|
68
|
+
"--",
|
|
69
|
+
],
|
|
70
|
+
cwd=resolved_workspace,
|
|
71
|
+
output_limit_bytes=output_limit_bytes,
|
|
72
|
+
command_timeout_seconds=command_timeout_seconds,
|
|
73
|
+
)
|
|
74
|
+
staged_diff = _run_git_evidence_command(
|
|
75
|
+
[
|
|
76
|
+
"diff",
|
|
77
|
+
"--cached",
|
|
78
|
+
"--no-ext-diff",
|
|
79
|
+
"--no-textconv",
|
|
80
|
+
"--no-color",
|
|
81
|
+
"--src-prefix=a/",
|
|
82
|
+
"--dst-prefix=b/",
|
|
83
|
+
"--",
|
|
84
|
+
],
|
|
85
|
+
cwd=resolved_workspace,
|
|
86
|
+
output_limit_bytes=output_limit_bytes,
|
|
87
|
+
command_timeout_seconds=command_timeout_seconds,
|
|
88
|
+
)
|
|
89
|
+
untracked_files = _run_git_evidence_command(
|
|
90
|
+
["ls-files", "--others", "--exclude-standard", "-z"],
|
|
91
|
+
cwd=resolved_workspace,
|
|
92
|
+
output_limit_bytes=output_limit_bytes,
|
|
93
|
+
command_timeout_seconds=command_timeout_seconds,
|
|
94
|
+
)
|
|
95
|
+
untracked_diff = _collect_untracked_text_evidence(
|
|
96
|
+
workspace_path=resolved_workspace,
|
|
97
|
+
git_paths=untracked_files.stdout,
|
|
98
|
+
output_limit_bytes=output_limit_bytes,
|
|
99
|
+
)
|
|
100
|
+
combined_diff = "".join((diff.stdout, staged_diff.stdout, untracked_diff))
|
|
101
|
+
return build_git_evidence(
|
|
102
|
+
workspace_path=resolved_workspace,
|
|
103
|
+
status=status.stdout,
|
|
104
|
+
diff=combined_diff,
|
|
105
|
+
stderr="\n".join(
|
|
106
|
+
part
|
|
107
|
+
for part in (status.stderr, diff.stderr, staged_diff.stderr, untracked_files.stderr)
|
|
108
|
+
if part
|
|
109
|
+
),
|
|
110
|
+
status_returncode=status.returncode,
|
|
111
|
+
diff_returncode=_first_nonzero(
|
|
112
|
+
diff.returncode,
|
|
113
|
+
staged_diff.returncode,
|
|
114
|
+
untracked_files.returncode,
|
|
115
|
+
),
|
|
116
|
+
output_limit_bytes=output_limit_bytes,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_git_evidence(
|
|
121
|
+
*,
|
|
122
|
+
workspace_path: Path,
|
|
123
|
+
status: str,
|
|
124
|
+
diff: str,
|
|
125
|
+
stderr: str = "",
|
|
126
|
+
status_returncode: int = 0,
|
|
127
|
+
diff_returncode: int = 0,
|
|
128
|
+
output_limit_bytes: int,
|
|
129
|
+
) -> GitEvidence:
|
|
130
|
+
"""Build evidence from deterministic text, applying the same redaction and limits."""
|
|
131
|
+
|
|
132
|
+
if output_limit_bytes <= 0:
|
|
133
|
+
raise ValueError("output_limit_bytes must be positive")
|
|
134
|
+
return GitEvidence(
|
|
135
|
+
workspace_path=workspace_path.resolve(),
|
|
136
|
+
**_limit_evidence_fields_to_total_budget(
|
|
137
|
+
status=redact_git_evidence_text(status),
|
|
138
|
+
diff=redact_git_evidence_text(diff),
|
|
139
|
+
stderr=redact_git_evidence_text(stderr),
|
|
140
|
+
output_limit_bytes=output_limit_bytes,
|
|
141
|
+
),
|
|
142
|
+
status_returncode=status_returncode,
|
|
143
|
+
diff_returncode=diff_returncode,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def redact_git_evidence_text(text: str) -> str:
|
|
148
|
+
"""Redact common token, credentialed URL, and private-key forms from evidence text."""
|
|
149
|
+
|
|
150
|
+
return redact_secret_like_text(text)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class _EvidenceCommandResult:
|
|
155
|
+
stdout: str
|
|
156
|
+
stderr: str
|
|
157
|
+
returncode: int
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _run_git_evidence_command(
|
|
161
|
+
args: list[str],
|
|
162
|
+
*,
|
|
163
|
+
cwd: Path,
|
|
164
|
+
output_limit_bytes: int,
|
|
165
|
+
command_timeout_seconds: int,
|
|
166
|
+
) -> _EvidenceCommandResult:
|
|
167
|
+
process = subprocess.Popen(
|
|
168
|
+
[
|
|
169
|
+
"git",
|
|
170
|
+
"-c",
|
|
171
|
+
"credential.helper=",
|
|
172
|
+
"-c",
|
|
173
|
+
"core.fsmonitor=false",
|
|
174
|
+
"-c",
|
|
175
|
+
f"core.hooksPath={os.devnull}",
|
|
176
|
+
# The same two the workspace adapter carries, for the same reason: this runs
|
|
177
|
+
# as the Runner over a worktree owned by the Contract's uid (ADR-0015 §1).
|
|
178
|
+
# Without `safe.directory` git refuses it outright with "dubious ownership";
|
|
179
|
+
# without `diff.ignoreSubmodules` a repository the Directive `git init`-ed
|
|
180
|
+
# inside its checkout would be descended into, and its config — which the
|
|
181
|
+
# Contract owns — read by the Runner. See integrations/git/workspace._run_git.
|
|
182
|
+
"-c",
|
|
183
|
+
f"safe.directory={cwd}",
|
|
184
|
+
"-c",
|
|
185
|
+
"diff.ignoreSubmodules=dirty",
|
|
186
|
+
*args,
|
|
187
|
+
],
|
|
188
|
+
cwd=cwd,
|
|
189
|
+
env=_git_evidence_env(),
|
|
190
|
+
shell=False,
|
|
191
|
+
stdout=subprocess.PIPE,
|
|
192
|
+
stderr=subprocess.PIPE,
|
|
193
|
+
)
|
|
194
|
+
stdout, stderr = _read_process_output_with_limit(
|
|
195
|
+
process,
|
|
196
|
+
output_limit_bytes=output_limit_bytes,
|
|
197
|
+
command_timeout_seconds=command_timeout_seconds,
|
|
198
|
+
)
|
|
199
|
+
return _EvidenceCommandResult(
|
|
200
|
+
stdout=stdout,
|
|
201
|
+
stderr=stderr,
|
|
202
|
+
returncode=process.returncode if process.returncode is not None else -1,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _read_process_output_with_limit(
|
|
207
|
+
process: subprocess.Popen[bytes],
|
|
208
|
+
*,
|
|
209
|
+
output_limit_bytes: int,
|
|
210
|
+
command_timeout_seconds: int,
|
|
211
|
+
) -> tuple[str, str]:
|
|
212
|
+
stdout_buffer = bytearray()
|
|
213
|
+
stderr_buffer = bytearray()
|
|
214
|
+
stdout_truncated = False
|
|
215
|
+
stderr_truncated = False
|
|
216
|
+
deadline = time.monotonic() + command_timeout_seconds
|
|
217
|
+
selector = selectors.DefaultSelector()
|
|
218
|
+
if process.stdout is not None:
|
|
219
|
+
selector.register(process.stdout, selectors.EVENT_READ, "stdout")
|
|
220
|
+
if process.stderr is not None:
|
|
221
|
+
selector.register(process.stderr, selectors.EVENT_READ, "stderr")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
while selector.get_map():
|
|
225
|
+
remaining_seconds = deadline - time.monotonic()
|
|
226
|
+
if remaining_seconds <= 0:
|
|
227
|
+
process.kill()
|
|
228
|
+
break
|
|
229
|
+
for key, _events in selector.select(timeout=remaining_seconds):
|
|
230
|
+
stream = key.fileobj
|
|
231
|
+
data = os.read(key.fd, _PIPE_READ_CHUNK_BYTES)
|
|
232
|
+
if not data:
|
|
233
|
+
selector.unregister(stream)
|
|
234
|
+
continue
|
|
235
|
+
if key.data == "stdout":
|
|
236
|
+
stdout_truncated = (
|
|
237
|
+
_append_bytes_with_limit(
|
|
238
|
+
stdout_buffer,
|
|
239
|
+
data,
|
|
240
|
+
output_limit_bytes,
|
|
241
|
+
)
|
|
242
|
+
or stdout_truncated
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
stderr_truncated = (
|
|
246
|
+
_append_bytes_with_limit(
|
|
247
|
+
stderr_buffer,
|
|
248
|
+
data,
|
|
249
|
+
output_limit_bytes,
|
|
250
|
+
)
|
|
251
|
+
or stderr_truncated
|
|
252
|
+
)
|
|
253
|
+
finally:
|
|
254
|
+
selector.close()
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
process.wait(timeout=0)
|
|
258
|
+
except subprocess.TimeoutExpired:
|
|
259
|
+
process.kill()
|
|
260
|
+
process.wait()
|
|
261
|
+
|
|
262
|
+
stdout = stdout_buffer.decode(errors="replace")
|
|
263
|
+
stderr = stderr_buffer.decode(errors="replace")
|
|
264
|
+
if stdout_truncated:
|
|
265
|
+
stdout = _append_truncation_marker(stdout, output_limit_bytes)
|
|
266
|
+
if stderr_truncated:
|
|
267
|
+
stderr = _append_truncation_marker(stderr, output_limit_bytes)
|
|
268
|
+
return stdout, stderr
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _append_bytes_with_limit(buffer: bytearray, data: bytes, limit_bytes: int) -> bool:
|
|
272
|
+
remaining_bytes = limit_bytes - len(buffer)
|
|
273
|
+
if remaining_bytes <= 0:
|
|
274
|
+
return True
|
|
275
|
+
buffer.extend(data[:remaining_bytes])
|
|
276
|
+
return len(data) > remaining_bytes
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _collect_untracked_text_evidence(
|
|
280
|
+
*,
|
|
281
|
+
workspace_path: Path,
|
|
282
|
+
git_paths: str,
|
|
283
|
+
output_limit_bytes: int,
|
|
284
|
+
) -> str:
|
|
285
|
+
evidence = ""
|
|
286
|
+
for git_path in (path for path in git_paths.split("\0") if path):
|
|
287
|
+
section = _build_untracked_file_section(
|
|
288
|
+
workspace_path=workspace_path,
|
|
289
|
+
git_path=git_path,
|
|
290
|
+
output_limit_bytes=output_limit_bytes,
|
|
291
|
+
)
|
|
292
|
+
if section:
|
|
293
|
+
evidence = _append_text_with_limit(evidence, section, output_limit_bytes)
|
|
294
|
+
if len(evidence.encode()) >= output_limit_bytes:
|
|
295
|
+
break
|
|
296
|
+
return evidence
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _build_untracked_file_section(
|
|
300
|
+
*,
|
|
301
|
+
workspace_path: Path,
|
|
302
|
+
git_path: str,
|
|
303
|
+
output_limit_bytes: int,
|
|
304
|
+
) -> str:
|
|
305
|
+
relative_path = Path(git_path)
|
|
306
|
+
rendered_git_path = _render_synthetic_git_path(git_path)
|
|
307
|
+
if relative_path.is_absolute() or ".." in relative_path.parts:
|
|
308
|
+
return _synthetic_untracked_section(git_path, "omitted unsafe untracked path")
|
|
309
|
+
|
|
310
|
+
candidate = workspace_path / relative_path
|
|
311
|
+
if candidate.is_symlink():
|
|
312
|
+
return _synthetic_untracked_section(git_path, "omitted untracked symlink")
|
|
313
|
+
|
|
314
|
+
resolved_candidate = candidate.resolve()
|
|
315
|
+
if workspace_path not in resolved_candidate.parents and resolved_candidate != workspace_path:
|
|
316
|
+
return _synthetic_untracked_section(git_path, "omitted untracked path outside workspace")
|
|
317
|
+
if not resolved_candidate.is_file():
|
|
318
|
+
return _synthetic_untracked_section(git_path, "omitted non-file untracked path")
|
|
319
|
+
|
|
320
|
+
content = _read_untracked_text_file(
|
|
321
|
+
resolved_candidate,
|
|
322
|
+
max_bytes=min(output_limit_bytes, _MAX_UNTRACKED_FILE_BYTES),
|
|
323
|
+
)
|
|
324
|
+
if content is None:
|
|
325
|
+
return _synthetic_untracked_section(git_path, "omitted binary or non-UTF-8 untracked file")
|
|
326
|
+
|
|
327
|
+
lines = content.splitlines(keepends=True)
|
|
328
|
+
prefixed_content = "".join(f"+{line}" for line in lines)
|
|
329
|
+
if content and not content.endswith("\n"):
|
|
330
|
+
prefixed_content += "\n\\n"
|
|
331
|
+
return (
|
|
332
|
+
f"diff --git a/{rendered_git_path} b/{rendered_git_path}\n"
|
|
333
|
+
"new file mode 100644\n"
|
|
334
|
+
"--- /dev/null\n"
|
|
335
|
+
f"+++ b/{rendered_git_path}\n"
|
|
336
|
+
f"{prefixed_content}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _read_untracked_text_file(path: Path, *, max_bytes: int) -> str | None:
|
|
341
|
+
with path.open("rb") as file:
|
|
342
|
+
content = file.read(max_bytes + 1)
|
|
343
|
+
bounded_content = content[:max_bytes]
|
|
344
|
+
if b"\0" in bounded_content:
|
|
345
|
+
return None
|
|
346
|
+
try:
|
|
347
|
+
text = bounded_content.decode("utf-8")
|
|
348
|
+
except UnicodeDecodeError:
|
|
349
|
+
return None
|
|
350
|
+
if len(content) > max_bytes:
|
|
351
|
+
text += "\n[TRUNCATED]\n"
|
|
352
|
+
return text
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _synthetic_untracked_section(git_path: str, message: str) -> str:
|
|
356
|
+
rendered_git_path = _render_synthetic_git_path(git_path)
|
|
357
|
+
return (
|
|
358
|
+
f"diff --git a/{rendered_git_path} b/{rendered_git_path}\n"
|
|
359
|
+
"new file evidence omitted\n"
|
|
360
|
+
f"# {message}\n"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _render_synthetic_git_path(git_path: str) -> str:
|
|
365
|
+
rendered_characters: list[str] = []
|
|
366
|
+
for character in git_path:
|
|
367
|
+
codepoint = ord(character)
|
|
368
|
+
if character == "\\":
|
|
369
|
+
rendered_characters.append("\\\\")
|
|
370
|
+
elif character == "\n":
|
|
371
|
+
rendered_characters.append("\\n")
|
|
372
|
+
elif character == "\r":
|
|
373
|
+
rendered_characters.append("\\r")
|
|
374
|
+
elif character == "\t":
|
|
375
|
+
rendered_characters.append("\\t")
|
|
376
|
+
elif codepoint < 32 or codepoint == 127:
|
|
377
|
+
rendered_characters.append(f"\\x{codepoint:02x}")
|
|
378
|
+
else:
|
|
379
|
+
rendered_characters.append(character)
|
|
380
|
+
return "".join(rendered_characters)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _first_nonzero(*returncodes: int) -> int:
|
|
384
|
+
for returncode in returncodes:
|
|
385
|
+
if returncode != 0:
|
|
386
|
+
return returncode
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _git_evidence_env() -> dict[str, str]:
|
|
391
|
+
env: dict[str, str] = {}
|
|
392
|
+
for key in ("PATH", "LANG", "LC_ALL", "LC_CTYPE"):
|
|
393
|
+
value = os.environ.get(key)
|
|
394
|
+
if value is not None:
|
|
395
|
+
env[key] = value
|
|
396
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
397
|
+
env["GIT_CONFIG_GLOBAL"] = os.devnull
|
|
398
|
+
env["GIT_CONFIG_NOSYSTEM"] = "1"
|
|
399
|
+
return env
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _limit_evidence_fields_to_total_budget(
|
|
403
|
+
*,
|
|
404
|
+
status: str,
|
|
405
|
+
diff: str,
|
|
406
|
+
stderr: str,
|
|
407
|
+
output_limit_bytes: int,
|
|
408
|
+
) -> dict[str, str]:
|
|
409
|
+
remaining_bytes = output_limit_bytes
|
|
410
|
+
limited_status = _limit_text_bytes(status, remaining_bytes)
|
|
411
|
+
remaining_bytes -= len(limited_status.encode())
|
|
412
|
+
limited_diff = _limit_text_bytes(diff, remaining_bytes)
|
|
413
|
+
remaining_bytes -= len(limited_diff.encode())
|
|
414
|
+
limited_stderr = _limit_text_bytes(stderr, remaining_bytes)
|
|
415
|
+
return {
|
|
416
|
+
"status": limited_status,
|
|
417
|
+
"diff": limited_diff,
|
|
418
|
+
"stderr": limited_stderr,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _append_text_with_limit(current: str, addition: str, limit_bytes: int) -> str:
|
|
423
|
+
remaining_bytes = limit_bytes - len(current.encode())
|
|
424
|
+
if remaining_bytes <= 0:
|
|
425
|
+
return current
|
|
426
|
+
return current + _limit_text_bytes(addition, remaining_bytes)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _limit_text_bytes(text: str, limit_bytes: int) -> str:
|
|
430
|
+
if limit_bytes <= 0:
|
|
431
|
+
return ""
|
|
432
|
+
encoded = text.encode()
|
|
433
|
+
if len(encoded) <= limit_bytes:
|
|
434
|
+
return text
|
|
435
|
+
if limit_bytes <= len(_TRUNCATION_MARKER.encode()):
|
|
436
|
+
return encoded[:limit_bytes].decode(errors="ignore")
|
|
437
|
+
content_limit = limit_bytes - len(_TRUNCATION_MARKER.encode())
|
|
438
|
+
return encoded[:content_limit].decode(errors="ignore") + _TRUNCATION_MARKER
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _append_truncation_marker(text: str, limit_bytes: int) -> str:
|
|
442
|
+
return _limit_text_bytes(text + _TRUNCATION_MARKER, limit_bytes)
|