Skip to content

fix(agent-core-v2): resolve provider credentials from process env in the auth gate - #2746

Open
JacobStephens2 wants to merge 3 commits into
MoonshotAI:mainfrom
JacobStephens2:fix/v2-auth-gate-process-env
Open

fix(agent-core-v2): resolve provider credentials from process env in the auth gate#2746
JacobStephens2 wants to merge 3 commits into
MoonshotAI:mainfrom
JacobStephens2:fix/v2-auth-gate-process-env

Conversation

@JacobStephens2

@JacobStephens2 JacobStephens2 commented Aug 8, 2026

Copy link
Copy Markdown

Fixes #2745.

Problem

resolveModelAuthMaterial passes args.provider?.env ?? {} to
explainProviderEndpoint:

: explainProviderEndpoint(providerAuthType, args.provider?.env ?? {});

explainProviderEndpoint defaults that parameter to process.env, so passing {}
replaces the default with an empty map for every provider that has no
[providers.<id>.env] table. The vendor's declared apiKeyEnv (OPENAI_API_KEY
for type = "openai") is then looked up in an empty map and
AuthService.ensureReady throws AuthTokenMissingError.

The adapters that actually issue the request do read the process environment —
firstProcessEnv(endpoint?.apiKeyEnv) in openai-legacy.contrib.ts,
openai-responses.contrib.ts, anthropic.contrib.ts and
google-genai.contrib.ts — so the readiness gate is stricter than the code it
guards, and rejects configurations that would have worked.

This was latent behind the experimental flag until 0.33.0 inverted the engine
selection in experimental-v2.ts (KIMI_CODE_EXPERIMENTAL_FLAG opt-in became
KIMI_CODE_LEGACY_FLAG opt-out), making agent-core-v2 the default for kimi -p.
That turned it into a user-visible regression: print mode now rejects an
environment-supplied provider key that 0.32.0 accepted. Full bisect and root-cause
evidence in #2745.

Change

Credentials are resolved in three ordered phases, and the process environment is
consulted only when nothing is configured at any layer.

  1. Explicitly configured credentials, resolved in isolation. The provider's
    own env bag is passed to explainProviderEndpoint on its own, exactly as
    before, so ambient variables never compete with configured ones.
  2. OAuth, if the provider declares it.
  3. Ambient process.env, as a last resort — this is the case that previously
    threw.

Only explicitly configured credentials take part in the apiKey/oauth
mutual-exclusion check.

Resulting precedence:

model.apiKey > model.oauth > provider.apiKey > provider.env bag >
provider.oauth > process.env

That is the documented order, with one deliberate difference: provider.oauth
now outranks an ambient process.env key. Nothing regresses, because before this
change process.env could not be reached from this function at all.

The resolution trace previously labelled every env hit as coming from the
"env bag"; it now distinguishes the bag from the process environment, so --trace
output stays truthful.

Note on the first revision of this PR

The initial commit merged process.env into the bag before resolution
({ ...process.env, ...args.provider?.env }). The Codex review correctly flagged
two P1 defects in that approach, both of which I reproduced as failing tests
before rewriting it:

  • Precedence inversion. standard.contrib.ts declares VERTEXAI_API_KEY
    ahead of GOOGLE_API_KEY. A provider explicitly configuring GOOGLE_API_KEY
    on a host that also exported VERTEXAI_API_KEY resolved the ambient Vertex
    key, silently sending the wrong credential.
  • OAuth providers broken. An unrelated vendor key present in the shell became
    providerApiKey and tripped the mutual-exclusion check, so a previously working
    oauth provider failed with config.invalid.

Resolving the configured bag in isolation and moving the OAuth branch ahead of the
ambient lookup fixes both. Each has a regression test that fails on the previous
revision.

Tests

