Skip to content

Toolport v1.14.0

Choose a tag to compare

@github-actions github-actions released this 16 Aug 18:45
· 281 commits to main since this release
3bbab40

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_script gains an
    immutable input mode (inputs schema-validated, deep-frozen in the VM, dropped after
    assessment), only immutable runs are promotion-eligible, and toolport_save_routine
    takes a runId rather 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 | iex on Windows and
    curl -fsSL https://toolport.app/install.sh | bash on 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.Toolport on 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 answering invalid api key sk-live-... put a
    live credential into audit.jsonl and 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.log never rotates
    and inspect.jsonl is not touched while capture is off, so those two never got
    there at all. This matters most for gateway.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 --verify only 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.sh and /install.ps1 redirect 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 unpinned main copy. 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: Bearer to 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_HOME for 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. Absolute GOOSE_PATH_ROOT is honoured on both the config and rules
    paths (<root>/config/config.yaml and <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 said conduit after 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-loopback treats an empty http_clients list as permission to bind and
    serve without a bearer, and that list came back empty for three unrelated reasons: a
    load error falling back to Registry::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 with TOOLPORT_HTTP_TOKEN still
    binds. (SBS-900)

  • A transient missing quarantine store no longer installs an empty block set.
    Rewriting quarantine.json uses a temp file plus rename, so a reader can see
    NotFound for 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.toml even when Codex was reading
    $CODEX_HOME/config.toml, so a relocated live config never got a gateway
    entry and Toolport still reported success. Team-instructions AGENTS.md
    follows the same home. Empty or relative CODEX_HOME still 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.json and GEMINI.md), Grok Build
    (GROK_HOME), and Qwen Code (QWEN_HOME). A leading ~ in QWEN_HOME is
    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 to CLAUDE_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 becomes team_slack. The HTTP bridge
    used sanitize_segment as 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 first tools/list and 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 of audit.jsonl, security.jsonl, inspect.jsonl, or search-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's audit_stats), and the existing
    error/retry UI surfaces it. GET /metrics answers 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 named Build + 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 the install.ps1 Pester 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 set destructiveHint: false at 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.toml or comments and YAML anchors from Goose/Hermes/Continue
    config.yaml.
    Those writers used to parse with toml / serde_yaml and
    pretty-print the whole file, so a Connect click or repoint_stale_gateways
    dropped every # comment and expanded every &anchor. Unrelated keys
    survived as data; the annotations did not. TOML now goes through toml_edit
    DocumentMut and YAML rewrites only the mcp_servers / extensions /
    mcpServers node, 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 unreachable list_routine_suggestions as "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.log or 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 in registry.json. A hand-launched loopback gateway with neither
    does not bind; it exits 1 unless --insecure-loopback is 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 next chezmoi apply / stow restored 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 .deb installs a toolport command. The package still ships the
    crate binary as conduit (compat alias) and now also puts toolport on
    PATH, matching the AppImage installer and the brand. install.sh tells apt
    users to run toolport. (SBS-846)

  • A second Claude Code profile no longer gets stuck on an old gateway.
    CLAUDE_CONFIG_DIR is usually set per shell or per launcher rather than exported, so a
    machine often has several Claude configs (a personal ~/.claude beside 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.sh verifies its downloads like install.ps1 already 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 $APPIMAGE rather than the
    FUSE mount that disappears between runs; the bundled gateway is recopied when its
    contents change rather than only its size; XDG_CONFIG_HOME is 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 .invalid never 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