monkeybot 2.1.1__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 (178) hide show
  1. monkeybot/__init__.py +3 -0
  2. monkeybot/cli/__init__.py +3 -0
  3. monkeybot/cli/__main__.py +8 -0
  4. monkeybot/cli/audio_io.py +8 -0
  5. monkeybot/cli/gateway_manager.py +17 -0
  6. monkeybot/cli/main.py +22 -0
  7. monkeybot/cli/push_to_talk.py +12 -0
  8. monkeybot/cli/realtime_client.py +13 -0
  9. monkeybot/core/__init__.py +19 -0
  10. monkeybot/core/attachments/__init__.py +22 -0
  11. monkeybot/core/attachments/catalog.py +62 -0
  12. monkeybot/core/attachments/config.py +52 -0
  13. monkeybot/core/attachments/freeze.py +158 -0
  14. monkeybot/core/attachments/resolve.py +70 -0
  15. monkeybot/core/attachments/store.py +180 -0
  16. monkeybot/core/attachments/text.py +72 -0
  17. monkeybot/core/attachments/tools.py +54 -0
  18. monkeybot/core/bootstrap.py +242 -0
  19. monkeybot/core/config/__init__.py +71 -0
  20. monkeybot/core/config/realtime_config.py +150 -0
  21. monkeybot/core/config/runtime_env.py +262 -0
  22. monkeybot/core/config/settings.py +341 -0
  23. monkeybot/core/config/validation.py +249 -0
  24. monkeybot/core/config/yaml_loader.py +45 -0
  25. monkeybot/core/context/__init__.py +781 -0
  26. monkeybot/core/context/campaign_context.py +8 -0
  27. monkeybot/core/context/common.py +14 -0
  28. monkeybot/core/context/curator.py +255 -0
  29. monkeybot/core/context/epoch.py +226 -0
  30. monkeybot/core/context/memory_prompt.py +222 -0
  31. monkeybot/core/context/tool_output_policy.py +270 -0
  32. monkeybot/core/context/tool_result_ingress.py +290 -0
  33. monkeybot/core/context/tool_shapers.py +361 -0
  34. monkeybot/core/hooks/__init__.py +261 -0
  35. monkeybot/core/llm/__init__.py +4 -0
  36. monkeybot/core/llm/provider.py +296 -0
  37. monkeybot/core/llm/realtime_provider.py +203 -0
  38. monkeybot/core/llm/usage.py +57 -0
  39. monkeybot/core/logging_utils.py +24 -0
  40. monkeybot/core/mcp/__init__.py +1 -0
  41. monkeybot/core/mcp/mcp_client.py +1215 -0
  42. monkeybot/core/mcp/ports_mcp.py +109 -0
  43. monkeybot/core/memory/__init__.py +24 -0
  44. monkeybot/core/memory/hook.py +413 -0
  45. monkeybot/core/memory/index_format.py +104 -0
  46. monkeybot/core/memory/integrity.py +180 -0
  47. monkeybot/core/memory/organizer.py +270 -0
  48. monkeybot/core/memory/storage_ops.py +139 -0
  49. monkeybot/core/memory/subsystem.py +91 -0
  50. monkeybot/core/messages/__init__.py +16 -0
  51. monkeybot/core/messages/convert_provider.py +41 -0
  52. monkeybot/core/messages/tool_integrity.py +262 -0
  53. monkeybot/core/messages/transform_context.py +84 -0
  54. monkeybot/core/path_safety.py +11 -0
  55. monkeybot/core/persistence/__init__.py +17 -0
  56. monkeybot/core/persistence/backends.py +236 -0
  57. monkeybot/core/persistence/db.py +28 -0
  58. monkeybot/core/persistence/durable_runs.py +286 -0
  59. monkeybot/core/persistence/firestore.py +658 -0
  60. monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
  61. monkeybot/core/persistence/history.py +156 -0
  62. monkeybot/core/persistence/postgres.py +895 -0
  63. monkeybot/core/persistence/runs.py +76 -0
  64. monkeybot/core/persistence/scheduled_loops.py +435 -0
  65. monkeybot/core/persistence/session_turn_locks.py +94 -0
  66. monkeybot/core/persistence/sqlite.py +218 -0
  67. monkeybot/core/persistence/sqlite_backend.py +74 -0
  68. monkeybot/core/persistence/thread_summary.py +61 -0
  69. monkeybot/core/persistence/transcript.py +194 -0
  70. monkeybot/core/persistence/usage.py +149 -0
  71. monkeybot/core/prompts/__init__.py +1 -0
  72. monkeybot/core/prompts/harness_prompt.py +197 -0
  73. monkeybot/core/prompts/prompt.py +215 -0
  74. monkeybot/core/runtime/__init__.py +1 -0
  75. monkeybot/core/runtime/context_budget.py +267 -0
  76. monkeybot/core/runtime/events.py +819 -0
  77. monkeybot/core/runtime/input_admission.py +154 -0
  78. monkeybot/core/runtime/loop.py +2374 -0
  79. monkeybot/core/runtime/provider_stream_mapper.py +159 -0
  80. monkeybot/core/runtime/realtime_loop.py +654 -0
  81. monkeybot/core/runtime/utterance_buffer.py +179 -0
  82. monkeybot/core/subagents/__init__.py +1 -0
  83. monkeybot/core/subagents/subagent_proto.py +331 -0
  84. monkeybot/core/subagents/subagent_worker.py +441 -0
  85. monkeybot/core/subagents/worker_pool.py +403 -0
  86. monkeybot/core/testing/__init__.py +1 -0
  87. monkeybot/core/testing/mocks_provider.py +86 -0
  88. monkeybot/core/testing/mocks_realtime_provider.py +137 -0
  89. monkeybot/core/tools/__init__.py +1 -0
  90. monkeybot/core/tools/core_tool_executor.py +1548 -0
  91. monkeybot/core/tools/inspector.py +226 -0
  92. monkeybot/core/tools/loop_inspector.py +45 -0
  93. monkeybot/core/tools/patch.py +480 -0
  94. monkeybot/core/tools/permission.py +284 -0
  95. monkeybot/core/tools/sandbox_executor.py +255 -0
  96. monkeybot/core/tools/spill_inventory.py +35 -0
  97. monkeybot/core/tools/terminal.py +381 -0
  98. monkeybot/core/tools/text_normalize.py +25 -0
  99. monkeybot/core/tools/types.py +33 -0
  100. monkeybot/core/tools/workspace_service.py +710 -0
  101. monkeybot/core/tools/workspace_tools.py +116 -0
  102. monkeybot/core/types/__init__.py +1 -0
  103. monkeybot/core/types/content_blocks.py +644 -0
  104. monkeybot/core/types/interfaces.py +156 -0
  105. monkeybot/core/types/types_tools.py +29 -0
  106. monkeybot/core/workspace/__init__.py +8 -0
  107. monkeybot/core/workspace/factory.py +45 -0
  108. monkeybot/core/workspace/gcs.py +130 -0
  109. monkeybot/core/workspace/local.py +162 -0
  110. monkeybot/core/workspace/protocol.py +45 -0
  111. monkeybot/core/workspace/s3.py +151 -0
  112. monkeybot/core/workspace_layout.py +27 -0
  113. monkeybot/gateway/__init__.py +1 -0
  114. monkeybot/gateway/bootstrap.py +18 -0
  115. monkeybot/gateway/main.py +47 -0
  116. monkeybot/gateway/realtime/__init__.py +31 -0
  117. monkeybot/gateway/realtime/app.py +321 -0
  118. monkeybot/gateway/realtime/deps.py +52 -0
  119. monkeybot/gateway/realtime/errors.py +81 -0
  120. monkeybot/gateway/realtime/guardrails.py +88 -0
  121. monkeybot/gateway/realtime/manager.py +77 -0
  122. monkeybot/gateway/realtime/metrics.py +144 -0
  123. monkeybot/gateway/realtime/routes.py +864 -0
  124. monkeybot/gateway/realtime/session.py +232 -0
  125. monkeybot/gateway/realtime/wire.py +412 -0
  126. monkeybot/gateway/realtime_main.py +49 -0
  127. monkeybot/gateway/sse/__init__.py +1 -0
  128. monkeybot/gateway/sse/app.py +733 -0
  129. monkeybot/gateway/sse/loop_port.py +31 -0
  130. monkeybot/gateway/sse/models.py +177 -0
  131. monkeybot/gateway/sse/reply_body.py +91 -0
  132. monkeybot/gateway/sse/routes.py +1101 -0
  133. monkeybot/gateway/sse/scheduler_routes.py +200 -0
  134. monkeybot/gateway/sse/scheduler_wiring.py +96 -0
  135. monkeybot/gateway/sse/session_bus.py +226 -0
  136. monkeybot/gateway/sse/sse.py +46 -0
  137. monkeybot/gateway/sse/workspace_layout.py +7 -0
  138. monkeybot/observability/__init__.py +220 -0
  139. monkeybot/observability/_state.py +10 -0
  140. monkeybot/observability/instrumentation.py +153 -0
  141. monkeybot/observability/propagation.py +65 -0
  142. monkeybot/observability/spans.py +455 -0
  143. monkeybot/providers/__init__.py +19 -0
  144. monkeybot/providers/_openai_compat.py +450 -0
  145. monkeybot/providers/_utils.py +473 -0
  146. monkeybot/providers/bedrock.py +145 -0
  147. monkeybot/providers/claude.py +125 -0
  148. monkeybot/providers/gemini.py +677 -0
  149. monkeybot/providers/gemini_live.py +398 -0
  150. monkeybot/providers/huggingface.py +129 -0
  151. monkeybot/providers/nvidia.py +104 -0
  152. monkeybot/providers/ollama.py +152 -0
  153. monkeybot/providers/openai.py +127 -0
  154. monkeybot/providers/pricing.py +60 -0
  155. monkeybot/providers/sampling.py +44 -0
  156. monkeybot/providers/vertex_claude.py +148 -0
  157. monkeybot/scaffold/__init__.py +33 -0
  158. monkeybot/scheduler/__init__.py +13 -0
  159. monkeybot/scheduler/__main__.py +4 -0
  160. monkeybot/scheduler/engine.py +333 -0
  161. monkeybot/scheduler/http_invoker.py +61 -0
  162. monkeybot/scheduler/interval.py +77 -0
  163. monkeybot/scheduler/tick_result.py +34 -0
  164. monkeybot/scheduler/worker.py +87 -0
  165. monkeybot/subagents/__init__.py +1 -0
  166. monkeybot/subagents/worker/__init__.py +1 -0
  167. monkeybot/subagents/worker/__main__.py +22 -0
  168. monkeybot/web_search/__init__.py +82 -0
  169. monkeybot/web_search/backends/__init__.py +5 -0
  170. monkeybot/web_search/backends/duckduckgo.py +32 -0
  171. monkeybot/web_search/backends/firecrawl.py +43 -0
  172. monkeybot/web_search/backends/tavily.py +45 -0
  173. monkeybot/web_search/protocol.py +25 -0
  174. monkeybot/web_search/tool.py +56 -0
  175. monkeybot-2.1.1.dist-info/METADATA +318 -0
  176. monkeybot-2.1.1.dist-info/RECORD +178 -0
  177. monkeybot-2.1.1.dist-info/WHEEL +4 -0
  178. monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,226 @@
