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.
117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
/**
|
|
* Real-composition guard for the dormant pi-ai posture: LlmService,
|
|
* settings-local, credentials-local, and a bare `llm-pi-ai` row boot from a
|
|
* test-only cordis.yml through the actual Loader + Include path, an external
|
|
* edit of settings.yaml registers the route live, and the next request
|
|
* carries the credential the credentials document supplies. A hand-mounted `ctx.plugin` cannot
|
|
* catch Loader export-shape failures, which is why the twin adapter has the
|
|
* same guard.
|
|
*/
|
|
|
|
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 CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
|
|
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
|
|
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
|
|
import { assemble } from './assemble.ts'
|
|
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
|
|
|
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()
|
|
})
|
|
|
|
/** Boot the dormant composition: a bare `llm-pi-ai` row with no config at all. */
|
|
async function loadComposition(): Promise<{ ctx: Context; settingsPath: string }> {
|
|
root = await mkdtemp(join(tmpdir(), 'dsh-pi-composition-'))
|
|
const settingsPath = join(root, 'settings.yaml')
|
|
await writeFile(settingsPath, '# personal settings\n')
|
|
await writeFile(join(root, '.credentials.yaml'), 'PI_COMPOSITION_KEY: key-from-store\n', { mode: 0o600 })
|
|
|
|
const configPath = join(root, 'cordis.yml')
|
|
await writeFile(configPath, [
|
|
'- id: llm',
|
|
" name: 'test-llm-service'",
|
|
'- 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(join(root, '.credentials.yaml'))}`,
|
|
' debounceMs: 10',
|
|
'- id: llm-pi-ai',
|
|
" name: '@deepseek-ai/dsh-llm-pi-ai'",
|
|
'',
|
|
].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-pi-ai', LlmPiAi],
|
|
])
|
|
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 }
|
|
}
|
|
|
|
describe('llm-pi-ai real dormant composition', () => {
|
|
it('boots with zero routes and registers one the moment settings supply a profile', async () => {
|
|
vi.stubEnv('PI_COMPOSITION_KEY', '')
|
|
const server = await mockServer([{ events: textEvents }])
|
|
const { ctx, settingsPath } = await loadComposition()
|
|
|
|
// The shipped posture: the adapter exists, no route does.
|
|
expect(ctx.llm.listProviders()).toEqual([])
|
|
|
|
// Exactly what the web Models page leaves on disk.
|
|
await writeFile(settingsPath, [
|
|
'llm-pi-ai:',
|
|
' providers:',
|
|
' deepseek:',
|
|
' apiKeyEnv: PI_COMPOSITION_KEY',
|
|
` baseURL: ${server.url}`,
|
|
'',
|
|
].join('\n'))
|
|
await vi.waitFor(() => {
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['deepseek'])
|
|
}, { timeout: 5000 })
|
|
|
|
const result = await assemble(ctx, { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] })
|
|
expect(result.message.content).toEqual([{ type: 'text', text: 'hello' }])
|
|
expect(server.headers[0]?.authorization).toBe('Bearer key-from-store')
|
|
})
|
|
})
|