Merge remote-tracking branch 'origin/master' into task/command-feedback-master

This commit is contained in:
Turtle
2026-08-06 18:38:35 +08:00
235 changed files with 7057 additions and 1227 deletions

View File

@@ -15,7 +15,7 @@ export type {
ModelReasoningEffort, ModelTarget, QueueAction, QueuedInboxItem, SessionModels,
GoalsApi, GoalRef,
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
CredentialsApi, CredentialView, ConfigurableProviderView, LlmApi,
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
} from '@deepseek-ai/dsh-host-apiproxy/api'
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'

View File

@@ -2505,6 +2505,12 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
],
}),
models: request => ok(request, { groups: fixtureModelGroups(), failures: [] }),
// The fixture endpoint is imaginary, so the interrogation answers the
// catalog it already serves — enough for a surface to exercise adopting
// candidates without a reachable provider.
discoverModels: request => ok(request, {
models: fixtureModelGroups().flatMap(group => group.models.map(model => ({ id: model.id, name: model.name }))),
}),
},
respond(message: ClientResponse): Promise<RpcReceipt> {
// Same routing discipline as the host: rpcId first, then the payload's
@@ -2622,6 +2628,7 @@ export class FixtureApiClient extends AbstractApiClient {
case 'credentials.unset': return this.api.credentials.unset(request)
case 'llm.providers': return this.api.llm.providers(request)
case 'llm.models': return this.api.llm.models(request)
case 'llm.discoverModels': return this.api.llm.discoverModels(request, signal)
}
}

View File

@@ -25,7 +25,7 @@ export type {
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
GoalsApi, GoalRef,
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
CredentialsApi, CredentialView, ConfigurableProviderView, LlmApi,
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
} from './api.ts'
export {
RpcId,

View File

@@ -44,10 +44,15 @@ export const Config: z<ConnectionConfig> = z.object({
* reconnaissance no anonymous caller should have. `trustedHosts` is a
* DNS-rebinding fence, explicitly not authentication, so the whole
* configuration plane stays loopback-same-origin until a real authentication
* layer exists. The model catalog (`llm.providers`, `llm.models`) is
* deliberately NOT here: it carries provider ids, display names, and model
* lists — no endpoints, keys, or key state — and a LAN client's model picker
* legitimately needs it.
* layer exists. `llm.discoverModels` belongs to that plane on both counts: it
* carries a draft credential, and it makes the HOST issue a GET to a URL the
* caller chose and reports back the status or the parsed body — an anonymous
* LAN caller would have a probe for whatever the host can reach and the
* browser cannot.
*
* The model catalog (`llm.providers`, `llm.models`) is deliberately NOT here:
* it carries provider ids, display names, and model lists — no endpoints,
* keys, or key state — and a LAN client's model picker legitimately needs it.
*/
const PRIVILEGED_METHODS = new Set([
'host.pickDirectory',
@@ -60,6 +65,7 @@ const PRIVILEGED_METHODS = new Set([
'credentials.describe',
'credentials.set',
'credentials.unset',
'llm.discoverModels',
])
/**

View File

@@ -197,6 +197,7 @@ export class FakeApiClient implements IApiClient {
readonly llm: IApiClient['llm'] = {
providers: payload => this.record('llm.providers', payload, Promise.resolve(ok({ providers: [] }))),
models: payload => this.record('llm.models', payload, Promise.resolve(ok({ groups: [], failures: [] }))),
discoverModels: payload => this.record('llm.discoverModels', payload, Promise.resolve(ok({ models: [] }))),
}
/** When true, streams never fire onOpen (misbehaving-carrier material for the handshake timeout guard). */

View File

@@ -129,13 +129,15 @@ describe('connection node half', () => {
it('pins privileged methods to loopback even for a declared trusted authority', async () => {
const { routes, dispose } = await mounted({ trustedHosts: ['harness.example'] })
// The privileged set: native dialogs plus the whole settings/credential
// configuration plane, reads included. The same declared authority reaches
// configuration plane, reads included, plus the one method that makes the
// host fetch a caller-chosen URL. The same declared authority reaches
// ordinary reads (carrier-level 404 from the empty proxy proves the fence
// passed), but each privileged method stays loopback-only and 403s.
for (const method of [
'host.pickDirectory', 'host.openPath',
'settings.describe', 'settings.openDocument', 'settings.update', 'settings.replace', 'settings.mutate',
'credentials.describe', 'credentials.set', 'credentials.unset',
'llm.discoverModels',
]) {
const denied = fakeResponse()
await routes[0]!.handler(
@@ -221,6 +223,9 @@ describe('connection node half over a real HTTP server', () => {
'settings.describe', 'settings.openDocument', 'settings.update', 'settings.replace', 'settings.mutate',
'credentials.describe', 'credentials.set', 'credentials.unset',
'host.pickDirectory', 'host.openPath',
// Carries a draft credential and turns the host into a fetcher for a
// URL the caller picked: an anonymous LAN caller must not reach it.
'llm.discoverModels',
]) {
expect([method, await call(port, method, 'harness.example')]).toEqual([method, 403])
}