Appearance
@ahpd/tunnel-devtunnel
A Dev Tunnel plugin for the @ahpd/server daemon. It forwards the daemon's port through a Microsoft Dev Tunnel, labelled the way VS Code expects, so VS Code can find and reach the daemon from a machine on another network.
Part of ahpd. The source is in packages/tunnel-devtunnel.
Install
bash
ahpd plugin install @ahpd/tunnel-devtunnel
ahpd restartUpdate, remove and one-run flags such as --plugin are in Plugins.
What it needs
The
devtunnelCLI, installed and logged in:bashdevtunnel user login # a Microsoft account devtunnel user login -g # or a GitHub one
Options
In the configuration file, with every option set:
json
{ "plugins": [{ "name": "@ahpd/tunnel-devtunnel", "options": { "name": "dev03", "anonymous": true } }] }| Option | Default | What it does |
|---|---|---|
name | this machine's hostname | What the tunnel is called in a client's list |
keep | true | Whether the tunnel outlives the daemon |
anonymous | false | Whether somebody not signed in to this account may reach it |
Keep keep on: a client derives its connection token from the tunnel's id, so a new tunnel on every start is a new token on every start.
No option holds a credential, so the plugin takes nothing from the vault. The tunnel signs in through the devtunnel CLI's own login.
In your own host
The package's apply is the plugin entry. Hand it a plugin host from @ahpd/sdk and fold what it registered into the host's options. The plugin does its work on the listening and stopping events, which the daemon raises and an embedding host raises itself:
ts
import { createHost, foldHostOptions, listen, pluginHost, raise } from '@ahpd/sdk';
import { claude } from '@ahpd/agent-claude';
import { apply, name } from '@ahpd/tunnel-devtunnel';
const path = process.cwd();
const context = { path, paths: [path], version: '0.10.0', hostName: 'my-host', configDir: '/var/lib/my-host', log: console.log, say: console.log };
const { host: plugin, contribution, seal } = pluginHost(name, context);
await apply(plugin, { name: 'dev03', keep: true });
seal();
const { options } = foldHostOptions({ path, agents: [claude({ paths: [path] })] }, [contribution]);
const host = createHost(options);
const listener = await listen({ port: 9187 }, (peer) => host.accept(peer));
const { runtime, host: address, port, guarded } = listener;
await raise(options.events, { type: 'listening', runtime, host: address, port, guarded });
// On the way down, before the listener closes:
await raise(options.events, { type: 'stopping' });
await listener.close();In VS Code
The daemon prints the tunnel beside its own address, and ahpd status shows it:
ahpd on ws://127.0.0.1:9187 (node), sessions in /work
tunnel sunny-otter-9k3 (dev03), port 31546In VS Code, sign in to the same Dev Tunnels account and run Agents: Connect to Remote Agent Host via Dev Tunnel. Finding a tunnel needs that sign-in. A direct WebSocket address does not, and is what Agents: Add Remote Agent Host... takes.
The connection token
A client connecting over a tunnel computes its token from the tunnel's id, as the unpadded base64url of its SHA-256, and presents it as ?tkn=. Only people who pass the Dev Tunnels access check can reach the tunnel, so the token does not need to be secret.
A daemon on loopback with no connection token of its own accepts it, and that needs no configuration. A daemon started with --connection-token refuses it, because the client presents the derived token and not the daemon's. The plugin prints the derived token on its own line so you can make the two agree:
tunnel token 4d8f... - what a client presents over the tunnel; this daemon requires its ownWhat it does at startup
- Finds the tunnel it made before, by its own label, or makes one.
- Labels it
vscode-server-launcherandprotocolv5, which is what makes a client list it. - Puts port 31546 on it, the fixed port VS Code looks for.
- Forwards 31546 on loopback to the port the daemon bound, unless the daemon is already on 31546.
- Hosts the tunnel and prints its address.
If any step fails, the daemon keeps running without the tunnel.
It claims protocolv5 and not 6, because protocol 6 clients also expect a selection gateway at /agent-host/select, which a plain port forward does not serve.
Documentation
| REMOTE.md | Connection tokens and remote access |
| PLUGINS.md | Loading a plugin into the daemon |
License
MIT © Softov