Skip to content

feat(preview): reach the preview gateway over BiBCode Connect - #91

Merged
mubeda merged 5 commits into
mainfrom
main-3
Oct 14, 2026
Merged

mubeda merged 5 commits into
mainfrom
main-3

Conversation

@mubeda

@mubeda mubeda commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Summary

Clients connected through BiBCode Connect (the relay) can now open dev servers that listen on the server's localhost, through the preview gateway, at their own https:// preview hostname. The Connect tunnel also comes back by itself after a server restart. Closes #68.

#69 (SOCKS-routed preview profiles) is a design proposal only, plus a Linux WebKitGTK spike: docs/superpowers/specs/2026-10-11-preview-socks-profiles-design.md. It ends with three open questions, so #69 stays open.

This PR also fixes an older bug that blocked every Connect link from a real client (see Fix below).

Changes

  • Relay: provisions 4 preview hostnames per managed endpoint (<env>-p1 … <env>-p4): proxied CNAMEs, plus ingress rules ahead of the catch-all. Records are found by name, so re-linking adds missing ones and deprovisioning removes them before the tunnel. The runtime config now carries hostname and previewHostnames.
  • Server, gateway:
    • A gatewayOpen caller that arrives on loopback under the endpoint hostname takes a free preview slot and gets origin: https://<preview hostname>. A target with live connections never loses its slot. New error reasons: relay-not-provisioned and relay-busy.
    • A new outermost layer on the main port serves preview hostnames through the slot's target. It never reaches BiBCode's own routes, CORS, or admission gate.
    • Preview responses are HTTPS-facing (Secure cookie, https Origin rule and Location) and marked no-store for both Cloudflare and browsers.
    • Service-worker scripts are refused, and plain HTTP on a relay hostname redirects to HTTPS.
    • The gateway starts draining as soon as shutdown is signaled, and every shutdown caller waits on that same drain.
  • Server, link state:
    • The Connect tunnel is restored from the stored link at startup, unless the server now listens on a different port than the tunnel was provisioned for. Link, unlink, and restore share one lock.
    • connect.linkState reports managedEndpoint, previewHostnames, managedEndpointStale, and linkGeneration.
    • relayConfig refuses a background re-link whose requireLinkGeneration no longer matches the stored link.
  • Web:
    • Relay reach (and any origin the server returns) bootstraps on that origin and maps it back to localhost per tab. Browser mode never frames these previews.
    • Once authenticated, the primary client re-links once per session, silently and fenced, when its tunnel lacks preview hostnames or points at an old port.
  • Profiler: the tunnel connector is now watched. An unrequested exit becomes a subprocess.crash; it is checked without reaping and terminated by process group.
  • Fix: link proofs over HTTP/1.1. connect.linkProof compared the loopback origin with the request URI's authority. HTTP/1.1 requests carry only the path, so every real link and re-link failed with "Invalid managed endpoint origin." The check now falls back to the Host header, as the auth layer already does. Tests had passed full URLs, which is why none caught it. Requests through the tunnel keep their public hostname in Host and are still refused.
  • Docs: remote architecture, user guide, relay README, Profiler events and findings, a relay step in docs/testing/cross-platform-validation.md, the Internal browser: preview gateway reach for BiBCode Connect (relay) environments #68 spec (with review addenda) and plan, and the Internal browser: SOCKS-routed preview profiles per environment (true-origin previews, Phase 2) #69 proposal.

Validation

On 4f60464f, after merging main (v0.9.1):

  • cargo fmt --all --check
  • cargo clippy with -D warnings
  • The full workspace test graph: desktop, server, web, relay, contracts, shared, client-runtime.
  • vp check (0 errors), vp run typecheck, vp run check:contracts

On b0e1ae64 (the link-proof fix and test cleanups):

  • cargo fmt --all --check
  • cargo clippy -p bibcode-server --all-targets -- -D warnings
  • cargo test -p bibcode-server --lib connect_mcp (18 passed; the new origin-form case failed before the fix)
  • cargo test -p bibcode-server --test production_jwt --test production_http_routes
  • Contracts tests for preview, relay, and environmentHttp (38 passed) and relay deploymentConfig (13 passed)
  • vp check, vp run typecheck

Reviews:

  • Codex review rounds on every commit; the last one found nothing to fix.
  • React changes checked with vercel-react-best-practices. The re-link component's effect depends on a derived boolean, and a module-level set limits it to one attempt per session.
  • User-visible notices checked against UI.md.

Not run:

  • Native relay validation: desktop and a browser-mode client over a deployed relay stage. It is described in the runbook step above.
  • The Windows-only line (connector_is_live via exited_status) was not compiled locally; this PR's CI compiles it.

Profiler coverage

  • Child processes: the Connect tunnel connector reports unrequested exits through exits::report_exit(Watch::Failures, …), in both the watcher and the apply and shutdown paths.
  • RPC methods: no new methods.
  • Shutdown: the gateway drain starts on the shutdown signal, before the Profiler's teardown.
  • Docs: the profiler skill's events.md and findings.md and the Profiler section are updated.
  • Worktree removal and binaries/re-exec modes are not touched.

