Merge branch 'worktree-llm-dynamic-config' into worktree-llm-web-config
# Conflicts: # apps/cli/cordis.yml # apps/web/tests/snapshots/code-mode-round/session.jsonl # apps/web/tests/snapshots/cordis-tool-round/session.jsonl # apps/web/tests/snapshots/fresh-round-trip/session.jsonl # apps/web/tests/snapshots/lifecycle-chrome/session.jsonl # apps/web/tests/snapshots/live-interactions/session.jsonl # apps/web/tests/snapshots/navigation-panes/seed.jsonl # apps/web/tests/snapshots/question-composer/session.jsonl # apps/web/tests/snapshots/seeded-history/seed.jsonl # apps/web/tests/snapshots/steering/session.jsonl # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/settings.i18n.yaml # docs/event-producer-consumer.md # docs/module-graph.md # examples/acp-agent/tests/snapshots/workspace-context/session.jsonl # packages/client/connection/README.i18n.yaml # packages/client/connection/src/index.ts # packages/client/connection/tests/node-half.spec.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/runtime/src/client/index.ts # packages/client/runtime/tests/fake-api.ts # packages/client/ui-models/README.i18n.yaml # packages/examples/tui-demo/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/package.json # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/rpc.schema.ts # packages/host/apiproxy/src/api/rpc.ts # packages/llm/llm-deepseek/README.i18n.yaml # packages/llm/llm-deepseek/README.zh.md # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm/README.i18n.yaml # packages/llm/llm/README.zh.md # packages/sdk/sdk-client/README.i18n.yaml # packages/settings/settings/README.i18n.yaml # packages/settings/settings/README.md # packages/settings/settings/README.zh.md # packages/subagent/subagent-dsh-sdk/README.i18n.yaml # packages/subagent/subagent-dsh-sdk/README.zh.md # packages/support/llm-replay/README.i18n.yaml # packages/ui/jsonrpc/README.i18n.yaml # packages/ui/jsonrpc/README.zh.md # packages/ui/tui/tests/snapshots/model-selector.expected.txt # packages/ui/tui/tests/snapshots/model-switching.expected.txt # packages/ui/tui/tests/snapshots/resume-sessions.expected.txt # packages/ui/tui/tests/snapshots/status-diagnostics-narrow.expected.txt # packages/ui/tui/tests/snapshots/status-diagnostics.expected.txt # packages/ui/tui/tests/tui.snapshot.ts # pnpm-lock.yaml # python/sdk/README.i18n.yaml # scripts/snapshots/translation-prompt-v4/request-response.expected.json
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
/** Host HTTP bridge for browser-client RPC. */
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
// Activates the httpServer Context merge used below.
|
||||
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
||||
import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
|
||||
import { API_PATH } from './api-path.ts'
|
||||
import { bridge } from './http-bridge.ts'
|
||||
import { isTrustedNativeDialogRequest } from './native-dialog-request.ts'
|
||||
import { assertTrustedAuthority, isTrustedApiRequest } from './api-request-trust.ts'
|
||||
|
||||
export { API_PATH } from './api-path.ts'
|
||||
|
||||
@@ -15,12 +16,29 @@ export const name = 'client-connection'
|
||||
/** Services required before mounting the route. */
|
||||
export const inject = ['httpServer', 'apiProxy']
|
||||
|
||||
/** Plugin config: the deployment's non-loopback serving authorities. */
|
||||
export interface ConnectionConfig {
|
||||
/**
|
||||
* Authorities this deployment serves beyond loopback: exact `host:port`, or
|
||||
* port-less `host` matching any port. The /api trust fence refuses any
|
||||
* request whose Host is neither loopback nor listed here, so a
|
||||
* non-loopback (`0.0.0.0`) deployment must declare the names it is reached
|
||||
* by (the dsh CLI derives the machine's LAN IP literals itself). An entry
|
||||
* that is not a bare, canonical authority fails the plugin load.
|
||||
*/
|
||||
trustedHosts?: string[]
|
||||
}
|
||||
|
||||
export const Config: z<ConnectionConfig> = z.object({
|
||||
trustedHosts: z.array(String).default([]),
|
||||
})
|
||||
|
||||
/**
|
||||
* Methods gated on the trusted same-origin loopback check. Native dialogs act
|
||||
* on the host machine; settings and credential writes mutate the user's
|
||||
* configuration and secret store. Under `--host 0.0.0.0` every other method
|
||||
* is reachable LAN-wide, but these stay browser-same-origin-on-loopback until
|
||||
* a real authentication layer exists.
|
||||
* Methods gated to loopback even on a trusted-host deployment. Native dialogs
|
||||
* act on the host machine; settings and credential writes mutate the user's
|
||||
* configuration and secret store. A declared `trustedHosts` authority reaches
|
||||
* every other method, but these stay loopback-same-origin until a real
|
||||
* authentication layer exists.
|
||||
*/
|
||||
const PRIVILEGED_METHODS = new Set([
|
||||
'host.pickDirectory',
|
||||
@@ -32,19 +50,33 @@ const PRIVILEGED_METHODS = new Set([
|
||||
])
|
||||
|
||||
/**
|
||||
* Mounts the API gateway under the browser transport prefix.
|
||||
* Mounts the API gateway under the browser transport prefix. Every request on
|
||||
* the prefix passes the browser-trust fence first (DNS-rebinding and
|
||||
* cross-site defense — [api-request-trust](./api-request-trust.ts));
|
||||
* privileged methods additionally pass it with an empty trust list, which
|
||||
* pins them to loopback.
|
||||
* @param ctx - Host plugin context.
|
||||
* @param config - resolved plugin config (schema defaults applied).
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
export function apply(ctx: Context, config?: ConnectionConfig): void {
|
||||
// The Loader resolves schema defaults; hand-built test contexts may pass none.
|
||||
const trustedHosts = config?.trustedHosts ?? []
|
||||
// Config boundary: a malformed entry fails the load loudly here rather than
|
||||
// silently authorizing its hostname prefix at request time.
|
||||
for (const entry of trustedHosts) assertTrustedAuthority(entry)
|
||||
const apiHandler = toFetchHandler(ctx.apiProxy)
|
||||
const route: WebRoute = {
|
||||
kind: 'prefix',
|
||||
path: API_PATH,
|
||||
handler: async (req, res) => {
|
||||
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname
|
||||
if (pathname.startsWith(`${API_PATH}/`)
|
||||
&& PRIVILEGED_METHODS.has(pathname.slice(API_PATH.length + 1))
|
||||
&& !isTrustedNativeDialogRequest(req)) {
|
||||
const method = pathname.startsWith(`${API_PATH}/`)
|
||||
? pathname.slice(API_PATH.length + 1)
|
||||
: undefined
|
||||
const allowed = method !== undefined && PRIVILEGED_METHODS.has(method)
|
||||
? isTrustedApiRequest(req, [])
|
||||
: isTrustedApiRequest(req, trustedHosts)
|
||||
if (!allowed) {
|
||||
res.writeHead(403)
|
||||
res.end('forbidden')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user