agentic-runner 3.4.1__tar.gz → 3.5.0__tar.gz

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 (69) hide show
  1. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/PKG-INFO +1 -1
  2. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/__init__.py +1 -1
  3. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/activities.py +92 -30
  4. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/service.py +10 -1
  5. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/testing/control_plane.py +2 -2
  6. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/_runtime_support.py +23 -6
  7. agentic_runner-3.5.0/src/agentic_runner/workers/acp_runtime.py +938 -0
  8. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/agent_runtime.py +32 -0
  9. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/contract_isolation.py +15 -3
  10. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/settings.py +7 -0
  11. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/.gitignore +0 -0
  12. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/LICENSE +0 -0
  13. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/README.md +0 -0
  14. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/pyproject.toml +0 -0
  15. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/attempts.py +0 -0
  16. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/auth_mode.py +0 -0
  17. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/build.py +0 -0
  18. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/callback.py +0 -0
  19. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/child_watcher.py +0 -0
  20. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/cli.py +0 -0
  21. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/cli_floor.py +0 -0
  22. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/config.py +0 -0
  23. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/credentials.py +0 -0
  24. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/device_login_activities.py +0 -0
  25. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/egress.py +0 -0
  26. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/harness_self_test.py +0 -0
  27. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/heartbeat_link.py +0 -0
  28. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/hooks.py +0 -0
  29. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/host_store.py +0 -0
  30. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/__init__.py +0 -0
  31. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  32. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  33. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  34. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  35. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  36. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  37. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  38. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  39. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  40. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/lifecycle.py +0 -0
  41. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/llm_proxy.py +0 -0
  42. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/mcp.py +0 -0
  43. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/message_store.py +0 -0
  44. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/private_state.py +0 -0
  45. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/py.typed +0 -0
  46. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  47. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/registration.py +0 -0
  48. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/runtime/__init__.py +0 -0
  49. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  50. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/sealed_box.py +0 -0
  51. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/testing/__init__.py +0 -0
  52. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/testing/__main__.py +0 -0
  53. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/testing/plugin.py +0 -0
  54. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  55. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/testing/workflow.py +0 -0
  56. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/tiny_http.py +0 -0
  57. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/triage_activities.py +0 -0
  58. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/user_sources.py +0 -0
  59. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/__init__.py +0 -0
  60. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  61. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  62. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/command_policy.py +0 -0
  63. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
  64. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  65. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
  66. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  67. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  68. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workers/skills.py +0 -0
  69. {agentic_runner-3.4.1 → agentic_runner-3.5.0}/src/agentic_runner/workstation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-runner
3
- Version: 3.4.1
3
+ Version: 3.5.0
4
4
  Summary: The Agentic OS Runner: an activity-only Temporal executor for workspaces, Agent Runtimes and verb seams
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -7,6 +7,6 @@ stay on the platform; this package imports none of them, and a CI partition test
7
7
  (``tests/test_package_partition.py``) fails the build if that changes.
