coderio 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. coderio/__init__.py +14 -0
  2. coderio/agent/__init__.py +5 -0
  3. coderio/agent/_deepagents_compat.py +139 -0
  4. coderio/agent/command_review.py +140 -0
  5. coderio/agent/deep_loop.py +758 -0
  6. coderio/agent/harness.py +768 -0
  7. coderio/agent/harness_middleware.py +293 -0
  8. coderio/agent/permission_middleware.py +52 -0
  9. coderio/agent/prompts.py +304 -0
  10. coderio/agent/skill_tool.py +70 -0
  11. coderio/agent/state.py +144 -0
  12. coderio/agent/stream.py +93 -0
  13. coderio/cli/__init__.py +0 -0
  14. coderio/cli/app.py +257 -0
  15. coderio/cli/commands.py +339 -0
  16. coderio/cli/credentials.py +70 -0
  17. coderio/cli/mcp_cmd.py +227 -0
  18. coderio/cli/multimodal.py +94 -0
  19. coderio/cli/onboarding.py +270 -0
  20. coderio/cli/providers.py +113 -0
  21. coderio/cli/render.py +28 -0
  22. coderio/cli/repl.py +189 -0
  23. coderio/cli/run_cmd.py +171 -0
  24. coderio/cli/skills_cmd.py +77 -0
  25. coderio/cli/stream.py +224 -0
  26. coderio/cli/tui.py +1584 -0
  27. coderio/cli/tui_onboarding.py +465 -0
  28. coderio/cli/tui_screens.py +334 -0
  29. coderio/cli/tui_widgets.py +453 -0
  30. coderio/config/__init__.py +21 -0
  31. coderio/config/bootstrap.py +38 -0
  32. coderio/config/loader.py +273 -0
  33. coderio/config/models.py +193 -0
  34. coderio/config/trust.py +136 -0
  35. coderio/llm/__init__.py +3 -0
  36. coderio/llm/factory.py +164 -0
  37. coderio/llm/probe.py +153 -0
  38. coderio/mcp_loader.py +274 -0
  39. coderio/session/__init__.py +3 -0
  40. coderio/session/message.py +79 -0
  41. coderio/session/store.py +244 -0
  42. coderio/skills/__init__.py +4 -0
  43. coderio/skills/bundled/executing-plans/SKILL.md +15 -0
  44. coderio/skills/lion-skills/LICENSE +21 -0
  45. coderio/skills/lion-skills/README.md +114 -0
  46. coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
  47. coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
  48. coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
  49. coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
  50. coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
  51. coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
  52. coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
  53. coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
  54. coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
  55. coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
  56. coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
  57. coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
  58. coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
  59. coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
  60. coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
  61. coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
  62. coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
  63. coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
  64. coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
  65. coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
  66. coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
  67. coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
  68. coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
  69. coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
  70. coderio/skills/models.py +66 -0
  71. coderio/skills/parser.py +50 -0
  72. coderio/skills/store.py +102 -0
  73. coderio/tools/__init__.py +94 -0
  74. coderio/tools/base.py +55 -0
  75. coderio/tools/bash.py +168 -0
  76. coderio/tools/command_policy.py +364 -0
  77. coderio/tools/edit_file.py +55 -0
  78. coderio/tools/glob_tool.py +21 -0
  79. coderio/tools/grep_tool.py +90 -0
  80. coderio/tools/linux_sandbox.py +196 -0
  81. coderio/tools/list_dir.py +58 -0
  82. coderio/tools/multi_edit.py +62 -0
  83. coderio/tools/note.py +72 -0
  84. coderio/tools/permission.py +214 -0
  85. coderio/tools/read_file.py +51 -0
  86. coderio/tools/sandbox_runner.py +113 -0
  87. coderio/tools/todo.py +71 -0
  88. coderio/tools/web_fetch.py +188 -0
  89. coderio/tools/web_search.py +49 -0
  90. coderio/tools/win_job.py +214 -0
  91. coderio/tools/win_sandbox.py +639 -0
  92. coderio/tools/write_file.py +25 -0
  93. coderio-0.4.0.dist-info/METADATA +490 -0
  94. coderio-0.4.0.dist-info/RECORD +97 -0
  95. coderio-0.4.0.dist-info/WHEEL +4 -0
  96. coderio-0.4.0.dist-info/entry_points.txt +2 -0
  97. coderio-0.4.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class WebSearchArgs(BaseModel):
