runspace-agent 0.1.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 (34) hide show
  1. runspace_agent/__init__.py +30 -0
  2. runspace_agent/_docker/Dockerfile +38 -0
  3. runspace_agent/agents/__init__.py +61 -0
  4. runspace_agent/agents/base.py +132 -0
  5. runspace_agent/agents/claude_code/__init__.py +13 -0
  6. runspace_agent/agents/claude_code/agent.py +171 -0
  7. runspace_agent/agents/claude_code/defaults.py +38 -0
  8. runspace_agent/agents/claude_code/env.py +42 -0
  9. runspace_agent/agents/claude_code/options.py +58 -0
  10. runspace_agent/agents/claude_code/serializer.py +109 -0
  11. runspace_agent/cli.py +235 -0
  12. runspace_agent/container.py +209 -0
  13. runspace_agent/core.py +187 -0
  14. runspace_agent/entrypoint.py +86 -0
  15. runspace_agent/local.py +124 -0
  16. runspace_agent/prompt.py +121 -0
  17. runspace_agent/sandbox.py +124 -0
  18. runspace_agent/server/__init__.py +1 -0
  19. runspace_agent/server/app.py +612 -0
  20. runspace_agent/server/models.py +93 -0
  21. runspace_agent/server/session_manager.py +249 -0
  22. runspace_agent/server/static/assets/index-BloqQ5R_.js +40 -0
  23. runspace_agent/server/static/assets/index-C5Az2FQF.css +2 -0
  24. runspace_agent/server/static/favicon.svg +1 -0
  25. runspace_agent/server/static/icons.svg +24 -0
  26. runspace_agent/server/static/index.html +14 -0
  27. runspace_agent/skills.py +134 -0
  28. runspace_agent/workspaces.py +80 -0
  29. runspace_agent-0.1.0.dist-info/METADATA +819 -0
  30. runspace_agent-0.1.0.dist-info/RECORD +34 -0
  31. runspace_agent-0.1.0.dist-info/WHEEL +5 -0
  32. runspace_agent-0.1.0.dist-info/entry_points.txt +2 -0
  33. runspace_agent-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
  34. runspace_agent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ """runspace_agent — Sandboxed execution environment for AI agents.
2
+
3
+ Provides a simple interface for running AI agents that operate on
4
+ filesystem directories: an **editable** directory (the agent's output)
5
+ and a **read-only context** directory (traces, domain knowledge, etc.).
6
+
7
+ Quick start::
8
+
9
+ from runspace_agent import RunspaceSession, run_agent
10
+
11
+ result = await run_agent(RunspaceSession(
12
+ editable_dir=Path("./my_skill"),
13
+ context_dir=Path("./context"),
14
+ prompt="Improve the skill based on the traces.",
15
+ ))
16
+ """
17
+
18
+ from runspace_agent.agents.base import AgentResult, FilesystemAgent, Workspace
19
+ from runspace_agent.core import RunspaceResult, RunspaceSession, run_agent
20
+ from runspace_agent.skills import Skill
21
+
22
+ __all__ = [
23
+ "AgentResult",
24
+ "FilesystemAgent",
25
+ "RunspaceResult",
26
+ "RunspaceSession",
27
+ "Skill",
28
+ "Workspace",
29
+ "run_agent",
30
+ ]
@@ -0,0 +1,38 @@
1
+ # Build recipe for the runspace-agent:latest image.
2
+ #
3
+ # This Dockerfile is shipped inside the wheel (declared as package-data) so it
4
+ # travels with any install — editable, git, or wheel. It is NOT meant to be built
5
+ # from a repo checkout with `docker build .`; the build context is assembled at
6
+ # runtime by runspace_agent.cli (_prepare_build_context), which lays out:
7
+ #
8
+ # ./requirements.txt runtime deps (base + the [claude] extra)
9
+ # ./runspace_agent/ the installed package source tree
10
+ #
11
+ # The image is built automatically by `runspace-srv` (or `--rebuild`).
12
+ FROM python:3.11-slim
13
+
14
+ # System deps + Node.js (required for the Claude Code runtime)
15
+ RUN apt-get update && \
16
+ apt-get install -y --no-install-recommends curl jq && \
17
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
18
+ apt-get install -y --no-install-recommends nodejs && \
19
+ apt-get clean && rm -rf /var/lib/apt/lists/*
20
+
21
+ # Install Claude Code CLI globally
22
+ RUN npm install -g @anthropic-ai/claude-code
23
+
24
+ # Install Python deps, then drop in the package source (PYTHONPATH instead of a
25
+ # pip install, so no pyproject.toml is needed in the build context)
26
+ WORKDIR /app
27
+ COPY requirements.txt .
28
+ RUN pip install --no-cache-dir -r requirements.txt
29
+ COPY runspace_agent/ ./runspace_agent/
30
+ ENV PYTHONPATH=/app
31
+
32
+ # Create non-root user (Claude Code refuses --dangerously-skip-permissions as root)
33
+ RUN useradd -m -s /bin/bash agent && \
34
+ mkdir -p /workspace && chown agent:agent /workspace
35
+
36
+ USER agent
37
+ WORKDIR /workspace
38
+ ENTRYPOINT ["python", "-m", "runspace_agent.entrypoint"]
@@ -0,0 +1,61 @@
1
+ """Agent abstractions and implementations for runspace_agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from typing import Any
7
+
8
+ from runspace_agent.agents.base import AgentResult, FilesystemAgent, Workspace
9
+
10
+ __all__ = [
11
+ "AgentResult",
12
+ "FilesystemAgent",
13
+ "Workspace",
14
+ "build_agent_options",
15
+ "create_agent",
16
+ "create_default_agent",
17
+ ]
18
+
19
+ _AGENT_REGISTRY: dict[str, str] = {
20
+ "claude-code": "runspace_agent.agents.claude_code",
21
+ }
22
+
23
+
24
+ def _get_agent_module(agent_type: str) -> Any:
25
+ module_path = _AGENT_REGISTRY.get(agent_type)
26
+ if module_path is None:
27
+ available = ", ".join(sorted(_AGENT_REGISTRY))
28
+ raise ValueError(f"Unknown agent_type: {agent_type!r}. Available: {available}")
29
+ return importlib.import_module(module_path)
30
+
31
+
32
+ def build_agent_options(
33
+ agent_type: str = "claude-code",
34
+ agent_settings: dict[str, Any] | None = None,
35
+ ) -> Any:
36
+ """Build agent-specific options from a freeform settings dict.
37
+
38
+ Each agent's ``build_options`` receives the full dict and reads
39
+ only the keys it understands.
40
+ """
41
+ mod = _get_agent_module(agent_type)
42
+ return mod.build_options(agent_settings)
43
+
44
+
45
+ def create_agent(
46
+ agent_type: str = "claude-code",
47
+ options: Any = None,
48
+ ) -> FilesystemAgent:
49
+ """Create an agent instance of the specified type."""
50
+ mod = _get_agent_module(agent_type)
51
+ return mod.create(options)
52
+
53
+
54
+ def create_default_agent(
55
+ options: Any = None,
56
+ ) -> FilesystemAgent:
57
+ """Create the default agent (currently :class:`ClaudeCodeAgent`).
58
+
59
+ Backward-compatible wrapper around :func:`create_agent`.
60
+ """
61
+ return create_agent(agent_type="claude-code", options=options)
@@ -0,0 +1,132 @@
1
+ """Base types for the FilesystemAgent abstraction.
2
+
3
+ Defines the Protocol that any agent implementation must satisfy,
4
+ plus the Workspace and AgentResult types used across all backends.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any, Protocol, runtime_checkable
12
+
13
+
14
+ @dataclass
15
+ class Workspace:
16
+ """The sandboxed view that any FilesystemAgent receives.
17
+
18
+ Attributes:
19
+ editable_dir: Directory the agent should focus on modifying.
20
+ context_dir: Directory with read-only reference material
21
+ (traces, domain knowledge, performance history, etc.).
22
+ prompt: Fully constructed prompt including directory descriptions.
23
+ skills_dir: Path to the agent-specific skills directory, if loaded.
24
+ cwd: Session root directory (the agent's working directory).
25
+ """
26
+
27
+ editable_dir: Path
28
+ context_dir: Path
29
+ prompt: str
30
+ skills_dir: Path | None
31
+ cwd: Path
32
+ hooks: dict[str, list[Any]] | None = None
33
+
34
+
35
+ @dataclass
36
+ class AgentResult:
37
+ """Result returned by a FilesystemAgent after execution.
38
+
39
+ Attributes:
40
+ success: Whether the agent completed without errors.
41
+ messages: Raw messages from the agent execution (SDK-specific).
42
+ conversation: Serialized conversation as a list of JSON-ready dicts.
43
+ Each agent implementation is responsible for populating this
44
+ from its SDK-specific message types.
45
+ total_tokens: Approximate total tokens consumed.
46
+ duration_ms: Wall-clock execution time in milliseconds.
47
+ error: Error message if the agent failed, None otherwise.
48
+ """
49
+
50
+ success: bool
51
+ messages: list[Any] = field(default_factory=list)
52
+ conversation: list[dict[str, Any]] = field(default_factory=list)
53
+ total_tokens: int = 0
54
+ total_cost_usd: float | None = None
55
+ duration_ms: int = 0
56
+ error: str | None = None
57
+
58
+
59
+ @runtime_checkable
60
+ class FilesystemAgent(Protocol):
61
+ """Protocol for agents that operate on filesystem directories.
62
+
63
+ Any class with a ``skills_folder_name`` attribute and an async ``run``
64
+ method matching this signature satisfies the protocol. This enables
65
+ pluggable agent backends (Claude Code, OpenCode, custom, ...) without
66
+ requiring inheritance.
67
+
68
+ Attributes:
69
+ skills_folder_name: Relative path from ``cwd`` where this agent
70
+ expects skills to be placed. For example ``".claude/skills"``
71
+ for Claude Code or ``".opencode/skills"`` for OpenCode.
72
+ Used to determine where to copy user-provided skills and default skills.
73
+ default_skills_dir: Absolute path to the directory containing the
74
+ agent's bundled/preinstalled skills on disk. Each subdirectory
75
+ is a separate skill. ``None`` means the agent ships no default
76
+ skills.
77
+
78
+ Adding a new agent type
79
+ -----------------------
80
+ 1. Create ``agents/<name>/`` with three files:
81
+
82
+ ``agent.py`` — a class satisfying this Protocol::
83
+
84
+ class MyAgent:
85
+ skills_folder_name: str = ".<name>/skills"
86
+ default_skills_dir: Path | None = ...
87
+
88
+ def __init__(self, *, options: MyAgentOptions | None = None): ...
89
+ async def run(self, workspace: Workspace) -> AgentResult: ...
90
+
91
+ ``options.py`` — two module-level functions the registry calls::
92
+
93
+ def build_options(
94
+ agent_settings: dict | None = None,
95
+ ) -> MyAgentOptions:
96
+ '''Build agent-specific options from the settings dict.
97
+ Read only the keys your agent understands.'''
98
+ ...
99
+
100
+ def create(options: Any = None) -> MyAgent:
101
+ '''Instantiate the agent.'''
102
+ ...
103
+
104
+ ``__init__.py`` — re-export everything::
105
+
106
+ from agents.<name>.agent import MyAgent
107
+ from agents.<name>.options import build_options, create
108
+
109
+ 2. Register the agent in ``agents/__init__.py``::
110
+
111
+ _AGENT_REGISTRY: dict[str, str] = {
112
+ "claude-code": "runspace_agent.agents.claude_code",
113
+ "<name>": "runspace_agent.agents.<name>", # add this
114
+ }
115
+
116
+ That's it. The server, container, and entrypoint all resolve the agent
117
+ through the registry, so no changes are needed outside ``agents/``.
118
+ Clients select the agent by passing ``"agent_type": "<name>"`` in the
119
+ ``POST /run`` request body (defaults to ``"claude-code"``).
120
+ """
121
+
122
+ skills_folder_name: str
123
+ default_skills_dir: Path | None
124
+
125
+ async def run(self, workspace: Workspace) -> AgentResult:
126
+ """Execute the agent inside the given workspace.
127
+
128
+ The agent should read from ``workspace.context_dir``, modify files
129
+ in ``workspace.editable_dir``, and follow the instructions in
130
+ ``workspace.prompt``.
131
+ """
132
+ ...
@@ -0,0 +1,13 @@
1
+ """Claude Code agent implementation."""
2
+
3
+ from runspace_agent.agents.claude_code.agent import ClaudeCodeAgent
4
+ from runspace_agent.agents.claude_code.env import ClaudeModel, build_claude_env
5
+ from runspace_agent.agents.claude_code.options import build_options, create
6
+
7
+ __all__ = [
8
+ "ClaudeCodeAgent",
9
+ "ClaudeModel",
10
+ "build_claude_env",
11
+ "build_options",
12
+ "create",
13
+ ]
@@ -0,0 +1,171 @@
1
+ """ClaudeCodeAgent -- FilesystemAgent implementation using the Claude Agent SDK.
2
+
3
+ Requires the optional ``claude-code-sdk`` dependency::
4
+
5
+ uv pip install runspace-agent[claude]
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ import time
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from runspace_agent.agents.base import AgentResult, Workspace
16
+ from runspace_agent.agents.claude_code.defaults import (
17
+ DEFAULT_ALLOWED_TOOLS,
18
+ DEFAULT_DISALLOWED_TOOLS,
19
+ DEFAULT_MAX_TURNS,
20
+ DEFAULT_SYSTEM_PROMPT,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from claude_code_sdk import ClaudeCodeOptions
25
+
26
+
27
+ class ClaudeCodeAgent:
28
+ """Run a Claude Code agent inside a :class:`Workspace`.
29
+
30
+ This is the built-in :class:`~runspace_agent.agents.base.FilesystemAgent`
31
+ implementation backed by the Claude Agent SDK.
32
+
33
+ Users pass a :class:`~claude_code_sdk.ClaudeCodeOptions` object to
34
+ configure the agent. The agent **enforces** the following fields
35
+ regardless of what the user sets:
36
+
37
+ * ``permission_mode`` → ``"bypassPermissions"`` (headless container)
38
+ * ``cwd`` → from the workspace (sandbox boundary)
39
+ * ``system_prompt`` → headless system prompt (no interactive prompts)
40
+ * ``disallowed_tools`` → interactive/scheduling tools disabled for headless
41
+ * ``hooks`` → from the workspace (sandbox enforcement)
42
+
43
+ If the user does not set ``allowed_tools`` or ``max_turns``, sensible
44
+ defaults are applied.
45
+
46
+ Parameters:
47
+ options: A :class:`~claude_code_sdk.ClaudeCodeOptions` instance.
48
+ When ``None``, a bare default is created at run time.
49
+ """
50
+
51
+ skills_folder_name: str = ".claude/skills"
52
+
53
+ # Repo-root-relative path to bundled skills. Computed once at module load.
54
+ _DEFAULT_SKILLS_DIR: Path = (
55
+ Path(__file__).resolve().parent.parent.parent.parent.parent / ".claude" / "skills"
56
+ )
57
+
58
+ def __init__(self, options: ClaudeCodeOptions | None = None) -> None:
59
+ self._user_options = options
60
+ self.default_skills_dir: Path | None = self._DEFAULT_SKILLS_DIR
61
+
62
+ async def run(self, workspace: Workspace) -> AgentResult:
63
+ """Execute the Claude Code agent inside *workspace*."""
64
+ query, ClaudeCodeOptions = self._import_sdk()
65
+ options = self._build_effective_options(ClaudeCodeOptions, workspace)
66
+ return await self._run_agent(query, options, workspace)
67
+
68
+ def _build_effective_options(
69
+ self,
70
+ ClaudeCodeOptions: type,
71
+ workspace: Workspace,
72
+ ) -> Any:
73
+ """Merge user options with enforced sandbox overrides.
74
+
75
+ Priority (highest to lowest):
76
+ 1. Enforced fields (always override, non-negotiable)
77
+ 2. User-provided fields (from self._user_options)
78
+ 3. Sensible defaults (only if user left field at dataclass default)
79
+ """
80
+ base = self._user_options if self._user_options is not None else ClaudeCodeOptions()
81
+
82
+ overrides: dict[str, Any] = {}
83
+
84
+ # Defaults: fill in only if user did not set them
85
+ if not base.allowed_tools:
86
+ overrides["allowed_tools"] = list(DEFAULT_ALLOWED_TOOLS)
87
+
88
+ if base.max_turns is None:
89
+ overrides["max_turns"] = DEFAULT_MAX_TURNS
90
+
91
+ # Enforced fields: always override regardless of user input
92
+ overrides["permission_mode"] = "bypassPermissions"
93
+ overrides["cwd"] = str(workspace.cwd)
94
+ overrides["system_prompt"] = DEFAULT_SYSTEM_PROMPT
95
+ overrides["disallowed_tools"] = list(DEFAULT_DISALLOWED_TOOLS)
96
+
97
+ if workspace.hooks:
98
+ overrides["hooks"] = self._build_sdk_hooks(workspace.hooks)
99
+
100
+ return dataclasses.replace(base, **overrides)
101
+
102
+ async def _run_agent(
103
+ self,
104
+ query: Any,
105
+ options: Any,
106
+ workspace: Workspace,
107
+ ) -> AgentResult:
108
+ """Iterate the agent loop with pre-built options."""
109
+ messages: list[Any] = []
110
+ total_tokens = 0
111
+ total_cost_usd: float | None = None
112
+ start_ms = int(time.time() * 1000)
113
+
114
+ async for message in query(
115
+ prompt=workspace.prompt,
116
+ options=options,
117
+ ):
118
+ messages.append(message)
119
+
120
+ if type(message).__name__ == "ResultMessage":
121
+ usage = getattr(message, "usage", None)
122
+ if isinstance(usage, dict):
123
+ total_tokens += usage.get("input_tokens", 0)
124
+ total_tokens += usage.get("output_tokens", 0)
125
+ cost = getattr(message, "total_cost_usd", None)
126
+ if cost is not None:
127
+ total_cost_usd = cost
128
+
129
+ from runspace_agent.agents.claude_code.serializer import serialize_messages
130
+
131
+ duration_ms = int(time.time() * 1000) - start_ms
132
+ return AgentResult(
133
+ success=True,
134
+ messages=messages,
135
+ conversation=serialize_messages(messages),
136
+ total_tokens=total_tokens,
137
+ total_cost_usd=total_cost_usd,
138
+ duration_ms=duration_ms,
139
+ )
140
+
141
+ @staticmethod
142
+ def _build_sdk_hooks(hooks: dict[str, list[Any]]) -> dict[str, list[Any]]:
143
+ """Convert intermediate hooks format to SDK HookMatcher objects."""
144
+ from claude_code_sdk.types import HookMatcher
145
+
146
+ sdk_hooks: dict[str, list[Any]] = {}
147
+ for event_name, matchers in hooks.items():
148
+ sdk_hooks[event_name] = [
149
+ HookMatcher(
150
+ matcher=m["matcher"],
151
+ hooks=[m["hook_fn"]],
152
+ )
153
+ for m in matchers
154
+ ]
155
+ return sdk_hooks
156
+
157
+ @staticmethod
158
+ def _import_sdk() -> tuple[Any, Any]:
159
+ """Lazily import the Claude Agent SDK.
160
+
161
+ Raises :class:`ImportError` with a helpful message when the
162
+ optional dependency is missing.
163
+ """
164
+ try:
165
+ from claude_code_sdk import ClaudeCodeOptions, query
166
+ except ImportError:
167
+ raise ImportError(
168
+ "ClaudeCodeAgent requires the 'claude-code-sdk' package. "
169
+ "Install it with: uv pip install claude-code-sdk"
170
+ ) from None
171
+ return query, ClaudeCodeOptions
@@ -0,0 +1,38 @@
1
+ """Default constants for the ClaudeCodeAgent."""
2
+
3
+ DEFAULT_MAX_TURNS: int = 300
4
+
5
+ DEFAULT_ALLOWED_TOOLS: list[str] = [
6
+ "Read",
7
+ "Write",
8
+ "Edit",
9
+ "Bash",
10
+ "Glob",
11
+ "Grep",
12
+ "Skill",
13
+ "WebSearch",
14
+ "WebFetch",
15
+ "Agent",
16
+ "LSP",
17
+ ]
18
+
19
+ DEFAULT_DISALLOWED_TOOLS: list[str] = [
20
+ "AskUserQuestion",
21
+ "EnterPlanMode",
22
+ "ExitPlanMode",
23
+ "EnterWorktree",
24
+ "ExitWorktree",
25
+ "ScheduleWakeup",
26
+ "CronCreate",
27
+ "CronDelete",
28
+ "CronList",
29
+ "Monitor",
30
+ ]
31
+
32
+ DEFAULT_SYSTEM_PROMPT: str = (
33
+ "You are running headless in an automated pipeline. "
34
+ "There is no human available to answer questions. "
35
+ "Do NOT use AskHumanQuestion or any interactive prompts. "
36
+ "Complete the task fully and autonomously. "
37
+ "If you encounter ambiguity, make a reasonable decision and proceed."
38
+ )
@@ -0,0 +1,42 @@
1
+ """Helper for building Claude Code environment variables from the current shell.
2
+
3
+ This is a convenience for the examples and manual tests in this repo — it reads
4
+ your real ``os.environ`` to assemble the env dict passed to Claude Code. Library
5
+ users typically don't need it: they call ``POST /run`` and pass their own
6
+ credentials explicitly via ``agent_settings.env`` (or ``ClaudeCodeOptions.env``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from enum import StrEnum
13
+
14
+
15
+ class ClaudeModel(StrEnum):
16
+ OPUS_4_8 = "claude-opus-4-8"
17
+ SONNET_4_6 = "claude-sonnet-4-6"
18
+ HAIKU_4_5 = "claude-haiku-4-5"
19
+
20
+
21
+ def build_claude_env(model: ClaudeModel | None = None) -> dict[str, str]:
22
+ """Build Claude Code env vars from the current environment.
23
+
24
+ Reads credentials/config from ``os.environ`` (with a couple of common
25
+ fallbacks). Callers typically drop empty values before use, e.g.
26
+ ``{k: v for k, v in build_claude_env().items() if v}``.
27
+ """
28
+ model_id = (
29
+ model.value if model else os.environ.get("ANTHROPIC_MODEL", "claude-opus-4-8")
30
+ )
31
+ return {
32
+ "ANTHROPIC_API_KEY": os.environ.get("ANTHROPIC_API_KEY", ""),
33
+ "ANTHROPIC_BASE_URL": os.environ.get("ANTHROPIC_BASE_URL", "")
34
+ or os.environ.get("CLAUDE_CODE_LITELLM_BASE_URL", "")
35
+ or os.environ.get("IBM_THIRD_PARTY_API_BASE", ""),
36
+ "ANTHROPIC_AUTH_TOKEN": os.environ.get("ANTHROPIC_AUTH_TOKEN", "")
37
+ or os.environ.get("IBM_THIRD_PARTY_API_KEY", ""),
38
+ "ANTHROPIC_MODEL": model_id,
39
+ "CLAUDE_CODE_SUBAGENT_MODEL": model_id,
40
+ "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1",
41
+ "opusPlanEnabled": "true",
42
+ }
@@ -0,0 +1,58 @@
1
+ """Claude Code agent options builder and factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from claude_code_sdk import ClaudeCodeOptions
9
+
10
+ from runspace_agent.agents.claude_code.agent import ClaudeCodeAgent
11
+
12
+
13
+ def build_options(
14
+ agent_settings: dict[str, Any] | None = None,
15
+ ) -> ClaudeCodeOptions:
16
+ """Build a :class:`ClaudeCodeOptions` from a settings dict.
17
+
18
+ The dict may contain any of the following keys (all optional):
19
+
20
+ - ``env``: dict of environment variables
21
+ - ``model``: Claude model name
22
+ - ``permissions.allow`` / ``permissions.disallow``: tool lists
23
+ - ``max_turns``: maximum conversation turns (default 300)
24
+ - ``mcp_servers``: MCP server configuration
25
+ """
26
+ from claude_code_sdk import ClaudeCodeOptions
27
+
28
+ settings = agent_settings or {}
29
+ kwargs: dict[str, Any] = {}
30
+
31
+ env_vars = {k: str(v) for k, v in settings.get("env", {}).items()}
32
+ if env_vars:
33
+ kwargs["env"] = env_vars
34
+
35
+ model = settings.get("model")
36
+ if model:
37
+ kwargs["model"] = model
38
+
39
+ permissions = settings.get("permissions", {})
40
+ if permissions.get("allow"):
41
+ kwargs["allowed_tools"] = list(permissions["allow"])
42
+ if permissions.get("disallow"):
43
+ kwargs["disallowed_tools"] = list(permissions["disallow"])
44
+
45
+ kwargs["max_turns"] = settings.get("max_turns", 300)
46
+
47
+ mcp_servers = settings.get("mcp_servers")
48
+ if mcp_servers:
49
+ kwargs["mcp_servers"] = mcp_servers
50
+
51
+ return ClaudeCodeOptions(**kwargs)
52
+
53
+
54
+ def create(options: Any = None) -> ClaudeCodeAgent:
55
+ """Create a :class:`ClaudeCodeAgent` instance."""
56
+ from runspace_agent.agents.claude_code.agent import ClaudeCodeAgent
57
+
58
+ return ClaudeCodeAgent(options=options)