Repository navigation
v1.10.2
A security release over 1.10.1, with three advisories: GHSA-3pwm-j9pq-4r6q (PII scrubbing,
traces and fork files), GHSA-m596-6q9j-x6mc (named credentials: artifacts and proxy personas)
and GHSA-hj9r-c8jj-mf9v (mesh peer registration). Update the controller image. The client,
LangChain and MCP bridge packages carry one bridge fix; updating them alone does not deploy the
controller fixes.
Upgrade notes:
- Playwright tracing is off by default, unless your own settings turn it on. The
.env.exampleof earlier releases setENABLE_TRACING=true, and an older compose file falls back
totrue: if yours do, setENABLE_TRACING=false, then recreate the controller container
(docker compose up -d;docker compose restartkeeps the old environment) or restart the
process. A trace is not scrubbed. Earlier releases recorded one for every session, so delete the
trace.zipfiles underARTIFACT_ROOT, thefork_*files underAUTH_ROOT, and the built-in
agent loop'sagent_steps.jsonlartifacts (which hold the request URLs it saw), with any copies.
Rotate any credential those files may have held. - Network log: query strings, form bodies and JSON bodies are now stored redacted, so a network
hook registered on a query value no longer fires, and some ordinary parameter names are redacted by
their name.docs/deployment.mdlists both. - Named credentials (
API_BEARER_TOKENS): proxy personas saved before this release have no owner
and stay open to every operator until you add"owner": "<operator id>"to each entry in
PROXY_PERSONA_FILE; the startup warning names them. Check each persona's server and username
against your own records first, since another operator could have changed them. Releases 1.8.1 to
1.9.1 also returned a persona's stored password to any operator: rotate those passwords.
/artifactsno longer serves a directory whose session record is gone, or a file placed directly
in the artifact root. - Mesh (
MESH_ENABLED=true):POSTandDELETE /mesh/peersare removed. Manage peers in the
fileMESH_PEERS_PATHnames, and review it for entries you did not add and for keys that differ
from what each peer's/mesh/identityreports. One invalid entry now turns every peer away: each
node_idmust be the SHA-256 of its key, so an entry registered under a name of your choosing,
which earlier releases accepted, has to be re-registered under its key's hash (docs/mesh.md). - Credential files are written
0600. A backup job or second container that reads them as
another user must now run as the controller's user.
Added
PII_SCRUB_SCREENSHOT_STRICT(defaultfalse). With screenshot scrubbing on, a screenshot that
cannot be scrubbed (no OCR to drive it: tesseract missing orOCR_ENABLED=false; or a redaction that
fails) used to be returned unscrubbed with a warning. In strict mode the image is deleted and the call
fails withscreenshot_withheld(HTTP 503); with OCR unavailable,/readinessalso reports
screenshot_scrubbingasfailinstead ofwarn. An action that already ran is still reported and
recorded (action log, audit event, witness receipt), withobservation_errorin place of the image.
Changed
- The harness's programmatic verifier tests every
url_matchespattern in one child process. It
started one Python process per pattern (about 180 ms each on Windows). A verification now starts one,
and another only after a pattern that outlasts the timeout; each stuck pattern still times out on
its own and the others keep their verdicts. - The stealth fingerprint layer is canvas noise only, and says so. Its WebGL1
getParameter
wrapper returned the real value for every parameter, so it changed nothing except the function's
toString(); it and its unused seed are removed. WebGL values are reported as the browser has them.
Removed
POST /mesh/peersandDELETE /mesh/peers/{node_id}; mesh peers are managed in the peers
file./mesh/receivedoes not use the bearer token, so registering a peer grants it access
to the tool gateway, and any API credential could register one (with any key and grants,
replacing an existing peer's entry too) and keep it after the credential was revoked. Only
mesh users (MESH_ENABLED=true) are affected. Migrating: the fileMESH_PEERS_PATH
names (default/data/mesh/peers.json) already holds every peer registered over HTTP, so
existing peers keep working. Review it, since anyone with an API token could add entries
before this release, then add, change and remove peers by editing it; the controller
re-reads it when it changes.GET /mesh/peersandGET /mesh/identitystay, and the
dashboard's mesh list no longer has a Remove button. See the newdocs/mesh.mdfor the file
format. (GHSA-hj9r-c8jj-mf9v)
Fixed
- The stdio MCP bridge no longer advertises
resources.subscribe. The bridge only POSTs and never
opens the controller's event stream, so a stdio client that subscribed would never receive
notifications/resources/updated.initializenow reportsresources.subscribe: false, and
resources/subscribe/resources/unsubscribeanswer JSON-RPC-32601. The HTTP transport is
unchanged. Both bridge copies stay byte-identical. GET /sessionsstill lists closed sessions when Redis is empty or unreachable. Reading one
session already fell back to the file record, but the listing asked only Redis, so a Redis restart
without persistence hid every closed session and a dropped connection turned the listing into an
error. The listing now merges the file records with Redis (Redis wins for an id in both) and falls
back to the file records, with a warning, if Redis fails.- A cancelled session create no longer leaves its isolated tunnel behind. Cancelling while
autossh was starting, or while waiting for the tunnel lock after the startup grace period, left
the log file open or autossh running and its remote port reserved. Both now stop the process and
mark the tunnel as errored. A cancelled release still stops autossh, frees the port and closes
the log before the cancel goes through; it used to be swallowed while release waited for the
tunnel's heartbeat task. - More session routes answer a bad request or a refusal as itself, not as a 500.
GET /sessions/{id}/downloads,/tabs,/audit,/witness,/witness/verifyand/witness/bundle,
POST /sessions/{id}/takeover,POST /sessions/{id}/storage-state,DELETE /sessions/{id}and
POST /sessions/{id}/forknow use the same mapping as the other session routes: a refused
request is 403, an invalid one is 400, and a failure the route does not expect is logged and
answers 500 "Internal error". AKeyErrorthat is not an unknown session still answers 404 but is
now logged as a warning, so a bug that surfaces as "Not found" can be traced. STEALTH_PROFILEis read through settings.STEALTH_ENABLEDcame from.env, but the
profile was read straight from the process environment, so a run outside Docker with both set in
.envkept the fingerprint profile off.STEALTH_PROFILE(off,lightoraggressive; any
other value is rejected at startup) is now a setting, and the composeenvironment:block passes
it next toSTEALTH_ENABLED.docker compose upno longer stalls on an unhealthybrowser-nodeaftercp .env.example .env.
Compose filled${BROWSER_WS_ENDPOINT_FILE}in thebrowser-nodehealthcheck from the project
.env, where.env.exampleset the controller's path (/data/browser-profile/...). That path does
not exist insidebrowser-node(its profile is at/data/profile), so the check failed on every
run and the controller, which waits for a healthybrowser-node, never started. The healthcheck
now expands the variable inside the container, and.env.exampleships the line commented out
because compose sets the path per service.CURATOR_ENABLEDno longer sends a placeholder transcript to the model. With the setting on
and a provider key present, every closed session started a background review that sent the model
a fixed placeholder string instead of a transcript, and could write what came back to
skills-staging. Nothing builds a session transcript yet, so the review is not started at all:
startup logs that the setting is reserved, and.env.examplesays so. The adapter setup and the
setting itself are unchanged.- A screenshot whose redaction fails is reported as failed, not as redacted. When Pillow could
not process the image,scrub_screenshotreturned the original bytes with the matches attached,
the caller auditedpii_redactionasok, and the result carried no sign of it. The function now
raises, the result carriesscreenshot_pii_scrub: {"applied": false, "reason": "redaction_failed"}
(the field used when OCR is unavailable), and the audit event has statusfailed. - A failing disk no longer hangs a download capture. Reserving a file name for a captured
download retried forever on any error other than "name already exists" (disk or quota full,
read-only or mis-owned volume), blocking the event loop. It now tries a generated name once,
tries at most 20 names, and raises the underlying error, which the background download handler
logs.
Security
- Network inspector: URL-encoded form bodies, JSON string escapes and URL query strings are now
scrubbed, and a body the scrubber cannot process is withheld instead of stored. With network
capture on (the default),PII_SCRUB_NETWORKhad matched raw text only, so these were stored
verbatim in the per-session network log (GHSA-3pwm-j9pq-4r6q). Network hooks match the stored
URL, which is now redacted, so a hook registered on a query value (for example
*email=ops%40example.com) no longer fires; one on the path or a parameter name still does.
A JSON member whose name marks a secret (pass,db_passandpage_tokendo; a paging cursor such as
next_page_tokendoes not) is redacted whether or not the body has escapes, with the default
PII_SCRUB_REPLACEMENTa redaction in JSON keeps the body valid (the exceptions: aCookie:line inside a string, a private-key block with noEND
line, and a value that is not a string, such astrueor12345678, under a name the password
pattern knows, such as"pass": true), and the fragment of a URL,RefererorLocationis scrubbed like its query. The URL of a
failed request, whichget_request_failuresand every observation returned as the page asked for it,
is scrubbed the same way. Playwright traces still record URLs, headers, bodies and typed text as they
are, and are now off by default (below).
docs/deployment.mdlists what the inspector still stores as it is. - Playwright tracing is now off by default (
ENABLE_TRACING=falsein the settings,.env.exampleand
the compose file). A session'strace.zipholds every request header (AuthorizationandCookie
included), the request and response bodies, and the text typed into the page, passwords included,
none of it scrubbed, and every session recorded one, written when tracing stopped or the session
closed. SetENABLE_TRACING=trueto record traces again; traces written before this release stay
on disk underARTIFACT_ROOTuntil you delete them (GHSA-3pwm-j9pq-4r6q). fork_sessionno longer leaves the source session's cookies on disk. It exported them to
AUTH_ROOT/<session_id>/fork_<id>.json(always plaintext through 1.3.0; from 1.3.1 plaintext unless
AUTH_STATE_ENCRYPTION_KEYis set) for the
new session to load, and never deleted the file; the file is now removed as soon as the new session
has loaded it or failed to. Files left by earlier forks stay until you delete them
(GHSA-3pwm-j9pq-4r6q)./artifactschecks the owner of the file a path resolves to. The mount took the session id
from the first component of the path as typed, so a path that starts with..and comes back
into the artifact root was judged as a session nobody owns and could be served to another
operator. The session is now the first component of the resolved file, so every spelling, and
a link from one session directory into another, is judged by the owner of what is actually
served. With named credentials (API_BEARER_TOKENS), a path whose first component is not a
session with a stored record (a directory whose record is gone, a file dropped into the
artifact root) is not served to anyone; deployments without named credentials are unchanged. (GHSA-m596-6q9j-x6mc)- Proxy personas belong to the operator who saved them. With named credentials, any
operator could list, read the server and username of, overwrite or delete another operator's
persona, and open a session through it with that operator's stored proxy password. The same
rule as memory profiles now applies over HTTP and MCP: a persona records the token-verified
operator who first saved it (an"owner"key inPROXY_PERSONA_FILE), and to every other
caller it is missing; overwriting its name answers 409. Upgrading: personas that were
saved before this release have no owner and stay open to every operator, exactly as before
(shared-token deployments are unaffected). The controller logs a warning at startup when
named credentials are set and such personas exist, naming how many there are, and a second one
for any entry whose"owner"is an empty string, which counts as no owner. Until an owner is
added, any operator can overwrite or delete the persona too, not only use it. To make one
private, add"owner": "<operator id>"to its entry in the file. A save by a release before this one
drops the"owner"keys, so do not roll back and save. (GHSA-m596-6q9j-x6mc) - Revoking a mesh peer by editing the peers file works reliably. Deleting the file, or
saving one that did not parse, left every peer in memory, and the controller rewrote the file
each time it recordedlast_seen, which could put a removed peer back or overwrite an edit made
at the same moment. A missing file is now an empty registry; a file that cannot be read, is not
valid JSON or has any invalid entry accepts no peer at all (the whole file is refused) and
logs amesh.peers:warning;last_seenis kept in memory and the controller never writes the
file; and the file is checked on every request, so an edit is seen by the next one. Replace the
file by rename, and in Docker mount its directory rather than the file;docs/mesh.mdlists
the changes that are not noticed. A request already past the sender check is not cancelled.
(GHSA-hj9r-c8jj-mf9v) - The peers file refuses a key its
node_iddoes not name. Any API credential could post a key
of its own under an existingnode_id, which kept the id and grants an operator would recognise.
Anode_idis the SHA-256 of the peer's 32-byte Ed25519 key, as/mesh/identityreports both;
a file with an entry whose key is not that (or is not strict base64, or is filed under another
node_id) is now refused as a whole and logged. Put back the key the peer's own
/mesh/identityreports. Earlier releases accepted anynode_ida sender signed as, so an entry
registered under a name of its own must be re-registered under its key's hash.add()checks the
same. (GHSA-hj9r-c8jj-mf9v) - Mesh envelopes with a NaN timestamp are refused. The timestamp window was written so a NaN
passed it. A correctly signed envelope from a registered peer carrying one was accepted at any
time. It now counts as outside the window, for requests and for replies. (GHSA-hj9r-c8jj-mf9v) - Files that hold credentials are written owner-only. The stores wrote every file as
0666 & ~umask, so a0600file became0644the next time it was saved, and a new
credentials file was created0644under the default umask.atomic_write_textnow keeps the
mode a file already has and takes an explicitmode=; the proxy persona file, the cron store
(webhook keys), encrypted auth state, session records, approvals, agent jobs and the MCP
session store ask for0600. A file written by an earlier release is tightened the next time
it is saved. A backup job or second container that reads these files from the data volume as
a different user must now run as the controller's user (orchmodafter each save).