feat(credentials): move the store to .credentials.yaml and layer $DSH_HOME/.env
$DSH_HOME/.env carried two incompatible jobs. As credentials-local's writable secret store it could not be hoisted into process.env — hoisting makes every stored key read as a read-only launch override and blocks rotation from the TUI and the web page. But its name and dotenv format promise an environment file, so a DEEPSEEK_BASE_URL sitting beside a working DEEPSEEK_API_KEY in the same file was silently ignored: only the credential provider read the document, and it addresses credential references alone. Split the two jobs into two files. .credentials.yaml is the provider-managed store: a strict YAML mapping of CredentialRef to non-empty string, no version field, no wrapper level. Because it holds credentials and nothing else, a non-mapping root, a non-identifier key, a non-string value, an empty string, a duplicate key, and malformed YAML are all rejections rather than skipped entries — loud at boot and at a write, warn-and-keep-last-good on a live reload. The dotenv physical-line editor gives way to a patch of the parsed document, so comments and untouched entries keep their formatting and any string value round-trips, multi-line included. Writer lock, read-modify-write, atomic 0600 write under a 0700 directory, watcher, self-write suppression, and quiescent disposal are unchanged. $DSH_HOME/.env becomes the user's ordinary environment layer. app-boot's new loadLayeredEnv loads the invoking directory's .env then the Harness home's, giving user < project < inherited; the home resolves from the inherited environment first, so a project .env cannot redirect it. Credential precedence is unchanged: the live environment still wins read-only over the file, and shadowed writes still reject. Whether a provider-managed store should instead win over the environment is a separate decision. No migration: a key already in $DSH_HOME/.env keeps resolving through the new environment layer, as a read-only env source that shadows the stored one.
This commit is contained in:
@@ -81,7 +81,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
await dialog.getByLabel('API 密钥').fill('sk-e2e-minimax')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
// The profile lands in settings.yaml with only the derived reference, the
|
||||
// key value lands in the harness home's .env, the dormant route
|
||||
// key value lands in the harness home's .credentials.yaml, the dormant route
|
||||
// registers, and the topology frame invalidates the page into the row.
|
||||
const row = dialog.getByText('minimax-cn', { exact: true }).first()
|
||||
await row.waitFor({ timeout: 10_000 })
|
||||
@@ -89,8 +89,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
expect(document).toContain('minimax-cn:')
|
||||
expect(document).toContain('apiKeyEnv: MINIMAX_CN_API_KEY')
|
||||
expect(document).not.toContain('sk-e2e-minimax')
|
||||
const stored = await readFile(join(scaffold.harnessHome, '.env'), 'utf8')
|
||||
expect(stored).toContain('MINIMAX_CN_API_KEY=sk-e2e-minimax')
|
||||
const stored = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8')
|
||||
expect(stored).toContain('MINIMAX_CN_API_KEY: sk-e2e-minimax')
|
||||
expect(await page.content()).not.toContain('sk-e2e-minimax')
|
||||
}, 60_000)
|
||||
|
||||
@@ -136,8 +136,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'),
|
||||
{ timeout: 10_000 },
|
||||
).not.toContain('minimax-cn:')
|
||||
expect(await readFile(join(scaffold.harnessHome, '.env'), 'utf8'))
|
||||
.toContain('MINIMAX_CN_API_KEY=sk-e2e-minimax')
|
||||
expect(await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8'))
|
||||
.toContain('MINIMAX_CN_API_KEY: sk-e2e-minimax')
|
||||
await expect.poll(
|
||||
async () => page.getByRole('dialog', { name: '删除模型提供方?' }).count(),
|
||||
{ timeout: 10_000 },
|
||||
|
||||
Reference in New Issue
Block a user