Merge remote-tracking branch 'origin/master' into mergebot/pr711

# Conflicts:
#	apps/cli/README.i18n.yaml
#	docs/module-graph.md
#	packages/client/connection/README.i18n.yaml
#	packages/client/runtime/README.i18n.yaml
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/src/api-proxy.ts
This commit is contained in:
imccyu
2026-07-31 02:02:47 +08:00
455 changed files with 9824 additions and 1402 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/connection/README.md
README.md: 86267449b15229a62c7b28a9e153a030f862b640
README.zh.md: 8f32b8d0a484d1ac18aa2a70e3d223e333c18503
README.md: c8b7c4787cbcbf6a202fb944459a589fcadd7c8d
README.zh.md: 693420183ffa4fb20e1fecbff523a12261a45d45

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` seam, and the loop's sink/config types. The platform subclasses (WebApiClient/FixtureApiClient), the ConnectionController loop, and the fixture data source are package-internal — apply selects and drives them; tests reach them via src. Contract: api-contracts v3 §3.
Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` seam, and the loop's sink/config types. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`, reads included, since describing returns the exposed configuration and probing an arbitrary reference reports where a credential comes from) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform subclasses (WebApiClient/FixtureApiClient), the ConnectionController loop, and the fixture data source are package-internal — apply selects and drives them; tests reach them via src. Contract: api-contracts v3 §3.
## /api browser-trust fence

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 单消费方流循环启动器);导出表层携带协议契约类型、`AbstractApiClient` seam以及循环的 sink配置类型。平台子类WebApiClient/FixtureApiClient、ConnectionController 循环和 fixture 数据源都属于包内部apply 负责选择并驱动它们,测试则通过 src 访问。契约api-contracts v3 §3。
协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 单消费方流循环启动器);导出表层携带协议契约类型、`AbstractApiClient` seam以及循环的 sink配置类型。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory``host.openPath`,以及整个配置面——`settings.describe`/`update`/`replace`/`mutate``credentials.describe`/`set`/`unset`,读取也在内,因为 describe 会返回已暴露的配置,而探测任意引用会报出某条凭据来自何处)以空信任表过信任 fence从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台子类WebApiClient/FixtureApiClient、ConnectionController 循环和 fixture 数据源都属于包内部apply 负责选择并驱动它们,测试则通过 src 访问。契约api-contracts v3 §3。
## /api 浏览器信任栅栏

View File

@@ -14,6 +14,8 @@ export type {
ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
InboxItemId, ModelReasoningEffort, ModelTarget, QueueAction, QueuedInboxItem, SessionModels,
GoalsApi, GoalRef,
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
CredentialsApi, CredentialView, ConfigurableProviderView, LlmApi,
} from '@deepseek-ai/dsh-host-apiproxy/api'
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
export type {

View File

@@ -29,7 +29,7 @@ import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import { foldSurface } from '@deepseek-ai/dsh-session/surface'
import type {
ApiProxy, ClientRequest, ClientResponse, HistoryEntry, HostFrame, MuxFrame, RpcReceipt,
ModelTarget, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
ModelProviderGroup, ModelTarget, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
ToolCallView, ToolEventView, ToolResultView, WorkspaceId, WorkspaceView,
} from './api.ts'
import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api'
@@ -156,6 +156,35 @@ const OPENAI_REASONING = {
defaultEffort: 'medium',
}
/** Catalog served by `session.models` and `llm.models` alike (fresh copies per call). */
function fixtureModelGroups(): ModelProviderGroup[] {
return [
{
id: 'deepseek-official',
name: 'DeepSeek',
models: [
{
id: 'deepseek-v4-flash',
name: 'DeepSeek-V4-Flash',
description: '快速响应',
reasoning: DEEPSEEK_REASONING,
},
{
id: 'deepseek-v4-pro',
name: 'DeepSeek-V4-Pro',
description: '复杂任务',
reasoning: DEEPSEEK_REASONING,
},
],
},
{
id: 'openai',
name: 'OpenAI',
models: [{ id: 'gpt-5', name: 'GPT-5', reasoning: OPENAI_REASONING }],
},
]
}
function sid(id: string): SessionId {
return id as SessionId
}
@@ -823,8 +852,14 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
const logs = new Map<SessionId, SessionEvent[]>([[sid('fx-alpha'), buildAlphaLog()]])
const modelTargets = new Map<SessionId, ModelTarget>(sessions.map(session => [
session.sessionId,
{ provider: 'deepseek', model: 'deepseek-v4-flash' },
{ provider: 'deepseek-official', model: 'deepseek-v4-flash' },
]))
/** Credential store double: set/unset flip the describe badge, values never read back. */
const fixtureCredentials = new Map<string, true>([
// The assembled fixture represents an already-configured shipped
// DeepSeek route so unrelated GUI journeys do not enter first-run setup.
['DEEPSEEK_API_KEY', true],
])
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 60]])
let nextSession = 1
let nextRpc = 1
@@ -1179,7 +1214,7 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
sessionId: requestedId ?? sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: true, cwd,
}
sessions.push(created)
modelTargets.set(created.sessionId, { provider: 'deepseek', model: 'deepseek-v4-flash' })
modelTargets.set(created.sessionId, { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
attachedSessions += 1
const emitSession = (): void => {
// Mirrors the host: the frame fires at creation, so blank is constantly true.
@@ -1289,32 +1324,8 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
},
models: request => ok(request, {
current: modelTargets.get(request.payload.sessionId)
?? { provider: 'deepseek', model: 'deepseek-v4-flash' },
groups: [
{
id: 'deepseek',
name: 'DeepSeek',
models: [
{
id: 'deepseek-v4-flash',
name: 'DeepSeek-V4-Flash',
description: '快速响应',
reasoning: DEEPSEEK_REASONING,
},
{
id: 'deepseek-v4-pro',
name: 'DeepSeek-V4-Pro',
description: '复杂任务',
reasoning: DEEPSEEK_REASONING,
},
],
},
{
id: 'openai',
name: 'OpenAI',
models: [{ id: 'gpt-5', name: 'GPT-5', reasoning: OPENAI_REASONING }],
},
],
?? { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
groups: fixtureModelGroups(),
failures: [],
}),
selectModel: (request) => {
@@ -1559,14 +1570,14 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
const spec = PERMISSION_PRESETS[preset]
if (preset === '') {
const current = permissionSelectOf(logOf(id)).currentValue
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `Current permission preset: ${current}. Available: ${Object.keys(PERMISSION_PRESETS).join(', ')}.` } })
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `current preset ${current} (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } })
} else if (spec === undefined) {
append(id, { type: 'command/done', data: { commandId, kind: 'error', text: `unknown permission preset ${JSON.stringify(preset)} (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } })
append(id, { type: 'command/done', data: { commandId, kind: 'error', text: `unknown preset "${preset}" (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } })
} else {
if (permissionSelectOf(logOf(id)).currentValue !== preset) append(id, { type: 'permission/preset', data: { preset } })
append(id, { type: 'sandbox/mode', data: { mode: spec.sandbox } })
append(id, { type: 'approval/policy', data: { policy: spec.approval } })
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `Permission preset: ${preset}.` } })
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `preset ${preset}` } })
}
return ok(request, { matched: true as const, commandId })
}
@@ -1750,6 +1761,64 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
}
},
},
settings: {
// Only the resolved DeepSeek address needed by first-run readiness is
// represented here. Fixture-backed journeys do not open its Models
// editor; real schema-driven forms ride the HTTP transport.
describe: request => ok(request, {
writable: true,
namespaces: [{
ns: 'llm-deepseek',
schema: {},
value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
applies: 'live',
secrets: [{ path: ['apiKey'], set: false }],
revision: 0,
}],
}),
update: request => err(request, {
code: 'settings-rejected',
message: 'fixture: the minimal readiness settings descriptor is read-only',
details: { ns: request.payload.ns },
}),
replace: request => err(request, {
code: 'settings-rejected',
message: 'fixture: the minimal readiness settings descriptor is read-only',
details: { ns: request.payload.ns },
}),
mutate: request => err(request, {
code: 'settings-rejected',
message: 'fixture: no settings namespaces are registered',
details: { ns: request.payload.ns },
}),
},
credentials: {
describe: request => ok(request, {
credentials: Object.fromEntries(request.payload.refs.map(ref => [ref, {
configured: fixtureCredentials.has(ref),
...fixtureCredentials.has(ref) ? { source: 'file' } : {},
writable: true,
}])),
}),
set: (request) => {
fixtureCredentials.set(request.payload.ref, true)
return ok(request, {})
},
unset: (request) => {
fixtureCredentials.delete(request.payload.ref)
return ok(request, {})
},
},
llm: {
providers: request => ok(request, {
providers: [
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true },
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false },
],
}),
models: request => ok(request, { groups: fixtureModelGroups(), failures: [] }),
},
respond(message: ClientResponse): Promise<RpcReceipt> {
// Same routing discipline as the host: rpcId first, then the payload's
// audit correlation; a settled or unknown id is not-pending.
@@ -1852,6 +1921,15 @@ export class FixtureApiClient extends AbstractApiClient {
case 'goal.resume': return this.api.goals.resume(request)
case 'goal.complete': return this.api.goals.complete(request)
case 'goal.clear': return this.api.goals.clear(request)
case 'settings.describe': return this.api.settings.describe(request)
case 'settings.update': return this.api.settings.update(request)
case 'settings.replace': return this.api.settings.replace(request)
case 'settings.mutate': return this.api.settings.mutate(request)
case 'credentials.describe': return this.api.credentials.describe(request)
case 'credentials.set': return this.api.credentials.set(request)
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)
}
}

