1 Commits
Author SHA1 Message Date
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
5 changed files with 100 additions and 92 deletions

No files matched your search

+59 -65
View File
@@ -1,28 +1,27 @@
# dsh-trustworthy
**Removes the untrusted-origin restriction from DeepSeek Harness without
removing its authentication.** Every browser window is treated as if it were
running on the harness machine itself: no declared trusted host and no
per-plugin origin fence, because one interceptor answers every request with the
loopback authority. The harness's own browser-session check stays in place —
open the one-time launch URL from the process log once, and the 30-day cookie it
exchanges covers the rewritten requests too.
**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 declared trusted
host, no `--trusted-host`, and no per-plugin configuration. Removing the session
check as well (`disableAuth: true`) is available, but it is now opt-in.
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.**
The session check is a separate matter: it stays unless you ask for it to go.
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
@@ -30,47 +29,36 @@ they are what stops a web page you happen to visit from driving your agent API
while the harness is reachable. With this plugin enabled:
- **DNS rebinding** against the harness API is no longer defended.
- **A page on any other origin** that carries a session cookie can issue
mutating requests to the API.
- **A page on any other origin** can issue mutating requests to the API.
- Those requests can **run commands on your machine** through the agent.
The default posture keeps the browser-session check, so the additional exposure
is limited to a page holding a live session cookie. Set `disableAuth: true` and
there is no check either: **anyone who can reach the port has full control of
the agent, including code execution.** Reachability becomes the only access
control.
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 is the only access control.
That second 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.
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 why the default changed
### 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. Open the URL once on each origin you use, and the harness exchanges it
for a **30-day cookie** bound to the authority it was minted under; until then
every page answers `401`.
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`.
The exchange, the cookie, and the authority check are all present in
`0.2.1-alpha.2` and later; what changed is that the bypass (0.3.0 and the
`disableAuth: true` default it shipped) discarded all three. The default now
keeps them.
`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.
Because this plugin rewrites `Host` to the loopback authority before any route
looks at it, the cookie is minted for the loopback authority too, and the
rewrite presents that same authority on every request afterwards. One exchange
therefore covers every entry domain, and the token never has to be pasted into
the address bar by hand again. That is why the plugin now keeps the check by
default instead of removing it: the token flow is the access control that makes
an internet-facing port defensible.
If you want the errors gone but not the exposure, leave `disableAuth` at its
default `false`. Only set it `true` when something else already authenticates
callers, and accept that the harness then has no second line of defence.
---
@@ -184,7 +172,7 @@ Override any key in your profile's `cordis.patch.yml`:
- id: trustworthy
config:
enabled: true
disableAuth: false
disableAuth: true
forceClientLoopback: true
grantPairing: true
provideWebRuntime: true
@@ -197,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` | `false` | **Also** removes the browser-session check, so no launch token is needed at all. The default keeps it: exchange the token once and the 30-day cookie covers every rewritten request. `true` 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. |
@@ -211,17 +199,19 @@ Override any key in your profile's `cordis.patch.yml`:
Every key above only answers fences. `disableAuth` is the one that changes who
may talk to the harness at all.
- **`false` (default).** The request-identity fence is rewritten away, and the
- **`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. This is the posture to use on a
port that faces anything you do not fully control.
- **`true`.** 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 — anyone who reaches the port controls the agent. Use it only
behind an authenticating proxy or on a network that is already trusted.
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
@@ -391,7 +381,8 @@ so a harness release that renames any of them will break it:
| 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 posture does not touch it at all.
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
@@ -417,9 +408,10 @@ 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 is now exercised by the default posture.** 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 the same sandbox, in order:
**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 |
|---|---|
@@ -431,8 +423,9 @@ harness mints the session cookie for. Measured on the same sandbox, in order:
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 the default
no longer needs to remove the check.
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
@@ -565,14 +558,14 @@ Three guards follow from that, and each has a test:
`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 only guards requests a browser itself marks as cross-site, so it has
no effect on normal navigation or on the WebSocket. Set `disableAuth: true`
and the fence is skipped entirely.
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)>`). The launch token is exchanged for the
rewritten authority, so the resulting 30-day cookie matches every request the
plugin intercepts — that is what makes the default posture work. 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
(`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
@@ -683,6 +676,7 @@ remote client and always looks like it passes.
| Version | What changed |
|---|---|
| **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. |
+9 -7
View File
@@ -4,15 +4,17 @@
# (package.json declares dsh.bundle.patch).
#
# The defaults below are the plugin's stated posture: the identity fence is
# disarmed, the browser is granted the host's own isLoopback flag, and the APIs
# a non-secure page is denied are restored. The browser-session check stays in
# place: the first visit is the one-time launch token printed in the process log
# (the Docker log from 0.2.1-alpha.4 on), exchanged for a 30-day cookie.
# 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: true # ALSO remove the session check; no token needed
# 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
@@ -25,7 +27,7 @@
name: 'dsh-trustworthy'
config:
enabled: true
disableAuth: false
disableAuth: true
forceClientLoopback: true
grantPairing: true
spoofSocketAddress: false
+22 -19
View File
@@ -11,13 +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. It removes only the
* origin fence by default: the harness's own browser-session check stays in
* place, so 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`), which is exchanged for a 30-day cookie bound to the
* authority it was minted under. Set `disableAuth: true` to remove that check
* as well.
* 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
@@ -56,10 +56,11 @@
* 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 keeps the session
* check, so the exposure is a page that carries a live cookie; `disableAuth:
* true` removes the check too, and then reachability is the only access
* control. Read the README before turning that on.
* 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
*/
@@ -128,15 +129,17 @@ export const Config = z.object({
),
disableAuth: z
.boolean()
.default(false)
.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. The default leaves the check in place: '
+ 'open the launch URL from the process log (in the Docker log on the '
+ 'container image) once, and the 30-day session cookie it exchanges '
+ 'covers this plugin\'s rewritten requests too. Set 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()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dsh-trustworthy",
"version": "0.4.0",
"version": "0.5.0",
"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",
+9
View File
@@ -544,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)
})