twinny 4.2.8 to 4.2.10: a key nobody sees, a version that lied, and WSL
Three releases in three days. Developers get the gateway's pull-request plugins, opened from VS Code with a one-time code instead of a pasted key; twinny-server learns its own version number; and the extension starts under WSL again.
Three releases in three days, and only the first was planned. 4.2.8 lets an admin share the gateway’s plugins with developers and solves the awkward question of how those developers sign in. 4.2.9 exists because 4.2.8’s server package introduced itself as 4.2.7. 4.2.10 exists because the extension stopped starting under WSL. This post covers all three, including the two that are apologies.
Plugins stop being admin-only
Since 4.2 the gateway has reviewed and triaged pull requests from GitHub, GitLab, Gitea and Bitbucket with the models you already serve. The reviews lived on the gateway’s admin page, which meant the people who wrote the pull requests could not read them unless they were admins.
4.2.8 fixes that. Each shareable plugin’s card under Plugins → Store now has a share control with three positions: admins only (the default), every developer, or the developers you tick, by key name. The same thing is available as PUT /twinny/v1/admin/plugins/<id>/access. Plugins remain a licence feature; sharing does not change that.
A grant opens the plugin, not the admin page. A developer signed in with their own key sees the plugins shared with them, without the settings, and nothing else. On the four forge plugins they can read pulls and issues, run a review, ask about it, post it as a comment, triage, apply the suggested labels and set their own username on the host. Approving or requesting changes through the repository’s token stays with admins, as do repositories, tokens, the GitHub App and the choice of review model. What a developer may do is decided by the plugin, not by the grant: each plugin names the routes that are theirs, and the gateway refuses the rest.
Grants are stored by key name in plugins.json. Every change is audited as plugin.access-changed, and a developer’s writes are audited under their own name, marked member. The shared token and a demo’s guests never open a plugin.
Admins get one addition of their own: an approve button on the pull page, next to the checks. It approves as the repository’s token and posts no review text. On GitHub, Gitea and GitLab the approval is pinned to the commit the page showed, so a push that lands between reading and clicking is not approved by accident.
Signing in without a key
Sharing a page with developers raises a problem. The page signs in with a gateway key. A developer who joined by invite link has never seen theirs: it went straight into VS Code’s secret storage. Telling them to ask the admin for it, then paste it into a browser, would undo the point of the invite.
So the extension asks the gateway for something smaller. The whole mechanism is src/gateway/page-links.ts, about eighty lines:
- VS Code sends
POST /twinny/v1/page-linkwith the developer’s key. The gateway answers with a code, 32 random bytes in hex, and an expiry one minute away. - VS Code opens
<gateway>/admin#link=<code>in the browser. - The page sends
POST /twinny/v1/page-link/openwith the code and gets back the key that asked for it. Then it wipes the fragment from the address bar.
The details are where it earns its keep. The code travels in the URL fragment, which a browser never sends to the server, so no proxy or access log records it. It opens once: open deletes the record before returning it, and a second attempt gets 410, the same answer as for a code that expired or never existed. It only ever returns the key that minted it, so holding a code proves nothing that holding the key does not. Codes live in memory, at most five outstanding per key name and two hundred in total, with the oldest giving way; a restart forgets them all, which costs one more click.
Before handing the key back, the gateway authenticates it again. A key revoked in the minute since the code was made gets the refusal the key itself would get. The sign-in is audited as page.signed-in. The shared token is refused a code, and so are a demo’s guests.
In VS Code, a newly shared plugin is announced once with an Open button, and the Providers tab lists it under Your team’s plugins. The extension looks at start, when the window regains focus, and every half hour, so there is nothing for the admin to send anyone. Twinny - Open your team’s plugins in the command palette does the same. Against a gateway older than 4.2.8 the extension falls back to putting the key on the clipboard and says so.
One convenience rides along. Opened from VS Code, the page fills in your GitHub username from the account VS Code is signed in with, once, so pulls waiting on your approval show up under waiting for me.
4.2.8 also carries a completion fix: a chat-only FIM model such as Qwen3-Coder now reaches twinny-server as the chat its prompt was rendered from, rather than being refused, so the backend does not apply the template twice.
The version that lied
The 4.2.8 twinny-server package was published with a bundle built before the version bump. So twinny-server --version and the gateway’s /twinny/v1 responses both said 4.2.7.
That is harmless until someone files a bug and quotes the version. 4.2.9 is the rebuilt package plus a guard: the package’s prepublishOnly script reads cli.js and refuses to publish unless the version baked into the bundle matches package.json. The fix is a check on the artefact, not a line added to a release checklist.
Starting under WSL
In a folder opened through WSL, the extension failed activation with Cannot read properties of undefined (reading 'header'). Chat never loaded and completions did nothing.
The cause was one line in a dependency. LanceDB, which stores the workspace index, ships a native module per platform, and its loader reads Node’s process report to tell glibc from musl. Under WSL the VS Code server returns no report, so the read threw at import time, and because the import was at the top of the embeddings module, it took the whole extension with it.
4.2.10 fixes it twice. A small esbuild plugin in scripts/build.mjs rewrites that read so that a missing report falls back to glibc, and fails the build if LanceDB ever changes the line, so the patch cannot silently stop applying. And src/extension/embeddings/database.ts now imports LanceDB on first connect rather than at startup. If a native module fails to load for some other reason on some other platform, the workspace index is unavailable and everything else still works.
The same release stops the Embeddings tab jumping while it indexes: the line naming the files in flight used to disappear between files and now keeps its place for the whole run.
Where to read more
The gateway’s plugin sharing, page links and audit events are documented with the rest of twinny-server at docs.twinny.dev, and the full list of changes per release is in the changelog in the repository.