v1.5.3
Closes the audit. The remaining lead turned out to hide a worse defect than the
one it described.
If you set REQUIRE_AUTH_STATE_ENCRYPTION=true, upgrade. It did not do what
it says.
Security
-
REQUIRE_AUTH_STATE_ENCRYPTION=trueloaded plaintext auth state. The
setting was enforced only at construction — "is a key configured?" — and never
on the read path, so a plaintext state file loaded happily and its cookies went
straight into a live browser context while the deployment believed encryption
was mandatory. Measured against the real code:inspect().encrypted : False encryption_required : True prepare_for_context : ACCEPTED, cookie value = 'SUPER-SECRET'prepare_for_contextnow refuses unencrypted state when encryption is
required (#137). -
Auth-state encryption was classified by filename, not content.
inspect()
decidedencryptedfrompath.name.endswith(".enc"), so the classification
was wrong in both directions. An encrypted envelope named without.encwas
not leaked as plaintext — it is ciphertext — but it was handed to Playwright
verbatim as if the envelope were storage state, so no cookies loaded and the
agent proceeded believing the auth profile had been applied. A silent auth
failure rather than an exposure. Content now decides for a file that exists;
the suffix remains only as the hint for a path that does not exist yet, and an
unclassifiable file falls back rather than guessing (#137).
Fixed
/readyzcould hang for a minute while holding the global browser lock.
It calledensure_browser(), which retriesCONNECT_RETRIES(default 60)
times a second apart while holding_browser_lock, so with browser-node down
every concurrent probe and everycreate_sessionqueued behind it — the API
looked dead rather than reporting "not ready", which is the opposite of a
readiness probe's purpose. Now bounded at 5 seconds (#138).- Isolated sessions leaked their entire profile tree.
_release_syncremoved
the container but never<data>/browser-sessions/<id>, which holds a full
Chromium profile — tens to hundreds of megabytes per session — and the
maintenance sweep covers only the artifact, upload and auth roots. Unbounded
growth with no signal until the volume filled mid-session. Removal is refused
for any path resolving outside the managed runtime root (#138). - browser-node's healthcheck watched the wrong service. It probed noVNC
:6080, not the Playwright server:9223the controller actually depends on,
so a wedged or dead Chromium reported healthy for as long as websockify
survived — and the controller'sdepends_on: service_healthygate passed
anyway (#137). - No restart policy on controller or browser-node, so a controller OOM left
the container stopped forever with nothing recovering. Added, with a 60s
stop_grace_period: sessions close sequentially and an isolated one can take
~10s, so the 10s default SIGKILLed cleanup mid-flight and orphaned containers
and tunnels (#137).
Changed
controller/pyproject.tomlraised torequires-python = ">=3.11"(with mypy
and rufftarget-versionto match) — the last instance of the unbacked-floor
pattern. Its ruff target also disagreed with the rootruff.toml(py310 vs
py311), which meant the two byte-identical stdio-bridge copies were formatted
under different targets and could have drifted apart silently (#137).
CI
- The Python floor is now an invariant, not a repeated correction. This
pattern shipped two bugs — 3.14 advertised and untested in langchain, 3.10 in
the client and mcp packages. A test parses the CI matrix and fails if any
package declares a Python version CI does not run, or ships a classifier below
its own floor (#137).