Six cases in packages/agent-core-v2/test/kosong/model/modelAuth.test.ts, using
vi.stubEnv with afterEach(() => vi.unstubAllEnvs()):

  • process-env fallback resolves when the provider declares no env bag;
  • the provider env bag and an inline apiKey both still win over the process
    environment;
  • a configured GOOGLE_API_KEY beats an ambient VERTEXAI_API_KEY despite the
    endpoint chain declaring Vertex first;
  • an ambient key does not invalidate a provider configured for oauth;
  • a configured apiKey alongside oauth is still rejected, whether the key comes
    from provider.apiKey or the provider's env bag;
  • a type = "anthropic" provider does not pick up an unrelated OPENAI_API_KEY.

The first fails on main; the third and fourth fail on this PR's first revision.

packages/agent-core-v2/test/kosong and test/app/auth are green (359 tests), as
are pnpm lint and tsc --noEmit for agent-core-v2.

Verification against the shipped CLI

Reproduced on Linux x86_64 / Node v22.22.2 with an openai-type provider whose
key exists only in OPENAI_API_KEY. A deliberately invalid key was used
throughout, so a provider 401 is the signal that the readiness gate was passed
and the request was actually issued:

build kimi -p with the key in the environment
0.32.0 request issued (provider 401)
0.33.0 provider oai has no credential configured
0.34.0 provider oai has no credential configured
0.34.0 with this change applied to dist/main.mjs request issued (provider 401)

Flipping the engine flag instead of the version moves the behaviour the same way,
which is what identifies 0.33.0's engine switch rather than any auth change as the
regression trigger:

run result
0.34.0 + KIMI_CODE_LEGACY_FLAG=1 request issued
0.32.0 + KIMI_CODE_EXPERIMENTAL_FLAG=1 provider oai has no credential configured

A control on the patched bundle confirms the configured bag still wins: with
[providers.oai.env] OPENAI_API_KEY set and a different ambient value present,
the request carries the bag's key.

Independent confirmation on two more platforms

The same checks were re-run on two additional machines. Every verdict matched:

platform arch Node result
macOS 26.5.2 arm64 v26.5.0 identical
Ubuntu 24.04.3 LTS x86_64 v22.22.0 identical

So the regression is not specific to a platform, architecture, or Node major.

On macOS the session wire log was inspected directly. A gate-blocked run emits only
metadata, profile.bind and permission.set_mode — no turn.prompt and no
llm.request — while a run that passes the gate adds turn.prompt,
llm.tools_snapshot, llm.request and turn.ended. The run aborts before the turn
begins, confirming at the trace level that no request is issued.

End-to-end with a real credential from a production secret store

The runs above use a deliberately invalid key, so they prove the gate's behaviour but
not that a valid environment-supplied credential reaches the model. This one does.

Ubuntu 24.04.3 / x86_64 / Node v22.22.0. The credential is resolved from Bitwarden
Secrets Manager and injected into the child environment by vaulted-agent, a vault
launcher whose entire mechanism is environment injection — the real-world shape this
regression breaks. The provider table contains no api_key, so the environment is
the only possible source. All three probes ran under a single vault injection in
one child shell, so the environment is provably identical and only the binary/flag
differs:

probe build flag result
A 0.34.0 stock provider oai has no credential configured
B 0.34.0 + this change PONG — model answered
C 0.34.0 stock KIMI_CODE_LEGACY_FLAG=1 PONG

Because B succeeds with no flag and no inline api_key, the failure in A cannot be
attributed to key validity, config path, provider wiring, or network reachability.

That transcript was produced with this PR's first revision. The rewrite does not
change behaviour for that configuration — a provider with no env bag and no
oauth reaches the same ambient lookup by both routes — and the dummy-key check
above was re-run against the current revision on the shipped bundle. I have not
re-run the live-key probe against the rewrite.

The patch was applied to a copy of the shipped dist/main.mjs; the target
expression occurs exactly once in the 0.34.0 bundle.