View File

@@ -22,6 +22,8 @@ export type {
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
GoalsApi, GoalRef,
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
CredentialsApi, CredentialView, ConfigurableProviderView, LlmApi,
} from './api.ts'
export {
RpcId,

View File

@@ -33,10 +33,38 @@ export const Config: z<ConnectionConfig> = z.object({
trustedHosts: z.array(String).default([]),
})
/**
* Methods gated to loopback even on a trusted-host deployment. Native dialogs
* act on the host machine; the settings and credential domains mutate the
* user's configuration and secret store, and READING them is equally
* privileged — `settings.describe` returns every exposed namespace's
* configuration and `credentials.describe` reports whether an arbitrary
* environment-variable name is configured and where from, which is
* 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.
*/
const PRIVILEGED_METHODS = new Set([
'host.pickDirectory',
'host.openPath',
'settings.describe',
'settings.update',
'settings.replace',
'credentials.describe',
'credentials.set',
'credentials.unset',
])
/**
* 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)).
* 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).
*/
@@ -51,7 +79,14 @@ export function apply(ctx: Context, config?: ConnectionConfig): void {
kind: 'prefix',
path: API_PATH,
handler: async (req, res) => {
if (!isTrustedApiRequest(req, trustedHosts)) {
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname
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

View File

@@ -54,11 +54,11 @@ export class FakeApiClient implements IApiClient {
() => Promise.resolve(ok({
events: [],
hasMore: false,
modelTarget: { provider: 'deepseek', model: 'deepseek-chat' },
modelTarget: { provider: 'deepseek-official', model: 'deepseek-chat' },
}))
onModels: (payload: unknown) => Promise<RpcResponse<SessionModels>> = () => Promise.resolve(ok({
current: { provider: 'deepseek', model: 'deepseek-chat' },
current: { provider: 'deepseek-official', model: 'deepseek-chat' },
groups: [],
failures: [],
}))
@@ -163,6 +163,24 @@ export class FakeApiClient implements IApiClient {
clear: payload => this.record('goal.clear', payload, Promise.resolve(ok({ cleared: true as const }))),
}
readonly settings: IApiClient['settings'] = {
describe: payload => this.record('settings.describe', payload, Promise.resolve(ok({ writable: true, namespaces: [] }))),
update: payload => this.record('settings.update', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
replace: payload => this.record('settings.replace', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
mutate: payload => this.record('settings.mutate', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
}
readonly credentials: IApiClient['credentials'] = {
describe: payload => this.record('credentials.describe', payload, Promise.resolve(ok({ credentials: {} }))),
set: payload => this.record('credentials.set', payload, Promise.resolve(ok({}))),
unset: payload => this.record('credentials.unset', payload, Promise.resolve(ok({}))),
}
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: [] }))),
}
/** When true, streams never fire onOpen (misbehaving-carrier material for the handshake timeout guard). */
suppressStreamOpen = false

View File

@@ -172,6 +172,36 @@ describe('createFixtureApi', () => {
expect(JSON.stringify(after.result.value.events)).toContain('openai/gpt-5')
})
it('serves configured DeepSeek readiness and keeps credential values write-only', async () => {
const api = createFixtureApi()
const settings = await api.settings.describe(req({}))
if (!settings.result.ok) throw new Error('settings describe failed')
expect(settings.result.value.namespaces).toMatchObject([{
ns: 'llm-deepseek',
value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
secrets: [{ path: ['apiKey'], set: false }],
}])
const initial = await api.credentials.describe(req({ refs: ['DEEPSEEK_API_KEY', 'TEST_API_KEY'] }))
if (!initial.result.ok) throw new Error('credential describe failed')
expect(initial.result.value.credentials).toEqual({
DEEPSEEK_API_KEY: { configured: true, source: 'file', writable: true },
TEST_API_KEY: { configured: false, writable: true },
})
await api.credentials.set(req({ ref: 'TEST_API_KEY', value: 'write-only-fixture-secret' }))
const configured = await api.credentials.describe(req({ refs: ['TEST_API_KEY'] }))
if (!configured.result.ok) throw new Error('credential describe failed')
expect(configured.result.value.credentials.TEST_API_KEY).toEqual({
configured: true,
source: 'file',
writable: true,
})
await api.credentials.unset(req({ ref: 'TEST_API_KEY' }))
const cleared = await api.credentials.describe(req({ refs: ['TEST_API_KEY'] }))
if (!cleared.result.ok) throw new Error('credential describe failed')
expect(cleared.result.value.credentials.TEST_API_KEY).toEqual({ configured: false, writable: true })
})
it('emits the todo/write snapshot at the real tool boundary: between tool/call and tool/result, timestamps monotonic', async () => {
const api = createFixtureApi()
const tail = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 10 }))

View File

@@ -1,8 +1,10 @@
/** Node half: registers the /api prefix route bridging to the api gateway. */
import { EventEmitter } from 'node:events'
import { createServer, request as httpRequest } from 'node:http'
import { Readable } from 'node:stream'
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import type { AddressInfo } from 'node:net'
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api'
import type { HttpServerService, WebRoute } from '@deepseek-ai/dsh-host-webserver'
@@ -21,9 +23,9 @@ function fakeHttpServer(routes: WebRoute[]): Pick<HttpServerService, 'register'
}
/** Bodyless GET carrying the given headers (enough for the trust fence + bridge). */
function fakeRequest(headers: Record<string, string>): IncomingMessage {
function fakeRequest(headers: Record<string, string>, url = `${API_PATH}/session.list`): IncomingMessage {
const request = Readable.from([]) as unknown as IncomingMessage
Object.assign(request, { url: `${API_PATH}/session.list`, method: 'GET', headers })
Object.assign(request, { url, method: 'GET', headers })
return request
}
@@ -97,6 +99,31 @@ describe('connection node half', () => {
await dispose()
})
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
// 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.update', 'settings.replace',
'credentials.describe', 'credentials.set', 'credentials.unset',
]) {
const denied = fakeResponse()
await routes[0]!.handler(
fakeRequest({ host: 'harness.example' }, `${API_PATH}/${method}`),
denied.response,
)
expect(denied.state.status).toBe(403)
expect(denied.state.body).toBe('forbidden')
}
const read = fakeResponse()
await routes[0]!.handler(fakeRequest({ host: 'harness.example' }), read.response)
expect(read.state.status).not.toBe(403)
await dispose()
})
it('passes loopback and declared-authority requests through to the bridge', async () => {
const { routes, dispose } = await mounted({ trustedHosts: ['harness.example:3080', '192.168.1.5'] })
// Loopback, no browser markers (curl shape): the fence passes; the carrier
@@ -118,3 +145,69 @@ describe('connection node half', () => {
await dispose()
})
})
describe('connection node half over a real HTTP server', () => {
/** Serve the registered prefix route from a real server and return its port. */
async function serve(routes: WebRoute[]): Promise<{ port: number; close: () => Promise<void> }> {
const server = createServer((request, response) => {
void routes[0]!.handler(request, response)
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address() as AddressInfo
return {
port: address.port,
close: () => new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error === undefined || error === null) resolve()
else reject(error)
})
}),
}
}
/** One real request; `host` spoofs the authority the way a LAN client's browser would send it. */
function call(port: number, method: string, host: string): Promise<number> {
return new Promise((resolve, reject) => {
const request = httpRequest(
{ host: '127.0.0.1', port, path: `${API_PATH}/${method}`, method: 'GET', headers: { host } },
(response) => {
response.resume()
response.on('end', () => { resolve(response.statusCode ?? 0) })
},
)
request.on('error', reject)
request.end()
})
}
it('answers a declared LAN authority with 403 on every configuration method, over real HTTP', async () => {
// The fence's input is a real IncomingMessage parsed by Node from the
// wire, not a hand-assembled object: the Host header a LAN browser sends
// is exactly what decides loopback-only here, so the boundary is asserted
// against the parse the server actually performs.
const { routes, dispose } = await mounted({ trustedHosts: ['harness.example'] })
const { port, close } = await serve(routes)
try {
// Reads are as privileged as writes: describe returns the exposed
// configuration, and credentials.describe probes arbitrary env-var names.
for (const method of [
'settings.describe', 'settings.update', 'settings.replace',
'credentials.describe', 'credentials.set', 'credentials.unset',
'host.pickDirectory', 'host.openPath',
]) {
expect([method, await call(port, method, 'harness.example')]).toEqual([method, 403])
}
// The model catalog stays reachable for the same authority: a LAN
// client's model picker needs it, and it carries no key or endpoint
// state (404 is the empty proxy's carrier answer — the fence passed).
for (const method of ['llm.providers', 'llm.models']) {
expect([method, await call(port, method, 'harness.example')]).toEqual([method, 404])
}
// Loopback reaches everything, configuration included.
expect(await call(port, 'settings.describe', `127.0.0.1:${String(port)}`)).toBe(404)
} finally {
await close()
await dispose()
}
})
})