toby-bridges/api-relay-audit
toby-bridges/api-relay-audit从 DeepSeek Harness 对 AI API 中转站和 LLM 代理运行本地安全审计,生成 Markdown 报告,覆盖提示词注入、模型替换信号、工具调用改写、错误泄漏、流完整性和按 profile 启用的 Web3 风险。
为 DSH Web 界面提供多用户登录网关:首次访问创建管理员账户,管理员在 GUI 设置面板中添加和管理用户,普通用户只能看到自己的会话,未登录访问重定向到 /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 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:
/loginsession.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 wholesaletrustedHosts: 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 设置 → 用户管理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'sUSER_ALLOWEDwas 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 deniedsession.modelCatalog(model dialog),directoryPicker.*(add workspace) andllm.listProviders/llm.discoverModels(settings→models provider catalog) withnot 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), addsscripts/dump-wire-surface.mjs+tests/wire-surface.spec.tsso an upstream rename fails the suite instead of silently locking users out, logs an unknown-method denial once to the host log, and alignscapabilities/remote-guardwith the same names. Option A: upstream reworked the/apitransport — dsh-login no longer takes it over (theconnectionrow is re-enabled) and answers 401 on the shared/apibridge 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/modulesboot graph,client/runtime/ui-slots, the/apicontrollers,web-app,app-boot) changed only inpackage.json, and the/apicontrollers have zero src diff — so the wire method registry (andUSER_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 namespacepluginMarket: 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) —setIndexesreconfigures the instance-wide index list, so it is never user-reachable; the client boot graph now carries areleasestamp that dsh-login'suiRoleGatepreserves 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 bidirectionalterminal.attachstream (the removedterminal.follow/terminal.write/terminal.resizemethods dropped from the allow-list), and the newharness-dev-contextweb-app row injectswebApp, 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 newwebStartupservice, and retired the `webRuntime` service upstream (dsh-login'stakeOverWebRuntimere-provide is now compat-only) — thedsh web:URL line andDSH_WEB_URLnow spell the actual bind address, matching upstream. 0.3.1: the bridge wall enforces theUSER_ALLOWEDmethod allow-list for ordinary users, narrows theirsession.list/session.search/workspace.listresponses to owned sessions, keeps the ownership sidecar warm (session/workspace/forkrecording + per-user default-workspace provisioning), and ships a deployment glue (composeDshLoginGuard+docs/compose-guard.md) for composing the REMOTE-layer isolation guard over the nativetypertGateway— 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). SeeCurrent status →anddocs/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 (theapiBridgeAuthwall on the shared/apibridge, the document-previewworkspaceFiles/officeToPdfuser surface, the nativepluginManagernamespace admin-only);0.2.3→ 0.1.7-alpha.2 (settings seam re-home, the newaccountnamespace admin-only, thejobcontroller 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-invariantsreferences 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-/apibridge wall gains per-user enforcement (theUSER_ALLOWEDmethod gate, owned-onlysession.list/session.search/workspace.listnarrowing, ownership recording including admin record-only, default-workspace provisioning wiring) plus thecomposeDshLoginGuarddeployment 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 thedsh web:URL line/DSH_WEB_URLnow spelling the actual bind address; upstream retired itswebRuntimeservice, 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;webAppprovided so the newharness-dev-contextrow injects; allow-list updated for the alpha.3 wire surface:terminal.attachreplaces follow/write/resize,remoteAccess.followadmitted; 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 newpluginMarketnamespace — the plugin market's instance-wide index configuration — gated admin-only across all three layers; theuiRoleGateboot-graph filter regression-tested to preserve the newreleasegraph 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,/apicontrollers zero diff — no code change; the newweb-deepseek-api/ui-deepseek-model-label-1017/ui-settings-web-searchpackages 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 indocs/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
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.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.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`)
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.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.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)
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.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.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.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.
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-logindsh web and open the GUI — the first visit shows a one-time "create administrator account" page; pick a username and passwordUninstall:
dsh plugin --profile web remove @islibaodong/dsh-loginWhy--profile web? DSH installs plugins per profile directory ($DSH_HOME/profiles/);webis the profile that boots the Web GUI. Use your profile's name if you run a custom one.
/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.For contributors, security reviewers, and troubleshooting. You don't need any of this to use the plugin.
dsh plugin add reads this package's declared cordis.patch.yml (a bundle patch) and automatically:
dsh-login plugin row (config defaults are sensible; distIndex resolves the frontend dist automatically)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)