fix(config): close the review findings on configuration source ownership

Two had real security consequences:

The bootstrap rejection ran on npm dotenv's parser while process.loadEnvFile
applied the file with Node's own. Two independently maintained dialects meant
the check and the thing it guards could disagree: a name Node accepts but the
checker misses would reach process.env unchecked, and BASH_ENV there runs a
file of the project's choosing on every `bash -c` the bash tool issues. Parse
once with node:util's parseEnv — the same engine loadEnvFile uses — and assign
the entries already checked, which also drops the dotenv dependency.

llm-pi-ai still returned a literal profile.apiKey ahead of everything, and it
registers a settings namespace, so the defect removed from llm-deepseek
survived intact in its design twin. The field is gone from the profile schema,
the resolution path, and the tests.

The rest are consistency and documentation defects the review named:

- verify-config-source-ownership did not scan the Python runtime's bundled
  cordis.yml, which still inlined apiKey and baseURL. Both are covered now, and
  the line-anchored INLINE_DENY documents that it is a tripwire, not a parser.
- The deny list missed NODE_TLS_REJECT_UNAUTHORIZED, the askpass hooks, the
  GIT_CONFIG_* redirections, and PYTHONHOME — all implied by its own stated
  rule about what a variable does.
- Snapshot lookups folded case on Windows, where environment names are
  case-insensitive and an exact-match Map could miss a higher-ranked layer.
- The credentials note claimed a read-time permission check was "not taken"
  while this PR implemented it; the credentials-local README still described
  two layers, live process.env reads, dotenv-era limitations, and a renamed
  anchor; the llm-deepseek README still advertised the removed literal apiKey;
  and web.ts and base.cordis.yml kept personal-overlay wording.
- The ownership note's literal-apiKey claim now names its scope: the
  web-search providers keep a literal field but register no settings
  namespace, so nothing can shadow a stored credential through them.
