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.
- coderio/__init__.py +14 -0
- coderio/agent/__init__.py +5 -0
- coderio/agent/_deepagents_compat.py +139 -0
- coderio/agent/command_review.py +140 -0
- coderio/agent/deep_loop.py +758 -0
- coderio/agent/harness.py +768 -0
- coderio/agent/harness_middleware.py +293 -0
- coderio/agent/permission_middleware.py +52 -0
- coderio/agent/prompts.py +304 -0
- coderio/agent/skill_tool.py +70 -0
- coderio/agent/state.py +144 -0
- coderio/agent/stream.py +93 -0
- coderio/cli/__init__.py +0 -0
- coderio/cli/app.py +257 -0
- coderio/cli/commands.py +339 -0
- coderio/cli/credentials.py +70 -0
- coderio/cli/mcp_cmd.py +227 -0
- coderio/cli/multimodal.py +94 -0
- coderio/cli/onboarding.py +270 -0
- coderio/cli/providers.py +113 -0
- coderio/cli/render.py +28 -0
- coderio/cli/repl.py +189 -0
- coderio/cli/run_cmd.py +171 -0
- coderio/cli/skills_cmd.py +77 -0
- coderio/cli/stream.py +224 -0
- coderio/cli/tui.py +1584 -0
- coderio/cli/tui_onboarding.py +465 -0
- coderio/cli/tui_screens.py +334 -0
- coderio/cli/tui_widgets.py +453 -0
- coderio/config/__init__.py +21 -0
- coderio/config/bootstrap.py +38 -0
- coderio/config/loader.py +273 -0
- coderio/config/models.py +193 -0
- coderio/config/trust.py +136 -0
- coderio/llm/__init__.py +3 -0
- coderio/llm/factory.py +164 -0
- coderio/llm/probe.py +153 -0
- coderio/mcp_loader.py +274 -0
- coderio/session/__init__.py +3 -0
- coderio/session/message.py +79 -0
- coderio/session/store.py +244 -0
- coderio/skills/__init__.py +4 -0
- coderio/skills/bundled/executing-plans/SKILL.md +15 -0
- coderio/skills/lion-skills/LICENSE +21 -0
- coderio/skills/lion-skills/README.md +114 -0
- coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
- coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
- coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
- coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
- coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
- coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
- coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
- coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
- coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
- coderio/skills/models.py +66 -0
- coderio/skills/parser.py +50 -0
- coderio/skills/store.py +102 -0
- coderio/tools/__init__.py +94 -0
- coderio/tools/base.py +55 -0
- coderio/tools/bash.py +168 -0
- coderio/tools/command_policy.py +364 -0
- coderio/tools/edit_file.py +55 -0
- coderio/tools/glob_tool.py +21 -0
- coderio/tools/grep_tool.py +90 -0
- coderio/tools/linux_sandbox.py +196 -0
- coderio/tools/list_dir.py +58 -0
- coderio/tools/multi_edit.py +62 -0
- coderio/tools/note.py +72 -0
- coderio/tools/permission.py +214 -0
- coderio/tools/read_file.py +51 -0
- coderio/tools/sandbox_runner.py +113 -0
- coderio/tools/todo.py +71 -0
- coderio/tools/web_fetch.py +188 -0
- coderio/tools/web_search.py +49 -0
- coderio/tools/win_job.py +214 -0
- coderio/tools/win_sandbox.py +639 -0
- coderio/tools/write_file.py +25 -0
- coderio-0.4.0.dist-info/METADATA +490 -0
- coderio-0.4.0.dist-info/RECORD +97 -0
- coderio-0.4.0.dist-info/WHEEL +4 -0
- coderio-0.4.0.dist-info/entry_points.txt +2 -0
- 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)
|
coderio/tools/win_job.py
ADDED
|
@@ -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)
|