wechatbridge-cli 1.4.9__tar.gz → 1.6.0__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 (27) hide show
  1. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/PKG-INFO +42 -15
  2. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/README.md +40 -14
  3. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/pyproject.toml +1 -0
  4. wechatbridge_cli-1.6.0/tests/test_dsh.py +1749 -0
  5. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/tests/test_hardening.py +372 -0
  6. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/__init__.py +1 -1
  7. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/config.py +68 -2
  8. wechatbridge_cli-1.6.0/wechatbridge/dsh.py +1084 -0
  9. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/main.py +61 -23
  10. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/runner_common.py +106 -27
  11. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge_cli.egg-info/PKG-INFO +42 -15
  12. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge_cli.egg-info/SOURCES.txt +2 -0
  13. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge_cli.egg-info/requires.txt +1 -0
  14. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/LICENSE +0 -0
  15. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/setup.cfg +0 -0
  16. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/tests/test_codex.py +0 -0
  17. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/tests/test_codex_auth.py +0 -0
  18. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/tests/test_grok_auth.py +0 -0
  19. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/__main__.py +0 -0
  20. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/agy.py +0 -0
  21. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/codex.py +0 -0
  22. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/grok.py +0 -0
  23. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/ilink.py +0 -0
  24. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge/update_check.py +0 -0
  25. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge_cli.egg-info/dependency_links.txt +0 -0
  26. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge_cli.egg-info/entry_points.txt +0 -0
  27. {wechatbridge_cli-1.4.9 → wechatbridge_cli-1.6.0}/wechatbridge_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wechatbridge-cli
3
- Version: 1.4.9
3
+ Version: 1.6.0
4
4
  Summary: Bridge WeChat messages to agy, Grok Build, or Codex CLIs — text/image/file/voice in, CLI replies and generated files back.
5
5
  Author: WeChatBridge contributors
6
6
  License: MIT
@@ -27,6 +27,7 @@ Requires-Dist: httpx
27
27
  Requires-Dist: qrcode
28
28
  Requires-Dist: Pillow
29
29
  Requires-Dist: cryptography
30
+ Requires-Dist: PyYAML
30
31
  Dynamic: license-file
31
32
 
32
33
  # WeChatBridge
