islibaodong/dsh-login

islibaodong/dsh-login

为 DSH Web 界面提供多用户登录网关:首次访问创建管理员账户,管理员在 GUI 设置面板中添加和管理用户,普通用户只能看到自己的会话,未登录访问重定向到 /login。

安全与权限authenticationcordis-plugindeepseek-harness

dsh-login

English | 简体中文

Adds a login page, user accounts, and per-user conversation isolation to the DeepSeek Harness Web GUI: every visit requires sign-in, ordinary users cannot see each other's conversations, and an admin manages all accounts from inside the GUI.

Login page · User management (设置 → 用户管理) ·

The problem it solves

The DSH Web GUI ships with no login — it assumes a single user on localhost. The moment you bind it to 0.0.0.0 (phone access, LAN sharing, a small team), anyone on that network can open your GUI: read every conversation, burn your configured model API keys, even change host settings.

dsh-login turns that exposure into a multi-user deployment:

  • 🔐 Login wall — pages, static assets, SPA routes, the API, and WebSockets all require a valid session; unauthenticated visitors are redirected to /login
  • 👥 Multiple accounts — the first visit creates the administrator account; further users are added by an admin right inside the GUI, no CLI needed
  • 🙈 Conversation isolation — ordinary users see and act on their own conversations only (including their subagents/forks); the bridge wall denies non-whitelisted methods and narrows session.list/session.search/workspace.list responses to owned sessions, and the composed REMOTE-layer guard scopes dispatches by ownership; other users' sessions, messages, and workspaces are invisible; credentials, host settings, and other admin domains are forbidden wholesale
  • 🛠 User management — 设置 → 用户管理: last-login time, online session count, reset password, disable, remove; disabling/removing/password-changes immediately revoke that user's live sessions
  • 👑 Admin exception — the admin is not isolated: all sessions visible, full configuration access
  • 🚪 Logout — every user gets a logout entry in their settings panel
  • 🌐 Remote access friendly — reach the GUI through frp/隧道/tunnels or a LAN IP without hand-editing trustedHosts: the /api host-trust fence uses a live set (LAN literals + trustedHosts + auto-learned hosts), and any successful login learns its Host into a persisted whitelist you can manage in 设置 → 用户管理

Current status — released (option A)

