Toolport v1.14.0
Agents can now keep what worked. A proven multi-tool orchestration can become a
saved routine that survives sessions and clients, with a human approving the exact
definition every time one is persisted.
The rest of the release is mostly a security pass, and the credential one is the
reason to upgrade rather than wait: the redaction gate in front of the public share
link, the diagnostics bundle and the team config push missed several of the most
ordinary ways a key is spelled, so live tokens could ride out to a public URL. Two
more findings in the same family are closed here, along with a set of local-file
permissions that were wider than intended and an installer that never checked who
signed the build it was about to install.
A run of hardening across the app, on one theme: a check that could not finish used
to look exactly like a check that passed. A reload signal, a vault read, a restart
check, a backup stat each had a failure path that came back looking like good news.
They now report the failure, so what the app shows you is what it actually knows.
Added
- Persistent agent routines. A proven multi-tool Code Mode orchestration can be
promoted into a saved, parameterized routine that outlives the session and works
from any client. Promotion is the only way in:toolport_run_scriptgains an
immutable input mode (inputs schema-validated, deep-frozen in the VM, dropped after
assessment), only immutable runs are promotion-eligible, andtoolport_save_routine
takes arunIdrather than source, so free-typed source can never be persisted.
Every save raises a one-shot desktop approval card showing the business summary,
the calls, the dependencies, the risk class and the content hash, with no
session-wide or always-allow shortcut. Saved routines are advertised as first-class
tools and preflight their arguments against the stored schema and observed
dependency fingerprints, failing closed to code mode on drift. A routine advisor
watches for repeated same-shape calls and puts strong candidates in a passive
Suggested routines queue in Settings, rather than prompting the model in-band.
Writes are off until you turn them on. (#625) - Short install commands.
irm https://toolport.app/install.ps1 | iexon Windows and
curl -fsSL https://toolport.app/install.sh | bashon macOS and Linux, replacing the
86-character raw.githubusercontent URLs. Both redirect to a pinned commit rather than
a branch: these are piped into a shell, so the content behind them should not be able
to change without a reviewed change on both sides. - winget package.
winget install Toolport.Toolporton Windows once the manifest is
published, and each release submits its own update. - The audit log records which client made the call. Records carry the caller's
name alongside the server and tool, so an audit answers "who invoked this?" rather
than only "what ran?". (#722) - Re-approve every drifted tool at once. A lost or unreadable pin baseline used to
leave the catalog hidden with no way back except approving each tool by hand.
The baseline is recoverable, and a bulk Re-approve all clears a whole drift set
in one deliberate action. (#747)
Security
- Credentials no longer ride out through share links, diagnostics or the team push.
One redaction gate sits in front of three egress paths: the public share link, the
diagnostics bundle people paste into issues, and the team config push that reaches
the org control plane and every teammate. It missed the ordinary spellings:
--api-key=sk-...,--header=Authorization: Bearer ...,X-API-Key:,Cookie:,
and the split form--token sk-..., whose value sits in its own argument with
nothing on it to recognise. Redaction now reads the whole argument list rather than
one argument at a time, so the split form is caught, and the flag name stays visible
while its value goes. A shared setup is meant to be readable, not to carry your keys.
(SBS-889) - A server that echoes your API key no longer writes it into the audit log. The
audited error text is the defended body, but "defended" is not "secret-free": the
injection scan looks for instruction overrides and the PII pass matches PII shapes,
and an API key is neither. A server answeringinvalid api key sk-live-...put a
live credential intoaudit.jsonland into every CSV exported from it. No attacker
needed, just a server that echoes its input on failure. A credential pass now runs
over the text before it is stored, and it reads JSON bodies as well as prose while
leaving an ordinary failure message readable. (SBS-890) - Local logs are owner-only from the start. Ten append-mode logs were created with
the process umask, which under the usual 022 means world-readable, and only became
owner-only at their first size-triggered rotation.oauth-debug.lognever rotates
andinspect.jsonlis not touched while capture is off, so those two never got
there at all. This matters most forgateway.log, which records the local approval
broker's bound port: on a shared machine a second OS account could read it. Every
log is now created 0600 and an existing wider file is tightened on next write, so an
upgrade fixes the files you already have. No-op on Windows. (SBS-868) - The macOS installer checks who signed the build.
codesign --verifyonly proves
a bundle satisfies its own embedded requirement, so an artifact tampered with before
upload and re-signed with any other Developer ID would have passed. The installer now
requires Toolport's team identifier and fails closed otherwise. It also stages the
new app and swaps by rename, where it used to delete the installed app before
copying: an interrupted install could leave a machine with no Toolport at all.
(SBS-897) - The installers' cross-OS hints stop routing around the pinned-commit rule.
toolport.app/install.shand/install.ps1redirect to a pinned commit precisely
because they are piped into a shell, and both scripts then told a user on the other
OS to fetch the unpinnedmaincopy. Both hints use the pinned URLs now, and CI
fails on any reference to a movable ref. (SBS-894) - A downstream server cannot forge Toolport's own error envelope. A private field
the gateway uses to report protocol errors was honoured on results coming back from
a server, which let a hostile server author an error attributed to Toolport and skip
the whole content-defense layer for that result. It is now stripped at the transport
boundary, before anything reads it. (SBS-891) - A revoked token can no longer stay live. The generation bump that tells a running
gateway to reload swallowed a failed registry write, so a revoke could remove the
credential from the keychain, never reach the gateway, and still report success. The
failure is now propagated and the partial outcome is stated plainly. (#737) - A locked or unreadable keyring is no longer read as "no secret stored". A failed
vault read used to be indistinguishable from an absent secret, so Toolport could mint
a replacement bearer, report a missing refresh token, or show a stale secret status.
Each of those paths now distinguishes the error from the absence. (SBS-840, SBS-841,
#758) - Disconnect fails loudly when the shared HTTP bearer cannot be revoked. Reporting
a client disconnected while its token still works is the one outcome that matters
here. (SBS-845) - The npx spawn guard covers the eval flags it missed, and Teams stops following
redirects with a member bearer. A redirect would have replayed
Authorization: Bearerto a host of the redirector's choosing; team servers under
review are now an execution gate rather than a label. (#711, #713)
Fixed
-
A space no longer smuggles a forged Toolport marker past the voice rewrite.
The gateway-voice matcher folded zero-width, bidi, fullwidth and homoglyph
evasions but compared whitespace literally, so[ Toolport advisor: …]with a
plain space (or a tab, a newline, a no-break space, or a doubled space inside
the brand) reached the model unchanged while the zero-width form was caught.
Whitespace is now folded like the other evasions: ignored before the brand and
collapsed to one space inside it. (SBS-896) -
Team Instructions now follow
XDG_CONFIG_HOMEfor Goose and Zed. Connect
already wrote those clients' MCP configs under XDG (#757 / SBS-847); the rules
writer still hardcoded~/.config, so an org push could succeed and never
apply. AbsoluteGOOSE_PATH_ROOTis honoured on both the config and rules
paths (<root>/config/config.yamland<root>/config/.goosehints). A member
who upgrades in place also gets the block moved to the new path: an unchanged
org text used to skip the write entirely, so the new location stayed empty and
coverage reported Stale until someone edited the instructions. (SBS-899) -
Untrusted MCP output can no longer speak as Toolport. The external-data
wrapper is Toolport-branded (it still saidconduitafter the rebrand) and
sanitizes the server label, so a quote in a resource URI cannot close the
marker. Downstream text that imitates[Toolport advisor:],[Toolport shaped],
[Toolport:], or[conduit:]is rewritten before it reaches the model,
including tools/list, search (ranked and pinned results), tool and resource
and prompt output, and resource errors, even when content defense is off. The
rewrite covers a whole tool definition (title, annotations, parameter
descriptions, enum values, property names) and a whole result (structured
output, prompt messages in any shape), and it folds the same zero-width,
fullwidth, and homoglyph evasions the injection scanner already folds.
(SBS-896) -
A registry that was not really read no longer counts as "no HTTP clients".
--insecure-loopbacktreats an emptyhttp_clientslist as permission to bind and
serve without a bearer, and that list came back empty for three unrelated reasons: a
load error falling back toRegistry::default(), a load that could not read the file
at all (locked, permissions) and reported it as absent, and a load recovered from a
backup, which is the state before the last save and so is missing exactly the save
that registered the first client. Loading now reports where the registry came from,
and the open branch requires a load that actually saw the configured state. The check
is also live rather than decided at boot: the file watcher republishes that verdict
with every reload, so a registry corrupted while the gateway runs closes the listener
instead of quietly re-opening it. A missing registry file is a real first run and still
opens with--insecure-loopback, and a failed load withTOOLPORT_HTTP_TOKENstill
binds. (SBS-900) -
A transient missing quarantine store no longer installs an empty block set.
Rewritingquarantine.jsonuses a temp file plus rename, so a reader can see
NotFoundfor a tick. That used to look like "nothing is blocked" and the
gateway would un-hide every quarantined tool. The read now retries the same
way the pin store already does, and a miss after those retries is an error
unless this profile has never written pins (a real first run). A rebuild
keeps the live set, or hides the catalog on a cold start until the store
reads. (SBS-871) -
Codex Connect honors
CODEX_HOME. Connect, migrate, and launch-time
re-point wrote~/.codex/config.tomleven when Codex was reading
$CODEX_HOME/config.toml, so a relocated live config never got a gateway
entry and Toolport still reported success. Team-instructionsAGENTS.md
follows the same home. Empty or relativeCODEX_HOMEstill falls back to
~/.codex(Toolport's cwd is not Codex's home). The same class is now
honored for Gemini CLI (GEMINI_CLI_HOME→
$GEMINI_CLI_HOME/.gemini/settings.jsonandGEMINI.md), Grok Build
(GROK_HOME), and Qwen Code (QWEN_HOME). A leading~inQWEN_HOMEis
expanded, because Qwen expands it too; the other three use their env value
verbatim, so a literal~there stays a fallback. A GUI-only Toolport
process still cannot see a shell-only export; that limitation already
applied toCLAUDE_CONFIG_DIR. (SBS-885) -
A flagged tool result can no longer self-close its provenance wrap. A
payload that embedded[/conduit: end external data](or[/Toolport) used
to terminate the wrap and leave a forged[Toolport: …]line reading as
gateway voice. The close tag is now per-call and includes a random nonce,
and an embedded close marker is stripped to a plain note that cannot read as
a terminator. The match runs on the folded form, so a close hidden with
zero-width characters, a Cyrillic homoglyph or fullwidth brackets is caught
too. (SBS-892) -
A Personal-scoped HTTP bearer can no longer call a team server whose id
collides after sanitizing. A personal server named Team Slack becomes
team-slack; a team server named slack becomesteam_slack. The HTTP bridge
usedsanitize_segmentas the profile allow-set key, and both ids map to
team_slack, so the Personal token could list and call the team Slack server.
Scope now keys on the raw registry id (SBS-866). Tool names are still sanitized.
The bridge answers its firsttools/listand OpenAPI document from the on-disk
catalog before any downstream server has connected, so scoping in that window
resolves the owning server through the registry and withholds any tool whose
prefix two server ids share, instead of guessing from the prefix. -
Unreadable activity/security log no longer renders as empty healthy. A failed
read ofaudit.jsonl,security.jsonl,inspect.jsonl, orsearch-trace.jsonl
used to come back as an empty list, so Activity showed No tool calls yet /
Protection active. A missing file is still empty (honest). Any other IO error
now rejects the invoke (including the dashboard'saudit_stats), and the existing
error/retry UI surfaces it.GET /metricsanswers 500 on the same unreadable log
instead of 200 with every series missing, so a Prometheus scrape fails rather than
reading as an idle instance. Security events also no longer lose an older row when
a corrupt or mid-write line lands in the newest page. (SBS-873) -
Windows keyring and install.ps1 tests can now block merge. Branch
protection still requires only the check namedBuild + test. That name now
belongs to a merge-gate job that fails unless the Linux suite, the
cross-platform headless Rust matrix (including the Windows keyring tests),
and theinstall.ps1Pester job all succeeded. Adding those names as
separate required contexts still needs a GitHub settings click; this change
does not edit branch protection. -
A write-named tool cannot disarm drift quarantine with
destructiveHint: false.
MCP annotations are untrusted unless the server is. Drift severity now escalates
when the tool name itself claims write capability (delete,run, …), even if
the server setdestructiveHint: falseat first sight. Call-time confirm still
honours an explicit false hint. First sight of that contradiction is recorded
in Activity and is not quarantined. (SBS-875) -
Connect and launch-time repoint no longer strip comments from Codex/Grok
config.tomlor comments and YAML anchors from Goose/Hermes/Continue
config.yaml. Those writers used to parse withtoml/serde_yamland
pretty-print the whole file, so a Connect click orrepoint_stale_gateways
dropped every#comment and expanded every&anchor. Unrelated keys
survived as data; the annotations did not. TOML now goes throughtoml_edit
DocumentMut and YAML rewrites only themcp_servers/extensions/
mcpServersnode, matching the JSONC CST contract. A user who Connects or
disconnects now keeps the comments and anchors they had outside that node.
(SBS-884) -
Failed routine-suggestion load no longer hides the save queue. Settings treated
an unreachablelist_routine_suggestionsas "nothing pending" and hid the section,
including wiping cards already on screen when a later refresh failed. A failed load
is now its own state: empty after a successful read still hides; error shows retry
and keeps the last loaded queue. (SBS-879) -
A diagnostics bundle or second gateway can no longer see an empty
gateway.logor lose a connect-failure line. Trim used to rewrite the shared log in place, so a concurrent diagnostics read could land in the empty window and a concurrent append could be overwritten by the pre-truncate snapshot. Trim now replaces the file atomically under the same lock as append. -
SECURITY.md now matches the HTTP bind admission policy. Every bind, loopback
or not, needs HTTP authentication: a bearer token, or at least one registered
HTTP client inregistry.json. A hand-launched loopback gateway with neither
does not bind; it exits 1 unless--insecure-loopbackis passed, and that flag
opens an unauthenticated listener only while no token is set and no HTTP clients
are registered. SECURITY.md also now explains what a registered HTTP client is
and how the desktop app creates one. (SBS-878) -
Connect keeps stow/chezmoi config symlinks. Writing a client config
(Connect, Disconnect, migrate, launch-time re-point) used to replace a
symlink at the config path with a regular file, leaving the file in the
dotfiles repo unchanged. The nextchezmoi apply/stowrestored the
old content and the gateway entry disappeared. Writes now follow the
link and update the target, so the path under home stays a symlink.
(SBS-886) -
Linux
.debinstalls atoolportcommand. The package still ships the
crate binary asconduit(compat alias) and now also putstoolporton
PATH, matching the AppImage installer and the brand.install.shtells apt
users to runtoolport. (SBS-846) -
A second Claude Code profile no longer gets stuck on an old gateway.
CLAUDE_CONFIG_DIRis usually set per shell or per launcher rather than exported, so a
machine often has several Claude configs (a personal~/.claudebeside a work
~/.claude-work). Toolport resolved exactly one of them, and every other one it had
written kept pinning whichever gateway binary was current the day it was written.
Because pruning deliberately keeps recent binaries, that profile went on launching
superseded gateway code indefinitely and the reaper could not win: it stopped the
process and the config Toolport never updated started it again. Every Claude config is
now re-pointed on launch, and each one's gateway binary counts as referenced so pruning
cannot delete it. Strictly a repair: a profile with no Toolport entry does not get one,
and a hand-customized entry is still left alone. -
A failed old-gateway check no longer hides the warning. Settings swallowed the
error, so "no apps need a restart" and "we could not find out" looked identical. The
failure is now shown with a Retry that re-runs the check in place, and running
Stop old gateways clears it, since that result answers the same question. (#730) -
A failed backup stat aborts the config write. The pre-write backup is what makes
a client-config write recoverable; continuing without one turned a safety step into
a silent no-op. (#745) -
A transient registry read failure retries instead of showing an empty app.
A reload that lost a race with a write used to leave the UI looking like a machine
with nothing configured. (#724) -
A failed quarantine poll is no longer reported as a confirmed zero. "Nothing is
quarantined" and "we could not ask" are different answers, and only one of them is
reassuring. (#741) -
Launch at login stays disabled until the OS state is actually known. The switch
used to render as a verified Off while the real value was still unread. (#721) -
The gateway routes every tool it advertises. The collapse guard could keep a tool
in the catalog whose route had been dropped, so a model could call something that
then failed to resolve. (#717) -
install.shverifies its downloads likeinstall.ps1already did. Per-asset
digest, published size, https-only URLs, and the AppImage now stages in a temp
directory so a failed verification cannot delete a working install. (#744) -
Linux fixes. AppImage launch-at-login points at
$APPIMAGErather than the
FUSE mount that disappears between runs; the bundled gateway is recopied when its
contents change rather than only its size;XDG_CONFIG_HOMEis honoured for Zed,
Goose and AnythingLLM; a crash in gnome-keyring under concurrent Secret Service
sessions is avoided by serializing them and retrying a dead daemon; and blocking
file and keychain reads no longer run on the GTK main loop, which is what made the
window controls stop responding. (SBS-844, SBS-843, SBS-847, SBS-815, SBS-813) -
Concurrent OAuth sign-ins agree on the outcome. A lock drop that failed to record
its verdict let a waiting process treat an unfinished attempt as a completed one.
(SBS-842) -
The audit log survives concurrent appends and rotation. Append and rotate are
serialized, so a rotation cannot drop a record another process just wrote. (#708) -
Opening the data folder reports its failure. It used to fail silently, leaving
the button looking broken rather than the action. (#768) -
Hermes is detected at its Windows platform config path. (#746)
-
A rich confirmation dialog renders valid markup. Descriptions containing block
elements produced invalid DOM nesting. (#723) -
The onboarding dialog is named for screen readers, and the role choice exposes
its selected state. (#726)
Internal
- Clippy runs in CI. (#749)
- Tests stopped depending on the machine that runs them. Four launcher tests read
the developer's real npx cache, and the SSRF tests assumed.invalidnever resolves,
which is not true behind a resolver that answers every name. Both now inject what
they were reading from the host, so a clean checkout is green. (SBS-839, SBS-827) - Three multi-process contention tests no longer flake on a loaded runner. (SBS-895)
Thanks
A big cycle for contributions: fifteen of the patches below came from outside, including
the routines feature that leads this release.
- forever-ivy - persistent agent routines, the
headline feature of this release: immutable runs, human-approved promotion, first-class
routine tools and the Suggested routines queue (#706). Also the registry-reload retry
(#724), and the report that our SSRF tests fail behind a resolver that answers every
name, which turned out to be right and is fixed here (SBS-827). - aryansk - eight patches: download verification in
install.sh(#744), the failed-reload-signal fix that stops a revoked token staying
live (#743), the quarantine poll that no longer reads as a confirmed zero (#742), the
aborted write on a failed backup stat (#745), routing every advertised tool (#717),
launch-at-login state (#721), valid dialog markup (#723), and onboarding
accessibility (#726). - joyheroes - surfaced two silently swallowed
failures: the old-gateway restart check (#778) and opening the data folder (#768). - BharadwajKanneveti - the caller name in
audit records, so an audit says who invoked a tool (#722). - Vermitrude - the Clippy gate in CI (#749).
- rohankumardubey - serialized audit append
and rotation (#708).
If we missed you, open an issue.
Full changelog: https://github.com/tsouth89/toolport/blob/v1.14.0/CHANGELOG.md