Toolport v1.15.0
Security
-
Release job no longer inherits Azure Trusted Signing credentials during
frontend install. They are now step-scoped to the Windows tauri build,
matching TAURI and APPLE. (SBS-925) -
Downstream stderr drain no longer grows without bound on a newline-less write.
Stdout was already capped at 16 MiB per line; stderr still used unbounded
read_lineand only trimmed the kept tail afterwards. A hostile or buggy
stdio server that wrote a multi-GB chunk with no newline could OOM the
gateway and take every HTTP-bridge client with it. The drain now uses the
sametake(MAX_RESPONSE_BYTES)bound as stdout and stops on an unterminated
full-cap line. (SBS-930)
Added
-
Arch Linux package:
toolport-bin(paru -S toolport-bin, or
omarchy pkg aur add toolport-bin, once AUR account registration reopens
upstream; until thenscripts/render-aur.sh <version> ./aur && cd aur && makepkg -sibuilds the identical package with no AUR account). The AppImage
bundles Ubuntu 22.04's
libwebkit2gtk-4.1, which has noWebKitGPUProcessand cannot initialise EGL
against a current Mesa, so on a rolling release the window opens grey and empty
whileWebKitWebProcessaborts every launch. NoWEBKIT_*variable avoids it.
The AUR package repackages the official.debpayload against the host
WebKitGTK, the same thing the.debalready does on Debian/Ubuntu. Published
by a newaur.ymlworkflow that build-tests the PKGBUILD in an Arch container
before pushing.scripts/install.shnow routes Arch users there. The fat
AppImage is unchanged and stays correct on Ubuntu/Debian. -
Agent activity: see what your agents do outside Toolport. Toolport routes every
MCP call, so it sees those; it has never seen what Claude Code does natively, which
is most of what an agent actually does (Bash,Edit,Read,WebFetch). A new
Agent activity tab installs a small recorder into Claude Code's own lifecycle and
keeps one line per event: which tool, in which folder, in which session. Off until
you turn it on, and it removes itself from every file it wrote when you turn it off.Two limits it is built around rather than promises about it. It cannot stop your
agent: the recorder is deliberately not attached to the step that can refuse a tool
call, so no defect in it can block your work. And it does not read your work:
commands, file contents and tool output are dropped before anything is written, and a
row keeps only names, a folder, a session, and a fingerprint that cannot be turned
back into the input.Every Claude Code profile on the machine is covered, not just the one Toolport
resolved:CLAUDE_CONFIG_DIRpicks a profile per shell, so~/.claudeand
~/.claude-workare both real and a recorder in only one of them would quietly
under-report. As with agent rules, your file is never rewritten wholesale: only a
marked block is added, comments and formatting elsewhere survive, and a preview shows
the exact bytes before the first write. (SBS-822) -
Devin Desktop, Devin Local, and Devin CLI support. The legacy
windsurf
integration now uses the current Devin Desktop (Cascade) name and brand while
keeping its compatible~/.codeium/windsurfpaths. A separate Devin Local / CLI
client manages the shared user MCP config and globalAGENTS.mdused by the new
default local agent and the terminal CLI. -
Agent rules: write your instructions once, apply them everywhere. A new Agent
rules tab holds one or more named rule sets and writes the active one into every
AI client's own global rules location (AGENTS.md,GEMINI.md,.goosehints,
Devin Desktop'sglobal_rules.md, and atoolport-rules.mdin the rules directory of
clients that read one), so keeping every supported client in agreement no longer
means hand-editing each file. Your own content is never overwritten:
Toolport either owns its own file in the client's rules directory or owns a
marked block inside a shared file and leaves every other byte exactly as it is.
Each client is off until you turn it on, a per-client preview shows the exact
bytes before the first write, and turning a client off or deleting the set
removes what Toolport wrote and nothing else. Cursor and Warp keep their globals
in their own UI, so the tab names them as clients it cannot write rather than
silently skipping them. Needs no MCP server or gateway. Team instructions are unaffected and
coexist in the same files. See docs/agent-rules.md. -
Official brand marks for 14 more clients. Grok Build, OpenCode, Qwen Code, Kimi
Code, JetBrains Junie, Kilo Code, GitHub Copilot CLI, Amp, Pi, Oh My Pi, Factory Droid,
BoltAI, AnythingLLM and Continue now show their own logo in the Clients view instead of
a letter badge, so 32 of the 35 supported clients now carry their own mark. Crush, Jan
and Witsy keep the badge, since none of them publish a usable vector mark.
Removed
- Toolport Studio is no longer a supported client. The project is discontinued, so
detection, the Connect flow, and its~/.toolport-studio/mcp.jsontarget are gone,
along with the session-scoped restart wording it was the only user of. Toolport now
auto-detects 35 clients. If you had connected it, the gateway entry in that file is
left where it is and can be deleted by hand; nothing else reads it.
Fixed
-
Stopping the HTTP bridge no longer reports a false success. When the child
process survived the stop, the app cleared the handle, port and bearer token
anyway, so the bridge read as stopped while it was still listening and nothing
was left to retry with. The stop failure is reported, and the state is kept
when the child is still running so a later stop can finish the job.
(#736, thanks @YuukiRitoTeng) -
The gateway no longer speaks on stdout before the client has handshaked.
MCP forbids a server sending anything before the client's
notifications/initialized, and the gateway builds its catalog on a
background thread that announces the result whenever it finishes. A client
that spawns the process early and sendsinitializeseconds later (Grok Code
does exactly this) readnotifications/tools/list_changedas the FIRST frame
of the stream, rejected it, and then looked like it had simply never been
answered. It cost 80 of 128 sessions their handshake for most of a day. The
notification is now withheld and replayed once the peer has both spoken past
initializeand been answered at least once, so the first frame a client
reads is always a reply to something it asked. Withheld, not dropped: the
catalog really did change while the client was starting up. (SBS-1019) -
A connection test whose details changed underneath it no longer reports a
verdict for the old details. Editing a server's command or URL while its
test was in flight left the finished result on screen as though it described
what is now in the form. The in-flight test stays visibly busy until it
settles and its result is then discarded, and a superseded test can no longer
overwrite a newer one. (#739, thanks @forever-ivy) -
Catalog and Onboarding tell a failed stack fetch apart from an empty one.
AlistStacks()failure rendered as "no stacks", indistinguishable from
having none, with no way to retry. Both surfaces now show a skeleton while
loading and an inline "Try again" on failure, and the stack region renders
independently of the popular-catalog empty state, so an empty catalog cannot
hide working stacks. (#732, thanks @rohankumardubey) -
The Linux AppImage no longer forces X11 in a way nothing can override.
linuxdeploy-plugin-gtkwritesexport GDK_BACKEND=x11into an AppRun hook
that is sourced AFTER the caller's environment, soGDK_BACKEND=waylandwas
silently ignored ("Trying x11 backend", then a tao panic). On Ubuntu/Debian
that is only opinionated; on a Wayland session whose Xwayland cannot survive
the app (reproduced on Arch/Hyprland in a VMware guest onvmwgfx) it was
fatal and nearly undiagnosable: the first launch printedEGL_BAD_PARAMETER,
showed no window, and killed Xwayland session-wide, breakingxdg-open
for every other app; every launch after that produced no window, no output and
no error, because the process connected to the orphaned X socket and blocked
forever. Release builds now rewrite that line to
export GDK_BACKEND="${GDK_BACKEND:-x11}"- the same default, so nothing
changes for anyone who sets nothing - and the release fails if the line the
patch expects has disappeared. -
The GHCR gateway image rebuilds from the current commit.
docker-publishwas still
caching all ofsrc-tauri/targetkeyed only on Cargo.lock, the same shape that shipped
v0.3.12/v0.3.13 with a stale signed binary. It now uses the same Swatinem/rust-cache
eviction asrelease.yml, so workspace crates always recompile while third-party deps
stay cached. (SBS-926) -
HITL and routine audit rows no longer count as successful tool calls.
Prometheustoolport_tool_calls_total, Activity "calls logged", and team
showbackcallstreated everyaudit.jsonlline as a routed call and a
missingokas success. An approved human-approval gate writes a
kind:approvaldecision (ok:trueon purpose, so a deny stays out of the
error rate) plus the timed exec, so one approval showed as two successful
calls. Advisor / suggestion / candidate lines omitokand were counted as
successes while the Activity list painted them as failures. Aggregators now
requireokto be present and skipkindin {approval, routine, advisor,
suggestion, candidate}. (SBS-932) -
Homebrew cask snapshot was three releases behind.
packaging/homebrew/toolport.rb
still said 1.11.0 after 1.14.0 shipped, anddocs/RELEASING.mdhad no tap-bump
step, so the next release would leavebrew install --cask tsouth89/toolport/toolport
stale again. The snapshot now matches 1.14.0 (sha256s from the published dmgs)
and the release doc namestsouth89/homebrew-toolport. The live tap is a
separate repo; bumping it is that step, not this file. (SBS-936) -
A refused Team Instructions rewrite no longer deletes the last-good org rules.
apply_instructions_toonly recorded a target whenwrite_targetreturned
Applied. Error, TooLong (a Devin Desktop char-cap miss) and BlockedOverride then
hitremove_recorded, which stripped the working v1 block, persisted the new
content watermark, and left later syncs with nothing to retry — coverage of
the missing file against too-long v2 isTooLong, notStale. Last-good now
stays on disk and in the recorded set when a rewrite is refused; a real
removal (org cleared, client gone, path moved) still cleans up. (SBS-917) -
Kimi Shared HTTP Connect writes
url, not Qwen'shttpUrl. Connect, rescope
and reset for Kimi went through the generic JSON editor, which remapped remotes
via Qwen'surl→httpUrlwhenever the map key was not VS Code"servers".
Kimi requiresurland rejectshttpUrl. The Kimi writer already emitted the
right shape; Shared HTTP now uses that same formatter. (SBS-921) -
Agent rules Preview no longer looks like a dead button. The preview card renders
after the clients list, so on any window too short to reach it, clicking Preview
scrolled nothing and showed nothing: the card was open the whole time, below the fold.
Opening a preview now brings the card into view, and its header names the client, which
the path alone does not settle wherever two clients share one file (Claude Code / VS
Code, Gemini CLI / Antigravity). Deliberately still an inline card rather than a dialog:
a modal makes the page behind it inert, and this card is a live panel that clears itself
when the editor or the view moves under it. The scroll is not animated for anyone whose
system asks for reduced motion:index.cssalready zeroesscroll-behavior, but an
explicitbehaviorin the options dict beats that CSS property, so the component reads
the preference itself. -
docs/agent-rules.mdnow names every client with no rules file, not two of them.
The "no rules file Toolport can write" section named only Cursor and Warp, but the
Clients section builds that list from whatever is detected, so a user with LM Studio,
Jan, Hermes, Claude Desktop or Continue installed saw names the doc never mentioned.
All seven are now listed with the reason each one is on it, including the note that
Claude Desktop there is the chat app - Claude Code inside the desktop app shares
~/.claudeand is already covered by the Claude Code row. Continue's stale
"deferred" comment inclients.rsis corrected too: its.continue/rules/is
per-project and its user-level rules are arules:array inside
~/.continue/config.yaml, which fits neither of Toolport's strategies, and
continuedev/continuewas archived read-only in June 2026 besides. Continue stays a
detected MCP client. Docs and a comment only; no behaviour change.
Internal
-
Dead
McpSession::upstream_callshim removed. Nothing called it.
(#805, thanks @forever-ivy) -
CI apt installs are bounded and retried instead of hanging.
apt-get updateon the hosted runners intermittently stops responding rather than
failing, which burned three jobs' entire timeouts on one pull request without
ever reaching a compiler, and a run stuck in progress also blocks
gh run rerun --failed.scripts/ci-apt-install.shnow bounds each attempt,
retries, gives apt a short enough acquire timeout to fail over to its backup
mirrors on its own, downloads before installing so a throttled mirror is
interrupted rather than crept through, and installs from cache with
--no-downloadso the final step has no network left to stall on. -
Every CI job now has a timeout, and apt retries instead of hanging.
Rust Clippy
andLinux build + testcarried notimeout-minutes, so they inherited GitHub's
6-hour default; the jobs inaudit,docker-publish,releaseandwingethad none
either. A flaky Ubuntu mirror made that concrete on one pull request, stalling
apt-get updatein three separate jobs without ever reaching a compiler. An unbounded
hang is the worst shape available: it burns the whole budget, and a run stuck in
progress also blocksgh run rerun --failedon the jobs that genuinely failed, so
recovery needs a manual cancel. All 14 jobs across the 6 workflows now carry an
explicit backstop, sized per job and generous where a tight limit could kill a real
release. The fiveapt-get updatecall sites are now one script,
scripts/ci-apt-install.sh, which bounds each attempt and retries, so a bad mirror
costs seconds rather than a job. -
The headless security smoke no longer fails on a stderr race.
expectBindRefusal
read the gateway's captured stderr as soon as the process emittedexit, but Node fires
that while piped stdio can still hold undelivered bytes. The Windows runner duly reported
refusing to bind 0.0.0.0without thewithout HTTP authenticationtail the assertion
matches on, failing a run in which the gateway had behaved correctly - and the identical
assertion passed on the next host, which is the signature of a race, not a defect. The
capture is now read only after stderr closes, bounded so a stream that never closes
cannot wedge a security check. -
A Windows write now retries instead of losing the race.
atomic_writepublished its temp
file with a singlerename. On Windows that fails outright while any other handle holds the
destination, and something usually does - Defender, the search indexer, a backup agent - for
a few milliseconds.hooks::tests::preview_text_is_the_bytes_install_actually_writesduly
failed one Windows run withinstall_atreturning an error while the identical test passed
everywhere else. This is not only a test problem: users writingsettings.jsonon a machine
with antivirus hit the same edge. The publish rename now retries with a capped backoff, bounded
at 8 attempts so a genuinely locked destination still reports its error. Unix is untouched,
whererename(2)is atomic against open handles and a failure is real. -
Three more sources of intermittent test failure, found by auditing rather than waiting.
The Windows Job Object test waited for its pid file to exist and then read it once, but the
launcher creates the file and fills it in separate operations, so a read in between parses an
empty string and panics - the Unix sibling had been fixed for exactly this and the fix was
never mirrored.LockTimeoutOverridesaved and restored the lock-timeout variable per guard,
so with several suites holding it at once the first one out reverted it while the others were
still relying on it, dropping them to the 5s production default mid-test; it is now refcounted.
And the HTTP concurrency test asserted an absolute 400ms wall-clock budget after two
guess-sleeps, which a loaded runner can miss while behaving correctly; it now waits for a real
signal that the slow call has parked and asserts the property it means - that the fast response
came back while the slow one was still in flight.
Thanks
Four of the patches in this release came from outside.
- forever-ivy - the stale connection-test
verdict, so a test whose connection details changed underneath it stops
reporting on the old ones (#814), and the removal of a dead
McpSession::upstream_callshim (#805). - YuukiRitoTeng - stopping the HTTP bridge
now reports a failure instead of a false success, and keeps the child handle so
a later stop can retry (#788). - rohankumardubey - stack loading
failures surfaced in the catalog and in onboarding, instead of a failed fetch
rendering as "no stacks" with no way to retry (#817).
Full changelog: https://github.com/tsouth89/toolport/blob/v1.15.0/CHANGELOG.md