comodor 0.8.3__tar.gz → 0.8.4__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 (147) hide show
  1. {comodor-0.8.3 → comodor-0.8.4}/PKG-INFO +1 -1
  2. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/_version.py +2 -2
  3. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/browser/launch.py +34 -15
  4. {comodor-0.8.3 → comodor-0.8.4}/tests/test_real_browser.py +28 -7
  5. {comodor-0.8.3 → comodor-0.8.4}/.gitignore +0 -0
  6. {comodor-0.8.3 → comodor-0.8.4}/LICENSE +0 -0
  7. {comodor-0.8.3 → comodor-0.8.4}/README.md +0 -0
  8. {comodor-0.8.3 → comodor-0.8.4}/pyproject.toml +0 -0
  9. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/__init__.py +0 -0
  10. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/__main__.py +0 -0
  11. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/agent/__init__.py +0 -0
  12. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/agent/context.py +0 -0
  13. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/agent/loop.py +0 -0
  14. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/agent/prompts.py +0 -0
  15. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/agent/spawn.py +0 -0
  16. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/agent/tokens.py +0 -0
  17. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/browser/__init__.py +0 -0
  18. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/browser/cdp.py +0 -0
  19. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/browser/page.py +0 -0
  20. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/catalogue.py +0 -0
  21. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/cli.py +0 -0
  22. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/config.py +0 -0
  23. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/doctor.py +0 -0
  24. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/events.py +0 -0
  25. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/__init__.py +0 -0
  26. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/associations.py +0 -0
  27. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/bm25.py +0 -0
  28. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/hotindex.py +0 -0
  29. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/memory.py +0 -0
  30. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/progress.py +0 -0
  31. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/reflect.py +0 -0
  32. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/rules.py +0 -0
  33. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/signals.py +0 -0
  34. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/store.py +0 -0
  35. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/learning/writer.py +0 -0
  36. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/mcp/__init__.py +0 -0
  37. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/mcp/catalogue.py +0 -0
  38. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/mcp/commands.py +0 -0
  39. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/mcp/http.py +0 -0
  40. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/mcp/manager.py +0 -0
  41. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/mcp/protocol.py +0 -0
  42. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/net/__init__.py +0 -0
  43. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/net/http.py +0 -0
  44. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/net/sse.py +0 -0
  45. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/paths.py +0 -0
  46. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/__init__.py +0 -0
  47. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/anthropic.py +0 -0
  48. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/base.py +0 -0
  49. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/caching.py +0 -0
  50. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/fake.py +0 -0
  51. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/gateway.py +0 -0
  52. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/openai_compat.py +0 -0
  53. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/providers/registry.py +0 -0
  54. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/safety/__init__.py +0 -0
  55. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/safety/checkpoints.py +0 -0
  56. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/safety/permissions.py +0 -0
  57. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/safety/redact.py +0 -0
  58. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/session/__init__.py +0 -0
  59. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/session/search.py +0 -0
  60. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/session/store.py +0 -0
  61. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/setup.py +0 -0
  62. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/skills/__init__.py +0 -0
  63. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/skills/catalogue.py +0 -0
  64. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/skills/commands.py +0 -0
  65. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/skills/examples.py +0 -0
  66. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/skills/loader.py +0 -0
  67. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/skills/propose.py +0 -0
  68. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/skills/registry.py +0 -0
  69. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/__init__.py +0 -0
  70. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/base.py +0 -0
  71. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/browse.py +0 -0
  72. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/browser.py +0 -0
  73. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/delegate.py +0 -0
  74. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/fs.py +0 -0
  75. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/history.py +0 -0
  76. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/mcp.py +0 -0
  77. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/overflow.py +0 -0
  78. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/registry.py +0 -0
  79. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/search.py +0 -0
  80. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/shell.py +0 -0
  81. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/skills.py +0 -0
  82. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/todo.py +0 -0
  83. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/tools/web.py +0 -0
  84. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/__init__.py +0 -0
  85. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/app.py +0 -0
  86. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/bidi.py +0 -0
  87. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/chooser.py +0 -0
  88. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/console.py +0 -0
  89. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/input/__init__.py +0 -0
  90. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/input/keys.py +0 -0
  91. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/input/reader.py +0 -0
  92. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/layout.py +0 -0
  93. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/markdown.py +0 -0
  94. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/screen.py +0 -0
  95. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/theme.py +0 -0
  96. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/__init__.py +0 -0
  97. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/buttons.py +0 -0
  98. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/chat.py +0 -0
  99. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/history.py +0 -0
  100. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/overlay.py +0 -0
  101. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/panel.py +0 -0
  102. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/progress.py +0 -0
  103. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/prompt.py +0 -0
  104. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/statusbar.py +0 -0
  105. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/ui/widgets/toast.py +0 -0
  106. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/uninstall.py +0 -0
  107. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/update.py +0 -0
  108. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/web/__init__.py +0 -0
  109. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/web/commands.py +0 -0
  110. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/web/index.html +0 -0
  111. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/web/server.py +0 -0
  112. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/web/session.py +0 -0
  113. {comodor-0.8.3 → comodor-0.8.4}/src/comodor/workspace.py +0 -0
  114. {comodor-0.8.3 → comodor-0.8.4}/tests/conftest.py +0 -0
  115. {comodor-0.8.3 → comodor-0.8.4}/tests/support/fake_mcp_server.py +0 -0
  116. {comodor-0.8.3 → comodor-0.8.4}/tests/test_agent_loop.py +0 -0
  117. {comodor-0.8.3 → comodor-0.8.4}/tests/test_app.py +0 -0
  118. {comodor-0.8.3 → comodor-0.8.4}/tests/test_associations.py +0 -0
  119. {comodor-0.8.3 → comodor-0.8.4}/tests/test_bidi.py +0 -0
  120. {comodor-0.8.3 → comodor-0.8.4}/tests/test_browser.py +0 -0
  121. {comodor-0.8.3 → comodor-0.8.4}/tests/test_caching.py +0 -0
  122. {comodor-0.8.3 → comodor-0.8.4}/tests/test_chooser.py +0 -0
  123. {comodor-0.8.3 → comodor-0.8.4}/tests/test_delegate.py +0 -0
  124. {comodor-0.8.3 → comodor-0.8.4}/tests/test_doctor.py +0 -0
  125. {comodor-0.8.3 → comodor-0.8.4}/tests/test_history.py +0 -0
  126. {comodor-0.8.3 → comodor-0.8.4}/tests/test_input.py +0 -0
  127. {comodor-0.8.3 → comodor-0.8.4}/tests/test_layout.py +0 -0
  128. {comodor-0.8.3 → comodor-0.8.4}/tests/test_learning.py +0 -0
  129. {comodor-0.8.3 → comodor-0.8.4}/tests/test_markdown.py +0 -0
  130. {comodor-0.8.3 → comodor-0.8.4}/tests/test_mcp.py +0 -0
  131. {comodor-0.8.3 → comodor-0.8.4}/tests/test_mcp_http.py +0 -0
  132. {comodor-0.8.3 → comodor-0.8.4}/tests/test_overflow.py +0 -0
  133. {comodor-0.8.3 → comodor-0.8.4}/tests/test_performance.py +0 -0
  134. {comodor-0.8.3 → comodor-0.8.4}/tests/test_prefix_stability.py +0 -0
  135. {comodor-0.8.3 → comodor-0.8.4}/tests/test_progress.py +0 -0
  136. {comodor-0.8.3 → comodor-0.8.4}/tests/test_propose.py +0 -0
  137. {comodor-0.8.3 → comodor-0.8.4}/tests/test_providers.py +0 -0
  138. {comodor-0.8.3 → comodor-0.8.4}/tests/test_reflex.py +0 -0
  139. {comodor-0.8.3 → comodor-0.8.4}/tests/test_run_loop.py +0 -0
  140. {comodor-0.8.3 → comodor-0.8.4}/tests/test_setup.py +0 -0
  141. {comodor-0.8.3 → comodor-0.8.4}/tests/test_skill_budget.py +0 -0
  142. {comodor-0.8.3 → comodor-0.8.4}/tests/test_skill_catalogue.py +0 -0
  143. {comodor-0.8.3 → comodor-0.8.4}/tests/test_skills.py +0 -0
  144. {comodor-0.8.3 → comodor-0.8.4}/tests/test_tools.py +0 -0
  145. {comodor-0.8.3 → comodor-0.8.4}/tests/test_uninstall.py +0 -0
  146. {comodor-0.8.3 → comodor-0.8.4}/tests/test_update.py +0 -0
  147. {comodor-0.8.3 → comodor-0.8.4}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: comodor
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: Comodor — a self-improving terminal coding agent with a Rich TUI
5
5
  Project-URL: Homepage, https://comodor.ai
