Skip to content

Securely connect the browser to a local model (0289) - #439

Merged
crs48 merged 12 commits into
mainfrom
claude/0289-securely-connecting-the-browser-to-a-local-model
Jul 9, 2026
Merged

Securely connect the browser to a local model (0289)#439
crs48 merged 12 commits into
mainfrom
claude/0289-securely-connecting-the-browser-to-a-local-model

Conversation

@crs48

@crs48 crs48 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Implements exploration 0289 — harden the loopback agent bridge into the secure spine the web app talks to local agents (Claude Code / Codex) and raw local models (Ollama / LM Studio) through.

What changed

Bridge daemon hardening (@xnetjs/devkit, breaking → major)

  • /v1/chat/completions and /run now require a per-launch pairing token (Authorization: Bearer, constant-time compared). /health stays unauthenticated so detection still works before pairing.
  • Host-header validation rejects DNS-rebinding requests before origin/token checks (the Ollama CVE-2024-28224 fix). Mirrored onto the MCP HTTP transport (@xnetjs/plugins).
  • New openAiChatAgent lets the bridge front a raw local model (Ollama/LM Studio) through the same authenticated door — xnet bridge serve --upstream http://localhost:11434.

Token delivery (out-of-band, never over HTTP)

  • Electron passes allowedOrigins (app.xnet.fyi + XNET_BRIDGE_ALLOWED_ORIGINS) and forwards the token to its renderer over IPC → auto-pairs.
  • xnet bridge serve prints a pairing code (and --token to pin it); the web app gets a pairing-code field to paste it into.

Web app (xnet-web)

  • Pairing-code field + bridge provider carries the token as its bearer.
  • Queries the Chrome loopback-network permission and shows an "allow local network access" hint on denial instead of failing silently.
  • CSP connect-src now includes http://127.0.0.1:* / ws://127.0.0.1:* (the bridge's default address).
  • local-server tier now shows the exact OLLAMA_ORIGINS=<origin> line (never a wildcard).

Trust boundary

Host gate → origin allowlist → bearer token, all server-side (the only layer that holds on Safari/older Firefox, which don't yet enforce Local Network Access).

Tests

New/updated coverage in bridge-server.test.ts (token + Host + rebind), mcp-http.test.ts (rebind), chat-agent.test.ts (upstream proxy), detect.test.ts (exact-origin hint), bridge.test.ts (--token), ai-chat-connector.test.ts (bridge token). All affected suites green; full typecheck (45 tasks) passes.

Scope note

11/12 implementation items shipped. The 12th — a native-messaging browser-extension bridge (Option C) for the pure-web PWA — is explicitly a follow-up in the exploration and is tracked as separate work; everything else (the complete secure bridge) is here. Exploration doc left at [_] pending that follow-up.

🤖 Generated with Claude Code

xNet Test added 11 commits July 9, 2026 07:21
… model

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

Signed-off-by: xNet Test <test@xnet.dev>
The loopback chat daemon (:31416) now requires a per-launch pairing token
(Authorization: Bearer, constant-time compared) on /v1/chat/completions and
/run, and validates the Host header to reject DNS-rebinding requests before
origin/token checks. /health stays unauthenticated so detection works before
pairing. Exploration 0289.

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

Signed-off-by: xNet Test <test@xnet.dev>
Mirror the agent-bridge anti-DNS-rebinding gate: reject any request whose Host
isn't the exact loopback authority before origin/token checks. Exploration 0289.

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

Signed-off-by: xNet Test <test@xnet.dev>
…very

Electron starts the bridge with allowedOrigins (app.xnet.fyi +
XNET_BRIDGE_ALLOWED_ORIGINS) and forwards the pairing token to its renderer
over IPC status (never HTTP). `xnet bridge serve` prints the pairing code and
gains --token to pin it. Exploration 0289.

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

Signed-off-by: xNet Test <test@xnet.dev>
…ver tier

detectConnectors accepts appOrigin and bakes the precise
`OLLAMA_ORIGINS=<origin> ollama serve` line (never a wildcard) into the
local-server setup hint. Exploration 0289.

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

Signed-off-by: xNet Test <test@xnet.dev>
Thread the bridge pairing code through the connector (Authorization: Bearer),
add a pairing-code field (auto-filled over IPC under Electron), query the Chrome
loopback-network permission and hint on denial, pass appOrigin so the
local-server hint names the exact OLLAMA_ORIGINS line, and add 127.0.0.1 to the
CSP connect-src. Exploration 0289.

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

Signed-off-by: xNet Test <test@xnet.dev>
openAiChatAgent forwards the conversation to an upstream OpenAI-compatible
server (Ollama/LM Studio/vLLM); `xnet bridge serve --upstream <url>` routes a
raw local model through the authenticated, origin-locked, Host-validated bridge
instead of the user weakening the model server's own CORS. Exploration 0289.

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

Signed-off-by: xNet Test <test@xnet.dev>
… minor)

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

