Skip to content

feat(coderd_chat_system_prompt): manage the deployment-wide chat system prompt - #412

Draft
bpmct wants to merge 3 commits into
mainfrom
bpmct/chat-system-prompt-resource
Draft

feat(coderd_chat_system_prompt): manage the deployment-wide chat system prompt#412
bpmct wants to merge 3 commits into
mainfrom
bpmct/chat-system-prompt-resource

Conversation

@bpmct

@bpmct bpmct commented Aug 18, 2026

Copy link
Copy Markdown
Member

Closes #411

Note

Draft until coder/coder#28283 is approved (exports the sanitizer as codersdk.SanitizePromptText). Depending on timing, either the mirrored chat_system_prompt_sanitize.go gets 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 means curling 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_prompt singleton resource:

resource "coderd_chat_system_prompt" "this" {
  system_prompt                  = file("${path.module}/system-prompt.md")
  include_default_system_prompt = true
}

Design notes:

  • Sanitization-aware drift: coderd sanitizes the stored prompt (strips invisible Unicode chars, normalizes CRLF, collapses blank-line runs, trims). system_prompt is a custom string type whose semantic equality compares sanitized forms — same approach as coderd_agents_model.model_config's normalized JSON — so the trailing newline from file(...) doesn't cause drift after apply, while real edits still diff. The sanitizer is a straight port of chatd.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, with trimspace(file(...)) as the escape hatch.)
  • Singleton semantics follow coderd_oauth2_provider_settings: create/update share one idempotent PUT, terraform destroy resets 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 flip include_default_system_prompt away from the live value.
  • Plan-time length validation against coderd's 128 KiB cap, same idea as fix(coderd_template): validate description length at plan time #404.
  • 404 → version hint: the endpoint shipped in Coder v2.32.0 (fix(agents): persist system prompt server-side instead of localStorage coder#22857); older deployments get an actionable error instead of a bare not-found.
  • Experimental warning like the other agents resources; the include-default pointer is always sent so the resource owns the value outright.

Testing

  • Real-Coder acceptance tests (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).
  • Fake-coderd tests in the oauth2_provider_settings style for assertions about requests made and not made: import issues no PUT, destroy resets defaults, the 404 version hint.
  • Unit tests for the sanitizer port (ZWNJ preserved, ZWJ stripped, idempotency), the semantic-equality matrix, the plan-time length validator, and a ModifyPlan table test covering the create-time overwrite advisories for both attributes.

make gen docs included; lint clean on the CI's golangci-lint v2.8.0; full package suite passes with and without TF_ACC.

Follow-ups

Generated by Coder Agents on behalf of @bpmct

bpmct added 2 commits August 18, 2026 01:08
…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.
@ethanndickson
ethanndickson self-requested a review August 18, 2026 04:23
Comment on lines +8 to +21
// 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.

@ethanndickson ethanndickson Aug 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread internal/provider/chat_system_prompt_resource.go Outdated
…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.
@bpmct
bpmct requested a review from ethanndickson August 18, 2026 22:59
@bpmct
bpmct marked this pull request as draft August 18, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a resource for the deployment-wide chat system prompt

2 participants