6
6
  Project-URL: Repository, https://github.com/ifekri/Comodor
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.8.3'
22
- __version_tuple__ = version_tuple = (0, 8, 3)
21
+ __version__ = version = '0.8.4'
22
+ __version_tuple__ = version_tuple = (0, 8, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -96,32 +96,47 @@ def free_port() -> int:
96
96
 
97
97
 
98
98
  #: What Chromium says when it will not run unprotected and cannot protect
99
- #: itself. Matched on the specifics, so a browser that failed for some other
100
- #: reason is not silently restarted with a weaker configuration.
99
+ #: itself. Matched against everything it printed rather than the first line:
100
+ #: it does not put the useful part first, and which line comes first depends on
101
+ #: how the container was started.
101
102
  _NO_SANDBOX_SIGNS = (
102
103
  "no usable sandbox",
103
104
  "failed to move to new namespace",
104
- "operation not permitted",
105
+ "setuid sandbox",
105
106
  "sandboxhelper",
106
107
  "chrome-sandbox",
108
+ "zygote_host",
107
109
  )
108
110
 
109
111
 
110
- def _refused_for_want_of_a_sandbox(message: str) -> bool:
111
- text = message.lower()
112
- return any(sign in text for sign in _NO_SANDBOX_SIGNS)
112
+ class SandboxUnavailable(BrowserError):
113
+ """It will not start because it cannot isolate its renderers here.
113
114
 
115
+ Its own class rather than a string match at the call site, because the
116
+ decision belongs where the full output is: by the time this reaches a
117
+ caller the message has been trimmed to the one line worth showing.
118
+ """
114
119
 
115
- def _said(handle) -> str:
116
- """Whatever the browser printed before giving up, trimmed to one line."""
120
+
121
+ def _looks_like_a_sandbox_problem(output: str) -> bool:
122
+ lowered = output.lower()
123
+ return any(sign in lowered for sign in _NO_SANDBOX_SIGNS)
124
+
125
+
126
+ def _read_all(handle) -> str:
117
127
  try:
118
128
  handle.seek(0)
119
- raw = handle.read(4000).decode("utf-8", "replace").strip()
129
+ return handle.read(20_000).decode("utf-8", "replace").strip()
120
130
  except (OSError, ValueError):
121
131
  return ""
122
- if not raw:
132
+
133
+
134
+ def _first_line(output: str) -> str:
135
+ """One line for a person. Forty lines of Chromium logging is not an error
136
+ message, it is a haystack."""
137
+ if not output:
123
138
  return ""
124
- first = next((line for line in raw.splitlines() if line.strip()), "")
139
+ first = next((line for line in output.splitlines() if line.strip()), "")
125
140
  return f" It said: {first.strip()[:300]}"
126
141
 
127
142
 
@@ -155,9 +170,8 @@ class Browser:
155
170
  try:
156
171
  return cls._spawn(binary, profile, chosen, headless, window,
157
172
  sandboxed=True)
158
- except BrowserError as error:
159
- if not _refused_for_want_of_a_sandbox(str(error)):
160
- raise
173
+ except SandboxUnavailable:
174
+ pass
161
175
  # It will not run its renderers in a user namespace here — almost
162
176
  # always a container, whose own confinement is the stronger boundary
163
177
  # anyway. Give up the inner one rather than weaken the outer one, and
@@ -205,7 +219,12 @@ class Browser:
205
219
  try:
206
220
  browser._await_port()
207
221
  except BrowserError as error:
208
- raise BrowserError(f"{error}{_said(complaints)}") from None
222
+ printed = _read_all(complaints)
223
+ message = f"{error}{_first_line(printed)}"
224
+ # Judged on everything it printed; reported as one line.
225
+ if sandboxed and _looks_like_a_sandbox_problem(printed):
226
+ raise SandboxUnavailable(message) from None
227
+ raise BrowserError(message) from None
209
228
  finally:
210
229
  complaints.close()
211
230
  return browser
@@ -480,14 +480,33 @@ def test_a_screenshot_is_a_real_png(tmp_path):
480
480
  def test_only_a_missing_sandbox_causes_a_retry():
481
481
  """Restarting a browser that failed for some other reason — with a weaker
482
482
  configuration, silently — would be a bad way to hide a real problem."""
483
- from comodor.browser.launch import _refused_for_want_of_a_sandbox
483
+ from comodor.browser.launch import _looks_like_a_sandbox_problem
484
484
 
485
- assert _refused_for_want_of_a_sandbox(
486
- "No usable sandbox! If this is a Debian system…")
487
- assert _refused_for_want_of_a_sandbox(
485
+ assert _looks_like_a_sandbox_problem(
486
+ "No usable sandbox! If this is a Debian system...")
487
+ assert _looks_like_a_sandbox_problem(
488
488
  "Failed to move to new namespace: Operation not permitted")
489
- assert not _refused_for_want_of_a_sandbox("could not start: file not found")
490
- assert not _refused_for_want_of_a_sandbox("the browser did not open a port")
489
+ assert not _looks_like_a_sandbox_problem("could not start: file not found")
490
+ assert not _looks_like_a_sandbox_problem("the browser did not open a port")
491
+
492
+
493
+ def test_the_useful_line_is_not_always_the_first_one():
494
+ """Chromium under compose says the setuid helper failed, and only mentions
495
+ the namespace four lines later. Matching the first line alone made the same
496
+ bug appear or not depending on how the container was started."""
497
+ from comodor.browser.launch import _first_line, _looks_like_a_sandbox_problem
498
+
499
+ printed = "\n".join([
500
+ "The setuid sandbox is not running as root. Common causes:",
501
+ " * An unprivileged process using ptrace on it, like a debugger.",
502
+ " * A parent process set prctl(PR_SET_NO_NEW_PRIVS, ...)",
503
+ "Failed to move to new namespace: ... errno = Operation not permitted",
504
+ ])
505
+
506
+ assert _looks_like_a_sandbox_problem(printed)
507
+ # And what a person is shown stays one line.
508
+ assert "\n" not in _first_line(printed)
509
+ assert "setuid sandbox is not running as root" in _first_line(printed)
491
510
 
492
511
 
493
512
  def test_a_browser_that_fails_for_another_reason_is_not_restarted(tmp_path,
@@ -517,9 +536,11 @@ def test_a_missing_sandbox_is_retried_once_without_one(tmp_path, monkeypatch):
517
536
  attempts = []
518
537
 
519
538
  def once(cls, binary, profile, port, headless, window, sandboxed):
539
+ from comodor.browser.launch import SandboxUnavailable
540
+
520
541
  attempts.append(sandboxed)
521
542
  if sandboxed:
522
- raise BrowserError("it exited. It said: No usable sandbox!")
543
+ raise SandboxUnavailable("it exited. It said: No usable sandbox!")
523
544
  return Browser("/bin/true", profile, port, sandboxed=False)
524
545
 
525
546
  monkeypatch.setattr(Browser, "_spawn", classmethod(once))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes