mirror of
https://github.com/coder/coder.git
synced 2026-09-23 22:20:22 +08:00
Relates to CODAGT-713 Depends on #27284 This makes the per-template `agents_allowed` field authoritative in the API and chatd. It adds optional create and metadata update fields with the intended default and omission semantics, supports `agents-allowed:` template search, includes the value in telemetry, and makes `list_templates`, `read_template`, and `create_workspace` read the template row directly. Existing-workspace retries remain idempotent, and blocked same-organisation templates return an actionable message. The experimental `/template-allowlist` routes remain temporarily because the shipped AI Settings page still calls them, but they no longer control chatd enforcement. #27514 moves that page to per-template metadata, #27515 removes the legacy storage, routes, SDK types, and utility, #27517 adds the CLI flags, and #27518 updates the platform controls documentation for the per-template model, directly addressing CRF-5 and CRF-6. The stack is intended to merge as a unit.
package chattool
import (
"bufio"
"strings"
"unicode"
"github.com/coder/coder/v2/coderd/render"
coderstrings "github.com/coder/coder/v2/coderd/util/strings"
)
// readmeInputMaxBytes caps how many README bytes are parsed so a giant README
// can't OOM coderd. It sits well above the output rune cap, so it never trims a
// real excerpt.
const readmeInputMaxBytes = 64 * 1024
// readmeText returns the README as bounded, frontmatter-stripped plain text
// truncated to maxRunes, or "" when the README is blank or conversion fails.
func readmeText(readme string, maxRunes int) string {
// Cap the parse input first (see readmeInputMaxBytes); goldmark and the
// tokenizer tolerate a mid-line or mid-rune cut.
bounded := readme[:min(len(readme), readmeInputMaxBytes)]
text, err := render.InnerTextFromMarkdown(stripReadmeFrontmatter(bounded))
if err != nil {
return ""
}
return coderstrings.Truncate(text, maxRunes, coderstrings.TruncateWithEllipsis)
}
// stripReadmeFrontmatter strips a leading frontmatter block from the README if it
// exists. An unterminated frontmatter block is treated as a regular body section.
// UTF-8 BOMs are stripped if present, and CRLF is normalized to LF. Leading
// whitespace is also stripped.
func stripReadmeFrontmatter(readme string) string {
trimmed := strings.TrimLeftFunc(readme, func(r rune) bool {
return unicode.IsSpace(r) || r == '\ufeff'
})
var out strings.Builder
scn := bufio.NewScanner(strings.NewReader(trimmed))
scn.Buffer(nil, readmeInputMaxBytes+1) // headroom
var lineNumber int
var fences int
for scn.Scan() {
line := scn.Text()
lineNumber++
// Only handle fences if we haven't already found an
// opening and closing fence.
if fences < 2 {
isFence := strings.TrimRight(line, " \t\r") == "---"
if isFence {
fences++
continue
} else if lineNumber == 1 {
// No leading fence -> no frontmatter. Return the entire document.
return trimmed
}
// We are still in the frontmatter block. Skip writing this line.
continue
}
_, _ = out.WriteString(line)
_, _ = out.WriteString("\n")
}
// Can err if input is too big. Shouldn't happen normally.
if scn.Err() != nil {
return trimmed
}
// Scanner did not scan any lines. No frontmatter to strip.
if lineNumber == 0 {
return trimmed
}
// If we are still fenced but we reached the end of the document
// we have an unterminated fence.
if fences == 1 {
return trimmed
}
return out.String()
}