Released: `@islibaodong/dsh-login@0.3.6` (git tag `v0.3.6`; published on npm — `latest` = 0.3.6). Verified against DSH 0.2.1-rc.2 (2026-10-21; full suite 21 files / 267 tests green on the published rc.2 builds). New in 0.3.6 — issue #4: the ordinary-user method allow-list had drifted off the wire surface. 0.3.5's USER_ALLOWED was written against a pre-0.2 spelling set, so on DSH 0.2.1-rc.2 every entry was a dead name and ordinary users were denied session.modelCatalog (model dialog), directoryPicker.* (add workspace) and llm.listProviders/llm.discoverModels (settings→models provider catalog) with not permitted for this user. 0.3.6 re-derives the allow-list from the real 146-method registry (src/api-filter.ts: 92 user / 53 admin-only / 1 host-internal, exhaustive), adds scripts/dump-wire-surface.mjs + tests/wire-surface.spec.ts so an upstream rename fails the suite instead of silently locking users out, logs an unknown-method denial once to the host log, and aligns capabilities/remote-guard with the same names. Option A: upstream reworked the /api transport — dsh-login no longer takes it over (the connection row is re-enabled) and answers 401 on the shared /api bridge without a session (apiBridgeAuth, default on). New in 0.3.5: tracking DSH 0.2.1-rc.2 — three upstream releases landed since alpha.4 (alpha.5 → rc.1 → rc.2; 49 commits) and the entire adapter surface is source-unchanged: every dsh-login integration point (host/webserver, client/connection, settings, host/frontend-static, client/modules boot graph, client/runtime/ui-slots, the /api controllers, web-app, app-boot) changed only in package.json, and the /api controllers have zero src diff — so the wire method registry (and USER_ALLOWED/ADMIN_ONLY_*) needed no edits. The harness also re-released its vendored packages at stable versions (PR #6388: cordis 4.0.5, schemastery 3.18.5, include 1.0.10, loader 1.0.6, …), so 0.3.5 pins those instead of the alphas; the new rc.2 packages (web-deepseek-api, ui-deepseek-model-label-1017, ui-settings-web-search) are agent-side/UI-only and add no wire surface. 0.3.4: tracking DSH 0.2.1-alpha.4 — upstream added the plugin market (packages/boot/plugin-market, typert namespace pluginMarket: browse configured market indexes and install their bundles), which dsh-login gates admin-only across all three layers (wire guard, /api/pluginMarket/* two-segment deny with quiet read probes, capability advertisement) — setIndexes reconfigures the instance-wide index list, so it is never user-reachable; the client boot graph now carries a release stamp that dsh-login's uiRoleGate preserves byte-identical through filtering. 0.3.3: tracking DSH 0.2.1-alpha.3 — upstream shipped its first native RBAC substrate (packages/api/remote-access: per-Peer policies for non-owner connections — dsh-login users remain owner principals, so dsh-login's own walls keep enforcing; still no native user accounts), the terminal became a bidirectional terminal.attach stream (the removed terminal.follow/terminal.write/terminal.resize methods dropped from the allow-list), and the new harness-dev-context web-app row injects webApp, which dsh-login now provides itself alongside its web-runtime takeover. 0.3.2: tracking DSH 0.2.1-alpha.2 — upstream added TLS (HTTPS) to the webserver, moved the connection trust authorities to the new webStartup service, and retired the `webRuntime` service upstream (dsh-login's takeOverWebRuntime re-provide is now compat-only) — the dsh web: URL line and DSH_WEB_URL now spell the actual bind address, matching upstream. 0.3.1: the bridge wall enforces the USER_ALLOWED method allow-list for ordinary users, narrows their session.list/session.search/workspace.list responses to owned sessions, keeps the ownership sidecar warm (session/workspace/fork recording + per-user default-workspace provisioning), and ships a deployment glue (composeDshLoginGuard + docs/compose-guard.md) for composing the REMOTE-layer isolation guard over the native typertGateway — the two-browser sign-off of that composition remains a deployment-side acceptance step. 0.3.0 added per-role whole-UI gating (uiRoleGate, default on) — ordinary users never mount the admin-only client bundles (the boot-graph lever documented below). See Current status → and docs/verify-option-A.md.
Adaptation history (all versions below are published on npm): 0.2.1 → DSH 0.1.6-alpha.1 (sidebar terminal, unarchive sessions); 0.2.2 → 0.1.6-alpha.2 (the apiBridgeAuth wall on the shared /api bridge, the document-preview workspaceFiles/officeToPdf user surface, the native pluginManager namespace admin-only); 0.2.3 → 0.1.7-alpha.2 (settings seam re-home, the new account namespace admin-only, the job controller user surface, session pinning); 0.2.4 → the DSH #4587 settings-error fix; 0.2.5 → DSH 0.2.0-rc.2 (peer retarget onto the 0.2.0 tuple line — the surface verified source-compatible, no code change); 0.2.6 → the issue #3 fix (a fresh device's first page load after login hit the core BrowserAuth's plain-text 401 — the gateway now bounces once through the ?token= exchange so the core mints its browser cookie); 0.2.7 → DSH 0.2.1-alpha.1 (peer retarget onto the 0.2.1 tuple line + the retired @deepseek-ai/dsh-invariants references removed — no source change); 0.3.0 → the per-role whole-UI gate (uiRoleGate) implemented on the boot-graph lever (still tracking DSH 0.2.1-alpha.1); 0.3.1 → the shared-/api bridge wall gains per-user enforcement (the USER_ALLOWED method gate, owned-only session.list/session.search/workspace.list narrowing, ownership recording including admin record-only, default-workspace provisioning wiring) plus the composeDshLoginGuard deployment glue (no upstream change — still 0.2.1-alpha.1); 0.3.2 → DSH 0.2.1-alpha.2 (devDep retarget — the peer tuple already admitted alpha.2 — plus the dsh web: URL line/DSH_WEB_URL now spelling the actual bind address; upstream retired its webRuntime service, so dsh-login's takeover is compat-only, and upstream added TLS); 0.3.3 → DSH 0.2.1-alpha.3 (devDep retarget onto the re-published vendor alphas — peers unchanged; webApp provided so the new harness-dev-context row injects; allow-list updated for the alpha.3 wire surface: terminal.attach replaces follow/write/resize, remoteAccess.follow admitted; upstream's first RBAC substrate evaluated); 0.3.4 → DSH 0.2.1-alpha.4 (devDep retarget — the peer tuples already admitted alpha.4; the new pluginMarket namespace — the plugin market's instance-wide index configuration — gated admin-only across all three layers; the uiRoleGate boot-graph filter regression-tested to preserve the new release graph field); 0.3.5 → DSH 0.2.1-rc.2 (devDep retarget onto the rc.2 tuple line plus the stable vendor re-release — peers unchanged, the alpha-peer ranges admit stable; the whole adapter surface verified source-unchanged, /api controllers zero diff — no code change; the new web-deepseek-api/ui-deepseek-model-label-1017/ui-settings-web-search packages add no wire surface; multi-user/RBAC re-verified, still none native). 0.1.7-rc.1 / rc.2 were verified compatible without a release — details in docs/adapt-dsh-0.1.7-rc.1.md / docs/adapt-dsh-0.1.7-rc.2.md / docs/adapt-dsh-0.2.0-rc.2.md. Upstream still has no native multi-user accounts and no per-role UI gating, but 0.2.1-alpha.3 shipped the first native RBAC substrate for non-owner connections (packages/api/remote-access + multi-principal Peers; re-evaluated 2026-10-15 — see below).

`@islibaodong/dsh-login@0.3.6` is released (git tag `v0.3.6`; published on npm — `latest` = 0.3.6). 0.3.6 is the issue-#4 fix: the ordinary-user method allow-list is re-derived from the actual DSH 0.2.1-rc.2 wire surface (92 user / 53 admin / 1 host-internal of 146 methods, exhaustive and test-enforced), so model selection, add-workspace and settings→models work again for non-admins. The option-A adaptation for DSH ≥ 0.1.5-alpha.1 is complete and tracks upstream through 0.2.1-rc.2 (verified 2026-10-18): dsh-login no longer takes over /api — the native connection + api-remotes/api-gateway stack owns the transport — and composes the login wall (fallback seat), the apiBridgeAuth wall on the shared /api bridge, and capability discovery on top of it. 0.3.0 ships the per-role whole-UI gate: with uiRoleGate: true (default) the gateway filters the window.__DSH_BOOT__ graph for ordinary users so admin-only client bundles never mount (src/ui-gate.ts; deny-list = ADMIN_ONLY_UI_PLUGINS; admins and uiRoleGate:false see the full graph; fail-open on any unexpected shape). The bridge wall adds (0.3.1) the per-user method gate (USER_ALLOWED), owned-only response narrowing for session.list/session.search/workspace.list, ownership recording (including admin-created resources, record-only), and per-user default-workspace provisioning. 0.3.2 mirrored upstream's bind-address URL line (upstream's webRuntime retirement made the takeover compat-only). 0.3.3 retargeted the devDeps to 0.2.1-alpha.3 and provided webApp for the new harness-dev-context row. 0.3.4 retargets the devDeps to 0.2.1-alpha.4 (peers already admitted it) and keeps the walls current with the new wire surface: alpha.4's plugin market (packages/boot/plugin-market, typert namespace pluginMarket — listIndexes/setIndexes/search over the instance-wide market index list) is gated admin-only across all three layers (src/remote-guard.ts ADMIN_ONLY_NAMESPACES, src/capabilities.ts two-segment deny + admin advertisement + quiet read probes for listIndexes/search, setIndexes staying a loud 403), and the boot graph's new `release` stamp (WebBootRelease {version, commit?} — alpha.4's client reload gating) is preserved byte-identical through the uiRoleGate filter with a regression test. 0.3.5 retargets the devDeps to 0.2.1-rc.2 (peers already admitted it, and the stable vendor re-release) — the adapter surface came out source-unchanged (every integration point changed only in package.json; the /api controllers have zero src diff), so the walls needed no edits; the rc.2 additions (web-deepseek-api, ui-deepseek-model-label-1017, ui-settings-web-search) add no wire surface. The full suite is 20 files / 256 tests green against the published 0.2.1-rc.2 builds, including the real-composition tests on booted servers. Deliberately NOT yet boot-verified (needs a real `dsh web` boot to confirm): the per-user isolation guard composed over the native typertGateway via the shipped deployment glue (docs/compose-guard.md) and the two-browser isolation sign-off (see docs/verify-option-A.md §B). See docs/adapt-dsh-0.2.1-rc.2.md for this release's full surface analysis and docs/adapt-dsh-0.1.5.md for the original status matrix.

Delivered and working

  • Login wall + multi-account user management (设置 → 用户管理).
  • Capability discovery (GET /api/auth/capabilities, session-authenticated) and quiet read-denials (204 for a no-permission read probe, 403 for writes; toggle via quietDenials), so ordinary-user browsers stop getting "forbidden" walls and retry storms.
  • dsh-login's own settings contribution is already per-user: admins see 设置 → 用户管理, ordinary users see 账户 (identity + logout); ordinary users never call the admin API.
  • Per-user isolation guard ships as a composition primitive (option A). Under DSH ≥ 0.1.5 the native connection/api-gateway owns /api, so dsh-login's isolation is a REMOTE-layer guard — wrapRemoteGateway + createRemoteIsolation, exported from this package's host bundle and unit/integration-tested (including ownership scoping and admin passthrough). The wrapper forwards every non-dispatch gateway member unchanged (e.g. DSH 0.2.0-rc.2's required hasLiveClient()), so composing it stays interface-complete. connection ownership + per-user default workspace are re-established on the native stack. The guard must be composed over the native `typertGateway` row at boot (a deployment decision, not a runtime hot-swap — re-providing typertGateway would be a duplicate-service error); dsh-login ships ready-made glue for that composition — the ownership sidecar is published as the dshLoginOwnership service and composeDshLoginGuard(ctx, gateway) wraps a native gateway in one call (see docs/compose-guard.md for a complete row module and acceptance checklist). The two-browser behavior sign-off remains a deployment-side step. See docs/verify-option-A.md §B.
  • Tracks upstream 0.1.5-alpha.1 → 0.2.1-rc.2. Each upstream release's user-facing wire surface joins the ordinary allow-list as it lands (sidebar terminal.*, workspace.unarchiveSession/pinSession/unpinSession, the read-only workspaceFiles/officeToPdf document preview, the job controller; 0.2.0 added no new wire methods or namespaces; 0.2.1 added the claudeCodeMods namespace — agent-mod band UI above the prompt — which rides the default deny, no user surface yet; 0.2.1-alpha.2 added none; 0.2.1-alpha.3 replaced the terminal trio with the terminal.attach stream and admitted remoteAccess.follow — the native remote-access snapshot stream that answers with only the caller's own grants; 0.2.1-alpha.4 added the pluginMarket namespace — admin-only wholesale, see below; 0.2.1-rc.2 added no wire methods or namespaces at all — the entire adapter surface is source-unchanged and the new rc.2 bundles (web-deepseek-api, ui-deepseek-model-label-1017, ui-settings-web-search) are agent-side or UI-only), while new admin-only namespaces (pluginManager + pluginRegistryProbe, the account DeepSeek-Platform controller — the one process-wide upstream grant, including 0.1.7-rc.2's watchExpiry expiry stream, and 0.2.1-alpha.4's pluginMarket) are denied wholesale. Since DSH 0.1.7-rc.1 the host runs a boot-time plugin-compatibility admission against peer ranges — dsh-login's ranges admit the current runtime (0.2.7 added the 0.2.1 branch), and the check is fail-open (stderr report only), so after any DSH upgrade verify the login page actually appears. 0.3.0 additionally ships uiRoleGate — the per-role whole-UI boot-graph filter (see below).

Per-role control of the whole UI — SHIPPED in 0.3.0 (`uiRoleGate`)

  • dsh-login 0.3.0 implements the whole-UI gate in-plugin (no upstream change needed). The gateway renders the index itself, and the client module roster travels in that HTML as window.__DSH_BOOT__; the page then activates exactly that roster (ClientEntries.reconcile() creates one entry per manifest row and removes any bundle absent from it). For an ordinary (non-admin) session with uiRoleGate: true (the default), the gateway rewrites the boot graph before serving the index: entries in the ADMIN_ONLY_UI_PLUGINS deny-list are dropped, their batches patched or removed, and the shell never mounts them. Presentation-only — the /api allow-list (apiBridgeAuth + USER_ALLOWED) remains the security boundary. Admins get the unfiltered graph; any unexpected shape fails open (original HTML served). See docs/adapt-dsh-0.2.1-alpha.1.md §6.
  • What the gate does NOT do: per-package granularity only — hiding one slot/section *inside* an allowed bundle (e.g. DSH's settings panel, whose SettingsRoot → useSections list carries no identity dimension) stays out of scope. DSH's WebServer route priority is exact-beats-prefix with no pre-routing hook, so a plugin that self-registers exact routes (e.g. @linxin666/dsh-pet serving /api/pet/pets) cannot be intercepted per user by dsh-login either.
  • Upstream still ships no per-identity UI gate: the client boot/activation layer and ui-slots carry no role/permission/isAdmin concept (re-verified 2026-10-18 at 0.2.1-rc.2; isAdmin/isAllowed greps return zero hits under packages/client — and note that ~238 files still match a raw role grep because of DOM role="…" accessibility attributes, not account roles). 0.2.1-alpha.3's remote-access RBAC gates non-owner Peer connections server-side (per-resource decide() + snapshot injection), not the client UI roster — an entirely different axis from this boot-graph gate; 0.2.1-alpha.4 changed nothing here (its release graph field is release-identity plumbing for client reload gating, not a permission axis, and dsh-login's gate preserves it); 0.2.1-rc.2 re-verified unchanged.

