From 7e6a1c5d6e0b96ae512f14b638c0debbbc98b2ab Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Tue, 4 Aug 2026 06:35:02 +0800 Subject: [PATCH 1/2] fix(kosong): forward full host headers only to first-party endpoints resolveOutboundHeaders decided solely on the provider type, so a kimi-typed provider pointed at a third-party base_url received the complete X-Msh-* identity set, device id included. Gate the full forwarding on the resolved endpoint being a first-party Moonshot host; everything else gets the User-Agent only. --- .../fix-host-headers-first-party-only.md | 5 +++++ .../src/kosong/model/catalogService.ts | 21 ++++++++++++++++++- .../test/kosong/model/catalog.test.ts | 19 +++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-host-headers-first-party-only.md diff --git a/.changeset/fix-host-headers-first-party-only.md b/.changeset/fix-host-headers-first-party-only.md new file mode 100644 index 0000000000..0662b253c3 --- /dev/null +++ b/.changeset/fix-host-headers-first-party-only.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Stop forwarding the host identity headers (including X-Msh-Device-Id) to kimi-typed providers whose endpoint is not the first-party Moonshot host, so a Kimi-compatible proxy or gateway no longer receives the device identity set. diff --git a/packages/agent-core-v2/src/kosong/model/catalogService.ts b/packages/agent-core-v2/src/kosong/model/catalogService.ts index c0b1c1ae9c..9b663a4e8c 100644 --- a/packages/agent-core-v2/src/kosong/model/catalogService.ts +++ b/packages/agent-core-v2/src/kosong/model/catalogService.ts @@ -397,6 +397,7 @@ export class ModelCatalog extends Disposable implements IModelCatalog { providerConfig?.type, providerConfig?.customHeaders, this.hostRequestHeaders.headers, + resolvedBaseUrl, ), capabilities, maxContextSize: model.maxContextSize, @@ -559,14 +560,32 @@ export function resolveOutboundHeaders( providerType: string | undefined, customHeaders: Readonly> | undefined, hostHeaders: Readonly>, + baseUrl: string | undefined, ): Readonly> { + // A vendor's `hostHeaders: 'full'` contract is meant for the vendor's own + // endpoint. A provider that speaks the same protocol but points elsewhere + // must not receive the host identity set (device id included). const forwardsAll = providerType !== undefined && - getProviderDefinition(providerType)?.hostHeaders === 'full'; + getProviderDefinition(providerType)?.hostHeaders === 'full' && + isFirstPartyBaseUrl(baseUrl); const hostLayer = forwardsAll ? hostHeaders : userAgentOnly(hostHeaders); return { ...parseKimiCodeCustomHeaders(), ...hostLayer, ...customHeaders }; } +const FIRST_PARTY_HOSTS = new Set(['api.moonshot.ai', 'api.moonshot.cn']); + +function isFirstPartyBaseUrl(baseUrl: string | undefined): boolean { + if (baseUrl === undefined) { + return true; + } + try { + return FIRST_PARTY_HOSTS.has(new URL(baseUrl).hostname); + } catch { + return false; + } +} + function userAgentOnly(headers: Readonly>): Record { const userAgent = headers['User-Agent']; return userAgent === undefined ? {} : { 'User-Agent': userAgent }; diff --git a/packages/agent-core-v2/test/kosong/model/catalog.test.ts b/packages/agent-core-v2/test/kosong/model/catalog.test.ts index a2d308a64d..85c63f4fed 100644 --- a/packages/agent-core-v2/test/kosong/model/catalog.test.ts +++ b/packages/agent-core-v2/test/kosong/model/catalog.test.ts @@ -167,6 +167,22 @@ describe('Model assembly (pure data)', () => { } }); + it('keeps full host headers when a kimi provider explicitly targets the first-party host', () => { + const { host, catalog } = createHost({ + providers: { kimi: { type: 'kimi', apiKey: 'sk', baseUrl: 'https://api.moonshot.ai/v1' } }, + models: { k2: { provider: 'kimi', model: 'kimi-k2', maxContextSize: 200000 } }, + }); + try { + const model = catalog.get('k2'); + expect(model.headers).toMatchObject({ + 'User-Agent': 'kimi-test/1.0', + 'X-Msh-Device-Id': 'device-1', + }); + } finally { + host.dispose(); + } + }); + it('forwards only the User-Agent to vendors without a full hostHeaders declaration', () => { const { host, catalog } = createHost({ providers: { @@ -199,6 +215,9 @@ describe('Model assembly (pure data)', () => { expect(model.baseUrl).toBe('https://api.example.test'); // Kimi thinking is trait-driven: no Anthropic effort profile is inferred. expect(model.supportEfforts).toBeUndefined(); + // A kimi-typed provider pointed at a third-party host gets only the + // User-Agent, never the host identity set. + expect(model.headers).toEqual({ 'User-Agent': 'kimi-test/1.0' }); } finally { host.dispose(); } From d7fb99d60cea7d88db9b31dfd38aaa1460b166e7 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:35:40 +0800 Subject: [PATCH 2/2] fix(kosong): require https for first-party identity forwarding Codex review on #2581: the hostname-only check would forward the device identity set to http://api.moonshot.ai in cleartext. The first-party gate now also requires the https scheme. Move the gate into hostRequestHeaders (shared with inspection, which attributes header provenance and now applies the same first-party decision so the attribution matches what actually goes out), and fold the rationale into the catalog module header instead of the inline comment. --- .../src/kosong/model/catalogService.ts | 25 +++++-------------- .../src/kosong/model/hostRequestHeaders.ts | 21 ++++++++++++++++ .../src/kosong/model/inspection.ts | 6 ++++- .../test/kosong/model/catalog.test.ts | 13 ++++++++++ 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/packages/agent-core-v2/src/kosong/model/catalogService.ts b/packages/agent-core-v2/src/kosong/model/catalogService.ts index 9b663a4e8c..50a22f33ae 100644 --- a/packages/agent-core-v2/src/kosong/model/catalogService.ts +++ b/packages/agent-core-v2/src/kosong/model/catalogService.ts @@ -21,8 +21,11 @@ * referenced provider vendor's declared `baseProtocol`; endpoint and * credential env fallbacks resolve through `resolveProviderEndpoint` against * the config env bag; host-header forwarding follows the vendor definition's - * `hostHeaders`; capability detection is `resolveCapability(protocol, name, - * providerType)`. + * `hostHeaders`, scoped by `isFirstPartyBaseUrl` — a vendor's + * `hostHeaders: 'full'` contract covers its own endpoint only, so a provider + * that speaks the same protocol but points elsewhere receives just the + * `User-Agent`, never the device identity set; capability detection is + * `resolveCapability(protocol, name, providerType)`. * * Caching (load-bearing): assembled entries are invalidated ONLY by the * model/provider config-change events. Tests that mutate config @@ -90,7 +93,7 @@ import { toProtocolProvider, } from './catalog'; import { ModelCatalogErrors } from './errors'; -import { IHostRequestHeaders } from './hostRequestHeaders'; +import { IHostRequestHeaders, isFirstPartyBaseUrl } from './hostRequestHeaders'; import { assembleModelInspection, attributeEffectiveFields, @@ -562,9 +565,6 @@ export function resolveOutboundHeaders( hostHeaders: Readonly>, baseUrl: string | undefined, ): Readonly> { - // A vendor's `hostHeaders: 'full'` contract is meant for the vendor's own - // endpoint. A provider that speaks the same protocol but points elsewhere - // must not receive the host identity set (device id included). const forwardsAll = providerType !== undefined && getProviderDefinition(providerType)?.hostHeaders === 'full' && @@ -573,19 +573,6 @@ export function resolveOutboundHeaders( return { ...parseKimiCodeCustomHeaders(), ...hostLayer, ...customHeaders }; } -const FIRST_PARTY_HOSTS = new Set(['api.moonshot.ai', 'api.moonshot.cn']); - -function isFirstPartyBaseUrl(baseUrl: string | undefined): boolean { - if (baseUrl === undefined) { - return true; - } - try { - return FIRST_PARTY_HOSTS.has(new URL(baseUrl).hostname); - } catch { - return false; - } -} - function userAgentOnly(headers: Readonly>): Record { const userAgent = headers['User-Agent']; return userAgent === undefined ? {} : { 'User-Agent': userAgent }; diff --git a/packages/agent-core-v2/src/kosong/model/hostRequestHeaders.ts b/packages/agent-core-v2/src/kosong/model/hostRequestHeaders.ts index 53b3bd0da8..1da2e94f83 100644 --- a/packages/agent-core-v2/src/kosong/model/hostRequestHeaders.ts +++ b/packages/agent-core-v2/src/kosong/model/hostRequestHeaders.ts @@ -19,3 +19,24 @@ export interface IHostRequestHeaders { } export const IHostRequestHeaders = createDecorator('hostRequestHeaders'); + +const FIRST_PARTY_HOSTS = new Set(['api.moonshot.ai', 'api.moonshot.cn']); + +/** + * True when a base URL points at the vendor's own endpoint, the only place + * the full host identity set (device id included) may be forwarded to. + * HTTPS is required: the same hostname over plain HTTP must not receive + * those headers in the clear. An unset base URL means the vendor's default + * endpoint, which is first-party by definition. + */ +export function isFirstPartyBaseUrl(baseUrl: string | undefined): boolean { + if (baseUrl === undefined) { + return true; + } + try { + const url = new URL(baseUrl); + return url.protocol === 'https:' && FIRST_PARTY_HOSTS.has(url.hostname); + } catch { + return false; + } +} diff --git a/packages/agent-core-v2/src/kosong/model/inspection.ts b/packages/agent-core-v2/src/kosong/model/inspection.ts index bf94aae227..3dc5c6421e 100644 --- a/packages/agent-core-v2/src/kosong/model/inspection.ts +++ b/packages/agent-core-v2/src/kosong/model/inspection.ts @@ -31,6 +31,7 @@ import { getProviderDefinition } from '../provider/providerDefinition'; import type { ModelRecord } from './model'; import type { ResolvedModelAuthMaterial } from './model.types'; +import { isFirstPartyBaseUrl } from './hostRequestHeaders'; export interface InspectedAuth { @@ -476,9 +477,12 @@ function attributeHeaders( ): void { const envLayer = parseKimiCodeCustomHeaders(); const rawHost = trace.captured>>(TRACE.hostHeaders) ?? {}; + // Keep the attribution aligned with resolveOutboundHeaders: full host + // identity only counts as forwarded on a first-party endpoint. const forwardsAll = providerConfig?.type !== undefined && - getProviderDefinition(providerConfig.type)?.hostHeaders === 'full'; + getProviderDefinition(providerConfig.type)?.hostHeaders === 'full' && + isFirstPartyBaseUrl(providerConfig.baseUrl); const hostLayer: Readonly> = forwardsAll ? rawHost : rawHost['User-Agent'] === undefined diff --git a/packages/agent-core-v2/test/kosong/model/catalog.test.ts b/packages/agent-core-v2/test/kosong/model/catalog.test.ts index 85c63f4fed..b246defff4 100644 --- a/packages/agent-core-v2/test/kosong/model/catalog.test.ts +++ b/packages/agent-core-v2/test/kosong/model/catalog.test.ts @@ -183,6 +183,19 @@ describe('Model assembly (pure data)', () => { } }); + it('withholds the identity set from a first-party hostname over plain http', () => { + const { host, catalog } = createHost({ + providers: { kimi: { type: 'kimi', apiKey: 'sk', baseUrl: 'http://api.moonshot.ai/v1' } }, + models: { k2: { provider: 'kimi', model: 'kimi-k2', maxContextSize: 200000 } }, + }); + try { + const model = catalog.get('k2'); + expect(model.headers).toEqual({ 'User-Agent': 'kimi-test/1.0' }); + } finally { + host.dispose(); + } + }); + it('forwards only the User-Agent to vendors without a full hostHeaders declaration', () => { const { host, catalog } = createHost({ providers: {