This commit is contained in:
Yichen Jiang
2026-08-05 11:18:06 +08:00
parent a45aa28ca6
commit 590b76a7f0
33 changed files with 180 additions and 110 deletions
-6
View File
@@ -20,8 +20,6 @@ export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000
/** Configuration for one pi-ai provider route; the `providers` dict key IS the route. */
export interface PiAiProviderProfile {
/** Literal provider credential; prefer {@link apiKeyEnv}. With both absent pi-ai uses its provider-native ambient discovery. */
apiKey?: string
/** Credential reference (environment-variable name) resolved per request through `ctx.credentials`. */
apiKeyEnv?: string
/** Override the selected catalog model's endpoint without changing its protocol metadata. */
@@ -76,7 +74,6 @@ const thinkingBudgets = z.object({
})
const profile = z.object({
apiKey: z.string().role('secret'),
apiKeyEnv: z.string().role('credential-ref'),
baseURL: z.string(),
headers: z.dict(z.string()),
@@ -126,9 +123,6 @@ export function resolveProfiles(
}
if (provider.length === 0) throw new Error('llm-pi-ai: provider names must be non-empty')
if (!supported.has(provider)) throw new Error(`llm-pi-ai: unknown pi-ai provider "${provider}"`)
if (source.apiKey !== undefined && source.apiKey.trim().length === 0) {
throw new Error(`llm-pi-ai: provider "${provider}" has an empty apiKey; omit it to use ambient authentication`)
}
if (source.baseURL !== undefined && source.baseURL.length === 0) {
throw new Error(`llm-pi-ai: provider "${provider}" has an empty baseURL`)
}
-1
View File
@@ -89,7 +89,6 @@ export function apply(ctx: Context, config: Config): void {
provider: string,
profile: ResolvedPiAiProviderProfile,
): Promise<string | undefined> => {
if (profile.apiKey !== undefined) return profile.apiKey
const ref = profile.apiKeyEnv
// Only a profile that names no credential at all defers to pi-ai's
// provider-native discovery. Once one is named, a miss must fail loud:
+25 -15
View File
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import LlmService, { createUserMessage, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, ReasoningEffortId, userAgent } from '@deepseek-ai/dsh-llm'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
@@ -15,22 +15,32 @@ afterEach(async () => {
})
async function harness(baseURL: string, overrides: Record<string, unknown> = {}): Promise<Context> {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: { deepseek: { apiKey: 'test-key', baseURL, ...overrides } },
providers: { deepseek: { apiKeyEnv: 'PI_TEST_KEY', baseURL, ...overrides } },
})
return ctx
}
/** Direct adapter over the real profile resolver, with literal-key resolution. */
function adapterOf(providers: Record<string, LlmPiAi.PiAiProviderProfile>): PiAiAdapter {
/** Direct adapter over the real profile resolver, with a fixed key per call. */
function adapterOf(
providers: Record<string, LlmPiAi.PiAiProviderProfile>,
apiKey: string | undefined = 'test-key',
): PiAiAdapter {
return new PiAiAdapter({
profiles: () => resolveProfiles(providers),
resolveApiKey: (_provider, profile) => Promise.resolve(profile.apiKey),
resolveApiKey: () => Promise.resolve(apiKey),
})
}
beforeEach(() => {
// Configuration carries only the reference; these mounts resolve it from
// the environment, which is the whole credential plane without a seam.
vi.stubEnv('PI_TEST_KEY', 'test-key')
})
describe('PiAiAdapter provider routing', () => {
it('resolves a catalog model dynamically and uses a private endpoint', async () => {
const server = await mockServer([{ events: textEvents }])
@@ -117,7 +127,7 @@ describe('PiAiAdapter provider routing', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['deepseek'], adapterOf({
deepseek: { apiKey: 'test-key', baseURL: server.url },
deepseek: { apiKeyEnv: 'PI_TEST_KEY', baseURL: server.url },
}))
const result = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
@@ -146,7 +156,7 @@ describe('PiAiAdapter provider routing', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: { openai: { apiKey: 'test-key', baseURL: `${server.url}/v1` } },
providers: { openai: { apiKeyEnv: 'PI_TEST_KEY', baseURL: `${server.url}/v1` } },
})
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
expect(result.finish.kind).toBe('error')
@@ -166,7 +176,7 @@ describe('PiAiAdapter provider routing', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: { openai: { apiKey: 'test-key', baseURL: `${server.url}/v1` } },
providers: { openai: { apiKeyEnv: 'PI_TEST_KEY', baseURL: `${server.url}/v1` } },
})
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
@@ -182,7 +192,7 @@ describe('PiAiAdapter provider routing', () => {
await ctx.plugin(LlmPiAi, {
providers: {
openai: {
apiKey: 'test-key',
apiKeyEnv: 'PI_TEST_KEY',
baseURL: `${server.url}/api/projects/openai/openai/v1`,
headers: { 'api-key': 'test-key', Authorization: '' },
},
@@ -372,7 +382,9 @@ describe('provider profile lifecycle', () => {
it('accepts absent credentials for pi-ai ambient authentication', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
const server = await mockServer([{ events: textEvents }])
const ctx = await harness(server.url, { apiKey: undefined })
// A profile that names no reference at all is the one case that defers to
// pi-ai's own provider-native discovery.
const ctx = await harness(server.url, { apiKeyEnv: undefined })
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
expect(server.headers[0]?.authorization).toBe('Bearer ambient-key')
})
@@ -410,8 +422,6 @@ describe('provider profile lifecycle', () => {
// loud with migration directions instead of half-working.
expect(() => resolveProfiles([{ provider: 'openai' }] as never)).toThrow(/dict keyed by provider/)
expect(() => resolveProfiles({ openai: { provider: 'openai' } as never })).toThrow(/moved to the providers dict key/)
expect(() => resolveProfiles({ openai: { apiKey: '' } })).toThrow(/empty apiKey/)
expect(() => resolveProfiles({ openai: { apiKey: ' ' } })).toThrow(/empty apiKey/)
expect(() => resolveProfiles({ openai: { baseURL: '' } })).toThrow(/empty baseURL/)
expect(() => resolveProfiles({ openai: { apiKeyEnv: 'not-a-var!' } })).toThrow(/must match/)
})
@@ -486,7 +496,7 @@ describe('abort wiring', () => {
const message = Object.defineProperty({}, 'role', {
get() { throw original },
})
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
const adapter = adapterOf({ deepseek: {} })
const drain = async (): Promise<void> => {
for await (const _chunk of adapter.stream({
provider: 'deepseek',
@@ -507,7 +517,7 @@ describe('abort wiring', () => {
throw original
},
})
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
const adapter = adapterOf({ deepseek: {} })
const drain = async (): Promise<void> => {
for await (const _chunk of adapter.stream({
provider: 'deepseek',
@@ -521,7 +531,7 @@ describe('abort wiring', () => {
})
it('resolves catalog endpoints without an override before honoring pre-abort', async () => {
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
const adapter = adapterOf({ deepseek: {} })
const controller = new AbortController()
controller.abort('already stopped')
const chunks = []
@@ -53,7 +53,11 @@ describe('request-level dynamic profiles', () => {
it('mounts bare and dormant, then registers routes the moment settings supply providers', async () => {
vi.stubEnv('PI_DYNAMIC_KEY', '')
const dir = await home()
await writeFile(join(dir, '.credentials.yaml'), 'PI_DYNAMIC_KEY: pk-from-settings\n', { mode: 0o600 })
await writeFile(
join(dir, '.credentials.yaml'),
'PI_DYNAMIC_KEY: pk-from-settings\nPI_LIVE_KEY: live-key\nPI_OTHER_KEY: other\n',
{ mode: 0o600 },
)
const server = await mockServer([{ events: textEvents }])
// The exact product posture: `- id: llm-pi-ai` with no config at all.
const ctx = await boot(dir, {})
@@ -86,14 +90,19 @@ describe('request-level dynamic profiles', () => {
it('adds a provider route from settings and drops it when the user layer resets', async () => {
const dir = await home()
await writeFile(
join(dir, '.credentials.yaml'),
'PI_LIVE_KEY: live-key\nPI_OTHER_KEY: other\n',
{ mode: 0o600 },
)
const server = await mockServer([{ events: textEvents }])
const ctx = await boot(dir, {
providers: { openai: { apiKey: 'k', baseURL: 'http://127.0.0.1:1/v1' } },
providers: { openai: { apiKeyEnv: 'PI_DYNAMIC_KEY', baseURL: 'http://127.0.0.1:1/v1' } },
})
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['openai'])
await ctx.settings.update(NS, {
providers: { deepseek: { apiKey: 'live-key', baseURL: server.url } },
providers: { deepseek: { apiKeyEnv: 'PI_LIVE_KEY', baseURL: server.url } },
})
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['openai', 'deepseek'])
@@ -158,15 +167,20 @@ describe('request-level dynamic profiles', () => {
it('keeps serving its routes when a settings-born route collides with another adapter', async () => {
const dir = await home()
await writeFile(
join(dir, '.credentials.yaml'),
'PI_LIVE_KEY: live-key\nPI_OTHER_KEY: other\n',
{ mode: 0o600 },
)
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
const ctx = await boot(dir, { providers: { openai: { apiKey: 'pk', baseURL: `${server.url}/v1` } } })
const ctx = await boot(dir, { providers: { openai: { apiKeyEnv: 'PI_LIVE_KEY', baseURL: `${server.url}/v1` } } })
// Another adapter owns `anthropic`; the registry must refuse to hand it over.
ctx.llm.registerAdapter(['anthropic'], new StubAdapter())
await ctx.settings.update(NS, {
providers: {
openai: { apiKey: 'pk', baseURL: `${server.url}/v1` },
anthropic: { apiKey: 'other' },
openai: { apiKeyEnv: 'PI_LIVE_KEY', baseURL: `${server.url}/v1` },
anthropic: { apiKeyEnv: 'PI_OTHER_KEY' },
},
})
@@ -23,7 +23,7 @@ describe('pi-ai SDK retry boundary', () => {
},
})
const adapter = new PiAiAdapter({
profiles: () => resolveProfiles({ openai: { apiKey: 'test-key' } }),
profiles: () => resolveProfiles({ openai: {} }),
resolveApiKey: () => Promise.resolve('test-key'),
})
const drain = async (): Promise<void> => {