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
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,5 @@
1
+ from coderio.agent.prompts import build_system_prompt
2
+ from coderio.agent.skill_tool import ActivateSkillTool
3
+ from coderio.agent.stream import NullStream, StreamHandler
4
+
5
+ __all__ = ["build_system_prompt", "ActivateSkillTool", "StreamHandler", "NullStream"]
@@ -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)