mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Review found five real defects in the configuration-source work, all confirmed against the code rather than argued: 1. The note claimed --config outranks settings.yaml. It does not: the settings seam registers a plugin's cordis entry config as the `base` layer and the user section layers over it, and the seam cannot tell a shipped value from a --config one. The note now states shipped reality and names --config-replace as the lever for a deployment that must win. Separately, a literal `apiKey` in settings outranked both the environment and .credentials.yaml — the field is removed, so configuration carries a reference and nothing else. 2. DEEPSEEK_SEARCH_BASE_URL was functionally deleted: the shipped inline went away without the provider learning to read it. It now resolves from the environment snapshot, as the README always claimed. 3. The bootstrap deny list missed the interpreter start-up hooks. BASH_ENV is the sharpest: `bash -c` sources it on every bash tool call, so a project .env could run a file of its choosing before every command. The list now covers BASH_ENV and its per-language siblings, the Git hook commands, and the remaining preload and CA variables, organised by what a variable does rather than which runtime owns it. 4. YAML parse errors quoted the offending source line — which in a credentials document is the secret — into boot stderr and the watcher's logger. Only the error code and position are reported now, in credentials-local and settings-local alike, pinned by a test that asserts the secret is absent. 5. 0600 governed only files the harness wrote. A hand-created 0644 document was read normally. POSIX now checks the mode before reading contents, at boot and on every reload; Windows has no mode to inspect and is skipped rather than faked. The project a session is launched in is trusted by default, with no prompt and no stored trust record: it may supply its own endpoint, ordinary variables, and a key ranked below the managed store. Trust stops at the harness itself — a discovered file still cannot set DSH_PERMISSION_MODE, PATH, BASH_ENV, or the rest, because those take effect with no user action, before any turn, outside the permission policy and the sandbox.
176 lines
7.9 KiB
TypeScript
176 lines
7.9 KiB
TypeScript
/**
|
|
* Real-composition guard for the dynamic-configuration chain: LlmService,
|
|
* settings-local, credentials-local, and llm-deepseek boot from a test-only
|
|
* cordis.yml through the actual Loader + Include path, external edits of
|
|
* settings.yaml and the credentials document hot-publish through their providers, and the very
|
|
* next request carries the fresh base URL and credential. The same adapter
|
|
* composition without settings or credentials entries keeps entry-config
|
|
* behavior — the documented optional-inject fallback.
|
|
*/
|
|
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import Loader from '@cordisjs/plugin-loader'
|
|
import Include from '@cordisjs/plugin-include'
|
|
import LlmService from '@deepseek-ai/dsh-llm'
|
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
import CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
|
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
|
|
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
|
import { assemble } from './assemble.ts'
|
|
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
|
|
|
const NS = settingsNamespace('llm-deepseek')
|
|
const KEY_REF = credentialRef('DEEPSEEK_API_KEY')
|
|
|
|
let root: string | undefined
|
|
let context: Context | undefined
|
|
|
|
afterEach(async () => {
|
|
await context?.fiber.dispose()
|
|
context = undefined
|
|
if (root !== undefined) await rm(root, { recursive: true, force: true })
|
|
root = undefined
|
|
await closeMockServers()
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
async function loadComposition(
|
|
options: { withDynamic: boolean; baseURL: string; reuseRoot?: string },
|
|
): Promise<{ ctx: Context; settingsPath: string; credentialsPath: string }> {
|
|
// A reused root is the restart case: the same harness home, its documents
|
|
// exactly as the previous process left them.
|
|
const fresh = options.reuseRoot === undefined
|
|
root = options.reuseRoot ?? await mkdtemp(join(tmpdir(), 'dsh-llm-composition-'))
|
|
const settingsPath = join(root, 'settings.yaml')
|
|
const credentialsPath = join(root, '.credentials.yaml')
|
|
if (options.withDynamic && fresh) {
|
|
await writeFile(settingsPath, '# personal settings\n')
|
|
await writeFile(credentialsPath, 'DEEPSEEK_API_KEY: boot-key\n', { mode: 0o600 })
|
|
}
|
|
|
|
const configPath = join(root, 'cordis.yml')
|
|
await writeFile(configPath, [
|
|
'- id: llm',
|
|
" name: 'test-llm-service'",
|
|
...options.withDynamic
|
|
? [
|
|
'- id: settings',
|
|
" name: '@deepseek-ai/dsh-settings-local'",
|
|
' config:',
|
|
` path: ${JSON.stringify(settingsPath)}`,
|
|
' debounceMs: 10',
|
|
'- id: credentials',
|
|
" name: '@deepseek-ai/dsh-credentials-local'",
|
|
' config:',
|
|
` path: ${JSON.stringify(credentialsPath)}`,
|
|
' debounceMs: 10',
|
|
]
|
|
: [],
|
|
'- id: llm-deepseek',
|
|
" name: '@deepseek-ai/dsh-llm-deepseek'",
|
|
' config:',
|
|
` baseURL: ${JSON.stringify(options.baseURL)}`,
|
|
'',
|
|
].join('\n'))
|
|
|
|
const ctx = new Context()
|
|
context = ctx
|
|
ctx.baseUrl = pathToFileURL(root).href + '/'
|
|
await ctx.plugin(Loader)
|
|
ctx.loader.builtins.include = Include
|
|
const modules = new Map<string, unknown>([
|
|
['test-llm-service', LlmService],
|
|
['@deepseek-ai/dsh-settings-local', SettingsLocal],
|
|
['@deepseek-ai/dsh-credentials-local', CredentialsLocal],
|
|
['@deepseek-ai/dsh-llm-deepseek', LlmDeepSeek],
|
|
])
|
|
ctx.loader.internal = {
|
|
version: 'v2',
|
|
async import(specifier: string) {
|
|
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
|
|
return modules.get(specifier)
|
|
},
|
|
} as unknown as NonNullable<typeof ctx.loader.internal>
|
|
await ctx.loader.create({
|
|
name: 'cordis:include',
|
|
config: { path: pathToFileURL(configPath).href },
|
|
})
|
|
await ctx.loader.await()
|
|
return { ctx, settingsPath, credentialsPath }
|
|
}
|
|
|
|
describe('llm-deepseek real dynamic composition', () => {
|
|
it('boots from cordis.yml and routes the next request after external settings and credential edits', async () => {
|
|
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
|
const serverA = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const serverB = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const { ctx, settingsPath, credentialsPath } = await loadComposition({ withDynamic: true, baseURL: serverA.url })
|
|
|
|
expect(ctx.get('settings')!.describe().map(entry => entry.ns)).toEqual([NS])
|
|
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
|
expect(serverA.headers[0]?.authorization).toBe('Bearer boot-key')
|
|
|
|
// External edits, exactly as a user or the web UI would leave them on disk.
|
|
await writeFile(settingsPath, `llm-deepseek:\n baseURL: ${serverB.url}\n`)
|
|
await vi.waitFor(() => {
|
|
expect((ctx.get('settings')!.get(NS) as { baseURL?: string }).baseURL).toBe(serverB.url)
|
|
}, { timeout: 5000 })
|
|
await writeFile(credentialsPath, 'DEEPSEEK_API_KEY: rotated-key\n', { mode: 0o600 })
|
|
await vi.waitFor(async () => {
|
|
expect(await ctx.get('credentials')!.resolve(KEY_REF)).toEqual({ value: 'rotated-key', source: 'file' })
|
|
}, { timeout: 5000 })
|
|
|
|
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
|
expect(serverA.requests).toHaveLength(1)
|
|
expect(serverB.headers[0]?.authorization).toBe('Bearer rotated-key')
|
|
})
|
|
|
|
it('keeps a stored key writable and rotatable across a real restart', async () => {
|
|
// No ambient DEEPSEEK_API_KEY: the shipped surfaces no longer hoist
|
|
// the credentials document into process.env, so a stored key must stay file-sourced.
|
|
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
|
const first = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const second = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const boot = await loadComposition({ withDynamic: true, baseURL: first.url })
|
|
const home = root!
|
|
await boot.ctx.get('credentials')!.set(KEY_REF, 'stored-by-ui')
|
|
expect(await boot.ctx.get('credentials')!.describe(KEY_REF))
|
|
.toEqual({ configured: true, source: 'file', writable: true })
|
|
await assemble(boot.ctx, { model: 'deepseek-v4-flash', messages: [] })
|
|
expect(first.headers[0]?.authorization).toBe('Bearer stored-by-ui')
|
|
await boot.ctx.fiber.dispose()
|
|
context = undefined
|
|
|
|
// Restart over the same harness home.
|
|
const restarted = await loadComposition({ withDynamic: true, baseURL: second.url, reuseRoot: home })
|
|
const credentials = restarted.ctx.get('credentials')!
|
|
// The stored key is still the provider's own writable file entry — not a
|
|
// read-only launch override, which is what hoisting it would have made it.
|
|
expect(await credentials.resolve(KEY_REF)).toEqual({ value: 'stored-by-ui', source: 'file' })
|
|
expect(await credentials.describe(KEY_REF)).toEqual({ configured: true, source: 'file', writable: true })
|
|
// Rotation still works after the restart, and the next request uses it.
|
|
await credentials.set(KEY_REF, 'rotated-after-restart')
|
|
await assemble(restarted.ctx, { model: 'deepseek-v4-flash', messages: [] })
|
|
expect(second.headers[0]?.authorization).toBe('Bearer rotated-after-restart')
|
|
})
|
|
|
|
it('boots the same adapter on entry config alone, resolving the reference from the environment', async () => {
|
|
// No settings and no credentials provider: configuration carries only the
|
|
// reference, so the environment is the whole credential plane here.
|
|
vi.stubEnv('DEEPSEEK_API_KEY', 'entry-key')
|
|
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const { ctx } = await loadComposition({ withDynamic: false, baseURL: server.url })
|
|
|
|
expect(ctx.get('settings')).toBeUndefined()
|
|
expect(ctx.get('credentials')).toBeUndefined()
|
|
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
|
expect(server.headers[0]?.authorization).toBe('Bearer entry-key')
|
|
})
|
|
})
|