mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
$DSH_HOME/.env had just become an ordinary environment layer, which left the harness resolving user-facing values from a flattened process.env that could no longer say where a value came from. A key stored through the web page stayed shadowed by an older key in the user's own .env. An endpoint could be redirected by the project: the invoking directory's .env is materialized like every other layer, and a base URL decides where a resolved API key is sent, so a DEEPSEEK_BASE_URL written into a model-editable workspace would send the user's credential — and the prompts carrying their code — to whatever host that file named. Give every user-facing value one ordering, with four kinds of source: explicit for this run per-operation override, CLI argument > authored by deployment --config / --config-replace > this launch's shell inherited process environment > product-managed store settings.yaml, .credentials.yaml > discovered file $DSH_HOME/.env > defaults schema default, shipped base, public default The domains differ only in which tiers exist. The earlier split — credentials ranking the environment over the managed file while settings ranked over the environment — was inconsistent: the distinguishing fact is who authored the source, not the domain. packages/util/environment owns an immutable snapshot with per-layer provenance. getFrom(name, sources) searches only the layers a caller names, and omitting one is a refusal rather than a demotion: the adapters ask for ['process', 'user-env'], so no reordering can let a project file back into a decision it was excluded from. isBootstrapOnly rejects, before anything is materialized, any .env setting a variable that governs how a process launches (PATH, SHELL, NODE_OPTIONS, LD_PRELOAD), where code or model-visible instructions load from (the whole DSH_* namespace, HOME, XDG_*), or how the network is reached (proxy and CA variables). The namespace is denied wholesale so a switch added later cannot become settable by being forgotten, and there is no opt-out. verify-config-source-ownership keeps both rules: no unregistered process.env read under packages/*/*/src (26 allowlisted with reasons), and no apiKey, baseURL, or headers inlined from the environment in shipped Cordis config — removing those inlines is what makes the deployment tier meaningful.
119 lines
5.1 KiB
TypeScript
119 lines
5.1 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import {
|
|
createEnvironmentSnapshot, DSH_ENVIRONMENT_KEY, ENVIRONMENT_SOURCES, environmentOf, isBootstrapOnly,
|
|
} from '../src/index.ts'
|
|
|
|
const layered = createEnvironmentSnapshot([
|
|
{ source: 'process', values: { SHARED: 'from-process', ONLY_PROCESS: 'p' } },
|
|
{ source: 'project-env', path: '/work/.env', values: { SHARED: 'from-project', ONLY_PROJECT: 'j' } },
|
|
{ source: 'user-env', path: '/home/.dsh/.env', values: { SHARED: 'from-user', ONLY_USER: 'u' } },
|
|
])
|
|
|
|
describe('createEnvironmentSnapshot', () => {
|
|
it('resolves across every layer, most trusted first, and reports the winning source', () => {
|
|
expect(layered.get('SHARED')).toEqual({ value: 'from-process', source: 'process' })
|
|
expect(layered.get('ONLY_PROJECT')).toEqual({ value: 'j', source: 'project-env', path: '/work/.env' })
|
|
expect(layered.get('ONLY_USER')).toEqual({ value: 'u', source: 'user-env', path: '/home/.dsh/.env' })
|
|
expect(layered.get('ABSENT')).toBeUndefined()
|
|
})
|
|
|
|
it('treats an omitted layer as invisible, not merely lower', () => {
|
|
// 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', [])).toBeUndefined()
|
|
})
|
|
|
|
it('lists its layers in trust order with their paths', () => {
|
|
expect(layered.layers).toEqual([
|
|
{ source: 'process' },
|
|
{ source: 'project-env', path: '/work/.env' },
|
|
{ source: 'user-env', path: '/home/.dsh/.env' },
|
|
])
|
|
expect(createEnvironmentSnapshot([{ source: 'process', values: {} }]).layers).toEqual([{ source: 'process' }])
|
|
})
|
|
|
|
it('copies each layer, so a later mutation of the source object cannot change it', () => {
|
|
const values: Record<string, string> = { KEY: 'first' }
|
|
const snapshot = createEnvironmentSnapshot([{ source: 'process', values }])
|
|
values.KEY = 'second'
|
|
values.LATE = 'added'
|
|
expect(snapshot.get('KEY')).toEqual({ value: 'first', source: 'process' })
|
|
expect(snapshot.get('LATE')).toBeUndefined()
|
|
})
|
|
|
|
it('keeps an empty value as a present value, for its owner to judge', () => {
|
|
const snapshot = createEnvironmentSnapshot([{ source: 'process', values: { EMPTY: '' } }])
|
|
expect(snapshot.get('EMPTY')).toEqual({ value: '', source: 'process' })
|
|
})
|
|
|
|
it('orders lookups by ENVIRONMENT_SOURCES 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' })
|
|
})
|
|
})
|
|
|
|
describe('environmentOf', () => {
|
|
it('returns the launcher snapshot when the product CLI provided one', () => {
|
|
const ctx = new Context()
|
|
ctx.provide(DSH_ENVIRONMENT_KEY, layered)
|
|
expect(environmentOf(ctx)).toBe(layered)
|
|
})
|
|
|
|
it('falls back to the inherited environment as the only layer', () => {
|
|
vi.stubEnv('DSH_ENV_SPEC_FALLBACK', 'ambient')
|
|
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')
|
|
expect(snapshot.layers).toEqual([{ source: 'process' }])
|
|
} finally {
|
|
vi.unstubAllEnvs()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('isBootstrapOnly', () => {
|
|
it.each([
|
|
'PATH', 'HOME', 'USERPROFILE', 'SHELL',
|
|
'NODE_OPTIONS', 'NODE_PATH', 'NODE_EXTRA_CA_CERTS',
|
|
'LD_PRELOAD', 'LD_LIBRARY_PATH',
|
|
'SSL_CERT_FILE', 'SSL_CERT_DIR',
|
|
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
])('rejects %s, which decides how the process starts or reaches the network', (name) => {
|
|
expect(isBootstrapOnly(name)).toBe(true)
|
|
})
|
|
|
|
it.each([
|
|
['DSH_HOME', 'the harness home'],
|
|
['DSH_PERMISSION_MODE', 'the permission mode'],
|
|
['DSH_AGENTS_HOME', 'a model-visible instruction root'],
|
|
['DSH_ANYTHING_ADDED_LATER', 'a switch that does not exist yet'],
|
|
['XDG_CONFIG_HOME', 'a state root'],
|
|
['DYLD_INSERT_LIBRARIES', 'a library preload'],
|
|
])('rejects the whole namespace: %s (%s)', (name) => {
|
|
expect(isBootstrapOnly(name)).toBe(true)
|
|
})
|
|
|
|
it('matches case-insensitively, so a lowercase proxy name is not a bypass', () => {
|
|
expect(isBootstrapOnly('https_proxy')).toBe(true)
|
|
expect(isBootstrapOnly('dsh_permission_mode')).toBe(true)
|
|
})
|
|
|
|
it('allows ordinary variables, including provider credentials and endpoints', () => {
|
|
for (const name of ['DEEPSEEK_API_KEY', 'DEEPSEEK_BASE_URL', 'EXA_API_KEY', 'MY_PROJECT_FLAG', 'PATHS']) {
|
|
expect(isBootstrapOnly(name)).toBe(false)
|
|
}
|
|
})
|
|
})
|