1
+ """Tool inspectors: ``run_command`` policy (allowlists + optional deny-regex YAML)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shlex
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Literal, Protocol
10
+
11
+ import yaml
12
+ from monkeybot.core.context import TurnContext
13
+ from monkeybot.core.types.interfaces import MonkeybotError
14
+ from monkeybot.core.tools.terminal import ALLOWED_COMMANDS, ALLOWED_PATHS
15
+
16
+ # Default deny-regex lines when YAML omits ``deny_patterns``. Blocks package installs
17
+ # via common verbs (pip, uv, npm, apt, brew, etc.) while leaving script execution allowed.
18
+ DEFAULT_DENY_PATTERNS: tuple[str, ...] = (
19
+ r"pip3?\s+install",
20
+ r"python3?\s+-m\s+pip\s+install",
21
+ r"(^|\s)uv\s+(add|remove|sync|pip\s+install|pip\s+sync)",
22
+ r"(^|\s)poetry\s+(add|install)",
23
+ r"(^|\s)conda\s+install",
24
+ r"(^|\s)(npm|pnpm|yarn)\s+(install|add|i)(\s|$)",
25
+ r"(^|\s)(apt|apt-get)\s+install",
26
+ r"(^|\s)brew\s+install",
27
+ )
28
+
29
+
30
+ class CommandTierConfigError(MonkeybotError):
31
+ """Invalid or unloadable run_command policy file."""
32
+
33
+ def __init__(self, path: Path, reason: str) -> None:
34
+ self.path = path
35
+ self.reason = reason
36
+ super().__init__(f"{path}: {reason}")
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Decision:
41
+ """Allow, deny, or confirm outcome from a tool inspector.
42
+
43
+ ``confirm`` requires an SSE session so the user can approve via tool-confirmations.
44
+ """
45
+
46
+ kind: Literal["allow", "deny", "confirm"]
47
+ message: str | None = None
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class InspectorToolCall:
52
+ """Normalized tool call at the inspection boundary (Story 6 maps provider calls here)."""
53
+
54
+ call_id: str
55
+ name: str
56
+ args: dict[str, object]
57
+
58
+
59
+ class ToolInspector(Protocol):
60
+ """Pre-flight gate for tool execution."""
61
+
62
+ async def check(self, call: InspectorToolCall, ctx: TurnContext) -> Decision:
63
+ """Return allow or deny before executing the tool."""
64
+ ...
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class CommandTierPolicy:
69
+ """Parsed ``command_allowlist.yaml``: execution allowlists + optional deny-regex lines."""
70
+
71
+ allowed_commands: tuple[str, ...]
72
+ allowed_path_prefixes: tuple[str, ...]
73
+ deny_patterns: tuple[str, ...]
74
+
75
+
76
+ def _optional_nonempty_str_list(path: Path, data: dict[str, Any], key: str) -> list[str] | None:
77
+ if key not in data:
78
+ return None
79
+ val = data[key]
80
+ if not isinstance(val, list) or not val:
81
+ raise CommandTierConfigError(
82
+ path, f"'{key}' must be a non-empty list of strings when present"
83
+ )
84
+ out: list[str] = []
85
+ for i, item in enumerate(val):
86
+ if not isinstance(item, str) or not item.strip():
87
+ raise CommandTierConfigError(
88
+ path, f"'{key}[{i}]' must be a non-empty string"
89
+ )
90
+ out.append(item)
91
+ return out
92
+
93
+
94
+ def norm_run_command_line(args: dict[str, object]) -> str | None:
95
+ """Build one line for deny-regex checks (mirrors ``run_command`` argv shapes)."""
96
+ argv_raw = args.get("argv")
97
+ if isinstance(argv_raw, list) and argv_raw:
98
+ parts = [str(x) for x in argv_raw]
99
+ return shlex.join(parts)
100
+
101
+ cmd = args.get("command")
102
+ if isinstance(cmd, str) and cmd.strip():
103
+ extra = args.get("args")
104
+ if extra is None:
105
+ extra = args.get("arguments")
106
+ if isinstance(extra, list) and extra:
107
+ parts = [cmd.strip()] + [str(x) for x in extra]
108
+ return shlex.join(parts)
109
+ parts = shlex.split(cmd.strip(), posix=True)
110
+ if len(parts) > 1:
111
+ return shlex.join(parts)
112
+ return " ".join(cmd.strip().split())
113
+
114
+ shell = args.get("shell") or args.get("script")
115
+ if isinstance(shell, str) and shell.strip():
116
+ return " ".join(shell.strip().split())
117
+
118
+ return None
119
+
120
+
121
+ def load_command_tier_policy(path: Path) -> CommandTierPolicy:
122
+ """Load ``run_command`` allowlists and optional ``deny_patterns`` from policy YAML."""
123
+ raw_bytes = path.read_bytes()
124
+ try:
125
+ data = yaml.safe_load(raw_bytes)
126
+ except yaml.YAMLError as e:
127
+ raise CommandTierConfigError(path, f"invalid YAML: {e}") from e
128
+
129
+ if data is None:
130
+ raise CommandTierConfigError(path, "file is empty")
131
+ if not isinstance(data, dict):
132
+ raise CommandTierConfigError(path, "root must be a mapping")
133
+
134
+ for legacy_key in ("tier_order", "default"):
135
+ if legacy_key in data:
136
+ raise CommandTierConfigError(
137
+ path,
138
+ f"obsolete key {legacy_key!r}: tier-based policy was removed; use "
139
+ "'allowed_commands', 'allowed_path_prefixes', and optional 'deny_patterns'",
140
+ )
141
+
142
+ ac_raw = _optional_nonempty_str_list(path, data, "allowed_commands")
143
+ ap_raw = _optional_nonempty_str_list(path, data, "allowed_path_prefixes")
144
+
145
+ deny_raw = data.get("deny_patterns")
146
+ if deny_raw is None:
147
+ deny_patterns = DEFAULT_DENY_PATTERNS
148
+ elif isinstance(deny_raw, list):
149
+ for i, item in enumerate(deny_raw):
150
+ if not isinstance(item, str) or not item.strip():
151
+ raise CommandTierConfigError(
152
+ path, f"'deny_patterns[{i}]' must be a non-empty string"
153
+ )
154
+ deny_patterns = tuple(str(x) for x in deny_raw)
155
+ else:
156
+ raise CommandTierConfigError(
157
+ path, "'deny_patterns' must be a list of strings when present"
158
+ )
159
+
160
+ allowed_commands = tuple(ac_raw) if ac_raw is not None else tuple(ALLOWED_COMMANDS)
161
+ allowed_path_prefixes = tuple(ap_raw) if ap_raw is not None else tuple(ALLOWED_PATHS)
162
+
163
+ return CommandTierPolicy(
164
+ allowed_commands=allowed_commands,
165
+ allowed_path_prefixes=allowed_path_prefixes,
166
+ deny_patterns=deny_patterns,
167
+ )
168
+
169
+
170
+ class CommandTierInspector:
171
+ """YAML policy: optional deny-regex preflight; execution allowlists are on the policy object."""
172
+
173
+ def __init__(self, tier_config_path: Path) -> None:
174
+ self._path = tier_config_path
175
+ self._policy = load_command_tier_policy(tier_config_path)
176
+ compiled: list[re.Pattern[str]] = []
177
+ for i, pat in enumerate(self._policy.deny_patterns):
178
+ try:
179
+ compiled.append(re.compile(pat, flags=re.IGNORECASE))
180
+ except re.error as e:
181
+ raise CommandTierConfigError(
182
+ tier_config_path, f"deny_patterns[{i}]: invalid regex: {e}"
183
+ ) from e
184
+ self._deny_regexes = tuple(compiled)
185
+
186
+ @property
187
+ def allowed_commands(self) -> tuple[str, ...]:
188
+ return self._policy.allowed_commands
189
+
190
+ @property
191
+ def allowed_path_prefixes(self) -> tuple[str, ...]:
192
+ return self._policy.allowed_path_prefixes
193
+
194
+ async def check(self, call: InspectorToolCall, ctx: TurnContext) -> Decision:
195
+ """Deny if any ``deny_patterns`` matches the normalized invocation; else allow."""
196
+ del ctx
197
+ if call.name != "run_command":
198
+ return Decision(kind="allow")
199
+
200
+ cmd_norm = norm_run_command_line(call.args)
201
+ if not cmd_norm:
202
+ return Decision(
203
+ kind="deny",
204
+ message="run_command policy check needs argv, command, or shell/script",
205
+ )
206
+
207
+ for rx in self._deny_regexes:
208
+ if rx.search(cmd_norm):
209
+ return Decision(kind="deny", message="denied by run_command deny_patterns policy")
210
+
211
+ return Decision(kind="allow")
212
+
213
+
214
+ class RulesInspector:
215
+ """Blocks tool calls whose serialized arguments contain any denied substring."""
216
+
217
+ def __init__(self, denied_patterns: list[str]) -> None:
218
+ self.denied_patterns = denied_patterns
219
+
220
+ async def check(self, call: InspectorToolCall, ctx: TurnContext) -> Decision:
221
+ del ctx
222
+ args_str = str(call.args)
223
+ for pattern in self.denied_patterns:
224
+ if pattern in args_str:
225
+ return Decision(kind="deny", message=f"Blocked by rules: {pattern}")
226
+ return Decision(kind="allow")
@@ -0,0 +1,45 @@
1
+ """Inspector that requires user confirmation before starting a scheduled loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from monkeybot.core.context import TurnContext
6
+ from monkeybot.core.tools.inspector import Decision, InspectorToolCall, ToolInspector
7
+
8
+
9
+ class LoopStartInspector:
10
+ """Return ``confirm`` for ``start_loop`` so the user approves the plan before it runs."""
11
+
12
+ async def check(self, call: InspectorToolCall, ctx: TurnContext) -> Decision:
13
+ if call.name != "start_loop":
14
+ return Decision(kind="allow")
15
+ prompt = call.args.get("prompt")
16
+ interval = call.args.get("interval", "?")
17
+ loop_id = call.args.get("loop_id") or "(auto)"
18
+ session_id = call.args.get("session_id") or "loop-main"
19
+ max_ticks = call.args.get("max_ticks")
20
+ max_runtime = call.args.get("max_runtime")
21
+ unbounded = call.args.get("unbounded", False) is True
22
+ preview = str(prompt).strip() if isinstance(prompt, str) else ""
23
+ if len(preview) > 400:
24
+ preview = preview[:400] + "…"
25
+ guards: list[str] = []
26
+ if max_ticks is not None:
27
+ guards.append(f"max_ticks={max_ticks}")
28
+ if max_runtime is not None:
29
+ guards.append(f"max_runtime={max_runtime}")
30
+ if unbounded:
31
+ guard_text = "UNBOUNDED (no max_ticks/max_runtime)"
32
+ elif guards:
33
+ guard_text = ", ".join(guards)
34
+ else:
35
+ guard_text = "MISSING — requires max_ticks, max_runtime, or unbounded=true"
36
+ message = (
37
+ f"Start scheduled loop?\n"
38
+ f"- loop_id: {loop_id}\n"
39
+ f"- session: {session_id}\n"
40
+ f"- interval: {interval}\n"
41
+ f"- guards: {guard_text}\n\n"
42
+ f"Plan:\n{preview or '(empty prompt)'}"
43
+ )
44
+ del ctx
45
+ return Decision(kind="confirm", message=message)