Repository navigation
Description
Context
Surfaced by the multi-LLM review of PR #403 (perps/margin API). KalshiAuth fully supports passphrase-encrypted PEM keys (from_key_path/from_pem/from_env/try_from_env all take password=), but the high-level clients do not thread a password through their explicit-credential kwarg path, so a user with an encrypted PEM cannot use the standard key_id/private_key(_path) constructor or from_env(password=...).
Problem
The explicit-credential path drops password on the floor in both perps and the core client:
kalshi/perps/client.py:91—KalshiAuth.from_key_path(key_id, private_key_path)(nopassword=)kalshi/perps/client.py:94—KalshiAuth.from_pem(key_id, private_key)(nopassword=)kalshi/perps/async_client.py:82/:85— same two calls, nopassword=kalshi/perps/client.py:30-47(PerpsClientInitKwargs) and:67-79(__init__) — nopasswordfield/param; dittokalshi/perps/async_client.py:30-42/:58-70kalshi/perps/client.py:171/kalshi/perps/async_client.py:155—from_envcallstry_perps_auth_from_env()with no args, so an explicitfrom_env(password=...)cannot be honored (andpasswordisn't in the kwargs TypedDict anyway)
This is not a perps regression — the core client has the identical limitation: kalshi/client.py:107/:110 call from_key_path/from_pem with no password=, ClientInitKwargs (kalshi/client.py:36-53) has no password field, and from_env (kalshi/client.py:198) calls KalshiAuth.try_from_env() with no args. So the right framing is a shared enhancement, with a perps parity guarantee.
Concrete failure: a user with an encrypted PEM who calls PerpsClient(key_id=..., private_key_path="~/.kalshi/perps.pem") (or the core KalshiClient equivalent) gets KalshiAuthError: Passphrase-protected private key requires a password... (raised at kalshi/auth.py:149). The only workaround is to build KalshiAuth.from_key_path(..., password=...) themselves and pass auth=.
Note the env-var route is already covered: try_perps_auth_from_env reads KALSHI_PERPS_PRIVATE_KEY_PASSPHRASE (kalshi/perps/_env.py:42-46) and core try_from_env reads KALSHI_PRIVATE_KEY_PASSPHRASE (kalshi/auth.py:237-239). Only the explicit-kwarg path is the gap.
Proposed fix
Add a password: bytes | str | Callable[[], bytes | str] | None = None parameter to both __init__s and forward it to the auth constructors, plus add it to the *InitKwargs TypedDicts and thread it through from_env. Minimal, symmetric change across four files:
- Core:
kalshi/client.py+kalshi/async_client.py- Add
passwordto__init__signature andClientInitKwargs. from_key_path(key_id, private_key_path, password=password)/from_pem(key_id, private_key, password=password).- In
from_env, passKalshiAuth.try_from_env(password=kwargs.pop("password", None))(env passphrase still used when kwarg is None —try_from_envalready implements that precedence).
- Add
- Perps:
kalshi/perps/client.py+kalshi/perps/async_client.py- Same
__init__/PerpsClientInitKwargsadditions. - In
from_env, calltry_perps_auth_from_env(password=kwargs.pop("password", None)).
- Same
Keep the auth is not None branch untouched (caller-built auth already carries its own key).
Acceptance criteria
-
PerpsClient/AsyncPerpsClientandKalshiClient/AsyncKalshiClient__init__acceptpassword=and forward it tofrom_key_path/from_pem. -
passwordis added toClientInitKwargsandPerpsClientInitKwargs(somypy --strictacceptsfrom_env(password=...)and rejects typos). -
from_env(password=...)is honored on all four clients; when omitted, the existingKALSHI[_PERPS]_PRIVATE_KEY_PASSPHRASEenv var still works and an explicit kwarg overrides the env var. - Regression test: construct each client with an encrypted PEM fixture +
password=(kwarg path) and assertis_authenticated/ a signed request succeeds (reuse the_encrypted_pemhelper already intests/test_auth.py). - Regression test:
from_env(password=...)with an encrypted PEM inKALSHI_PERPS_PRIVATE_KEY/KALSHI_PRIVATE_KEYbuilds an authenticated client; explicitpassword=beats the*_PASSPHRASEenv var (mirrortest_from_env_explicit_password_beats_env_passphrase). -
uv run mypy kalshi/passes (note: usebytes | str | Callable[[], bytes | str] | Noneto match theKalshiAuthsignature exactly).
Notes
- Deferred from the PR perps: full Perps (margin) API — REST + WebSocket + SCM/Klear (#387) #403 fix stack because it is a pre-existing limitation shared with the core client, not a perps-specific defect introduced by the branch — the perps env-var passphrase path already works, so encrypted-key users are not fully blocked (workaround: build
KalshiAuthand passauth=). - Scope decision to make in the issue: ship as a single shared enhancement touching all four clients (recommended, keeps perps at parity with core), or split core vs. perps. Doing only perps would leave the core client inconsistent.
- Related: perps EPIC perps: [EPIC] Perps (margin) API — full SDK implementation tracker #387.
password-as-callable is already supported byKalshiAuth(vault-deferred secrets), so the new kwarg should accept the same union for consistency. breakinglabel intentionally omitted — this is a purely additive optional kwarg.
Activity