Follow-ups (documented, not built)

  • Add the tunnel zone to the Public Suffix List, so a previewed page can't set cookies across every preview hostname in that zone.
  • A background re-link that the server refuses leaves a stale link on the relay side, because the relay has no conditional unlink.
  • Relay previews skip the browser cache. Per-target cache validators would bring caching back if load times hurt.
  • The "too many previews" notice doesn't say which tabs hold the 4 slots.

🤖 Generated with Claude Code

mubeda and others added 4 commits October 12, 2026 15:50
Relay-connected clients now open server-loopback dev servers through the
preview gateway at their own https origin, and the Connect tunnel comes
back after a server restart. Refs #68. Also proposes the SOCKS-routed
preview profiles of #69 (design only, with the Linux WebKitGTK spike).

- relay: provision 4 preview hostnames per managed endpoint
  (<env>-p1..p4: proxied CNAMEs plus ingress rules before the catch-all),
  found by name so re-linking adds missing ones and deprovisioning removes
  them before the tunnel; the runtime config carries hostname and
  previewHostnames.
- server: the tunnel runtime publishes its hostnames; a gatewayOpen caller
  on loopback under the endpoint hostname takes a free preview slot (a live
  target never loses its slot) and gets origin https://<preview hostname>;
  new reasons relay-not-provisioned and relay-busy. An outermost main-port
  layer serves preview hostnames through the slot's target handler,
  HTTPS-facing (Secure cookie, https Origin rule and Location), never
  BiBCode's routes, CORS, or admission gate. Preview responses are
  Cloudflare-CDN-Cache-Control and Cache-Control no-store, service worker
  scripts are refused, and relay hostnames over plain HTTP redirect to
  HTTPS. The gateway drain starts when shutdown is signaled and is shared
  by every caller.
- server: restore the Connect tunnel from the stored link at startup,
  unless the server now listens on another port than the tunnel was
  provisioned for; link, unlink, and restore share one lock;
  connect.linkState reports managedEndpoint, previewHostnames,
  managedEndpointStale, and linkGeneration, and relayConfig refuses a
  background re-link whose requireLinkGeneration no longer matches.
- web: relay resolution (and any server-returned origin) bootstraps on the
  returned origin and maps it back to localhost per tab; browser mode never
  frames these previews; the primary client, once authenticated, re-links
  once per session, silently and fenced, when its tunnel lacks preview
  hostnames or points at an old port.
- docs: remote architecture, user guide, relay README, a relay step in the
  cross-platform runbook, the #68 spec (with review addenda) and plan, and
  the #69 proposal.

Native relay validation (desktop and browser-mode client over a deployed
relay stage) is recorded in the runbook and was not run here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connector now also starts at boot (tunnel restore), so its own death
must reach the Profiler like any other child BiBCode runs (AGENTS.md:
Profiler, Crash, and Trace Coverage).

