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.7` (git tag `v0.3.7`; published on npm — `latest` = 0.3.7). Verified against DSH 0.2.1-rc.3 (2026-10-24; full suite 21 files / 267 tests green on the published rc.3 builds). New in 0.3.7: tracking DSH 0.2.1-rc.3 — one upstream release since the 0.3.6 watch (tag dsh-v0.2.1-rc.3 → 1f81c047fc, 2026-10-23, 6 commits) and the entire adapter surface is source-unchanged: rc.3 is a client-UI-only release (navigation entry customization, nav pin/menu icon fixes, analytics events, the llm /models listing re-home), the 146-method wire registry is byte-identical to rc.2's, and the boot-graph lever is untouched — so 0.3.7 is a devDependency retarget (^0.2.1-rc.3) plus the WIRE_SURFACE_DSH_VERSION pin, with no code logic change. 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.3.7 → DSH 0.2.1-rc.3 (devDep retarget — the peers already admitted rc.3; rc.3 is a client-UI-only release (6 commits: navigation entry customization, nav pin/menu icon fixes, analytics events, the llm /models listing re-home), the 146-method wire registry is byte-identical to rc.2's and the boot-graph lever is untouched — no source change). 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.7` is released (git tag `v0.3.7`; published on npm — `latest` = 0.3.7). 0.3.7 tracks DSH 0.2.1-rc.3 (2026-10-24): a devDep retarget + version-pin metadata release with no source change — rc.3 is a client-UI-only release (navigation entry customization #6247, nav pin/menu icon fixes #6883, analytics events #6600, the llm /models listing re-home #6944), the 146-method wire registry is byte-identical to rc.2's, and the boot-graph lever (packages/client/modules/src/index.ts) is untouched. 0.3.6 was 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.3 (verified 2026-10-24): 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.3. 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; 0.2.1-rc.3 added no wire methods or namespaces either — its changes are client-UI-only (navigation entry customization, nav pins/menu icons, analytics events, the llm /models listing re-home)), 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-24 at 0.2.1-rc.3; 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; 0.2.1-rc.3 likewise (its UI changes are content-level customization, not a permission axis).

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, 0.2.1-rc.2, 2026-10-18, and 0.2.1-rc.3, 2026-10-24)

  • 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, and still the only one at 0.2.1-rc.3, 2026-10-24). 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.3 (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; rc.3 added nothing here either — its navigation-entry customization is content-level configuration, not a permission axis); 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 u