From 8a9d4d5348a3571d224ecb669626abee41571f1d Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 10 Jun 2026 14:24:57 +0200 Subject: [PATCH 1/4] 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/4] 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` From 502ed86b5495a7279ce5ba381e4963b9acc7146f Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 18 Jun 2026 09:25:45 -0400 Subject: [PATCH 3/4] feat: remove client-side deployment string validation Drop validateAgentDeploymentString and its call site in createDispatch, along with its tests, per review feedback. Validation, if needed, is better handled in upstream systems and surfaced over the wire. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/AgentDispatchClient.test.ts | 82 ------------------- .../src/AgentDispatchClient.ts | 18 ---- 2 files changed, 100 deletions(-) delete mode 100644 packages/livekit-server-sdk/src/AgentDispatchClient.test.ts diff --git a/packages/livekit-server-sdk/src/AgentDispatchClient.test.ts b/packages/livekit-server-sdk/src/AgentDispatchClient.test.ts deleted file mode 100644 index 219a4715..00000000 --- a/packages/livekit-server-sdk/src/AgentDispatchClient.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -// 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 b2f7be5e..36d4e88a 100644 --- a/packages/livekit-server-sdk/src/AgentDispatchClient.ts +++ b/packages/livekit-server-sdk/src/AgentDispatchClient.ts @@ -28,21 +28,6 @@ interface CreateDispatchOptions { 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 */ @@ -76,9 +61,6 @@ export class AgentDispatchClient extends ServiceBase { agentName: string, options?: CreateDispatchOptions, ): Promise { - if (options?.deployment) { - validateAgentDeploymentString(options.deployment); - } const req = new CreateAgentDispatchRequest({ room: roomName, agentName, From 2f12c42dd90f9f256b309c0241b02bb556668f13 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 18 Jun 2026 09:28:29 -0400 Subject: [PATCH 4/4] fix: revert docs update --- packages/livekit-server-sdk/src/AgentDispatchClient.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/livekit-server-sdk/src/AgentDispatchClient.ts b/packages/livekit-server-sdk/src/AgentDispatchClient.ts index 36d4e88a..d97bc486 100644 --- a/packages/livekit-server-sdk/src/AgentDispatchClient.ts +++ b/packages/livekit-server-sdk/src/AgentDispatchClient.ts @@ -20,9 +20,7 @@ interface CreateDispatchOptions { metadata?: string; /** 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. - */ + /** optional deployment to dispatch to. Leave empty to target the production deployment. */ deployment?: string; }