Skip to main content
Everything on this page applies only to bots that publish or receive audio. The voice control plane is pure JavaScript and needs none of it.

Install the engine

1

Install at the exact pin

The exact version matters. See why the pin is exact.
2

Copy the patch out of the SDK

The patch file ships inside the SDK package for exactly this.
3

Apply it on every install

The constants VOICE_ENGINE_PIN and VOICE_ENGINE_PATCH_FILE are exported so your own setup scripts can read the version and filename rather than hardcoding them.

Why the patch is mandatory

The engine’s only per-participant key setter takes no key argument, at any published version, while its wire protocol marks that field required. Every call throws. Under per-participant encryption a publisher must install a key under its own identity, so an unpatched engine cannot publish audio anyone can decrypt. The SDK repo carries a one-line fix. Patches are repo-local: the engine installs into your node_modules unpatched, so a bot that publishes audio has to apply the same patch in its own app. The patch retires when upstream lands the fix.
npm install --ignore-scripts silently skips the patch. patch-package runs as a postinstall script. If your CI, Docker build, or lockfile-install step uses --ignore-scripts, the engine ends up unpatched and the bot publishes undecryptable audio with no error anywhere. Run npx patch-package explicitly in that case.

Why the pin is exact

A patch-package patch is named for one version. A floating range resolves to a version the patch does not name, patch-package skips it with a warning, and the bot publishes audio nobody can decrypt. The frame-key parameters the engine sends are also a cross-platform contract with the Cloak desktop, web, and mobile apps, and a minor version bump can change a default.

Host requirements

The Node floor is a crypto requirement, not a transport or voice one. The HPKE stack needs globalThis.crypto.getRandomValues, which Node 18 does not define in ESM. The default wire is a plain wss:// WebSocket over the pure-JS ws package, and installing the SDK itself compiles nothing. The voice engine ships prebuilt binaries, so it does not compile either.
The engine sits in optionalDependencies specifically because npm tolerates platform and install-script failures there. A musl host or a prebuild miss still gets a working text bot.

Optional executables

Neither is a dependency of correctness. Feeding the player raw PCM needs neither.

Checking the host

Two different questions, two different tools.
probeVoiceRuntime() never throws. A text-only bot must not be failed by an absent voice engine, so it is a diagnostic rather than a gate.
Probe before joining, and report probe.remedy to whoever invoked the command. It is written to be shown to a human.
Run the dependency report from a file, not from node -e. The -e form fabricates a crypto global that no module in the process can see, so the secure-random row would lie on exactly the hosts it exists to diagnose.
voiceEnvError(cause) is the same mapping the SDK uses internally: hand it a load failure and it returns a CloakEnvironmentError with a remedy naming what to change on the machine. It recognises old glibc, musl hosts, and a missing package.

The blind spot

The media engine surfaces no per-participant encryption failure state, so a bot cannot detect that nobody can decrypt it. Decryption failures and missing keys are dropped rather than reported. The one per-participant event that does exist reports that a participant’s frames are marked encrypted, which stays true through exactly the key and identity mismatches that matter. The failure tolerance is set to never give up, so a frame decryptor with no key retries quietly forever.There is no event to subscribe to and no counter to read. Silence is the only symptom.
Because of that, the SDK ships no voice health check and no retry-on-decrypt-failure path. Both would be theatre. Do not build one. The only defence is to be correct by construction, which is what VoiceMediaSession.attach() does: install exactly what voiceFrameKeyFor() returns, under the identity the SFU reported, before publishing, and fail closed on every precondition. The real acceptance test is a live call with a human on the other end.

Symptom to cause

Next

Publishing audio

The media session, the player, and the resource helpers.

Voice keys and scopes

The other half of every silence bug.

Voice reference

probeVoiceRuntime, voiceEnvError, and the voice constants.

Handling errors

CloakEnvironmentError and the rest of the error model.