From cb8f6408655be709770a2a342c822cd6380822fc Mon Sep 17 00:00:00 2001 From: Paul Wells Date: Wed, 16 Sep 2026 07:26:25 -0700 Subject: [PATCH] agent endpoints: key templates by shape and export their ambiguity Canonical renders a template as its literals and convertor kinds, so two templates the trie cannot tell apart share a key. Param names reach the trie only as a kind, so /x/{a} and /x/{b} accept the same paths; anything keying routes by the declared spelling splits them apart. Literals are length-prefixed because a brace that opens no well-formed param is literal text: /x/{:str} matches itself and must not collide with /x/{a}. ambiguousTemplate becomes Template.Ambiguous. It reads only the template's own elements, so it answers for a manifest that was validated but never compiled into a table of its own. Co-authored-by: Claude Opus 5 (1M context) --- pkg/agent/endpoint/router/build.go | 20 +------------ pkg/agent/endpoint/router/template.go | 41 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/pkg/agent/endpoint/router/build.go b/pkg/agent/endpoint/router/build.go index a2ec9f3a8..f250eecf6 100644 --- a/pkg/agent/endpoint/router/build.go +++ b/pkg/agent/endpoint/router/build.go @@ -54,7 +54,7 @@ func (b *Builder[T]) Add(t *Template, m Mask, v T) error { idx := b.n b.n++ - if ambiguousTemplate(t) { + if t.Ambiguous() { b.ambiguous = append(b.ambiguous, t.raw) } @@ -125,24 +125,6 @@ func singleRun[T any](e *buildEdge[T]) bool { return true } -// ambiguousTemplate reports whether a template's own shape can force a search, -// independent of what other templates put in the tree. -func ambiguousTemplate(t *Template) bool { - for i, e := range t.elements { - if e.kind == kindLiteral || e.kind == kindUUID { - continue - } - if i == len(t.elements)-1 { - continue // terminal: only the greedy run can reach the end - } - next := t.elements[i+1] - if next.kind != kindLiteral || e.kind.charset(next.lit[0]) { - return true - } - } - return false -} - func insertLiteral[T any](n *buildNode[T], lit string, idx uint32) *buildNode[T] { for len(lit) > 0 { e := literalEdge(n, lit[0]) diff --git a/pkg/agent/endpoint/router/template.go b/pkg/agent/endpoint/router/template.go index 97a5d9f14..a065f8390 100644 --- a/pkg/agent/endpoint/router/template.go +++ b/pkg/agent/endpoint/router/template.go @@ -16,6 +16,7 @@ package router import ( "fmt" + "strconv" "strings" "unicode/utf8" ) @@ -67,6 +68,46 @@ type Template struct { // String returns the template as declared. func (t *Template) String() string { return t.raw } +// Canonical returns a key identifying what the template matches: two templates +// with the same canonical form accept exactly the same paths and differ only in +// param names. Literals are length-prefixed so that a literal brace cannot forge +// a convertor - "/x/{:str}" is literal text, and must not collide with "/x/{a}". +func (t *Template) Canonical() string { + var b strings.Builder + for _, e := range t.elements { + if e.kind == kindLiteral { + b.WriteByte('L') + b.WriteString(strconv.Itoa(len(e.lit))) + b.WriteByte(':') + b.WriteString(e.lit) + continue + } + b.WriteByte('K') + b.WriteString(strconv.Itoa(int(e.kind))) + } + return b.String() +} + +// Ambiguous reports whether the template's own shape can force the matcher to +// backtrack - a param a following literal can extend, adjacent params, or a +// non-final path convertor - independent of what other templates put in the +// tree. +func (t *Template) Ambiguous() bool { + for i, e := range t.elements { + if e.kind == kindLiteral || e.kind == kindUUID { + continue + } + if i == len(t.elements)-1 { + continue // terminal: only the greedy run can reach the end + } + next := t.elements[i+1] + if next.kind != kindLiteral || e.kind.charset(next.lit[0]) { + return true + } + } + return false +} + // ParseTemplate parses a starlette path template. Custom convertors are // rejected: only the five built-ins may travel over the wire. func ParseTemplate(path string) (*Template, error) {