8
8
  """
9
9
 
10
- __version__ = "3.4.1"
10
+ __version__ = "3.5.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -29,7 +29,7 @@ import shlex
29
29
  import shutil
30
30
  import tempfile
31
31
  import time
32
- from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
32
+ from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
33
33
  from dataclasses import asdict, dataclass, field, fields, replace
34
34
  from datetime import UTC, datetime
35
35
  from pathlib import Path
@@ -128,6 +128,8 @@ from agentic_runner.workers.agent_runtime import (
128
128
  AuthMode,
129
129
  DirectiveRequest,
130
130
  DirectiveResult,
131
+ ModelUsage,
132
+ PermissionFallback,
131
133
  ResumableAgentRuntime,
132
134
  )
133
135
  from agentic_runner.workers.contract_device_login import login_file_counts
@@ -588,6 +590,9 @@ class _RuntimeContextState:
588
590
  # on the Profile's `command_policy` blob — the sandbox floor already lives there, and
589
591
  # a ceiling is exactly a floor rule. None means the Runner's own default applies.
590
592
  memory_limit_bytes: int | None = None
593
+ # PRD decision 12: what a runtime asked per command does when the command policy has
594
+ # no answer. Carried on the same `command_policy` blob as the ceiling above.
595
+ permission_fallback: PermissionFallback = PermissionFallback.DENY
591
596
  # Pushed on change and applied on receipt (ADR-0011 §11, PRD issue 44): the evaluator
592
597
  # is local, so a narrowing lands on the next verb this state is read for.
593
598
  grant_snapshot: GrantSnapshot = UNENFORCED_SNAPSHOT
@@ -1296,6 +1301,56 @@ def _profile_memory_limit_bytes(command_policy: Mapping[str, object]) -> int | N
1296
1301
  return None
1297
1302
 
1298
1303
 
1304
+ def _model_usage_reports(
1305
+ cli_kind: str, model_usage: Sequence[ModelUsage]
1306
+ ) -> list[tuple[dict[str, Any], str | None]]:
1307
+ """A harness's per-model usage in the shapes the harness usage route already prices.
1308
+
1309
+ Claude Code: one ``result`` message whose ``modelUsage`` names every model. Codex: one
1310
+ ``turn.completed.usage`` per model, whose ``input_tokens`` includes the cached reads,
1311
+ with the model the harness itself named rather than the Agent's configured one.
1312
+ """
1313
+
1314
+ if cli_kind == "claude_code":
1315
+ return [
1316
+ (
1317
+ {
1318
+ "type": "result",
1319
+ "modelUsage": {
1320
+ row.model: {
1321
+ "inputTokens": row.input_tokens,
1322
+ "outputTokens": row.output_tokens,
1323
+ "cacheReadInputTokens": row.cached_read_tokens,
1324
+ "cacheCreationInputTokens": row.cached_write_tokens,
1325
+ }
1326
+ for row in model_usage
1327
+ },
1328
+ },
1329
+ None,
1330
+ )
1331
+ ]
1332
+ return [
1333
+ (
1334
+ {
1335
+ "input_tokens": row.input_tokens + row.cached_read_tokens,
1336
+ "cached_input_tokens": row.cached_read_tokens,
1337
+ "output_tokens": row.output_tokens,
1338
+ "reasoning_output_tokens": row.reasoning_output_tokens,
1339
+ },
1340
+ row.model,
1341
+ )
1342
+ for row in model_usage
1343
+ ]
1344
+
1345
+
1346
+ def _profile_permission_fallback(command_policy: Mapping[str, object]) -> PermissionFallback:
1347
+ """The Profile's `permission_fallback`; anything but exactly ``hold`` is ``deny``."""
1348
+
1349
+ if command_policy.get("permission_fallback") == PermissionFallback.HOLD.value:
1350
+ return PermissionFallback.HOLD
1351
+ return PermissionFallback.DENY
1352
+
1353
+
1299
1354
  def _contract_workspace_path(
1300
1355
  *,
1301
1356
  workspace_root: Path,
@@ -1992,6 +2047,7 @@ class RunnerRalphActivities:
1992
2047
  mcp_servers=attempt.mcp_servers,
1993
2048
  egress_allow_list=attempt.egress_allow_list,
1994
2049
  auth_mode=attempt.auth_mode,
2050
+ permission_fallback=runtime_state.permission_fallback,
1995
2051
  prompt=attempt.skills_preamble
1996
2052
  + _directive_prompt(
1997
2053
  completion_criteria=runtime_state.completion_criteria,
@@ -3363,6 +3419,7 @@ class RunnerRalphActivities:
3363
3419
  cli_kind=runtime_context.cli_kind,
3364
3420
  model=runtime_context.model,
3365
3421
  memory_limit_bytes=_profile_memory_limit_bytes(runtime_context.command_policy),
3422
+ permission_fallback=_profile_permission_fallback(runtime_context.command_policy),
3366
3423
  grant_snapshot=snapshot,
3367
3424
  grant_snapshot_payload=snapshot_payload,
3368
3425
  persona_slug=runtime_context.persona_slug,
@@ -4636,6 +4693,7 @@ class RunnerRalphActivities:
4636
4693
  mcp_servers=attempt.mcp_servers,
4637
4694
  egress_allow_list=attempt.egress_allow_list,
4638
4695
  auth_mode=attempt.auth_mode,
4696
+ permission_fallback=state.permission_fallback,
4639
4697
  prompt=attempt.skills_preamble
4640
4698
  + _learning_prompt(
4641
4699
  request,
@@ -4896,6 +4954,7 @@ class RunnerRalphActivities:
4896
4954
  mcp_servers=attempt.mcp_servers,
4897
4955
  egress_allow_list=attempt.egress_allow_list,
4898
4956
  auth_mode=attempt.auth_mode,
4957
+ permission_fallback=state.permission_fallback,
4899
4958
  prompt=attempt.skills_preamble + await kind.prompt(state, prompt_notes),
4900
4959
  base_branch=request.base_ref,
4901
4960
  work_branch=state.work_branch,
@@ -5155,38 +5214,41 @@ class RunnerRalphActivities:
5155
5214
  if auth_mode == AuthMode.API_KEY:
5156
5215
  return
5157
5216
  directive = directive_id(work_record_id=work_record_id, directive_number=directive_number)
5158
- if runtime_state.cli_kind == "claude_code":
5217
+ reports: list[tuple[dict[str, Any], str | None]]
5218
+ if result.model_usage:
5219
+ reports = _model_usage_reports(runtime_state.cli_kind, result.model_usage)
5220
+ elif runtime_state.cli_kind == "claude_code":
5159
5221
  usage_event = extract_claude_result(result.stdout)
5160
- model_name = None
5222
+ reports = [(usage_event, None)] if usage_event else []
5161
5223
  else:
5162
5224
  usage_event = extract_codex_turn_usage(result.stdout)
5163
- model_name = runtime_state.model or None
5164
- if not usage_event:
5165
- return
5166
- if runtime_state.cli_kind != "claude_code" and model_name is None:
5167
- # The backend 422s a Codex harness report with no model (`report_harness_usage`
5168
- # requires one to price it), and the `suppress` below swallows that -- an Agent
5169
- # with no model configured would otherwise lose this Usage Record with no trace.
5170
- activity.logger.warning(
5171
- "harness usage unreported: Agent %s has no model configured for a Codex "
5172
- "harness Directive (work record %s)",
5173
- runtime_state.agent_id,
5174
- work_record_id,
5175
- )
5176
- with contextlib.suppress(Exception): # noqa: BLE001 - metering never fails a Directive
5177
- await self._fastapi_client.report_harness_usage(
5178
- {
5179
- "work_record_id": work_record_id,
5180
- "agent_id": runtime_state.agent_id,
5181
- "contract_id": runtime_state.contract_id,
5182
- "directive_id": directive,
5183
- "runner_id": None,
5184
- "runtime_kind": runtime_state.cli_kind,
5185
- "provider_name": runtime_state.cli_kind,
5186
- "model_name": model_name,
5187
- "usage_event": usage_event,
5188
- }
5189
- )
5225
+ reports = [(usage_event, runtime_state.model or None)] if usage_event else []
5226
+ for usage_event, model_name in reports:
5227
+ if runtime_state.cli_kind != "claude_code" and model_name is None:
5228
+ # The backend 422s a Codex harness report with no model
5229
+ # (`report_harness_usage` requires one to price it), and the `suppress`
5230
+ # below swallows that -- an Agent with no model configured would otherwise
5231
+ # lose this Usage Record with no trace.
5232
+ activity.logger.warning(
5233
+ "harness usage unreported: Agent %s has no model configured for a Codex "
5234
+ "harness Directive (work record %s)",
5235
+ runtime_state.agent_id,
5236
+ work_record_id,
5237
+ )
5238
+ with contextlib.suppress(Exception): # noqa: BLE001 - metering never fails a Directive
5239
+ await self._fastapi_client.report_harness_usage(
5240
+ {
5241
+ "work_record_id": work_record_id,
5242
+ "agent_id": runtime_state.agent_id,
5243
+ "contract_id": runtime_state.contract_id,
5244
+ "directive_id": directive,
5245
+ "runner_id": None,
5246
+ "runtime_kind": runtime_state.cli_kind,
5247
+ "provider_name": runtime_state.cli_kind,
5248
+ "model_name": model_name,
5249
+ "usage_event": usage_event,
5250
+ }
5251
+ )
5190
5252
 
5191
5253
  async def _harness_hold(
5192
5254
  self,
@@ -99,6 +99,7 @@ from agentic_runner.sealed_box import (
99
99
  from agentic_runner.tiny_http import HttpRequest, read_request, write_json
100
100
  from agentic_runner.triage_activities import RunnerTriageActivities
101
101
  from agentic_runner.user_sources import ProxyTriage, UserSourcePoller
102
+ from agentic_runner.workers.acp_runtime import ACP_BRIDGES, AcpRuntime
102
103
  from agentic_runner.workers.agent_runtime import AgentRuntime
103
104
  from agentic_runner.workers.claude_runtime import ClaudeRuntime
104
105
  from agentic_runner.workers.codex_runtime import CodexRuntime
@@ -388,10 +389,18 @@ def build_agent_runtimes(
388
389
  picks from here by the Work Record's `cli_kind` (local-agents 01) -- there is no
389
390
  process-wide choice. The same `clis` ride the heartbeat's attestation, so what this
390
391
  Runner serves and what routing believes it serves are one list.
392
+
393
+ Which runtime serves a kind -- its pinned ACP bridge or the per-CLI one -- is this
394
+ Runner's setting (`ACP_CLI_KINDS`), never the payload's (local-agents 12 items 6, 7).
391
395
  """
392
396
 
397
+ acp_kinds = {kind.strip() for kind in settings.ACP_CLI_KINDS.split(",") if kind.strip()}
393
398
  return {
394
- cli.cli_kind: _RUNTIMES[cli.cli_kind](settings)
399
+ cli.cli_kind: (
400
+ AcpRuntime(cli_kind=cli.cli_kind, settings=settings)
401
+ if cli.cli_kind in acp_kinds and cli.cli_kind in ACP_BRIDGES
402
+ else _RUNTIMES[cli.cli_kind](settings)
403
+ )
395
404
  for cli in clis
396
405
  if cli.meets_floor and cli.cli_kind in _RUNTIMES
397
406
  }
@@ -37,7 +37,7 @@ from dataclasses import dataclass, field
37
37
  from datetime import UTC, datetime, timedelta
38
38
  from email.utils import formatdate
39
39
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
40
- from typing import Any, Literal
40
+ from typing import Any
41
41
  from uuid import UUID, uuid4
42
42
 
43
43
  import httpx
@@ -166,7 +166,7 @@ class FakeControlPlane:
166
166
  contracts_floor: str = contracts_version
167
167
  contract_id: UUID = DEFAULT_CONTRACT_ID
168
168
  agent_id: UUID = DEFAULT_AGENT_ID
169
- cli_kind: Literal["codex_cli", "claude_code"] = "codex_cli"
169
+ cli_kind: str = "codex_cli"
170
170
 
171
171
  bootstraps: list[BootstrapRequest] = field(default_factory=list)
172
172
  runner_ids: list[UUID] = field(default_factory=list)
@@ -42,7 +42,10 @@ from agentic_runner.workers.contract_isolation import DirectiveSandbox
42
42
  # the funder anyway, or route it round the Profile's egress allow-list. The two Claude Code
43
43
  # bearers are here for the last reason too: either would authenticate the harness on a
44
44
  # credential the Runner never chose, around the proxy and the mode it decided
45
- # (local-agents 04; research 01 §4 gap 6).
45
+ # (local-agents 04; research 01 §4 gap 6). The ACP bridges' control variables are here
46
+ # for the same reason (local-agents 12): each swaps the vendor binary, hands approvals
47
+ # back to the harness's own reviewer, authenticates the bridge, or re-enables the MCP
48
+ # filtering that drops a granted server.
46
49
  RESERVED_DIRECTIVE_ENV: Final[frozenset[str]] = frozenset(
47
50
  {
48
51
  "HOME",
@@ -53,6 +56,20 @@ RESERVED_DIRECTIVE_ENV: Final[frozenset[str]] = frozenset(
53
56
  "ANTHROPIC_API_KEY",
54
57
  "ANTHROPIC_AUTH_TOKEN",
55
58
  "CLAUDE_CODE_OAUTH_TOKEN",
59
+ "CODEX_PATH",
60
+ "CODEX_CONFIG",
61
+ "CODEX_API_KEY",
62
+ "MODEL_PROVIDER",
63
+ "DEFAULT_AUTH_REQUEST",
64
+ "INITIAL_AGENT_MODE",
65
+ "NO_BROWSER",
66
+ "APP_SERVER_LOGS",
67
+ "DISABLE_MCP_CONFIG_FILTERING",
68
+ "CLAUDE_CODE_EXECUTABLE",
69
+ "CLAUDE_MODEL_CONFIG",
70
+ "CLAUDE_AGENT_LOGS",
71
+ "MAX_THINKING_TOKENS",
72
+ "ANTHROPIC_MODEL",
56
73
  CALLBACK_SOCKET_ENV,
57
74
  CALLBACK_TOKEN_ENV,
58
75
  *PROXY_ENV_NAMES,
@@ -154,10 +171,10 @@ async def run_subprocess_exec(
154
171
  timeout=timeout_seconds,
155
172
  )
156
173
  except (TimeoutError, asyncio.CancelledError):
157
- await _terminate_process_tree(process)
174
+ await terminate_process_tree(process)
158
175
  raise
159
176
  except PriorAttemptAliveError:
160
- await _terminate_process_tree(process)
177
+ await terminate_process_tree(process)
161
178
  raise
162
179
 
163
180
  return SubprocessResult(
@@ -240,7 +257,7 @@ async def _read_stream_limited(
240
257
  return bytes(tail), total_bytes > limit_bytes
241
258
 
242
259
 
243
- async def _terminate_process_tree(process: asyncio.subprocess.Process) -> None:
260
+ async def terminate_process_tree(process: asyncio.subprocess.Process) -> None:
244
261
  if process.returncode is not None:
245
262
  return
246
263
  with contextlib.suppress(ProcessLookupError, PermissionError):
@@ -379,7 +396,7 @@ async def run_subprocess_launch_and_detach(
379
396
  raise RuntimeError(f"process failed to start: {error.__class__.__name__}") from None
380
397
 
381
398
  if process.stdout is None:
382
- await _terminate_process_tree(process)
399
+ await terminate_process_tree(process)
383
400
  raise RuntimeError("process stdout stream was unavailable")
384
401
 
385
402
  loop = asyncio.get_running_loop()
@@ -408,7 +425,7 @@ async def run_subprocess_launch_and_detach(
408
425
  _detach(process)
409
426
  return text
410
427
  except BaseException:
411
- await _terminate_process_tree(process)
428
+ await terminate_process_tree(process)
412
429
  raise
413
430
 
414
431