dsh-service-portal
One random domain + one random password to expose your local services on demand.
中文 ·
Installation ·
How it works ·
Service manifest ·
Changelog ·
Issues
**dsh-service-portal (Service Portal)** is a [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) plugin.
It exposes **any local service** to the public internet on demand: `cloudflared` creates a **random domain**,
a **password login** (password only, no username) guards the entrance, and a settings-page toggle
shows the URL and the password.
It originally lived inside `ld-rl-poker` (to put its training dashboard / model viewer online), but the
portal itself has nothing to do with poker — **any local service** can use the same
"one domain + one password + on-demand mount" pattern.
## Features
- **One-click exposure** — the settings page starts the service → auth proxy → tunnel, no manual steps.
- **Password only, no username** — visitors get a custom login page with a single password field;
the browser's native username+password dialog is never triggered.
- **Two password shapes** — (1) **random per start** (default: smallest leak window);
(2) a **fixed password you set** (convenient, but once leaked it stays valid — the settings page says so).
- **Random domain** — changes on every start, so the entrance itself is short-lived.
- **Two shapes** — one random domain per service, or **one domain + path prefix**
(`https:///train/`).
- **Manifest-driven** — write `services.json` to expose any service without touching code; an empty
manifest falls back to the built-in default (ld-rl-poker's two GUIs, backwards compatible).
- **Control checks** (house rule) — 26 Python control-check scripts plus 8 end-to-end scripts, and 83
Node checks; **a missing dependency must "skip and return 2", never silently pass**.
- **Host metrics** — CPU / memory / VRAM / GPU utilization; unreadable values render "—", never a fake 0.
## Screenshots
The portal page (what you get after scanning the QR code — read-only: test + guide).
Captured from a real portal process with headless Chrome via `tools/shoot_portal.py`,
not design mockups.
| Phone (light) | Phone (dark) | Wide |
| :---: | :---: | :---: |
|
|
|
|
## How it works
```
https://.trycloudflare.com (cloudflared quick tunnel, new domain every start)
|
v
127.0.0.1: tools/tunnel_auth.py (password login + session cookie, auth before forwarding)
|
v
127.0.0.1: your local service (GUI / Ollama / any HTTP service)
```
## Installation
### Via npm (after publish)
```powershell
$dsh = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
& $dsh plugin --profile desktop add dsh-service-portal
```
### Via link (during development)
```powershell
$dsh = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
& $dsh plugin --profile desktop add "link:D:/DshProject/dsh-service-portal"
```
Restart DSH after installing (the bundle loads in the host process).
## Configuration (DSH settings page)
| Field | Default | Description |
| :--- | :--- | :--- |
| `infraPath` | this repo root | Infrastructure repo: contains `tools/` and `runs/` (tunnel/proxy/credentials) |
| `repoPath` | **empty** | Host repo root: where the exposed services live. **You must set this** |
| `pythonPath` | empty | Interpreter for services; auto-selects `.venv313` → `.runtime` → PATH |
| `passwordMode` | `random` | Password shape: `random` = new password each start / `fixed` = your fixed password |
| `fixedPassword` | empty | Used when `passwordMode = fixed`, at least 12 chars (validated on save) |
| `webEnabled` | `true` | Mount the `/service-portal/api` routes |
All four are `.volatile()`: changes apply immediately without remounting the plugin.
## HTTP API
Mounted on the host webServer (`prefix` routes under `/service-portal/api`):
| Method | Path | Purpose |
| :--- | :--- | :--- |
| GET | `/service-portal/api/status` | Current state: running, mode, URL, password, password mode, endpoints |
| GET | `/service-portal/api/settings` | Password-shape settings (mode + fixed password + min length) |
| POST | `/service-portal/api/settings` | Save password shape (`{"passwordMode":"fixed","fixedPassword":"..."}`) |
| GET | `/service-portal/api` | Same as `/status` |
| POST | `/service-portal/api/start` | Start (body optional `{"singleDomain": true}`, default single-domain+paths) |
| POST | `/service-portal/api/stop` | Stop |
Unknown sub-paths return **404** (no status fallback — a mistyped path must not "look usable").
## Service manifest
With no configuration the built-in manifest is used (= ld-rl-poker's two GUIs). To expose other
services, write `services.json`:
```json
{
"hostRepo": "D:\\DshProject\\ld-rl-poker",
"services": [
{"key": "train", "script": "gui/server.py", "guiPort": 8810,
"proxyPort": 8890, "name": "训练配置台", "mount": "train"},
{"key": "ollama", "script": "serve.py", "guiPort": 11434,
"proxyPort": 11499, "name": "Ollama", "mount": "ollama"}
]
}
```
Priority: `--services ` > `$SERVICE_PORTAL_SERVICES` > `./services.json` > built-in manifest.
- `hostRepo`: host repo root the service scripts are relative to;
- `script` / `guiPort`: the service to launch and its port;
- `proxyPort`: per-service auth proxy port in multi-domain mode;
- `mount`: path prefix in single-domain mode (`/train/`).
## Tools (usable without the plugin)
| File | Purpose |
| :--- | :--- |
| `tools/start_public_gui.py` | One-click launcher: read manifest → start services → proxy → cloudflared tunnel |
| `tools/tunnel_auth.py` | Password-login reverse proxy with multi-mount path routing (auth before forwarding) |
```powershell
python -X utf8 tools/start_public_gui.py --single-domain # one domain + paths
python -X utf8 tools/start_public_gui.py # two independent random domains
python -X utf8 tools/start_public_gui.py --status / --stop
```
## Control checks
| Check | Count | Bug it catches |
| :--- | ---: | :--- |
| `tools/check_portal_integration.py` | — | A malformed Host counted as LAN (`garbage value` was password-free), double-slash console links |
| `tools/check_portal_registry.py` | 88 | Duplicate/illegal mounts, key collisions, **changes not persisted** (lost on restart), crash on a corrupt file |
| `tools/check_portal_console.py` | — | Disabled services still clickable, `