3 Commits
Author SHA1 Message Date
Miles Ward b8001cbebd 0.5.1: validated on dsh 0.2.2-alpha.2
No code change from 0.5.0. The 0.2.2-alpha.2 review found the allow path's
bundles byte-identical, the webserver's new resource route kind orthogonal to
the interceptor, and the frontend static bundle's resource rewrite still
emitting webserver/index-inject. Full probe matrix (11/11) and Files pane
checks pass against a sandbox of the published release; README records the
new dsh web --host wildcard refusal.
2026-10-28 11:06:21 -04:00
mward4 8ae1e1fafa 0.5.0: remove the launch-token check by default again
0.4.0 made the browser-session check opt-out, so a deployment whose whole
point is "no trusted host, no token" started asking for the launch token on
every new device — including a phone, where producing it is the friction the
plugin exists to remove.

The bypass itself was never in doubt and is unchanged: `disableAuth: true`
still replaces `connection.admit`, `requestRejection`, `authorizeIndex` and
`authenticatedUrl`. Measured on DSH 0.2.1-rc.2 over a non-loopback Host with
no cookie: `GET /` answers 200 and a socket upgrade to `/api/remote.mux`
answers 101. Only the default moved.

`disableAuth: false` keeps 0.4.0's posture — the launch token, the 30-day
cookie bound to the rewritten loopback authority, and the check.
2026-10-18 22:55:44 -04:00
mward4 a2700854a9 0.4.0: keep the launch-token session check by default
0.2.1-alpha.3 moved the request gate into `connection.admit`: the `/api`
gateway and the WebSocket upgrade call it directly, and `requestRejection`
became a façade over it. The 0.3.0 bypass only replaced `requestRejection`, so
under `disableAuth: true` the index loaded and every socket upgrade answered
401. Replace `admit` too, returning the peer the harness already owns
(`browserPeer` from alpha.3 on, `operator` before).

The default posture now keeps the harness's browser-session check instead of
removing it. The plugin's Host rewrite to the loopback authority is also the
authority the one-time launch token is exchanged for, so the 30-day cookie is
minted for the rewritten authority and the rewrite presents it on every later
request. Measured on 0.2.1-rc.1, 0.2.1-alpha.4 and 0.2.1-alpha.2: 401 without
the cookie, 303 + Set-Cookie on `/?token=`, 200 index and 101 upgrade with it.

Declare `engines.dsh` (`>=0.1.7-rc.2 <0.3.0-0`) as 0.2.1-alpha.4 now prefers.
The removed session check moves to an opt-in `disableAuth: true`, which still
prints a token-free URL. Tests: 60 pass, including the operator-peer fallback
and restoring the real `admit` when the fiber is disposed.
2026-10-17 06:52:12 -04:00
5 changed files with 307 additions and 54 deletions

No files matched your search

