Summary
locus.memory.backends.OracleBackend (and OracleSyncBackend) accepts a wallet_password argument that's converted to pydantic.SecretStr. The pool builder then uses a truthy guard to decide whether to forward it to oracledb.create_pool_async:
# locus/memory/backends/oracle.py — inside _get_pool's _build()
if self.config.wallet_password:
params["wallet_password"] = self.config.wallet_password.get_secret_value()
In Pydantic v2, SecretStr("") is falsy (bool(SecretStr("")) == False). So an explicit empty-string wallet password — the documented python-oracledb idiom for auto-login wallets (cwallet.sso) — is silently dropped. The kwarg never reaches oracledb.create_pool_async, and the driver falls through to the encrypted ewallet.pem path, which on a TTY prompts for a PEM passphrase and on a non-TTY fails with:
OSError: [Errno 22] Invalid argument
File "src/oracledb/impl/thin/transport.pyx", line 171, in oracledb.thin_impl.Transport.create_ssl_context
This shows up at connection acquisition time as:
oracledb.exceptions.OperationalError:
DPY-6005: cannot connect to database
[Errno 22] Invalid argument
Reproduction
import asyncio
from locus.memory.backends import OracleBackend
async def main():
b = OracleBackend(
dsn="mydb_high",
user="MYAPP",
password="<real password>",
wallet_location="/path/to/auto-login/wallet",
wallet_password="", # auto-login (cwallet.sso)
table_name="my_table",
)
pool = await b._get_pool() # pool object created OK (lazy)
async with pool.acquire() as c: # ← OSError: [Errno 22]
...
asyncio.run(main())
A direct oracledb.create_pool_async(..., wallet_password="") with the same wallet and credentials connects and serves queries fine, confirming the regression is in Locus's guard, not in oracledb or in the wallet.
Diagnostic probe
Monkey-patching oracledb.create_pool_async to print the kwargs Locus actually sends:
>> oracledb.create_pool_async kwargs:
config_dir = '…/wallet'
dsn = 'mydb_high'
wallet_location = '…/wallet'
user = 'MYAPP'
password = '…'
# ← no `wallet_password` key, despite the caller passing ""
Suggested fix
Change the guard from truthy to "explicitly None":
# locus/memory/backends/oracle.py
- if self.config.wallet_password:
+ if self.config.wallet_password is not None:
params["wallet_password"] = self.config.wallet_password.get_secret_value()
This preserves "if the user didn't pass anything, don't add the kwarg" while honoring an explicit "" for auto-login wallets.
Same pattern should be audited anywhere else in Locus that stores SecretStr and gates with if self.foo: — Pydantic v2 changed SecretStr("") falsiness from previous behaviour, so older code that worked under v1 may quietly drop empty secrets.
Impact
- Triggers when: the application uses an auto-login wallet (
cwallet.sso) and follows python-oracledb's documented wallet_password="" idiom.
- Doesn't trigger when: the wallet is encrypted and a real (non-empty) password is supplied — that's why production deployments that pull a real wallet password from a vault won't see this; only deployments that depend on auto-login do.
Environment
locus-sdk 0.2.0b24
python-oracledb 2.4+ (thin mode)
pydantic v2
- Python 3.13
- Oracle Autonomous Database 26ai (free-tier), wallet-based mTLS
Related
- We file a sibling issue against
modelcontextprotocol/python-sdk (#2727) for a different bug surfaced while debugging this stack — unrelated mechanism, separate fix.
Summary
locus.memory.backends.OracleBackend(andOracleSyncBackend) accepts awallet_passwordargument that's converted topydantic.SecretStr. The pool builder then uses a truthy guard to decide whether to forward it tooracledb.create_pool_async:In Pydantic v2,
SecretStr("")is falsy (bool(SecretStr("")) == False). So an explicit empty-string wallet password — the documentedpython-oracledbidiom for auto-login wallets (cwallet.sso) — is silently dropped. The kwarg never reachesoracledb.create_pool_async, and the driver falls through to the encryptedewallet.pempath, which on a TTY prompts for a PEM passphrase and on a non-TTY fails with:This shows up at connection acquisition time as:
Reproduction
A direct
oracledb.create_pool_async(..., wallet_password="")with the same wallet and credentials connects and serves queries fine, confirming the regression is in Locus's guard, not inoracledbor in the wallet.Diagnostic probe
Monkey-patching
oracledb.create_pool_asyncto print the kwargs Locus actually sends:Suggested fix
Change the guard from truthy to "explicitly None":
This preserves "if the user didn't pass anything, don't add the kwarg" while honoring an explicit
""for auto-login wallets.Same pattern should be audited anywhere else in Locus that stores
SecretStrand gates withif self.foo:— Pydantic v2 changedSecretStr("")falsiness from previous behaviour, so older code that worked under v1 may quietly drop empty secrets.Impact
cwallet.sso) and followspython-oracledb's documentedwallet_password=""idiom.Environment
locus-sdk0.2.0b24python-oracledb2.4+ (thin mode)pydanticv2Related
modelcontextprotocol/python-sdk(#2727) for a different bug surfaced while debugging this stack — unrelated mechanism, separate fix.