P2P WebRTC transport + daemon: seamless CDP over data channel - #15
Merged
Conversation
added 10 commits
July 19, 2026 15:09
…e import logging, visible stderr error - Sync __init__.__version__ to 2.36.0 matching pyproject.toml - Close log_file handle after subprocess.Popen in _ensure_daemon() and _cmd_daemon_start() - Move import logging to top-level imports - Add user-visible stderr error on daemon auto-start failure in _ensure_daemon() - Remove redundant is_running() check after for-loop (proc.poll() already covers it) - Add .claude/ .graphifyignore .mypy_cache/ to .gitignore
Missing [project.scripts] ceki-daemon = ceki_sdk.daemon:main entry point — the published PyPI package had no ceki-daemon binary. Every Joe review round (4400, 4439, 4444) cited this as the blocker.
Same wire call, same args, different name — lets AI agents express 'edit the task' intent instead of 'propose a correction'. - New top-level subcommand 'edit' (ceki edit <eid> --label ...) - Delegates to ContractClient.propose() under the hood - Accepts all the same args: --status, --label, --desc, --start, --end, --date, --duration, --amount, --currency, --benefitable, --tags
Added 'edit' as a contract subparser (ceki contract edit <eid> ...) that delegates to ContractClient.propose() — same args, same wire call, different name for 'edit task' intent.
- WebRTCTransport: wraps aiortc RTCPeerConnection for P2P CDP
over DataChannel('ceki-cmd') with ICE candidate queuing
- _client.py: P2P init, webrtc.offer/answer/ice_candidate dispatch
- _browser.py: DC-aware Browser.send() — P2P transport preferred
over WS fallback when ceki-cmd DC is open
- CEKI_FORCE_WS env flag, CEKI_TURN_SERVERS env,
CEKI_ICE_TRANSPORT_POLICY env
- 41 unit tests in tests/test_webrtc_p2p.py (134/135 pass,
1 pre-existing failure in test_cli.py)
- pyproject.toml: aiortc>=1.9,<2 dependency
BUG: In P2P mode (cmd_dc_open=true), CDP responses arrive via BOTH
WS relay (as cdp_response) and the ceki-cmd data channel. The WS
response arrives first but has empty result {} for large payloads
like screenshot base64. The pending CDP future resolves with empty
data before the DC response with real data arrives.
FIX: In _reader_loop, when P2P is active and cmd_dc_open is true,
skip WS cdp_response messages. The DC response arrives and resolves
the future with complete data. When DC is not open (WS-only mode),
WS cdp_response is handled as before.
Affected: screenshot(), snapshot() return 0 bytes in P2P mode without this fix.
v1 (25cb42e) was too broad: skipped ALL WS cdp_response when P2P DC was open, including responses for commands sent via WS before P2P connected. v2 fix: tag each pending CDP future with its transport ('dc' or 'ws') at send time. _on_cdp_response only skips WS echo for futures tagged as 'dc' (the screenshot race). WS-sent commands resolve normally even if P2P connects before their response arrives. Key changes: - Browser.send(): tag fut._cdp_transport before storing - Browser._on_cdp_response(): check tag + message origin; skip WS echo for DC-sent commands, accept all others
added 7 commits
July 24, 2026 08:32
…context managers, orphan cleanup - Browser.send(): catch DC send failure → auto-fallback to WS with _p2p_fallback flag - Client._init_p2p(): wire on_connection_state + on_data_channel_state for lifecycle logging - Client._p2p_set_remote(): handle webrtc.answer with error logging - Client.__aenter__/__aexit__: context manager for async with - Browser.__aenter__/__aexit__: context manager for async with - connect(): register SIGINT/SIGTERM cleanup handlers for orphan prevention
- Add _dc_open_event (asyncio.Event) to WebRTCTransport - Clear before createOffer, set when ceki-cmd DC opens - Browser.send() now waits up to 5s for DC readiness before sending CDP - Timeout → WS fallback (sticky _p2p_fallback for the session) - Prevents startup-race WS congestion that starves heartbeat ping Part of ev 4864 P2P seamless review fix (GAP1/Joe)
join() sent {type:'attach', browser_id} but relay attachSchema
expects schedule_id. Result: -1014 Invalid attach payload.
Fix: send schedule_id matching relay schema.
_dispatch() didn't handle webrtc.answer — P2P answer never reached WebRTCTransport.setRemoteDescription. Add dispatch to _p2p_set_remote. Also add timing debug logging in Browser.send() and fix CDP cleanup.
…ntee first CDP via DC (ev 4902) - _p2p_ready Event signals when self._p2p is assigned - rent() waits up to 15s for P2P transport init before returning Browser - Browser.send() DC timeout 5s→30s, no permanent _p2p_fallback on TimeoutError - ConnectionError/OSError still sets permanent WS fallback (DC broken)
wait_dc_open() was placed before create_offer() — DC didn't exist yet, resulting in 15s timeout and permanent P2P disable. Fix: wait for DC open AFTER offer is created and sent. _p2p_ready signals only when DC is actually usable. rent() then returns Browser with P2P already active — first CDP guaranteed via DC. Also await _p2p_set_remote in _dispatch() instead of create_task for proper ordering.
iWedmak
added a commit
that referenced
this pull request
Sep 4, 2026
* fix(sdk): daemon session-death cleanup + shared client WS reuse (ev 6621) - _client: accept the relay's session_ended alias and event_id field, and route error -1011/-1018 (provider death / grace expiry / admin kill) through the same cleanup as session.ended. Call an optional _on_session_ended hook so the daemon learns about relay-initiated ends. - daemon: share ONE Client per api_key — all sessions multiplex over a single WebSocket. Disconnect only when the last session for a key ends, fixing the per-rent WS leak that orphaned live connections on the relay. - cli: _daemon_request now returns (ok, result). A null result from a void command (navigate/click/type/scroll/switch-tab/configure/stop/...) is a SUCCESS, not a daemon-down signal — commands no longer fall through to the one-shot resume fallback that disconnects and kills the rented session. - tests: test_daemon.py (shared client, cleanup, session_ended/-1011 paths) * style(client): wrap overlong cdp_response debug line (E501) The line predates this branch (from #15) but fails ruff on CI, which runs on feature/** pushes — so it would have broken this branch's check. * feat(sdk): provider mode as a docker-browser wrapper (ev 6745) ceki provider run no longer reimplements the provider launcher. It pulls and runs the official image ceki/provider (Docker Hub), whose launcher lives in the public repo Ceki-me/docker-browser — the single source of truth for the provider. The SDK only orchestrates docker: - _provider: token/image resolution, docker presence check, optional local build, image inspect/pull, docker run. Only the public provider envs (CEKI_PROVIDER_VIEWPORT/LOG_LEVEL, TZ, DISPLAY) are passed through; internal docker-browser envs are not part of the SDK contract. - --timeout is passed by appending the image's default command, since docker run args replace the image CMD; the container self-stops after N seconds. - docker stop / Ctrl-C reach the provider (PID 1) and shut it down cleanly. Docs: README provider section (public env table, docker-browser link). Tests: test_provider.py (token/env/image resolution, env map incl. internal envs NOT forwarded, docker run cmd, inspect+pull+run orchestration). * feat(provider): fall back to GHCR image when the Docker Hub pull fails ceki provider run tries ceki/provider:latest (Docker Hub) first; if the pull fails the run proceeds with the GHCR build ghcr.io/ceki-me/docker-browser:latest that docker-browser CI publishes on every GitHub Release. An explicit --image (or $CEKI_PROVIDER_IMAGE) is used as-is and never swapped. * fix(daemon): drop half-dead shared WS client and retry rent once A cached client can go half-dead (relay stops routing rent/match without a close frame): TCP stays ESTABLISHED, recv() never raises, pongs keep coming, so every rent through it times out and the daemon 504s forever. On rent TimeoutError/ConnectionLost the poisoned client is now dropped from the cache (and its orphan sessions discarded) and the rent is retried ONCE on a fresh connection. Also fix the shared-client exception path: only disconnect the cached client when it still has no sessions and is still the one cached — the old 'created' flag disconnected a shared client that other requests were using. rent() timeout now also removes the dead future from _pending_rents so a long-running client (the daemon) does not accumulate one entry per timed-out rent. * feat: add prompts/list and prompts/get support in raw() - raw() now checks for tool.startswith('prompts/') and calls _rpc() directly - prompts/list and prompts/get are MCP protocol methods, not tools - Allows: ceki contract raw prompts/list '{}' and ceki contract raw prompts/get '{"name":"..."}' --------- Co-authored-by: ceki-plugin <hello@ceki.me> Co-authored-by: iWedmak <iWedmak@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Python SDK: P2P WebRTC transport + daemon + seamless lifecycle
What's included:
P2P WebRTC transport — aiortc-based RTCPeerConnection with ceki-cmd data channel for CDP commands/responses. Primary CDP path with WS fallback. 41 tests.
Seamless integration — SDK API unchanged (connect → rent → cdp/navigate/screenshot). P2P is under the hood, user code stays the same.
DC→WS fallback — if P2P data channel send fails mid-session, auto-fallback to WS without losing the command.
_p2p_fallbackflag prevents repeated DC attempts.P2P lifecycle monitoring — connection state + data channel state callbacks with full logging. WebRTC failure → cleanup
_p2p→ transparent WS fallback.Screenshot race fix — per-command transport tagging: skip WS echo for DC-sent commands. Prevents empty screenshot from WS echo racing ahead of full DC response.
Orphan cleanup — SIGINT/SIGTERM signal handlers in
connect()shut down all browsers + disconnect. Client + Browser context managers (async with) for clean teardown.ceki-daemon — persistent renter-process with local HTTP/JSON IPC server. Auto-starts on rent command.
CEKI_FORCE_WS — env var to force WS path for regression testing.
Usage:
No P2P-specific code needed — it's automatic.