Repository navigation
Description
Problem
KalshiClient.from_env (and the async mirror) overrides _auth_owned based on whether kwargs["auth"] is truthy after setdefault, not based on who actually built the auth. This breaks the #210 contract that caller-supplied KalshiAuth is never closed by the SDK, and simultaneously leaks the sign ThreadPoolExecutor when env vars are empty and the caller passes only key_id + private_key.
Two concrete triggers:
- Case B (executor leak):
KalshiClient.from_env(key_id='K', private_key=PEM)with no env vars set.try_from_env()returnsNone, sokwargs["auth"]=None.__init__builds auth fromkey_id/private_keyand correctly sets_auth_owned=True.from_envthen overwrites it:(None is not None) == False. The SDK now owns auth thatclose()will never shut down, leaking twokalshi-sign-*threads per client across long-running pytest sessions, FastAPI lifespans, and worker pools. - Case C (closes caller-owned auth): env vars set and caller passes
from_env(auth=my_auth).setdefaultkeepsmy_auth;__init__records_auth_owned=Falseper Auth: KalshiClient.close() shouldn't shut down externally-provided KalshiAuth executor #210.from_envthen flips it toTrue.client.close()callsmy_auth.close(), and every subsequentsign_request_asyncon the caller's still-referenced auth raisesRuntimeError("KalshiAuth has been closed")(auth.py:317).
Evidence
kalshi/client.py:182-188—from_envbuilds kwargs then forcibly recomputes ownership from the input kwarg:
kwargs.setdefault("auth", KalshiAuth.try_from_env())
kwargs.setdefault("demo", os.environ.get("KALSHI_DEMO", "").lower() == "true")
kwargs.setdefault("base_url", os.environ.get("KALSHI_API_BASE_URL"))
client = cls(**kwargs)
# from_env constructs auth locally — caller never owned it.
client._auth_owned = kwargs["auth"] is not None
return clientkalshi/client.py:96-107—__init__already encodes the correct invariant (_auth_owned = (auth is None)) and may construct auth fromkey_id/private_keyarguments. The override at :187 ignores that branch entirely.kalshi/async_client.py:214-220— identical bug inAsyncKalshiClient.from_env.kalshi/client.py:193-194,kalshi/async_client.py:225-226—close()gatesauth.close()on_auth_owned, so case B silently skips it and case C silently runs it on caller-owned auth.
Suggested fix
Recompute ownership from what __init__ actually did, instead of from the input kwarg:
client = cls(**kwargs)
client._auth_owned = (kwargs.get("auth") is None) and (client._auth is not None)
return clientEquivalently, have from_env build auth itself and pass it via the explicit auth= kwarg, dropping the override entirely so the __init__ invariant _auth_owned = (auth is None) carries through. Mirror the fix in both kalshi/client.py and kalshi/async_client.py. Add regression tests covering both case B (executor not leaked when constructed from key_id/private_key) and case C (caller-supplied auth= survives client.close()).
Source
Round-3 independent audit (reviewer: codex+security_auth).
Activity
fix(auth): from_env preserves caller-owned KalshiAuth + lazy env eval (…
release: 2.7.0 — post-v2.6 independent multi-LLM reviewer SDK audit c…