fix(http/agents): validate agent_key slug on update path

handleUpdate accepted any string in the agent_key allowlist field
without running it through isValidSlug. A client could rename an
agent to "weird:key", which would break router cache exact-segment
invalidation (the cache splits on the last colon for invalidation
matching). Add the slug check inline after the allowlist filter and
return MsgInvalidSlug on failure. The slug regex already rejects
colons, slashes, whitespace, and other characters that would confuse
path rendering or cache key parsing — add a dedicated predicate test
covering the full trap surface.
This commit is contained in:
viettranx committed 2026-04-11 21:22:23 +07:00
1 parent c7a93703ba
commit 93099cc682
2 files changed
+53

No files matched your search

+12
View File
@@ -368,6 +368,18 @@ func (h *AgentsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
allowed := filterAllowedKeys(updates, agentAllowedFields)
allowed["restrict_to_workspace"] = true
// If agent_key is being changed, enforce the slug format. The router
// cache uses `tenantID:agentKey` as its canonical key and splits on the
// last colon for exact-segment invalidation — a colon inside agent_key
// would silently break invalidation. Slug regex already rejects colons
// and any other shell/path-unfriendly characters.
if newKey, ok := allowed["agent_key"].(string); ok && newKey != "" {
if !isValidSlug(newKey) {
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidSlug, "agent_key"))
return
}
}
// Validate v3 flag values in other_config (must be boolean).
if oc, ok := allowed["other_config"]; ok && oc != nil {
switch v := oc.(type) {
+41
View File
@@ -0,0 +1,41 @@
package http
import "testing"
// TestIsValidSlug covers the slug predicate used by agent_key, skill slug,
// provider name, and MCP server name validation. The slug format is the
// router cache's canonical anchor — the cache splits on the last colon for
// exact-segment invalidation, so the predicate MUST reject any character
// that would collide with that split (notably `:`).
func TestIsValidSlug(t *testing.T) {
cases := []struct {
name string
input string
want bool
}{
{"simple lowercase", "agent", true},
{"with hyphen", "goctech-leader", true},
{"with digits", "agent-42", true},
{"single char", "a", true},
{"starts with digit", "1-agent", true},
{"empty", "", false},
{"uppercase rejected", "Agent", false},
{"starts with hyphen", "-agent", false},
{"ends with hyphen", "agent-", false},
{"colon rejected", "weird:key", false},
{"slash rejected", "weird/key", false},
{"whitespace rejected", "weird key", false},
{"dot rejected", "weird.key", false},
{"tab rejected", "weird\tkey", false},
{"underscore rejected", "weird_key", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isValidSlug(tc.input); got != tc.want {
t.Errorf("isValidSlug(%q) = %v, want %v", tc.input, got, tc.want)
}
})
}
}