Files
livekit/pkg/agent/worker_test.go
T
Paul Wells 4d622438e9 agent endpoints: accept any agent name, keep request escaping intact
The name is percent-encoded into its URL path segment, so it needs no charset.
Only "_", "." and ".." are refused, and only for workers declaring endpoints:
all three are unreserved, so no encoded form addresses them distinctly. "_"
now addresses a worker registered with an empty agent name, which the
implicit-dispatch default leaves unset.

Two escaping fixes the escaped-path split depends on:

- the trailing-slash alternate treated a decoded slash as literal, so
  /known%2F normalized onto route /known and the worker was handed a path it
  does not serve.
- RemoveDoubleSlashes trimmed URL.Path and left RawPath, which makes
  EscapedPath() re-encode from Path and lose every escape in the request.

Request logs now carry the encoded path, since a decoded %0A forges a log line.
2026-09-14 10:44:28 -07:00

78 lines
2.5 KiB
Go

// Copyright 2026 LiveKit, Inc.
package agent_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/protocol/livekit"
)
type nopSignalConn struct{}
func (nopSignalConn) WriteServerMessage(*livekit.ServerMessage) (int, error) { return 0, nil }
func (nopSignalConn) ReadWorkerMessage() (*livekit.WorkerMessage, int, error) { return nil, 0, nil }
func (nopSignalConn) SetReadDeadline(time.Time) error { return nil }
func (nopSignalConn) Close() error { return nil }
func (nopSignalConn) CloseWithReason(string) error { return nil }
// the name is percent-encoded into a URL path segment.
func TestHandleRegisterEndpointAgentNames(t *testing.T) {
cases := []struct {
name string
agentName string
ok bool
}{
{"plain", "test-agent", true},
{"underscore", "my_agent", true},
{"spaces", "LODHA Vayam Agent", true},
{"colon and star", "prod:*", true},
{"at sign", "charlie@v1.42.0", true},
{"slash", "models/gemma/v2", true},
{"brackets", "Nathan [Elara Agent]", true},
{"non ascii", "Sehhaty الصحة", true},
{"past the old 64 byte cap", strings.Repeat("n", 77), true},
{"empty", "", false},
{"unnamed sentinel", "_", false},
{"dot", ".", false},
{"dot dot", "..", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
h := agent.NewWorkerRegisterer(nopSignalConn{}, &livekit.ServerInfo{}, agent.WorkerRegistration{}).
WithEndpointSettings(func(*livekit.RegisterWorkerRequest) (*livekit.AgentHttp_AgentEndpointSettings, error) {
return &livekit.AgentHttp_AgentEndpointSettings{}, nil
})
err := h.HandleRegister(&livekit.RegisterWorkerRequest{
Type: livekit.JobType_JT_ROOM,
AgentName: c.agentName,
Endpoints: []*livekit.AgentHttp_AgentEndpoint{{Path: "/hook", Methods: []string{"GET"}}},
})
if c.ok {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
// the reserved names constrain only workers that declare endpoints.
func TestHandleRegisterWithoutEndpointsIgnoresReservedNames(t *testing.T) {
for _, name := range []string{"", "_", ".", ".."} {
h := agent.NewWorkerRegisterer(nopSignalConn{}, &livekit.ServerInfo{}, agent.WorkerRegistration{})
require.NoError(t, h.HandleRegister(&livekit.RegisterWorkerRequest{
Type: livekit.JobType_JT_ROOM,
AgentName: name,
}), name)
}
}