Upstream multi-user / RBAC evaluation (first at 0.2.1-alpha.3, 2026-10-15; re-verified 0.2.1-alpha.4, 2026-10-16, and 0.2.1-rc.2, 2026-10-18)

  • No native user accounts — no user store, no login, no identity UI (the recurring multi.?user|multiuser|role.?based|\brbac\b grep produced its first hit ever: packages/api/remote-access/README.md; the same single hit at 0.2.1-rc.2, 2026-10-18). dsh-login remains the account/authn layer.
  • What 0.2.1-alpha.3 actually added: a native authorize-only RBAC engine for non-owner Peers (packages/api/remote-access: Principal/PrincipalMap — owner is the only kind today, merge-extensible — plus registerPolicy/decide per resource, an owner baseline injected as the __DSH_REMOTE_ACCESS__ global, and the always-allowed remoteAccess.follow snapshot stream) and the multi-principal connection machinery behind it (createPeer/admitConnection, v2 API cookies {version:2, id: PeerId}, the GET /connect token exchange, socket-ownership 409). This is the substrate a future native per-resource/whole-UI permission system would build on — nothing consumes it for dsh-login's use case yet.
  • Why it does not change dsh-login's security model: dsh-login users reach the native stack as owner principals (the browser cookie mints browserPeer {kind:'owner'}), and remote-access never gates owners — dsh-login's login wall, USER_ALLOWED bridge wall, remote guard, and uiRoleGate remain the actual multi-user boundary. Known substrate caveats dsh-login's own scrubbing already covers: undeclared resources are UNCHECKED, api-session added entries leak parentSessionId, the ready frame exposes full host/home.
  • Future option (documented, not integrated): if dsh-login ever mints non-owner principals, it could register remote-access policies per dsh-login role to get native per-resource enforcement for free.

Still wanted from upstream: per-identity slot/section filtering (for sub-package granularity *inside* an allowed bundle) and a host-side per-user plugin activation gate. Neither exists at 0.2.1-rc.2 (the remote-access substrate is per-resource *connection* policy, not UI activation; 0.2.1-alpha.4 added the admin-only plugin market and the release boot-graph stamp — release-identity plumbing, not a permission axis; rc.2 added nothing here); the 0.3.0 boot-graph gate covers whole packages only.

Quick start

No environment variables, no config edits — three steps:

# 1. Install (web is the profile that boots the Web GUI)
dsh plugin --profile web add github:islibaodong/dsh-login
  1. Initialize: restart dsh web and open the GUI — the first visit shows a one-time "create administrator account" page; pick a username and password
  2. Add users: sign in as admin → 设置 → 用户管理 → 新建用户

Uninstall:

dsh plugin --profile web remove @islibaodong/dsh-login
Why --profile web? DSH installs plugins per profile directory ($DSH_HOME/profiles/); web is the profile that boots the Web GUI. Use your profile's name if you run a custom one.

FAQ

  • Do I have to log in again after restarting DSH? No — login sessions are persisted to /sessions.json (0o600), so an existing cookie keeps working across a process restart (the cookie otherwise lasts 7 days by default). Only an actual log-out, password change, user removal, or TTL expiry invalidates it.
  • What can an ordinary user do? Use the chat normally: create/open/continue their own sessions, run subagents, manage their own workspace content. Everything else (other people's sessions, credentials, plugins/presets/host settings, model-key management) is refused.
  • Upgrading from the old single-password version? The old single password no longer logs anyone in; the first visit after upgrading bootstraps a fresh administrator account (details in the migration note below).

Technical details

For contributors, security reviewers, and troubleshooting. You don't need any of this to use the plugin.

What installation does

dsh plugin add reads this package's declared cordis.patch.yml (a bundle patch) and automatically:

  • mounts the dsh-login plugin row (config defaults are sensible; distIndex resolves the frontend dist automatically)
  • disables the web-runtime row (where dsh-web-app mounts the frontend-static fallback); dsh-login takes over the fallback seat and re-provides the webRuntime service (LAN trust for the /api fence + the DSH_WEB_URL variable)
  • keeps the shipped `connection` row enabled