@karoc/dsh-proxy 0.1.3 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +495 -0
- package/CONTRIBUTING.md +40 -8
- package/README.md +142 -26
- package/README.zh.md +77 -16
- package/cordis.patch.yml +2 -1
- package/lib/client.js +297 -58
- package/lib/index.js +696 -77
- package/package.json +50 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,500 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.18] - 2026-11-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **A gate for what `npm publish` actually ships** (G30): the tarball's member set
|
|
8
|
+
must equal the documented file list, the `lib/*.js` inside it must be
|
|
9
|
+
byte-identical to a FRESH BUILD of the current source (so a stale bundle cannot
|
|
10
|
+
ship), and the packed host bundle is
|
|
11
|
+
loaded **from a profile-shaped install path**, where it must start its proxy,
|
|
12
|
+
serve `/proxy/api` and resolve + self-install the harness egress package
|
|
13
|
+
(through a stub, so the gate stays offline). Written while preparing the first
|
|
14
|
+
publish: every other spec exercises `src/*.ts` or the repo's own `lib/`, so a
|
|
15
|
+
`files` mistake or a stale bundle could only have surfaced in someone else's
|
|
16
|
+
install. Two negative controls keep it load-bearing (an extra shipped directory;
|
|
17
|
+
a stale in-tarball bundle).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **The proxy's own outbound HTTP is no longer captured by the launch
|
|
22
|
+
environment's proxy** (G31). Node ≥ 24.22 makes `http.request` honor
|
|
23
|
+
`NODE_USE_ENV_PROXY`/`HTTP_PROXY`; the deployment this plugin documents
|
|
24
|
+
(systemd `Environment=…` so dsh's own fetch rides the proxy) therefore routed
|
|
25
|
+
the proxy's plain-HTTP forwarding — and its HTTP upstream leg — back into
|
|
26
|
+
itself. Those requests now go through an explicit `Agent` with no `proxyEnv`,
|
|
27
|
+
restoring the documented guarantee that the proxy never proxies itself.
|
|
28
|
+
|
|
29
|
+
- **Absolute-form requests use the target authority as `Host`.** A client that
|
|
30
|
+
sent its own address (or anything else) in `Host` had it forwarded to the
|
|
31
|
+
origin; on Node ≥ 24.22 the resulting `Host`/authority disagreement threw
|
|
32
|
+
synchronously inside the request handler, which takes down the host process.
|
|
33
|
+
The header is now replaced per RFC 9112 §3.2.2, and a synchronous client
|
|
34
|
+
construction error answers `502` instead of escaping the handler.
|
|
35
|
+
|
|
36
|
+
The shipped `lib/*.js` changed accordingly; the bundles still do not embed the
|
|
37
|
+
version.
|
|
38
|
+
|
|
39
|
+
## [0.1.17] - 2026-10-06
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
|
|
43
|
+
- **`/proxy/api` now sits behind the same operator-session gate as the harness's
|
|
44
|
+
own `/api`** (401/403 without the browser cookie). The route hands out the
|
|
45
|
+
upstream address and credentials and can rewrite the routing, and on a shared
|
|
46
|
+
machine any local process can reach 127.0.0.1 — the app root was gated, this
|
|
47
|
+
route was not. G27 pins it; compositions without the connection service keep
|
|
48
|
+
the previous behaviour (there is no credential model to consult there) and the
|
|
49
|
+
README says so. CLI checks need the cookie, and the recipe is in the README.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A broken or half-written `proxy.json` silently disabled the upstream.** Every
|
|
54
|
+
parse failure fell back to the default config, which has the upstream switched
|
|
55
|
+
off and no hosts listed — so one accidentally broken edit (or a read landing
|
|
56
|
+
inside a non-atomic write) sent the traffic that was meant to be proxied
|
|
57
|
+
straight out. Reads now keep the last document that parsed (G28), and writes go
|
|
58
|
+
through a sibling temp file plus `rename`, so a concurrent read can never
|
|
59
|
+
observe a torn file.
|
|
60
|
+
|
|
61
|
+
- The settings page matched hosts by exact string, so an IPv6 host listed as
|
|
62
|
+
`[2001:db8::1]` (provider baseURL) and observed as `2001:db8::1` (CONNECT
|
|
63
|
+
authority) could read *direct* while traffic actually went upstream. Both halves
|
|
64
|
+
now share one matcher (`src/host-match.ts`), which also removes the chance of
|
|
65
|
+
the two copies drifting apart again.
|
|
66
|
+
|
|
67
|
+
- **The loopback-only listener is now pinned by a gate** (G29): the assertion
|
|
68
|
+
reads the socket address the OS actually bound and, when the machine has a
|
|
69
|
+
non-loopback interface, proves the proxy is unreachable there. Dropping the host
|
|
70
|
+
argument from `listen` would have exposed the proxy — and the upstream
|
|
71
|
+
credentials it injects — to the whole network without any test noticing.
|
|
72
|
+
|
|
73
|
+
- A failed LOAD of the settings data is no longer reported as "save failed": the
|
|
74
|
+
panel had one error slot, so an expired session (now that the route is gated)
|
|
75
|
+
pointed the reader at the Save button. A load failure has its own line, and a
|
|
76
|
+
401/403 says the session expired and to refresh.
|
|
77
|
+
|
|
78
|
+
### Docs
|
|
79
|
+
|
|
80
|
+
- README (both languages): the `/proxy/api` contract lists `op: 'forget'` and the
|
|
81
|
+
authentication requirement, with a cookie recipe for command-line checks. The
|
|
82
|
+
scenario counts are gone from README/CONTRIBUTING (they drifted twice in one
|
|
83
|
+
session); each spec prints its own count when it runs.
|
|
84
|
+
|
|
85
|
+
## [0.1.16] - 2026-10-06
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **The observed-host list can be cleared** (*Clear* next to the heading, or
|
|
90
|
+
`POST /proxy/api {"op":"forget"}`). Until now the list only ever grew — it
|
|
91
|
+
collects everything dsh has contacted, including hosts from experiments — and
|
|
92
|
+
hand-editing `proxy.json` did not stick: the running proxy merges its own
|
|
93
|
+
session set into that file on the next persist, so removed entries came back.
|
|
94
|
+
Clearing therefore drops the session set *and* the persisted list (G26).
|
|
95
|
+
|
|
96
|
+
- README: the two readings of the checkbox list now have a table and a worked
|
|
97
|
+
example each, in both languages.
|
|
98
|
+
|
|
99
|
+
## [0.1.15] - 2026-10-06
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **A SOCKS5 upstream was sent IPv6 targets as DOMAIN names.** The handshake chose
|
|
104
|
+
between `ATYP=1` (IPv4) and `ATYP=3` (domain) only, so `2001:db8::1` went out as a
|
|
105
|
+
hostname no resolver can answer. It now sends `ATYP=4` with the 16 address bytes:
|
|
106
|
+
`ipv6ToBytes()` expands the compressed form, the full eight-group form, an
|
|
107
|
+
embedded IPv4 tail (`::ffff:1.2.3.4`) and drops a zone id (which has no wire
|
|
108
|
+
representation). An address that cannot be encoded fails loudly instead of being
|
|
109
|
+
sent as a name. G25 pins it, with a negative control that re-injects the domain
|
|
110
|
+
branch.
|
|
111
|
+
|
|
112
|
+
### Notes
|
|
113
|
+
|
|
114
|
+
- The dsh-desktop shell carries a copy of this engine and had the same
|
|
115
|
+
empty-provider defect plus the older loopback rule (literal `127.0.0.1` only,
|
|
116
|
+
bare `::1` only). Both are fixed in that repository — its provider list now
|
|
117
|
+
reads the active profile's document, and its routing/observation/provider code
|
|
118
|
+
uses the same shared classifier and CONNECT-authority parsing as here. Changes
|
|
119
|
+
are committed there on a branch (commit `6bb8316`), per that repo's PR-only rule.
|
|
120
|
+
|
|
121
|
+
## [0.1.14] - 2026-10-06
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- **IPv6 loopback is loopback in every spelling it arrives in.** The guard only
|
|
126
|
+
matched the bare `::1`, while a URL parser hands over `[::1]`, Node rewrites an
|
|
127
|
+
IPv4-mapped literal to `::ffff:7f00:1`, and a CONNECT authority is
|
|
128
|
+
`[::1]:443` — so a bracketed loopback target could be sent to the upstream
|
|
129
|
+
proxy, against the guarantee that loopback is always direct. One shared
|
|
130
|
+
classifier now normalizes all of them (brackets, dotted and hex IPv4-mapped
|
|
131
|
+
forms) and is used by routing, the self-loop guard, the observed-host list and
|
|
132
|
+
the provider list.
|
|
133
|
+
|
|
134
|
+
- **CONNECT authorities are parsed, not split on `:`.** `[::1]:443` used to
|
|
135
|
+
become host `[`, which both defeated the loopback test and wrote a junk `[`
|
|
136
|
+
entry into the observed-host list a user could then tick. The upstream CONNECT
|
|
137
|
+
line keeps IPv6 addresses bracketed, and `proxiedHosts` matching is normalized
|
|
138
|
+
so a listed `[2001:db8::1]` also matches the `2001:db8::1` form a CONNECT
|
|
139
|
+
produces.
|
|
140
|
+
|
|
141
|
+
- **`llm-deepseek` no longer vanishes from the provider list.** Without a
|
|
142
|
+
configured `baseURL` the document carries no host — the harness resolves that
|
|
143
|
+
endpoint from a runtime constant — so the provider carrying real traffic was
|
|
144
|
+
absent. It now falls back to the public endpoint (`api.deepseek.com`); a
|
|
145
|
+
configured `baseURL` still wins.
|
|
146
|
+
|
|
147
|
+
### Added
|
|
148
|
+
|
|
149
|
+
- README: a deployment matrix (terminal/unit, dsh-desktop, launcher-declared
|
|
150
|
+
proxy, headless, unusual builds) stating what routes dsh's own requests and
|
|
151
|
+
what the page shows for each.
|
|
152
|
+
|
|
153
|
+
### Verified
|
|
154
|
+
|
|
155
|
+
- **npm-installed layout, for real**: `npm install @deepseek-ai/dsh@0.2.1-alpha.1`
|
|
156
|
+
in a scratch tree, then the plugin's own resolver anchored at the installed
|
|
157
|
+
entry (`node_modules/@deepseek-ai/dsh/lib/bin.js`) — it resolves
|
|
158
|
+
`installProxyFromEnvironment` from the installed `dsh-http-proxy`, and a real
|
|
159
|
+
in-process `fetch` issued a `CONNECT example.org:443` through the installed
|
|
160
|
+
policy. The earlier "[assumed]" note about this layout is settled.
|
|
161
|
+
- **dsh-desktop carries the same empty-provider defect** (its `proxy.mjs` reads
|
|
162
|
+
the removed `settings.yaml`). Fixed in that repository: a profile-document
|
|
163
|
+
reader (`profiles/<name>/cordis.patch.yml`), loopback filtering, and the same
|
|
164
|
+
DeepSeek public-endpoint rule; its `test-proxy` (12 scenarios, incl. a new
|
|
165
|
+
profile-document case) and `test-copy-consistency` gates pass.
|
|
166
|
+
|
|
167
|
+
## [0.1.13] - 2026-10-06
|
|
168
|
+
|
|
169
|
+
### Added
|
|
170
|
+
|
|
171
|
+
- **Routing rule: "only checked hosts" (default) or "everything except
|
|
172
|
+
checked".** The second reading exists for setups whose local proxy already
|
|
173
|
+
does its own filtering — there, ticking every host is busywork. The rule lives
|
|
174
|
+
in `proxy.json` as `routeMode` (`only-listed` | `except-listed`), flips the
|
|
175
|
+
meaning of the SAME checkbox list, and changes the list title ("hosts going
|
|
176
|
+
through the proxy" ↔ "hosts staying direct") so the screen never contradicts
|
|
177
|
+
the rule. An unreadable stored value falls back to the default — the safe
|
|
178
|
+
direction, never "send everything upstream" (G22).
|
|
179
|
+
|
|
180
|
+
### Changed
|
|
181
|
+
|
|
182
|
+
- The page copy was rewritten for plainness and length: the egress block is now
|
|
183
|
+
one short sentence per state instead of a paragraph of mechanism, protocol
|
|
184
|
+
hints and empty-state texts were trimmed, and four strings nothing rendered
|
|
185
|
+
were deleted. Two real i18n defects fell out of that pass: the per-host route
|
|
186
|
+
labels ("via upstream" / "direct") and the protocol labels were hard-coded
|
|
187
|
+
English, so Chinese users saw English in the middle of a Chinese page; both now
|
|
188
|
+
come from the dictionary.
|
|
189
|
+
- Pointing the upstream at this plugin's own port now says so on the page
|
|
190
|
+
(previously only the routing guard silently ignored it).
|
|
191
|
+
|
|
192
|
+
## [0.1.12] - 2026-10-06
|
|
193
|
+
|
|
194
|
+
### Changed
|
|
195
|
+
|
|
196
|
+
- **The listen-port field now says what it is not.** A user reading the panel
|
|
197
|
+
reasonably asked whether the upstream's port belongs there (their upstream is
|
|
198
|
+
on loopback, so the two ports look interchangeable). It is the address this
|
|
199
|
+
plugin LISTENS on, not a destination: the label now reads "plugin listen port
|
|
200
|
+
(optional — NOT the upstream address)", the hint points at the upstream card,
|
|
201
|
+
and typing the configured upstream's own loopback port produces a specific
|
|
202
|
+
warning instead of leaving the bind failure as the only feedback. The same
|
|
203
|
+
clarification is in the README's `proxy.json` section.
|
|
204
|
+
|
|
205
|
+
What actually happens if you do it anyway: the bind fails (the upstream owns
|
|
206
|
+
that port), the plugin falls back to an ephemeral port and reports it, and —
|
|
207
|
+
because the upstream card is off in that scenario — every host goes DIRECT, so
|
|
208
|
+
the local upstream is bypassed entirely.
|
|
209
|
+
|
|
210
|
+
## [0.1.11] - 2026-10-06
|
|
211
|
+
|
|
212
|
+
### Changed
|
|
213
|
+
|
|
214
|
+
- **The listen port is no longer presented as a setting.** It never was one:
|
|
215
|
+
`listenPort` defaults to `0`, which means the plugin asks the OS for a free
|
|
216
|
+
port, and a port that cannot be bound already falls back to an ephemeral one
|
|
217
|
+
and reports it. Only a deployment whose LAUNCHER must name the address in
|
|
218
|
+
advance needs a fixed value — and even then an unconfigured plugin adopts the
|
|
219
|
+
loopback port that launcher declares. The settings page now reflects that:
|
|
220
|
+
the primary line states the state ("in-process traffic rides this proxy"), the
|
|
221
|
+
port and the `Environment=` line live in a **collapsed diagnostics panel** that
|
|
222
|
+
opens by itself only when something needs attention (a launcher proxy owning
|
|
223
|
+
egress, a failed self-install, an unbindable port, a port awaiting a
|
|
224
|
+
restart), and the port field is labelled optional with the reason spelled out.
|
|
225
|
+
|
|
226
|
+
- A page served by a newer client bundle against an older host half (the window
|
|
227
|
+
before a restart) now says so instead of rendering `undefined` into the status
|
|
228
|
+
text.
|
|
229
|
+
|
|
230
|
+
## [0.1.10] - 2026-10-06
|
|
231
|
+
|
|
232
|
+
### Fixed
|
|
233
|
+
|
|
234
|
+
- **A launcher-declared port is adopted instead of fought.** If your service
|
|
235
|
+
environment says `HTTP_PROXY=http://127.0.0.1:45999` (the 0.1.7 contract) while
|
|
236
|
+
`listenPort` is unset, the plugin now LISTENS on 45999 instead of binding an
|
|
237
|
+
ephemeral port and leaving dsh's already-installed policy pointed at a dead
|
|
238
|
+
address. A port that something else already serves is the exception: binding
|
|
239
|
+
fails, the plugin falls back to an ephemeral port and reports it — which is
|
|
240
|
+
also the right outcome when the launch proxy is a *different* local proxy,
|
|
241
|
+
because that one keeps owning egress.
|
|
242
|
+
|
|
243
|
+
Non-loopback and malformed launch proxies are never adopted (G20).
|
|
244
|
+
|
|
245
|
+
## [0.1.9] - 2026-10-06
|
|
246
|
+
|
|
247
|
+
### Fixed
|
|
248
|
+
|
|
249
|
+
- **The plugin no longer needs the user to touch their service definition.**
|
|
250
|
+
0.1.7 shipped the honest but demanding answer: dsh resolves its egress policy
|
|
251
|
+
from the LAUNCH environment before plugins mount, so model traffic could only
|
|
252
|
+
be routed by editing the service unit. That is not the whole truth — the
|
|
253
|
+
harness's own policy installer is reachable from a plugin, and layering a
|
|
254
|
+
policy is a behaviour its suite covers. `src/egress.ts` now resolves
|
|
255
|
+
`@deepseek-ai/dsh-http-proxy` (anchored at the active profile, so the import
|
|
256
|
+
lands on the same module instance the harness uses) and installs a policy
|
|
257
|
+
pointing http/https at this plugin's listener **when, and only when, the launch
|
|
258
|
+
environment declared no proxy at all**. Model calls, web fetch and web search
|
|
259
|
+
then ride this proxy with per-host routing intact — zero configuration.
|
|
260
|
+
|
|
261
|
+
The condition is a correctness rule, not politeness: this plugin's proxy sends
|
|
262
|
+
un-ticked hosts DIRECT, so layering it over a launcher's proxy would silently
|
|
263
|
+
bypass that launcher's egress policy. A launcher that already decides egress
|
|
264
|
+
keeps deciding it, and the page says so. An unresolvable policy package is
|
|
265
|
+
reported as `unavailable` (the page then prints the environment-variable
|
|
266
|
+
fallback) rather than being papered over.
|
|
267
|
+
|
|
268
|
+
- **`listenPort` defaults to `0` (ephemeral) again.** With self-installed egress
|
|
269
|
+
a fixed port is no longer needed; it remains the opt-in for deployments where
|
|
270
|
+
the launcher declares the proxy itself (`HTTP_PROXY=http://127.0.0.1:<listenPort>`).
|
|
271
|
+
0.1.7's fixed default (45999) is withdrawn before publication — it would have
|
|
272
|
+
made two dsh instances on one machine warn about a port nobody needs.
|
|
273
|
+
|
|
274
|
+
- The settings page now distinguishes four egress states (self-installed /
|
|
275
|
+
launch environment / another launcher proxy owns egress / unavailable) instead
|
|
276
|
+
of assuming the launch environment is the only way.
|
|
277
|
+
|
|
278
|
+
### Added
|
|
279
|
+
|
|
280
|
+
- `scripts/egress.spec.mjs` (decision + resolution + failure reporting) wired
|
|
281
|
+
into `npm test` and `verify:all`; guarantees G18–G19 pin "never displace a
|
|
282
|
+
launcher proxy" and "a failed self-install is reported"; negative controls
|
|
283
|
+
re-inject both defects and must redden the suite.
|
|
284
|
+
|
|
285
|
+
## [0.1.8] - 2026-10-06
|
|
286
|
+
|
|
287
|
+
### Fixed
|
|
288
|
+
|
|
289
|
+
- **`scripts/verify-tooltip.mjs` could report "no tooltip bubble" for reasons
|
|
290
|
+
that have nothing to do with the tooltip.** Two of them: (1) the target row can
|
|
291
|
+
sit below the fold — the host list is a scrollable box inside a panel that grew
|
|
292
|
+
(the new egress block sits above it), so the synthetic mouse moved to a
|
|
293
|
+
coordinate nothing occupied; (2) the bubble wrapper only exists after React has
|
|
294
|
+
seen the injected narrowing (ResizeObserver → setState → re-render), so a hover
|
|
295
|
+
that lands too early finds nothing. The script now scrolls the target into
|
|
296
|
+
view and fails loudly when it is not on screen (with the measured `y`), and it
|
|
297
|
+
retries the hover a bounded three times; the assertion itself is unchanged, so
|
|
298
|
+
it can still only pass when the bubble really appears.
|
|
299
|
+
|
|
300
|
+
## [0.1.7] - 2026-10-06
|
|
301
|
+
|
|
302
|
+
### Fixed
|
|
303
|
+
|
|
304
|
+
- **Model requests did not go through the proxy at all — only child processes
|
|
305
|
+
did.** dsh installs its egress policy from the environment it was **launched**
|
|
306
|
+
with, before any plugin mounts (`apps/cli/src/profile-boot.ts` →
|
|
307
|
+
`@deepseek-ai/dsh-http-proxy`'s `installProxyFromEnvironment`, which owns
|
|
308
|
+
undici's global dispatcher and answers its own callers through
|
|
309
|
+
`proxyRouteFor()`). Setting `HTTP_PROXY`/`NODE_USE_ENV_PROXY` later, from a
|
|
310
|
+
plugin's `apply()`, reaches spawned children only: on a systemd-run `dsh web`
|
|
311
|
+
the harness's own `fetch` — model calls, web fetch, web search — stayed direct.
|
|
312
|
+
Measured on the affected machine in one process: a child `curl https://example.org`
|
|
313
|
+
and `npm` (registry.npmjs.org) appeared in the proxy's observed hosts, while an
|
|
314
|
+
in-process `web_fetch` (successful, HTTP 200) and dozens of model calls never
|
|
315
|
+
did. The dsh-desktop shell avoids this by setting the variables before spawning
|
|
316
|
+
`dsh` as a child; a directly-started `dsh web` has nobody to do that.
|
|
317
|
+
|
|
318
|
+
The proxy now listens on a **fixed port** (`listenPort` in `proxy.json`,
|
|
319
|
+
default `45999`) instead of an ephemeral one, so a launch environment can name
|
|
320
|
+
it: `Environment="HTTP_PROXY=http://127.0.0.1:45999" …` in the service unit,
|
|
321
|
+
then one restart, and dsh's own requests ride this proxy with per-host routing
|
|
322
|
+
intact (README → "Putting dsh's own requests behind the proxy"). An existing
|
|
323
|
+
`proxy.json` without the field adopts the default; an explicit `0` still means
|
|
324
|
+
"ephemeral".
|
|
325
|
+
|
|
326
|
+
- **The page never said which traffic it was actually handling.** Settings →
|
|
327
|
+
Smoothly Proxy now reports the bound port, the configured port, whether the
|
|
328
|
+
requested port was honored, and whether dsh's own requests are routed here —
|
|
329
|
+
with the exact `Environment=` line for the current port when they are not. The
|
|
330
|
+
old header comment's "a dsh restart is the documented remedy" was **wrong**: a
|
|
331
|
+
restart alone never fixed in-process routing, because the ordering is the same
|
|
332
|
+
on every boot.
|
|
333
|
+
|
|
334
|
+
- A requested fixed port that is taken now falls back to an ephemeral one **and
|
|
335
|
+
says so** (log + `portHonored: false`) instead of leaving a launcher pointed at
|
|
336
|
+
a port nothing listens on.
|
|
337
|
+
|
|
338
|
+
### Added
|
|
339
|
+
|
|
340
|
+
- Negative controls for both new rules: `portHonored` forced true, and the
|
|
341
|
+
launch-proxy matcher forced true, each redden `proxy-core.spec.mjs` /
|
|
342
|
+
`host-route.spec.mjs`. Guarantees G16–G17 pin them.
|
|
343
|
+
|
|
344
|
+
## [0.1.6] - 2026-10-06
|
|
345
|
+
|
|
346
|
+
### Fixed
|
|
347
|
+
|
|
348
|
+
- **`npm run accept` reported a false red on a healthy GUI.** The boot manifest
|
|
349
|
+
can hand out a ROOT-RELATIVE bundle URL with no leading slash
|
|
350
|
+
(`plugins/??@karoc/dsh-proxy/client.js&rev=…`); `scripts/accept-gui.mjs`
|
|
351
|
+
concatenated it onto the origin and asked for
|
|
352
|
+
`http://127.0.0.1:3080plugins/…`, so "plugin client bundle served" failed while
|
|
353
|
+
the GUI was rendering the plugin correctly. The URL is now resolved the way the
|
|
354
|
+
browser resolves it (`new URL(bundleUrl, ORIGIN)`), and the check prints the
|
|
355
|
+
path it actually fetched.
|
|
356
|
+
|
|
357
|
+
- **`scripts/verify-tooltip.mjs` skipped its own assertions.** Whether any host
|
|
358
|
+
label truncates depends on the content (longest provider/host label vs the
|
|
359
|
+
panel width), so on a page whose labels all fit the script failed its
|
|
360
|
+
precondition and silently skipped the hover checks — a red that explained
|
|
361
|
+
nothing and verified nothing. It now measures the natural layout first and, when
|
|
362
|
+
nothing truncates, forces a narrow label box so the behavior under test
|
|
363
|
+
(truncated label → bubble carrying the FULL text) runs on every invocation; the
|
|
364
|
+
detail line names which mode produced the evidence.
|
|
365
|
+
|
|
366
|
+
### Note
|
|
367
|
+
|
|
368
|
+
- Acceptance tooling only: `scripts/` is outside the published `files` list, so
|
|
369
|
+
no runtime behavior changed. Both fixes make the acceptance entry points
|
|
370
|
+
truthful on dsh 0.2.
|
|
371
|
+
|
|
372
|
+
## [0.1.5] - 2026-10-06
|
|
373
|
+
|
|
374
|
+
### Fixed
|
|
375
|
+
|
|
376
|
+
- **The model-provider list went empty (`0`) on dsh ≥ 0.2.** The host half
|
|
377
|
+
derived provider hosts by parsing `<DSH_HOME>/settings.yaml`. dsh 0.2 removed
|
|
378
|
+
that document: at boot `SettingsForms.importLegacyDocument()` renames it to
|
|
379
|
+
`settings.yaml.imported` and imports every section into the **active profile's**
|
|
380
|
+
config document (`~/.dsh/profiles/<name>/cordis.patch.yml`, a top-level YAML
|
|
381
|
+
array of `{id, name, config}` entries; upstream commit `601d6761e4`,
|
|
382
|
+
`feat(settings): project volatile Config through profile-backed forms`). The
|
|
383
|
+
reader therefore found no file, returned `[]`, and Settings → Smoothly Proxy
|
|
384
|
+
rendered an empty list — while the built-in model pages kept showing the same
|
|
385
|
+
providers normally.
|
|
386
|
+
|
|
387
|
+
Evidence on the affected machine: `GET /proxy/api` answered `providers: []`,
|
|
388
|
+
`~/.dsh/settings.yaml` was gone (only `settings.yaml.imported` remained), and
|
|
389
|
+
the GUI's own settings read (`POST /api/settings/describe`) answered
|
|
390
|
+
`llm-pi-ai.value.providers.<key>.{displayName, baseURL}` for all seven
|
|
391
|
+
configured routes.
|
|
392
|
+
|
|
393
|
+
The host half now reads the **live settings document**
|
|
394
|
+
(`ctx.get('settings').describe()` — the same call dsh's own settings pages
|
|
395
|
+
read) and keeps the legacy `<DSH_HOME>/settings.yaml` only as the fallback for
|
|
396
|
+
a composition without a settings service (dsh < 0.2, or a read that throws). A
|
|
397
|
+
readable live document always wins, so a leftover legacy file can never
|
|
398
|
+
resurrect stale hosts. The provider list is read per request, so adding or
|
|
399
|
+
renaming a provider route reaches the page on the next load — no dsh restart.
|
|
400
|
+
|
|
401
|
+
- **Loopback provider hosts were offered as proxy targets.** A provider whose
|
|
402
|
+
`baseURL` is loopback (`127.0.0.1`, `localhost`, `::1`, `0.0.0.0`) appeared in
|
|
403
|
+
the model-provider list with a checkbox that could never take effect — routing
|
|
404
|
+
sends loopback direct no matter what. Such hosts are now dropped from the list
|
|
405
|
+
on both the live and the legacy path, matching the observed-host list, and the
|
|
406
|
+
page copy now says the list is read live from your dsh settings (the old text
|
|
407
|
+
named `settings.yaml`, a file dsh 0.2 no longer has).
|
|
408
|
+
|
|
409
|
+
### Added
|
|
410
|
+
|
|
411
|
+
- Negative controls for the new behavior, re-proved on every `npm test`: the
|
|
412
|
+
reported defect re-injected (host half stops reading the live document) must
|
|
413
|
+
redden `host-route.spec.mjs`; a resolver that prefers the legacy file, and a
|
|
414
|
+
provider list that offers loopback hosts again, must redden
|
|
415
|
+
`proxy-core.spec.mjs`. Guarantees G13–G15 (`docs/guarantees.md`) pin the
|
|
416
|
+
loopback rule, the live-over-legacy order, and the "a settings read that
|
|
417
|
+
throws still answers 200" fallback.
|
|
418
|
+
|
|
419
|
+
## [0.1.4] - 2026-09-25
|
|
420
|
+
|
|
421
|
+
### Fixed
|
|
422
|
+
|
|
423
|
+
- **DSH 0.1.7 icon rename (upstream commit `4937343a5e`, released from
|
|
424
|
+
`0.1.7-alpha.1`).** `IconChevronDownOutline14`, `IconGlobeOutline14` and
|
|
425
|
+
`IconLinkOutline14` no longer exist in `dsh-client-ui-primitives` — the size
|
|
426
|
+
suffix moved into each artwork's default and the name now carries the stroke
|
|
427
|
+
weight — so all three resolved to `undefined` and the Settings → Smoothly
|
|
428
|
+
Proxy section crashed on render (`React error #130`,
|
|
429
|
+
`slot entry crashed in 'settings.section'`). `src/client/ProxySection.tsx`
|
|
430
|
+
now imports the `*Regular` variants the built-in pages use. Rendered sizes: the chevron and the globe
|
|
431
|
+
stay 14 px (their artwork defaults); only the link moves from 14 px to its
|
|
432
|
+
16 px artwork default. **Support floor:** the client half requires dsh ≥ 0.1.7
|
|
433
|
+
**since v0.1.4** — **v0.1.3 remains the release for 0.1.2–0.1.6**.
|
|
434
|
+
|
|
435
|
+
- **Loopback was only recognised as the literal `127.0.0.1`.** The routing guard
|
|
436
|
+
was `/^(127\.0\.0\.1|localhost|::1|0\.0\.0\.0)$/`, so every other address in the
|
|
437
|
+
`127.0.0.0/8` block (e.g. `127.0.0.2`, used by interface aliases and some
|
|
438
|
+
container setups) was sent to the **upstream proxy** when it was listed in
|
|
439
|
+
`proxiedHosts` — contradicting the documented "loopback targets are always
|
|
440
|
+
direct" guarantee. The guard now matches the whole `127/8` block. The spec
|
|
441
|
+
assertion that was supposed to pin this was **vacuous** (it asserted the
|
|
442
|
+
loopback hosts against a `proxiedHosts` list that did not contain them, so it
|
|
443
|
+
returned "direct" for the unlisted-host reason and would have stayed green with
|
|
444
|
+
the guard deleted) — it now lists them, adds a positive control, and covers
|
|
445
|
+
`127.0.0.2`; verified red against the old guard before the fix.
|
|
446
|
+
- Dropped the dead `@deepseek-ai/dsh-client-runtime/client` entry from the
|
|
447
|
+
bundler externals and the client-boot spec: that package no longer ships with
|
|
448
|
+
DSH and nothing imports it (same stale reference as the `dsh.client.inject`
|
|
449
|
+
row removed above).
|
|
450
|
+
|
|
451
|
+
### Tests
|
|
452
|
+
|
|
453
|
+
- **`npm run verify:all` — every gate, one command, bound to the commit.**
|
|
454
|
+
It runs script syntax, the spec suite, the guarantee gate, the negative
|
|
455
|
+
controls and the release gate, and writes `{ts, commit, node, steps[]}` to the
|
|
456
|
+
gitignored `lib/verify-report.json`. `npm run verify:fresh` FAILS when HEAD no
|
|
457
|
+
longer matches the recorded commit — because a green claim is only as good as
|
|
458
|
+
the commit it was measured on, and this repo deliberately re-points tags for
|
|
459
|
+
unpublished versions. A failed step is a failed verification, never an
|
|
460
|
+
"unknown".
|
|
461
|
+
- **Negative controls, now part of `npm test`** (`npm run test:controls`).
|
|
462
|
+
A gate nobody has seen fail is not evidence, so `scripts/test-negative-controls.mjs`
|
|
463
|
+
clones the committed tree per scenario, injects ONE defect, and asserts the
|
|
464
|
+
gate fails with the documented message: a dirty tree, a removed CHANGELOG
|
|
465
|
+
entry, a deleted release tag, a removed build artifact, and a guarantee row
|
|
466
|
+
whose pinning assertion no longer exists — plus a positive control (an
|
|
467
|
+
unmutated clone must pass both gates). 5/5 mutations caught.
|
|
468
|
+
- **Guarantee gate, now part of `npm test`.** `docs/guarantees.md` lists this
|
|
469
|
+
plugin's negative guarantees and names the assertion label that pins each one;
|
|
470
|
+
`scripts/check-guarantees.mjs` fails the suite when a label disappears (12 rows
|
|
471
|
+
today, and the selector must exist in `scripts/*.spec.mjs`). Verified by
|
|
472
|
+
negative control: pointing a row at a non-existent label makes the gate FAIL.
|
|
473
|
+
The routing guard that shipped matching only the literal `127.0.0.1` stayed
|
|
474
|
+
invisible for months precisely because the assertion meant to pin it could not
|
|
475
|
+
fail — the gate plus the `127/8` assertion close that class of defect.
|
|
476
|
+
|
|
477
|
+
### Changed
|
|
478
|
+
|
|
479
|
+
- **The dsh floor is now declared, not just documented.** `package.json`
|
|
480
|
+
declares an optional peer dependency
|
|
481
|
+
`@deepseek-ai/dsh-client-ui-settings: ">=0.1.7-rc.1"`. The gate that reads it ships from **DSH 0.1.7-rc.1** on — it
|
|
482
|
+
compares every `@deepseek-ai/dsh*` peer against the running runtime and refuses
|
|
483
|
+
a plugin the runtime fails, printing the `dsh plugin allow-version` remedy.
|
|
484
|
+
Runtimes older than that gate evaluate **no** peers
|
|
485
|
+
and refuse nothing: on 0.1.2–0.1.6 the client half cannot render (the
|
|
486
|
+
`*Regular` icon names arrived in 0.1.7-alpha.1), so **v0.1.3 remains the
|
|
487
|
+
release for 0.1.2–0.1.6** — the 0.1.7 alphas have those icons and work. It is
|
|
488
|
+
marked `peerDependenciesMeta.optional` because the host supplies that package
|
|
489
|
+
at runtime, so npm installs nothing extra. Note the prerelease rule the range encodes (measured with the semver DSH
|
|
490
|
+
actually resolves — 7.8.5): `>=0.1.7` and `^0.1.7` do **not** match a
|
|
491
|
+
`0.1.7-rc.N` runtime, hence the explicit `-rc.1` floor. semver 7.7.4 answers
|
|
492
|
+
`true` for the caret form.
|
|
493
|
+
- Removed the stale `@deepseek-ai/dsh-client-runtime` entry from
|
|
494
|
+
`dsh.client.inject`: that package no longer ships with DSH (11 versions exist on npm, but `latest`
|
|
495
|
+
is still the historical `0.0.1-rc.1` and nothing in the harness tree provides
|
|
496
|
+
it), and the loader silently ignored the unresolvable row.
|
|
497
|
+
|
|
3
498
|
## [0.1.3]
|
|
4
499
|
|
|
5
500
|
- **Brand name**: Smoothly Proxy / 思磨力代理插件 — the plugin's user-visible
|
package/CONTRIBUTING.md
CHANGED
|
@@ -8,7 +8,9 @@ model-provider proxying.
|
|
|
8
8
|
```sh
|
|
9
9
|
pnpm install # dev deps (tsdown, typescript, react types)
|
|
10
10
|
pnpm typecheck # tsc --noEmit
|
|
11
|
-
pnpm test #
|
|
11
|
+
pnpm test # node --test: proxy-core + host-route / client-boot / cordis-mount
|
|
12
|
+
# (each spec prints its own scenario count)
|
|
13
|
+
# client-boot / cordis-mount specs (no external network)
|
|
12
14
|
pnpm bundle # emit lib/index.js + lib/client.js
|
|
13
15
|
```
|
|
14
16
|
|
|
@@ -24,7 +26,7 @@ To see the plugin live in the GUI, link it into a profile:
|
|
|
24
26
|
```jsonc
|
|
25
27
|
// ~/.dsh/profiles/web/package.json
|
|
26
28
|
"dependencies": { "@karoc/dsh-proxy": "link:/path/to/dsh-proxy", ... },
|
|
27
|
-
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", ..., "dsh-proxy"] } }
|
|
29
|
+
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", ..., "@karoc/dsh-proxy"] } }
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
Then `cd ~/.dsh/profiles/web && pnpm install` and **restart `dsh web`**
|
|
@@ -69,10 +71,18 @@ Release steps:
|
|
|
69
71
|
# 1. write code + update README.md/README.zh.md + CHANGELOG.md together
|
|
70
72
|
# 2. bump version in package.json
|
|
71
73
|
git add -A && git commit -m "dsh-proxy v<version>"
|
|
72
|
-
git tag v<version>
|
|
74
|
+
git tag v<version>
|
|
73
75
|
pnpm release:check
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
**Keep the tag local until the upload lands.** An unpublished version is allowed
|
|
79
|
+
to move: if the release still needs commits (a gate fix, a re-run of
|
|
80
|
+
verification), re-point the tag with `git tag -f v<version>` at the new HEAD and
|
|
81
|
+
re-run `pnpm verify:all` — the report and the tag must name the same commit.
|
|
82
|
+
Push the tag only after the registry confirms the version
|
|
83
|
+
(`git push origin v<version>`). Pushing it earlier freezes it: a later fix then
|
|
84
|
+
needs a force-push, and the remote advertises a release that is not on npm.
|
|
85
|
+
|
|
76
86
|
### Publishing is a manual step (2FA)
|
|
77
87
|
|
|
78
88
|
The npm account has two-factor authentication enabled, so **the agent cannot
|
|
@@ -82,13 +92,16 @@ passing, `npm pack --dry-run` content confirmed), then the human runs:
|
|
|
82
92
|
|
|
83
93
|
```sh
|
|
84
94
|
npm login # 2FA
|
|
85
|
-
npm publish #
|
|
95
|
+
npm run publish:release # gate → build → pack → verify the tarball → publish → advisory verify
|
|
86
96
|
```
|
|
87
97
|
|
|
88
|
-
`npm publish
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
`npm publish` is still gated (prepack/prepublishOnly), but it rebuilds after the
|
|
99
|
+
gate and forwards a failing `postpublish` exit code — `publish:release` is the
|
|
100
|
+
path that publishes the bytes it verified. `npm publish --ignore-scripts`
|
|
101
|
+
bypasses all gates and post-publish verification — treat it as a process
|
|
102
|
+
violation. After a successful publish, the post-publish check polls the registry
|
|
103
|
+
until the version is visible and verifies `dist-tags.latest` plus the tarball
|
|
104
|
+
contents; then push the tag (`git push origin v<version>`).
|
|
92
105
|
|
|
93
106
|
### Known release-check "failures" that are actually fine
|
|
94
107
|
|
|
@@ -103,3 +116,22 @@ plus the tarball contents.
|
|
|
103
116
|
## License
|
|
104
117
|
|
|
105
118
|
MIT
|
|
119
|
+
|
|
120
|
+
## Publish verification contract (2026-11-01)
|
|
121
|
+
|
|
122
|
+
`npm publish` forwards lifecycle-hook exit codes: a nonzero `postpublish` makes a
|
|
123
|
+
**landed** upload look failed. Therefore:
|
|
124
|
+
|
|
125
|
+
- `postpublish` runs `node scripts/post-publish-check.mjs --advisory`: it reports
|
|
126
|
+
the verdict (`CONFIRMED` / `PENDING_PROPAGATION` / `UNVERIFIED` /
|
|
127
|
+
`CONTENT_MISMATCH` / `NOT_PUBLISHED`) but always exits 0.
|
|
128
|
+
- `npm run verify:publish` is the strict gate (`--strict`; PENDING exits 3).
|
|
129
|
+
- Preferred release path: `npm run publish:release` — gates, builds once, packs,
|
|
130
|
+
verifies the packed bytes (sha512 + `files` whitelist → `.verify/pack.json`),
|
|
131
|
+
publishes the tarball (npm runs no lifecycle scripts for a tarball argument),
|
|
132
|
+
then verifies advisory.
|
|
133
|
+
- If verification reports `PENDING_PROPAGATION` after a publish: do **not**
|
|
134
|
+
re-publish. Wait 10–30 minutes and re-run `npm run verify:publish` (a retry
|
|
135
|
+
that returns 409 means the earlier upload landed — that is success).
|
|
136
|
+
- Env seams: `DSH_POSTPUBLISH_{ATTEMPTS,INTERVAL_MS,TARBALL_ATTEMPTS,TARBALL_INTERVAL_MS,TARBALL_TIMEOUT_MS,BUDGET_MS}`,
|
|
137
|
+
`DSH_POSTPUBLISH_ALLOW_CROSS_ORIGIN_TARBALL=1` for CDN-fronted mirrors.
|