mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
cleanup(environment): keep one lookup order
This commit is contained in:
@@ -182,7 +182,7 @@ export function resolveAdapterOptions(config: Config, environment?: EnvironmentS
|
||||
return {
|
||||
apiKeyEnv: credentialRef(config.apiKeyEnv ?? DEFAULT_API_KEY_ENV),
|
||||
baseURL: config.baseURL
|
||||
?? environment?.getFrom(BASE_URL_ENV, ['process', 'project-env', 'user-env'])?.value
|
||||
?? environment?.get(BASE_URL_ENV)?.value
|
||||
?? PUBLIC_BASE_URL,
|
||||
defaults: {
|
||||
thinking: config.thinking,
|
||||
@@ -232,7 +232,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
} else {
|
||||
// Without the seam there is no managed store to rank against, so the
|
||||
// environment is the whole credential plane.
|
||||
const ambient = environmentOf(ctx).getFrom(ref, ['process', 'project-env', 'user-env'])
|
||||
const ambient = environmentOf(ctx).get(ref)
|
||||
if (ambient !== undefined && ambient.value.length > 0) {
|
||||
return assertUsableApiKey(ambient.value, 'llm-deepseek', ref)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const hit = credentials !== undefined
|
||||
? (await credentials.resolve(ref))?.value
|
||||
// Without the seam the environment is the whole credential plane.
|
||||
: environmentOf(ctx).getFrom(ref, ['process', 'project-env', 'user-env'])?.value
|
||||
: environmentOf(ctx).get(ref)?.value
|
||||
if (hit !== undefined && hit.length > 0) return assertUsableApiKey(hit, 'llm-pi-ai', ref)
|
||||
throw new LlmError(
|
||||
`llm-pi-ai: no credential for provider route "${provider}"; its profile resolves ${ref}, which is not`
|
||||
|
||||
@@ -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/util/environment/README.md
|
||||
README.md: 1bb444bc217ce1a01fb98f954d6e1c2bbc3db957
|
||||
README.zh.md: a46adf0beeb0fb2069e198c99e4c00c2e8c09c6c
|
||||
README.md: 1df857851f0f0a5a5ac52365c5e563a1c001bfca
|
||||
README.zh.md: 41ea1af3a6eb95d58456f43e0f7f0a90e4fe7ac6
|
||||
|
||||
@@ -14,7 +14,7 @@ Values do also reach `process.env` — a user's `--config` tree and third-party
|
||||
|
||||
## Resolving
|
||||
|
||||
`get(name)` searches every layer, most trusted first. `getFrom(name, sources)` searches only the layers the caller trusts.
|
||||
`get(name)` searches every layer, most trusted first. `getFrom(name, sources)` searches only the named layers without changing that trust order.
|
||||
|
||||
**Omitting a layer is a refusal, not a demotion** — a caller that must never accept a layer leaves it out of the list, so no future reordering can let it back in. The provider adapters name all three, because the product trusts the project it runs in; the mechanism exists for the decisions where that is not true.
|
||||
|
||||
@@ -25,7 +25,7 @@ import type { Context } from 'cordis'
|
||||
import { environmentOf } from '@deepseek-ai/dsh-environment'
|
||||
|
||||
declare const ctx: Context
|
||||
const endpoint = environmentOf(ctx).getFrom('DEEPSEEK_BASE_URL', ['process', 'project-env', 'user-env'])?.value
|
||||
const endpoint = environmentOf(ctx).get('DEEPSEEK_BASE_URL')?.value
|
||||
```
|
||||
|
||||
`environmentOf(ctx)` returns the launcher's snapshot when the product CLI booted the tree, and otherwise the inherited environment as the only layer. That fallback does not weaken the rules: an SDK host or a bare `cordis.yml` discovered no files, so everything it has really is the environment it was launched with.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
## 解析
|
||||
|
||||
`get(name)` 按可信度从高到低搜索所有层。`getFrom(name, sources)` 只搜索调用方信任的层。
|
||||
`get(name)` 按可信度从高到低搜索所有层。`getFrom(name, sources)` 只搜索指定的层,不改变这一可信顺序。
|
||||
|
||||
**省略某一层是拒绝,不是降级**——绝不能接受某一层的调用方直接不把它列进去,后续任何重新排序都无法让它回来。provider 适配器三层全列,因为产品信任它所运行的项目;该机制是为那些「并非如此」的决策准备的。
|
||||
|
||||
@@ -25,7 +25,7 @@ import type { Context } from 'cordis'
|
||||
import { environmentOf } from '@deepseek-ai/dsh-environment'
|
||||
|
||||
declare const ctx: Context
|
||||
const endpoint = environmentOf(ctx).getFrom('DEEPSEEK_BASE_URL', ['process', 'project-env', 'user-env'])?.value
|
||||
const endpoint = environmentOf(ctx).get('DEEPSEEK_BASE_URL')?.value
|
||||
```
|
||||
|
||||
当产品 CLI(命令行界面)启动了这棵树时,`environmentOf(ctx)` 返回启动器的快照;否则返回只含继承环境的那一层。该回退并不削弱规则:SDK 宿主或裸 `cordis.yml` 从未发现过任何文件,因此它拥有的一切确实就是它被启动时的环境。
|
||||
|
||||
@@ -22,8 +22,8 @@ import type { Context } from 'cordis'
|
||||
*/
|
||||
export type EnvironmentSource = 'process' | 'project-env' | 'user-env'
|
||||
|
||||
/** Layer order, most trusted first — the default search order of {@link EnvironmentSnapshot.get}. */
|
||||
export const ENVIRONMENT_SOURCES: readonly EnvironmentSource[] = ['process', 'project-env', 'user-env']
|
||||
/** Layer order, most trusted first. */
|
||||
const SOURCE_ORDER: readonly EnvironmentSource[] = ['process', 'project-env', 'user-env']
|
||||
|
||||
/** One resolved variable and the layer it came from. */
|
||||
export interface EnvironmentEntry {
|
||||
@@ -54,7 +54,7 @@ export interface EnvironmentSnapshot {
|
||||
* that must never come from a project directory omits `project-env` so no
|
||||
* ordering change can let it back in.
|
||||
* @param name - the variable name.
|
||||
* @param sources - the layers to search, in the caller's own priority order.
|
||||
* @param sources - the layers allowed in the canonical trust order.
|
||||
* @returns the first matching entry, or `undefined`.
|
||||
*/
|
||||
getFrom(name: string, sources: readonly EnvironmentSource[]): EnvironmentEntry | undefined
|
||||
@@ -81,7 +81,7 @@ export interface EnvironmentLayerInput {
|
||||
|
||||
/**
|
||||
* Build the snapshot from each layer's contents.
|
||||
* @param layers - the layers in any order; the result searches them by {@link ENVIRONMENT_SOURCES}.
|
||||
* @param layers - the layers in any order; the result searches them by canonical trust order.
|
||||
* @returns the immutable snapshot.
|
||||
*/
|
||||
export function createEnvironmentSnapshot(layers: readonly EnvironmentLayerInput[]): EnvironmentSnapshot {
|
||||
@@ -101,7 +101,8 @@ export function createEnvironmentSnapshot(layers: readonly EnvironmentLayerInput
|
||||
}
|
||||
const getFrom = (name: string, sources: readonly EnvironmentSource[]): EnvironmentEntry | undefined => {
|
||||
const key = lookupKey(name)
|
||||
for (const source of sources) {
|
||||
for (const source of SOURCE_ORDER) {
|
||||
if (!sources.includes(source)) continue
|
||||
const layer = bySource.get(source)
|
||||
const value = layer?.values.get(key)
|
||||
if (value === undefined) continue
|
||||
@@ -110,7 +111,7 @@ export function createEnvironmentSnapshot(layers: readonly EnvironmentLayerInput
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
get: name => getFrom(name, ENVIRONMENT_SOURCES),
|
||||
get: name => getFrom(name, SOURCE_ORDER),
|
||||
getFrom,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import {
|
||||
createEnvironmentSnapshot, DSH_ENVIRONMENT_KEY, ENVIRONMENT_SOURCES, environmentOf, isBootstrapOnly,
|
||||
createEnvironmentSnapshot, DSH_ENVIRONMENT_KEY, environmentOf, isBootstrapOnly,
|
||||
} from '../src/index.ts'
|
||||
|
||||
const layered = createEnvironmentSnapshot([
|
||||
@@ -18,13 +18,12 @@ describe('createEnvironmentSnapshot', () => {
|
||||
expect(layered.get('ABSENT')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('treats an omitted layer as invisible, not merely lower', () => {
|
||||
it('filters layers without changing their trust order', () => {
|
||||
// The point of getFrom: a routing field that must never come from a
|
||||
// project directory cannot be reached by reordering, only by listing it.
|
||||
expect(layered.getFrom('ONLY_PROJECT', ['process', 'user-env'])).toBeUndefined()
|
||||
expect(layered.getFrom('SHARED', ['user-env', 'process'])).toEqual({
|
||||
value: 'from-user', source: 'user-env', path: '/home/.dsh/.env',
|
||||
})
|
||||
expect(layered.getFrom('SHARED', ['user-env', 'process']))
|
||||
.toEqual({ value: 'from-process', source: 'process' })
|
||||
expect(layered.getFrom('SHARED', [])).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -42,12 +41,11 @@ describe('createEnvironmentSnapshot', () => {
|
||||
expect(snapshot.get('EMPTY')).toEqual({ value: '', source: 'process' })
|
||||
})
|
||||
|
||||
it('orders lookups by ENVIRONMENT_SOURCES regardless of construction order', () => {
|
||||
it('orders lookups canonically regardless of construction order', () => {
|
||||
const reversed = createEnvironmentSnapshot([
|
||||
{ source: 'user-env', path: '/u', values: { K: 'u' } },
|
||||
{ source: 'process', values: { K: 'p' } },
|
||||
])
|
||||
expect(ENVIRONMENT_SOURCES).toEqual(['process', 'project-env', 'user-env'])
|
||||
expect(reversed.get('K')).toEqual({ value: 'p', source: 'process' })
|
||||
})
|
||||
})
|
||||
@@ -64,9 +62,6 @@ describe('environmentOf', () => {
|
||||
try {
|
||||
const snapshot = environmentOf(new Context())
|
||||
expect(snapshot.get('DSH_ENV_SPEC_FALLBACK')).toEqual({ value: 'ambient', source: 'process' })
|
||||
// A host that discovered no files has exactly one layer, so the trusted
|
||||
// lookups every consumer makes still find what it was launched with.
|
||||
expect(snapshot.getFrom('DSH_ENV_SPEC_FALLBACK', ['process', 'user-env'])?.value).toBe('ambient')
|
||||
} finally {
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
|
||||
@@ -90,12 +90,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const credentials = ctx.get('credentials')
|
||||
if (credentials !== undefined) return (await credentials.resolve(apiKeyEnv))?.value
|
||||
// Without the seam the environment is the whole credential plane.
|
||||
const ambient = environmentOf(ctx).getFrom(apiKeyEnv, ['process', 'project-env', 'user-env'])
|
||||
const ambient = environmentOf(ctx).get(apiKeyEnv)
|
||||
return ambient !== undefined && ambient.value.length > 0 ? ambient.value : undefined
|
||||
},
|
||||
apiKeyEnv,
|
||||
baseURL: config.baseURL
|
||||
?? environmentOf(ctx).getFrom(SEARCH_BASE_URL_ENV, ['process', 'project-env', 'user-env'])?.value
|
||||
?? environmentOf(ctx).get(SEARCH_BASE_URL_ENV)?.value
|
||||
?? DEEPSEEK_DEFAULT_BASE_URL,
|
||||
model: config.model ?? DEEPSEEK_DEFAULT_MODEL,
|
||||
apiVersion: config.apiVersion ?? DEEPSEEK_DEFAULT_API_VERSION,
|
||||
|
||||
@@ -61,7 +61,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.web.registerSearchProvider(new ExaSearchProvider({
|
||||
// Every environment layer may name this key: the product trusts the
|
||||
// project it is launched in, and the managed store is not involved here.
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).getFrom('EXA_API_KEY', ['process', 'project-env', 'user-env'])?.value ?? '',
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).get('EXA_API_KEY')?.value ?? '',
|
||||
baseURL: config.baseURL ?? EXA_DEFAULT_BASE_URL,
|
||||
searchType: config.searchType ?? EXA_DEFAULT_SEARCH_TYPE,
|
||||
highlightsPerResult: config.highlightsPerResult ?? EXA_DEFAULT_HIGHLIGHTS_PER_RESULT,
|
||||
|
||||
@@ -55,7 +55,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.web.registerSearchProvider(new PerplexitySearchProvider({
|
||||
// Every environment layer may name this key: the product trusts the
|
||||
// project it is launched in, and the managed store is not involved here.
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).getFrom('PERPLEXITY_API_KEY', ['process', 'project-env', 'user-env'])?.value ?? '',
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).get('PERPLEXITY_API_KEY')?.value ?? '',
|
||||
baseURL: config.baseURL ?? PERPLEXITY_DEFAULT_BASE_URL,
|
||||
model: config.model ?? PERPLEXITY_DEFAULT_MODEL,
|
||||
maxTokens: config.maxTokens ?? PERPLEXITY_DEFAULT_MAX_TOKENS,
|
||||
|
||||
Reference in New Issue
Block a user