fix(wing,server): client names are matched as whole Unicode words; unsafe lists are a startup error (review of #214)
- WrapNames bounds the bare name list with Unicode-aware edges instead of \b
(Go's \b is ASCII-only, so a name with a non-ASCII edge letter such as Åke,
Société, Ørsted would never have matched), and validates the list on its own:
it must compile, must not match the empty string, and cannot break out of its
group ('x)|.*(' compiles alone only if balanced)
- NewRouter refuses any pattern that matches text naming no client
- docs state the whole-word semantics and what is not matched
Mutation-checked. Open decision for Mathias: whole-word versus substring recall
for a confidentiality filter (plurals, CamelCase and digits glued on do not match).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -152,17 +152,18 @@ func envInt(key string, fallback int) int {
|
||||
|
||||
// ingestClientPattern picks the client-content pattern for raw/ routing.
|
||||
// INGEST_CLIENT_BLOCK is a complete regexp and is used as is. Otherwise the
|
||||
// shared CLAUDE_INGEST_CLIENT_BLOCK is used, which the deployment injects as a
|
||||
// bare alternation of client names that claudewatcher wraps in \b(...)\b: it is
|
||||
// wrapped the same way here, or a short name would match inside ordinary words.
|
||||
func ingestClientPattern(getenv func(string) string) string {
|
||||
// shared CLAUDE_INGEST_CLIENT_BLOCK is used: the deployment injects it as a bare
|
||||
// list of client names (regexp alternation), which wing.WrapNames bounds so a
|
||||
// short name does not match inside ordinary words, and which it refuses if it
|
||||
// would break out of its group or match the empty string.
|
||||
func ingestClientPattern(getenv func(string) string) (string, error) {
|
||||
if p := getenv("INGEST_CLIENT_BLOCK"); p != "" {
|
||||
return p
|
||||
return p, nil
|
||||
}
|
||||
if names := getenv("CLAUDE_INGEST_CLIENT_BLOCK"); names != "" {
|
||||
return `\b(` + names + `)\b`
|
||||
return wing.WrapNames(names)
|
||||
}
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// envFlag reads a boolean switch. Unset is off; a value strconv.ParseBool does
|
||||
@@ -441,7 +442,11 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
if routing {
|
||||
pattern := ingestClientPattern(os.Getenv)
|
||||
pattern, perr := ingestClientPattern(os.Getenv)
|
||||
if perr != nil {
|
||||
logger.Error("client-content pattern", "err", perr)
|
||||
os.Exit(1)
|
||||
}
|
||||
router, rerr := wing.NewRouter(brainDir, envOr("INGEST_DEFAULT_WING", "web-clip"), pattern)
|
||||
if rerr != nil {
|
||||
logger.Error("build wing router", "err", rerr)
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/wing"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -131,40 +130,29 @@ func TestEnvFlag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The deployment injects CLAUDE_INGEST_CLIENT_BLOCK as an ALTERNATION OF CLIENT
|
||||
// NAMES that claudewatcher wraps in \b(...)\b. Used raw, a short name would match
|
||||
// inside ordinary words and park half the clips.
|
||||
// The deployment injects CLAUDE_INGEST_CLIENT_BLOCK as a bare list of client
|
||||
// names (regexp alternation). Used raw, a short name would match inside ordinary
|
||||
// words; wrapped with \b, a name with a non-ASCII edge letter would never match.
|
||||
func TestIngestClientPattern(t *testing.T) {
|
||||
get := func(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
want string
|
||||
}{
|
||||
{"nothing set", map[string]string{}, ""},
|
||||
{"explicit pattern is a full regexp, used as is", map[string]string{"INGEST_CLIENT_BLOCK": `acme\s+corp`, "CLAUDE_INGEST_CLIENT_BLOCK": "other"}, `acme\s+corp`},
|
||||
{"the shared client-name alternation is wrapped like claudewatcher wraps it", map[string]string{"CLAUDE_INGEST_CLIENT_BLOCK": "dif|trucker"}, `\b(dif|trucker)\b`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, ingestClientPattern(get(tt.env)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngestClientPattern_AShortClientNameDoesNotParkOrdinaryWords(t *testing.T) {
|
||||
pattern := ingestClientPattern(func(k string) string {
|
||||
if k == "CLAUDE_INGEST_CLIENT_BLOCK" {
|
||||
return "dif|trucker"
|
||||
}
|
||||
return ""
|
||||
t.Run("nothing set", func(t *testing.T) {
|
||||
p, err := ingestClientPattern(get(map[string]string{}))
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, p)
|
||||
})
|
||||
t.Run("explicit pattern is a full regexp, used as is", func(t *testing.T) {
|
||||
p, err := ingestClientPattern(get(map[string]string{"INGEST_CLIENT_BLOCK": `acme\s+corp`, "CLAUDE_INGEST_CLIENT_BLOCK": "other"}))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `acme\s+corp`, p)
|
||||
})
|
||||
t.Run("the shared names are wrapped", func(t *testing.T) {
|
||||
p, err := ingestClientPattern(get(map[string]string{"CLAUDE_INGEST_CLIENT_BLOCK": "dif|trucker"}))
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, p, "dif|trucker")
|
||||
assert.NotEqual(t, "dif|trucker", p)
|
||||
})
|
||||
t.Run("a shared value that would break out of the group is a startup error", func(t *testing.T) {
|
||||
_, err := ingestClientPattern(get(map[string]string{"CLAUDE_INGEST_CLIENT_BLOCK": "x)|.*("}))
|
||||
assert.Error(t, err)
|
||||
})
|
||||
r, err := wing.NewRouter(t.TempDir(), "web-clip", pattern)
|
||||
require.NoError(t, err)
|
||||
d := r.Resolve(context.Background(), wing.Note{Text: "a different approach to modifying prompts"})
|
||||
assert.Equal(t, wing.Default, d.Source, "'dif' inside 'different' and 'modifying' is not the client")
|
||||
d = r.Resolve(context.Background(), wing.Note{Text: "kickoff for the DIF project next week"})
|
||||
assert.Equal(t, wing.Exception, d.Source)
|
||||
d = r.Resolve(context.Background(), wing.Note{Text: "Trucker, the client, asked for"})
|
||||
assert.Equal(t, wing.Exception, d.Source)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user