Skip to content

ThinkWatch Core 0.59.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 10:33
· 5 commits to main since this release
ef10a8b

This release keeps a plugin's configuration in the plugin's own file. What a plugin does when it fails, which requests it handles and the values of its settings are written in the manifest of plugins/<id>.js, which has to be plain data, and config.yaml keeps only the plugin's id, file, approved hash and whether it is on. Saving a plugin is one endpoint that tells a change of data from a change of code, and only plugins that can rewrite tool calls need a confirmation in the app. On Windows, twcore.exe no longer needs the Visual C++ Redistributable.

Upgrade notes

  • The control-plane protocol version (CONTROL_API_VERSION) goes from 34 to 35. ThinkWatch Lite connects only to a core with the same protocol version. No ThinkWatch Lite release includes 0.58.0. ThinkWatch Lite 2026.10.0 includes 0.57.1 (protocol 31) and does not connect to 0.59.0. A server used with it stays on 0.57.1 until the app is updated to a release that includes 0.59.0; sudo twcore upgrade --version 0.57.1 --restart switches a server back.
  • The request store's schema is unchanged (25): upgrading from 0.58.0 keeps the request history. A server upgrading from 0.57.x also gets 0.58.0's changes, so the 0.58.0 upgrade notes apply as well: the history is cleared once, and a configuration that still sets security.hidden_text or security.output_limit does not load.
  • A plugin's manifest has to be plain data.
    • The file declares it as export const manifest = { … }. A manifest exported any other way, such as export { manifest }, does not load.
    • It may hold objects, arrays, strings, numbers, true, false and null. Keys are names or quoted strings. Trailing commas are fine, and so is a minus sign before a number.
    • Expressions, function calls, references to variables, spreads, computed keys, getters and methods, and template literals with ${…} are not data. Neither is a manifest that the module's code changes after declaring it.
    • Such a plugin does not load. gw.plugin.manifest_not_data_at gives the line and column; gw.plugin.manifest_not_data is used when there is no position.
  • The manifest holds the plugin's configuration.
    • on_error ("reject", the default, or "skip") is a manifest field.
    • match is the scope the plugin runs with. It used to be a suggestion that filled in the configured scope at install.
    • Settings take value in place of default: it is the value the plugin gets. A setting still written with default does not load; rename the field to value. A setting without value gets its type's empty value ("", 0 or false).
  • Plugin entries in config.yaml hold only id, file, sha256 and enabled.
    • Entries written by 0.58.0 may still have on_error, scope and settings. These are ignored whatever they contain, so the configuration still loads, and they are removed the next time core writes the plugins section.
    • Their values are not carried over. To keep them, write them into the plugin's manifest, or set them again in the app.
    • rewrite and confirmed are now reserved: a configuration with a plugin of either id does not load.
  • Plugins installed by 0.58.0.
    • The two default plugins are replaced with this release's versions on first start and take this release's setting values. One that was on is turned off, because this core cannot read what its 0.58.0 version asked for; turn it on again in the app.
    • A plugin of your own whose manifest uses default, or is not plain data, does not load until its file is fixed and the change approved. While it is on, it refuses the requests it covers: on_error defaults to reject, and a 0.58.0 manifest cannot set it.
  • Changed in the protocol:
    • Removed: PUT /plugins/{id}/source (ReplacePluginSource, PluginSourceReplace), UpdatePlugin, UpdatePluginConfirmed, PluginUpdate, and PluginView.settings (the values are in settings_schema[].value).
    • PUT /plugins/{id} is SavePlugin and PUT /plugins/{id}/confirmed is SavePluginConfirmed. Both take PluginSave: source, enabled and base_version.
    • New: POST /plugins/rewrite (PluginRewrite: PluginRewriteRequest → PluginSource), POST /plugins/confirmed (CreatePluginConfirmed) and POST /plugins/{id}/approve/confirmed (ApprovePluginFileConfirmed).
    • PluginCreate has only source, id, enabled and base_version. SettingSpecView.default is now value, and ManifestView has on_error. PluginView.on_error and PluginView.scope are what the plugin's file says.
    • A web view may call CreatePlugin, SavePlugin and ApprovePluginFile. A client calls the three …Confirmed endpoints from native code after the user confirms, never from its web view.
  • Confirmations. A plugin that can rewrite tool calls (the reply.tool_calls permission) needs the user's confirmation outside the web page to be installed, to be turned on, to have its code changed, or to have a change to its file approved. A plugin counts as one when its old or its new version has the permission, or when its old version cannot be read.
    • The plain endpoints refuse these with 403 control.plugin.needs_confirmation.
    • Nothing else needs a confirmation: changing only data, even of a plugin that can rewrite tool calls; turning a plugin off, deleting and reordering; and installing, turning on, changing or approving a plugin that cannot rewrite tool calls. In 0.58.0, every install, source replacement and file approval needed one, and so did changing the settings or scope of a plugin that can rewrite tool calls.
    • PUT /config, PATCH /config and rollback refuse to add, turn on or re-approve a plugin that can rewrite tool calls, with the same 403. They are open to the web view and used to bypass the rule. They have no confirmed variant: those changes go through the plugin endpoints.
  • Message codes, compared with 0.58.0:
    • New: gw.plugin.manifest_not_data and gw.plugin.manifest_not_data_at.
    • Removed: config.plugin.blank_pattern and config.plugin.setting_type. The configuration fields they checked are gone.
    • control.plugin.needs_confirmation has a new sentence that names the four actions. Its argument (plugin) is unchanged.

