From 53fa0e37289dc72437805fb827edfbf00e9f8885 Mon Sep 17 00:00:00 2001 From: Brad Thornton Date: Thu, 3 Sep 2026 13:31:37 -0700 Subject: [PATCH 1/5] fix(daemon): fail closed when dashboard secret storage fails --- docs/CONTAINER.md | 11 ++- packages/daemon/src/daemon/web/server.test.ts | 39 +++++++++- packages/daemon/static/index.html | 74 ++++++++++++++----- 3 files changed, 104 insertions(+), 20 deletions(-) diff --git a/docs/CONTAINER.md b/docs/CONTAINER.md index 0235802d..c3e6e77b 100644 --- a/docs/CONTAINER.md +++ b/docs/CONTAINER.md @@ -45,7 +45,11 @@ The `start` command runs all services: - **No keytar / keychain.** The container has no D-Bus session or gnome-keyring, so `api_key_keychain_name` will not work. Use `api_key_env_var_name` in your config and pass keys as environment - variables. + variables. When configuring a provider in the dashboard, select **File** + to store the key in `secrets.json` on the config volume, or select **Env + Var** when the key is injected into the container environment. The File + option requires a writable config directory; the dashboard reports an + error and does not save the provider if the selected secret store fails. - **Config is mounted, not baked in.** Bind-mount your `config.yaml` into the container at runtime. @@ -91,6 +95,11 @@ Mount this file into the container at: /home/abbenay/.config/abbenay/config.yaml ``` +For dashboard-managed credentials, mount the containing config directory as +writable so the File source can persist `/home/abbenay/.config/abbenay/secrets.json`. +Environment variables remain preferable when the deployment platform provides +secret injection. + --- ## Running diff --git a/packages/daemon/src/daemon/web/server.test.ts b/packages/daemon/src/daemon/web/server.test.ts index b2662300..f7d1a1d9 100644 --- a/packages/daemon/src/daemon/web/server.test.ts +++ b/packages/daemon/src/daemon/web/server.test.ts @@ -18,7 +18,8 @@ import { import type { DaemonState } from '../state.js'; import type { ConnectedClient } from '../state.js'; import type { ProviderInfo, ModelInfo, ChatToolOptions } from '../../core/state.js'; -import type { SecretStore } from '../../core/secrets.js'; +import { MemorySecretStore, type SecretStore } from '../../core/secrets.js'; +import { SecretStoreRegistry } from '../secrets/registry.js'; import { SessionStore } from '../../core/session-store.js'; import { API_TOKEN_COOKIE, CSRF_COOKIE } from './http-security.js'; @@ -624,6 +625,42 @@ describe('createWebApp routes', () => { expect(res.statusCode).toBe(200); }); + it('POST /api/secrets/:key supports the file backend', async () => { + const fileStore = new MemorySecretStore(); + const fileState = createMockState({ + sessionsDir, + secretStore: new SecretStoreRegistry(new MemorySecretStore(), new MemorySecretStore(), fileStore), + }); + const { httpServer, baseUrl: fileBase } = await startTestApp(fileState); + try { + const res = await httpRequest(fileBase, 'POST', '/api/secrets/FILE_KEY', { + body: { value: 'file-value', secretStore: 'file' }, + }); + expect(res.statusCode).toBe(200); + expect((res.body as { secretStore: string }).secretStore).toBe('file'); + expect(await fileStore.get('FILE_KEY')).toBe('file-value'); + } finally { + await stopTestApp(httpServer); + } + }); + + it('returns an error when the selected secret backend cannot save', async () => { + const failingStore = createSecretStore({ + async set() { throw new Error('Keychain storage not available'); }, + }); + const failingState = createMockState({ sessionsDir, secretStore: failingStore }); + const { httpServer, baseUrl: failingBase } = await startTestApp(failingState); + try { + const res = await httpRequest(failingBase, 'POST', '/api/secrets/KEYCHAIN_KEY', { + body: { value: 'secret-value' }, + }); + expect(res.statusCode).toBe(500); + expect((res.body as { error: string }).error).toMatch(/keychain storage not available/i); + } finally { + await stopTestApp(httpServer); + } + }); + it('DELETE /api/secrets/:key deletes a secret', async () => { const res = await httpRequest(baseUrl, 'DELETE', '/api/secrets/MY_KEY'); expect(res.statusCode).toBe(200); diff --git a/packages/daemon/static/index.html b/packages/daemon/static/index.html index 045db13b..c0c24062 100644 --- a/packages/daemon/static/index.html +++ b/packages/daemon/static/index.html @@ -939,6 +939,8 @@

+