From 8a9d4d5348a3571d224ecb669626abee41571f1d Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 10 Jun 2026 14:24:57 +0200 Subject: [PATCH 1/2] Add to --- packages/livekit-server-sdk/package.json | 2 +- .../src/AgentDispatchClient.test.ts | 82 +++++++++++++++++++ .../src/AgentDispatchClient.ts | 30 ++++++- pnpm-lock.yaml | 11 ++- 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 packages/livekit-server-sdk/src/AgentDispatchClient.test.ts diff --git a/packages/livekit-server-sdk/package.json b/packages/livekit-server-sdk/package.json index de280971..082559bd 100644 --- a/packages/livekit-server-sdk/package.json +++ b/packages/livekit-server-sdk/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@bufbuild/protobuf": "^1.10.1", - "@livekit/protocol": "^1.46.3", + "@livekit/protocol": "^1.46.6", "camelcase-keys": "^9.0.0", "jose": "^5.1.2" }, diff --git a/packages/livekit-server-sdk/src/AgentDispatchClient.test.ts b/packages/livekit-server-sdk/src/AgentDispatchClient.test.ts new file mode 100644 index 00000000..219a4715 --- /dev/null +++ b/packages/livekit-server-sdk/src/AgentDispatchClient.test.ts @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2024 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, it } from 'vitest'; +import { validateAgentDeploymentString } from './AgentDispatchClient.js'; + +describe('validateAgentDeploymentString', () => { + it('accepts a valid deployment string with allowed separators', () => { + expect(() => validateAgentDeploymentString('my-agent_v1.0')).not.toThrow(); + }); + + it('accepts a single alphanumeric character', () => { + expect(() => validateAgentDeploymentString('a')).not.toThrow(); + }); + + it('throws when the string exceeds 63 characters', () => { + expect(() => validateAgentDeploymentString('a'.repeat(64))).toThrow( + 'Deployment string must not exceed 63 characters', + ); + }); + + it('throws when it does not start with an alphanumeric character', () => { + expect(() => validateAgentDeploymentString('-agent')).toThrow( + 'Deployment must start and end with an alphanumeric character', + ); + }); + + it('throws when it does not end with an alphanumeric character', () => { + expect(() => validateAgentDeploymentString('agent.')).toThrow( + 'Deployment must start and end with an alphanumeric character', + ); + }); + + it.each(['my agent', 'a/b', 'a@b', 'a:b', 'a+b', 'a,b'])( + 'throws on unallowed character in the middle: %j', + (deployment) => { + expect(() => validateAgentDeploymentString(deployment)).toThrow( + 'Deployment must start and end with an alphanumeric character', + ); + }, + ); + + it('accepts an empty string (targets the production deployment)', () => { + // An empty deployment is explicitly allowed: per the docstring, leaving it + // empty targets the production deployment. The validator early-returns + // before the alphanumeric regex would otherwise reject it. + expect(() => validateAgentDeploymentString('')).not.toThrow(); + }); + + it.each([ + ['null byte', 'agent\0'], + ['tab', 'a\tb'], + ['newline in the middle', 'a\nb'], + ['carriage return', 'a\rb'], + ['vertical tab', 'a\x0bb'], + ])('throws on control character (%s)', (_label, deployment) => { + expect(() => validateAgentDeploymentString(deployment)).toThrow( + 'Deployment must start and end with an alphanumeric character', + ); + }); + + it('rejects a trailing newline (JS $ must not match before it)', () => { + // Without the `m` flag, JS anchors `$` at the true end of string, so a + // smuggled trailing newline ("agent\n") is correctly rejected rather than + // treated as a valid "agent". + expect(() => validateAgentDeploymentString('agent\n')).toThrow( + 'Deployment must start and end with an alphanumeric character', + ); + }); + + it.each([ + ['path traversal', '../etc'], + ['leading slash', '/agent'], + ['leading whitespace', ' agent'], + ['trailing whitespace', 'agent '], + ['unicode homoglyph', 'agént'], + ])('rejects security-relevant input (%s)', (_label, deployment) => { + expect(() => validateAgentDeploymentString(deployment)).toThrow( + 'Deployment must start and end with an alphanumeric character', + ); + }); +}); diff --git a/packages/livekit-server-sdk/src/AgentDispatchClient.ts b/packages/livekit-server-sdk/src/AgentDispatchClient.ts index 5fc0698f..b2f7be5e 100644 --- a/packages/livekit-server-sdk/src/AgentDispatchClient.ts +++ b/packages/livekit-server-sdk/src/AgentDispatchClient.ts @@ -14,15 +14,35 @@ import { ServiceBase } from './ServiceBase.js'; import { type Rpc, TwirpRpc, livekitPackage } from './TwirpRPC.js'; interface CreateDispatchOptions { - // any custom data to send along with the job. - // note: this is different from room and participant metadata + /** any custom data to send along with the job. + * note: this is different from room and participant metadata + */ metadata?: string; - // controls whether the job should be restarted when it fails (cloud only) + /** controls whether the job should be restarted when it fails (cloud only) */ restartPolicy?: JobRestartPolicy; + /** optional deployment to dispatch to. Leave empty to target the production deployment. + * Deployment must start and end with an alphanumeric character and may contain -, _, and . in between. + */ + deployment?: string; } const svc = 'AgentDispatchService'; +/** @throws TypeError on invalid deployment names */ +export function validateAgentDeploymentString(deployment: string): void { + if (deployment.length > 63) { + throw new TypeError('Deployment string must not exceed 63 characters'); + } + if (deployment.length === 0) { + return undefined; + } + if (!/^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/.test(deployment)) { + throw new TypeError( + 'Deployment must start and end with an alphanumeric character and may contain -, _, and . in between.', + ); + } +} + /** * Client to access Agent APIs */ @@ -56,11 +76,15 @@ export class AgentDispatchClient extends ServiceBase { agentName: string, options?: CreateDispatchOptions, ): Promise { + if (options?.deployment) { + validateAgentDeploymentString(options.deployment); + } const req = new CreateAgentDispatchRequest({ room: roomName, agentName, metadata: options?.metadata, restartPolicy: options?.restartPolicy, + deployment: options?.deployment, }).toJson(); const data = await this.rpc.request( svc, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fc35857..194dc853 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,8 +250,8 @@ importers: specifier: ^1.10.1 version: 1.10.1 '@livekit/protocol': - specifier: ^1.46.3 - version: 1.46.3 + specifier: ^1.46.6 + version: 1.46.6 camelcase-keys: specifier: ^9.0.0 version: 9.1.3 @@ -824,6 +824,9 @@ packages: '@livekit/protocol@1.46.3': resolution: {integrity: sha512-YvsE4UN5i+wY9vXfwhF6EUrRyUm/YhiFU1jBcsmsLd/xodUJxYTBcWS4OgL4IJffjzIoyxsrbKp1h9qC55mtcQ==} + '@livekit/protocol@1.46.6': + resolution: {integrity: sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==} + '@livekit/rtc-ffi-bindings-darwin-arm64@0.12.60': resolution: {integrity: sha512-YHXqybkYfaTc3txJXXWoVogiSP3yKJdkaZlIlZ6IDMGnN9elUoHDYU+ZSn/rbdGu0pp4HUOzffXkbkItN735Bw==} engines: {node: '>= 18'} @@ -4265,6 +4268,10 @@ snapshots: dependencies: '@bufbuild/protobuf': 1.10.1 + '@livekit/protocol@1.46.6': + dependencies: + '@bufbuild/protobuf': 1.10.1 + '@livekit/rtc-ffi-bindings-darwin-arm64@0.12.60': optional: true From 80b5d172f4ba0813bebebee42857a41163fec8f2 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 10 Jun 2026 14:25:54 +0200 Subject: [PATCH 2/2] Create flat-monkeys-kick.md --- .changeset/flat-monkeys-kick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/flat-monkeys-kick.md diff --git a/.changeset/flat-monkeys-kick.md b/.changeset/flat-monkeys-kick.md new file mode 100644 index 00000000..aa585b3d --- /dev/null +++ b/.changeset/flat-monkeys-kick.md @@ -0,0 +1,5 @@ +--- +"livekit-server-sdk": patch +--- + +Add `deployment` to `CreateDispatchOptions`