Changelog

Every release, newest first. Downloads and the full commit list for each one are on GitHub Releases; what is coming next is on the roadmap.

2.8.0 A workbench in two languages and several panes, a test pillar that reaches CI, and a Bowire that knows the others

· Release, downloads and commits →

2.8 is the release in which the workbench stops assuming one of everything: one language, one live method, one Bowire. It speaks German and English throughout, holds two methods open side by side, and one Bowire can list a whole fleet of others. The test pillar reaches the pipeline — design-time lint, snapshot approval and contract suites run from bowire test — and MCP, parallel sessions and a new scaffolder round out the agent side.

Localisation and layout

German and English, all the way down

Every string of the workbench goes through the translation layer now, with German and English catalogues shipped side by side and a parity test that fails a build where the two drift apart — a key missing, or a placeholder dropped or invented (#117). The language follows the browser until one is picked under Settings → General. Walking the workbench under a pseudo-locale after the sweep turned up strings that no screenshot could catch — a bundle outside the workbench scope whose translations threw before they rendered, among them — and those are fixed at their root, not per string. Plugin settings and protocol descriptions, which arrive from the backend, are covered as well (#691), and the action log stores keys rather than the English sentence it happened to render (#689).

Two methods at once

Request and response state used to live in globals, so only one method could ever be live (#695). With that gone, a tab can split its request and response (automatic, by drag or by shortcut, remembered per tab), and two tabs can sit side by side in panes, each streaming into its own view with its own state (#250). Wiring that waited for an animation frame never ran in a hidden tab; it does now (#696). Popping a pane out into its own window follows as #753.

A shelf for what you carry between surfaces

The visual shelf is a holding area next to the operating system’s clipboard: right-click a value — a response field, an environment variable — and it waits on the shelf, typed, until it is dropped where it is needed (#251). Building it turned up a bug older than the shelf: the first click into any text field that had never had focus was lost, because the variable-chip overlay re-parented the field on focus (#706).

Header sets, and names or paths

Headers can be kept as named, scoped sets and switched on and off instead of retyped (#95), and the sidebar shows methods by name or by path, as the operator prefers (#47).

The test pillar reaches CI

Design-time lint

The schema linter learned naming and PII rules and shows its findings inline next to the method, and bowire test --suite lint puts them into the test runner’s reports — JUnit with one test case per rule, SARIF with the service, method and field as the location — without failing a pipeline on its first run (#583). Response-shape rules could never fire on REST, because discovery populated no output fields; they can now (#663).

Snapshots, data, faults, contracts

A snapshot diff can be reviewed and approved in the workbench, against the same baseline CI compares with; data-driven runs get a results view; and the mock can drop frames on purpose to test what a client does when they go missing (#366). bowire test --workspace-id <id> runs a workspace’s stored suite, and bowire workspace list names the ids (#365).

MCP, agents and the assistant

MCP tools stream

A tool call no longer waits in silence: progress notifications and log messages from the server arrive while it runs, and the result is the last frame (#46). The MCP forwarding over HTTP starts again and --attach finds the standalone server where it listens (#730, #731).

A Bowire that knows the others

Bowires embedded across a fleet can register with a hub (Bowire:Agent:HubUrl), which lists them with a link to each one’s own workbench and serves them as a catalogue for other Bowires (#128). Agents push: the hub never has to reach them, and a restarted hub is whole again after one heartbeat. The hub also offers the agents tagged parallel-executor as executors for parallel sessions.

Parallel sessions you can expose

An executor listens beyond loopback, which made it a load generator for whoever could reach it. It now takes a target allowlist and a token, a coordinator can refuse to send anything to an executor in clear text, and both sides write a hash-chained audit log that joins a run to what each executor did (#313).

A service from a sentence

“REST CRUD for User with email + role” becomes a schema, a runnable C# stub, a workbench collection and a smoke test that passes against the stub as generated (#177). The assistant — a local model first — writes only the entity spec; the files come from checked-in templates, so a spec gives the same code with any model or none, and a deterministic parser reads the sentence when no model answers.

Protocols and plugins

GraphQL as a full client

GraphQL closes the distance between discovery and a real client (#710): queries over GET, automatic persisted queries, batching, multipart file uploads with a file input in the request builder, and multiplexing several operations over one socket (#713, #715). The stream contract has one error shape now instead of one per plugin (#712).

The schema as a graph

The Schema Designer draws a discovered schema’s type graph — which message uses which, for the .proto with fifty types where a tree cannot answer that — and ships as the first default-off rail module, switched on under Settings → Rail modes (#247, #249, #711).

Rails out of core, plugins that get what they declare

The remaining rails moved into their own packages, so core carries the discover, home and workspace surfaces and nothing else (#311). A sidecar’s manifest can name an interpreter that resolves through PATH, and its declared settings reach it (#692, #693). gRPC discovery keeps a type referenced twice (#694), and a service that imports google/protobuf/empty.proto can be called again. The AsyncAPI binding resolvers are implemented instead of throwing (#357).

Security

Out-of-band callbacks in the workbench

The Security rail can generate a callback payload and watch the interactions arrive live, and the workbench’s own SSRF probe now plants a callback per URL parameter instead of inferring the finding from a latency difference (#486).

Getting started

Starting Bowire

Every platform gets a desktop shortcut, the workbench runs without a console window, and the docs say how to start it (#685). Starting a second Bowire on a port the first already holds opens the running workbench instead of crashing (#684). Uploaded schemas belong to their workspace and survive a restart, and bowire discover --schema hands the CLI a file directly (#654).

The site

The launch wizard and the quickstart set up the AI assistant as an optional step, with the same provider picker as Settings → AI (#110); every quickstart path now ends on its finish flag (#738); and the README, the workbench, the CLI (once, never in CI) and the site ask for a star and a watch (#669).

Fixes

  • el() no longer turns disabled: false into a disabled control (#686).
  • The map host unmounts the widget it mounted; every re-render used to leak one (#707).
  • A mock emitter set to loop at replay speed 0 no longer publishes without bound (#708).
  • SignalR ad-hoc invoke takes the JSON array it always required (#665); the method-name form on /api/invoke is the same for every plugin (#664).
  • Links from the docs with ?rail= and ?topic= open the rail and the topic they name (#735, #736).
  • No setting decides silently by assembly load order any more; a failed /api/protocols is shown instead of an empty settings page (#751, #752).
  • plugin install no longer suggests a re-run its own guard blocks (#666).
  • Console lines no longer carry the action log’s exemption reason (#739).
  • Test infrastructure: an InvokeEndpoints fixture and replayer coverage, isolated test storage that no longer writes to the real ~/.bowire, automated tests for the schema drop zone and the upload shortcut, and loopback test ports that look for foreign traffic instead of tolerating it (#216, #714, #732, #733, #734, #737, #740).
  • Test-pillar and request-builder polish for GraphQL layouts (#292).

How this release was planned

2.8 is the first release planned again rather than reconstructed: every ticket names the product that ships it, the release it is planned for and that release’s milestone, and scripts/ci/release.mjs drafted these notes from that plan (project board conventions).

2.7.0 A map you can read, and a gRPC plugin that no longer needs reflection

· Release, downloads and commits →

The map widget stops being a scatter of dots and becomes something an operator can read: paths per entity, entities told apart by their own identity, and a cursor that rewinds. Alongside it, the gRPC plugin can finally call a server with Server Reflection switched off — the state Bowire’s own scanner recommends — and the scanner learns to see three failures that survive every “is it public?” check.

The map

A path, not a scatter of dots

A stream of positions rendered as independent pins. Good for “where has it been”, useless for “where is it going”: the operator connected the dots by eye. There is now an optional LineString layer under the pins, off by default, toggled from the map’s own overlay and remembered per workspace (#238).

The geometry is derived from the pin collection rather than accumulated beside it. Two things fall out of that: the existing 5000-pin FIFO cap already bounds it, so a vertex cannot outlive the pin it came from; and a pin and its segment cannot disagree about position, selection or colour, because there is one copy of that state.

Entities told apart by their own id

The map grouped and coloured by discriminator — the protocol’s word for message type. On a wire carrying many entities that is one bucket for all of them, while the operator’s actual question is “show me every ping of UnitID 42 as one track”. A configurable track-id path now answers it (#240), with a collapsible legend: one row per track, its colour, how many pins it has on the map, and a switch to hide it.

Two stream shapes had to work and they pull in opposite directions. TacticalAPI puts N entities in one frame; DIS sends one entity per PDU across many. So a path resolves absolutely first, then relatively — walking up from the coordinate’s own parent, because the identity lives above the node that holds the position: one level for a flat position, five for symbol.location.content.point.geoPoint. Only the data knows how deep.

Hiding a track is a render concern and never a state one. The master collection keeps every pin, so a hidden track still counts, keeps its selection, and comes back unchanged — rather than being deleted and having to be re-streamed, which for a finished stream means never.

The pin cap stays global. A per-track quota of 5000/N shrinks every existing track’s allowance the moment an N+1th track appears, retroactively deleting history from tracks that did nothing, and the quiet ones lose the most. Taking each pin from whichever track is currently longest gives the same protection, because a chatty entity is the longest track.

Rewinding a stream

Once a stream stopped there was no way back to minute 7, and “where was it when the alert fired” needed a tool outside Bowire. A cursor now decides which frames count as already arrived, and the pin layer, the trajectory and the legend all read that one answer (#239).

The axis is frames, not pins — a frame is one update, and one frame may carry thirteen pins. Playback follows the data’s own rate rather than a fixed tick, clamped at both ends so two frames stamped the same millisecond cannot spin and a five-minute gap cannot look frozen. The transport stays locked while the stream is live, because a cursor holding a position while the tail moves flickers between the two; the stream ending unlocks it, from finally rather than the happy path, since a stream that ends by throwing is exactly when someone wants to rewind.

Scrubbing never touches the selection. A frame selected while the cursor sits before it stays selected, is simply not drawn, and returns when the cursor passes it.

gRPC without reflection

Calling a server that has reflection switched off

Disabling Server Reflection is the recommended production state — Bowire’s own scanner says so — and until now that made the gRPC plugin unable to call such a server at all, because descriptors came from exactly one place (#653).

A caller can now supply a compiled descriptor set, the artefact of protoc --descriptor_set_out=api.protoset --include_imports and the same input grpcurl -protoset takes. It travels in request metadata under a marker key, the way mTLS configuration already does, so no plugin signature changes and the marker is stripped before anything reaches the wire.

Reachable from every surface

The capability stopped at the plugin boundary, so walking it end to end against the sample carried it out: --grpc-descriptor-set on bowire list, describe, discover, call, scan, export and diff; ?grpcDescriptorSet= on /api/services; grpcDescriptorSet on bowire.discover and bowire.lint. All of them write the same metadata marker the plugin already reads, so there is no second channel to keep in step.

Security

The map widget never mounted at all

Not “rendered wrong” — did not render, in every build carrying the current Content-Security-Policy (#655).

MapLibre parses tiles in a web worker it constructs from a blob: URL. worker-src has no default of its own: absent, the browser falls back to script-src, which we set to 'self' plus a nonce. Neither covers a blob, so the worker was refused, MapLibre never reached its load event, and the widget’s mount awaits exactly that. Nothing on the server said so — the page was 200, the header strict, and Bowire’s own scanner read the policy as healthy, because it grades the header and the header was good.

worker-src 'self' blob: is now spelled out rather than left to inherit, so a later script-src edit cannot move the worker rule with it unnoticed.

Three failures that survive “is it public?”

  • A gRPC server that authenticates but does not authorize. The existing probe asks whether a method answers a caller with no credential; here the stranger is correctly turned away, and what fails is the next question. GrpcAuthorizationProbe reaches a verdict from three observations — an anonymous call refused, identity A reaching the handler, identity B reaching it too — because only that combination shows a server gating on having a credential rather than on which one.
  • A WebSocket with the same shape. A handler that establishes who you are and then proceeds without consulting the permission model passes every “is it public?” check cleanly, and lets every authenticated account reach every channel.
  • MQTT credentials crossing a link in the clear. A broker that correctly demands credentials and then takes them over plaintext hands every username and password to anyone on the path — MQTT carries them as plain fields inside the CONNECT packet, with no challenge-response to hide behind.

A reflection marker that read as reassurance

When reflection returned nothing, the probe reported “reflection is disabled (the desired production state)” and stopped. Two things were true and only one was being said: reflection is off, and the transport-authentication check never ran, because it needs a method to call. Read together, that told an operator their server was in the recommended state — about a server whose authentication had not been examined at all. And the better a deployment follows that recommendation, the more often it lands in that branch.

A plugin’s documentation page is checked before it is published

fetch-plugin-docs.mjs pulls docs/protocol.md out of every repository carrying the plugin topic and writes it where DocFX renders it onto bowire.io — and DocFX passes raw HTML straight through markdown, so a page’s bytes reach a visitor’s browser on our own origin. The repositories are the org’s own, which makes this a supply-chain guard: it is what stops one compromised plugin repository from putting script on the documentation site. Pages are now bounded in size, required to carry front matter, and refused outright if they carry markup that would execute.

Also in this release

MCP resources address a workspace

They read the workspace-less files, so they answered with the wrong data (#642). Resources are now workspace-addressable, with an index to address them by.

The correlation suggester weighs evidence

It gated on the field name before weighing any evidence, so a shared identifier under an unexpected name was never suggested and a coincidental name always was (#650).

The VS Code extension is 1.0

kuestenlogik.bowire-vscode 1.0.0 is on the Marketplace (#619). 1.0 is a promise about the surface: the bowire.* settings keys and the command ids stay where they are, and moving one is a major version. The extension’s version stays independent of Bowire’s — it drives an installed CLI rather than bundling one, and hosts anything from 2.5 upwards.

A SCIM provisioning round-trip leaves evidence

scim/events.jsonl records mutations and their outcome, so a connector’s reads — the paging walk, the existence filter — left no trace, and the reads are most of what a live round-trip is for. Bowire:Scim:TraceProvisioning (off by default) now writes one line per SCIM request: method, path and query, status, duration, and for PATCH the dialect read off the wire. docs/setup/scim.md grows the matrix to walk and a table naming which provider version was exercised and when.

Off by default, and meant to be turned off again: the lines carry what the connector sent — user names, e-mail addresses, the filters a directory walk used.

SOAP ships in the workbench bundle

It was the only in-repo protocol left out of it without a reason. NATS and Pulsar sit outside because each carries a third-party client library, which the optional-package rule keeps out of a bundle an embedded host has to pay for; SOAP has no dependency at all, its sample’s header printed bowire --url soap@… with no install step, and the bundle’s own comment already listed SOAP among the universal protocols it contains. Only the ProjectReference was missing. soap@ now answers on a fresh install.

The protocol guide gained a Bundled column at the same time, because the table’s split was “ships from this repo” and a reader took it for “works out of the box” — which for NATS and Pulsar it is not.

Under the hood

The release cascade resolves dotnet new template defaults

Bowire.Templates holds its Bowire version twice: as a placeholder in the plugin template’s Directory.Packages.props, and as the defaultValue of the template.json symbol that replaces it. The bump step walks past the placeholder on purpose — it is not a version number — and nothing resolved the other half. The default had read 1.6.0 since May while the same template’s non-CPM file tracked every release, so dotnet new bowire-plugin generated a plugin against a Bowire five minors old. Four cascade runs merged green, because the postcondition only ever looked at project files.

One marker for “was I explicitly asked for”, not one per plugin

A plugin that discovers from a bundled schema, or has an ad-hoc separate-target fallback, needs to know whether the caller pinned it with hint@url — and BowireServerUrl.Parse consumes that prefix before any plugin is reached. SSE and SignalR had each grown a private URL marker for it, kept aligned with a comment. TacticalAPI gated on the prefix itself, which can never arrive, so its discovery returned an empty list on every path while its own unit test passed. There is now one shared marker, and both private ones are gone.

The release gate agrees with the publish step

Two steps in release.yml resolved the same source chain and disagreed about what counts as curated. A template body cleared the first test and failed the second, so the gate would report “found curated block”, the publish step would discard it, and the release would go out carrying the auto-generated change list — the exact outcome the gate exists to prevent, with nothing failing.

Acknowledgements

The gRPC reflection work started from a walk against the sample rather than from a unit test, and the three defects it found were the kind only that walk produces. The same is true of the map: every fixture passed while the browser showed thirteen entities as eight tracks, a map that never framed its data, and a checkbox reading unchecked over a visible layer.

2.6.2 A surface the correlation scanner claimed but did not walk, and plugin settings that arrive

· Release, downloads and commits →

A small patch release: one defect in the correlated timeline, one plugin finally honouring the setting it advertises, and a documentation change that moves where plugin pages are written. No API, package, or wire changes.

Fixes

The correlation scanner walks interpretation payloads

ScanStep documented itself as covering “every JSON-bearing surface of one step” and skipped the interpretations. ScanFrame walked a frame’s body and data but not its interpretations either, so a streaming frame had the same hole as its step.

An interpretation payload is where a semantic widget’s data lives — a coordinate pair, a decoded identifier, a resolved entity reference — and it is carried verbatim through save and load. A recording can legitimately carry its only shared identifier there, and when it did, both steps stayed dark on the correlated timeline with nothing on screen to say why. Nothing errored, so there was no thread to pull.

The DIS plugin honours probeDuration

Kuestenlogik.Bowire.Protocol.Dis declared a probeDuration setting and then hard-coded three seconds. The control rendered, the value persisted across reloads, and discovery ignored it — so anyone who raised the window because a probe missed an entity watched the number stick and concluded the entity was not there.

It read like a forgotten line in that plugin and was not. Nothing upstream carried plugin settings back to any plugin until v2.6.0 built the seam, and this repository was still pinned to Bowire 2.5.0 — the type the fix needs did not exist in its compilation. MQTT, NATS and SOAP have honoured their settings since v2.6.0; DIS joins them now, and MQTT’s and SOAP’s settings are documented for the first time.

The publish pipeline can report its own failures again

Homebrew and Winget both failed during the v2.6.1 release and neither raised the issue that exists to make exactly that visible. Two defects stacked:

The reporter spliced its inputs into JavaScript template literals, and every caller’s hint text contains backticks — `homebrew-bowire`, `WINGET_TOKEN`. Those closed the literal, so the step died with a syntax error before filing anything. Inputs now travel through the environment, which also removes the injection shape.

The token guard used run: exit 0, which ends that step and nothing else. Every following step still ran, so an unconfigured channel did not skip — it failed, on the first step needing the token. The check is now its own job gating the rest through needs. That distinction is not cosmetic: a channel reporting red on every release teaches everyone to ignore red publish jobs, which is how seven releases of silent failure happened once already.

Documentation

Protocol plugins own the page that describes them

A plugin living in its own repository had its documentation here, in a repository it cannot push to. A claim and the code making it true could only be fixed in two separate commits — and they drifted: this site said DIS’s probeDuration was configurable for weeks while the plugin hard-coded three seconds.

Seven pages — AMQP, Akka.NET, DIS, Kafka, Surgewave, TacticalAPI, UDP — now live in their plugin repositories. The docs build discovers those repositories by topic, fetches one file from each, and links it automatically. The site pulls; nothing pushes at it, and the destination path is derived from the repository name, so a plugin contributes exactly one file at one path it does not choose.

Nothing on the site changes as a result — the pages moved verbatim.

Two stale preview marks removed

TacticalAPI’s node in the protocol diagram was dashed, from when it was a preview entry in May. It has been on nuget.org since. The cruise-ship deployment card carried a “preview” ribbon whose note said the multi-user and OIDC story was still being delivered; v2.6 shipped it, Kuestenlogik.Bowire.Auth.Oidc ships as a first-party provider, and v2.6.1 moved the last browser-only piece into the identity’s slot.

A stale preview mark is not neutral — it tells people not to rely on something they could have relied on for months.

AsyncAPI is filed as a discovery source

Its page describes a schema format Bowire reads, not a protocol Bowire speaks, which is why it appeared in neither the protocol diagram nor the protocol table. It had also been orphaned since May — reachable by URL, findable by search, unreachable by clicking. It now sits under Making requests, next to Auto-discovery.

Upgrading

Drop-in. Nothing in this release changes behaviour you can configure.

2.6.1 Storage isolation, the workspace list, and a rate limiter that throttled the workbench

· Release, downloads and commits →

A patch release for four defects found while using v2.6.0, plus the workspace list finally becoming per identity. Three of the four are things v2.6.0 itself introduced or moved, and one of them could make the workbench come up blank. No API, package, or wire changes — collections, recordings, flows and plugin settings load identically.

Fixes

The rate limiter no longer throttles the workbench serving itself

The per-client limiter added in v2.6.0 was global: it counted the workbench document, its bundle and its icons alongside the API calls it was meant to protect. A session that reloads a 5 MB document a few times and then polls reaches 600 requests inside a minute without trying, and the next page load came back 429 with an empty body — a blank window, indistinguishable from a broken server.

It now applies to /api, /mcp and /scim only. Everything the workbench serves to render itself is exempt, because a 429 there is an outage that looks like a crash: the browser has nothing to display and nothing to say.

This is what was behind the intermittent end-to-end failures reported as flaky. The status never reached anything that logged it, so finding it took instrumenting the harness to report the reload response’s status and body length rather than only the page state.

BOWIRE_DATA_DIR reaches workspace-scoped storage

The variable moved the plugin directory but not the user store, so every workspace-scoped artifact — collections, recordings, flows, plugin settings — stayed in the real ~/.bowire. A run that believed it was isolated wrote into the developer’s own storage and left directories behind.

Both resolvers now read the override through one function. The assertion added with the fix is not “each store lands in the right place” — that is a list somebody has to keep extending — but that the two resolvers give the same answer, which is a property that cannot rot as stores are added.

A workspace pointed at a checkout still wins over the override. That directory is the operator’s, not Bowire’s storage, and its contents must keep travelling with a clone.

Stopping an OAST listener during startup is a stop, not an exception

Shutting down while the listener was still coming up surfaced as an error rather than as the cancellation it was.

Changes

The workspace list belongs to the identity, not the browser

Multi-tenancy separated everything inside a workspace and nothing about which workspaces exist. Recordings, environments, collections, flows and plugin settings all resolved into the signed-in identity’s slot; the list naming those workspaces stayed in one browser’s local storage. Three consequences, each of which read as its own defect:

  • Two identities sharing a browser profile saw one list. Their data never mixed, but the inventory in front of them was not theirs, and deleting an entry removed it for the other.
  • One identity on a second machine saw no workspaces at all, while their directories sat on the server — unreachable, because nothing else knew the ids.
  • Deprovisioning and bowire users migrate both missed it. Neither could reach a list that was never on disk.

The inventory now lives at workspaces.json in the identity’s slot, behind GET/PUT /api/workspaces, with the browser copy demoted to a cache that reconciles on boot. Putting the file in the slot fixes the third point by construction rather than by adding a case: the migrator carries everything under the root that is not install state, and deprovisioning archives the slot whole.

Existing installs lose nothing. A single-user install has exactly one identity by definition, so the list in the browser can only be theirs and is adopted the first time the workbench starts. On a shared install it is not, because the first person to sign in would otherwise inherit whatever the previous one left in that browser profile — and the server decides which case applies, not the page.

Git-native workspaces are listed like any other. Their contents are shared on purpose, but the entry pointing at a checkout is a personal bookmark, which is what lets two people open the same repository without inheriting each other’s list.

Which workspace this window is looking at stays local. That is view state; syncing it would make opening a second tab move the first one.

Rollup and Contracts look like the rest of the workbench

Both panes were written against a dialect the workbench had left behind, and against four CSS classes that did not exist at all — so their buttons rendered as raw browser chrome beside rails full of styled controls. Their empty states are now the shared card every other rail uses, carrying the rail’s own icon, and the Rollup card says what to do next and separates “nothing there” from “files I could not read”.

The tables themselves are unchanged. Rollup is a table at heart and stays one; what changed is the frame around it.

Upgrading

Drop-in. The one behavioural change to know about: on the first start after upgrading, a workbench whose active workspace is not in the reconciled list reloads once to re-point its per-workspace storage. It happens at most once per browser.

2.6.0 Bowire v2.6.0

· Release, downloads and commits →

Multi-tenancy. An install with an identity provider now gives each authenticated person their own slice of state, provisioned from your directory — and the surfaces around that grew the gates, the headers and the storage discipline that a shared install needs.

Multi-tenancy is complete

  • Per-identity storage — recordings, environments, collections and flows live under <root>/users/<slug>/. A single-user install keeps the flat layout it always had.
  • A migration you can take back — bowire users migrate <subject> copies an existing single-user install into a person’s slot, once, recorded in the slot itself. It copies rather than moves, and --undo reverses it.
  • SCIM 2.0 provisioning — /scim/v2/Users and /Groups with discovery, bearer auth, filtering and paging. Deactivation is enforced, not recorded: the person is refused at the door and their slot moves out of reach, reversibly, until the purge window closes. PATCH handles both the Okta and the Entra dialects, and provisioning is no longer quadratic — a userName index replaced the directory walk each create performed.
  • An account chip, and administration that is auditable — an administrator can look at somebody else’s workbench; the cookie names whose slot, the authority is re-derived from the caller’s own token on every request, and both identities land in audit/actions.jsonl.

Live round-trips against a real Okta org and Entra tenant are scheduled for v2.7 (#639). The surface is complete and tested against both dialects’ fixtures; what remains is evidence from a real directory.

Plugin settings now reach the plugin

IBowireProtocol.Settings was a schema the workbench rendered into the browser’s local storage and nothing else. Four plugins shipped a control that persisted across reloads and changed nothing — worse than an absent one, because someone widens a probe window, watches the value stick, and concludes the thing they were looking for is not there.

Plugins now resolve IBowirePluginSettings and ask for their own values, stored per workspace. MQTT and NATS scanDuration and SOAP defaultSoapVersion honour their settings for the first time.

Flows are files

Flows were the last artifact the workbench kept only in the browser. They now save per workspace, which means a flow travels with a git-native workspace and is reviewable in a pull request, lives in the identity’s slot on a shared install, and can be run by bowire test without an export step first.

Security

Bowire’s own scanner is now pointed at the surface Bowire ships, and its findings are uploaded as ours rather than filtered away:

  • Baseline response headers on everything served — nosniff, a frame policy that survives embedding, and a nonce-based CSP that keeps the workbench working.
  • The MCP mounts sit behind the same auth gate as the workbench. They did not, and nothing in the compiler says so when a mount forgets.
  • Installing a plugin requires an administrator on an install with an identity provider. It had been open to every authenticated identity, and an install puts assemblies where the next start loads them into the server process.
  • Per-client rate limiting in the standalone host, with the budget stated in the response headers.

The standalone host honours the address you configured

UseUrls sits at the top of ASP.NET’s address precedence, and the tool called it unconditionally. Setting ASPNETCORE_URLS=https://… produced plaintext on 5080 with nothing logged — and, because the scheme was hardcoded, there was no way to serve the workbench over TLS at all.

The default port is now treated as what it is — a guess, not a decision — so ASPNETCORE_URLS, ASPNETCORE_HTTP_PORTS/HTTPS_PORTS and Kestrel:Endpoints all stand. --port still wins when you pass it, and says so when it overrode something. bowire mcp serve --bind http had the same defect and the same fix; that one matters more, because --token puts a bearer token on the wire.

TLS needs no Bowire flag — see docs/setup/standalone.md.

Breaking

Nothing in the public API is removed or renamed. Three behaviours change:

  • Plugin settings move from the browser to the workspace. Values already in local storage are lifted on first use and only cleared once the save is confirmed. With no workspace open the page says where a value would go instead of accepting one that goes nowhere.
  • Flows move from the browser to the workspace, the same way and with the same one-time lift.
  • disabled-plugins.json moves from the user slot to the storage root, where its effect always was — unloading a plugin swaps the registry every session reads. In single-user mode this is the same path as before, so nothing moves on a laptop. A multi-tenant install with per-identity copies stops reading them; they are inert and can be deleted.

Also

  • Host-declared environments: services.AddBowireEnvironment("Staging", …) lets an embedded host publish its own base URLs and tenant ids into the workbench instead of asking somebody to retype them out of appsettings.json. They are contributed on every start, never written to disk, and the workbench marks them as the host’s rather than offering an edit it would discard.
  • bowire proxy exits cleanly when interrupted while starting, instead of throwing, and reports its bound ports through a callback — with --port 0 that is the only place they exist.
  • Plugins resolve in two tiers — a machine-wide directory an administrator manages, and the running account’s overlay on top. Uninstalling something from the machine tier is refused, and names who can remove it.
  • Per-identity protocol visibility: hide a protocol from your own sidebar without touching anybody else’s, and find it again under a disclosure rather than losing it.

2.5.0 The CI pillar — design-time lint, latency gates, a contract matrix, one rollup, and the workbench in your editor

· Release, downloads and commits →

v2.5 is about what happens after you leave the workbench. The things Bowire already knew — your schemas, your recordings, your benchmarks, your contracts — now show up where the work actually gets judged: in CI, on a pull request, and in an editor panel next to the code.

215 commits since v2.4.0. Every workspace, collection, recording, mock and flow loads identically. Two changes are worth reading before you upgrade — see Breaking changes.

Highlights

The workbench, in VS Code (#101)

ext install kuestenlogik.bowire-vscode opens Bowire in an editor panel beside your code.

It does not bundle a CLI — it drives one. The extension uses a bowire you configured, one your repository pins in a tool manifest, or one on your PATH, and offers to fetch a verified copy when it finds none. That is the whole point: the workbench in your editor, the bowire in your terminal and the one in CI are the same binary reading the same collections.

Uninstalling the extension removes a CLI it downloaded. One you installed yourself is never touched.

Design-time lint — the review comments you keep writing (#189)

Most API design problems are caught in review if the reviewer is sharp — inconsistent naming, a field that returns a password, an unbounded list with no pagination, a missing version. That scales with reviewer attention, which does not scale.

bowire lint <snapshot|url> runs a typed rule engine over the schema and reports them without a reviewer having to be sharp that day:

bowire lint https://api.example.com --format markdown --fail-on high

Text for a terminal, markdown for a PR comment, JSON for whatever you build next. --fail-on gates the build — and none or an unrecognised level never fails, because a lint gate is advisory and a typo should not break a build that was passing.

Latency budgets as CI gates (#360, #232)

k6-style thresholds: a budget is either met or the run exits non-zero.

bowire bench run Weather/getCurrent --url rest@http://localhost:6000 --threshold "p95 < 200" --threshold "error-rate < 0.01"

Budgets are plain numbers — milliseconds for the latency metrics, a fraction for error-rate. Metric spellings are forgiving (p(95), error_rate and error-rate all land on the same metric), the budget is not: a unit suffix is refused rather than guessed at, and every threshold is parsed before the run starts so a typo in the fifth budget does not surface after two minutes of load.

Benchmarks also gained a scheduled shape — a cron-driven run that survives a restart, with the schedule readable and pausable from the workbench. Deliberately no “create” button in the browser: a schedule carries a target URL the server will call unattended, so authoring one stays on the CLI where the operator is explicit about it.

Contract matrix in the workbench (#364)

Consumer × provider, pass/fail, in one grid. Each cell carries how many interactions backed the verdict — 11 of 12 and 0 of 12 are very different mornings — and a pair that was never run reads as a dash rather than a blank, which would look like “checked, nothing wrong”.

One rollup over the artefacts you already write (#587)

Lint findings, contract results, benchmark envelopes, k6 summaries, SARIF, JUnit — Bowire already wrote all of it as structured files. bowire report rollup reads them into one portfolio view, available identically from the CLI, GET /api/report/rollup, and the bowire.report.rollup MCP tool.

Counts stay nullable on purpose: “no lint report was read” and “lint found nothing” are different statements, and flattening both to zero would let a missing report read as a clean bill of health.

A PR bot you can use (#183, #582)

The PR-report action moved to its own public repository and the Marketplace:

- uses: Kuestenlogik/bowire-action@v1

It posts one comment per PR — API-schema delta, test results, security findings, perf — and edits that comment on later runs instead of stacking new ones.

Secrets stay secret in CI output (#361)

Values marked secret are redacted in logs, reports, annotations and exported commands. Bowire’s CI story previously printed resolved variables into all of them.

Your repo can own its Bowire configuration (#172, #616)

// .bowire/project.json
{ "version": 1, "storage": "project" }

Collections, environments and recordings then live beside the code and commit, diff and review like any other file. Opt-in — a manifest that says nothing keeps the machine-wide store, so nothing moves under you.

Underneath, one resolver now decides where anything is stored. Fourteen files across six assemblies used to build ~/.bowire/… by hand, which meant the project opt-in reached some stores and silently missed others — the plugin directory, the proxy CA, the vuln-db cache and the MCP stores among them.

Starting Bowire from another program (#615)

bowire --port 0 --port-file ./run/bowire.json

The OS picks a free port; Bowire writes the address it actually bound once it is listening, and deletes the file on shutdown. The file exists if and only if the workbench is bound, which makes it the address and the readiness signal.

Do not scrape the startup banner for this. It is a log line, so it disappears at a quieter log level — and it used to be printed before the bind was known to have worked, so it could announce a URL that never served. That is fixed too, but --port-file is the contract.

Faster REST discovery (#585)

OpenAPI discovery took 5–8 s and flaked near the 8 s probe timeout. It no longer does.

Security

  • Path injection in the workspace-scoped endpoints (#617). ?workspaceId= and ?storageRoot= went from the query string into Path.Combine unchecked, across six endpoint files. A request could name any directory on the machine and have Bowire read or write there, reporting success. Both are now validated in one place: a workspace id must be a single segment from an allow-list; a storage root must be absolute, free of .., and name a directory that already exists.
  • A reverse-proxy upstream must be http/https. Uri.TryCreate(s, UriKind.Absolute) returns true for an absolute file path on Unix, so /etc/passwd parsed as file:///etc/passwd and was accepted as an upstream — on Linux only, which is where this runs in production.

Breaking changes

BrowserUiHost.HostRunner gained a parameter. Embedded hosts that substituted the runner (a test seam) now receive an onListening callback and must invoke it with the bound URL. Everything downstream of the address — the banner, --port-file, auto-opening a browser — hangs off that call, because with --port 0 the port is not knowable before the bind.

Storage paths route through IBowirePathResolver. If you referenced DefaultBowireUserStore.UserProfileRoot or built ~/.bowire/… yourself, use BowirePaths.Resolve(BowireStorageScope.Data, …) or take IBowirePathResolver as a dependency. The old property still works; it is simply no longer the thing that knows where your data is. See Storage locations.

Fixes worth naming

  • Workspace rows could act on the wrong workspace (#610). morphdom matched overview rows positionally because they carried no id, so after a delete a row kept the click handlers of the workspace that had just been removed while displaying the next one. The visible symptom was a delete button that did nothing; one step further it would have renamed or deleted the wrong workspace.
  • Collections were not workspace-scoped (#612). Every workspace read and wrote the same file, so whichever saved last handed its collections to all the others.
  • Streaming responses pushed the detail pane off screen with no scrollbar.
  • The per-workspace disk purge never ran. DELETE /api/workspace/{id} anchored its containment check on the user root, which it asked for with an empty filename — and the store rejects that, so every call ended in an unhandled ArgumentException before a single byte was deleted. The check anchors on the workspaces folder now, which also closes a gap the old prefix test left open: a sibling directory whose name merely starts with the root’s.
  • bowire contract publish met a hand-edited recording with a stack trace. The catch around the recording loader listed IOException but not InvalidDataException, which lives in System.IO and derives from SystemException — so every rejection the loader makes (unsupported format version, an ambiguous --select, a store with no recordings) escaped as an unhandled exception instead of the intended message and exit 65.
  • The release cascade waited for nuget.org indexing before dispatching to siblings (#236), so a sibling no longer builds against a version the feed has not published yet.

Acknowledgements

The competitive-research inputs behind the threshold gates (k6) and the redaction work (Hurl) came from a mid-2026 survey pass; both landed as table stakes rather than as differentiators, which is the right way round.

2.4.0 The Dev pillar — mock-from-schema, schema-watch diff, side-by-side, and CLI/UI/MCP parity

· Release, downloads and commits →

v2.4 makes Bowire a better development companion. You can now stand up a running mock from just a schema — no recording, no provider — so a consumer team can build against a realistic server before the real one exists. You can watch a schema and see exactly what changed since you last looked, and put two versions side by side. And a running theme this cycle: every capability is reachable from the CLI, the workbench, and MCP — no forced hops between surfaces, so CI and agents can do everything a human can.

200+ commits since v2.3.0. Every existing workspace, collection, recording, mock and flow loads identically, but three changes below are source- or wire-breaking for embedded hosts and API consumers — see Breaking changes.

Highlights

Mock from a schema — no recording needed (#179)

bowire mock --schema orders.openapi.yaml (also --grpc-schema / --graphql-schema) synthesises a running mock straight from the declared types — no captured traffic required. Declared examples win over type-defaults (#559): OpenAPI example / examples, proto2 field defaults, and a GraphQL @example directive. From the workbench, a Start a schema mock card does the same (#560).

A persisted, versioned mock-configuration sidecar (#558) refines the generated responses without re-discovering: per-field overrides and per-method conditional rules, edited in the schema-mock detail pane and applied live (#561).

A mock can now require authentication (#562): a 401 before replay when a request presents no credential — or the wrong one — with bowire mock --require-auth <token> or a workbench card. And the credential can be a captured auth recording (#563) referenced by id rather than pasted inline — created, listed, and removed from CLI, workbench, and MCP, either statically or by running a scriptable login → token flow.

Schema-watch diff — what changed since you last looked (#185, #48)

Point Bowire at a schema and it watches for change, reporting what changed (added / removed / altered methods and fields) rather than just that something did — without saturating the main thread, and without reporting a delta on a failed poll.

Side-by-side service version diff (#182)

Compare two versions of a discovered service in one view — the methods and shapes that moved between them.

CLI / UI / MCP parity (#538, #564, #253)

A running principle this cycle: anything you can do in one surface you can do in all three. Copy as Bowire CLI turns any request in the workbench into the exact command line that reproduces it. bowire version [--plugins] prints the running version and every loaded protocol plugin’s version. bowire call is now protocol-generic, and the invocation URL is split from the schema URL so you can point discovery and invocation at different hosts.

Discovery + catalogue (#537, #534, #544, #535, #536)

The service catalogue becomes a primary “add a source” entry point instead of a side path. Discovery now explains why it failed instead of reporting 0 services, and reports a partial fault (some probes succeeded, some didn’t) rather than hiding half the truth. An embedded Bowire lands on Discover on first run instead of a workspace-creation gate, and a successful response offers next-step handoffs so you keep moving.

Recordings (#539, #545)

A cross-protocol correlated timeline reads a multi-protocol recording as one transaction, and the correlation join now follows a renamed identifier across edges.

Scanner (#491)

bowire scan now runs Nuclei code: templates (behind an explicit opt-in), network: / tcp: / ssl:, and dns: templates instead of skipping them.

Breaking changes

Corrected after the fact. This release originally shipped saying “None”, which was wrong: the sections below were written during the 2.4 cycle but were left behind in upcoming.md instead of being folded into these notes. Artifacts (workspaces, collections, recordings, mocks, flows) do load unchanged — but an embedded host reading AutoCreateInitialWorkspace, a client parsing /api/services attempts, or an operator with a stale ~/.bowire/catalogue.json is affected.

The standalone bowire tool now wires the catalogue seam (#537)

bowire calls AddBowireCatalogue() unconditionally. With no provider configured this is a no-op — the accessor resolves to null and the endpoints short-circuit to an empty list, exactly as before.

The one behaviour change: the local provider defaults to ~/.bowire/catalogue.json. An operator who has that file left over from an earlier experiment and selects the local provider (via --catalogue-provider local, appsettings, or a persisted bowire catalogue use) will now see those entries merged into their workspace’s sources. Merged entries are not persisted to browser storage, so removing the file or the configuration removes them again.

BowireOptions.AutoCreateInitialWorkspace is now bool? (#535)

The property changed from bool to bool? so that “the host has no stance” is expressible and distinct from an explicit opt-out. null (the new default) resolves from BowireOptions.Mode — Embedded seeds a workspace, Standalone does not — and leaves the per-browser Settings → General toggle in control. true / false remain an explicit host stance that locks that toggle read-only.

This is source-breaking for readers, not writers:

options.AutoCreateInitialWorkspace = true;          // unchanged
bool seeded = options.AutoCreateInitialWorkspace;   // no longer compiles
bool seeded = options.AutoCreateInitialWorkspace ?? false;  // migration

Hosts that never touched the property need no change, but note that leaving it unset now means “seed” in embedded mode where it previously meant “don’t”. Pass false to keep the 2.2 behaviour.

window.__BOWIRE_CONFIG__.autoCreateInitialWorkspace follows the same shape and can now be null; a new hostName key next to it carries the resolved host display name.

/api/services — attempts changes from string[] to object[] (#534)

The attempts extension on the urn:bowire:discovery:no-match ProblemDetails body used to be an array of pre-formatted strings ("gRPC: connection refused"). It is now an array of objects:

{ "pluginId": "grpc", "plugin": "gRPC", "outcome": "error",
  "servicesFound": 0, "durationMs": 2011, "message": "connection refused" }

outcome is one of ok / empty / partial / error / timeout — see below for partial, which is additive on top of #534’s four. The array now covers every probed plugin, not only the failing ones, and it is also present (empty) on the urn:bowire:discovery:no-plugins body so clients can render one code path.

Migration — scripts that string-matched the old entries should read plugin + message instead, and filter on outcome rather than assuming every entry is a failure. Bowire’s own workbench accepts both shapes, so a newer workbench pointed at an older embedded host keeps working.

Two smaller wire-adjacent changes ride along:

  • The hint extension (Add a `protocol@` prefix …) is now omitted when there is no server URL to prefix. It previously emitted the nonsense text rest@ for an embedded host with no configured URL.
  • The bowire.discover MCP tool’s JSON result gained an attempts field next to url and services.

/api/services — additive partial outcome, details, and an opt-in success envelope (#544)

Three additive changes on top of the #534 shape above. Nothing moves for a client that ignores all three.

outcome gains a fifth value, partial: the plugin returned services and reported a fault while producing them, so its contribution is incomplete. It is deliberately not folded into ok — a dashboard has to be able to tell a populated-but-incomplete tree from a clean one. Only plugins implementing IBowireDiscoveryDiagnostics can produce it.

An attempt may carry an optional details array: the per-step breakdown behind message — one line per faulted MCP surface, one per well-known path a REST sweep tried. The field is omitted entirely when there is nothing to break down.

{ "pluginId": "mcp", "plugin": "MCP", "outcome": "partial",
  "servicesFound": 2, "durationMs": 431,
  "message": "2 services, but tools/list returned a payload this MCP revision rejects — …",
  "details": ["tools/list returned a payload this MCP revision rejects — …"] }

partial implies servicesFound > 0, which means it arrives on a 200, where the body has always been a bare BowireServiceInfo[] with nowhere to put a diagnostic. GET /api/services?includeAttempts=1 switches the success body to { "services": [...], "attempts": [...] }. Without the flag the bare array ships byte-for-byte as before, so no existing consumer moves; Bowire’s own workbench sends the flag and accepts both shapes (Array.isArray(body) ? body : body.services), so a newer workbench pointed at an older embedded host keeps working. There is no “fetch the attempts afterwards” endpoint on purpose — BowireDiscoveryProbe is stateless, so it would have to probe twice.

Plugin authors: build the diagnostic from locals of the call that produced it. The channel is a return value so that two concurrent probes of two URLs through one plugin instance cannot read each other’s diagnosis; stashing it in a field or a static ring buffer re-creates exactly that bug.

Telemetry: bowire.discover.count outcome vocabulary widened (#534, #544)

The outcome dimension now takes ok / empty / partial / error / timeout. canceled is gone (it reports as timeout), and a probe that succeeded with zero results now reports empty instead of ok. Dashboards or alerts filtering on outcome="ok" will see counts drop with no change in behaviour — sum ok + empty to recover the old total, and add partial if you are counting “probes that produced something”.

Acknowledgements

Thanks to everyone who built against schema mocks, watched schemas drift, and pushed on CLI/UI/MCP parity during the 2.4 cycle.

2.3.0 The Security pillar — OWASP API coverage, active probes, out-of-band detection, and a template cache

· Release, downloads and commits →

v2.3 turns Bowire into a real API security tool. bowire scan now runs the full OWASP API Security Top 10 — including protocol-specific probes for gRPC, GraphQL, WebSocket, SignalR, Socket.IO, SSE, MCP and MQTT — with a per-scan compliance overview (OWASP / CWE / CVSS). An opt-in active probe tier goes beyond passive checks into deliberately-mutating attacks (MQTT retained-message poisoning, WebSocket compression-bomb / slow-loris, gRPC concurrent-stream fork-bomb, SSE slow-consumption, MCP tool-call injection). A complete out-of-band (OAST) stack lands: an interactsh-compatible client, bowire scan --oast-server, a self-hosted bowire oast serve server, and a manual pen-test panel — so blind SSRF / RCE / XXE that leave nothing in the response can finally be proven. The curated template corpus becomes usable with bowire vulndb update / list, and the AI-assisted layer (threat model, JWT analyzer, OWASP panel, one-button scan, SARIF report) rounds out the lane.

Alongside the security work, the mock server reaches WireMock feature parity (request matchers, body predicates, response templating, scenarios, selective upstream proxy, fault injection, HTTPS, admin CRUD, verification) and a new Monitoring surface runs saved recordings as scheduled health probes with Slack / PagerDuty / OTLP signalers.

196 commits since v2.2.1. No breaking changes — every existing workspace, collection, recording, benchmark and flow loads identically, and all new surfaces are additive and opt-in.

Highlights

OWASP API Security Top 10 — the full suite (#173, #106, #381, #184)

bowire scan --suite owasp-api exercises all ten OWASP API Security Top 10 (2023) entries and rolls the result up per entry: exercised / clean / vulnerable, with the concrete probe behind each. Coverage reaches 10/10 including API6 (business-flow) and API10 (unsafe consumption). Protocol-specific probes go past HTTP: GraphQL introspection + query-depth / complexity / alias-batching, gRPC server-reflection + transport-auth, WebSocket message-size, SSE stream guards, MCP resource-traversal, and MQTT checks. A Compliance tab turns any scan into an OWASP / CWE / CVSS overview with a severity histogram and peak-CVSS readout.

Active probe tier — opt-in, mutating attacks (#395–#400)

Passive checks tell you what a target discloses; the new --active tier tells you what it does under attack. Off by default and gated behind explicit opt-in, it adds MQTT retained-message poisoning + wildcard-subscribe privilege + will-message abuse, WebSocket compression-bomb amplification + slow-loris, SSE slow-consumption, and a gRPC concurrent-stream fork-bomb — each namespacing and cleaning up its own side effects, with an honest verdict about how far it got. Budgets (--active-duration, --active-concurrency, --active-expected-topic) keep it bounded.

Out-of-band detection (OAST) — end to end (#35 Phase 2f)

Blind SSRF / RCE / XXE leave nothing in the response; the only proof is the target reaching out to a host you planted. v2.3 ships the whole chain:

  • bowire scan --oast-server <url> plants callback hosts into Nuclei OAST templates ({{interactsh-url}}, part: interactsh_protocol) and proves a finding only when a genuine callback lands — failing closed when no server is configured, never reporting an unproven blind finding.
  • bowire oast serve is a self-hosted, interactsh-wire-compatible interaction server (DNS + HTTP catchers + register/poll API) — no third-party service, no Go binary. --token gates it; --oast-token authenticates the client.
  • A manual OAST panel in the Security rail generates callback payloads to paste by hand and shows a live feed of what hit the catcher — the interactive counterpart for pen-testing, self-hosted.

The crypto (RSA-OAEP + AES-CTR) is pinned against the NIST SP 800-38A vectors and the client↔server pairing is tested against the real server, not in isolation.

bowire vulndb — the template cache (#26)

bowire vulndb update fetches the curated Kuestenlogik/Bowire.VulnDb template set into ~/.bowire/vulndb (latest GitHub release by default; --source for a checkout / tarball / URL and --ref to pin, for air-gapped and reproducible installs). bowire scan reads that cache by default, so an operator runs one update and then scans without repeating --templates. bowire vulndb list shows what’s cached.

Monitoring — scheduled health probes (#102)

bowire monitor run <probes> runs saved recordings on a schedule, records every outcome to an append-only ledger, and signals on pass↔fail transitions through opt-in Slack, PagerDuty, and OTLP channels (bowire.monitoring.* instruments). A read-only Monitoring rail renders the live ledger — per-probe status, a latency sparkline, and the outcome history. Sessions resume their cadence across restarts.

Mock server — WireMock feature parity (#401–#411, #430)

The standalone mock server grows from happy-path replay into a full mocking tool: request-matching predicates (query / header / cookie, regex / glob paths, stub priority), body matchers (equalToJson / JSONPath / XPath / JSON-schema / regex), response templating (helpers / math / faker + bodyFileName + a transformer hook), a named scenario state machine, selective upstream proxy (proxyBaseUrl — mock some routes, forward the rest), fault injection (malformed chunks, faults on unmatched requests), HTTPS/TLS (+ a Docker image), per-stub admin CRUD on a running server, and a verification API (verify / findAll / near-misses).

AI-assisted security (#104–#107)

With Kuestenlogik.Bowire.Ai in the process, the Security rail gains an AI threat model (rank endpoints by attack surface), a JWT analyzer (deterministic claim-by-claim flags + an AI narrative), an OWASP API Top 10 panel (per-method status + a concrete probe per risk), a one-button scan that chains threat-model → templates → fuzz → triage → report, and a markdown security report from SARIF with diff-vs-baseline. The deterministic core always runs; the AI layer degrades gracefully when no model is connected.

Scanner foundations

Rounding out the lane: a headless auth-flow runner (bowire scan --auth-flow — login → token chain injected into every probe, secrets from {{env.NAME}}), CVE lookup against discovered server banners, HAR import (Chrome DevTools network tab → recordings, with secret redaction), an endpoint spider with confirm/ignore triage, a schema-aware mutation engine (bowire scan mutate), and Nuclei template compatibility including the Phase 2g DNS-transport translator. bowire scan treats SignalR / Socket.IO / MCP / gRPC / gRPC-Web as HTTP-class transports so their templates run.

Breaking changes

None. v2.3 is purely additive; every new surface is opt-in.

Acknowledgements

Thanks to everyone who exercised the security lane against real targets and reported sharp edges during the 2.3 cycle.

2.2.1 Workbench fixes — response map, splitter parity, pane view-switcher, Compose inputs

· Release, downloads and commits →

A patch release rolling up four workbench fixes surfaced during v2.2.0 hands-on testing. No API, package, or wire changes — workspaces, collections, recordings and flows load identically.

Fixes

Response Map pane no longer appears for methods with no coordinates

The split JSON Map response layout was showing an empty Map pane for methods whose response carries no coordinate data (e.g. Petstore’s getPetById). preferredSplitExtensionForMethod fell through to a race-only fallback — which returns the map split whenever the Map plugin is loaded — even when the effective-annotation cache for the method was resolved and carried no split-kind. It now treats a resolved cache as authoritative and returns no split, so the JSON viewer fills the full response width and the empty Map (and the dead space it created) are gone. The fallback still applies during the genuine first-render race before /api/semantics/effective lands.

Request/Response splitter matched to the sidebar splitter

The divider between the request and response panes is now visually and behaviourally aligned with the sidebar splitter: a 1 px hairline with a 0-footprint ±5 px hit overlay (replacing the earlier 10 px bar whose transparent half left a gap between the line and the response pane), a single centred 3-dot grip, and the same dwell-gated accent-on-hover. The two directional maximize chevrons stay — the request/response divider collapses in both directions (request-only / response-only), unlike the one-way sidebar splitter.

New segmented pane view-switcher

A segmented Request · Split · Response control now sits in the statusbar next to the split-orientation toggle — the explicit, discoverable counterpart to the splitter’s drag + edge chevrons. It drives the same maximize state (window.__bowirePaneView, rebound per render), so the switcher, the chevrons, and a splitter drag stay in sync with no new state model.

Compose parameter / header inputs no longer jump

Typing into a new Compose Parameter or Header row could tear focus off the field (“two clicks to activate”) and shuffle the row’s cells (“the input springs”). The KV rows lacked stable ids, so morphdom matched them positionally and replaced the input nodes on the focus-out render that appends a fresh trailing row. Each row now carries a stable, non-enumerable per-row id, so morphdom preserves the input nodes across that render — focus survives and the cells stay aligned. The id never leaks into saved presets or request exports.

Acknowledgements

Thanks for the fast hands-on feedback on the v2.2.0 build that surfaced all four.

2.2.0 The Test pillar — flow assertions, a headless CI runner, snapshot / contract / data-driven testing

· Release, downloads and commits →

v2.2 turns Bowire from “drive a call and look at the response” into a real testing tool. A flow can now carry assertions; bowire test runs those flows headless and emits JUnit / SARIF / HTML so a pipeline fails on a regression; responses can be pinned with snapshot baselines, parameterised with data-driven rows, verified against Pact-style contracts, and stressed with mock fault injection. A new Regression Coverage surface tracks per-method run history, and the variable resolver gains its last three sources — OS keyring, AI re-roll, and per-frame streaming resolution. Underneath, the Mocks + Traffic rails collapse into a single Intercept rail, the reverse proxy moves to YARP, and the pluggable-workbench cut-over (#294 / #306) finishes.

60 commits since v2.1.1. Two breaking changes for embedded hosts and operators: the Mocks + Traffic rails merged into one Intercept rail (state migrates automatically at first paint), and Kuestenlogik.Bowire.Protocol.TacticalApi is now opt-in rather than bundled. See Breaking changes below for the migration paths. Existing workspaces, collections, recordings, benchmarks and flows load identically.

Highlights

Flow Assertions — the expectations engine (#342)

A flow step now carries an expectations list evaluated after the response returns: status, body-path (JSONPath + equals / exists / matches / less-than / …), header, and latency checks. The schema, the runtime FlowExpectationEvaluator, and a UI editor ship together (T1 — the foundation the rest of the Test pillar builds on). Each expectation reports pass/fail with the actual value inline so a failure reads like a test assertion, not a diff.

bowire test — the headless CI runner (#344, #181, #100)

bowire test <flow.json> executes a flow end-to-end against a real backend, evaluates its expectations, and reports through the format your pipeline wants: --report path.html, --junit path.xml, --sarif path.sarif (GitHub Code Scanning), and --annotations (inline ::error on the PR diff). --fail-on any|never, --workspace <dir> (aggregate every flow in a git-native workspace), --env / --env-file / --vars, and --base-url round out the surface. The recording-format test-collection runner (#100) and the flow runner auto-detect the input shape, so one command covers both.

Snapshot testing — capture-once, diff-on-change (#171)

A flow step can carry "snapshot": { "mode": "exact" | "structural", "ignore": ["$.ts", …] }. The first run captures the response as a baseline under __snapshots__/<flow>/<step>.snap.json (checked in next to the flow); every later run diffs against it and fails with the drifted JSON paths. ignore marks dynamic fields (timestamps, generated ids) whose value may vary — their kind is still checked. --update-snapshots re-baselines after an intended change.

Data-driven flow steps — inline / CSV / generator (#174)

A step can carry a data source and run once per row: an inline JSON array, a CSV file (resolved relative to the flow, RFC-4180 quoting), or a deterministic generator (range, or seeded random — same seed, same rows, on every .NET version). Row columns join the {{var}} resolver scope and shadow --env values of the same name. Each row reports as stepId[label] so JUnit / SARIF / HTML group the parameterisation as one step family. Zero-row sources and expansions beyond 100 000 rows fail loudly instead of passing vacuously or hanging CI.

Assertion DSL for collection / recording replay (#180)

The v2.1 test-collection format gains a real assertion DSL so a recording replay can assert structural checks (“status is OK”, “body.id matches”) the same way a flow does — the shared expectation vocabulary that both the recording runner and the flow runner evaluate.

Contract testing — Pact-style publish / verify (#191)

bowire contract publish turns a recording into a Pact-shaped consumer contract; bowire contract verify replays it against a provider and structural-matches the response, projecting the result into the same run report the test runner emits. The broker integration (--broker-url) is opt-in so nothing egresses by default. Structural body matching reuses the flow runner’s FlowSnapshotComparer.

Regression Coverage surface (#343)

The Discover sidebar now carries per-method coverage chips backed by a run-history store (T3): at a glance, which methods have been exercised, when they last passed, and where a regression crept in. Coverage is the third leg of the Test pillar next to assertions (T1) and the CLI runner (T2).

Mock-server fault injection (#170)

A mock can now inject failure instead of only replaying the happy path: added latency, error status codes, dropped connections, and partial / truncated responses — so a client can be tested against 503s, timeouts, slow responses and dropped frames without a bespoke broken server.

Variable resolver Phase 5 — OS keyring, AI re-roll, streaming (#208)

The resolver gains its last three sources. {{keyring.service/account}} reads secrets straight from the OS credential store (Windows Credential Manager / macOS Keychain / libsecret) via the new optional Kuestenlogik.Bowire.Keyring package — local-first, on-demand, scrubbed to *** on export, and available headless via bowire test --keyring. {{ai.*}} vars get a ↻ re-roll button in the resolver preview, plus bowire test --ai-seed <seed> for byte-reproducible CI resolution without a model call. And streaming sends now resolve per outbound frame (WebSocket / gRPC duplex), so {{runtime.now}} / {{ai.*}} / {{keyring.*}} resolve live on every frame instead of only the initial connect.

Mocks + Traffic → a single Intercept rail, now on YARP (#334, #315, #335, #336, #341, #323)

The Mocks and Traffic rails collapse into one Intercept rail with four sub-tabs in a locked order — Captured, Live overrides, Mock servers, Settings — with cross-rail transition CTAs (#335) and an activation empty-state that offers a Reverse-Proxy CTA (#336). The interceptor’s reverse-proxy forwarder swaps the hand-rolled HttpClient for YARP IHttpForwarder (#341 / #323), so streaming, upgrades, and header handling match a production proxy. See Breaking changes for the rail-merge migration.

Plugin lifecycle backend (#340)

Every loaded plugin gains a lifecycle surface — Restart / Unload / Load / Reset-storage / Health — behind POST /api/plugins/{id}/lifecycle/{action}, wired into the Settings → Plugins rows so an operator can recycle a misbehaving plugin without restarting the host.

Pluggable field-detector auto-discovery (#345)

Semantic field detectors (the seam the Map extension uses to light up coordinate.wgs84 payloads) are now auto-discovered via the [BowireExtension] attribute — a package drops a detector in and the workbench picks it up, no Core edit.

Workspace-deletion hardening (#337, #348)

Workspace delete now does a cascade purge with a Soft / Hard toggle, and its Undo is decoupled from the Trash drawer: the action-log entry carries the full snapshot inline so Undo reads from the entry itself. The two surfaces get independent retention (Trash: operator-curated days; Action log: last-200 sliding window). Settings → Workspace… expands into four scope-clear sub-pages (#348).

Pluggable workbench, finished — Phase G + Bootcamp (#306, #347)

The #294 pluggable-workbench cut-over completes: every remaining rail / module is a package contribution behind the #314 renderer-key seam, and the last core-resident descriptors (Home / Discover / Workspaces) are consolidated into Core. New Bootcamp lessons (#347) walk operators through Intercept, Flow Assertions, the bowire test CLI, Coverage, plugin lifecycle, and workspace deletion.

Fixes + test infrastructure (#346, #349, #338, #213, #312, #38)

Compose tab Duplicate now deep-copies request state (morphdom stale-closure fix, #346); the Parameter → Header sub-tab preserves Parameter row inputs (#349); rail-load calls are guarded for embedded hosts that don’t reference every rail package (#338). The SidecarFake JSON-RPC handshake is finished to unlock SidecarBowireProtocol coverage (#213), a coverage-gap audit kicked off the cycle (#312), and CLI Phase-3 polish landed completion + validators + error rendering (#38). A new JS unit-test net (node --test over the wwwroot fragments) + in-process CLI integration tests guard the workbench and CLI against regressions.

Breaking changes

Mocks + Traffic rails merged into a single Intercept rail (#334)

The Mocks rail and the Traffic rail (which itself unified the earlier Proxy + Intercepted rails) collapse into one Intercept rail with four sub-tabs in a locked order:

  • Captured — passive observation of flows captured by UseBowireInterceptor() (was Traffic → “Flows”).
  • Live overrides — selective response substitution inside the interceptor pipeline (was Traffic → “Mock Rules”).
  • Mock servers — standalone mock-server-from-recording hosts (was the entire Mocks rail).
  • Settings — interceptor / proxy config; adapts to Standalone vs Embedded deployment.

Migration is automatic at first paint (idempotent). On boot, prologue.js:

  • Rewrites localStorage.bowire_rail_mode from mocks / traffic / proxy / intercepted → intercept, and seeds the sub-tab discriminator (bowire_intercept_sub_tab) from the legacy mode (mocks → mock-servers; traffic+flows → captured; traffic+mocks → live-overrides; traffic+settings → settings; proxy / intercepted → captured).
  • Rewrites localStorage.bowire_sidebar_view (mocks / traffic / proxy / intercepted → intercept).
  • Collapses any mocks / traffic entry in localStorage.bowire_enabled_rails into a single intercept.

Embedded hosts that referenced the deleted descriptors must update their DI registrations:

  • BowireTrafficRailContribution → BowireInterceptRailContribution.
  • BowireProxyRailContribution, BowireInterceptedRailContribution, BowireMocksRailContribution, BowireEnvironmentsRailContribution → no replacement (the Environments surface renders inside Workspaces; Mocks now lives inside the Intercept rail’s Mock servers sub-tab).

.bww workspace files are unaffected — they don’t persist rail mode.

Kuestenlogik.Bowire.Protocol.TacticalApi retired from Bundle.Workbench

The TacticalApi protocol is now opt-in. Operators who need the Rheinmetall Situation service install it explicitly:

dotnet add package Kuestenlogik.Bowire.Protocol.TacticalApi

or, once the plugin marketplace ships, via Settings → Plugins → Install. Bundle.Workbench stays the universal-web-protocol set (REST, gRPC, GraphQL, MQTT, WebSocket, SSE, MCP, SignalR, JSON-RPC, OData, Socket.IO); domain-specific protocols follow the opt-in pattern. Nothing to do if you didn’t use TacticalApi — if you did, a one-line dotnet add package restores the surface. Related upstream fix: Bowire.Protocol.TacticalApi v1.0.4 gates its DiscoverAsync on the tacticalapi@ URL-scheme prefix, so it no longer surfaces Situation methods for unrelated sources like a plain Petstore OpenAPI URL.

Acknowledgements

Thanks to everyone who exercised the v2.2 release candidates and filed issues against the Test-pillar surfaces.

2.1.0 Compose rail, pluggable workbench, transparent interceptor, and a tactical map

· Release, downloads and commits →

v2.1 turns Bowire from a discovery + invoke workbench into a real composition surface. The new Compose rail carries a Hoppscotch-style request builder with a protocol picker, per-protocol layouts, history persistence, binary uploads, and full Collections + Presets integration. Underneath, the workbench is now pluggable: rails and modules ship as package contributions, so the standalone Tool, embedded MapBowire(), and operator-built distributions can pick exactly the surface they want. The new interceptor middleware lets a host hand Bowire every inbound request without client setup. A MapLibre extension + a stand-alone TacticalApi sample demo coordinate-bearing payloads on a live map with bi-directional JSON↔map sync. Streaming subscriptions are now legible, the JSON viewer reaches Hoppscotch parity, and the guided tour walks first-time operators through every rail.

300+ commits since v2.0.1. Three packaging breaking changes for embedded hosts: the standalone Kuestenlogik.Bowire.Rail.Collections package is retired, the whole Rail. package prefix dropped (e.g. Rail.Compose → Compose, Rail.Benchmarks → Benchmarking), and the interceptor surface (Proxy + Intercepted + Traffic rails + the middleware that lived in Core) consolidated into a single Kuestenlogik.Bowire.Interceptor package. See Breaking changes below for the one-to-one mapping table. Existing workspaces, collections, recordings, benchmarks load identically; operator-saved railMode / enabled-rails state migrates on first boot.

Highlights

Compose rail (#293, #289, #290, #291, #295)

The old Design rail is rebranded and rebuilt as Compose — a single-line Hoppscotch-style request bar at the top, per-protocol layout below, history + collections + presets on the side. The protocol picker switches the layout in place: REST gets a URL + KV-table chrome, gRPC gets service / method / JSON body, MCP gets tool / arguments, MQTT gets topic / QoS, WebSocket / SSE get connect / subscribe shells. History persists across reloads, binary uploads ride a base64 side-channel through the wire, and benchmarks can re-run any historical request directly. A Compose tab supports right-click → Duplicate tab so you can fan out a request without losing the original. The toolbar lives where you’d expect (no toolbar in Raw mode, of course).

Pluggable workbench — rails + modules as package contributions (#294)

Bundle.Workbench is now a meta-package: every rail, every module, every extension is a separate project that registers itself via [BowirePlugin] / [BowireExtension] attributes. The standalone Tool depends on Bundle.Workbench; embedded hosts can drop the bundle and pick per-package references for a smaller surface (e.g. just REST + Compose without the AI module). Bundle.Minimal carries the Core + Home + Discover only — useful for “tiny embedded probe” deployments. Settings → Plugins now lists every loaded protocol AND every UI extension with kind / capability chips so the operator can see what’s installed at a glance.

Transparent in-process interceptor (#153)

app.UseBowireInterceptor() registers a pass-through middleware that records method / path / headers / request body / response status / response headers / response body / latency for every request the host receives — from any client, with zero client-side setup, no cert trust, no separate process. Captured flows land in the workbench’s new Intercepted rail live over SSE. When the operator starts a recording, intercepted flows auto-append as recording steps — point any client at the host, click stop, replay.

MapLibre extension + TacticalApi integration (#new)

A new Kuestenlogik.Bowire.Map extension auto-mounts a MapLibre GL JS viewer whenever a response carries the coordinate.wgs84 semantic kind. The Wgs84CoordinateDetector picks up {lat, lon}, {latitude, longitude}, AND {latitudeCoordinate, longitudeCoordinate} shapes (TacticalAPI naming) anchored by anchored case-insensitive regex. Bidirectional sync: hover on a JSON {lat,lon} block highlights the matching map pin; hover on a pin highlights the JSON. Click on a pin scrolls the JSON to the line and auto-expands collapsed ancestors. Right-click on a coordinate offers Center on map; double-click on either side copies the path to clipboard. A right-side gutter hint surfaces the semantic kind on hover (wgs84 coordinate).

The new Sample.TacticalApi server (separate samples/ project) hosts Rheinmetall’s Situation gRPC service with eight NATO-phonetic-callsign tactical entities (Alpha-1 Recon UAV, Bravo-2 Air Defence Btry, …) drifting on a per-object sine-wave so subscriptions feel live. Bundle.Workbench ships the Kuestenlogik.Bowire.Protocol.TacticalApi plugin out of the box.

Streaming subscriptions, finally legible (#new)

The action button adapts to the active method type: Execute for unary, Subscribe for server-streaming (changes to Stop while a stream is live), Connect / Disconnect for bidirectional and client-streaming. A live state badge in the response pane shows ● Subscribed — 0 msgs → ● Receiving — 42 msgs → ○ Idle — 42 msgs after 5 s without a frame → ○ Closed on stop → × Error on stream failure. A statusbar pill lists every active subscription across methods; click → dropdown with switch / stop / copy-path / “stop all”. The action button keeps a stable footprint so layout doesn’t jitter when methods change.

Hoppscotch-parity JSON response viewer + toolbar (#302, #new)

The response viewer renders one JSON line per row with a sticky left gutter carrying line numbers AND the collapse chevron — chevrons line up on the same vertical column regardless of indent depth. Click anywhere on an opener line OR the gutter chevron to toggle. Click = toggle, double-click = Copy path, right-click = unified menu with Copy ${response.X}, Copy path, plus extension-contributed items (Center on map when the map plugin resolves the target). A dedicated JSON toolbar — Expand all, Collapse all, Wrap long lines, Search (Ctrl/Cmd+F), Copy response, Download — sits above the viewer and only renders when the JSON tab is active in tab mode (so the map widget gets its own toolbar real estate). Expand / Collapse rebuild the viewer in place — no full render, so the map’s WebGL canvas survives.

Tab ↔ Split layout for response viewers (#new)

Server-streaming and unary response panes get a tab-strip with JSON + per-extension widget tab + a layout toggle. Click the toggle → split-pane with JSON on one side, widget on the other; click again → tabs. Per-method layout persists in localStorage so the operator’s preference for each method is remembered. The split-layout decision is extension-driven — Core asks the framework preferredSplitExtensionForMethod(svc, method) rather than hard-coding the coordinate.wgs84 kind. Layering stays clean: Map-specific code in Kuestenlogik.Bowire.Map, Core stays generic.

Guided tour — page-navigation, alternative paths, per-rail empties (#281, #303)

A spotlight-overlay tour engine walks first-time operators through every rail: workspace creation, source-URL add, method invoke, recording, collection composition, flow building, traffic capture, security scan. Each rail’s empty state surfaces its own secondary tour. The tour engine handles dialog-modal yields, alternative paths (e.g. operator already has a workspace), advance modes (on-cta / on-event / next-button), saved-once dismissal, and reusable fragment includes (_createWorkspaceSteps). Six rail-specific tours land: bowireStartCaptureRecordingTour, bowireStartBuildCollectionTour, bowireStartBuildFlowTour, bowireStartComposeRequestTour, bowireStartCaptureTrafficTour, bowireStartSecurityScanTour.

REST auto-probing of well-known OpenAPI paths

When the operator types a bare URL like http://localhost:5181, the REST plugin now probes a short list of well-known OpenAPI document paths: /openapi.json, /openapi/v1.json, /swagger/v1/swagger.json, /swagger.json, /v3/api-docs, /v3/api-docs.yaml, /api-docs, /openapi.yaml. First valid OpenAPI document wins; the resolved spec URL is cached per origin for the next call. Services are tagged with the operator-supplied URL (not the probed spec URL) so the Discover sidebar groups everything under the URL the operator typed. Skips probing when the supplied URL already looks like a spec URL.

Discovery self-origin gate for SSE / WebSocket / SignalR

Three protocol plugins (SSE, WebSocket, SignalR) used to scan the workbench host’s own EndpointDataSource and return locally-registered endpoints regardless of the source URL the operator asked about. With the Tool’s --enable-mcp-adapter flag, the workbench’s own /mcp route leaked into every external serverUrl the operator added — read as a phantom “SSE Endpoints” service tagged to the wrong source. The new SelfOriginCheck.IsSelfOrigin(serverUrl, sp) helper compares against IServer.Features.IServerAddressesFeature (with loopback aliases + Kestrel wildcards). Each protocol’s discovery now gates its local scan on this check — operator-registered endpoints keep working in both directions.

Catalogue plugin — provider seam (#136)

Kuestenlogik.Bowire.Catalogue lands as a provider abstraction with three first-party providers: local file (JSON catalogue), HTTP (fetch + parse remote catalogue), and Consul (service registry integration). Workspaces can pin a catalogue source so the Discover rail enumerates services from a curated list instead of probing one URL at a time.

Topbar — undo / redo / aggregated trash (#296), responsive overflow (#297)

The topbar’s right cluster grows Undo / Redo buttons backed by the action log (cross-reload undo from #194 Phase 2) and an aggregated Trash drawer that lists every soft-deleted workspace / recording / collection / mock / flow / environment with a one-click restore. The right cluster also gains horizontal overflow handling — buttons collapse into a … overflow popover when the viewport gets tight, preserving the most recent items.

Workspaces — sort + manual ordering (#279)

The Workspaces rail picks up a sort dropdown (last-used / created / alphabetical / manual) and a drag-handle on each row for explicit manual ordering. Per-user preference persists; manual ordering survives a rebuild of the underlying workspace list.

Benchmarks — random targets + diff banner + exports (#231, #233, #234)

Benchmark target shapes get a new random option that picks a target from a pool per iteration — useful when a downstream service partitions by URL. The previous-run diff banner shows p95 / rps / status histogram delta against the last run for the same benchmark. Results export as CSV (per-method or per-iteration), k6-summary JSON, and OTLP metrics files for ingestion into Grafana / Tempo / Prometheus.

MCP-over-MCP forwarder (#286)

bowire mcp serve --attach localhost:5198 --port 5199 boots a thin Bowire process that relays every incoming MCP tool call to a heavier Bowire running on the operator’s workstation. tools/list, tools/call, prompts, resources, resource templates — all marshalled to the parent. The parent gains --token <secret> bearer-auth (--bind http only); the child passes the secret with --attach-token <secret>. Useful when an LLM agent on a CI runner / container should drive the workstation Bowire without sharing the parent’s MCP socket directly.

Bowire emits <link rel="mcp" href="/mcp"> + <meta name="mcp-endpoint" content="/mcp"> in the workbench HTML so MCP-aware crawlers and AI agents can discover the tool surface. Prefix-aware so embedded MapBowire("/bowire") emits /bowire/mcp correctly. Separately, Edge Reader Mode used to extract stale JSON snippets and render them as “the article” — og:type=website + role="application" on the workbench root signal “app, not document” so Reader Mode stays out of the way. Deliberately no robots=noai — Bowire is meant to be AI-driven.

Mock host + Mocks rail consolidated into one package

Kuestenlogik.Bowire.Mock now ships both the mock-host runtime (MockServer, MockHandler, MockOptions, MockServerOptions) and the Mocks rail contribution (BowireMocksRailContribution) in a single package — same pluggable pattern Help follows after #324. The provisional Kuestenlogik.Bowire.Rail.Mocks package is retired before its first NuGet release, so existing operators on Kuestenlogik.Bowire.Mock 1.0.x see no migration. Bundle.Workbench keeps referencing Mock; embedded hosts that want the Mocks rail simply reference Kuestenlogik.Bowire.Mock and the contribution self-registers.

Welle 2 — Rail. prefix dropped, interceptor consolidated (#325)

The Mock + Help collapses above were Welle 1 of a v2.1 package-shape cleanup. Welle 2 finishes the job by dropping the Rail. prefix from every remaining package id, folding two descriptor-only rails into Core, and consolidating the three deprecated interceptor descriptors AND the interceptor runtime into a single Kuestenlogik.Bowire.Interceptor package.

Renames (package ids change, contribution Id strings keep verbatim so saved railMode + bowire_enabled_rails keep dispatching):

Old package New package Notes
Kuestenlogik.Bowire.Rail.Home (folded into Core) descriptor-only, no per-rail JS
Kuestenlogik.Bowire.Rail.Discover (folded into Core) descriptor-only, no per-rail JS
Kuestenlogik.Bowire.Rail.Workspaces + …Rail.Environments Kuestenlogik.Bowire.Workspaces env vars are workspace-scoped — same package
Kuestenlogik.Bowire.Rail.Compose Kuestenlogik.Bowire.Compose rename only
Kuestenlogik.Bowire.Rail.Recordings Kuestenlogik.Bowire.Recordings rename only (plural stem kept)
Kuestenlogik.Bowire.Rail.Flows Kuestenlogik.Bowire.Flows rename only (plural stem kept)
Kuestenlogik.Bowire.Rail.Proxy + …Rail.Intercepted + …Rail.Traffic Kuestenlogik.Bowire.Interceptor three rails + middleware + reverse-proxy host + /api/intercepted/* + /api/tools/reverse-proxy/* all in one package
Kuestenlogik.Bowire.Rail.Benchmarks Kuestenlogik.Bowire.Benchmarking gerund matches the activity-rail pattern (Compose / Mock / Discover / Help / Telemetry)

The Interceptor consolidation is the biggest shape change in v2.1. The interceptor middleware (UseBowireInterceptor), the reverse-proxy host (BowireReverseProxyHost), the stores (InterceptedFlowStore, InterceptorMockStore), the reverse-proxy registry (ReverseProxyRegistry), and the /api/intercepted/* + /api/tools/reverse-proxy/* endpoints all moved out of Core into the new Kuestenlogik.Bowire.Interceptor package. Two new contribution seams (IBowireServiceContribution, IBowireEndpointContribution) let Core discover the package’s DI registrations + endpoint mounts without taking a compile-time reference on its types. Embedded hosts that don’t reference Kuestenlogik.Bowire.Interceptor lose the entire interceptor stack — no middleware, no rails, no admin endpoints. The standalone Tool keeps the same shape because Bundle.Workbench pulls the package in transitively.

Settings IA tightened in the same wave: the top-level Rail modes row renames to Rails. See the dedicated “Settings tree organized by extension point” highlight below for the full new layout — operator review caught that the first-pass “collapse Modules + Plugins + Assistant into one bucket” was too coarse and that Data is app-wide rather than workspace-scoped, so the IA was reworked around extension points.

Settings tree organized by extension point

Settings groups per-plugin configuration under each plugin’s extension point — protocols, UI widgets, modules, formats, tools, discovery providers — instead of one big bucket. A plugin contributing at multiple points shows up under each with its specific settings. Plugin lifecycle (load, unload, restart, reset storage, install) lives in its own Plugins section; load actions surface a v2.2 placeholder while the listing + per-plugin status work today. Data management stays at the top level (it’s app-wide, not workspace-scoped — the workspace list itself lives in app storage).

Breaking changes

  • Collections rail retired. Default-off since #304; Compose rail’s side panel (#295) is the canonical surface for collections + presets. Operators with railMode === 'collections' saved are migrated to Compose on next boot. The standalone Collections package (Kuestenlogik.Bowire.Rail.Collections) is removed from Bundle.Workbench; embedded hosts that referenced it explicitly need to drop the reference. The renderCollectionDetail + renderSaveToCollectionDropdown + importPostmanCollection + runCollectionItem helpers + the Parallel-sessions panel moved back into core so the Workspaces-rail collection-detail leaf and the Recordings-rail parallel-sessions panel keep working without the package.
  • Rail. package prefix dropped (#325). Every standalone rail NuGet renamed: Kuestenlogik.Bowire.Rail.Compose → Kuestenlogik.Bowire.Compose, Rail.Recordings → Recordings, Rail.Flows → Flows, Rail.Workspaces (now also carrying Environments) → Workspaces, Rail.Benchmarks → Benchmarking. The descriptor-only Rail.Home + Rail.Discover packages fold into Core. Embedded hosts that pinned the old package ids need to swap the <PackageReference> lines (one-to-one mapping in the table above). Contribution Id strings, IconKey, SortIndex, Group, SidebarKind keep verbatim so saved operator settings, railMode deep links, and bowire_enabled_rails filters survive unchanged.
  • Interceptor surface consolidated into Kuestenlogik.Bowire.Interceptor (#325). The three v2.0 packages (Rail.Proxy + Rail.Intercepted + Rail.Traffic) AND the interceptor middleware that lived in Core (UseBowireInterceptor, InterceptedFlowStore, InterceptorMockStore, BowireReverseProxyHost, ReverseProxyRegistry, BowireInterceptorEndpoints, BowireToolsEndpoints) all moved into a single new package. Embedded hosts that didn’t reference any of the old packages were already losing the deprecated rails; now they also lose the middleware + reverse-proxy host + /api/intercepted/* + /api/tools/reverse-proxy/* endpoints unless they add Kuestenlogik.Bowire.Interceptor. Existing app.UseBowireInterceptor() call sites compile unchanged — the namespace stayed at Kuestenlogik.Bowire.Interceptor, only the assembly moved. The standalone Tool ships the package transitively through Bundle.Workbench so CLI users see no behavioural change.

Bug fixes

  • Sources tree right-click Remove URL was a silent no-op — handler called an undefined removeServerUrl function; inlined the same splice + persist + meta-clear + selection-reset fallback the inline trash button uses (3d9b265).
  • bowireConfirm callback vs. Promise shape mismatch — three Promise-style callers (Remove URL on workspace detail, preset delete, benchmark delete) silently TypeError’d inside .then(). The function now polymorphic: detect arg2’s type, route both shapes through a shared settle() (9ae9bf1).
  • REST probe-resolved services were tagged with the spec URL, not the operator URL — Discover sidebar grouped a single source as two panels. Fixed via RetagOriginUrl + AliasSchemaCache so the cache works under both keys (a1c5831, 2b1c045).
  • Bundle.Workbench was missing the Map extension — standalone Tool surfaced “Install Kuestenlogik.Bowire.Map” placeholder cards instead of mounting the viewer (76ed326).
  • Settings → Plugins didn’t list UI extensions — only protocols showed. Added installedExtensions array to /api/plugins + a parallel “Installed UI extensions” section; clicking an extension routes to its own detail tab (0182a20, b3b98ba).
  • Sample.TacticalApi gRPC discovery failed for plain URLs — Kestrel defaulted to HTTP/1.1, Http1AndHttp2 only upgrades via TLS+ALPN. Pinned to HTTP/2 only so the generic grpc@http://... URL works via Server Reflection; tacticalapi@... still uses bundled descriptors (61ba0ef, e83dce7).
  • Map widget disappeared on Tab ↔ Split toggle — extension bootstrap was fire-and-forget, so preferredSplitExtensionForMethod returned null at first render. Chained render() onto the load promise; stamped distinct host IDs so morphdom replaces wrapper subtrees wholesale (a9d403f, a00b534).
  • JSON viewer click toggle didn’t actually toggle — viewer mutated togglesByPath Set but waited on a caller’s onToggle to trigger render; render-main.js callers don’t pass one. Viewer now self-rebuilds in place via _rebuildViewerLines (df3344c).
  • <details>-based JSON viewer lost line numbers + gutter chevron after refactor — Hoppscotch-style renderJsonViewer re-adopted in both response paths so line numbers + gutter chevron come back. Extension hooks (gestures, decorators) preserved (0aea6f8).
  • bowire.io “Recently shipped” stuck on v1.5.0 — scripts/site/build-activity-snapshot.mjs wrote to scripts/site/site/_data/activity.json (one .. short). Documentation workflow ran daily refreshing the wrong file (2ed7ef5).

Polish

  • Action button width stabilised across method types — min-width: 132px accommodates “Disconnect” + icon so switching unary ↔ streaming doesn’t reflow the action bar.
  • JSON validation pill moved from below the editor to the pane header toolbar — a long payload no longer pushes the ✓ Valid JSON indicator off-screen.
  • Map widget pin gestures — hover highlights JSON (no scroll); click scrolls + auto-expands collapsed ancestors; double-click copies path.
  • Workbench sub-tab strip for response viewers — JSON + Map tabs share the same chrome as the rail tab strip.
  • Splitter Hover-Intent gate (250 ms dwell before highlight) so a casual mouseover near the rail edge doesn’t flash a splitter accent.
  • Toast pattern with leading icons + undo / log actions hook.
  • Tour buttons icon-only with stable footprint + tooltip on hover.
  • Welcome card icons fixed (home → house, mocks → mock, discover → discover icon).
  • Statusbar pills are now rounded (matching GET/POST chips).
  • Tactical Sample uses real Guid UUIDs + NATO-phonetic callsigns instead of slug ids + civilian transport hubs.

Acknowledgements

Operator review during this release surfaced more than fifty UX issues — from “the chevron isn’t in the gutter” to “Subscribe semantics are invisible”. Every Highlights and Polish entry above traces back to feedback received and validated. Special thanks for the patience holding rc-grade work in production until v2.1.

2.0.1 freeform request builder + ad-hoc requests + rail-modes plumbing

· Release, downloads and commits →

v2.0.1 is a patch release driven by operator feedback during real workbench use against discovery-enabled targets. The headline work is the complete rewrite of the Freeform Request Builder so it shares chrome with the discovered-method pane, the new self-contained-vs-source URL semantic for collection items, the foundation plumbing for optional rail modules (no default-flip on upgrade), and a stack of critical fixes around error reporting and pane re-rendering. No breaking changes — existing workspaces, collections, recordings, and benchmarks load identically; only the freeform builder + the console error format change visibly.

Highlights

Freeform Request Builder rebuilt on the discovered-method skeleton (#40, #245, #246, #252)

The Freeform Request Builder is now end-to-end consistent with a discovered-method pane: same .bowire-header chrome at the top, same .bowire-content[data-split] request/response panes with the draggable divider, same .bowire-action-bar at the bottom with Execute. Service + method become editable inputs inside the standard header info column; protocol is a compact icon-popover in the header’s right cluster, and the method type lives as a segmented button bar right next to it. URL was lifted out of the Payload/Metadata/Mock tab strip into an identity-level row because the URL determines which tabs make sense — it isn’t a payload part. The previous chrome layered freeform-specific overrides on top of the shared classes; those are retired so the padding, font weights, spacing all match exactly.

The Execute-button dropdown gains an As new request entry on every discovered method. Clicking it snapshots the current request shape — URL, method, body, metadata, auth, methodType — into the freeform builder pre-filled, with a cloned: <svc>/<method> lineage hint riding on the Cancel tooltip and the default save name. The discovered service tree stays untouched; the operator edits the clone in place, executes one-shot, or persists it via the header’s + Add to… collection picker. Switching to another method tab and back preserves the clone — freeformRequest is now per-tab session state rather than a singleton that died on the first tab switch.

Self-contained vs source-bound URLs in collection items (#252)

Collection items learn a urlMode discriminator: inline (URL lives verbatim on the item, no central reference) or source (URL is bound to one of the workspace’s centrally-managed Source URLs). The freeform builder URL row grows a two-state segmented toggle to pick mode at save time; switching from Inline to Source with a typed-but-unsaved URL prompts to either persist as a new Source then switch, or discard. On replay, items saved with urlMode='source' re-resolve their URL against the current Source list — a rename / retirement propagates to every bound item without manual touch-up. Items saved before this release default to urlMode='inline', which preserves their old behaviour exactly.

Two compose entry-points land alongside the toggle: Compose new request on the Home rail Quick Actions tile (self-contained URL, the most common case) and New from source… which opens a Source-URL picker before dropping into the builder. Both also surface in the command palette under the new New group, matching against compose, new, request, freeform, ad-hoc. The previous + New button retired from the Discover toolbar — Discover’s job is browsing the discovered tree, not composing, so the action moved to where ad-hoc work actually starts.

Rich error detail in the console (#243)

Failed REST / gRPC invocations used to surface as a bare Error chip with no body — the actual signal (status code, response body, exception, stack frame) lived on the BowireInvokeResult object but was dropped by problemTitle. The console-entry path now uses richErrorDetail, which assembles a multi-line block carrying the title, problem+json status code and reason, detail line, response body (truncated to 4 KB), exception type + message, and the first three non-framework stack frames. Server-side problem+json fields (type, instance, numeric status) are now picked up correctly — earlier regressions where the helper only looked for statusCode / httpStatus aliases are fixed so the Status: 502 line lands as expected. Network errors (no response received) still carry the exception text directly.

Optional rail modules — plumbing (#248 Phase 1)

Settings → Rail modes editor adds a new section under My preferences where the operator can hide non-essential rails (Recordings, Mocks, Flows, Proxy, Benchmarks, Security) without losing access — disabled rails stay reachable via the command palette and deep links, only the rail icon disappears. Always-on rails (Home, Discover, Workspaces) are hard-coded and can’t be toggled. Existing workspaces ship with every rail enabled on upgrade, so no operator sees a surprise hide; the opt-out is explicit. Per-user storage lives in bowire_enabled_rails; per-workspace overrides land later.

This is Phase 1 — pure plumbing, no default-flip. v2.1 (Test pillar) + v2.3 (Security pillar) can ship their new rails as opt-in from day one instead of forcing the always-on assumption, and v2.4’s Schema Designer (#247) lands as the first default-OFF module on top of the mechanic this release ships.

User-defined workspace templates (#242)

The Workspaces rail’s create-workspace dialog grows a Your templates section between the built-in templates and the last-picked default. The active workspace’s “Save as template…” tool button snapshots the current URLs, env vars, collection, global vars, and plugin pins into a localStorage-backed bowire_user_workspace_templates record. Templates show as click-to-pick entries with a hover-revealed delete (no accidental purges). Rename and delete affordances live next to each row; the user-template apply(wsId) adapter writes into the new workspace’s wsKey buckets exactly the way built-in templates do, so the create-workspace flow is single-pass for both kinds.

The default URLs for the built-in templates also change: REST moves from httpbin.org (no OpenAPI = 0 services) to petstore.swagger.io/v2 (discovers a full Pet/Store/User tree on first connect), gRPC moves from grpc.postman-echo.com:443 (HTTP 502) to grpcs@grpcb.in:443 (server-reflection enabled), and Mock + Multi-protocol templates pick up the same discovery-enabled endpoints.

Bug fixes

  • Rail force-home clicks: clicking a non-home rail from the no-workspace empty state used to leave the DOM showing the new rail as active while the module state silently reset to home, blocking the next click. Force-home now runs at the top of render() before the rail paints (9a91940).
  • Env load wiped on every boot: loadEnvironmentsFromDisk ran even for browser-only workspaces and clobbered the localStorage seed on first paint. Skip path added for non-disk-mode workspaces (e501ad8).
  • setUrlMeta TDZ-style boot race: urlMeta was referenced from a render handler before its assignment; the getter/setter pair now type-guards the variable so a boot-time ensureAliasForUrl doesn’t throw (30a0974).
  • REST OpenApiUploadStore test pollution: parallel test isolation fix; the discover-empty test now brackets OpenApiUploadStore.Clear() so sibling tests can’t leak fixtures (30a0974).
  • MapLibre extension assembly-load race: assembly forcibly loaded via typeof(MapLibreExtension).Assembly.FullName + per-test belt-and-suspenders helper (30a0974).
  • CoverageTo95Tests.MapBowireTestAppFactory ContentRoot race: pinned to AppContext.BaseDirectory so CI’s Environment.CurrentDirectory toggling can’t flip the test app’s content root mid-run (30a0974).
  • Discover toolbar + retire-day crash: the protocol-filter button’s insertBefore(_, newBtnWrapper) failed after #244 retired newBtnWrapper; switched to appendChild (b8e1ac3).
  • As new request no longer flips [+]-tab behaviour: selectedMethod / selectedService are preserved when entering the freeform builder so the [+]-tab keeps its “pin current method” behaviour after a clone (62485f9).

Polish

  • Console toolbar — three distinct icons replace the X-glyph duplication: selectionClear (dashed rectangle + X) for Clear selection, trash for Clear all, close for Close console (1eb4022, b656a36).
  • Freeform protocol dropdown — min-width 220px so the option labels stay readable after the button shrank to icon-only in the editable header (8b9c116).

Coverage

  • BowireMcpChatClient 81% → 93% line / 93% branch (6e8da96)
  • SidecarPluginManifest 79% → 100% line (647c8ad)

Acknowledgements

Operator feedback during real-workbench iteration drove the freeform builder layout work end-to-end. The clone-then-edit clarification (“as new request” should be an editable form, not an auto-save), the URL ownership semantics (inline vs source), and the tab-persistence requirement (“the clone has to survive a tab detour”) all came from in-session use against the petstore + grpcb.in samples.

2.0.0 re-architected workbench + git-backed workspaces

· Release, downloads and commits →

v2.0 is the cut where every optional surface gets pulled out of the core, the workbench shell gets re-architected end-to-end, and a couple of API-level decisions get unwound now that they have stable replacements. Embedded hosts that vendored Kuestenlogik.Bowire directly will need a small set of explicit changes — see Breaking changes at the bottom.

Highlights

Workbench shell re-architected (#115)

The workbench surface that ships in bowire and embedded hosts was redrawn from the structural level up — Topbar carries identity + workspace context, the left rail holds mode switching, the sidebar holds per-mode lists, the main pane holds the editor, the new statusbar holds system state. Tabs cohere visually into their panes, empty-states are uniform across every surface, and a shared renderDrawer primitive backs Assistant / Help / Tests / Activity / future Inspector — drawer chrome is no longer hand-rolled per surface. Single-Accent + Protocol-Glyph is the only colour encoding; sharper radii (2/4/6 px) come from CSS custom properties. Sidebar means navigation; URL/Schema-Files/AI-Settings became their own surfaces.

Workspace = project folder (#147–#151 + #196 Phase 2)

bowire workspace init / export / import / migrate-format makes a workspace a real directory you can commit. Per-entity files (one JSON per request / environment / collection / recording) survive merges; secrets live in a sibling secrets/ tree that’s .gitignored by default. The Kuestenlogik.Bowire.Workspace.Git runtime (shipped in this release) plumbs the per-entity layout through the workbench at read + write time, watches the directory for external edits via FileSystemWatcher + SSE so the workbench reload-toast fires when a teammate commits over your shoulder, merges <env>.json with its <env>.secrets.json sibling at resolve-time so the secret split stays a load-time concern instead of a save-time worry, and gates concurrent edits across two open Bowire instances via a stale-pid-aware .bowire.lock. For disk-mode workspaces (recordings / collections / scripts live on disk rather than in localStorage) the new workspace export <file.json> + workspace import CLI verbs capture every per-entity file in a single archive — .bww-style state download stays available for browser-mode workspaces.

Workspace integrity — plugin pins + scope-split settings (#193)

A workspace can now pin the protocol plugins it expects (workspace.pluginPins: { rest: ">=2.0", grpc: "*", mqtt: "5.x" }). When a team member opens the workspace, a banner fires if any pinned plugin isn’t loaded — with “Install all” and “Open editor” actions instead of cryptic “no such protocol” errors at first request. The pin editor lives in the workspace’s own Settings tab, so pins ride the workspace into git instead of getting buried in per-user state. The Settings dialog itself grows a scope-split tree: “My preferences” (per-user — Assistant config, theme, shortcuts, plugins) vs “This project” (per-workspace — pins, sources, env-inclusion, AI override). The Assistant tab supports per-workspace overrides directly: a scope chip in Settings, a matching reminder chip on the workspace surface, and one-click “Use global instead” to drop the override.

Optional packages live outside core

Core Kuestenlogik.Bowire ships with zero PackageReference entries — only the Microsoft.AspNetCore.App framework reference. Embedded hosts add only what they actually use:

  • Kuestenlogik.Bowire.Extension.MapLibre → Kuestenlogik.Bowire.Map — the v1.3.0-rc.1 of the MapLibre extension will be deprecated + unlisted on nuget.org after 2.0 ships (#197).
  • OpenTelemetry extracted into Kuestenlogik.Bowire.Telemetry — embedded hosts that don’t want self-observability no longer pull the OTel transitive weight. The standalone CLI keeps --telemetry working because the tool transitively references it.
  • .Ai, .Help stay opt-in as before. Standalone bowire bundles them.

Naming convention going forward — optional first-party packages are Kuestenlogik.Bowire.<feature> (.Ai, .Help, .Telemetry, .Map) or Kuestenlogik.Bowire.<area>.<backend> (e.g. Kuestenlogik.Bowire.Workspace.Git, shipped in this release). The legacy .Extension.* prefix isn’t used for new packages.

OpenAPI library decoupled via adapter packages

REST plugin no longer takes a hard dependency on Microsoft.OpenApi. A new IBowireOpenApiAdapter seam in Kuestenlogik.Bowire.Protocol.Rest lets the consumer pick the library version:

  • Kuestenlogik.Bowire.Protocol.Rest.OpenApi2 — pins Microsoft.OpenApi 2.x, matches what ASP.NET Core 10’s AddOpenApi() transitively pulls. Standalone bowire bundles this one.
  • Kuestenlogik.Bowire.Protocol.Rest.OpenApi3 — pins Microsoft.OpenApi 3.x for hosts that want the modern grammar.

Both can be loaded side-by-side; the adapter registry auto-picks the one matching the runtime’s Microsoft.OpenApi major. Resolves the OpenAPI-DLL-version conflict reported on .NET 10 hosts running side-by-side with ASP.NET’s bundled discovery.

Across-the-pane omnibox (#124 / #162)

Cmd/Ctrl+K opens a single search line that ranks methods, recordings, collections, settings, and ?-prefixed AI prompts uniformly. Replaces the per-surface searches that used to live in the sidebar, drawer, and method picker.

Action log + Ctrl+Z (#168) and hint dismiss (#169)

Every destructive action (delete recording, delete collection, delete workspace, swap environment scope) is reversible from the Activity drawer or via Ctrl+Z. Hints carry a permanent dismiss key that lands them in Settings → Hints and warnings so the user can restore one without restarting.

Workspaces tree + per-plugin DisplayName (#192 / #167)

Sources, collections, recordings, and environments all live as nodes under the Workspaces tree on the left rail. Plugin Settings reads each plugin’s DisplayName from its registration so the row labels match what the plugin author calls itself (no more raw assembly-name fallback).

Recordings as portable artefacts — .bwr format + bowire mock <recording.bwr> (#210 / #211)

Recordings get a standalone, self-contained file format. A .bwr is a single JSON archive that carries every step of a captured session — protocol metadata, request/response pairs, content-addressed bodies, timing — without depending on a workspace it was authored in. Drop one on a colleague’s machine and it loads cleanly even when their workspace pins a different plugin set.

bowire recording validate <file.bwr> lints a recording before you share it. bowire mock <recording.bwr> is the positional one-shot replay verb: point it at a .bwr, the mock server spins up and matches incoming requests against the recorded steps via MockHandler.TryMatch, dispatching the recorded response body verbatim. Unmatched requests return a structured 404. All seven protocols (REST / gRPC / GraphQL / WebSocket / SSE / MQTT / SignalR) round-trip through replay; the integration suite pins each one.

AI side-panel — BYOK cloud + MCP-client reversal (#25 Phase 3 + 4)

AI providers ship as opt-in NuGet plugins through a new IBowireAiProviderFactory seam. The standalone CLI bundles every option out of the box; embedded hosts pay only for the providers they install — Kuestenlogik.Bowire.Ai core stays free of cloud-provider SDK weight.

  • Kuestenlogik.Bowire.Ai.OpenAi — BYOK OpenAI + OpenRouter via Microsoft.Extensions.AI.OpenAI. OpenRouter rides the same SDK with its own base URL, so one package covers both.
  • Kuestenlogik.Bowire.Ai.Anthropic — BYOK Claude via the community-maintained Anthropic.SDK whose Messages property is already an IChatClient.
  • Kuestenlogik.Bowire.Ai.Mcp — MCP-client reversal: Bowire connects as an MCP client to a user-configured host (stdio command or http URL) and routes chat through the first tool whose name reads as a chat / completion / sampling gateway. Lazy-connect on first call, so Settings-UI hot-swap stays cheap.

The Settings → Assistant tab grows a six-option provider dropdown (Ollama / LM Studio / OpenAI / Anthropic / OpenRouter / MCP), a password-input API-key row with a leave-blank-keep-existing convention + an explicit __bowire_clear__ sentinel, and a per-provider privacy banner that names exactly where prompts go (“Prompts go to api.openai.com — Küstenlogik never sees the key, prompts, or responses”). API keys are never echoed back over the wire; the status endpoint surfaces a hasApiKey boolean instead.

Benchmarks as a first-class rail — envelope architecture + 3 shapes (#131)

Benchmarks are their own rail-mode peer with Discover / Mocks / Flows / Recordings / Collections, no longer a buried inline expansion on a single request pane. The shipped surface is the envelope architecture — a saved Benchmark is a {targets[], phases[], mode} bundle rather than a one-method-one-config row — with three production-relevant target shapes wired:

  • single — one unary call against a service + method
  • collection — replay every item of a saved collection
  • recording — replay every step of a saved recording
Phases follow the Artillery / k6 stages model (duration + arrivalRate vus) so import / export round-trips cleanly to those formats:
  • Round-trip exports: native Bowire envelope JSON · Artillery JSON · k6 script (a complete .js with stages + per-target HTTP calls)
  • Imports: Artillery JSON + Postman Collection
  • {{var}} ↔ ${var} auto-rewrite at the import / export boundary so variables stay legal in both directions

Run output covers latency percentiles (p50 / p95 / p99), throughput (rps), and a status histogram (success / 4xx / 5xx / timeout / network). Saved runs persist in the active workspace + carry their spec so you can re-run them after switching workspaces — the sidebar shows a p95 meta on each saved row + live N/total while a run is in flight. Per-request “Benchmark this method” affordance on method-header + tree-row drops the current method into an existing envelope or spawns a new one seeded with it.

The remaining random + scheduled shapes, previous-run diff banner, and CSV / k6-summary / OTLP result exports ship in v2.1+ under their own tickets — #231 (random), #232 (scheduled + cron), #233 (diff banner), #234 (CSV / k6-summary / OTLP). #131 closes with the envelope architecture this release.

Recordings as GB-scale artefacts — chunked storage Phase 1 (#144 → closeout #220)

Recordings move off the legacy single-file shape onto a chunked disk layout: one JSON file per step under ~/.bowire/recordings/<id>/, with a manifest at <id>.json carrying the step ids + ordering. Request and response bodies extract into bodies/<sha256>.bin so duplicate payloads dedupe across steps + recordings — typical recordings of long-poll / SSE / WebSocket traffic shrink dramatically. The runtime reads the chunked layout transparently; old recordings load via the legacy reader and get re-saved into the chunked shape on the next mutation. Every entityKind / id / responseRef / step-file path passes through SanitiseId / SanitiseHash / SanitiseStepFile barriers that reject traversal / non-hex / out-of-bucket inputs — CodeQL cs/path-injection is pinned to zero alerts at the v2.0 cut. Filesystem watch + reconcile UI + lazy step-load on UI scroll continue under #144 in v2.1.

Per-mode saved configurations — Presets framework (#140 Phase 1 → closeout #221)

A generic presets API replaces the per-mode hand-rolled “save current config” patches: loadPresetsForMode / savePresetForMode / setDefaultPreset / deletePreset, per-workspace storage keyed under bowire_presets_<mode> so presets ride the .bww export, a uniform Manage-Presets modal across modes, and a reusable renderPresetsBar(mode, …) top-of-pane bar with picker + “Save current as preset…” + “Set default” + “Manage…”. Benchmarks and the Discover request-pane integrate first — Discover’s method header carries a presets dropdown with per-method save / apply / set-default / add-to-collection and a single Save-as-preset action that mirrors the workspace dropdown pattern. Mocks / Parallel / Security / Proxy / Catalogue integrations follow in v2.1 under #140.

Parallel sessions from recordings + collections — local fan-out (#132 minimal → closeout #222)

Recordings and collections get a “Run in parallel sessions” toolbar action that fans N copies of the run locally. Live state tracks per-session progress (started / active / completed counts, errors) and result aggregation reports per-session pass/fail + the overall outcome (“12 / 15 sessions completed, 3 errors at step 4”). No standalone rail mode — results land inline under the source that started the run so an operator chasing parallelism doesn’t need to learn a new navigation step. Distributed fan-out across workers stays under #132 in v2.1.

UI polish across the rc series

Through the v2.0 rc cycle every rail in the new shell got a uniform polish pass so they read as part of the same family — not nine slightly-different layouts. The biggest wins:

  • Recording detail pane — shared .bowire-pane-header chrome (heading + rename pencil + danger trash via bowirePrompt); the flat 10-button toolbar regroups into Run / Build / Export gangs, Export becomes a split-button ▾ menu (HAR / HTML report / JSON with one-line hints).
  • Discover sidebar consolidation — favorites toggle moves into the filter popup as its first option; the filter button moves up into the unified toolbar row next to + New. One filter control instead of two related-but-separate ones.
  • Flows + Proxy sidebars stop falling through to the legacy Discover services-tree path — each gets its own minimal sidebar renderer.
  • Home rhythm + drawer — Continue / Start / Sections / Footer fit a 1080 px viewport without scrolling; Favorites / Recent titles open a right-side renderDrawer overlay with the full list (recent activity tiles carry relative timestamps).
  • Preset picker in the Discover request-pane header — per-method save / apply / set-default / add-to-collection; default-star sits right in the tools cluster (analog of the workspace ✓ marker); + Save as preset… action row at the bottom of the dropdown.
  • Security rail drops its wrapper “Security” heading (no other rail puts a rail-name heading above its own content); Threat Model empty state swaps the bare ⚠ no-endpoints line for the shared empty-card chrome with “Open Discover” + “Add a source” CTAs.
  • Workspace settings tabs reorder Variables → Secrets → Auth (Variables + Secrets are KV-shape, Auth is a different mental model).
  • Root-cause fix — code-export.js was calling syncFormToJson from inside the render path, clobbering formValues + requestMessages with stale pre-merge DOM values every frame and breaking preset apply / history replay / Repeat-last-call. The fix swaps the mutation for a read-only collectFormValuesFromState snapshot.

Breaking changes

Each change has been on a back-compat ramp through v1.9.x and is removed in 2.0.

Wire format: application/problem+json drops the { error } shim (#88 follow-up)

Every Bowire endpoint that returns a problem+json body has emitted both the RFC 7807 title + a legacy error field set to the same string. v2.0 drops the error field on the wire. Clients reading the response body should switch to body.title (RFC 7807) / body.detail. The shim is gone server-side; the JS workbench bundled with the CLI was updated in lock-step. OAuth callbacks keep their error field — that’s RFC 6749 from the IdP, unrelated.

Microsoft.OpenApi is no longer a transitive of Kuestenlogik.Bowire.Protocol.Rest

Hosts that referenced REST + called the discovery helpers directly must now also reference one of Kuestenlogik.Bowire.Protocol.Rest.OpenApi2 or .OpenApi3. The standalone CLI bundles OpenApi2 (matches the .NET 10 ASP.NET ecosystem); embedded hosts pick whichever matches the rest of their stack.

Kuestenlogik.Bowire.Extension.MapLibre → Kuestenlogik.Bowire.Map

Old package will be deprecated + unlisted on nuget.org after this release (#197). Swap the <PackageReference> to the new name; no API surface change.

OpenTelemetry moved into Kuestenlogik.Bowire.Telemetry

Embedded hosts that want self-observability add a <PackageReference Include="Kuestenlogik.Bowire.Telemetry" />. Standalone CLI users see no behaviour change.

localStorage cosmetic-state reset on major bump

Persistent data (workspaces, environments, recordings, collections, history, favorites) and persistent user choices (theme, watch interval) are preserved across the v1.x → v2.0 upgrade. Cosmetic UI state (active rail mode, open drawer, expanded services, filter chips, split mode) is reset to v2.0 defaults on first boot so the new shell isn’t fighting a stale layout. A toast announces the reset; data is untouched.

Workbench CSS surface

Several bowire-ai-* and helper classes were renamed or removed as part of the shell refactor (.bowire-ai-empty → .bowire-pane-empty, legacy .bowire-ai-drawer* chrome classes dropped, dead-class audit removed 20 unused rules). External CSS that hard-targeted these internal classes will need to update; no JS / HTML API surface is affected.

Migration guides

${name} → {{name}} variable syntax (#145 Phase 1 — soft deprecation)

Bowire has two interpolation syntaxes that resolve identically: the original Bash-style ${name} (escape: $${name}) and the Postman / Mustache {{name}} (escape: {{{{name}}}}). v2.0 starts the planned migration window — both syntaxes still work, but ${name} is now flagged as legacy and the canonical form going forward is {{name}}.

What changes in v2.0:

  • A one-time per-workspace toast fires on workbench load when the active workspace’s stored data contains ${...} placeholders. The toast is snoozed via localStorage so the operator isn’t nagged on every reload.
  • The workspace-scope scanner walks recordings, collections, freeform requests, flows, and environments using the regex /(?<!\$)\${[^}]+}/ — correctly skips escaped $${...}.
  • New surfaces (Cmd+K palette, AI prompts, empty-state copy) only emit {{...}}.
  • Documentation explicitly marks ${...} as legacy.
  • substituteVars() continues to handle both syntaxes in parallel — no existing recording / collection / saved request breaks.

What you need to do for v2.0:

  • Nothing forced. Existing workspaces keep working with both syntaxes.
  • When the toast fires, you can either dismiss it (the legacy syntax will keep resolving forever in v2.0) or open Settings → Hints and warnings to permanently dismiss it.
  • For new content authored in v2.0, prefer {{name}} — and {{env.NAME}} / {{prev.field}} / {{step1.field}} / {{runtime.now}} / {{secret.NAME}} / {{ai.NAME}} for the source-prefixed forms that landed under #125.

What’s planned for v2.1 (#145 Phase 2):

  • A dedicated migration tool — Settings → Migration page or a one-shot CLI command — that walks every recording, collection, environment, flow, and freeform draft and rewrites ${name} → {{name}} (and ${response.X} → {{prev.X}}, ${now} → {{runtime.now}}, &c.).
  • Dry-run + diff view before commit. Per-workspace scope. Disk-stored recordings get touched too. Migration emits a single transition record so the workspace metadata says “migrated to {{}}-syntax at <ts>”.
  • Hard removal of the ${name} parser is NOT planned for v2.1. The earliest deprecation cut would be v3.0, and only after the migration tool has been in place for a full minor.

Acknowledgements

Closes 72 issues across the milestone — the full list is on the v2.0 milestone page. Special thanks to everyone who exercised the rc series and reported off-by-one drawer behaviour, single-tab vs multi-tab edge cases, and .bowire-ai-* class targeting in downstream CSS — every one of those reports landed as a concrete fix above.