feat(coderd_chat_system_prompt): manage the deployment-wide chat system prompt - #412
feat(coderd_chat_system_prompt): manage the deployment-wide chat system prompt#412bpmct wants to merge 3 commits into
Conversation
…em prompt
Adds a singleton resource for the Coder Agents chat system prompt
(Settings -> Instructions), backed by the experimental
/api/experimental/chats/config/system-prompt endpoint via
ExperimentalClient.GetChatSystemPrompt/UpdateChatSystemPrompt.
Coder sanitizes the stored prompt (invisible-char stripping, CRLF
normalization, blank-line collapsing, trimming), so system_prompt is a
custom string type whose semantic equality compares sanitized forms.
The everyday case this absorbs is the trailing newline from
file("system-prompt.md"); real edits still diff. The sanitizer is a
straight port of chatd.SanitizePromptText, and divergence fails loud
(a visible diff) rather than silent.
Follows the coderd_oauth2_provider_settings singleton pattern: shared
PUT for create/update, destroy resets the never-configured defaults
(empty prompt, include_default_system_prompt = true), import adopts
the live value without writing, a plan-time warning fires when a first
apply would overwrite a non-empty out-of-band prompt, and a 404 maps
to an actionable version hint (the endpoint shipped in Coder v2.32.0).
Prompt length is validated at plan time against coderd's 128 KiB cap.
Closes #411
- Gate the fake-server TestAcc tests on TF_ACC with testAccPreCheck, matching every other TestAcc in the repo. - Rewrite the length-validator test as a direct ValidateString unit test; ungated Test* functions here do not spin up the Terraform CLI. - Add real-Coder acceptance tests via integration.StartCoder, the dominant pattern for resources the stock coder image serves (the fake-only approach is justified for oauth2_provider_settings because its endpoint needs unreleased Coder; that does not apply here). The no-drift test is the live proof the sanitizer port matches the server: the prompt carries CRLF, a zero-width space, a blank-line run, and a trailing newline, and the re-plan must be empty. The import test pins both convergence behaviors: a byte-matching config plans clean immediately; a config differing only by sanitization applies one normalization update and then converges. - Document the one-time post-import normalization update and the trimspace(file(...)) escape hatch in the resource description. No license required: the endpoint has no entitlement check, so UseLicense would only cause needless skips on fork PRs.
| // sanitizePromptText mirrors coderd's chatd.SanitizePromptText | ||
| // (coderd/x/chatd/sanitize.go in coder/coder): it strips invisible | ||
| // Unicode characters, normalizes line endings, collapses excessive | ||
| // blank lines, and trims surrounding whitespace. | ||
| // | ||
| // The chat system prompt endpoint stores the sanitized form of | ||
| // whatever is PUT to it, so the value read back rarely matches the | ||
| // configured value byte-for-byte (a trailing newline from | ||
| // `file("system-prompt.md")` is the everyday case). This local copy | ||
| // exists so `system_prompt` can compare semantically: two values are | ||
| // the same setting iff they sanitize to the same string. The logic is | ||
| // deliberately a straight port; if the upstream sanitizer changes, the | ||
| // worst case is a visible (loud) diff on the next plan rather than | ||
| // silent drift. |
There was a problem hiding this comment.
Instead of mirroring code from coder/coder, we should just export it as part of codersdk, and then we can just import it. I don't want to introduce drift.
If we desperately want to get this resource in ASAP, I'm happy for this to be done as a followup.
Feel free to ping me for review on the coder/coder PR.
There was a problem hiding this comment.
Agreed on not mirroring — opened coder/coder#28283 exporting it as codersdk.SanitizePromptText (pure move + callsite updates, tests move with it) and requested you as reviewer.
For this PR I kept the local copy with a TODO pointing at that PR, since actually importing it requires bumping the pinned coder/coder here, and each dep bump is coupled to a release's model_config schema. So the swap lands as the follow-up you suggested: once 28283 merges and the next SDK bump happens, chat_system_prompt_sanitize.go gets deleted and the resource calls the codersdk function directly. The real-Coder acceptance test (applies a prompt with CRLF + zero-width space + blank-line run and requires an empty re-plan) is what keeps the copy honest in the meantime — if the server's sanitizer changes, that test breaks.
Generated by Coder Agents on behalf of @bpmct
…ystem_prompt Per review, the create-time overwrite advisory now also fires when the first apply would change include_default_system_prompt away from the deployment's live value, not just when it would overwrite a non-empty prompt. Covered by a direct ModifyPlan table test in the oauth2_provider_settings style. Also leaves a TODO on the mirrored sanitizer pointing at coder/coder#28283, which exports it as codersdk.SanitizePromptText; once the pinned coder/coder includes that commit the local copy goes away.
Closes #411
Note
Draft until coder/coder#28283 is approved (exports the sanitizer as
codersdk.SanitizePromptText). Depending on timing, either the mirroredchat_system_prompt_sanitize.gogets swapped for the codersdk function here, or the swap lands as the agreed follow-up with the next SDK bump.Problem
The deployment-wide chat system prompt (
Settings → Instructions,GET/PUT /api/experimental/chats/config/system-prompt) has no provider resource, so managing it as code meanscurling the API from CI — which is exactly what coder/dogfood does for cdrstable.dev today, with a hand-rolled drift-diff step in the plan job (coder/dogfood#400). It's one of the most frequently-changing pieces of agents config and should be codified like the AI providers and models already are.What
A new
coderd_chat_system_promptsingleton resource:Design notes:
system_promptis a custom string type whose semantic equality compares sanitized forms — same approach ascoderd_agents_model.model_config's normalized JSON — so the trailing newline fromfile(...)doesn't cause drift after apply, while real edits still diff. The sanitizer is a straight port ofchatd.SanitizePromptText; if upstream changes it, the failure mode is a visible diff, not silent drift. (Framework semantic equality can't rewrite the planned value of a Required attribute, so the first plan after an import shows a one-time normalization update when the config differs only by sanitization; documented, withtrimspace(file(...))as the escape hatch.)coderd_oauth2_provider_settings: create/update share one idempotent PUT,terraform destroyresets the never-configured defaults (empty prompt,include_default_system_prompt = true; the API has no DELETE), import adopts the live value without writing, and a plan-time warning fires when a first apply is about to overwrite a non-empty out-of-band prompt or flipinclude_default_system_promptaway from the live value.Testing
integration.StartCoder, TF_ACC-gated, no license needed — the endpoint has no entitlement check): the no-drift test is the live proof the sanitizer port matches the server — the applied prompt carries a CRLF, a zero-width space, a blank-line run, and a trailing newline, and the re-plan must be empty; destroy is verified via the API to reset the deployment defaults. The import test pins both convergence behaviors (byte-matching config plans clean; sanitization-only difference applies one normalization update, then converges).oauth2_provider_settingsstyle for assertions about requests made and not made: import issues no PUT, destroy resets defaults, the 404 version hint.ModifyPlantable test covering the create-time overwrite advisories for both attributes.make gendocs included; lint clean on the CI's golangci-lint v2.8.0; full package suite passes with and without TF_ACC.Follow-ups
codersdk.SanitizePromptTextonce refactor: move chat prompt sanitization into codersdk coder#28283 is in the pinned SDK.plan-mode-instructionsendpoint could get the same treatment; left out to keep this reviewable.PUT+ drift steps incoderd-stable/and foldsystem-prompt.mdinto theai/module (docs: add "Manage the system prompt as code" recipe under ai-coder/agents/platform-controls/ coder#28090 tracks documenting the recipe).