Plugin files hold their configuration. The app changes on_error, match or a setting by rewriting the plugin's source and saving it. POST /plugins/rewrite takes a source and the new values and returns the rewritten source; it reads and writes no file. POST /plugins/inspect reads the values back from a source.

  • Only the manifest's bytes change. Every other byte of the file stays as it was.
  • Core writes the manifest in one fixed style: two-space indent, one manifest field per line, and nested objects and lists on one line when they fit in 80 columns. Comments inside the manifest are not kept.
  • In a rewrite request, on_error and scope are the values to end up with, and settings changes only the settings it names. Each scope entry is trimmed. A setting the plugin does not declare (gw.plugin.setting_unknown), a value of the wrong type (gw.plugin.setting_type) and an empty scope entry (control.plugin.blank_pattern) are refused with 400.
  • Core finds the manifest with a small tokenizer that skips strings, template literals, comments and regular expressions, so text that looks like a manifest inside them is not taken for it.
  • A plugin whose file changed, or that does not load, follows the on_error and match of its approved file. The manifest is data, so core reads them even when the plugin cannot run.

Saving a plugin. PUT /plugins/{id} takes the whole source and whether the plugin is on. Core compares the source with the approved one:

  • Data-only: the bytes outside the manifest are identical, and the manifest differs only in on_error, match and the settings' value.
  • Code change: anything else, including a change to the name, description, permissions, request kinds, or a setting's type or label.
  • Same bytes: only enabled changes, and the files are not touched.

When a save writes the file, the plugin file, its approved copy and its sha256 in config.yaml change together, with no moment in between when the file counts as changed: a plugin that is on keeps running across the save. A save is tied to the configuration version it read, and fails with 409 when the configuration has moved on. A file edited by hand still stops the plugin until the change is approved, even when only a value changed.

Default plugins. reply-language and wsl-paths write on_error and their settings' value in their manifests, in the style core writes, so changing a setting in the app changes one line of the file. The record of what core offered follows data-only saves and approvals, so a default whose settings, scope or on_error were changed still counts as unchanged code. A later version replaces it and keeps what was written in the file: on_error, match, and the values of the settings it still declares with the same type. A default whose code was changed is still left alone, and a new version that asks for more permissions or request kinds still comes back turned off.

Windows. twcore.exe no longer needs the Visual C++ Redistributable; the C runtime is linked statically. The twcore.exe of 0.58.0 imported VCRUNTIME140.dll, which comes with that package and not with Windows, so the gateway did not start on a machine without it.

Requests full of secrets. Outbound redaction no longer slows down when one request holds many distinct values that look like secrets: finding, replacing and restoring them, and hiding them from plugins, now take time in proportion to the request's size instead of growing with the square of the number of values. The security log records at most the first 100 distinct values of a request, and every value is still replaced.

Downloads

Platform Binary Archive for server installation
Linux, x86_64 twcore-x86_64-unknown-linux-gnu twcore-x86_64-unknown-linux-gnu.tar.gz
Linux, aarch64 twcore-aarch64-unknown-linux-gnu twcore-aarch64-unknown-linux-gnu.tar.gz
macOS, Apple silicon twcore-aarch64-apple-darwin —
Windows, x64 twcore-x86_64-pc-windows-msvc.exe —
Windows, ARM64 twcore-aarch64-pc-windows-msvc.exe —

Each file is published with a .sha256 file beside it. A Linux archive contains twcore, the systemd unit twcore.service and LICENSE. ThinkWatch Lite includes its own copy of twcore; the files here are for running core separately, such as on a server.

Server installation

On Linux (x86_64 or aarch64), the install script sets up twcore as a systemd service. This installs 0.59.0:

curl -fsSL https://raw.githubusercontent.com/ThinkWatchProject/ThinkWatch-Core/main/scripts/install.sh | sudo sh -s -- --version 0.59.0

An installation made with the script switches to 0.59.0 with:

sudo twcore upgrade --version 0.59.0 --restart

Configuration, the remote control port and connecting ThinkWatch Lite are described in docs/server.md.

Verifying a download

A .sha256 file holds the SHA-256 of the file followed by its name. With both files in the current directory, on Linux:

sha256sum -c twcore-x86_64-unknown-linux-gnu.tar.gz.sha256

On macOS:

shasum -a 256 -c twcore-aarch64-apple-darwin.sha256

On Windows, in PowerShell, the following prints True when the binary matches:

(Get-FileHash .\twcore-x86_64-pc-windows-msvc.exe).Hash -eq (Get-Content .\twcore-x86_64-pc-windows-msvc.exe.sha256).Split()[0]

The install script and twcore upgrade check the SHA-256 themselves.

What's Changed

  • Plugins keep their configuration in their own file (protocol 35) by @fylorn in #276
  • Link the VC runtime statically into twcore.exe by @fylorn in #277
  • chore: v0.59.0 by @fylorn in #278
  • Linear-time redaction for requests full of secrets, a per-request report cap, and output-cap edge cases by @fylorn in #279

Full Changelog: v0.58.0...v0.59.0