From f57262e27620262eb08c6e8f0d9b2e5c066ddce9 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Sat, 8 Aug 2026 21:14:33 +0000 Subject: [PATCH 1/3] fix(agent-core-v2): resolve provider credentials from process env in the auth gate resolveModelAuthMaterial passed `args.provider?.env ?? {}` to explainProviderEndpoint, replacing that function's `process.env` default with an empty map for every provider without a `[providers..env]` table. The vendor's declared apiKeyEnv was therefore looked up in an empty map and authService.ensureReady threw AuthTokenMissingError, even though the adapters that issue the request do read process.env. This was latent behind the experimental flag until 0.33.0 made agent-core-v2 the default engine for `kimi -p`, which turned it into a user-visible regression: print mode rejects an environment-supplied provider key that 0.32.0 accepted. Merge the provider env bag over process.env so the gate and the request client agree on where a key may come from, with explicit config still taking precedence (model apiKey > provider apiKey > provider env bag > process env). Also report the actual source in the resolution trace. --- .../src/kosong/model/modelAuth.ts | 12 +++++-- .../test/kosong/model/modelAuth.test.ts | 36 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/agent-core-v2/src/kosong/model/modelAuth.ts b/packages/agent-core-v2/src/kosong/model/modelAuth.ts index e2a923edd4..b05d2ee59a 100644 --- a/packages/agent-core-v2/src/kosong/model/modelAuth.ts +++ b/packages/agent-core-v2/src/kosong/model/modelAuth.ts @@ -65,7 +65,10 @@ export function resolveModelAuthMaterial( const providerEndpoint = providerAuthType === undefined ? {} - : explainProviderEndpoint(providerAuthType, args.provider?.env ?? {}); + : explainProviderEndpoint(providerAuthType, { + ...process.env, + ...args.provider?.env, + }); const providerApiKey = nonEmpty(args.provider?.apiKey) ?? nonEmpty(providerEndpoint.apiKey); if (providerApiKey !== undefined && args.provider?.oauth !== undefined) { throw authConflictError('Provider', args.providerName); @@ -77,7 +80,12 @@ export function resolveModelAuthMaterial( ? { kind: 'config', detail: `provider '${args.providerName}' apiKey` } : { kind: 'env', - detail: `${providerEndpoint.apiKeyEnvName ?? '?'} (provider '${args.providerName}' env bag)`, + detail: `${providerEndpoint.apiKeyEnvName ?? '?'} (${ + providerEndpoint.apiKeyEnvName !== undefined && + args.provider?.env?.[providerEndpoint.apiKeyEnvName] !== undefined + ? `provider '${args.providerName}' env bag` + : 'process env' + })`, }, ); return { apiKey: providerApiKey }; diff --git a/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts b/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts index ec7540a765..2e18065208 100644 --- a/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts +++ b/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts @@ -12,7 +12,7 @@ * profile — inferred only for vendors whose thinking is not trait-driven. */ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { ConfigErrors } from '#/app/config/errors'; import '#/kosong/provider/providers/kimi/kimi.contrib'; @@ -101,10 +101,44 @@ describe('resolveModelAuthMaterial', () => { ).toEqual({ apiKey: 'vertex-env-key' }); }); + it('falls back to the process environment when the provider declares no env bag', () => { + vi.stubEnv('OPENAI_API_KEY', 'process-env-key'); + expect(authMaterial({ model: { model: 'm' }, provider: { type: 'openai' } })).toEqual({ + apiKey: 'process-env-key', + }); + }); + + it('prefers the provider env bag and inline apiKey over the process environment', () => { + vi.stubEnv('OPENAI_API_KEY', 'process-env-key'); + expect( + authMaterial({ + model: { model: 'm' }, + provider: { type: 'openai', env: { OPENAI_API_KEY: 'bag-key' } }, + }), + ).toEqual({ apiKey: 'bag-key' }); + expect( + authMaterial({ + model: { model: 'm' }, + provider: { type: 'openai', apiKey: 'inline-key' }, + }), + ).toEqual({ apiKey: 'inline-key' }); + }); + + it('does not leak an unrelated vendor key from the process environment', () => { + vi.stubEnv('OPENAI_API_KEY', 'process-env-key'); + vi.stubEnv('ANTHROPIC_API_KEY', ''); + expect(authMaterial({ model: { model: 'm' }, provider: { type: 'anthropic' } })).toEqual({}); + }); + it('returns empty material when nothing is configured', () => { + vi.stubEnv('OPENAI_API_KEY', ''); expect(authMaterial({ model: { model: 'm' }, provider: { type: 'openai' } })).toEqual({}); expect(authMaterial({ model: { model: 'm' } })).toEqual({}); }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); }); describe('effectiveModelConfig', () => { From 62e998550767546506c9d6bc093e77c2802ce368 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Sat, 8 Aug 2026 21:27:08 +0000 Subject: [PATCH 2/3] chore: add changeset --- .changeset/print-mode-env-provider-key.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/print-mode-env-provider-key.md diff --git a/.changeset/print-mode-env-provider-key.md b/.changeset/print-mode-env-provider-key.md new file mode 100644 index 0000000000..237d9bbc37 --- /dev/null +++ b/.changeset/print-mode-env-provider-key.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix `kimi -p` refusing to start with an environment-supplied provider key: the auth readiness gate looked the vendor's declared `apiKeyEnv` up in the provider's `env` bag alone, so a key present only in the process environment was reported as `provider has no credential configured` even though the adapter issuing the request would have read it. The gate now falls back to the process environment, with an explicit `[providers..env]` entry still taking precedence. From 79ebe02fa08850ffac45e6c01f917f63efebf3b6 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Sat, 8 Aug 2026 23:47:42 +0000 Subject: [PATCH 3/3] fix(agent-core-v2): resolve configured provider env before the ambient fallback Addresses two review findings on the previous revision. Merging process.env into the provider env bag before resolution let ambient values compete with configured ones: - a provider that explicitly configures GOOGLE_API_KEY on a host that also exports VERTEXAI_API_KEY resolved the ambient Vertex key, because standard.contrib declares VERTEXAI_API_KEY earlier in the endpoint chain; - an unrelated vendor key present in the shell was treated as providerApiKey and tripped the apiKey/oauth mutual-exclusion check, breaking provider configurations that previously worked. The provider's own env bag is now resolved in isolation, oauth is consulted before any ambient lookup, and process.env is read only as a last resort when nothing is configured at any layer. Only explicitly configured credentials participate in the conflict check. Precedence is unchanged from the documented order except that provider oauth now outranks an ambient process.env key, which previously could not be reached at all. --- .../src/kosong/model/modelAuth.ts | 46 +++++++++++++------ .../test/kosong/model/modelAuth.test.ts | 41 +++++++++++++++++ 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/packages/agent-core-v2/src/kosong/model/modelAuth.ts b/packages/agent-core-v2/src/kosong/model/modelAuth.ts index b05d2ee59a..340348b600 100644 --- a/packages/agent-core-v2/src/kosong/model/modelAuth.ts +++ b/packages/agent-core-v2/src/kosong/model/modelAuth.ts @@ -62,33 +62,35 @@ export function resolveModelAuthMaterial( } const providerAuthType = args.provider?.type ?? args.model.protocol; - const providerEndpoint = + + // Explicitly configured credentials are resolved first and in isolation: the + // provider's own `env` bag must not have to compete with ambient process env, + // or a vendor whose endpoint chain declares several keys (google-genai lists + // VERTEXAI_API_KEY ahead of GOOGLE_API_KEY) could resolve an ambient key in + // preference to the one the user configured. + const configuredEndpoint = providerAuthType === undefined ? {} - : explainProviderEndpoint(providerAuthType, { - ...process.env, - ...args.provider?.env, - }); - const providerApiKey = nonEmpty(args.provider?.apiKey) ?? nonEmpty(providerEndpoint.apiKey); - if (providerApiKey !== undefined && args.provider?.oauth !== undefined) { + : explainProviderEndpoint(providerAuthType, args.provider?.env ?? {}); + const configuredApiKey = nonEmpty(args.provider?.apiKey) ?? nonEmpty(configuredEndpoint.apiKey); + + // Only explicitly configured credentials participate in the apiKey/oauth + // conflict check. An unrelated key that merely happens to exist in the shell + // must never invalidate a working oauth provider. + if (configuredApiKey !== undefined && args.provider?.oauth !== undefined) { throw authConflictError('Provider', args.providerName); } - if (providerApiKey !== undefined) { + if (configuredApiKey !== undefined) { trace?.record( 'resolved.auth', nonEmpty(args.provider?.apiKey) !== undefined ? { kind: 'config', detail: `provider '${args.providerName}' apiKey` } : { kind: 'env', - detail: `${providerEndpoint.apiKeyEnvName ?? '?'} (${ - providerEndpoint.apiKeyEnvName !== undefined && - args.provider?.env?.[providerEndpoint.apiKeyEnvName] !== undefined - ? `provider '${args.providerName}' env bag` - : 'process env' - })`, + detail: `${configuredEndpoint.apiKeyEnvName ?? '?'} (provider '${args.providerName}' env bag)`, }, ); - return { apiKey: providerApiKey }; + return { apiKey: configuredApiKey }; } if (args.provider?.oauth !== undefined) { trace?.record('resolved.auth', { @@ -100,6 +102,20 @@ export function resolveModelAuthMaterial( oauthProviderKey: args.model.providerId ?? args.model.provider, }; } + + // Nothing was configured anywhere. Fall back to the ambient process env, which + // is what the adapters themselves read when they construct the request; without + // this the readiness gate is stricter than the code it guards. + const ambientEndpoint = + providerAuthType === undefined ? {} : explainProviderEndpoint(providerAuthType, process.env); + const ambientApiKey = nonEmpty(ambientEndpoint.apiKey); + if (ambientApiKey !== undefined) { + trace?.record('resolved.auth', { + kind: 'env', + detail: `${ambientEndpoint.apiKeyEnvName ?? '?'} (process env)`, + }); + return { apiKey: ambientApiKey }; + } trace?.record('resolved.auth', { kind: 'none', detail: 'no credential resolved at any layer (adapter construction may still read process.env)', diff --git a/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts b/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts index 2e18065208..682801be27 100644 --- a/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts +++ b/packages/agent-core-v2/test/kosong/model/modelAuth.test.ts @@ -124,6 +124,47 @@ describe('resolveModelAuthMaterial', () => { ).toEqual({ apiKey: 'inline-key' }); }); + it('prefers a configured env-bag key over an ambient key declared earlier in the chain', () => { + // google-genai declares VERTEXAI_API_KEY ahead of GOOGLE_API_KEY, so an + // ambient Vertex key must not outrank the Gemini key the user configured. + vi.stubEnv('VERTEXAI_API_KEY', 'ambient-vertex-key'); + expect( + authMaterial({ + model: { model: 'm' }, + provider: { type: 'google-genai', env: { GOOGLE_API_KEY: 'configured-google-key' } }, + }), + ).toEqual({ apiKey: 'configured-google-key' }); + }); + + it('does not let an ambient key invalidate a provider configured for oauth', () => { + vi.stubEnv('OPENAI_API_KEY', 'ambient-unrelated-key'); + expect( + authMaterial({ + model: { model: 'm', providerId: 'p1' }, + provider: { type: 'openai', oauth: { storage: 'file', key: 'k' } }, + }), + ).toEqual({ oauth: { storage: 'file', key: 'k' }, oauthProviderKey: 'p1' }); + }); + + it('still rejects a configured apiKey alongside oauth', () => { + expect(() => + authMaterial({ + model: { model: 'm' }, + provider: { type: 'openai', apiKey: 'k', oauth: { storage: 'file', key: 'k' } }, + }), + ).toThrowError(expect.objectContaining({ code: ConfigErrors.codes.CONFIG_INVALID })); + expect(() => + authMaterial({ + model: { model: 'm' }, + provider: { + type: 'openai', + env: { OPENAI_API_KEY: 'bag-key' }, + oauth: { storage: 'file', key: 'k' }, + }, + }), + ).toThrowError(expect.objectContaining({ code: ConfigErrors.codes.CONFIG_INVALID })); + }); + it('does not leak an unrelated vendor key from the process environment', () => { vi.stubEnv('OPENAI_API_KEY', 'process-env-key'); vi.stubEnv('ANTHROPIC_API_KEY', '');