Files
livekit/pkg/service/agentendpoint.go
T
Paul Wells 1944cb495d agent endpoints: scope the registry, let the embedder own tenancy
The registry was doing three jobs at once: fencing worker epochs by id,
listing a deployment's candidates, and holding its merged route table. The
last two are per-deployment state, so it keyed them on
(api key, agent name, deployment) and grew a tenancy concept that only an
embedder can actually define. Cloud has to lie to it, passing a project id
in a field named APIKey.

Split them. Scope is one deployment's serving state and stores no identity
at all; whoever embeds the package keys a map of scopes however its own
tenancy works, and hands the front a resolved one. Registry keeps only the
worker-id fence, which is genuinely node-wide: worker ids are server-issued,
so an epoch is superseded wherever it was scoped.

The front loses its registry, its SingleKeyFallback and FallbackRequest: the
resolver now returns the scope and a fallback already curried on the
deployment, plus an ok that carries the 401-vs-503 split the empty api key
used to encode. routeTable drops its key and takes the scope's logger, so
identity is curried in rather than stored.

pkg/service takes ownership of the "api key is the tenant" rule, which is
true there and nowhere else, and of releasing a scope once nothing holds it.
Behavior is unchanged, including serving public routes to an unauthenticated
caller when one configured key or one attached tenant makes the key
unambiguous.
2026-09-16 08:47:37 -07:00

64 lines
2.1 KiB
Go

// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"net/http"
"github.com/livekit/livekit-server/pkg/agent/endpoint"
)
// AgentEndpointService is the /agents/{agent_name}/{deployment}/{path...} front
// backed by this node's attached workers. The api key comes from validated grants
// when a token is present; a non-public route additionally requires an
// agent-endpoint grant scoped to this agent and deployment.
type AgentEndpointService struct {
*endpoint.Front
}
func NewAgentEndpointService(h *AgentHandler, scopes *EndpointScopes) *AgentEndpointService {
return &AgentEndpointService{
Front: endpoint.NewFront(endpoint.FrontParams{
ResolveAccess: func(r *http.Request, agentName, deployment string) (endpoint.Access, bool) {
if claims := GetGrants(r.Context()); claims != nil {
level := endpoint.AccessCredentialed
if claims.AgentEndpoint.Allows(agentName, deployment) {
level = endpoint.AccessGranted
}
return endpoint.Access{
Scope: scopes.Scope(GetAPIKey(r.Context()), agentName, deployment),
Level: level,
}, true
}
// unauthenticated: one configured key makes the api key
// unambiguous, and failing that a single attached tenant does.
// A guessed api key confers no access, so such a request still
// reaches only routes marked public.
apiKey := h.singleAPIKey
if apiKey == "" {
var ok bool
if apiKey, ok = scopes.SingleKey(); !ok {
return endpoint.Access{}, false
}
}
return endpoint.Access{
Scope: scopes.Scope(apiKey, agentName, deployment),
}, true
},
Logger: h.logger,
}),
}
}