@@ -36,21 +37,21 @@ Dynamic: license-file
36
37
  ![license](https://img.shields.io/badge/license-MIT-blue.svg)
37
38
  ![python](https://img.shields.io/badge/python-3.10+-blue.svg)
38
39
 
39
- WeChatBridge connects a WeChat bot to agentic coding CLIs (Google's agy / Antigravity, xAI's Grok Build, or OpenAI's Codex). From WeChat you can send text, images, files, and voice-as-text to the active CLI, get replies back, and receive certain generated files over the WeChat CDN. Switch backends per user with `/backend` — no restart.
40
+ WeChatBridge connects a WeChat bot to agentic coding CLIs (Google's agy / Antigravity, xAI's Grok Build, OpenAI's Codex, or DeepSeek Harness' dsh). From WeChat you can send text, images, files, and voice-as-text to the active CLI, get replies back, and receive certain generated files over the WeChat CDN. Switch backends per user with `/backend` — no restart.
40
41
 
41
42
  ```
42
- WeChat (phone) ⇄ iLink bot API ⇄ WeChatBridge ⇄ agy / grok / codex CLI
43
+ WeChat (phone) ⇄ iLink bot API ⇄ WeChatBridge ⇄ agy / grok / codex / dsh CLI
43
44
  (this project) (runs tools)
44
45
  ```
45
46
 
46
- The bridge process stays up and long-polls iLink. For prompts that go to a CLI, it spawns one `agy` or `grok` child (`-p` single-turn) and exits that child when done — the child does not stay resident. Many slash commands (`/help`, `/backend`, `/persona`, …) are handled inside the bridge and never start a CLI. Only artifacts the bridge can detect under the user's allowed session paths are pushed back via CDN.
47
+ The bridge process stays up and long-polls iLink. For prompts that go to a CLI, it spawns one `agy` / `grok` / `codex` / `dsh` child (single-turn) and exits that child when done — the child does not stay resident. Many slash commands (`/help`, `/backend`, `/persona`, …) are handled inside the bridge and never start a CLI. Only artifacts the bridge can detect under the user's allowed session paths are pushed back via CDN.
47
48
 
48
49
  ## Features
49
50
 
50
- - Text, image, file, and voice (WeChat server-side transcription only) go to the **active** backend (`agy`, `grok`, or `codex`)
51
+ - Text, image, file, and voice (WeChat server-side transcription only) go to the **active** backend (`agy`, `grok`, `codex`, or `dsh`)
51
52
  - Detected CLI artifacts under the per-user allowed tree can be sent back (size-capped); not every file the CLI touches
52
53
  - Each WeChat user gets an isolated workspace; model / effort / mode are remembered **per backend**
53
- - Runtime backend switch: `/backend agy`, `/backend grok`, or `/backend codex` (clears that backend's continuation state — the agy/grok continuation flag and the codex `thread_id`/resume state — so the next CLI turn starts a fresh session; history files on disk are not wiped immediately)
54
+ - Runtime backend switch: `/backend agy`, `/backend grok`, `/backend codex`, or `/backend dsh` (clears that backend's continuation state — the agy/grok continuation flag and the codex `thread_id`/resume state — so the next CLI turn starts a fresh session; history files on disk are not wiped immediately)
54
55
  - Slash commands for model, session reset, persona, and more (see below)
55
56
  - Dangerous-prompt gate: a **keyword list** of concrete destructive patterns asks for confirmation before run
56
57
  - Sender whitelist (`WECHATBRIDGE_ALLOWED_SENDERS`; empty = allow all)
@@ -72,8 +73,22 @@ Default data paths expand from `~` (e.g. `~/.local/share/wechatbridge/<instance>
72
73
  - **agy** (default) — Google Antigravity CLI
73
74
  - **grok** — xAI Grok Build CLI
74
75
  - **codex** — OpenAI Codex CLI
76
+ - **dsh** — DeepSeek Harness CLI (one-shot `headless` profile)
75
77
 
76
- Per-user switch: `/backend agy`, `/backend grok`, or `/backend codex`. Each backend keeps its own model / effort / mode memory and persona file layout. Global default is `WECHATBRIDGE_BACKEND`.
78
+ Per-user switch: `/backend agy`, `/backend grok`, `/backend codex`, or `/backend dsh`. Each backend keeps its own model / effort / mode memory and persona file layout. Global default is `WECHATBRIDGE_BACKEND`.
79
+
80
+ ### dsh backend notes
81
+
82
+ - **What is dsh:** DeepSeek Harness CLI (`dsh`), running one-shot tasks via the `headless` profile.
83
+ - **Enable / Switch:** Set `WECHATBRIDGE_BACKEND=dsh` globally in `.env`, or switch per-user in WeChat via `/backend dsh`.
84
+ - **Dependency (PyYAML):** The dsh backend requires `PyYAML` to parse profile plugin configurations. If missing, `/backend dsh` returns a user-visible notice and preserves existing backend preferences. Install with `pip install PyYAML` or `pipx inject wechatbridge-cli PyYAML`.
85
+ - **Window memory (default):** The `headless` profile creates a fresh session per invocation (`session-<uuid>`), so the bridge manages long-term conversation memory: recent turns (default 10 pairs, up to `WECHATBRIDGE_DSH_MEMORY_CHARS` chars) are saved to `dsh_memory.jsonl` and injected into every prompt. `/clear` or `/new` wipes this memory to start fresh.
86
+ - **Persistent resume mode:** Set `WECHATBRIDGE_DSH_RESUME=true` for true persistent sessions (codex-style resume). Requires the headless profile bundle to mount the `dsh-bridge-runner` plugin (`dsh_bridge_runner.py`, which reads `DSH_BRIDGE_SESSION_ID` and `DSH_BRIDGE_TASK` from env). Context accumulates natively across turns without windowing. `/clear` or `/new` deletes the stored session ID to start a fresh session. Window memory injection is skipped in resume mode.
87
+ - **Workspace & State Isolation:** Runs `dsh --profile headless -- <prompt>` with `cwd` = per-user session directory (`WECHATBRIDGE_SESSION_DIR/<user_id>`). Bridge-private state files (`dsh_memory.jsonl` and persistent session ID) are stored under `WECHATBRIDGE_DSH_STATE_DIR` (`~/.local/share/wechatbridge/<instance>/dsh_state/`), outside the child's `session_dir` tree to eliminate direct 1-level parent/sibling relative traversal (`../`).
88
+ - **Threat Model:** Child processes run under the same host UID without container sandbox isolation. While 1-level relative traversal (`../`) cannot reach private state, 2-level traversal (`../../dsh_state/<user_id>`) remains reachable on the filesystem and is accepted. Treat as trusted-user tooling.
89
+ - Image and file attachments are merged into the prompt text as `@/absolute/path` mentions. Verified against dsh v0.1.1-rc.2 source: `headless` profile does not pre-read or inline @mention files at the CLI/runtime level, but passes prompt text directly to the model; the model receives system guidance on @-paths and invokes file tools (`read`) as needed. The bridge filters out-of-bounds mentions (`@/abs`, `@~/x`, `@file://`, replaced with `[blocked-path]`), which is a prompt text layer best-effort filter, not a sandbox boundary.
90
+ - Auth / profiles are **machine-wide** (resolved via precedence `WECHATBRIDGE_DSH_HOME` > `WECHATBRIDGE_HOST_HOME/.dsh` > `~/.dsh`, same host-fallback model as grok/codex): the child's `HOME` points at the per-user session dir, so the bridge always passes `DSH_HOME` explicitly. Set `WECHATBRIDGE_DSH_HOME` to configure a dedicated service home with automatic session retention cleanup; when unset, it falls back to `$WECHATBRIDGE_HOST_HOME/.dsh` (if set) or `~/.dsh` without automatic session cleanup (managed by the operator). If the bridge runs as a dedicated system user (e.g. `wechatbridge`), set `WECHATBRIDGE_HOST_HOME` to point to the operator's home directory so CLI credentials and profiles are discovered. `DSH_BIN_PATH`, `DSH_PROFILE`, and `DSH_TIMEOUT` are configurable.
91
+ - **Status:** Implemented against the published dsh CLI contract and covered by the test suite (fake CLI). The persistent resume mode requires users to configure and mount the `dsh-bridge-runner` plugin in their dsh profile bundle; plugin files are not bundled with this repository.
77
92
 
78
93
  ### Grok backend notes
79
94
 
@@ -93,7 +108,8 @@ Per-user switch: `/backend agy`, `/backend grok`, or `/backend codex`. Each back
93
108
  - **agy** on `PATH`, or set `AGY_BIN_PATH`
94
109
  - **and/or grok** on `PATH`, or set `GROK_BIN_PATH`
95
110
  - **and/or codex** on `PATH`, or set `CODEX_BIN_PATH`
96
- - Antigravity is Google's terminal agentic coding CLI (successor to Gemini CLI). Grok Build is xAI's counterpart; Codex is OpenAI's terminal agentic coding CLI.
111
+ - **and/or dsh** on `PATH`, or set `DSH_BIN_PATH` (DeepSeek Harness, `dsh login` required)
112
+ - Antigravity is Google's terminal agentic coding CLI (successor to Gemini CLI). Grok Build is xAI's counterpart; Codex is OpenAI's terminal agentic coding CLI; dsh is DeepSeek Harness' CLI.
97
113
  - A WeChat account with a [ClawBot / iLink](https://ilinkai.weixin.qq.com) bot (QR bind on first run)
98
114
  - Python 3.10+
99
115
 
@@ -164,10 +180,19 @@ Key variables (all have defaults):
164
180
  | `AGY_BIN_PATH` | `agy` | path to the agy binary |
165
181
  | `GROK_BIN_PATH` | `grok` | path to the grok binary |
166
182
  | `CODEX_BIN_PATH` | `codex` | path to the codex binary |
167
- | `WECHATBRIDGE_BACKEND` | `agy` | global default backend (`agy` / `grok` / `codex`; overridable per user via `/backend`) |
183
+ | `DSH_BIN_PATH` | `dsh` | path to the dsh binary |
184
+ | `DSH_PROFILE` | `headless` | dsh profile booted for one-shot tasks |
185
+ | `DSH_TIMEOUT` | `600` | dsh CLI run timeout in seconds |
186
+ | `WECHATBRIDGE_DSH_MEMORY_TURNS` | `10` | dsh backend: recent user+assistant turns injected as context |
187
+ | `WECHATBRIDGE_DSH_MEMORY_CHARS` | `6000` | dsh backend: max chars of injected memory context |
188
+ | `WECHATBRIDGE_DSH_RESUME` | `false` | dsh backend: true persistent-session mode — one dsh session per user resumed on every message (codex-style); requires the dsh-bridge-runner plugin in the headless profile; skips windowed memory when enabled |
189
+ | `WECHATBRIDGE_DSH_HOME` | _empty_ | explicit `DSH_HOME` passed to the dsh child. Explicitly set = dedicated home + auto session cleanup; unset = fallback to `WECHATBRIDGE_HOST_HOME/.dsh` or `~/.dsh` without auto cleanup (precedence: `WECHATBRIDGE_DSH_HOME` > `WECHATBRIDGE_HOST_HOME/.dsh` > `~/.dsh`) |
190
+ | `WECHATBRIDGE_HOST_HOME` | _empty_ | host home directory override for service user deployments; used as fallback base for grok (`~/.grok`), codex (`~/.codex`), and dsh (`~/.dsh`) |
191
+ | `WECHATBRIDGE_DSH_STATE_DIR` | _derived_ | bridge-private dsh state directory (memory JSONL and persistent session IDs; defaults to `~/.local/share/wechatbridge/<instance>/dsh_state`) |
192
+ | `WECHATBRIDGE_BACKEND` | `agy` | global default backend (`agy` / `grok` / `codex` / `dsh`; overridable per user via `/backend`) |
168
193
  | `WECHATBRIDGE_INSTANCE` | `default` | instance name; state / session / QR paths derive from it |
169
194
  | `WECHATBRIDGE_ALLOWED_SENDERS` | _empty_ | comma-separated WeChat IDs (empty = allow all) |
170
- | `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (all three backends) |
195
+ | `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (agy / grok / codex backends) |
171
196
  | `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
172
197
  | `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
173
198
  | `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
@@ -253,8 +278,8 @@ See [`deploy/wechatbridge-windows.md`](deploy/wechatbridge-windows.md).
253
278
  | Command | Action |
254
279
  |---|---|
255
280
  | `/help` | list supported commands for the active backend |
256
- | `/backend <agy\|grok\|codex>` | switch CLI backend for this WeChat user (on real change: clears that backend's continuation state — agy/grok flag and codex `thread_id`/resume — so the next turn starts a fresh session; history files may remain until retention cleanup) |
257
- | `/clear` or `/new` | drop continue flag so the next CLI turn is a new conversation (does not instantly delete history files) |
281
+ | `/backend <agy\|grok\|codex\|dsh>` | switch CLI backend for this WeChat user (on real change: clears that backend's continuation state — agy/grok flag, codex `thread_id`/resume, or dsh memory/session ID — so the next turn starts a fresh session; history files may remain until retention cleanup) |
282
+ | `/clear` or `/new` | start a new conversation (agy/grok: drops continue flag; codex: clears thread ID; dsh: clears window memory or persistent session ID) |
258
283
  | `/model <name>` | set model (all backends validate against a live list: agy/grok via CLI `models`; codex via `codex debug models` [then `--bundled`]; unknown name or list-fetch failure refuse and do not write prefs; see `/models`) |
259
284
  | `/models` | list models — agy/grok/codex all query the live CLI (codex: `debug models`; falls back to a built-in reference note only if the live list cannot be fetched) |
260
285
  | `/fast` | set low reasoning effort (**on only** — not a toggle; no “off” command) |
@@ -273,10 +298,10 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
273
298
  ## Ops & security (what the bridge actually enforces)
274
299
 
275
300
  - **Whitelist first.** Empty `WECHATBRIDGE_ALLOWED_SENDERS` means anyone who can message the bot can use it.
276
- - **Auto-approve CLIs.** agy runs with `--dangerously-skip-permissions`; grok with `--always-approve` (unless planning mode). Treat this as trusted-user tooling, not a multi-tenant sandbox.
301
+ - **Auto-approve CLIs.** agy runs with `--dangerously-skip-permissions`; grok with `--always-approve` (unless planning mode); dsh tools run without host path restrictions. Treat this as trusted-user tooling, not a multi-tenant sandbox.
277
302
  - **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
278
303
  - **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
279
- - **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
304
+ - **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir; dsh: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
280
305
  - **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
281
306
  - **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
282
307
  - **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
@@ -285,8 +310,10 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
285
310
 
286
311
  ## Limitations
287
312
 
288
- - Not a standalone agent — requires agy and/or grok and/or codex.
313
+ - Not a standalone agent — requires agy and/or grok and/or codex and/or dsh.
289
314
  - The **codex** backend is not yet verified against a real Codex subscription/CLI; it is validated by source research, a JSONL fixture, and a fake CLI in tests. Treat it as community-tested until a real user confirms.
315
+ - The **dsh** backend supports both windowed memory (default, last `WECHATBRIDGE_DSH_MEMORY_TURNS` turns) and true persistent sessions (`WECHATBRIDGE_DSH_RESUME=true`). Covered by the test suite; persistent resume mode requires users to configure and mount their own `dsh-bridge-runner` plugin (this repository does not bundle plugin files).
316
+ - dsh model/effort/mode/persona slash commands are not wired yet; `/model`, `/fast`, `/planning`, `/persona`, `/add-dir` return a "not supported" notice on the dsh backend.
290
317
  - Voice is WeChat speech-to-text only; no local ASR; empty transcript → “type instead”.
291
318
  - No video send/receive; no native WeChat voice-bubble replies (no silk encode).
292
319
  - One WeChat binding per process; multiple accounts need multiple instances (`WECHATBRIDGE_INSTANCE`).
@@ -5,21 +5,21 @@
5
5
  ![license](https://img.shields.io/badge/license-MIT-blue.svg)
6
6
  ![python](https://img.shields.io/badge/python-3.10+-blue.svg)
7
7
 
8
- WeChatBridge connects a WeChat bot to agentic coding CLIs (Google's agy / Antigravity, xAI's Grok Build, or OpenAI's Codex). From WeChat you can send text, images, files, and voice-as-text to the active CLI, get replies back, and receive certain generated files over the WeChat CDN. Switch backends per user with `/backend` — no restart.
8
+ WeChatBridge connects a WeChat bot to agentic coding CLIs (Google's agy / Antigravity, xAI's Grok Build, OpenAI's Codex, or DeepSeek Harness' dsh). From WeChat you can send text, images, files, and voice-as-text to the active CLI, get replies back, and receive certain generated files over the WeChat CDN. Switch backends per user with `/backend` — no restart.
9
9
 
10
10
  ```
11
- WeChat (phone) ⇄ iLink bot API ⇄ WeChatBridge ⇄ agy / grok / codex CLI
11
+ WeChat (phone) ⇄ iLink bot API ⇄ WeChatBridge ⇄ agy / grok / codex / dsh CLI
12
12
  (this project) (runs tools)
13
13
  ```
14
14
 
15
- The bridge process stays up and long-polls iLink. For prompts that go to a CLI, it spawns one `agy` or `grok` child (`-p` single-turn) and exits that child when done — the child does not stay resident. Many slash commands (`/help`, `/backend`, `/persona`, …) are handled inside the bridge and never start a CLI. Only artifacts the bridge can detect under the user's allowed session paths are pushed back via CDN.
15
+ The bridge process stays up and long-polls iLink. For prompts that go to a CLI, it spawns one `agy` / `grok` / `codex` / `dsh` child (single-turn) and exits that child when done — the child does not stay resident. Many slash commands (`/help`, `/backend`, `/persona`, …) are handled inside the bridge and never start a CLI. Only artifacts the bridge can detect under the user's allowed session paths are pushed back via CDN.
16
16
 
17
17
  ## Features
18
18
 
19
- - Text, image, file, and voice (WeChat server-side transcription only) go to the **active** backend (`agy`, `grok`, or `codex`)
19
+ - Text, image, file, and voice (WeChat server-side transcription only) go to the **active** backend (`agy`, `grok`, `codex`, or `dsh`)
20
20
  - Detected CLI artifacts under the per-user allowed tree can be sent back (size-capped); not every file the CLI touches
21
21
  - Each WeChat user gets an isolated workspace; model / effort / mode are remembered **per backend**
22
- - Runtime backend switch: `/backend agy`, `/backend grok`, or `/backend codex` (clears that backend's continuation state — the agy/grok continuation flag and the codex `thread_id`/resume state — so the next CLI turn starts a fresh session; history files on disk are not wiped immediately)
22
+ - Runtime backend switch: `/backend agy`, `/backend grok`, `/backend codex`, or `/backend dsh` (clears that backend's continuation state — the agy/grok continuation flag and the codex `thread_id`/resume state — so the next CLI turn starts a fresh session; history files on disk are not wiped immediately)
23
23
  - Slash commands for model, session reset, persona, and more (see below)
24
24
  - Dangerous-prompt gate: a **keyword list** of concrete destructive patterns asks for confirmation before run
25
25
  - Sender whitelist (`WECHATBRIDGE_ALLOWED_SENDERS`; empty = allow all)
@@ -41,8 +41,22 @@ Default data paths expand from `~` (e.g. `~/.local/share/wechatbridge/<instance>
41
41
  - **agy** (default) — Google Antigravity CLI
42
42
  - **grok** — xAI Grok Build CLI
43
43
  - **codex** — OpenAI Codex CLI
44
+ - **dsh** — DeepSeek Harness CLI (one-shot `headless` profile)
44
45
 
45
- Per-user switch: `/backend agy`, `/backend grok`, or `/backend codex`. Each backend keeps its own model / effort / mode memory and persona file layout. Global default is `WECHATBRIDGE_BACKEND`.
46
+ Per-user switch: `/backend agy`, `/backend grok`, `/backend codex`, or `/backend dsh`. Each backend keeps its own model / effort / mode memory and persona file layout. Global default is `WECHATBRIDGE_BACKEND`.
47
+
48
+ ### dsh backend notes
49
+
50
+ - **What is dsh:** DeepSeek Harness CLI (`dsh`), running one-shot tasks via the `headless` profile.
51
+ - **Enable / Switch:** Set `WECHATBRIDGE_BACKEND=dsh` globally in `.env`, or switch per-user in WeChat via `/backend dsh`.
52
+ - **Dependency (PyYAML):** The dsh backend requires `PyYAML` to parse profile plugin configurations. If missing, `/backend dsh` returns a user-visible notice and preserves existing backend preferences. Install with `pip install PyYAML` or `pipx inject wechatbridge-cli PyYAML`.
53
+ - **Window memory (default):** The `headless` profile creates a fresh session per invocation (`session-<uuid>`), so the bridge manages long-term conversation memory: recent turns (default 10 pairs, up to `WECHATBRIDGE_DSH_MEMORY_CHARS` chars) are saved to `dsh_memory.jsonl` and injected into every prompt. `/clear` or `/new` wipes this memory to start fresh.
54
+ - **Persistent resume mode:** Set `WECHATBRIDGE_DSH_RESUME=true` for true persistent sessions (codex-style resume). Requires the headless profile bundle to mount the `dsh-bridge-runner` plugin (`dsh_bridge_runner.py`, which reads `DSH_BRIDGE_SESSION_ID` and `DSH_BRIDGE_TASK` from env). Context accumulates natively across turns without windowing. `/clear` or `/new` deletes the stored session ID to start a fresh session. Window memory injection is skipped in resume mode.
55
+ - **Workspace & State Isolation:** Runs `dsh --profile headless -- <prompt>` with `cwd` = per-user session directory (`WECHATBRIDGE_SESSION_DIR/<user_id>`). Bridge-private state files (`dsh_memory.jsonl` and persistent session ID) are stored under `WECHATBRIDGE_DSH_STATE_DIR` (`~/.local/share/wechatbridge/<instance>/dsh_state/`), outside the child's `session_dir` tree to eliminate direct 1-level parent/sibling relative traversal (`../`).
56
+ - **Threat Model:** Child processes run under the same host UID without container sandbox isolation. While 1-level relative traversal (`../`) cannot reach private state, 2-level traversal (`../../dsh_state/<user_id>`) remains reachable on the filesystem and is accepted. Treat as trusted-user tooling.
57
+ - Image and file attachments are merged into the prompt text as `@/absolute/path` mentions. Verified against dsh v0.1.1-rc.2 source: `headless` profile does not pre-read or inline @mention files at the CLI/runtime level, but passes prompt text directly to the model; the model receives system guidance on @-paths and invokes file tools (`read`) as needed. The bridge filters out-of-bounds mentions (`@/abs`, `@~/x`, `@file://`, replaced with `[blocked-path]`), which is a prompt text layer best-effort filter, not a sandbox boundary.
58
+ - Auth / profiles are **machine-wide** (resolved via precedence `WECHATBRIDGE_DSH_HOME` > `WECHATBRIDGE_HOST_HOME/.dsh` > `~/.dsh`, same host-fallback model as grok/codex): the child's `HOME` points at the per-user session dir, so the bridge always passes `DSH_HOME` explicitly. Set `WECHATBRIDGE_DSH_HOME` to configure a dedicated service home with automatic session retention cleanup; when unset, it falls back to `$WECHATBRIDGE_HOST_HOME/.dsh` (if set) or `~/.dsh` without automatic session cleanup (managed by the operator). If the bridge runs as a dedicated system user (e.g. `wechatbridge`), set `WECHATBRIDGE_HOST_HOME` to point to the operator's home directory so CLI credentials and profiles are discovered. `DSH_BIN_PATH`, `DSH_PROFILE`, and `DSH_TIMEOUT` are configurable.
59
+ - **Status:** Implemented against the published dsh CLI contract and covered by the test suite (fake CLI). The persistent resume mode requires users to configure and mount the `dsh-bridge-runner` plugin in their dsh profile bundle; plugin files are not bundled with this repository.
46
60
 
47
61
  ### Grok backend notes
48
62
 
@@ -62,7 +76,8 @@ Per-user switch: `/backend agy`, `/backend grok`, or `/backend codex`. Each back
62
76
  - **agy** on `PATH`, or set `AGY_BIN_PATH`
63
77
  - **and/or grok** on `PATH`, or set `GROK_BIN_PATH`
64
78
  - **and/or codex** on `PATH`, or set `CODEX_BIN_PATH`
65
- - Antigravity is Google's terminal agentic coding CLI (successor to Gemini CLI). Grok Build is xAI's counterpart; Codex is OpenAI's terminal agentic coding CLI.
79
+ - **and/or dsh** on `PATH`, or set `DSH_BIN_PATH` (DeepSeek Harness, `dsh login` required)
80
+ - Antigravity is Google's terminal agentic coding CLI (successor to Gemini CLI). Grok Build is xAI's counterpart; Codex is OpenAI's terminal agentic coding CLI; dsh is DeepSeek Harness' CLI.
66
81
  - A WeChat account with a [ClawBot / iLink](https://ilinkai.weixin.qq.com) bot (QR bind on first run)
67
82
  - Python 3.10+
68
83
 
@@ -133,10 +148,19 @@ Key variables (all have defaults):
133
148
  | `AGY_BIN_PATH` | `agy` | path to the agy binary |
134
149
  | `GROK_BIN_PATH` | `grok` | path to the grok binary |
135
150
  | `CODEX_BIN_PATH` | `codex` | path to the codex binary |
136
- | `WECHATBRIDGE_BACKEND` | `agy` | global default backend (`agy` / `grok` / `codex`; overridable per user via `/backend`) |
151
+ | `DSH_BIN_PATH` | `dsh` | path to the dsh binary |
152
+ | `DSH_PROFILE` | `headless` | dsh profile booted for one-shot tasks |
153
+ | `DSH_TIMEOUT` | `600` | dsh CLI run timeout in seconds |
154
+ | `WECHATBRIDGE_DSH_MEMORY_TURNS` | `10` | dsh backend: recent user+assistant turns injected as context |
155
+ | `WECHATBRIDGE_DSH_MEMORY_CHARS` | `6000` | dsh backend: max chars of injected memory context |
156
+ | `WECHATBRIDGE_DSH_RESUME` | `false` | dsh backend: true persistent-session mode — one dsh session per user resumed on every message (codex-style); requires the dsh-bridge-runner plugin in the headless profile; skips windowed memory when enabled |
157
+ | `WECHATBRIDGE_DSH_HOME` | _empty_ | explicit `DSH_HOME` passed to the dsh child. Explicitly set = dedicated home + auto session cleanup; unset = fallback to `WECHATBRIDGE_HOST_HOME/.dsh` or `~/.dsh` without auto cleanup (precedence: `WECHATBRIDGE_DSH_HOME` > `WECHATBRIDGE_HOST_HOME/.dsh` > `~/.dsh`) |
158
+ | `WECHATBRIDGE_HOST_HOME` | _empty_ | host home directory override for service user deployments; used as fallback base for grok (`~/.grok`), codex (`~/.codex`), and dsh (`~/.dsh`) |
159
+ | `WECHATBRIDGE_DSH_STATE_DIR` | _derived_ | bridge-private dsh state directory (memory JSONL and persistent session IDs; defaults to `~/.local/share/wechatbridge/<instance>/dsh_state`) |
160
+ | `WECHATBRIDGE_BACKEND` | `agy` | global default backend (`agy` / `grok` / `codex` / `dsh`; overridable per user via `/backend`) |
137
161
  | `WECHATBRIDGE_INSTANCE` | `default` | instance name; state / session / QR paths derive from it |
138
162
  | `WECHATBRIDGE_ALLOWED_SENDERS` | _empty_ | comma-separated WeChat IDs (empty = allow all) |
139
- | `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (all three backends) |
163
+ | `AGY_TIMEOUT` | `600` | CLI run timeout in seconds (agy / grok / codex backends) |
140
164
  | `WECHATBRIDGE_MAX_OUTBOUND_BYTES` | `104857600` | max file size sent back to WeChat (100 MB) |
141
165
  | `WECHATBRIDGE_MAX_INBOUND_BYTES` | `20971520` | max inbound image/file after download (20 MB) |
142
166
  | `WECHATBRIDGE_MAX_CONCURRENT` | `4` | global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply |
@@ -222,8 +246,8 @@ See [`deploy/wechatbridge-windows.md`](deploy/wechatbridge-windows.md).
222
246
  | Command | Action |
223
247
  |---|---|
224
248
  | `/help` | list supported commands for the active backend |
225
- | `/backend <agy\|grok\|codex>` | switch CLI backend for this WeChat user (on real change: clears that backend's continuation state — agy/grok flag and codex `thread_id`/resume — so the next turn starts a fresh session; history files may remain until retention cleanup) |
226
- | `/clear` or `/new` | drop continue flag so the next CLI turn is a new conversation (does not instantly delete history files) |
249
+ | `/backend <agy\|grok\|codex\|dsh>` | switch CLI backend for this WeChat user (on real change: clears that backend's continuation state — agy/grok flag, codex `thread_id`/resume, or dsh memory/session ID — so the next turn starts a fresh session; history files may remain until retention cleanup) |
250
+ | `/clear` or `/new` | start a new conversation (agy/grok: drops continue flag; codex: clears thread ID; dsh: clears window memory or persistent session ID) |
227
251
  | `/model <name>` | set model (all backends validate against a live list: agy/grok via CLI `models`; codex via `codex debug models` [then `--bundled`]; unknown name or list-fetch failure refuse and do not write prefs; see `/models`) |
228
252
  | `/models` | list models — agy/grok/codex all query the live CLI (codex: `debug models`; falls back to a built-in reference note only if the live list cannot be fetched) |
229
253
  | `/fast` | set low reasoning effort (**on only** — not a toggle; no “off” command) |
@@ -242,10 +266,10 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
242
266
  ## Ops & security (what the bridge actually enforces)
243
267
 
244
268
  - **Whitelist first.** Empty `WECHATBRIDGE_ALLOWED_SENDERS` means anyone who can message the bot can use it.
245
- - **Auto-approve CLIs.** agy runs with `--dangerously-skip-permissions`; grok with `--always-approve` (unless planning mode). Treat this as trusted-user tooling, not a multi-tenant sandbox.
269
+ - **Auto-approve CLIs.** agy runs with `--dangerously-skip-permissions`; grok with `--always-approve` (unless planning mode); dsh tools run without host path restrictions. Treat this as trusted-user tooling, not a multi-tenant sandbox.
246
270
  - **Danger gate is keyword-based**, not full intent understanding. Defaults target concrete patterns (`rm -rf /`, pipe-to-shell, `mkfs`, `format c:`, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is **not** gated. Override list via `WECHATBRIDGE_CONFIRM_KEYWORDS`; approve with `WECHATBRIDGE_CONFIRM_TOKEN` (default `y`), TTL `WECHATBRIDGE_PENDING_TTL`.
247
271
  - **Inbound media** is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing `aes_key` returns a clear error.
248
- - **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
272
+ - **Outbound artifacts** only leave the allowed per-user tree (agy: session scratch; grok: under session dir; dsh: under session dir), after `realpath` checks, and only if under `WECHATBRIDGE_MAX_OUTBOUND_BYTES`.
249
273
  - **Concurrency:** global process-slot cap (`WECHATBRIDGE_MAX_CONCURRENT`, default 4). Same user is serialized and does **not** hold a global slot while waiting on their previous message; different users can run in parallel up to the cap.
250
274
  - **Long replies** are split into chunks (`WECHATBRIDGE_MESSAGE_CHUNK`, default 2000 characters).
251
275
  - **Data layout:** instance data under `~/.local/share/wechatbridge/<instance>/` (override with env). Runtime dirs prefer `0700`; token/QR files prefer `0600` (Unix; Windows relies on NTFS ACLs).
@@ -254,8 +278,10 @@ Other `/…` commands are either rejected (e.g. `/exit`), reported as unsupporte
254
278
 
255
279
  ## Limitations
256
280
 
257
- - Not a standalone agent — requires agy and/or grok and/or codex.
281
+ - Not a standalone agent — requires agy and/or grok and/or codex and/or dsh.
258
282
  - The **codex** backend is not yet verified against a real Codex subscription/CLI; it is validated by source research, a JSONL fixture, and a fake CLI in tests. Treat it as community-tested until a real user confirms.
283
+ - The **dsh** backend supports both windowed memory (default, last `WECHATBRIDGE_DSH_MEMORY_TURNS` turns) and true persistent sessions (`WECHATBRIDGE_DSH_RESUME=true`). Covered by the test suite; persistent resume mode requires users to configure and mount their own `dsh-bridge-runner` plugin (this repository does not bundle plugin files).
284
+ - dsh model/effort/mode/persona slash commands are not wired yet; `/model`, `/fast`, `/planning`, `/persona`, `/add-dir` return a "not supported" notice on the dsh backend.
259
285
  - Voice is WeChat speech-to-text only; no local ASR; empty transcript → “type instead”.
260
286
  - No video send/receive; no native WeChat voice-bubble replies (no silk encode).
261
287
  - One WeChat binding per process; multiple accounts need multiple instances (`WECHATBRIDGE_INSTANCE`).
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "qrcode",
29
29
  "Pillow",
30
30
  "cryptography",
31
+ "PyYAML",
31
32
  ]
32
33
 
33
34
  [project.scripts]