+213 -32
View File
@@ -1,24 +1,27 @@
# dsh-trustworthy
**Removes the untrusted-origin restriction from DeepSeek Harness.** Every browser
window is treated as if it were running on the harness machine itself: no process
token, no declared trusted host, and no per-plugin origin fence, because one
interceptor answers every request with the loopback authority.
**Removes the untrusted-origin restriction from DeepSeek Harness, and the
launch-token prompt with it.** Every browser window is treated as if it were
running on the harness machine itself: no declared trusted host, no per-plugin
origin fence, and no process token to paste into a phone — one interceptor
answers every request with the loopback authority, and the session check is off
by default.
A window that reaches the harness by a LAN address, a DNS name, or a tunnel
authenticates normally and then fails every mutating request with
`403 {"error":"untrusted origin"}`. Cards render, but Install, Update, toggle,
and "Update all" do nothing that the user can see. This plugin removes the
distinction between a local window and a remote one — with no token, no
`--trusted-host`, no per-plugin configuration, and no second listener on a
second port.
distinction between a local window and a remote one — with no declared trusted
host, no `--trusted-host`, and no per-plugin configuration. The launch-token
check is removed as well; `disableAuth: false` puts it back.
---
## ⚠️ Read this before enabling it
**This plugin is a deliberate and total removal of the harness's request-identity
fence. It is not a bug fix and it is not a workaround for one specific plugin.**
fence, and by default of its browser-session check too. It is not a bug fix and
it is not a workaround for one specific plugin.**
The harness fences requests by their `Host` and `Origin` headers for one
reason: those are the only headers a page on another origin cannot forge, so
@@ -29,25 +32,41 @@ while the harness is reachable. With this plugin enabled:
- **A page on any other origin** can issue mutating requests to the API.
- Those requests can **run commands on your machine** through the agent.
With `disableAuth: true` (the default) there is no session check either, so
And with the default `disableAuth: true` there is no session check either:
**anyone who can reach the port has full control of the agent, including code
execution.** Reachability becomes the only access control.
execution.** Reachability is the only access control.
That is a reasonable posture when the port is behind something that
authenticates on your behalf, or when it is bound to a network you fully
control. It is a poor posture for a `0.0.0.0` bind fronted by a public tunnel
with no authentication of its own. Decide accordingly, and re-read this
section if you change how the port is exposed.
That posture is reasonable when the port is behind something that authenticates
on your behalf, or when it is bound to a network you fully control. It is a poor
posture for a `0.0.0.0` bind fronted by a public tunnel with no authentication of
its own. Decide accordingly, and re-read this section if you change how the port
is exposed. Set `disableAuth: false` to restore the launch token as a second
line of defence.
### The launch token, and the `disableAuth` switch
The Web UI prints a one-time launch token in the process log —
`dsh web: http://…/?token=…` — and from `0.2.1-alpha.4` the container entrypoint
writes that line into the Docker log as well. The token rotates on every
restart. With the check enabled, every new device or origin must open that URL
once; the harness exchanges the token for a **30-day cookie** bound to the
authority it was minted under, and until then every page answers `401`.
`0.4.0` made that check the default, on the argument that one exchange covers
every domain because the plugin rewrites `Host` to the same loopback authority
the cookie is bound to. In practice it still means producing a token on each new
device, which is the friction this plugin exists to remove — `0.5.0` therefore
restores `disableAuth: true` as the default. Set `disableAuth: false` to get the
check, and the cookie, back.
If you want the errors gone but not the exposure, set `disableAuth: false`.
That keeps the process token as the access control while still answering the
identity fence, which is what actually produces the 403.
---
## What it changes
Five things, each one reachable only by a different kind of fence.
Five things, each one reachable only by a different kind of fence. None of them
touches the browser-session check; removing that is the separate `disableAuth`
key described at the end of this section.
**1. Request identity, at the server.** The plugin installs `request` and
`upgrade` listeners with `prependListener` on the raw `node:http` server the
@@ -166,7 +185,7 @@ Override any key in your profile's `cordis.patch.yml`:
| Key | Default | Effect |
|---|---|---|
| `enabled` | `true` | Master switch. `false` loads the plugin and changes nothing. |
| `disableAuth` | `true` | Also removes the browser-session check, so no process token is needed. **This is the one that removes access control.** |
| `disableAuth` | `true` | **Also** removes the browser-session check, so no launch token and no per-device pairing are needed. This is the default: reachability is the only access control. Set `false` to keep the token — exchange it once and the 30-day cookie covers every rewritten request. |
| `forceClientLoopback` | `true` | Grants the browser-side `isLoopback` flag so the settings and model surfaces act as they do on the host. |
| `grantPairing` | `true` | Provides the paired-device service that plugins like the Skill Center accept instead of a loopback peer. See below. |
| `provideWebRuntime` | `true` | Restores the `webRuntime` trust snapshot the harness stopped publishing in 0.2.1-alpha.2, so plugins that inject it (the sidebar) mount instead of answering 405. See below. |
@@ -175,6 +194,33 @@ Override any key in your profile's `cordis.patch.yml`:
| `shimRandomUuid` | `true` | Restores `crypto.randomUUID` on plain HTTP. See below. |
| `shimPwa` | `true` | Serves a manifest and adds the installable-app metadata. See below. |
### `disableAuth` — the two postures
Every key above only answers fences. `disableAuth` is the one that changes who
may talk to the harness at all.
- **`true` (default).** The session check is replaced too: `connection.admit`
returns the owner's own peer for every request, `authorizeIndex` admits the
index, and `authenticatedUrl` stops appending the token to the printed URL. No
token, no cookie, no login, nothing to type on a phone — and anyone who reaches
the port controls the agent. Use it behind an authenticating proxy, on a
network that is already trusted, or when the port is otherwise unreachable from
untrusted networks.
- **`false`.** The request-identity fence is still rewritten away, and the
browser-session check is left exactly as the harness built it. A new browser
shows `401` until the launch URL from the process log is opened once; that
exchange sets the 30-day cookie, and every later request — including the ones
this plugin rewrote the `Host` on — carries it. Each new device still has to be
given the token, which is why it is not the default.
The implementation replaces `admit` because that is where every shape of the
gate ends up: releases up to `0.2.1-alpha.2` call `requestRejection` from inside
`admit` and wrap its answer, while `0.2.1-alpha.3` and later apply the fence and
session check in `admit` itself. `requestRejection` is replaced as well so the
older shape cannot short-circuit around the newer one, and every replacement is
liveness-aware, so a host that switches the plugin off without running its
disposers gets the real gate back.
### `provideWebRuntime` — the Files pane, and HTTP 405
`0.2.1-alpha.2` stopped providing a service named `webRuntime`. Its value was the
@@ -304,9 +350,30 @@ three are `true` by default; serving over HTTPS makes the first two redundant.
## Compatibility
Verified against **DSH 0.1.7-rc.2**, **0.2.0-rc.1**, **0.2.1-alpha.1**, and
**0.2.1-alpha.2**. The plugin depends on several things that are internal rather
than public API, so a harness release that renames any of them will break it:
Verified against **DSH 0.1.7-rc.2**, **0.2.0-rc.1**, **0.2.1-alpha.1**,
**0.2.1-alpha.2**, **0.2.1-alpha.4**, **0.2.1-rc.1**, and **0.2.2-alpha.2**.
From 0.4.0 the manifest also declares the range:
```json
"engines": { "dsh": ">=0.1.7-rc.2 <0.3.0-0" }
```
`0.2.1-alpha.4` made `engines.dsh` the compatibility declaration a plugin should
provide — the harness uses it, when present, in place of guessing from
`@deepseek-ai/dsh-*` peer ranges. This plugin has no dsh peers at all, so the
declaration is the only statement of support it makes, and it is bounded above
at the next minor release, excluding its prereleases, rather than left open.
`0.2.1-rc.2` is not listed separately because all four bundles the plugin hooks
are byte-identical between rc.1 and rc.2. `0.2.2-alpha.2` *is* listed: it is the
first release since 0.2.1-rc.1 that moves a hooked bundle, so it was re-checked live
rather than inferred. Its `dsh-client-connection` and `dsh-web-app` are still
byte-identical to `0.2.1-rc.2`; `dsh-host-webserver` and
`dsh-host-frontend-static` changed in ways that leave the surfaces used here
intact, and `dsh-session`'s stricter publication does not touch this plugin.
The plugin depends on several things that are internal rather than public API,
so a harness release that renames any of them will break it:
| Depends on | Where |
|---|---|
@@ -314,9 +381,14 @@ than public API, so a harness release that renames any of them will break it:
| `webServer.host` — the listener's own bind address | `@deepseek-ai/dsh-host-webserver` |
| `webServer.register` / `webServer.tapIndex` — the route registry and index transforms | `@deepseek-ai/dsh-host-webserver` |
| `connection.trustedHosts` — the authorities the `/api` fence reads | `@deepseek-ai/dsh-client-connection` |
| `connection.admit` — the fence and session check, alone from 0.2.1-alpha.3 | `@deepseek-ai/dsh-client-connection` |
| `connection.requestRejection` / `authorizeIndex` / `authenticatedUrl` | `@deepseek-ai/dsh-client-connection` |
| the `webserver/index-inject` channel and the `ownsHost` marker | webserver + connection client |
The only one used conditionally is `connection.admit`: the bypass path replaces
it when `disableAuth: true` (the default). With `disableAuth: false` the plugin
does not touch it at all.
Every one of those is probed before use. If a release reshapes the surface, the
plugin logs which capability it lost and carries on with the rest, rather than
failing the fiber and taking the parts that still work down with it. Losing the
@@ -328,6 +400,104 @@ interceptor was not installed and a Host fence will still refuse a remote
window; the harness surface may have changed
```
### What was checked on 0.2.2-alpha.2
The first release since 0.2.1-rc.1 that moves a bundle this plugin hooks, so it was
re-checked end to end instead of inferred from byte-identity.
**What moved.** `dsh-client-connection` and `dsh-web-app` are still
byte-identical to `0.2.1-rc.2`, so the allow path — `admit`, `trustedHosts`,
`requestRejection`, `authorizeIndex`, `authenticatedUrl` — is untouched. The two
surfaces that did move are `dsh-host-webserver` and `dsh-host-frontend-static`.
The webserver gained a second route kind (`{ resource }`: a Fetch-API
`Request → Response` handler, plus a `readResource` variant) beside the plain
`{ handler }` one; `server`, `register`, `tapIndex`, `host`, `port`, and
`protocol` are unchanged, and the interceptor wraps the raw server's request
emission before routing, so a route's kind does not matter to it. The frontend
static bundle still authorizes the index through
`connection.authorizeIndex(request, shim)` and still emits
`webserver/index-inject`, so the browser grant, the shims, the viewport, and the
PWA route inject exactly as before. `dsh-session` tightened store-owned
publication (`cannot append outside a SessionStore`), which this plugin never
does. The CLI's new optional bundles do not change a profile's explicit bundle
list, so nothing new is inserted next to this one.
**Measured on a sandbox of the published `@deepseek-ai/[email protected]`** (the
npm `alpha` tag; `latest` still points at `0.2.1-rc.2`), with a copy of a real
profile, over a non-loopback peer:
| Check | Result |
|---|---|
| interceptor answers `GET /` for a foreign `Host` | `200` index, no cookie |
| WebSocket upgrade for a foreign `Host` | `101` |
| `/api/*` over the non-loopback peer and over a named host | `200`, not the `400` trust fence |
| browser grant, shims, viewport, and PWA markers in the served index | all present |
| PWA manifest | `200 application/manifest+json` |
| Files pane over a foreign `Host` — `fs.tree`, `fs.read`, `GET /sidebar/file` download | `200`, and the downloaded bytes are the file's |
| `dsh plugin` load | no skip line, no missing-surface warning |
`node scripts/live-probe.mjs` passes **11/11** against that sandbox, including
the sidebar Files API row. A control run with the bundle disabled answers `401`
to `GET /` — the token gate stands — so a remote browser never gets as far as
the pane; that is the difference the bypass makes. The installed sidebar bundle
(0.28.0) no longer injects `webRuntime` at all, so on this harness its Files
routes register whether or not the service is provided; `provideWebRuntime`
stays for bundles that still inject it (0.26 and older), where the missing
service is what left the pane answering `405`.
One harness deployment change to know about: since DSH 0.2.1-alpha.2, `dsh web --host` refuses
a wildcard address (`0.0.0.0`, `::`) — *"is an unspecified (wildcard) address,
which is not supported: binding every interface would expose remote code
execution to the network; bind one concrete IPv4 or IPv6 address instead"* — so
a container that bound wildcard must pass its concrete container IP. The bind
address is still accepted as an authority by the trust snapshot, so
`provideWebRuntime` and the no-`--trusted-host` posture keep working.
### What was checked on 0.2.1-rc.1
This is the release the 0.4.0 change answers. Two things moved.
**The gate moved into `admit`.** `requestRejection` is now a façade over
`admit`, and the `/api` route and the WebSocket upgrade call `admit` directly.
A plugin that only answered `requestRejection` — which is all 0.3.0 did when
`disableAuth: true` — left the index reachable and every WebSocket refused with
`401`, so the page loaded and then showed a disconnected shell. The bypass path
now replaces `admit` itself and returns the owner's peer (`browserPeer` on this
release, `operator` before alpha.3); measured on a sandboxed rc.1 with a
non-loopback `Host`, the upgrade answered `101` with no cookie.
**The session check, when enabled, survives the rewrite.** The plugin rewrites
`Host`/`Origin` to `127.0.0.1:<port>`, which is also the authority the harness
mints the session cookie for. Measured on a sandbox, with `disableAuth: false`,
in order:
| Request (`Host: ai.milesward.dev`) | Result |
|---|---|
| `GET /` with no cookie | `401` |
| `GET /?token=<launch token>` | `303`, `location: ./`, `Set-Cookie: dsh-auth-…` |
| `GET /` with that cookie | `200` index |
| `Upgrade /api/remote.mux` with that cookie | `101` |
The cookie payload's `authority` is `127.0.0.1:<port>` — the rewritten one — so
the browser keeps the cookie for the public origin it opened, and the plugin's
rewrite presents the matching authority on every request afterwards. This is
what lets the 30-day session survive a Host rewrite, and it is why
`disableAuth: false` is a viable posture for anyone who would rather keep the
check.
The token rotates whenever the process restarts (`dsh web: …?token=…` in the
process log; the container entrypoint prints that line into the Docker log from
0.2.1-alpha.4). The safety property the old bypass traded away is exactly this
one: after `disableAuth: true` the printed URL has no token and every session is
unauthenticated.
Also verified on this release: `webServer.server` / `register` / `tapIndex` and
the `webserver/index-inject` channel are unchanged, so the interceptor, the
browser grant, the shims, and the PWA route need no adjustment. The
`dsh-client-connection`, `dsh-host-webserver`, `dsh-web-app`, and
`dsh-host-frontend-static` bundles are byte-identical between 0.2.1-rc.1 and
0.2.1-rc.2.
### What was checked on 0.2.1-alpha.2
Everything in the table above is still present and still of the shape this plugin
@@ -445,14 +615,17 @@ Three guards follow from that, and each has a test:
- `sec-fetch-site: cross-site` is refused **unconditionally** by
`isTrustedApiRequest`, before the `Origin` check and regardless of the
trusted-host list. No header rewrite can defeat it, and this plugin does not
try. It is inert with the default `disableAuth: true`, because the gateway
never reaches the fence. It is the one thing that can still refuse a request
with `disableAuth: false`.
- Rewriting `Host` changes the **cookie authority** the harness derives from it
(`dsh-auth-<sha256(authority)>`). With `disableAuth: false` that invalidates
sessions minted under the hostname you used before — one re-login — and
afterwards a single login covers every entry domain instead of one per domain.
Inert with `disableAuth: true`.
try. It only guards requests a browser itself marks as cross-site, so it has
no effect on normal navigation or on the WebSocket. With `disableAuth: true`
(the default) the gate that calls it is replaced, so it does not run either.
- Rewriting `Host` decides the **cookie authority** the harness derives from it
(`dsh-auth-<sha256(authority)>`). With `disableAuth: false` the launch token is
exchanged for the rewritten authority, so the resulting 30-day cookie matches
every request the plugin intercepts. The authority is the loopback bind
(`127.0.0.1:<port>`), so changing `--port` mints a new one and requires one new
exchange. A session cookie you already
hold under the public hostname is not reused; expect one re-login on enabling
the plugin.
- `Referer` is read by no layer of the harness, core or plugin. It is rewritten
only for third-party code that might read it.
@@ -532,11 +705,16 @@ node test/web-runtime.test.mjs # the restored webRuntime value, and when it is
node scripts/live-probe.mjs # against the running harness, over a non-loopback peer
```
All five suites together — **59 tests** (fence 8, apply 20, handover 15,
All five suites together — **61 tests** (fence 8, apply 22, handover 15,
shims 9, web-runtime 7) — are `node --test test/*.test.mjs` from the plugin
directory. The shell has to expand that glob: `node --test test/` fails with `MODULE_NOT_FOUND`
on Node 22.
The manifest pins what it supports: `engines.dsh` is
`>=0.1.7-rc.2 <0.3.0-0`, so a harness from a future minor release skips this
plugin instead of loading it against a surface it was never checked on. Bump the
range only after re-running the probe matrix above.
`test/fence.test.mjs` loads `sameOrigin()` from the installed `dshmarket` when a
profile is present and says which source it used, so a run against a bundled
copy is never mistaken for a run against the real thing. `test/shims.test.mjs`
@@ -556,6 +734,9 @@ remote client and always looks like it passes.
| Version | What changed |
|---|---|
| **0.5.1** | Documentation and compatibility only — **no code change from 0.5.0**. Records the 0.2.2-alpha.2 review: the allow path's bundles are byte-identical, the webserver's new route kind does not touch the interceptor, and `scripts/live-probe.mjs` passes 11/11 against a sandbox of the published release (Files pane, PWA, shell, Skill Center, shims). Also notes the new `--host` wildcard refusal. |
| **0.5.0** | Restores `disableAuth: true` as the default: no launch token and no per-device pairing, which is what the plugin exists to remove. `0.4.0`'s keep-the-check posture remains available with `disableAuth: false`. No code change beyond the default and the documentation; validated on DSH `0.2.1-rc.2`, where the bypass still replaces `connection.admit`. |
| **0.4.0** | Keeps the browser-session check by default (`disableAuth: false`) and makes the 0.2.1-alpha.3 launch-token → 30-day-cookie flow survive the Host rewrite, including on a remote origin; the no-check posture moves to `disableAuth: true` and now replaces `connection.admit` as well, because the gate moved there. Declares `engines.dsh` (the 0.2.1-alpha.4 recommendation) and is validated on 0.2.1-alpha.4 and 0.2.1-rc.1. |
| **0.3.0** | Provides the `webRuntime` trust snapshot that `0.2.1-alpha.2` stopped publishing (`lib/web-runtime.js`, `provideWebRuntime`), so a plugin that injects it — the sidebar among them — mounts instead of answering 405 on every request. The value is read live from `connection.trustedHosts` plus the listener's bind address, and an existing provider is left alone. |
| 0.2.1 | Documentation and compatibility only — **no code change from 0.2.0**. Records the 0.2.1-alpha.1 validation above, and how to install so the market can update it. |
| 0.2.0 | Renamed from `dsh-disable-untrusted-origin`. Added `lib/handover.js`: originals recorded once on a `Symbol.for` carrier, a liveness check (`fiber.uid === null`) on every delegated path, and an interceptor that retires a tagged predecessor. Capabilities now degrade with a warning instead of failing the fiber. |
+8 -4
View File
@@ -4,13 +4,17 @@
# (package.json declares dsh.bundle.patch).
#
# The defaults below are the plugin's stated posture: the identity fence is
# disarmed, the browser-session check is removed, the browser is granted the
# host's own isLoopback flag, and the APIs a non-secure page is denied are
# restored. Override any key in your profile patch:
# disarmed, no process token or per-device pairing is required, the browser is
# granted the host's own isLoopback flag, and the APIs a non-secure page is
# denied are restored. Reachability is therefore the only access control — put
# the port behind something that authenticates on your behalf if it is exposed.
# To keep the harness's own browser-session check instead (the first visit is
# then the one-time launch token from the process log, exchanged for a 30-day
# cookie), set `disableAuth: false`. Override any key in your profile patch:
#
# - id: trustworthy
# config:
# disableAuth: false # keep the process token, disarm only the fence
# disableAuth: false # keep the launch-token check; one token per device
# grantPairing: false # when dsh-remote-web-ui provides the pairing service itself
# spoofSocketAddress: true # also satisfy socket-based fences with no pairing path
# shimClipboard: false # unnecessary when you reach the harness over HTTPS
+42 -14
View File
@@ -11,7 +11,13 @@
* request with `403`, which reads to the user as a button that does nothing.
*
* This plugin removes the distinction in two places, and separately restores
* the browser capabilities a non-secure page is denied.
* the browser capabilities a non-secure page is denied. It removes the origin
* fence and the harness's own browser-session check by default, so a window is
* admitted with no process token and no per-device pairing. Set
* `disableAuth: false` to keep the session check, in which case the first visit
* is the one-time launch token from the process log (`dsh web: …?token=…`; the
* container entrypoint prints it into the Docker log from `0.2.1-alpha.4`),
* exchanged for a 30-day cookie bound to the authority it was minted under.
*
* 1. A `request`/`upgrade` listener installed ahead of every route answers
* each connection with the loopback authority. One interception covers the
@@ -47,12 +53,14 @@
* still owns the installation — so a reload cannot leave the plugin inert while
* it believes it is mounted, nor let a superseded interceptor keep acting.
*
* Security: this is a deliberate and total removal of the request-identity
* fence across the whole HTTP surface. It defends against DNS rebinding and
* against another page in the same browser driving this API, and both
* defences stop applying once this plugin is enabled. Read the README before
* turning it on, and keep the listening port behind something that
* authenticates on your behalf.
* Security: removing the request-identity fence is deliberate and total for
* every window that reaches the port. It defends against DNS rebinding and
* against another page in the same browser driving this API, and both defences
* stop applying once this plugin is enabled. The default also removes the
* session check, so reachability is the only access control — a `0.0.0.0` bind
* fronted by a public tunnel with no authentication of its own is wide open.
* Set `disableAuth: false` to keep the launch-token check. Read the README
* before deploying either posture.
*
* @module dsh-trustworthy
*/
@@ -123,10 +131,15 @@ export const Config = z.object({
.boolean()
.default(true)
.description(
'Also remove the browser-session check, so no process token is needed. '
+ 'Anyone who can reach the port then has full control of the agent, '
+ 'including code execution. Keep this true only behind a port or '
+ 'tunnel that authenticates on its own.',
'Remove the browser-session check as well, so no process token is ever '
+ 'needed and no device has to be paired. Anyone who can reach the port '
+ 'then has full control of the agent, including code execution: '
+ 'reachability is the only access control. This is the default because '
+ 'the plugin exists to make every window behave like the host window, '
+ 'and a token paste on each new device defeats that. Set it false to '
+ 'keep the harness\'s launch-token check: open the URL from the process '
+ 'log once, and the 30-day cookie it exchanges covers this plugin\'s '
+ 'rewritten requests too.',
),
forceClientLoopback: z
.boolean()
@@ -240,9 +253,17 @@ export function apply(ctx, config) {
if (config.disableAuth) {
const connection = ctx.connection
// The `/api` gateway asks one method whether the request may proceed; it
// applies the identity fence and then the session check. Answering
// `undefined` admits both.
// The gate is one method. Releases up to `0.2.1-alpha.2` asked
// `requestRejection` first and `admit` wrapped its answer; from
// `0.2.1-alpha.3` on `admit` applies the fence and the session check
// itself, and `requestRejection` is a façade over it. Replacing `admit`
// therefore satisfies both shapes, and replacing `requestRejection` keeps
// the older shape from short-circuiting around it.
//
// The peer that comes back is one the harness already owns: `operator` up
// to `0.2.1-alpha.2`, `browserPeer` from `0.2.1-alpha.3` on. There is no
// synthetic identity to invent, and returning the owner's peer is exactly
// what the session check would have produced for a logged-in browser.
//
// `authenticatedUrl` is the startup URL and the remote-access handoff;
// returning the base URL keeps the process token out of the printed link.
@@ -262,7 +283,14 @@ export function apply(ctx, config) {
const original = originalOf(connection, key)
return original === undefined ? undefined : original.apply(this, args)
}
const admit = (request) => {
const peer = connection.browserPeer ?? connection.operator
if (peer !== undefined) return { peer }
const original = originalOf(connection, 'admit')
return original === undefined ? undefined : original.call(connection, request)
}
const restore = installReplacements(connection, {
admit: delegate('admit', admit),
requestRejection: delegate('requestRejection', () => undefined),
authorizeIndex: delegate('authorizeIndex', () => true),
authenticatedUrl: delegate('authenticatedUrl', (baseUrl) => baseUrl),
+6 -3
View File
@@ -1,7 +1,7 @@
{
"name": "dsh-trustworthy",
"version": "0.3.0",
"description": "Removes the untrusted-origin restriction from DeepSeek Harness. Every browser window is treated as if it were running on the harness machine itself: no process token, no declared trusted host, and no per-plugin origin fence, because one interceptor answers every request with the loopback authority.",
"version": "0.5.1",
"description": "Removes the untrusted-origin restriction from DeepSeek Harness while keeping its own session check: every browser window is treated as if it were running on the harness machine itself, so no declared trusted host and no per-plugin origin fence refuse it. One interceptor answers every request with the loopback authority, and the one-time launch token is exchanged for a 30-day cookie exactly as on the loopback host.",
"type": "module",
"main": "lib/index.js",
"exports": {
@@ -19,6 +19,9 @@
"LICENSE"
],
"license": "MIT",
"engines": {
"dsh": ">=0.1.7-rc.2 <0.3.0-0"
},
"keywords": [
"dsh",
"dsh-plugin",
@@ -33,7 +36,7 @@
"patch": "./cordis.patch.yml"
},
"compatibility": {
"dsh": ">=0.1.7-rc.2"
"dsh": ">=0.1.7-rc.2 <0.3.0-0"
}
},
"peerDependencies": {
+38 -1
View File
@@ -25,6 +25,9 @@ async function mockHost({ rawServer = true, registry = true } = {}) {
requestRejection: function requestRejection() {
return 403
},
admit: function admit() {
return { rejection: 401 }
},
authorizeIndex: function authorizeIndex() {
return false
},
@@ -32,7 +35,12 @@ async function mockHost({ rawServer = true, registry = true } = {}) {
return `${baseUrl}?token=launch-secret`
},
}
const connection = { ...originals }
const connection = {
...originals,
// Releases up to 0.2.1-alpha.2 named the owner peer `operator`; from
// 0.2.1-alpha.3 on it is `browserPeer`. A real host has one or the other.
browserPeer: { id: 'browser-peer' },
}
const provided = new Map()
const server = createServer((_request, response) => {
response.writeHead(200)
@@ -195,21 +203,39 @@ test('disableAuth replaces the session methods and unload restores them', async
const host = await mockHost()
const { apply } = await import(PLUGIN)
apply(host.ctx, CONFIG)
assert.deepEqual(host.connection.admit({}), { peer: host.connection.browserPeer })
assert.equal(host.connection.requestRejection({}), undefined)
assert.equal(host.connection.authorizeIndex({}, {}), true)
assert.equal(host.connection.authenticatedUrl('http://box:3080'), 'http://box:3080')
host.unload()
assert.deepEqual(host.connection.admit({}), { rejection: 401 })
assert.equal(host.connection.requestRejection({}), 403)
assert.equal(host.connection.authorizeIndex({}, {}), false)
assert.equal(host.connection.authenticatedUrl('http://box:3080'), 'http://box:3080?token=launch-secret')
await new Promise((resolve) => host.server.close(resolve))
})
test('disableAuth admits through the operator peer on 0.2.1-alpha.2 and older', async () => {
const host = await mockHost()
const operator = { id: 'operator-peer' }
delete host.connection.browserPeer
host.connection.operator = operator
const { apply } = await import(PLUGIN)
apply(host.ctx, CONFIG)
try {
assert.deepEqual(host.connection.admit({}), { peer: operator })
} finally {
host.unload()
await new Promise((resolve) => host.server.close(resolve))
}
})
test('disableAuth false leaves the session methods untouched', async () => {
const host = await mockHost()
const { apply } = await import(PLUGIN)
apply(host.ctx, { ...CONFIG, disableAuth: false })
try {
assert.deepEqual(host.connection.admit({}), { rejection: 401 })
assert.equal(host.connection.requestRejection({}), 403)
assert.equal(host.connection.authorizeIndex({}, {}), false)
} finally {
@@ -418,11 +444,13 @@ test('a disposed fiber stops admitting requests', async () => {
const { apply } = await import(PLUGIN)
apply(host.ctx, CONFIG)
assert.deepEqual(host.connection.admit({}), { peer: host.connection.browserPeer }, 'admitting while mounted')
assert.equal(host.connection.requestRejection(), undefined, 'admitted while mounted')
assert.equal(host.connection.authorizeIndex(), true)
assert.equal(host.connection.authenticatedUrl('/base'), '/base')
host.ctx.fiber.uid = null
assert.deepEqual(host.connection.admit({}), { rejection: 401 }, 'the real gate answers again')
assert.equal(host.connection.requestRejection(), 403, 'the real fence answers again')
assert.equal(host.connection.authorizeIndex(), false)
assert.equal(
@@ -516,3 +544,12 @@ test('a webserver with no route registry is reported, not fatal', async () => {
host.unload()
await new Promise((resolve) => host.server.close(resolve))
})
test('the shipped default removes the session check', async () => {
// A token prompt on every new device is the friction this plugin exists to
// remove; 0.4.0 made the check opt-out, and that regressed the posture.
const { Config } = await import(PLUGIN)
const defaults = Config({})
assert.equal(defaults.enabled, true)
assert.equal(defaults.disableAuth, true)
})