7
+ query: str = Field(description="Search query.")
8
+ max_results: int = Field(default=5, description="Maximum number of results.")
9
+
10
+
11
+ class WebSearchTool:
12
+ """Free, zero-config web search backed by DuckDuckGo (via the `ddgs` lib).
13
+
14
+ No API key required — works out of the box. DuckDuckGo occasionally rate-
15
+ limits automated queries; on failure we return a friendly error string
16
+ (not an exception) so the model can react (retry or switch approach).
17
+ """
18
+
19
+ name = "web_search"
20
+ description = "Search the web (DuckDuckGo) and return result titles + urls + snippets. No API key needed."
21
+ args_schema = WebSearchArgs
22
+
23
+ def run(self, query: str, max_results: int = 5) -> str:
24
+ try:
25
+ from ddgs import DDGS
26
+ except ImportError:
27
+ return "Error: web search unavailable (ddgs package not installed)."
28
+
29
+ try:
30
+ with DDGS() as ddgs:
31
+ results = list(ddgs.text(query, max_results=max_results))
32
+ except Exception as e:
33
+ # DuckDuckGo may rate-limit (HTTP 202/429) or be temporarily
34
+ # unreachable. Surface a clear, retryable message — the model can
35
+ # wait and retry, or fall back to its own knowledge.
36
+ return (
37
+ f"Error: web search failed ({type(e).__name__}: {e}). The service may be rate-limiting; retry shortly."
38
+ )
39
+
40
+ if not results:
41
+ return "No results found."
42
+
43
+ lines = []
44
+ for r in results:
45
+ title = r.get("title", "")
46
+ url = r.get("href", "") or r.get("url", "")
47
+ body = (r.get("body", "") or r.get("snippet", ""))[:200]
48
+ lines.append(f"- {title}\n {url}\n {body}")
49
+ return "\n".join(lines)
@@ -0,0 +1,214 @@
1
+ """Windows Job Object helpers for process-tree control and resource limits.
2
+
3
+ A Job Object groups processes so they can be controlled as a unit: kill the
4
+ whole tree, cap memory/CPU/process-count, etc. This module provides:
5
+
6
+ - ``create_job_with_limits``: create a Job Object with resource limits set.
7
+ - ``assign_to_job``: attach a process (and its children) to a job.
8
+ - ``kill_process_tree``: kill a process and all its descendants (the original
9
+ use case in bash.py, now shared with the production shell backend).
10
+
11
+ On non-Windows platforms these are no-ops / use ``os.killpg`` instead — callers
12
+ don't need to branch on ``sys.platform``.
13
+
14
+ Resource limits applied by default (when ``memory_limit_mb`` / ``process_limit``
15
+ are provided):
16
+ - ``JOB_OBJECT_LIMIT_PROCESS_MEMORY``: per-process memory cap (prevents a
17
+ single runaway test from OOM-ing the machine).
18
+ - ``JOB_OBJECT_LIMIT_ACTIVE_PROCESS``: max processes in the job (prevents
19
+ fork bombs from spawning unbounded children).
20
+ - ``JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE``: when the job handle closes (e.g.
21
+ coderio crashes), all child processes are terminated — no orphans.
22
+
23
+ These are RESOURCE limits, not a security sandbox. A process in a job can still
24
+ read/write any file the user has access to. For file-write isolation see
25
+ ``win_sandbox.py`` (Restricted Token).
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ import os
32
+ import sys
33
+ from typing import TYPE_CHECKING
34
+
35
+ if TYPE_CHECKING:
36
+ import subprocess
37
+
38
+ _log = logging.getLogger(__name__)
39
+
40
+ # Windows Job Object limit flags (winnt.h JOB_OBJECT_LIMIT_*).
41
+ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
42
+ JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x1000
43
+ JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x0008
44
+
45
+
46
+ def _build_win32_structs():
47
+ """Build the ctypes Structures needed for Job Object calls.
48
+
49
+ Lazily imported/built so non-Windows platforms don't pay the ctypes import
50
+ cost. Returns (kernel32, IO_COUNTERS, BASIC_LIMIT, EXTENDED_LIMIT) or raises
51
+ ImportError on non-Windows.
52
+ """
53
+ if sys.platform != "win32":
54
+ raise ImportError("win_job is Windows-only")
55
+
56
+ import ctypes
57
+
58
+ class _IO_COUNTERS(ctypes.Structure):
59
+ _fields_ = [
60
+ ("ReadOperationCount", ctypes.c_ulonglong),
61
+ ("WriteOperationCount", ctypes.c_ulonglong),
62
+ ("OtherOperationCount", ctypes.c_ulonglong),
63
+ ("ReadTransferCount", ctypes.c_ulonglong),
64
+ ("WriteTransferCount", ctypes.c_ulonglong),
65
+ ("OtherTransferCount", ctypes.c_ulonglong),
66
+ ]
67
+
68
+ class _JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
69
+ _fields_ = [
70
+ ("PerProcessUserTimeLimit", ctypes.c_longlong),
71
+ ("PerJobUserTimeLimit", ctypes.c_longlong),
72
+ ("LimitFlags", ctypes.c_ulong),
73
+ ("MinimumWorkingSetSize", ctypes.c_size_t),
74
+ ("MaximumWorkingSetSize", ctypes.c_size_t),
75
+ ("ActiveProcessLimit", ctypes.c_ulong),
76
+ ("Affinity", ctypes.c_size_t),
77
+ ("PriorityClass", ctypes.c_ulong),
78
+ ("SchedulingClass", ctypes.c_ulong),
79
+ ]
80
+
81
+ class _JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
82
+ _fields_ = [
83
+ ("BasicLimitInformation", _JOBOBJECT_BASIC_LIMIT_INFORMATION),
84
+ ("IoInfo", _IO_COUNTERS),
85
+ ("ProcessMemoryLimit", ctypes.c_size_t),
86
+ ("JobMemoryLimit", ctypes.c_size_t),
87
+ ("PeakProcessMemoryUsed", ctypes.c_size_t),
88
+ ("PeakJobMemoryUsed", ctypes.c_size_t),
89
+ ]
90
+
91
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
92
+ return kernel32, _IO_COUNTERS, _JOBOBJECT_BASIC_LIMIT_INFORMATION, _JOBOBJECT_EXTENDED_LIMIT_INFORMATION
93
+
94
+
95
+ def create_job_with_limits(
96
+ *,
97
+ memory_limit_mb: int | None = None,
98
+ process_limit: int | None = None,
99
+ ) -> int | None:
100
+ """Create a Job Object with the given resource limits.
101
+
102
+ Returns the job handle (int) on success, or None on failure (non-Windows,
103
+ or Win32 API error — caller should degrade gracefully). The caller owns the
104
+ handle and must CloseHandle it when done (or let process exit close it,
105
+ which triggers KILL_ON_JOB_CLOSE).
106
+
107
+ Args:
108
+ memory_limit_mb: per-process memory cap in MB. None = no limit.
109
+ process_limit: max processes in the job. None = no limit.
110
+ """
111
+ if sys.platform != "win32":
112
+ return None
113
+ try:
114
+ kernel32, _, _, _ExtendedLimit = _build_win32_structs()
115
+ import ctypes
116
+
117
+ h_job = kernel32.CreateJobObjectW(None, None)
118
+ if not h_job:
119
+ return None
120
+
121
+ info = _ExtendedLimit()
122
+ flags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
123
+ if memory_limit_mb is not None:
124
+ flags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY
125
+ info.ProcessMemoryLimit = memory_limit_mb * 1024 * 1024
126
+ if process_limit is not None:
127
+ flags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS
128
+ info.BasicLimitInformation.ActiveProcessLimit = process_limit
129
+ info.BasicLimitInformation.LimitFlags = flags
130
+
131
+ ok = kernel32.SetInformationJobObject(
132
+ h_job,
133
+ 9, # JobObjectExtendedLimitInformation
134
+ ctypes.byref(info),
135
+ ctypes.sizeof(info),
136
+ )
137
+ if not ok:
138
+ _log.warning("SetInformationJobObject failed (err=%s)", kernel32.GetLastError())
139
+ kernel32.CloseHandle(h_job)
140
+ return None
141
+ return h_job
142
+ except Exception as e: # noqa: BLE001 — best-effort, never crash the agent
143
+ _log.warning("create_job_with_limits failed (degrading to no limits): %s", e)
144
+ return None
145
+
146
+
147
+ def assign_to_job(job_handle: int, pid: int) -> bool:
148
+ """Attach a process to a Job Object. Returns True on success, False otherwise."""
149
+ if sys.platform != "win32" or not job_handle:
150
+ return False
151
+ try:
152
+ import ctypes
153
+
154
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
155
+ PROCESS_ALL_ACCESS = 0x1F0FFF
156
+ h_proc = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
157
+ if not h_proc:
158
+ return False
159
+ try:
160
+ ok = kernel32.AssignProcessToJobObject(job_handle, h_proc)
161
+ return bool(ok)
162
+ finally:
163
+ kernel32.CloseHandle(h_proc)
164
+ except Exception as e: # noqa: BLE001
165
+ _log.warning("assign_to_job failed: %s", e)
166
+ return False
167
+
168
+
169
+ def kill_process_tree(proc: subprocess.Popen) -> None:
170
+ """Kill a process AND all its children (the whole process tree).
171
+
172
+ On Windows: creates a Job Object, assigns the process, then terminates the
173
+ job — this kills the entire tree reliably (subprocess.run's timeout only
174
+ kills the direct child, leaving grandchildren running with open pipes).
175
+
176
+ On POSIX: uses os.killpg on the process group (requires the process to have
177
+ been started with start_new_session=True or in its own process group).
178
+
179
+ This is the shared implementation — bash.py (self-hosted tool) and
180
+ deep_loop.py (production shell backend) both use it.
181
+ """
182
+ pid = proc.pid
183
+ if sys.platform == "win32":
184
+ try:
185
+ import ctypes
186
+
187
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
188
+ # Create a one-shot job just for the kill (simpler than tracking a
189
+ # persistent job handle across the process lifetime). The
190
+ # KILL_ON_JOB_CLOSE + TerminateJobObject combo ensures the whole
191
+ # tree dies even if some children escaped assignment.
192
+ h_job = create_job_with_limits()
193
+ if h_job:
194
+ if assign_to_job(h_job, pid):
195
+ kernel32.TerminateJobObject(h_job, 1)
196
+ kernel32.CloseHandle(h_job)
197
+ # Fallback: if job creation failed, try a direct TerminateProcess
198
+ # (kills only the direct child, but better than hanging).
199
+ PROCESS_TERMINATE = 0x0001
200
+ h_proc = kernel32.OpenProcess(PROCESS_TERMINATE, False, pid)
201
+ if h_proc:
202
+ kernel32.TerminateProcess(h_proc, 1)
203
+ kernel32.CloseHandle(h_proc)
204
+ except Exception as e: # noqa: BLE001
205
+ _log.warning("kill_process_tree (Windows) failed: %s", e)
206
+ else:
207
+ try:
208
+ import signal
209
+
210
+ os.killpg(os.getpgid(pid), signal.SIGKILL) # type: ignore[name-defined]
211
+ except (ProcessLookupError, PermissionError):
212
+ pass
213
+ except Exception as e: # noqa: BLE001
214
+ _log.warning("kill_process_tree (POSIX) failed: %s", e)