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
coderio/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""coderio: a skill-driven coding agent."""
|
|
2
|
+
|
|
3
|
+
# Single source of truth for the version is pyproject.toml's [project].version.
|
|
4
|
+
# Read it at runtime via importlib.metadata so __version__ can never drift.
|
|
5
|
+
# Falls back to "0.0.0" only when the package isn't installed (e.g. running
|
|
6
|
+
# directly from a source checkout without `pip install -e .`) — in that case
|
|
7
|
+
# the banner shows "0.0.0", which is obviously wrong rather than silently
|
|
8
|
+
# stale, prompting the developer to install the package.
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("coderio")
|
|
13
|
+
except PackageNotFoundError: # pragma: no cover - dev-only fallback
|
|
14
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Compatibility layer for deepagents internal APIs.
|
|
2
|
+
|
|
3
|
+
Centralizes all usage of deepagents internals (non-public APIs) so that:
|
|
4
|
+
1. There's a single file to update when deepagents changes its internals.
|
|
5
|
+
2. Version-specific breakage produces a clear error message instead of a
|
|
6
|
+
cryptic AttributeError/ImportError deep in a call stack.
|
|
7
|
+
3. Future migration to public APIs (if/when available) is a single-file change.
|
|
8
|
+
|
|
9
|
+
Current internal dependencies:
|
|
10
|
+
- BASE_AGENT_PROMPT (deepagents.graph): module-level string, monkey-patched
|
|
11
|
+
to empty to prevent prompt conflicts. Public alternative not yet available.
|
|
12
|
+
- PlanningState.todos state key (langchain.agents.middleware.todo): the graph
|
|
13
|
+
state field where write_todos persists its output. The key name "todos" is
|
|
14
|
+
an undocumented implementation detail of langchain's planning middleware —
|
|
15
|
+
centralized here (TODOS_STATE_KEY + get_state_todos) so a rename upstream
|
|
16
|
+
only requires updating this one constant instead of scattered state.get()
|
|
17
|
+
reads across harness_middleware.
|
|
18
|
+
|
|
19
|
+
The research subagent's tool isolation (_ToolWhitelistMiddleware below) is
|
|
20
|
+
NOT a deepagents internal dependency — it subclasses the public AgentMiddleware
|
|
21
|
+
base and uses the documented ModelRequest.override mechanism. The old
|
|
22
|
+
_ToolExclusionMiddleware (blacklist) was removed in favor of this whitelist
|
|
23
|
+
approach (2026-08-07 report P2-9).
|
|
24
|
+
|
|
25
|
+
If BASE_AGENT_PROMPT patching fails, we degrade gracefully (the duplicate
|
|
26
|
+
prompt will appear but won't crash).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
35
|
+
|
|
36
|
+
_log = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# Graph state key under which langchain's planning middleware (TodoListMiddleware)
|
|
39
|
+
# persists the todo list. Sourced from langchain.agents.middleware.todo.PlanningState
|
|
40
|
+
# (the "todos: Annotated[NotRequired[list[Todo]], OmitFromInput]" field). This key
|
|
41
|
+
# name is an undocumented implementation detail — if langchain renames it (e.g. to
|
|
42
|
+
# "todo_list"), only this constant needs updating, not every state.get("todos") call.
|
|
43
|
+
TODOS_STATE_KEY = "todos"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_state_todos(state: Any) -> list | None:
|
|
47
|
+
"""Read the todos list from graph state, dict-or-object agnostic.
|
|
48
|
+
|
|
49
|
+
Centralizes the state.get("todos") read so that a key rename upstream
|
|
50
|
+
produces a single-file fix (update TODOS_STATE_KEY) instead of scattered
|
|
51
|
+
silent failures across harness_middleware. Returns None when the key is
|
|
52
|
+
absent (the caller treats None as "no todos to sync").
|
|
53
|
+
"""
|
|
54
|
+
if hasattr(state, "get"):
|
|
55
|
+
return state.get(TODOS_STATE_KEY)
|
|
56
|
+
return getattr(state, TODOS_STATE_KEY, None)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def neutralize_base_prompt() -> bool:
|
|
60
|
+
"""Set deepagents' BASE_AGENT_PROMPT to empty string.
|
|
61
|
+
|
|
62
|
+
create_deep_agent appends BASE_AGENT_PROMPT after the caller's system_prompt,
|
|
63
|
+
causing conflicts (e.g. 'explore first' vs 'match effort', macOS path
|
|
64
|
+
examples vs virtual paths). coderio's prompt covers everything — setting
|
|
65
|
+
BASE to empty ensures only coderio's prompt reaches the model.
|
|
66
|
+
|
|
67
|
+
Returns True if successful, False if the internal API has changed (in
|
|
68
|
+
which case the duplicate prompt will appear but won't crash).
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
import deepagents.graph as _dg_graph
|
|
72
|
+
|
|
73
|
+
if hasattr(_dg_graph, "BASE_AGENT_PROMPT") and _dg_graph.BASE_AGENT_PROMPT:
|
|
74
|
+
_dg_graph.BASE_AGENT_PROMPT = ""
|
|
75
|
+
return True
|
|
76
|
+
except Exception as e:
|
|
77
|
+
_log.warning("Could not neutralize BASE_AGENT_PROMPT (deepagents API may have changed): %s", e)
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _tool_name(tool: Any) -> str | None:
|
|
82
|
+
"""Extract the name from a BaseTool or dict tool (mirrors deepagents' helper)."""
|
|
83
|
+
if isinstance(tool, dict):
|
|
84
|
+
name = tool.get("name")
|
|
85
|
+
return name if isinstance(name, str) else None
|
|
86
|
+
name = getattr(tool, "name", None)
|
|
87
|
+
return name if isinstance(name, str) else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _ToolWhitelistMiddleware(AgentMiddleware):
|
|
91
|
+
"""Whitelist-first tool filter — the model only sees tools in ``allowed``.
|
|
92
|
+
|
|
93
|
+
Safer than the blacklist approach (``_ToolExclusionMiddleware``) because a
|
|
94
|
+
new destructive tool added by deepagents in a future version is
|
|
95
|
+
automatically blocked (it's not in the whitelist). The blacklist would
|
|
96
|
+
leak it until coderio is updated to exclude the new name.
|
|
97
|
+
|
|
98
|
+
Wraps the same ModelRequest.override + filter mechanism as
|
|
99
|
+
``_ToolExclusionMiddleware`` but inverts the predicate.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, *, allowed: frozenset[str]) -> None:
|
|
103
|
+
self._allowed = allowed
|
|
104
|
+
|
|
105
|
+
def wrap_model_call(self, request, handler):
|
|
106
|
+
if self._allowed:
|
|
107
|
+
filtered = [t for t in request.tools if _tool_name(t) in self._allowed]
|
|
108
|
+
request = request.override(tools=filtered)
|
|
109
|
+
return handler(request)
|
|
110
|
+
|
|
111
|
+
async def awrap_model_call(self, request, handler):
|
|
112
|
+
if self._allowed:
|
|
113
|
+
filtered = [t for t in request.tools if _tool_name(t) in self._allowed]
|
|
114
|
+
request = request.override(tools=filtered)
|
|
115
|
+
return await handler(request)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def make_research_subagent_middleware():
|
|
119
|
+
"""Build the middleware list for the research subagent.
|
|
120
|
+
|
|
121
|
+
WHITELIST approach (2026-08-07 report P2-9): the research subagent can
|
|
122
|
+
ONLY use tools in the explicit read-only set below. This is safer than the
|
|
123
|
+
old blacklist (exclude write_file/edit_file/execute/write_todos) because
|
|
124
|
+
any new tool deepagents adds is blocked by default until explicitly
|
|
125
|
+
allowlisted here.
|
|
126
|
+
|
|
127
|
+
Returns a list with a _ToolWhitelistMiddleware. If the deepagents
|
|
128
|
+
AgentMiddleware base class API is unavailable (very old or very new
|
|
129
|
+
version), returns an empty list — the subagent will inherit all tools
|
|
130
|
+
(less safe, but won't crash).
|
|
131
|
+
"""
|
|
132
|
+
# Read-only tools the research subagent is allowed to use. Add new tools
|
|
133
|
+
# here ONLY after confirming they're safe for a read-only agent.
|
|
134
|
+
allowed = frozenset({"read_file", "ls", "glob", "grep", "web_search"})
|
|
135
|
+
try:
|
|
136
|
+
return [_ToolWhitelistMiddleware(allowed=allowed)]
|
|
137
|
+
except Exception as e: # noqa: BLE001 — AgentMiddleware API may differ
|
|
138
|
+
_log.warning("Could not build _ToolWhitelistMiddleware: %s", e)
|
|
139
|
+
return []
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Command-content review as a deepagents AgentMiddleware.
|
|
2
|
+
|
|
3
|
+
Sits in the same middleware chain as PermissionMiddleware, AFTER it. While
|
|
4
|
+
PermissionMiddleware decides based on TOOL TYPE + MODE (which tier), this
|
|
5
|
+
middleware inspects the CONTENT of shell commands and network calls — blocking
|
|
6
|
+
destructive patterns (``rm -rf /``, ``mkfs``, fork bombs) regardless of the
|
|
7
|
+
permission mode.
|
|
8
|
+
|
|
9
|
+
This is NOT a real OS sandbox: a determined model can obfuscate commands to
|
|
10
|
+
bypass regex matching. The goal is to stop accidental/careless destruction,
|
|
11
|
+
which is the vast majority of real-world incidents. See command_policy.py for
|
|
12
|
+
the pattern list and the limitations.
|
|
13
|
+
|
|
14
|
+
Runs even in FULL mode: safety takes priority over the "FULL = allow
|
|
15
|
+
everything" literal semantics. A user who selects FULL accepts that file edits
|
|
16
|
+
and shell execution happen without prompts, but not that ``rm -rf /`` proceeds
|
|
17
|
+
without even a logged block.
|
|
18
|
+
|
|
19
|
+
Whitelist interaction: when whitelist_mode is enabled on the policy, commands
|
|
20
|
+
outside the whitelist are flagged. Unlike blacklist (hard block), whitelist
|
|
21
|
+
misses degrade gracefully:
|
|
22
|
+
- FULL mode: allowed (FULL = explicit trust, user accepted all commands)
|
|
23
|
+
- CONFIRM/AUTO_EDIT: the whitelist hint is appended to the command result so
|
|
24
|
+
the user can see it during the (already-triggered) permission prompt. We
|
|
25
|
+
don't hard-block because PermissionMiddleware already gates execute.
|
|
26
|
+
- PLAN mode: execute is already blocked by PermissionMiddleware before this
|
|
27
|
+
middleware runs, so the whitelist check is moot for plan mode.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
33
|
+
from langchain_core.messages import ToolMessage
|
|
34
|
+
|
|
35
|
+
# Tools whose CONTENT this middleware inspects. Note deepagents native names.
|
|
36
|
+
_SHELL_TOOL = "execute"
|
|
37
|
+
_NETWORK_TOOLS = frozenset({"web_fetch", "web_search"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _augment_with_whitelist_note(result, note: str):
|
|
41
|
+
"""Return ``result`` with the whitelist note appended.
|
|
42
|
+
|
|
43
|
+
Handles three result shapes the execute tool can return:
|
|
44
|
+
- str (plain string): return a new string with the note appended.
|
|
45
|
+
- ToolMessage (deepagents wraps results): mutate .content in-place and return.
|
|
46
|
+
- ExecuteResponse (deepagents backends): mutate .output in-place and return.
|
|
47
|
+
|
|
48
|
+
Returns the (possibly mutated) result so the caller can return it directly.
|
|
49
|
+
"""
|
|
50
|
+
content = getattr(result, "content", None)
|
|
51
|
+
if content is not None and isinstance(content, str):
|
|
52
|
+
result.content = f"{content}\n[whitelist] {note}"
|
|
53
|
+
return result
|
|
54
|
+
output = getattr(result, "output", None)
|
|
55
|
+
if output is not None and isinstance(output, str):
|
|
56
|
+
result.output = f"{output}\n[whitelist] {note}"
|
|
57
|
+
return result
|
|
58
|
+
if isinstance(result, str):
|
|
59
|
+
return f"{result}\n[whitelist] {note}"
|
|
60
|
+
# Unknown type — return unchanged (best-effort; note is informational).
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CommandReviewMiddleware(AgentMiddleware):
|
|
65
|
+
"""Inspects shell commands and network calls against a CommandPolicy.
|
|
66
|
+
|
|
67
|
+
Sibling to PermissionMiddleware. Both run in wrap_tool_call; this one is
|
|
68
|
+
content-focused (regex on the command string), the other is tier-focused
|
|
69
|
+
(plan/confirm/full mode). Order: PermissionMiddleware first (tier gate),
|
|
70
|
+
then this (content gate) — so we only inspect commands that already passed
|
|
71
|
+
the tier check. But the content rules are mode-independent: even FULL mode
|
|
72
|
+
blocks ``rm -rf /``.
|
|
73
|
+
|
|
74
|
+
Optional ``gate`` reference: if provided, whitelist misses use gate.mode to
|
|
75
|
+
decide behavior (FULL allows, other modes get a hint appended to result).
|
|
76
|
+
If None (no gate), whitelist misses are logged but not blocked (the policy
|
|
77
|
+
can't enforce confirmation without the gate).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, policy, gate=None) -> None:
|
|
81
|
+
self.policy = policy
|
|
82
|
+
self.gate = gate
|
|
83
|
+
|
|
84
|
+
def wrap_tool_call(self, request, handler):
|
|
85
|
+
"""Inspect execute/web commands; block if the policy says no."""
|
|
86
|
+
tc = getattr(request, "tool_call", None) or {}
|
|
87
|
+
name = tc.get("name", "")
|
|
88
|
+
args = dict(tc.get("args", {}) or {})
|
|
89
|
+
tool_call_id = tc.get("id", "")
|
|
90
|
+
|
|
91
|
+
if name == _SHELL_TOOL:
|
|
92
|
+
command = str(args.get("command", ""))
|
|
93
|
+
# Layer 1: blacklist (hard block, always active).
|
|
94
|
+
violation = self.policy.check_command(command)
|
|
95
|
+
if violation:
|
|
96
|
+
# Surface the reason so the model understands WHY and can
|
|
97
|
+
# reformulate — e.g. "rm -rf /build" is fine, "rm -rf /" is not.
|
|
98
|
+
return ToolMessage(
|
|
99
|
+
content=f"Blocked by command policy: {violation}. "
|
|
100
|
+
"Reformulate the command to avoid the destructive pattern "
|
|
101
|
+
"(e.g. target a specific subdirectory, not root/home).",
|
|
102
|
+
tool_call_id=tool_call_id,
|
|
103
|
+
name=name,
|
|
104
|
+
)
|
|
105
|
+
# Layer 2: whitelist (soft — degrade based on gate mode, not hard block).
|
|
106
|
+
# Unlike the blacklist (which hard-blocks), a whitelist miss is enforced
|
|
107
|
+
# by the gate's tier semantics:
|
|
108
|
+
# - PLAN mode: execute is already blocked by PermissionMiddleware, so
|
|
109
|
+
# we won't reach here in production. But if we do (e.g. gate=None),
|
|
110
|
+
# we hard-block to stay safe.
|
|
111
|
+
# - FULL mode: explicit trust — allow without annotation.
|
|
112
|
+
# - CONFIRM/AUTO_EDIT: let the tool run (PermissionMiddleware already
|
|
113
|
+
# prompted the user), but APPEND the whitelist hint to the result so
|
|
114
|
+
# the model/user sees WHY the command was flagged.
|
|
115
|
+
whitelist_miss = self.policy.check_whitelist(command)
|
|
116
|
+
if whitelist_miss:
|
|
117
|
+
mode = getattr(self.gate, "mode", "confirm") if self.gate is not None else "confirm"
|
|
118
|
+
if mode == "plan":
|
|
119
|
+
return ToolMessage(
|
|
120
|
+
content=f"Blocked by whitelist policy (plan mode): {whitelist_miss}",
|
|
121
|
+
tool_call_id=tool_call_id,
|
|
122
|
+
name=name,
|
|
123
|
+
)
|
|
124
|
+
if mode != "full":
|
|
125
|
+
# Non-FULL, non-PLAN: annotate the result. Let the tool execute
|
|
126
|
+
# (the user was already prompted by PermissionMiddleware), but
|
|
127
|
+
# surface the whitelist note in the result content so the model
|
|
128
|
+
# understands the command was outside the trusted set.
|
|
129
|
+
result = handler(request)
|
|
130
|
+
return _augment_with_whitelist_note(result, whitelist_miss)
|
|
131
|
+
# FULL mode: fall through to the normal handler(request) below.
|
|
132
|
+
elif name in _NETWORK_TOOLS and not self.policy.network_allowed:
|
|
133
|
+
return ToolMessage(
|
|
134
|
+
content="Blocked: network access is disabled (network_allowed=false "
|
|
135
|
+
"in config). web_fetch/web_search are unavailable in this session.",
|
|
136
|
+
tool_call_id=tool_call_id,
|
|
137
|
+
name=name,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return handler(request)
|