monkeybot-cli 0.2.1__tar.gz → 0.3.0__tar.gz

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 (78) hide show
  1. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/.gitignore +1 -3
  2. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/PKG-INFO +3 -2
  3. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/pyproject.toml +4 -2
  4. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/skills/monkeybot/SKILL.md +3 -1
  5. monkeybot_cli-0.3.0/src/monkeybot_cli/__init__.py +8 -0
  6. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/chat.py +8 -1
  7. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/doctor.py +111 -11
  8. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/run_cmd.py +2 -3
  9. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/validate.py +2 -1
  10. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/compat.py +1 -1
  11. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/config_resolve.py +6 -12
  12. monkeybot_cli-0.3.0/src/monkeybot_cli/gateway_health.py +71 -0
  13. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold.py +27 -51
  14. monkeybot_cli-0.3.0/src/monkeybot_cli/scaffold_defaults/Dockerfile +27 -0
  15. monkeybot_cli-0.3.0/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +15 -0
  16. monkeybot_cli-0.3.0/src/monkeybot_cli/scaffold_defaults/dockerignore +5 -0
  17. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/env.example +1 -0
  18. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +3 -6
  19. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +5 -2
  20. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_errors.py +1 -0
  21. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_cli.py +64 -7
  22. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_config_resolve.py +12 -1
  23. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_extras_catalog.py +1 -1
  24. monkeybot_cli-0.3.0/tests/test_gateway_health.py +135 -0
  25. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_opensandbox_lifecycle.py +2 -2
  26. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_run_cmd.py +3 -2
  27. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_scaffold.py +15 -10
  28. monkeybot_cli-0.2.1/src/monkeybot_cli/__init__.py +0 -3
  29. monkeybot_cli-0.2.1/src/monkeybot_cli/gateway_health.py +0 -34
  30. monkeybot_cli-0.2.1/src/monkeybot_cli/scaffold_defaults/setup-workspace.sh +0 -24
  31. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/skills/monkeybot/references/config-sections.md +0 -0
  32. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_renderer.py +0 -0
  33. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_session.py +0 -0
  34. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
  35. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_theme.py +0 -0
  36. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_tool_display.py +0 -0
  37. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_tui.py +0 -0
  38. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
  39. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/__init__.py +0 -0
  40. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/loop.py +0 -0
  41. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/new.py +0 -0
  42. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/talk.py +0 -0
  43. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/exit_commands.py +0 -0
  44. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/extras_catalog.py +0 -0
  45. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/main.py +0 -0
  46. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/opensandbox_lifecycle.py +0 -0
  47. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/output.py +0 -0
  48. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/providers.py +0 -0
  49. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
  50. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
  51. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/client.py +0 -0
  52. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
  53. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
  54. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/session.py +0 -0
  55. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/session_controller.py +0 -0
  56. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
  57. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
  58. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/runtime_python.py +0 -0
  59. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
  60. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
  61. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
  62. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
  63. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
  64. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/session_controller.py +0 -0
  65. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
  66. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_e2e.py +0 -0
  67. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_grounding.py +0 -0
  68. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_renderer_parity.py +0 -0
  69. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_session.py +0 -0
  70. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_status_bar.py +0 -0
  71. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_theme.py +0 -0
  72. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_tool_display.py +0 -0
  73. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_tui.py +0 -0
  74. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_doctor.py +0 -0
  75. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_realtime_session_controller.py +0 -0
  76. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_runtime_python.py +0 -0
  77. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_terminal_markdown.py +0 -0
  78. {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/uv.lock +0 -0
@@ -9,9 +9,6 @@ internal/
9
9
  # Local scaffolded harness config (defaults ship in monkeybot-cli scaffold_defaults/)
10
10
  /monkeybot_config/
11
11
 
12
- # Demo agent live config (materialized from monkeybot_config_example/ by demo_agent/run.sh)
13
- demo_agent/monkeybot_config/
14
-
15
12
  # Live eval run artifacts (only evals/baselines/ is versioned)
16
13
  /evals/runs/
17
14
 
@@ -40,6 +37,7 @@ data/
40
37
  htmlcov/
41
38
  .mypy_cache/
42
39
  .ruff_cache/
40
+ .deepeval/
43
41
 
44
42
  # macOS
45
43
  .DS_Store
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeybot-cli
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: CLI to create, configure, validate, and chat with monkeybot agents.
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
7
- Requires-Dist: monkeybot[cli]<3,>=2.1.0
7
+ Requires-Dist: monkeybot-browser-mcp<1,>=0.2.0
8
+ Requires-Dist: monkeybot[cli]<3,>=2.2.0
8
9
  Requires-Dist: python-dotenv>=1.0.0
9
10
  Requires-Dist: pyyaml>=6.0.2
10
11
  Requires-Dist: textual>=8.2.8
@@ -4,13 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "monkeybot-cli"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "CLI to create, configure, validate, and chat with monkeybot agents."
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
11
11
  # `cli` pulls realtime + typer so `monkeybot talk` works out of the box (text mode).
12
12
  # Audio mic/PTT needs the agent (or this env) to also install monkeybot[cli-realtime].
13
- "monkeybot[cli]>=2.1.0,<3",
13
+ "monkeybot[cli]>=2.2.0,<3",
14
+ "monkeybot-browser-mcp>=0.2.0,<1",
14
15
  "httpx>=0.27.0",
15
16
  "pyyaml>=6.0.2",
16
17
  "python-dotenv>=1.0.0",
@@ -22,6 +23,7 @@ monkeybot = "monkeybot_cli.main:main"
22
23
 
23
24
  [tool.uv.sources]
24
25
  monkeybot = { path = "..", editable = true }
26
+ monkeybot-browser-mcp = { path = "../integrations/browser-mcp", editable = true }
25
27
 
26
28
  [dependency-groups]
27
29
  dev = [
@@ -85,7 +85,9 @@ monkeybot new --dest /path/to/bot --provider openai --yes
85
85
  # monkeybot new --dest /path/to/bot --provider openai --with postgres,sandbox --yes
86
86
  ```
87
87
 
88
- Creates `monkeybot_config/`, `workspace/` (file-tool sandbox), `workspace/skills` → `skills/`, `data/memory/`, `.env.example`, `scripts/setup-workspace.sh`, and an agent `pyproject.toml`. Use `--force` only when overwriting is explicitly requested.
88
+ Creates `monkeybot_config/`, read-only `skills/`, writable `workspace/`,
89
+ `data/memory/`, `.env.example`, a Dockerfile, and an agent `pyproject.toml`.
90
+ Use `--force` only when overwriting is explicitly requested.
89
91
 
90
92
  Then:
91
93
 
@@ -0,0 +1,8 @@
1
+ """monkeybot setup CLI — create, configure, validate, and chat with agents."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("monkeybot-cli")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0-dev"
@@ -39,7 +39,10 @@ from monkeybot_cli.config_resolve import (
39
39
  resolve_agent_root,
40
40
  resolve_config,
41
41
  )
42
- from monkeybot_cli.gateway_health import wait_for_health as _wait_for_health
42
+ from monkeybot_cli.gateway_health import (
43
+ occupied_gateway_message as _occupied_gateway_message,
44
+ wait_for_health as _wait_for_health,
45
+ )
43
46
  from monkeybot_cli.opensandbox_lifecycle import (
44
47
  ensure_opensandbox_for_agent,
45
48
  is_sandbox_enabled,
@@ -713,6 +716,10 @@ def run_chat(args: argparse.Namespace) -> int:
713
716
  file=sys.stderr,
714
717
  )
715
718
  port = args.port if args.port else _port_from_config(config_path)
719
+ occupied = _occupied_gateway_message(base, port)
720
+ if occupied is not None:
721
+ print(occupied, file=sys.stderr)
722
+ return 1
716
723
  spawned = _spawn_gateway(config_path, agent_root, port)
717
724
  proc = spawned.proc
718
725
  if not _wait_for_health(base, proc):
@@ -5,34 +5,27 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import os
8
- import socket
8
+ import shlex
9
9
  import subprocess
10
10
  import tomllib
11
11
  from pathlib import Path
12
12
 
13
13
  import httpx
14
14
 
15
+ from monkeybot.core.layout import AgentLayout, bootstrap_agent_layout
16
+ from monkeybot.core.tools.sandbox_executor import SandboxConfig
15
17
  from monkeybot_cli.config_resolve import (
16
18
  load_agent_dotenv,
17
19
  load_config_doc,
18
20
  resolve_agent_root,
19
21
  resolve_config,
20
22
  )
23
+ from monkeybot_cli.gateway_health import port_free as _port_free
21
24
  from monkeybot_cli.output import CommandReport, check
22
25
  from monkeybot_cli.providers import credentials_present, extra_module, spec_for_provider
23
26
  from monkeybot_cli.runtime_python import resolve_runtime_python, run_probe
24
27
 
25
28
 
26
- def _port_free(port: int) -> bool:
27
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
28
- s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
29
- try:
30
- s.bind(("127.0.0.1", port))
31
- return True
32
- except OSError:
33
- return False
34
-
35
-
36
29
  def _runtime_python_version(runtime, agent_root: Path) -> tuple[int, int, int]:
37
30
  """Ask the runtime interpreter for its version (major, minor, micro)."""
38
31
  code = "import sys; print(sys.version_info[0], sys.version_info[1], sys.version_info[2])"
@@ -88,6 +81,105 @@ def _extra_remediation(extra: str, agent_root: Path, runtime) -> str:
88
81
  )
89
82
 
90
83
 
84
+ def _storage_kind(uri: str) -> str:
85
+ """Render storage kind without leaking URL credentials or filesystem details."""
86
+ if "://" not in uri:
87
+ return "unknown"
88
+ scheme, remainder = uri.split("://", 1)
89
+ if scheme == "sqlite":
90
+ return "sqlite"
91
+ host = remainder.split("@")[-1].split("/")[0]
92
+ return f"{scheme}://{host}" if host else scheme
93
+
94
+
95
+ def _add_layout_checks(report: CommandReport, layout: AgentLayout) -> None:
96
+ check(
97
+ report,
98
+ id="layout.resolved",
99
+ category="layout",
100
+ severity="error",
101
+ passed=True,
102
+ message="Canonical agent layout resolved",
103
+ value={
104
+ "agent_root": str(layout.agent_root),
105
+ "workspace": str(layout.workspace_root),
106
+ "skills": str(layout.skills_path),
107
+ "data": str(layout.data_root),
108
+ "db": _storage_kind(layout.db_url),
109
+ "memory": _storage_kind(layout.memory_storage_uri),
110
+ },
111
+ )
112
+
113
+ legacy_skills = layout.workspace_root / "skills"
114
+ populated_skills = layout.skills_path.exists() and any(layout.skills_path.iterdir())
115
+ legacy_exists = legacy_skills.exists() or legacy_skills.is_symlink()
116
+ collision = legacy_exists and populated_skills
117
+ source = str(legacy_skills)
118
+ destination = str(layout.skills_path)
119
+ check(
120
+ report,
121
+ id="layout.legacy_nested_skills",
122
+ category="layout",
123
+ severity="warning",
124
+ passed=not legacy_exists,
125
+ message=(
126
+ "Legacy workspace/skills detected"
127
+ + ("; destination skills/ is populated (resolve collision manually)" if collision else "")
128
+ if legacy_exists
129
+ else "No legacy nested skills directory"
130
+ ),
131
+ remediation=(
132
+ "Collision detected: do not move automatically; reconcile the two skill trees first."
133
+ if collision
134
+ else (
135
+ "Preview only (not executed): "
136
+ f"mv {shlex.quote(source)} {shlex.quote(destination)}"
137
+ )
138
+ if legacy_exists
139
+ else None
140
+ ),
141
+ value=(
142
+ {
143
+ "source": source,
144
+ "destination": destination,
145
+ "collision": collision,
146
+ "action": None if collision else "mv",
147
+ }
148
+ if legacy_exists
149
+ else None
150
+ ),
151
+ )
152
+
153
+ browser_enabled = False
154
+ if layout.mcp_config_path.is_file():
155
+ try:
156
+ servers = json.loads(layout.mcp_config_path.read_text(encoding="utf-8")).get("mcpServers", {})
157
+ browser = servers.get("browser", {}) if isinstance(servers, dict) else {}
158
+ browser_enabled = bool(browser.get("enabled")) if isinstance(browser, dict) else False
159
+ except (OSError, json.JSONDecodeError):
160
+ pass
161
+ check(
162
+ report,
163
+ id="browser.bundled",
164
+ category="layout",
165
+ severity="warning",
166
+ passed=True,
167
+ message="browser: enabled" if browser_enabled else "browser: disabled (bundled)",
168
+ )
169
+
170
+ sandbox = SandboxConfig.from_env()
171
+ sandbox_mode = "remote (compute-only)" if not sandbox.shared_filesystem else "shared-filesystem"
172
+ check(
173
+ report,
174
+ id="sandbox.status",
175
+ category="layout",
176
+ severity="warning",
177
+ passed=True,
178
+ message=f"sandbox: {'enabled' if sandbox.enabled else 'disabled'} ({sandbox_mode})",
179
+ value={"image": sandbox.image, "mode": sandbox_mode},
180
+ )
181
+
182
+
91
183
  def run_doctor(args: argparse.Namespace) -> int:
92
184
  cwd = Path(args.cwd).expanduser().resolve() if args.cwd else None
93
185
  config_path = resolve_config(args.config, cwd=cwd)
@@ -97,6 +189,14 @@ def run_doctor(args: argparse.Namespace) -> int:
97
189
  report.config_path = str(config_path.resolve())
98
190
 
99
191
  agent_root = resolve_agent_root(cwd=cwd, config_path=config_path)
192
+ layout = bootstrap_agent_layout(cwd=agent_root, config_path=config_path)
193
+ _add_layout_checks(report, layout)
194
+ if not args.json:
195
+ print("layout:")
196
+ print(f" agent_root: {layout.agent_root}")
197
+ print(f" workspace: {layout.workspace_root}")
198
+ print(f" skills: {layout.skills_path}")
199
+ print(f" data: {layout.data_root}")
100
200
  runtime = resolve_runtime_python(agent_root)
101
201
 
102
202
  py_version = _runtime_python_version(runtime, agent_root)
@@ -20,9 +20,8 @@ def run_run(args: argparse.Namespace) -> int:
20
20
  env["MONKEYBOT_CONFIG"] = str(config_path)
21
21
  if args.port:
22
22
  env["PORT"] = str(args.port)
23
- # Derive the agent root from --cwd when given, else from --config so an
24
- # off-tree config selects the right project venv / `uv run` cwd — matching
25
- # `monkeybot doctor`. Falls back to cwd when neither is provided.
23
+ # --cwd intentionally pins the subprocess and runtime directory. Without
24
+ # it, an explicit config determines the agent root.
26
25
  agent_root = resolve_agent_root(cwd=cwd, config_path=config_path)
27
26
  runtime = resolve_runtime_python(agent_root)
28
27
  cmd = gateway_argv(runtime)
@@ -20,6 +20,7 @@ from monkeybot.core.config import (
20
20
  from monkeybot.core.config.runtime_env import ENV_MAP
21
21
  from monkeybot.core.config.settings import ConfigError, normalize_model_provider
22
22
  from monkeybot.core.config.yaml_loader import load_monkeybot_yaml_dict
23
+ from monkeybot.core.layout import resolve_agent_root
23
24
 
24
25
  from monkeybot_cli.config_resolve import load_agent_dotenv, resolve_config
25
26
  from monkeybot_cli.output import CommandReport, check
@@ -84,7 +85,7 @@ def run_validate(args: argparse.Namespace) -> int:
84
85
  return report.emit(as_json=args.json)
85
86
 
86
87
  report.config_path = str(config_path.resolve())
87
- base = config_path.parent.parent if config_path.parent.name == "monkeybot_config" else Path.cwd()
88
+ base = resolve_agent_root(config_path=config_path)
88
89
 
89
90
  try:
90
91
  raw = config_path.read_text(encoding="utf-8")
@@ -4,4 +4,4 @@ from __future__ import annotations
4
4
 
5
5
  # Floor matches published ``monkeybot-cli`` → ``monkeybot[cli]`` bound.
6
6
  # Raise together when scaffolding or gateway APIs break compatibility.
7
- COMPATIBLE_CORE_RANGE = ">=2.1.0,<3"
7
+ COMPATIBLE_CORE_RANGE = ">=2.2.0,<3"
@@ -9,18 +9,14 @@ from monkeybot.core.config.yaml_loader import (
9
9
  load_monkeybot_yaml_dict,
10
10
  resolve_monkeybot_config_path,
11
11
  )
12
+ from monkeybot.core.layout import resolve_agent_root as _resolve_agent_root
12
13
 
13
14
 
14
15
  def resolve_agent_root(*, cwd: Path | None = None, config_path: Path | None = None) -> Path:
15
16
  """Return the agent project root (directory containing monkeybot_config/ or .env)."""
16
17
  if cwd is not None:
17
18
  return cwd.expanduser().resolve()
18
- if config_path is not None:
19
- cp = config_path.expanduser().resolve()
20
- if cp.parent.name == "monkeybot_config":
21
- return cp.parent.parent
22
- return cp.parent
23
- return Path.cwd()
19
+ return _resolve_agent_root(config_path=config_path)
24
20
 
25
21
 
26
22
  def load_agent_dotenv(*, cwd: Path | None = None, config_path: Path | None = None) -> Path | None:
@@ -34,10 +30,6 @@ def load_agent_dotenv(*, cwd: Path | None = None, config_path: Path | None = Non
34
30
  if env_file.is_file():
35
31
  load_dotenv(env_file, override=False)
36
32
  return env_file
37
- fallback = Path.cwd() / ".env"
38
- if fallback.is_file() and fallback != env_file:
39
- load_dotenv(fallback, override=False)
40
- return fallback
41
33
  return None
42
34
 
43
35
 
@@ -46,8 +38,10 @@ def resolve_config(explicit: str | None, *, cwd: Path | None = None) -> Path | N
46
38
  if explicit:
47
39
  p = Path(explicit).expanduser()
48
40
  return p.resolve() if p.is_file() else None
49
- resolved_cwd = cwd.expanduser().resolve() if cwd is not None else None
50
- return resolve_monkeybot_config_path(cwd=resolved_cwd)
41
+ if cwd is not None:
42
+ candidate = cwd.expanduser().resolve() / "monkeybot_config" / "monkeybot.yaml"
43
+ return candidate.resolve() if candidate.is_file() else None
44
+ return resolve_monkeybot_config_path()
51
45
 
52
46
 
53
47
  def load_config_doc(config_path: str | Path | None = None) -> tuple[Path | None, dict]:
@@ -0,0 +1,71 @@
1
+ """Gateway health polling helpers shared by chat and talk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import socket
6
+ import subprocess
7
+ import time
8
+
9
+ import httpx
10
+
11
+
12
+ def port_free(port: int, host: str = "127.0.0.1") -> bool:
13
+ """Return True when ``host:port`` can be bound (nothing listening)."""
14
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
15
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
16
+ try:
17
+ s.bind((host, port))
18
+ return True
19
+ except OSError:
20
+ return False
21
+
22
+
23
+ def wait_for_health(
24
+ base: str, proc: subprocess.Popen[str] | None, timeout_s: float = 30.0
25
+ ) -> bool:
26
+ """Poll ``GET {base}/health`` until 200 from *our* child, process dies, or timeout.
27
+
28
+ On success we re-check ``proc.poll()`` so a pre-existing listener on the same
29
+ port cannot satisfy health while our spawn has already exited (failed bind).
30
+ """
31
+ deadline = time.monotonic() + timeout_s
32
+ while time.monotonic() < deadline:
33
+ if proc is not None and proc.poll() is not None:
34
+ return False
35
+ try:
36
+ resp = httpx.get(f"{base}/health", timeout=2.0)
37
+ if resp.status_code == 200:
38
+ if proc is not None and proc.poll() is not None:
39
+ return False
40
+ return True
41
+ except httpx.HTTPError:
42
+ pass
43
+ time.sleep(0.3)
44
+ return False
45
+
46
+
47
+ def health_ok(base: str) -> bool:
48
+ """Single-shot health check."""
49
+ try:
50
+ return httpx.get(f"{base}/health", timeout=2.0).status_code == 200
51
+ except httpx.HTTPError:
52
+ return False
53
+
54
+
55
+ def occupied_gateway_message(base: str, port: int) -> str | None:
56
+ """If ``port`` is already taken, return a user-facing error; else ``None``.
57
+
58
+ Used before ``monkeybot chat`` spawns a gateway so we never talk to a stale
59
+ process while the status bar shows the current yaml provider/model.
60
+ """
61
+ listening = not port_free(port)
62
+ healthy = health_ok(base)
63
+ if not listening and not healthy:
64
+ return None
65
+ detail = " (gateway /health OK)" if healthy else ""
66
+ return (
67
+ f"Port {port} is already in use{detail}. "
68
+ f"Another monkeybot process is likely still running at {base}.\n"
69
+ "Exit that session with /bye, stop `monkeybot run`, or free the port, "
70
+ "then retry — or connect with `monkeybot chat --attach`."
71
+ )
@@ -3,8 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- import shutil
7
- import stat
8
6
  from importlib import resources
9
7
  from importlib.resources.abc import Traversable
10
8
  from pathlib import Path
@@ -26,12 +24,11 @@ _CONFIG_BUNDLE: Final[tuple[tuple[str, str], ...]] = (
26
24
  ("command_allowlist.yaml", "command_allowlist.yaml"),
27
25
  ("permissions.yaml", "permissions.yaml"),
28
26
  ("AGENT.md", "AGENT.md"),
29
- ("env.example", "env.example"),
30
27
  ("otel-collector.example.yaml", "otel-collector.example.yaml"),
31
28
  )
32
29
 
33
30
  _MEMORY_INDEX: Final = (
34
- "# Memory index\n\nAdd sections here or let memory tools populate this file.\n"
31
+ "# Memory Index\n\nAdd sections here or let memory tools populate this file.\n"
35
32
  )
36
33
 
37
34
 
@@ -115,7 +112,7 @@ def ensure_memory(dest: Path, *, force: bool) -> list[str]:
115
112
 
116
113
 
117
114
  def ensure_workspace(dest: Path, *, force: bool) -> list[str]:
118
- """Create workspace/ sandbox and workspace/skills -> ../skills symlink."""
115
+ """Create the writable workspace and a separate trusted skills root."""
119
116
  lines: list[str] = []
120
117
  workspace = dest / "workspace"
121
118
  workspace.mkdir(parents=True, exist_ok=True)
@@ -129,52 +126,14 @@ def ensure_workspace(dest: Path, *, force: bool) -> list[str]:
129
126
  lines.append(" workspace/.gitkeep: skipped")
130
127
 
131
128
  dest.joinpath("skills").mkdir(parents=True, exist_ok=True)
132
- link = workspace / "skills"
133
- expected = (dest / "skills").resolve()
134
-
135
- if link.is_symlink():
136
- if link.resolve() == expected:
137
- lines.append(" workspace/skills: skipped (symlink ok)")
138
- return lines
139
- link.unlink()
140
-
141
- if link.exists() and not link.is_symlink():
142
- if not force:
143
- lines.append(" workspace/skills: skipped (path exists)")
144
- return lines
145
- if link.is_dir():
146
- shutil.rmtree(link)
147
- else:
148
- link.unlink()
149
-
150
- try:
151
- link.symlink_to("../skills", target_is_directory=True)
152
- lines.append(" workspace/skills: symlink -> ../skills")
153
- except OSError:
154
- readme = workspace / "SKILLS_README.txt"
155
- readme.write_text(
156
- "Could not create workspace/skills symlink on this platform.\n"
157
- "Run: bash scripts/setup-workspace.sh\n"
158
- "Or copy/symlink skills/ into workspace/skills manually.\n",
159
- encoding="utf-8",
160
- )
161
- lines.append(" workspace/skills: symlink failed (see workspace/SKILLS_README.txt)")
162
-
163
129
  return lines
164
130
 
165
131
 
166
- def install_setup_script(dest: Path, *, force: bool) -> str:
167
- scripts = dest / "scripts"
168
- scripts.mkdir(parents=True, exist_ok=True)
169
- dest_script = scripts / "setup-workspace.sh"
170
- if dest_script.exists() and not force:
171
- return "skipped"
172
- existed = dest_script.exists()
173
- dest_script.write_bytes(
174
- (resources.files(_DEFAULTS_PKG) / "setup-workspace.sh").read_bytes()
175
- )
176
- dest_script.chmod(dest_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
177
- return "overwritten" if existed else "created"
132
+ def install_browser_skill(dest: Path, *, force: bool) -> str:
133
+ """Install the bundled, trusted browser procedure into ``skills/browser``."""
134
+ target = dest / "skills" / "browser" / "SKILL.md"
135
+ target.parent.mkdir(parents=True, exist_ok=True)
136
+ return _install_file(target, resources.files(_DEFAULTS_PKG) / "browser" / "SKILL.md", force=force)
178
137
 
179
138
 
180
139
  def install_env_example(dest: Path, *, force: bool) -> str:
@@ -186,6 +145,17 @@ def install_env_example(dest: Path, *, force: bool) -> str:
186
145
  return "overwritten" if existed else "created"
187
146
 
188
147
 
148
+ def install_container_files(dest: Path, *, force: bool) -> list[str]:
149
+ """Install a deployable agent image definition, never the repo demo image."""
150
+ files = (("Dockerfile", "Dockerfile"), ("dockerignore", ".dockerignore"))
151
+ lines: list[str] = []
152
+ for source, target_name in files:
153
+ target = dest / target_name
154
+ status = _install_file(target, resources.files(_DEFAULTS_PKG) / source, force=force)
155
+ lines.append(f" {target_name}: {status}")
156
+ return lines
157
+
158
+
189
159
  def _sanitize_project_name(raw: str) -> str:
190
160
  name = re.sub(r"[^a-z0-9._-]+", "-", raw.strip().lower()).strip("-._")
191
161
  return name or "agent"
@@ -244,7 +214,9 @@ def write_agent_pyproject(
244
214
  if path.exists() and not force:
245
215
  return "skipped"
246
216
  existed = path.exists()
247
- dep = monkeybot_requirement(provider=provider, extras=extras)
217
+ # Sandbox is configured in every generated agent, so install its SDK with
218
+ # the provider dependencies rather than imposing it on every core install.
219
+ dep = monkeybot_requirement(provider=provider, extras=["sandbox", *(extras or [])])
248
220
  name = _sanitize_project_name(dest.name)
249
221
  path.write_text(
250
222
  (
@@ -254,7 +226,10 @@ def write_agent_pyproject(
254
226
  'requires-python = ">=3.11"\n'
255
227
  "dependencies = [\n"
256
228
  f' "{dep}",\n'
229
+ ' "monkeybot-browser-mcp>=0.2.0,<1",\n'
257
230
  "]\n"
231
+ "\n[tool.uv]\n"
232
+ "package = false\n"
258
233
  ),
259
234
  encoding="utf-8",
260
235
  )
@@ -269,7 +244,7 @@ def run_new(
269
244
  model: str | None = None,
270
245
  extras: list[str] | None = None,
271
246
  ) -> list[str]:
272
- """Full scaffold: config bundle, workspace, env example, and setup script."""
247
+ """Full scaffold: config bundle, trusted skills, writable state, and image files."""
273
248
  cfg_dir = dest / "monkeybot_config"
274
249
  report = install_config_bundle(cfg_dir, force=force)
275
250
  report.append(
@@ -278,8 +253,9 @@ def run_new(
278
253
  )
279
254
  report.extend(ensure_memory(dest, force=force))
280
255
  report.extend(ensure_workspace(dest, force=force))
281
- report.append(f" scripts/setup-workspace.sh: {install_setup_script(dest, force=force)}")
256
+ report.append(f" skills/browser/SKILL.md: {install_browser_skill(dest, force=force)}")
282
257
  report.append(f" .env.example: {install_env_example(dest, force=force)}")
258
+ report.extend(install_container_files(dest, force=force))
283
259
  report.append(
284
260
  f" pyproject.toml: "
285
261
  f"{write_agent_pyproject(dest, provider=provider, extras=extras, force=force)}"
@@ -0,0 +1,27 @@
1
+ FROM python:3.12-slim
2
+
3
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
4
+ WORKDIR /agent
5
+
6
+ COPY pyproject.toml uv.lock ./
7
+ RUN uv sync --frozen --no-dev
8
+
9
+ ARG INSTALL_CHROMIUM=0
10
+ USER root
11
+ RUN if [ "$INSTALL_CHROMIUM" = "1" ]; then \
12
+ apt-get update && apt-get install -y --no-install-recommends chromium \
13
+ && rm -rf /var/lib/apt/lists/*; \
14
+ fi
15
+
16
+ COPY monkeybot_config ./monkeybot_config
17
+ COPY skills ./skills
18
+ RUN useradd --create-home agent \
19
+ && mkdir -p workspace data/memory \
20
+ && chown -R agent:agent workspace data
21
+
22
+ USER agent
23
+ ENV PATH="/agent/.venv/bin:$PATH" PORT=8080
24
+ EXPOSE 8080
25
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
26
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')"
27
+ CMD ["python", "-m", "monkeybot.gateway.main"]
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: browser
3
+ description: Control a real browser through the optional bundled browser MCP server.
4
+ ---
5
+
6
+ # Browser
7
+
8
+ When browser tools are needed, call `enable_mcp("browser")` first. The server is
9
+ bundled with every scaffold but is disabled by default in `monkeybot_config/mcp.json`.
10
+
11
+ Use indexed DOM tools before screenshots. Browser screenshots and agent-written
12
+ playbooks are workspace data: `browser/Screenshots/` and `browser/playbooks/`.
13
+ They are not trusted skills and may be discarded with an ephemeral workspace.
14
+
15
+ Finish browser sessions with `browser_stop`.
@@ -0,0 +1,5 @@
1
+ workspace/
2
+ data/
3
+ .env
4
+ .venv/
5
+ .git/
@@ -1,6 +1,7 @@
1
1
  # Copy to .env and fill in secrets / machine-local values only.
2
2
 
3
3
  # Conversation history — SQLite is fine for single-agent dev.
4
+ # Relative paths in monkeybot.yaml are already anchored to this agent root.
4
5
  DB_URL=sqlite:///data/monkeybot.db
5
6
  # DB_URL=postgresql://postgres:postgres@localhost:5432/monkeybot
6
7
  # DB_URL=firestore://your-gcp-project/(default)
@@ -30,18 +30,15 @@
30
30
  }
31
31
  },
32
32
  "browser": {
33
- "command": "uv",
33
+ "enabled": false,
34
+ "command": "${MONKEYBOT_PYTHON}",
34
35
  "args": [
35
- "run",
36
- "--project",
37
- "/path/to/monkeybot/integrations/browser-mcp",
38
- "python",
39
36
  "-m",
40
37
  "browser_mcp.server"
41
38
  ],
42
39
  "env": {
43
40
  "BU_NAME": "monkeybot",
44
- "BROWSER_MCP_PLAYBOOKS_DIR": "./workspace/skills/browser/playbooks",
41
+ "BROWSER_MCP_PLAYBOOKS_DIR": "${MONKEYBOT_WORKSPACE_ROOT}/browser/playbooks",
45
42
  "BROWSER_MCP_SCREENSHOTS_DIR": "${MONKEYBOT_WORKSPACE_ROOT}/browser/Screenshots"
46
43
  }
47
44
  }