Signed-off-by: xNet Test <test@xnet.dev>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-439 July 9, 2026 14:54 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No visual differences detected in the changed UI.

CI run

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #439.

github-actions Bot added a commit that referenced this pull request Jul 9, 2026
@crs48
crs48 temporarily deployed to pr-439 July 9, 2026 15:24 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 9, 2026
github-actions Bot added a commit that referenced this pull request Jul 9, 2026
@crs48
crs48 merged commit 8b3ccb2 into main Jul 9, 2026
23 of 24 checks passed
@crs48
crs48 deleted the claude/0289-securely-connecting-the-browser-to-a-local-model branch July 9, 2026 15:40
github-actions Bot added a commit that referenced this pull request Jul 9, 2026
crs48 added a commit that referenced this pull request Jul 10, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @xnetjs/devkit@1.0.0

### Major Changes

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- [#446](#446)
[`10c9f87`](10c9f87)
Thanks [@crs48](https://github.com/crs48)! - Isolate git subprocesses
from inherited repo-location env. When the dev loop (or
its tests) ran while a git hook was active — e.g. husky `pre-push`
running
`pnpm test` — the hook's exported
`GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE`
leaked into `git` children and overrode the explicit `cwd`, so
operations
(`config`, `commit`, even `push`) targeted the hook's repo instead of
the
requested worktree. `NodeCommandRunner` now scrubs git's repo-location
env vars
  for `git` invocations so `cwd` is always authoritative; an explicit
  `options.env` entry still wins.
## @xnetjs/cli@0.1.0

### Minor Changes

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`853d849`](853d849),
[`10c9f87`](10c9f87),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/runtime@0.2.0
  - @xnetjs/devkit@1.0.0
  - @xnetjs/data@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/plugins@0.8.0

### Minor Changes

- [#420](#420)
[`dd3b1cb`](dd3b1cb)
Thanks [@crs48](https://github.com/crs48)! - Single-shell layout
primitives (exploration 0284): `createDefaultTree()` and
`DEFAULT_WORKSPACE_ID` join the workspace layout API — the one canonical
tree (a sectioned sidebar in the rail, the full left dock, tabs on) that
replaces the quiet/calm/bench preset trichotomy. Purely additive:
`createPresetTree` and the preset ids remain for the devtools seed and
portable-workspace round-trips.

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/abuse@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/runtime@0.2.0

### Minor Changes

- [#448](#448)
[`853d849`](853d849)
Thanks [@crs48](https://github.com/crs48)! - `NodeStoreSyncProvider` now
handles hub capacity rejections gracefully: on the first
`QUOTA_EXCEEDED` (over the hub's per-user cap) or `STORAGE_FULL` (hub
disk full) rejection it pauses outbound sync instead of re-flooding the
hub, keeps local data intact, and resumes on the next reconnect.
Subscribe to the new `onSyncBlocked(listener)` API (with
`SyncBlockedReason`/`SyncBlockedListener` types) to surface a "storage
full" notice in your app.

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/history@0.8.0
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/storage@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/abuse@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/crypto@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/core@0.8.0
## @xnetjs/data@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/storage@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/data-bridge@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/history@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/identity@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/react@0.8.0

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`853d849`](853d849),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/runtime@0.2.0
  - @xnetjs/history@0.8.0
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/storage@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/sync@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/core@0.8.0


## @xnetjs/sqlite@0.8.0


## xnet-cloud@0.0.11

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
  - @xnetjs/cloud@0.0.1
## @xnetjs/brain@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/vectors@0.0.1
## @xnetjs/comms@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/dashboard@0.0.12

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/react@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/social@0.0.12
## @xnetjs/labs@0.0.12

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/data@0.8.0
## @xnetjs/licenses@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
## @xnetjs/maps@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
## @xnetjs/meetings@0.0.5

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/data@0.8.0
## @xnetjs/server@0.0.11

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/social@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/unreal@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
## xnet-desktop@0.8.0

Desktop shell release riding the @xnetjs/core 0.8.0 train.
Desktop-specific changes are not tracked here; see the core packages'
changelogs for what shipped.
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.

1 participant