- A watcher checks the active connector every 5 s without reaping it. An
  exit nobody asked for withdraws the relay preview hostnames, logs a
  warning, reports subprocess.exit and subprocess.crash (Watch::Failures,
  labelled by the connector's executable), and ends the rest of its
  process group. It holds the runtime weakly, so a runtime dropped without
  shutdown still drops its connector.
- apply and shutdown report a connector that died on its own since the
  last check before replacing or stopping it; the Windows liveness probe
  no longer consumes the process handle.
- Relay hostname matching on every main-port request no longer clones or
  allocates.
- Docs: Profiler section, profiler skill events and findings, remote
  architecture, and a connector-crash step in the relay runbook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connect.linkProof compared the proof's loopback origin with the request
URI's authority, but an HTTP/1.1 request carries an origin-form target
with no authority, so every BiBCode Connect link and background re-link
from a real client failed with "Invalid managed endpoint origin." The
check now falls back to the Host header, as the auth layer does; tests
had passed absolute URIs and never hit the real shape.

Also pins the preview-hostname label boundary (a 60-character label
keeps its hash whole, a 61-character one shortens its stage prefix),
tests that gatewayOpen and relayConfig payloads decode with and
without the new optional fields, and corrects the contracts comment
on `origin`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T20:02:09.194625Z a15a09a Manual request
🔒 Security Review ✅ Completed 2026-10-13T13:34:44.668729Z b0e1ae6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Oct 13, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0e1ae648d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// Serves a request that reached the main port under this target's BiBCode Connect
/// preview hostname. TLS ended at Cloudflare, so the client-facing origin is `https`.
pub async fn serve_https<B: GatewayBody>(&self, request: Request<B>) -> Response<ProxyBody> {
let _activity = self.0.activity();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop unauthenticated traffic from refreshing relay slots

Every request to a public preview hostname calls activity() before the gateway validates a capability or session, and handle_request also touches the target before authentication. Consequently, anyone who learns a preview hostname can request it periodically without a cookie and prevent its target from reaching the 10-minute idle timeout; after four legitimate targets have existed, this can keep every slot occupied and make subsequent opens fail with relay-busy. Track the exchange for shutdown without updating idle activity until bootstrap/session authentication succeeds.

AGENTS.md reference: AGENTS.md:L230-L236

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a15a09a. Requests without a gateway session no longer touch the target: the per-request activity in serve_https and the touch() at the top of handle_request are gone. Only a bootstrap, or a request whose cookie names a gateway session, counts as activity. That request holds an activity guard through its principal check, so the idle sweep can't close the target under it before the lease takes over (a follow-up local Codex pass caught that window).

Tests in relay.rs:

  • only_authenticated_requests_keep_a_slot_from_going_idle: a sessionless request no longer keeps the slot, and an authenticated one does.
  • a_session_request_awaiting_its_principal_check_keeps_the_slot: holds the principal check open and runs the idle sweep.

Both failed before the fix. The direct (non-relay) gateway port keeps its per-connection activity; it has no shared slots to exhaust.

Comment on lines +275 to +276
if (isBrowserMode() && tabId !== undefined) {
return fail({ message: relayFrameMessage(label) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route managed-endpoint primary previews to a new tab

When the web UI itself is loaded from the environment's managed endpoint, its connection remains a PrimaryConnectionTarget, so the resolver classifies the public DNS name as via: "host" rather than via: "relay". openLink therefore selects the same-host Browser panel; only after gatewayOpen returns an origin does this branch reject the framed navigation, leaving a normal click at an error instead of opening the promised new tab. Detect this primary-over-managed-endpoint case before choosing the destination, so browser user activation can synchronously reserve the new tab.

AGENTS.md reference: AGENTS.md:L61-L73

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this one happens. A page served from the managed endpoint is always HTTPS, because plain HTTP on the environment's hostname gets a 308 to HTTPS. On an HTTPS browser-mode page, canFrameGatewayPreviews() is false (the HTTP gateway would be mixed content), so isPreviewSupportedInRuntime() is false. That makes canUseApp in openLink false: the click takes the system path, openGatewayInSystemBrowser, and reserves the new tab synchronously from the click. The framed refusal in previewGateway.ts is never reached from a click on that page. The HTTPS rule is covered by previewBridge.test.ts ("has no preview host on an HTTPS page"). No change made.

A preview hostname is public, and every request to it refreshed its
target's idle clock before any session check, so anyone polling the
hostname (or a stale tab's reload client) kept the target, and its
relay slot, from ever closing; four such targets left every open
failing relay-busy. Now only a bootstrap or a request carrying a
gateway session counts as activity. That request holds the target open
through its principal check, so the idle sweep cannot close the target
under it before its lease takes over.

Addresses the Codex review on #91.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mubeda

mubeda commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: a15a09a6bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

mubeda added a commit that referenced this pull request Oct 13, 2026
Bump the application manifests and Cargo.lock to 0.9.2 and add the curated
v0.9.2 changelog entry covering #88, #89, #90, #93, #94, #95, the PR #91
preview gateway over BiBCode Connect (#68), the skill catalog cache, and the
Create PR/MR dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsdQGFUPxjNZ8GQocThxT1
@mubeda
mubeda merged commit 4eb03ef into main Oct 14, 2026
32 of 33 checks passed
mubeda added a commit that referenced this pull request Oct 14, 2026
Requested: merge PR #92 and PR #91 and release v0.9.2 for everyone,
including everything found in the 2026-10-13 profiling session.

Implementation:
- #89 remote clients: thread stream patches and coalescing, E2EE
  compression of large server messages (negotiated, kill switch), and
  per-method `rpc.bytes` in the Profiler.
- #90 PR panel: glab without telemetry, Approve permission reads in one
  wave, stored-copy paint for GitHub and GitLab.
- #88 Git watcher: rescan backoff after overflows; on Linux the worktree
  watch skips Git-ignored directories and nested repositories.
- #93 Claude delivery: an early result no longer ends the new turn; late
  replays confirm uncertain deliveries.
- #94 update relaunch under a systemd unit; Profiler reports coredumps no
  session covered.
- #95 seeded upgrade lanes from any stable tag; Windows relaunch required.
- Provider skill catalog cache; hide-worktrees and VCS summary fixes;
  Create PR/MR dialog; PR #91 preview gateway over BiBCode Connect (#68).

Tests: regression tests for every fix (each fails without it); server lib,
rpc_wire, provider_claude, desktop lib, vp test, check, typecheck,
contracts, release smoke, locked cargo check. Review: Codex after every
change until clean. Docs: architecture overview, providers, remote,
rpc-and-orchestration, release and Windows testing runbooks, profiler
references, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsdQGFUPxjNZ8GQocThxT1
@mubeda
mubeda deleted the main-3 branch October 19, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Internal browser: preview gateway reach for BiBCode Connect (relay) environments

1 participant