Skip to content

Let an agent share a running web app through a private dev tunnel - #82

Merged
splitbrain merged 1 commit into
splitbrain:mainfrom
splitbrain-bot:claude/share-app-devtunnels
Oct 3, 2026
Merged

splitbrain merged 1 commit into
splitbrain:mainfrom
splitbrain-bot:claude/share-app-devtunnels

Conversation

@splitbrain-bot

Copy link
Copy Markdown

A box had no way to show you a web app that runs in it. The new share-app skill gives a port in the box a public HTTPS link through Microsoft Dev Tunnels, which is free and hosted. The tunnel is private: only the GitHub account of the deployment's login can open it. You click the link, sign in with GitHub once, and see the app.

The credential

The settings page has a new Dev Tunnels card with a Log in button and no paste form. The service accepts only tokens that GitHub issued to its own GitHub App, so a personal access token does not work.

  • The orchestrator runs GitHub's device flow itself, without a container. The card shows GitHub's URL and code, like the Codex login.
  • The orchestrator stores the access token and the refresh token. The access token lasts 8 hours and the refresh token 6 months.
  • The credential refresh renews the access token in its last hour, and also after it has expired, for example after downtime. Every renewal rotates both tokens, so the login stays alive as long as the orchestrator renews it at least once every 6 months.
  • Every box gets a placeholder in DEVTUNNELS_TOKEN. The proxy swaps in the real token on *.rel.tunnels.api.visualstudio.com.

Two proxy changes

Pass-through schemes. While devtunnel host runs, the CLI sends a token that the service issued for that one tunnel, as Authorization: tunnel <token>, to the same hosts. The proxy refused that as a foreign credential. A credential can now name pass-through schemes, and values under them pass unchanged at that credential's hosts only. Any other unknown value is still refused.

WebSockets on intercepted hosts. The interception engine refused every WebSocket upgrade, which blocked the tunnel's relay connection. An upgrade now gets the same checks as a request:

The upgrade carries Before Now
the placeholder 501 403, because the engine cannot swap a header in an upgrade
a foreign credential 501 403
nothing 501 forwarded
a value under a pass-through scheme 501 forwarded

The engine forwards upgrades unchanged, so a forwarded upgrade cannot carry the real credential. Only a swap puts the real credential on the wire, and an upgrade that needs a swap is refused.

Box image

  • The devtunnel CLI, pinned to release 1.0.2094 and to the checksum of each build, like glab.
  • The share-app skill, which the entrypoint installs at every start. It creates a private tunnel through the API, hosts it with the CLI, gives you the link, and deletes the tunnel at the end.

Testing

  • Unit tests for the credential, the device login, the refresh, the policy, the pass-through schemes and the WebSocket rule. The proxy tests run against the real interception engine.
  • Settings page test: the card has a login and no paste form.
  • I ran the real interception engine in a box with the new rules and hosted a tunnel through it. The CLI trusted the proxy CA through SSL_CERT_FILE, the API calls to the global and the regional host passed, the relay WebSocket connected, and the public URL served the page.

Not tested yet, because it needs a deployment built from this branch:

  • the login through the settings card
  • renewal without a client secret, which worked once by hand
  • the skill's create and delete calls with the token swapped in by the proxy

After the merge: log in on the card, then ask an agent in a box to share an app with the share-app skill.

A box had no way to show the person a web app that runs in it. The new
share-app skill gives a port in the box a public HTTPS link through
Microsoft Dev Tunnels. The tunnel is private, so only the GitHub account
of the deployment's login can open it.

The settings page has a Dev Tunnels card. The service takes only a
token that GitHub issued to its own app, so the card has a login and no
paste form. The orchestrator runs GitHub's device flow itself and stores
the access token and the refresh token. The access token lasts eight
hours, so the credential refresh renews it, also after it has expired.

Every box gets a placeholder in DEVTUNNELS_TOKEN, and the proxy swaps in
the real token on the Dev Tunnels hosts. Two proxy rules had to change
for hosting to work:

- While the CLI hosts a tunnel, it sends a token that the service issued
  for that tunnel, under the "tunnel" scheme. The proxy refused that as
  a foreign credential. A credential can now name such pass-through
  schemes, and values under them pass at that credential's hosts.
- The interception engine refused every WebSocket upgrade, which
  blocked the relay connection. An upgrade now gets the same checks as a
  request. It is forwarded when it needs no swap, and refused when it
  would carry the placeholder or a foreign credential.

The box image carries the devtunnel CLI, pinned to a release and to the
checksum of each build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants