mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
c933ddcffd
## Problem The Admin → Agents → System Prompt textarea saved only to the browser's `localStorage`. The value was never sent to the backend, never stored in the database, and never injected into chats. Entering text, clicking Save, and refreshing the page showed no changes — the prompt was effectively a no-op. ## Root Cause Three disconnected layers: 1. **Frontend** wrote to `localStorage`, never called an API. 2. **`handleCreateChat`** never read `savedSystemPrompt`. 3. **Backend** hardcoded `chatd.DefaultSystemPrompt` on every chat creation — no field in `CreateChatRequest` accepted a custom prompt. ## Changes ### Database - Added `GetChatSystemPrompt` / `UpsertChatSystemPrompt` queries on the existing `site_configs` table (no migration needed). ### API - `GET /api/experimental/chats/system-prompt` — returns the configured prompt (any authenticated user). - `PUT /api/experimental/chats/system-prompt` — sets the prompt (admin-only, `rbac: deployment_config update`). - Input validation: max 32 KiB prompt length. ### Backend - `resolvedChatSystemPrompt(ctx)` checks for a custom prompt in the DB, falls back to `chatd.DefaultSystemPrompt` when empty/unset. - Logs a warning on DB errors instead of silently swallowing them. - Replaced the hardcoded `defaultChatSystemPrompt()` call in chat creation. ### Frontend - Replaced `localStorage` read/write with React Query `useQuery`/`useMutation` backed by the new endpoints. - Fixed `useEffect` draft sync to avoid clobbering in-progress user edits on refetch. - Added `try/catch` error handling on save (draft stays dirty for retry). - Save button disabled during mutation (`isSavingSystemPrompt`). - Query key follows kebab-case convention (`chat-system-prompt`). ### UX - Added hint: "When empty, the built-in default prompt is used." ### Tests - `TestChatSystemPrompt`: GET returns empty when unset, admin can set, non-admin gets 403. - dbauthz `TestMethodTestSuite` coverage for both new querier methods.
164 lines
6.1 KiB
SQL
164 lines
6.1 KiB
SQL
-- name: UpsertDefaultProxy :exec
|
|
-- The default proxy is implied and not actually stored in the database.
|
|
-- So we need to store it's configuration here for display purposes.
|
|
-- The functional values are immutable and controlled implicitly.
|
|
INSERT INTO site_configs (key, value)
|
|
VALUES
|
|
('default_proxy_display_name', @display_name :: text),
|
|
('default_proxy_icon_url', @icon_url :: text)
|
|
ON CONFLICT
|
|
(key)
|
|
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
|
|
;
|
|
|
|
-- name: GetDefaultProxyConfig :one
|
|
SELECT
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_display_name'), 'Default') :: text AS display_name,
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_icon_url'), '/emojis/1f3e1.png') :: text AS icon_url
|
|
;
|
|
|
|
-- name: InsertDeploymentID :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('deployment_id', $1);
|
|
|
|
-- name: GetDeploymentID :one
|
|
SELECT value FROM site_configs WHERE key = 'deployment_id';
|
|
|
|
-- name: InsertDERPMeshKey :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('derp_mesh_key', $1);
|
|
|
|
-- name: GetDERPMeshKey :one
|
|
SELECT value FROM site_configs WHERE key = 'derp_mesh_key';
|
|
|
|
-- name: UpsertLastUpdateCheck :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('last_update_check', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'last_update_check';
|
|
|
|
-- name: GetLastUpdateCheck :one
|
|
SELECT value FROM site_configs WHERE key = 'last_update_check';
|
|
|
|
-- name: UpsertAnnouncementBanners :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('announcement_banners', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'announcement_banners';
|
|
|
|
-- name: GetAnnouncementBanners :one
|
|
SELECT value FROM site_configs WHERE key = 'announcement_banners';
|
|
|
|
-- name: UpsertLogoURL :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('logo_url', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'logo_url';
|
|
|
|
-- name: GetLogoURL :one
|
|
SELECT value FROM site_configs WHERE key = 'logo_url';
|
|
|
|
-- name: UpsertApplicationName :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('application_name', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'application_name';
|
|
|
|
-- name: GetApplicationName :one
|
|
SELECT value FROM site_configs WHERE key = 'application_name';
|
|
|
|
-- name: GetAppSecurityKey :one
|
|
SELECT value FROM site_configs WHERE key = 'app_signing_key';
|
|
|
|
-- name: UpsertAppSecurityKey :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('app_signing_key', $1)
|
|
ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'app_signing_key';
|
|
|
|
-- name: GetOAuthSigningKey :one
|
|
SELECT value FROM site_configs WHERE key = 'oauth_signing_key';
|
|
|
|
-- name: UpsertOAuthSigningKey :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1)
|
|
ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key';
|
|
|
|
-- name: GetCoordinatorResumeTokenSigningKey :one
|
|
SELECT value FROM site_configs WHERE key = 'coordinator_resume_token_signing_key';
|
|
|
|
-- name: UpsertCoordinatorResumeTokenSigningKey :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('coordinator_resume_token_signing_key', $1)
|
|
ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'coordinator_resume_token_signing_key';
|
|
|
|
-- name: GetHealthSettings :one
|
|
SELECT
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'health_settings'), '{}') :: text AS health_settings
|
|
;
|
|
|
|
-- name: UpsertHealthSettings :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('health_settings', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'health_settings';
|
|
|
|
-- name: GetNotificationsSettings :one
|
|
SELECT
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'notifications_settings'), '{}') :: text AS notifications_settings
|
|
;
|
|
|
|
-- name: UpsertNotificationsSettings :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('notifications_settings', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notifications_settings';
|
|
|
|
-- name: GetPrebuildsSettings :one
|
|
SELECT
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'prebuilds_settings'), '{}') :: text AS prebuilds_settings
|
|
;
|
|
|
|
-- name: UpsertPrebuildsSettings :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('prebuilds_settings', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'prebuilds_settings';
|
|
|
|
-- name: GetRuntimeConfig :one
|
|
SELECT value FROM site_configs WHERE site_configs.key = $1;
|
|
|
|
-- name: UpsertRuntimeConfig :exec
|
|
INSERT INTO site_configs (key, value) VALUES ($1, $2)
|
|
ON CONFLICT (key) DO UPDATE SET value = $2 WHERE site_configs.key = $1;
|
|
|
|
-- name: DeleteRuntimeConfig :exec
|
|
DELETE FROM site_configs
|
|
WHERE site_configs.key = $1;
|
|
|
|
-- name: GetOAuth2GithubDefaultEligible :one
|
|
SELECT
|
|
CASE
|
|
WHEN value = 'true' THEN TRUE
|
|
ELSE FALSE
|
|
END
|
|
FROM site_configs
|
|
WHERE key = 'oauth2_github_default_eligible';
|
|
|
|
-- name: UpsertOAuth2GithubDefaultEligible :exec
|
|
INSERT INTO site_configs (key, value)
|
|
VALUES (
|
|
'oauth2_github_default_eligible',
|
|
CASE
|
|
WHEN sqlc.arg(eligible)::bool THEN 'true'
|
|
ELSE 'false'
|
|
END
|
|
)
|
|
ON CONFLICT (key) DO UPDATE
|
|
SET value = CASE
|
|
WHEN sqlc.arg(eligible)::bool THEN 'true'
|
|
ELSE 'false'
|
|
END
|
|
WHERE site_configs.key = 'oauth2_github_default_eligible';
|
|
|
|
-- name: UpsertWebpushVAPIDKeys :exec
|
|
INSERT INTO site_configs (key, value)
|
|
VALUES
|
|
('webpush_vapid_public_key', @vapid_public_key :: text),
|
|
('webpush_vapid_private_key', @vapid_private_key :: text)
|
|
ON CONFLICT (key)
|
|
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key;
|
|
|
|
-- name: GetWebpushVAPIDKeys :one
|
|
SELECT
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_public_key'), '') :: text AS vapid_public_key,
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_private_key'), '') :: text AS vapid_private_key;
|
|
|
|
-- name: GetChatSystemPrompt :one
|
|
SELECT
|
|
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt;
|
|
|
|
-- name: UpsertChatSystemPrompt :exec
|
|
INSERT INTO site_configs (key, value) VALUES ('agents_chat_system_prompt', $1)
|
|
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_system_prompt';
|