Notes for reviewers

  • Scope is deliberately limited to the credential gate. catalogService.ts has the
    same providerConfig.env ?? {} pattern for baseUrl resolution; that one does
    not block a turn, and changing it would let an ambient OPENAI_BASE_URL
    redirect configured providers, so I left it alone. Happy to address it here or
    in a follow-up if you'd prefer consistency.
  • If you would rather thread an explicit env source through
    resolveModelAuthMaterial than read process.env at this layer, say the word
    and I will rework it — the observable behaviour is the part I care about.
  • The interactive TUI path was not tested on any machine; that it is unaffected is
    an inference from the engine-selection code, not a measurement.

…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.<id>.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.
@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 79ebe02

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f57262e276

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +68 to +71
: explainProviderEndpoint(providerAuthType, {
...process.env,
...args.provider?.env,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve configured env before ambient fallback

When a google-genai provider explicitly configures GOOGLE_API_KEY while the host also exports VERTEXAI_API_KEY, merging both maps before resolution lets the endpoint declaration order in standard.contrib.ts:59-60 select the ambient Vertex key first. This reverses the intended provider-env-over-process-env precedence and can send requests with the wrong credential; resolve the provider env bag first, then consult process.env only if the bag contains no usable declared key.

Useful? React with 👍 / 👎.

Comment on lines +68 to +71
: explainProviderEndpoint(providerAuthType, {
...process.env,
...args.provider?.env,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep ambient keys from invalidating OAuth providers

When a provider has a valid oauth configuration and the corresponding vendor key happens to exist in process.env, this merge turns the ambient key into providerApiKey, so the conflict check immediately throws config.invalid. Such OAuth configurations worked before this change and the request path supplies the OAuth token explicitly, so an unrelated shell-level key should not make them unusable; only explicitly configured provider credentials should participate in the apiKey/OAuth conflict check.

Useful? React with 👍 / 👎.

…t 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.
@JacobStephens2

Copy link
Copy Markdown
Author

Thanks — both findings were real, and I reproduced each as a failing test before changing anything.

P1a, precedence inversion. standard.contrib.ts declares VERTEXAI_API_KEY ahead of GOOGLE_API_KEY, so a provider explicitly configuring GOOGLE_API_KEY on a host that also exported VERTEXAI_API_KEY resolved the ambient Vertex key. Silent wrong-credential, exactly as described.

P1b, OAuth providers. An unrelated vendor key in the shell became providerApiKey and tripped the mutual-exclusion check, so a working oauth provider started failing with Provider "…" has both apiKey and oauth.

The merge-then-resolve approach can't avoid either, so I dropped it. 79ebe02 resolves in three ordered phases instead:

  1. the provider's own env bag, passed to explainProviderEndpoint in isolation — ambient values never compete with configured ones, so the endpoint chain's declaration order can no longer invert user intent;
  2. oauth, if declared;
  3. process.env, only when nothing is configured at any layer.

Only explicitly configured credentials take part in the apiKey/oauth conflict check, which is what unbreaks P1b.

Precedence is now model.apiKey > model.oauth > provider.apiKey > provider.env > provider.oauth > process.env. The one deliberate difference from the documented order is that provider.oauth outranks an ambient key — that ordering didn't exist before, since process.env was unreachable from this function.

Regression tests for both cases are in the same commit; each fails on f57262e and passes on 79ebe02. test/kosong and test/app/auth are green at 359 tests, plus lint and tsc --noEmit. I also re-ran the dummy-key check against the shipped 0.34.0 bundle with the rewritten logic to confirm the original bug is still fixed, with a control verifying a configured env bag still beats an ambient variable of the same name.

@JacobStephens2

Copy link
Copy Markdown
Author

Reopened — apologies for the noise. This was closed accidentally by a cross-repo closing keyword: a PR in an unrelated repository of mine referenced this one as "fix #2746", and merging it auto-closed this PR. Nothing here is withdrawn, and the branch is unchanged at 79ebe02.

The reference has been corrected on my side so it can't happen again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant