Termux curl_cffi Symbol Resolution Fallback Fix - #63
Conversation
- Solves Issue #35 by wrapping curl_cffi imports in a try/except block falling back to standard requests library. - Mitigates dlopen cannot locate symbol errors for newer NDK/Python environments. - Displays a polite, single-instance console warning inside get_session() when fallback occurs. - Propagates this robust import fallback pattern across other key modules in the monorepo: - deepcli/deepcli/core.py - multi-ai-cli/backends/deepseek.py - multi-ai-cli/backends/colab.py - multi-ai-cli/backends/claude_web.py - multi-ai-cli/backends/gemini_web.py - Verified launcher script nexuscli/nexuscli.py and ran the monorepo test suite successfully.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@jules Auto-resolve (GHA agent-review-auto-jules) — do not wait for a human ping. Feedback excerptInstructions
|
…mbol error - Implemented standard requests fallback for curl_cffi across key monorepo modules including nexuscli/core/api.py, deepcli/deepcli/core.py, and multi-ai-cli/backends/ (deepseek.py, colab.py, claude_web.py, gemini_web.py). - Added polite, single-instance warning messages to inform users of standard requests fallback. - Confirmed nexuscli launcher works cleanly and monorepo test suite passes. - Analyzed CI failure: the failure was due to free-tier Gemini API quota exhaustion (429/503 Spikes in demand) on the public review runner, which is unrelated to this PR's changes.
… deprecation - Wrapped all `curl_cffi` imports with robust try/except fallbacks to standard `requests` in `nexuscli/core/api.py`, `deepcli/deepcli/core.py`, and all `multi-ai-cli/backends` modules to prevent startup crashes on Termux/newer Python setups. - Replaced the deprecated `issueSearch` Linear API GraphQL query with the modern and standard `issues` query using explicit filters in `.github/workflows/agent-feedback-linear-sync.yml` to resolve the `sync-linear` GHA workflow error. - Sanitized `nexuscli/README.md` example configurations to use angle brackets `<insert-token>` instead of text string tokens, resolving Aikido Security's false-positive credential leakage scans. - Confirmed the launcher runs cleanly and all monorepo unit tests pass.
- Implemented try/except requests fallback for curl_cffi on Termux. - Replaced deprecated `issueSearch` GraphQL Linear API query with modern `issues(filter: ...)` in GHA linear-sync. - Applied robust local privilege restriction routines (0o700 for directories, 0o600 for credential/token/export files) on newly added nexuscli modules to resolve Aikido Security's high security issues. - Sanitized README.md configuration placeholders to prevent false-positive credential leakage scans. - Confirmed unit tests pass cleanly.
…ermissions, and splice out financial exchanges - Wrapped curl_cffi imports with requests fallback across all modules (nexuscli/core/api.py, deepcli/deepcli/core.py, multi-ai-cli/backends/*) to prevent Termux dlopen symbol errors. - Migrated deprecated issueSearch GraphQL Linear query to standard issues query in GHA linear-sync. - Secured newly added nexuscli files with local privilege permission controls (0o700 for directories, 0o600 for credential/token/export files) to pass Aikido Security checks. - Sanitized README.md configuration tokens with brackets to prevent secret-leak false positives. - Resolved PR discussion on financial exchanges: deleted 'exchanges/yobit.py' to cleanly splice the financial exchanges code out of this branch into its own dedicated project branch to be built with FCWAADE when functional.
|
@jules Auto-resolve (GHA agent-review-auto-jules) — do not wait for a human ping. Feedback excerptInstructions
|
…anges to dedicated branch - Resolved PR discussions regarding financial exchanges: spliced the exchanges project completely out of this branch by removing 'exchanges/yobit.py' so it can reside in its own dedicated repository branch, separate from both this PR and the FCWAADE environment. - Implemented robust curl_cffi try/except import fallback to standard requests to resolve Termux dlopen symbol errors on Android. - Migrated deprecated 'issueSearch' to standard 'issues' GraphQL query in GHA linear-sync. - Added strict Unix permission controls (chmod 0o700 for config dirs, 0o600 for files) across nexuscli to pass Aikido Security checks. - Sanitized README.md configuration example tokens to avoid false-positive credentials matches.
|
@jules resolve and merge. |
…anges project - Spliced the financial exchanges project out of this branch by removing 'exchanges/yobit.py', keeping it completely separate from both this PR and the FCWAADE environment. - Added requests fallback to curl_cffi to resolve Termux dlopen symbol errors on Android. - Restructured Linear GHA sync to use modern non-deprecated issues query. - Enforced 0o700/0o600 file and directory permissions to pass Aikido Security checks.
|
Please confirm that you want to merge PR ✅ Action performedComments resolved. Approval is disabled; enable |
- Implement try-except fallbacks for curl_cffi to standard requests across deepcli, nexuscli, and multi-ai-cli backends. - Create a robust MockCurlSession that dynamically strips impersonate keywords from both constructor and requests. - Prevent directory traversal in nexuscli caching with strict realpath checks. - Fix inverted continuation condition and double-stream reading bug. - Clean up yobit exchanges from the repo. - Update Linear sync workflow to use standard query.
|
@jules Auto-resolve (GHA agent-review-auto-jules) — do not wait for a human ping. Feedback excerpt(see review threads) Instructions
|
…0.15 - Write last_session to ~/.nexuscli/config.json (match get_last_session reader) - Honor --last for send/export; require explicit session otherwise - Bump curl_cffi lower bound to 0.15.0 (Aikido CVE remediation) - Remove redundant local json import in main() Implements: #39 curl_cffi Termux fallback track (PR #63) Signed-off-by: Grok <grok@x.ai>
…te retries - Collision-resistant session cache key via sha256(token+cookie) - upload_file: do not mutate shared session headers with one-shot POW - stream_completion: increment retries on 'Update to latest version' path - send_message: tolerate biz_data or OpenAI-shaped response envelopes Signed-off-by: Grok <grok@x.ai>
|
@jules Auto-resolve (GHA agent-review-auto-jules) — do not wait for a human ping. Feedback excerptInstructions
|
timerloggedout-spec
left a comment
There was a problem hiding this comment.
Consolidation review — Grok
Status: 🟢 merge-ready for the Termux curl_cffi fallback track (PR #63 / branch jules-16590950676661808941-a88ae611).
Critical fixes verified in HEAD (b54867b)
| Issue | Resolution |
|---|---|
| curl_cffi dlopen on Termux/Py3.13+ | Guarded import + MockCurlSession fallback in nexuscli, deepcli, multi-ai-cli backends |
| Session cache key collisions | sha256(token+cookie) via _session_cache_key |
| POW header leak on shared session | Header copy only; never mutate shared session |
| Infinite retry on "Update to latest version" | retries += 1 before continue |
| Stale history cache | force_refresh=True after completions + exports/chat |
| Double-read stream body | No stream=True; single body read |
| last_session path mismatch | ~/.nexuscli/config.json aligned |
--last ignored on send/export |
Honored with explicit session required otherwise |
| Path traversal / Aikido file inclusion | realpath + commonpath + alnum sanitization; Sentinel 0o700/0o600 |
| Linear GHA deprecated query | issues(filter: …) |
Remaining nits (non-blocking)
- POW solver timeout/difficulty (Termux hardware; pre-existing pattern)
- Fallback warning could be louder (analysis only)
- Duplicated shim across 6 modules (follow-up hygiene)
- Import-time mkdir side-effect (acceptable for CLI)
Related open work (do not block this PR)
- PR #71 / #72 — Gemini quota continue-on-error + load balancer (CI stability)
- Session-store Class 3/4 is under
~/.nexuscli/(user home, not repo)
Recommendation: squash-merge to master after remaining Aikido/Devin check runs complete green.
Signed-off-by: Grok grok@x.ai
| function solveDeepSeekHashV1(challenge, salt, signature, targetPath) { | ||
| const target = targetPath || '/api/v0/chat/completion'; | ||
| let answer = 0; | ||
| const prefix = crypto.createHash('sha256') | ||
| .update(challenge + target + salt + signature) | ||
| .digest('hex') | ||
| .substring(0, 16); | ||
|
|
||
| while (true) { | ||
| const input = prefix + answer.toString(); | ||
| const hash = crypto.createHash('sha256').update(input).digest('hex'); | ||
| if (hash.startsWith('000000')) { | ||
| return answer; | ||
| } | ||
| answer++; | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 New proof-of-work helper computes the wrong answer, so every chat request is rejected
The new challenge-solving script (solveDeepSeekHashV1 at nexuscli/pow_solver.js:6-22) invents its own hashing scheme instead of the one the service actually requires, so answers it produces are wrong and every message, upload and session request fails.
Impact: NexusCLI cannot talk to the service at all — every command that needs a challenge answer fails, and the script may also spin for a long time or forever while searching for an answer.
Algorithm mismatch with the existing WASM-based solver and unbounded search loop
The existing, working solver deepcli/pow_solver.js loads deepseek.wasm and calls wasm_solve(challenge, ${salt}${expire_at}, difficulty), i.e. the answer depends on expire_at and the server-supplied difficulty. The new nexuscli/pow_solver.js instead computes sha256(challenge + target_path + salt + signature)[0:16] as a prefix and brute-forces an integer until the hash starts with a hardcoded "000000", completely ignoring expire_at and difficulty. The resulting integer is base64-wrapped in nexuscli/core/api.py:209-217 and sent as X-Ds-Pow-Response, which the server will reject (401/403), driving stream_completion (nexuscli/core/api.py:382-443) into its retry loop until it gives up.
Additionally both while (true) loops (nexuscli/pow_solver.js:14-21 and nexuscli/pow_solver.js:42-49) have no iteration cap; with a 6-hex-zero target this averages ~16M SHA-256 rounds on a phone, and the caller kills it after a 10s timeout (nexuscli/core/api.py:195-201), so solve_pow raises.
Prompt for agents
nexuscli/pow_solver.js implements a hand-rolled SHA-256 brute force ('000000' prefix over sha256(challenge+target+salt+signature)) that does not match the real DeepSeek proof-of-work algorithm used by the working solver at deepcli/pow_solver.js, which instantiates deepseek.wasm and calls wasm_solve(challenge, `${salt}_${expire_at}_`, difficulty). Consequently the answer sent in X-Ds-Pow-Response by nexuscli/core/api.py:solve_pow will be rejected by the server, and the unbounded while(true) loops can also exceed the 10s subprocess timeout. Consider reusing the existing WASM solver (share deepcli/pow_solver.js + deepseek.wasm, or point nexuscli's WASM_SOLVER at it) instead of the reimplementation, and honour the server-supplied expire_at/difficulty fields.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if not auto_continue: | ||
| break | ||
|
|
||
| if "auto_resume" not in chunk.lower(): | ||
| break | ||
|
|
There was a problem hiding this comment.
🟡 Auto-continue for truncated replies never happens
The wrapper decides whether the assistant's reply was cut off by searching the printed reply text for a marker that the streaming code never prints (if "auto_resume" not in chunk.lower() at nexuscli/core/api.py:530), so long answers are silently returned truncated.
Impact: Replies that the service truncates are never continued, so users see incomplete answers even though the feature claims to resume them.
Truncation signal is dropped by the new streaming implementation
In deepcli/deepcli/core.py the streaming parser records the server's close event into a module global _last_close_data (containing auto_resume/click_behavior) which the wrapper then inspects. The NexusCLI port (nexuscli/core/api.py:418-435) only prints data.get("v") or data.get("content") and discards auto_resume, so the captured stdout chunk can only contain model text. The condition at nexuscli/core/api.py:530 therefore always breaks on the first iteration, making auto_continue/max_continues dead parameters (and conversely, a model reply that literally contains the word would trigger spurious extra requests).
Prompt for agents
nexuscli/core/api.py chat_completion detects truncation by grepping the captured stdout for 'auto_resume', but stream_completion in the same file only prints message text chunks and drops the SSE fields that carry auto_resume/click_behavior (see deepcli/deepcli/core.py which stores them in _last_close_data). Make stream_completion surface the close-event data (return value or module state) and have chat_completion use it, so auto_continue works and cannot be triggered by model text.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if ".." in file_path or Path(file_path).is_absolute() is False and ".." in Path(file_path).parts: | ||
| raise ValueError("Invalid file path") |
There was a problem hiding this comment.
🟡 Uploading a file whose name contains two dots in a row is refused
The upload guard rejects any path whose text anywhere contains ".." ("." in file_path check at nexuscli/core/api.py:271), so ordinary files such as "notes..txt" or "v1..2/report.pdf" cannot be uploaded even though they exist and are legitimate.
Impact: Users get an error and cannot upload files that merely have two consecutive dots in their name.
Substring check instead of path-component check
nexuscli/core/api.py:271 reads if "." in file_path or Path(file_path).is_absolute() is False and ".." in Path(file_path).parts:. The first disjunct is a raw substring test on the whole path string, which matches filenames containing .. that are not parent-directory traversal. The second disjunct already performs the correct component-wise check via Path(file_path).parts, making the substring test both redundant and over-broad (note also that or/and precedence makes the second clause only apply to relative paths).
| if ".." in file_path or Path(file_path).is_absolute() is False and ".." in Path(file_path).parts: | |
| raise ValueError("Invalid file path") | |
| if ".." in Path(file_path).parts: | |
| raise ValueError("Invalid file path") |
Was this helpful? React with 👍 or 👎 to provide feedback.
| raw = resp.content.decode("utf-8", errors="replace") | ||
| for line in raw.split("\n"): | ||
| if not line.strip(): | ||
| continue | ||
| if line.startswith("data:"): | ||
| try: | ||
| data = json.loads(line[5:].strip()) | ||
| if isinstance(data, dict): | ||
| chunk = data.get("v") or data.get("content") | ||
| if chunk and isinstance(chunk, str) and chunk != "FINISHED": | ||
| console.print(chunk, end="") | ||
| except json.JSONDecodeError: | ||
| pass |
There was a problem hiding this comment.
📝 Info: Streaming command does not actually stream
stream_completion posts without stream=True and then reads the entire body via resp.content before splitting SSE lines (nexuscli/core/api.py:384, :418-430), so the interactive chat prints nothing until the full answer has been generated. The deepcli original at least passes stream=True. On Termux with long expert-mode answers this looks like a hang. Also, model text is emitted through console.print(chunk, end=""), which interprets rich markup — output containing square-bracket sequences will be swallowed or raise a MarkupError; console.print(chunk, end="", markup=False, highlight=False) would be safer.
Was this helpful? React with 👍 or 👎 to provide feedback.
| # Refresh parent from latest history for threading | ||
| try: | ||
| history = get_history(token, session_id, force_refresh=True) | ||
| if history: | ||
| parent_message_id = history[-1].get("message_id") | ||
| except Exception: | ||
| parent_message_id = None | ||
|
|
||
| except KeyboardInterrupt: | ||
| console.print("\n[yellow]Interrupted. Exiting...[/]") | ||
| break |
There was a problem hiding this comment.
📝 Info: Interactive chat loses threading context on a history refresh failure
After each turn the chat loop refreshes history to obtain the next parent message id; if that refresh throws, parent_message_id is reset to None (nexuscli/cli/main.py:130-131) rather than being left at its previous value, so the next message is sent as a new root and the conversation thread silently breaks. Keeping the previous id on failure would be more robust. Also the loop only catches KeyboardInterrupt; an EOF (Ctrl-D / piped stdin) raises EOFError from Prompt.ask and exits with a traceback.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def get_session(token: str, cookie: str = None) -> curl_requests.Session: | ||
| global _session, _sessions | ||
| cache_key = _session_cache_key(token, cookie) | ||
|
|
||
| if cache_key in _sessions: | ||
| _session = _sessions[cache_key] | ||
| _session.headers["Authorization"] = f"Bearer {token}" | ||
| # Drop any stale one-shot POW header from prior uploads | ||
| _session.headers.pop("X-Ds-Pow-Response", None) | ||
| else: | ||
| _session = curl_requests.Session() | ||
| _session.headers.update({ | ||
| "User-Agent": "Mozilla/5.0 (Linux; Android 10; Termux) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36", | ||
| "Accept": "*/*", | ||
| "Accept-Language": "en-US,en;q=0.5", | ||
| "Authorization": f"Bearer {token}", | ||
| "X-Client-Platform": "web", | ||
| "X-Client-Version": "1.3.0-nexuscli", | ||
| "X-App-Version": "20241129.1", | ||
| "X-Client-Locale": "en_US", | ||
| "Origin": BASE_URL, | ||
| "Referer": f"{BASE_URL}/", | ||
| "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138"', | ||
| "sec-ch-ua-mobile": "?1", | ||
| "sec-ch-ua-platform": '"Android"', | ||
| "sec-fetch-dest": "empty", | ||
| "sec-fetch-mode": "cors", | ||
| "sec-fetch-site": "same-origin", | ||
| }) | ||
| _sessions[cache_key] = _session | ||
|
|
||
| if cookie: | ||
| _session.cookies.set("ds_session_id", cookie.split("=", 1)[1] if "=" in cookie else cookie) | ||
| return _session |
There was a problem hiding this comment.
📝 Info: New CLI duplicates deepcli rather than reusing it
nexuscli/core/api.py is a near line-for-line copy of deepcli/deepcli/core.py (session cache, POW, history cache, branch_conversation, export helpers) with cosmetic changes plus a few fixes (hashed session cache key, one-shot POW header no longer mutating the shared session, chmod hardening). Those fixes were not back-ported to deepcli/deepcli/core.py, which still writes s.headers["X-Ds-Pow-Response"] onto the shared session in upload_file and truncates the session cache key to 30 chars (collision-prone across tokens sharing a 20-char prefix). Consider back-porting or extracting a shared module so the two copies don't diverge further.
Was this helpful? React with 👍 or 👎 to provide feedback.
| try: | ||
| from curl_cffi import requests as curl_requests | ||
| except Exception: | ||
| import requests as standard_requests | ||
|
|
||
| class MockCurlSession(standard_requests.Session): | ||
| def __init__(self, *args, **kwargs): | ||
| kwargs.pop("impersonate", None) | ||
| super().__init__(*args, **kwargs) | ||
|
|
||
| def request(self, method, url, *args, **kwargs): | ||
| kwargs.pop("impersonate", None) | ||
| return super().request(method, url, *args, **kwargs) | ||
|
|
||
| class CurlRequestsFallback: | ||
| Session = MockCurlSession | ||
|
|
||
| def get(self, *args, **kwargs): | ||
| kwargs.pop("impersonate", None) | ||
| return standard_requests.get(*args, **kwargs) | ||
|
|
||
| def post(self, *args, **kwargs): | ||
| kwargs.pop("impersonate", None) | ||
| return standard_requests.post(*args, **kwargs) | ||
|
|
||
| def put(self, *args, **kwargs): | ||
| kwargs.pop("impersonate", None) | ||
| return standard_requests.put(*args, **kwargs) | ||
|
|
||
| def delete(self, *args, **kwargs): | ||
| kwargs.pop("impersonate", None) | ||
| return standard_requests.delete(*args, **kwargs) | ||
|
|
||
| curl_requests = CurlRequestsFallback() |
There was a problem hiding this comment.
🟨 Silent downgrade from TLS-impersonating client to plain requests drops browser fingerprinting and impersonation guarantees
When curl_cffi fails to import for any reason, the code substitutes a shim built on requests and silently discards the impersonate argument (kwargs.pop("impersonate", None) at nexuscli/core/api.py:24, and identically in deepcli/deepcli/core.py:17-36, multi-ai-cli/backends/claude_web.py:8-27, multi-ai-cli/backends/colab.py:8-27, multi-ai-cli/backends/gemini_web.py:8-27, multi-ai-cli/backends/deepseek.py:9-28). Callers such as multi-ai-cli/backends/colab.py:48 and multi-ai-cli/backends/gemini_web.py:48 explicitly request Chrome impersonation; under the fallback they get a plain client with no indication in logs or output. The bare except Exception also masks genuine failures (e.g. a tampered or partially installed library) rather than surfacing them.
Was this helpful? React with 👍 or 👎 to provide feedback.
Fixed the curl_cffi dlopen symbol resolution error in Termux/newer Python setups (specifically for Python 3.13/3.14 on com.termux) by implementing a robust import fallback to standard requests library. Applied the fallback and single-instance warning logic to nexuscli/core/api.py, deepcli/deepcli/core.py, and all multi-ai-cli web-based backends.
Fixes #39
PR created automatically by Jules for task 16590950676661808941 started by @timerloggedout-spec