fix: /sidebar 信任栅栏改用 webRuntime.trustedHosts,修复远程访问全部 403

根因:trustedHostsOf 按 entry.options.name === 'connection' 查找 connection
行,但该行 name 是模块标识符(@deepseek-ai/dsh-client-connection,id 才是
connection),查找永远落空 → trustedHosts 为空 → 栅栏只放行回环 Host → 通过
域名或局域网 IP 访问时 /sidebar/bundle chunk 加载失败、资源管理器/代码管理器
全部 403 forbidden(/api 不受影响,它读的是 apply 时已求值的配置)。

修复(按 review 建议):host half 直接注入 webRuntime 服务,栅栏每请求实时
读取 ctx.webRuntime.trustedHosts —— 与 /api 网关同一信任列表(启动时采样的
LAN IP 字面量 + --trusted-host 权威),不再依赖 Loader entry / fiber /
!!js 节点求值;自定义信任域名由 dsh web --trusted-host 提供。

新增 tests/trusted-hosts.spec.ts 回归:域名/局域网放行、空列表仅回环、
跨站标记拒绝、每请求实时读取;plugin-shape 断言同步更新。
This commit is contained in:
Rookie_Zoe
2026-08-14 23:03:26 +08:00
committed by Atlas Zeng
parent 9b87c1da16
commit ea250c9e87
5 changed files with 209 additions and 27 deletions
+12 -2
View File
@@ -12,7 +12,7 @@
* - conversation: client side ui-conversation's IConversation (composer
* draft), read lazily through `ctx.get` — cross-plugin service reads need
* an inject declaration, so the direct property is never typed here
* - loader: @cordisjs/plugin-loader (entry options)
* - webRuntime: @deepseek-ai/dsh-web-app (bind-derived trusted hosts)
* - slots: the client runtime SlotRegistry
* - effect: the DSH-vendored cordis lifecycle helper
* Drift from upstream is contained to this file.
@@ -102,6 +102,16 @@ export interface SidebarLoader {
entries(): Iterable<SidebarLoaderEntry>
}
/**
* The web runtime service face (mirror of @deepseek-ai/dsh-web-app's
* WebRuntimeValues): the bind-derived trust list the /api gateway's fence
* accepts — LAN IP literals sampled when the server binds all interfaces,
* plus explicit `--trusted-host` authorities.
*/
export interface SidebarWebRuntime {
trustedHosts: readonly string[]
}
/** Registration options the sidebar passes to `ctx.slots.register` (subset of the real options). */
export interface SidebarSlotRegisterOptions {
name: string
@@ -416,7 +426,7 @@ declare module 'cordis' {
webServer: SidebarWebServer
sessions: SidebarSessionStore & SidebarSessionsService
connection: SidebarConnectionHandle
loader: SidebarLoader
webRuntime: SidebarWebRuntime
slots: SidebarSlotsService
workspaces: SidebarWorkspacesService
settings: SidebarSettingsService
+10 -19
View File
@@ -4,9 +4,9 @@
* preview route, the /sidebar/bundle lazy-chunk route (client code splits),
* and the terminal WebSocket upgrade. Every route passes the same
* browser-trust fence as the /api gateway — Host-header loopback or the
* connection row's `trustedHosts` (the `dsh web` launcher derives LAN IP
* literals per boot) — with the trustedHosts read live from the connection
* loader row so the fence never drifts from the deployment's.
* web runtime's `trustedHosts` (LAN IP literals derived per boot plus
* `--trusted-host` authorities) — read live per request so the fence never
* drifts from the deployment's.
*
* All operations are conversation-scoped: requests carry a sessionId, the
* session's authoritative cwd comes from the session store, and terminal
@@ -59,8 +59,8 @@ export type {
/** Plugin identity for cordis.yml rows. */
export const name = 'dsh-better-sidebar'
/** Services required before mounting: the webserver routes, the session store, the loader's connection row, and the tool registry. */
export const inject = ['webServer', 'sessions', 'loader', 'tools']
/** Services required before mounting: the webserver routes, the session store, the web runtime's trusted hosts, and the tool registry. */
export const inject = ['webServer', 'sessions', 'webRuntime', 'tools']
/** Content types for the media route, by extension. */
const MEDIA_TYPES: Record<string, string> = {
@@ -83,17 +83,6 @@ export function mediaTypeForPath(path: string): string {
return MEDIA_TYPES[extname(path).toLowerCase()] ?? 'application/octet-stream'
}
/** The connection row's resolved trustedHosts (live read; the /api fence's own list). */
function trustedHostsOf(ctx: Context): string[] {
for (const entry of ctx.loader.entries()) {
if (entry.options.name === 'connection') {
const config = entry.options.config as { trustedHosts?: string[] } | undefined
return config?.trustedHosts ?? []
}
}
return []
}
/**
* Resolve a session's authoritative working directory. The attached session
* header wins; while the session is still hydrating from persistence (the
@@ -426,7 +415,7 @@ function buildApi(
/**
* Plugin body: mount the fenced routes and the pty lifecycle.
* @param ctx - host plugin context (webServer, sessions, loader).
* @param ctx - host plugin context (webServer, sessions, webRuntime).
* @param config - deployment-provided limits; the Loader validates against
* {@link Config} and fills defaults, direct callers get them from
* {@link resolveSidebarConfig}.
@@ -436,8 +425,10 @@ export function apply(ctx: Context, config?: SidebarConfig): void {
// restore it before any terminal can spawn (idempotent).
ensureSpawnHelper()
const resolved = resolveSidebarConfig(config)
const trustedHosts = trustedHostsOf(ctx)
const fence = (req: SidebarHttpRequest): boolean => isTrustedApiRequest(req, trustedHosts)
// The web runtime's bind-derived trust list (LAN literals plus
// --trusted-host authorities) — the same list the /api gateway fence
// accepts. Read per request so config reloads apply without a restart.
const fence = (req: SidebarHttpRequest): boolean => isTrustedApiRequest(req, ctx.webRuntime.trustedHosts)
const ptyManager = new PtyManager(defaultShell(), resolved.terminalsPerSession)
// The agent-owned terminal registry: parallel to the UI-tab ptyManager,
// keyed by uuid (the model's opaque handle) instead of `${sessionId}:${tabId}`,