Skip to main content
The SDK is configured from the environment, and nothing here is read at import: a variable is read when the thing that needs it is built or called, so a worker that never opens a lane never needs that lane’s settings. An explicit argument always wins over the environment, which is the rule to reach for whenever the value already lives somewhere better, such as a secret store your host resolved for you.

Everything the SDK reads

Every STANDIN_ variable, in one place, because configuring a deployment out of nine pages is how a deployment ends up half configured. Every variable above except STANDIN_SHOW_ROOTS is read by the TypeScript SDK under the same name and with the same meaning, so a deployment that runs both workers configures both the same way. Two of the names are exported as constants rather than written out, in both SDKs, so your code and the SDK cannot drift apart:

Three things that cost an afternoon

  • A blank STANDIN_CHAT_SECRET falls through here, and does not in TypeScript. ChatChannel reads the secret= argument, then STANDIN_CHAT_SECRET, then STANDIN_SECRET, and in Python an exported empty string is skipped like an unset one. The TypeScript twin stops at the first variable that is merely present, so export STANDIN_CHAT_SECRET= shadows a perfectly good STANDIN_SECRET there and the channel refuses to construct. Unset the variable rather than blanking it, and the two behave alike.
  • STANDIN_PORT is read with int(). A value that is not a number raises ValueError while CallServer is being built, and the message names the bad value rather than the variable it came from, so check a templated value before you export it. The TypeScript twin coerces instead and carries NaN as far as start().
  • A STANDIN_WS_PATH that reduces to / is refused with ws_path must be a real path such as /msteams/calling. A worker listening at the root answers anything that reaches it, so this fails at construction rather than on the first call.
The two roots variables are separate on purpose, and neither is a substitute for the other: STANDIN_SHOW_ROOTS governs what may be drawn onto the tile in a call, STANDIN_MEDIA_ROOTS governs what may be uploaded into a chat. Both default to empty, because the paths reaching them were chosen by a model that somebody is steering.

Reading your own

Writing a plugin means reading the same five things every time: a value that must be set, one that may be, a boolean, a check that a vendor host is really that vendor’s, and a header map. Use these rather than rolling your own, and the error somebody sees for a missing key reads the same whichever provider they picked. That is the whole reason the module exists: the messages had drifted once already, one wording per provider.
These five are not re-exported at the package root in Python: from standin import required is an ImportError. Import them from standin.config. The TypeScript twin differs on both counts: it exports required, optional, flag, vendorHost and jsonObject from the package root, in camel case. Knowing that here is cheaper than discovering it as a failed import while porting a plugin.
Every failure is a StandInError, which is at the package root: from standin import StandInError.
purpose completes the sentence “X is required to …”, so write it as a verb phrase. The point is that the reader learns what they were trying to do, not only which string was empty.

Why a flag accepts only true

flag trims and lowercases the value and compares it to the single string true. TRUE and True are therefore true, and 1, yes, on and y are all false. That is deliberately strict, and the direction matters more than the strictness. The settings worth a flag in this SDK are guards, and a guard must not be turned off by a typo. If somebody writes yes where true was meant, the value reads as false: the guard stays in whatever state the default gives it rather than being switched by a string nobody validated. An unset variable is not a typo, so it returns default untouched.

Why a vendor host is pinned

Your API key travels to whatever host the configuration names. A mistyped or injected host is therefore not a failed call, it is credential exfiltration, which is why this raises rather than warns: a warning gets logged after the key has already left. The host is accepted when it equals the suffix with its leading dot stripped, or when it ends with the suffix exactly as you passed it.
Pass the suffix with its leading dot. ".acme.com" accepts api.acme.com and rejects evilacme.com. "acme.com" accepts both, because evilacme.com ends with it, and that is a guard that reads as present and is not.

Why a header map is never logged

json_object parses a JSON object and coerces every key and value to str. Anything that is not a JSON object, including a valid JSON array or number, raises the same message. That message names the variable and stops. It never echoes the value, and neither does anything else on the failure path, because this helper exists for header maps: the value carries your credentials to somebody else’s endpoint. A parse error that pretty-prints the malformed value is a bearer token in a log file, on the one code path most likely to be run with debug logging turned up.

A missing extra is not a broken install

One package holds the SDK and every plugin, so import standin sits above code that references frameworks you may never install. Nothing under standin.plugins imports its framework while loading, and import standin imports no plugin at all. When you do touch one whose framework is absent, you get this rather than a bare traceback from inside somebody else’s package:
It carries plugin, module and extra as attributes, and it subclasses both StandInError and ImportError. The ImportError half is the one worth recording: except ImportError, importlib probes and pytest.importorskip all treat a missing extra as a missing import, which is exactly what it is, so a plugin that is absent degrades the same way any optional import does rather than escaping as an unfamiliar error class. It is also raised only when the framework root is what went missing. A framework that is installed but broken raises its own ModuleNotFoundError for its own dependency, and that propagates untouched: telling somebody to run pip install "standin-sdk[livekit]" when LiveKit is already installed sends them to fix something that is not broken. The real extras, from the package manifest: tile and render are not plugins, so a missing one does not raise PluginNotInstalled. jpeg_encoder() returns None after logging a line that names the extra, and the tile relay simply does not run while audio is unaffected; rendering a PDF page without the extra raises StandInError with showing a PDF needs the render extra, followed by that install line. Both are optional because most deployments never put their own video on the tile and never show a file, and a PDF engine in every install is the wrong trade. The SDK requires Python 3.10 or newer.

Version

One string, single-sourced from the package itself and read by the build, so the installed standin-sdk distribution and the imported module can never disagree about which version you are running. Quote it in a bug report: the wire protocol is versioned separately, and knowing both is what separates “your worker is old” from “the message was wrong”. The TypeScript twin exports its own as VERSION.

Next

CallServer

The listener most of these variables configure.

Checking the install

Prove the configuration is right before a real call does.