fix(config): a strict shape for a local model alias; the upload chat model must be local (adversarial review F1, F8)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 16:38:30 +02:00
co-authored by Claude Sonnet 5.5
parent 7fe746173d
commit a9a2a9270f
+16 -12
View File
@@ -3,6 +3,7 @@ package config
import ( import (
"fmt" "fmt"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -116,6 +117,9 @@ func loadUploadConfig() (UploadConfig, error) {
if err := u.validateReconcile(); err != nil { if err := u.validateReconcile(); err != nil {
return UploadConfig{}, err return UploadConfig{}, err
} }
if !IsLocalModel(u.UploadChatModel) {
return UploadConfig{}, fmt.Errorf("config: TAPIR_UPLOAD_CHAT_MODEL=%q is not a local model (koala/ or iguana/): the whole private transcript is sent to it", u.UploadChatModel)
}
chars, err := intOr("TAPIR_MAX_UPLOADED_TRANSCRIPT_CHARS", defaultMaxUploadedChars) chars, err := intOr("TAPIR_MAX_UPLOADED_TRANSCRIPT_CHARS", defaultMaxUploadedChars)
if err != nil { if err != nil {
@@ -141,18 +145,18 @@ func positiveInt(key string, fallback int) (int, error) {
return n, nil return n, nil
} }
// localPrefixes are the host prefixes of the models that run on the homelab. Private // localAlias is the shape of a model alias that runs on the homelab: a local host
// meeting content may be sent to these and to nothing else for reconciliation // prefix, then one plain name. A bare prefix check let through aliases that are shaped
// (ADR-034): a cloud alias is refused at start-up rather than discovered later. // like an escape (koala/../berget/x, koala//x, koala/x?host=berget): whether the
var localPrefixes = []string{"koala/", "iguana/"} // gateway would route them elsewhere is unknown, so they are refused (ADR-035 review).
var localAlias = regexp.MustCompile(`^(koala|iguana)/[A-Za-z0-9][A-Za-z0-9._:-]*$`)
func isLocalModel(alias string) bool { // IsLocalModel reports whether alias is a local model. Private meeting content may be
for _, p := range localPrefixes { // sent to these and to nothing else: reconciliation (ADR-034) and chat over an uploaded
if strings.HasPrefix(alias, p) { // recording (ADR-035) are both held to it, and a cloud alias is refused at start-up
return true // rather than discovered later.
} func IsLocalModel(alias string) bool {
} return localAlias.MatchString(alias) && !strings.Contains(alias, "..")
return false
} }
// validateReconcile checks the two-engine settings. They are inert while no base // validateReconcile checks the two-engine settings. They are inert while no base
@@ -168,7 +172,7 @@ func (u UploadConfig) validateReconcile() error {
{"TAPIR_RECONCILE_BASE_MODEL", u.ReconcileBaseModel}, {"TAPIR_RECONCILE_BASE_MODEL", u.ReconcileBaseModel},
{"TAPIR_RECONCILE_MODEL", u.ReconcileModel}, {"TAPIR_RECONCILE_MODEL", u.ReconcileModel},
} { } {
if !isLocalModel(m.alias) { if !IsLocalModel(m.alias) {
return fmt.Errorf("config: %s=%q is not a local model (koala/ or iguana/): private meeting content must not leave the homelab for reconciliation", m.name, m.alias) return fmt.Errorf("config: %s=%q is not a local model (koala/ or iguana/): private meeting content must not leave the homelab for reconciliation", m.name, m.alias)
} }
} }