webbee 0.6.7__tar.gz → 0.6.9__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.
- {webbee-0.6.7 → webbee-0.6.9}/CHANGELOG.md +14 -0
- {webbee-0.6.7 → webbee-0.6.9}/PKG-INFO +2 -2
- {webbee-0.6.7 → webbee-0.6.9}/pyproject.toml +2 -2
- webbee-0.6.9/src/webbee/__init__.py +1 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/repl.py +19 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/session.py +18 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tools.py +42 -0
- webbee-0.6.9/tests/test_mode_and_remote_0_6_8.py +107 -0
- webbee-0.6.7/src/webbee/__init__.py +0 -1
- {webbee-0.6.7 → webbee-0.6.9}/.github/workflows/publish.yml +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/.gitignore +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/LICENSE +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/README.md +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/install.sh +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/__main__.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/_launch.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/account.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/active_sessions.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/autoupdate.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/banner_art.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/boot.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/browser.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/checkpoints.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/cli.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/clipboard.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/clipboard_session.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/coding_context.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/commands.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/config.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/consent.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/details.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/events.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/frames.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/home.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/home_view.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/http.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/instance_lock.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/graph.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/outline.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/query.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/service.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/store.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/watch.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/media.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/modals.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/mode_store.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/native_files.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/native_read.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/output_pane.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/pricing_constants.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/queue_panel.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/reflow.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/remote.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/render.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/repo.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/selection.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/sessions.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/sizing.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/slots.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/steer.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/stream.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tab_store.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tabs.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/thread.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tier_store.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/todo_panel.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tokens.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tui.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/update.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/urlopen.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/wallet.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/src/webbee/worktrees.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/__init__.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/conftest.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_account.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_active_sessions.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_autoupdate.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_checkpoints.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_cli.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_clipboard.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_clipboard_read.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_coding_context.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_commands.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_config.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_cpc_outline.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_cross_loop_token.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_details.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_events.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_freeze_fix.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_home.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_home_view.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_home_view_mouse.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_inspect_page.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_instance_lock.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_chunker.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_embed.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_graph.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_query.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_service.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_store.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_vectors.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_watch.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_launch.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_marathon.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_modals_e2e.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_mode_store.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_native_files.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_newtab_mode.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_packaging.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_probe_cost.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_reflow.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_render.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_repl.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_repo.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_resilient_http.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_session.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_sessions.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_sizing.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_slots.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_steer.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_stream.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tab_bar_stability.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tabs.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_thread.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tier_invalidation.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tier_store.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_todo_panel.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tokens.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tools.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tools_vision_websearch.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tui.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_tui_hardening.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_update.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_urlopen.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_version.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_wallet.py +0 -0
- {webbee-0.6.7 → webbee-0.6.9}/tests/test_worktrees.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.9
|
|
4
|
+
|
|
5
|
+
* imperal-mcp 0.8.5 at least: a server's repo that cannot be answered says why; parallel code questions never mix
|
|
6
|
+
their freshness; a change's second-depth users are real ones.
|
|
7
|
+
|
|
8
|
+
## 0.6.8
|
|
9
|
+
|
|
10
|
+
* **Autopilot asks nothing**: the coding mode rides every turn, and a switch (Shift-Tab, /mode, a remote flip) reaches
|
|
11
|
+
the running session at once -- in autopilot the kernel sends no consent request at all, so no turn waits for one.
|
|
12
|
+
A production release still runs only on your own words.
|
|
13
|
+
* **A server's repo is synced only where it is**: a folder that does not exist, or is in no repository, is refused at
|
|
14
|
+
once (a wrong path walked `/home` for half an hour), and the repo's HEAD is read in a few small reads.
|
|
15
|
+
* imperal-mcp 0.8.4 at least.
|
|
16
|
+
|
|
3
17
|
## 0.6.7
|
|
4
18
|
|
|
5
19
|
* **The OS's file-watch limit never crashes webbee**: on a large tree (Linux `fs.inotify.max_user_watches`, a repo with
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webbee
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
4
4
|
Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
|
|
5
5
|
Project-URL: Homepage, https://imperal.io
|
|
6
6
|
Project-URL: Documentation, https://docs.imperal.io
|
|
@@ -31,7 +31,7 @@ Requires-Python: >=3.7
|
|
|
31
31
|
Requires-Dist: asyncssh>=2.14; python_version >= '3.10'
|
|
32
32
|
Requires-Dist: httpx-sse>=0.4; python_version >= '3.10'
|
|
33
33
|
Requires-Dist: httpx>=0.27; python_version >= '3.10'
|
|
34
|
-
Requires-Dist: imperal-mcp>=0.8.
|
|
34
|
+
Requires-Dist: imperal-mcp>=0.8.5; python_version >= '3.10'
|
|
35
35
|
Requires-Dist: imperal-sdk>=6.3.6; python_version >= '3.10'
|
|
36
36
|
Requires-Dist: model2vec>=0.8.2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
|
|
37
37
|
Requires-Dist: numpy>=2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "webbee"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.9"
|
|
4
4
|
description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "GPL-3.0-or-later"
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
# release on a Python 3.12 it sets up once with uv -- `uv` comes as a wheel on 3.8+,
|
|
14
14
|
# 3.7 fetches it with uv's own installer.
|
|
15
15
|
'uv>=0.4; python_version >= "3.8" and python_version < "3.10"',
|
|
16
|
-
'imperal-mcp>=0.8.
|
|
16
|
+
'imperal-mcp>=0.8.5; python_version >= "3.10"', # raised to the newest at each release
|
|
17
17
|
'imperal-sdk>=6.3.6; python_version >= "3.10"',
|
|
18
18
|
'asyncssh>=2.14; python_version >= "3.10"',
|
|
19
19
|
'httpx>=0.27; python_version >= "3.10"',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.9"
|
|
@@ -265,10 +265,29 @@ def set_slot_mode(slot: SessionSlot, mode: str) -> None:
|
|
|
265
265
|
slot.mode = mode
|
|
266
266
|
if slot.agent is not None:
|
|
267
267
|
slot.agent.mode = mode
|
|
268
|
+
_notify_mode(slot.agent, mode)
|
|
268
269
|
from webbee.mode_store import save_mode
|
|
269
270
|
save_mode(slot.workspace, mode)
|
|
270
271
|
|
|
271
272
|
|
|
273
|
+
_MODE_POSTS: set = set() # in-flight mode notices (a task kept alive until it is done)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _notify_mode(agent, mode: str) -> None:
|
|
277
|
+
"""The running session hears the switch at once (``AgentSession.post_mode``): fire and forget on the running
|
|
278
|
+
loop; outside a loop (tests, boot) nothing is sent -- the next turn's coding_context carries the mode."""
|
|
279
|
+
post = getattr(agent, "post_mode", None)
|
|
280
|
+
if post is None or not getattr(agent, "session_id", ""):
|
|
281
|
+
return
|
|
282
|
+
try:
|
|
283
|
+
loop = asyncio.get_running_loop()
|
|
284
|
+
except RuntimeError:
|
|
285
|
+
return
|
|
286
|
+
task = loop.create_task(post(mode))
|
|
287
|
+
_MODE_POSTS.add(task)
|
|
288
|
+
task.add_done_callback(_MODE_POSTS.discard)
|
|
289
|
+
|
|
290
|
+
|
|
272
291
|
def set_slot_tier(slot: SessionSlot, tier: str) -> None:
|
|
273
292
|
"""The ONE place a slot's model tier is ever assigned (webbee-code-
|
|
274
293
|
model-tier-slash-command-v1) -- mutates `slot.model_tier` + the live
|
|
@@ -176,6 +176,21 @@ class AgentSession:
|
|
|
176
176
|
token = await self.token_provider()
|
|
177
177
|
return {"Authorization": f"Bearer {token}"}
|
|
178
178
|
|
|
179
|
+
async def post_mode(self, mode: str) -> bool:
|
|
180
|
+
"""Tell the running session its new coding mode (the gateway signals the kernel's ``set_mode``): autopilot
|
|
181
|
+
is asked nothing from now on, the others are asked again. False when there is no session yet or no live one
|
|
182
|
+
took it -- the next turn carries the mode anyway. Never raises."""
|
|
183
|
+
if not self.session_id or self.cfg is None or self.token_provider is None:
|
|
184
|
+
return False
|
|
185
|
+
try:
|
|
186
|
+
headers = await self._headers()
|
|
187
|
+
async with make_client(self.cfg, timeout=15) as client:
|
|
188
|
+
resp = await client.post(f"/v1/agent/sessions/{self.session_id}/mode", json={"mode": mode},
|
|
189
|
+
headers=headers)
|
|
190
|
+
return resp.status_code == 200 and bool((resp.json() or {}).get("ok"))
|
|
191
|
+
except Exception:
|
|
192
|
+
return False
|
|
193
|
+
|
|
179
194
|
async def run(self, task: str, sink, *, marathon: bool = False, goal: str = "",
|
|
180
195
|
surface: str = "", steer_iid: str = "") -> str:
|
|
181
196
|
import httpx
|
|
@@ -199,6 +214,9 @@ class AgentSession:
|
|
|
199
214
|
coding_context = await asyncio.to_thread(build_coding_context, self.workspace_root, self._intel)
|
|
200
215
|
if self.model_tier:
|
|
201
216
|
coding_context = {**coding_context, "model_tier": self.model_tier}
|
|
217
|
+
# the mode this turn runs under: in autopilot the kernel asks no consent at all (no card, no park); a switch
|
|
218
|
+
# mid-turn reaches the running session through post_mode
|
|
219
|
+
coding_context = {**coding_context, "mode": str(self.mode or "default")}
|
|
202
220
|
if marathon:
|
|
203
221
|
# verify_cmd is CLIENT-detected here and carried in coding_context —
|
|
204
222
|
# the trusted proof-of-done the kernel runs (never brain-authored).
|
|
@@ -1141,6 +1141,44 @@ async def _open_sftp(host: str):
|
|
|
1141
1141
|
return await conn.start_sftp_client(), conn, fps, ""
|
|
1142
1142
|
|
|
1143
1143
|
|
|
1144
|
+
async def _no_repo_at(sftp, ci, root: str) -> str:
|
|
1145
|
+
"""Why ``root`` on the server is no repository to sync ("" when it is one): it must exist and be (inside) a
|
|
1146
|
+
repository -- a ``.git`` or a manifest at it or above."""
|
|
1147
|
+
try:
|
|
1148
|
+
await sftp.readdir(root)
|
|
1149
|
+
except Exception as e:
|
|
1150
|
+
return f"{root} does not exist or is not a readable directory ({type(e).__name__})"
|
|
1151
|
+
repo_root = await ci.sftp_repo_root(sftp, root)
|
|
1152
|
+
try:
|
|
1153
|
+
names = {e.filename for e in await sftp.readdir(repo_root)}
|
|
1154
|
+
except Exception:
|
|
1155
|
+
names = set()
|
|
1156
|
+
if not names & (set(getattr(ci, "_REPO_MARKERS", (".git",))) | set(getattr(ci, "_MANIFESTS", ()))):
|
|
1157
|
+
return f"{root} is in no repository (no .git or project manifest at it or above)"
|
|
1158
|
+
return ""
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
async def remote_head(host: str, root: str) -> dict:
|
|
1162
|
+
"""The commit the server's repo at ``root`` is on -- a few small SFTP reads (``.git/HEAD`` and its ref), never a
|
|
1163
|
+
listing: ``{"commit", "branch", "root", "why"}``; ``why`` says what failed (an unknown host, a missing folder)."""
|
|
1164
|
+
from imperal_sdk import code_index as ci
|
|
1165
|
+
sftp, conn, _fps, why = await _open_sftp(host)
|
|
1166
|
+
if sftp is None:
|
|
1167
|
+
return {"commit": "", "branch": "", "root": root, "why": f"{host}: {why}"}
|
|
1168
|
+
try:
|
|
1169
|
+
missing = await _no_repo_at(sftp, ci, root)
|
|
1170
|
+
if missing:
|
|
1171
|
+
return {"commit": "", "branch": "", "root": root, "why": f"{host}: {missing}"}
|
|
1172
|
+
repo_root = await ci.sftp_repo_root(sftp, root)
|
|
1173
|
+
ident = ci.git_identity((await ci.sftp_read_meta(sftp, repo_root))["files"])
|
|
1174
|
+
return {"commit": ident.get("commit") or "", "branch": ident.get("branch") or "", "root": repo_root, "why": ""}
|
|
1175
|
+
except Exception as e:
|
|
1176
|
+
return {"commit": "", "branch": "", "root": root, "why": f"{host}: {type(e).__name__}: {e}"}
|
|
1177
|
+
finally:
|
|
1178
|
+
if conn is not None:
|
|
1179
|
+
conn.close()
|
|
1180
|
+
|
|
1181
|
+
|
|
1144
1182
|
async def remote_sync(client, host: str, root: str, paths: list) -> dict:
|
|
1145
1183
|
"""List and read the repo holding ``root`` on ``host`` over SFTP (the sdk's one walk: .gitignore,
|
|
1146
1184
|
secrets and dependency trees skipped), extract and embed here, and sync it into the vault through
|
|
@@ -1155,6 +1193,10 @@ async def remote_sync(client, host: str, root: str, paths: list) -> dict:
|
|
|
1155
1193
|
if sftp is None:
|
|
1156
1194
|
return {"repo_key": "", "partial_reason": why, "content": f"{host}: {why}"}
|
|
1157
1195
|
try:
|
|
1196
|
+
missing = await _no_repo_at(sftp, ci, root)
|
|
1197
|
+
if missing:
|
|
1198
|
+
# never a walk of the folder above (live 2026-10-25: a root that did not exist synced /home for 30 min)
|
|
1199
|
+
return {"repo_key": "", "partial_reason": missing, "content": f"{host}: {missing}"}
|
|
1158
1200
|
repo_root = await ci.sftp_repo_root(sftp, root)
|
|
1159
1201
|
try: # the repo's identity is its real path: a symlinked dir names the release it points into
|
|
1160
1202
|
real_root = str(await sftp.realpath(repo_root) or repo_root) if hasattr(sftp, "realpath") else repo_root
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""0.6.8: the kernel hears the coding mode (autopilot asks nothing -- owner 2026-10-25: "remove the cards, they stall
|
|
2
|
+
her"); a server's repo is synced only where it is (a missing root walked /home for 30 min) and its HEAD is cheap."""
|
|
3
|
+
import asyncio
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
import webbee.tools as wt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _Entry:
|
|
10
|
+
def __init__(self, name):
|
|
11
|
+
self.filename = name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Sftp:
|
|
15
|
+
def __init__(self, tree, files=None):
|
|
16
|
+
self.tree, self.files = tree, files or {}
|
|
17
|
+
|
|
18
|
+
async def readdir(self, path):
|
|
19
|
+
if path not in self.tree:
|
|
20
|
+
raise FileNotFoundError(path)
|
|
21
|
+
return [_Entry(n) for n in self.tree[path]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_a_missing_root_is_refused_before_any_walk(monkeypatch):
|
|
25
|
+
from imperal_sdk import code_index as ci
|
|
26
|
+
sftp = _Sftp({"/home": ["ipw-repo"], "/": ["home"]})
|
|
27
|
+
why = asyncio.run(wt._no_repo_at(sftp, ci, "/home/no-such-repo"))
|
|
28
|
+
assert "does not exist" in why
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_a_folder_in_no_repository_is_refused(monkeypatch):
|
|
32
|
+
from imperal_sdk import code_index as ci
|
|
33
|
+
sftp = _Sftp({"/srv/photos": ["a.jpg"], "/srv": ["photos"], "/": ["srv"]})
|
|
34
|
+
assert "in no repository" in asyncio.run(wt._no_repo_at(sftp, ci, "/srv/photos"))
|
|
35
|
+
repo = _Sftp({"/r": [".git", "a.py"], "/": ["r"]})
|
|
36
|
+
assert asyncio.run(wt._no_repo_at(repo, ci, "/r")) == ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_remote_sync_of_a_missing_root_says_why_and_lists_nothing(monkeypatch):
|
|
40
|
+
sftp = _Sftp({"/": ["home"], "/home": []})
|
|
41
|
+
|
|
42
|
+
async def opened(host):
|
|
43
|
+
return sftp, None, [], ""
|
|
44
|
+
monkeypatch.setattr(wt, "_open_sftp", opened)
|
|
45
|
+
out = asyncio.run(wt.remote_sync(None, "h", "/home/nope", []))
|
|
46
|
+
assert out["repo_key"] == "" and "does not exist" in out["partial_reason"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_remote_head_reads_the_commit(monkeypatch):
|
|
50
|
+
from imperal_sdk import code_index as ci
|
|
51
|
+
sftp = _Sftp({"/r": [".git"], "/": ["r"]})
|
|
52
|
+
|
|
53
|
+
async def opened(host):
|
|
54
|
+
return sftp, None, [], ""
|
|
55
|
+
|
|
56
|
+
async def meta(s, root):
|
|
57
|
+
return {"files": {".git/HEAD": b"ref: refs/heads/master\n", ".git/refs/heads/master": b"fd751713" + b"0" * 32}}
|
|
58
|
+
monkeypatch.setattr(wt, "_open_sftp", opened)
|
|
59
|
+
monkeypatch.setattr(ci, "sftp_read_meta", meta)
|
|
60
|
+
got = asyncio.run(wt.remote_head("h", "/r"))
|
|
61
|
+
assert got["commit"].startswith("fd751713") and got["branch"] == "master" and got["why"] == ""
|
|
62
|
+
|
|
63
|
+
async def unreachable(host):
|
|
64
|
+
return None, None, [], "OSError: Name or service not known"
|
|
65
|
+
monkeypatch.setattr(wt, "_open_sftp", unreachable)
|
|
66
|
+
assert "Name or service not known" in asyncio.run(wt.remote_head("nope", "/r"))["why"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_a_mode_switch_reaches_the_running_session(monkeypatch):
|
|
70
|
+
import webbee.repl as R
|
|
71
|
+
sent = []
|
|
72
|
+
|
|
73
|
+
class _Agent:
|
|
74
|
+
session_id = "imp_u_a-r1"
|
|
75
|
+
mode = "default"
|
|
76
|
+
|
|
77
|
+
async def post_mode(self, mode):
|
|
78
|
+
sent.append(mode)
|
|
79
|
+
return True
|
|
80
|
+
slot = SimpleNamespace(mode="default", agent=_Agent(), workspace="/w")
|
|
81
|
+
monkeypatch.setattr("webbee.mode_store.save_mode", lambda ws, m: None)
|
|
82
|
+
|
|
83
|
+
async def go():
|
|
84
|
+
R.set_slot_mode(slot, "autopilot")
|
|
85
|
+
await asyncio.sleep(0)
|
|
86
|
+
await asyncio.sleep(0)
|
|
87
|
+
asyncio.run(go())
|
|
88
|
+
assert slot.agent.mode == "autopilot" and sent == ["autopilot"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_no_session_yet_sends_nothing(monkeypatch):
|
|
92
|
+
import webbee.repl as R
|
|
93
|
+
|
|
94
|
+
class _Agent:
|
|
95
|
+
session_id = ""
|
|
96
|
+
mode = "default"
|
|
97
|
+
|
|
98
|
+
async def post_mode(self, mode):
|
|
99
|
+
raise AssertionError("posted without a session")
|
|
100
|
+
slot = SimpleNamespace(mode="default", agent=_Agent(), workspace="/w")
|
|
101
|
+
monkeypatch.setattr("webbee.mode_store.save_mode", lambda ws, m: None)
|
|
102
|
+
|
|
103
|
+
async def go():
|
|
104
|
+
R.set_slot_mode(slot, "plan")
|
|
105
|
+
await asyncio.sleep(0)
|
|
106
|
+
asyncio.run(go())
|
|
107
|
+
assert slot.agent.mode == "plan"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.7"
|
|
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
|
|
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
|
|
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
|