Skip to content

Commit b023692

Browse files
committed
docs + release: the readme documents the distribution, and the pack is 0.1.0
The README grows a Distribution section - what a release archive is, the different jobs of `START-HERE.bat` / `install.bat` / `vn-harness.exe`, and how a cut is made; `SECURITY.md` gains "Secrets never enter this repository", and its "what this pack does not do" list now says what is true after `keystate.rs` (a presence check of `$DSH_HOME/.credentials.yaml`, a boolean and a layer name); `app/README.md` documents the splash's two lines and the credentials document's real shape. Also repairs what the prose had wrong: - the double-encoded characters a PowerShell round trip left in the new README and DISTRIBUTE sections (`—` for an em dash, `…` for an ellipsis, `→` for an arrow) - the same damage an earlier commit undid elsewhere in the tree; - the test count: `cargo test` is 56/56 with `keystate.rs` at 18, not 55/55/17; - the CI matrix in DISTRIBUTE.md still named the retired `macos-13`/`macos-14` runners, and the archive count disagreed with itself in three files. There are six legs: four required (win-x64, mac-x64, mac-arm64, linux-x64) and two non-blocking ARM64 ones; - `package.json` is now `0.1.0`, the version the first release tag names, since the release job refuses to attach anything to a tag that disagrees with it. The screenshots are renamed to the timestamped spelling the README links and re-captured, and the shell's own README keeps the readme's image references and the asset rename in the same commit so no revision links a file that is not there.
1 parent 9b57920 commit b023692

9 files changed

Lines changed: 232 additions & 25 deletions

‎README.md‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Windows half is PowerShell, the macOS/Linux half is plain POSIX shell.
1515
![print](assets/vn-harness-20260920-164101.png)
1616

1717
<p align="center">
18-
<img src="assets/vn-harness 26_09_2026 09_53_53.png" alt="The vn-harness desktop window while the pinned harness starts: a dark splash showing the mark, the name and a &quot;Starting the harness…&quot; line" width="49%">
18+
<img src="assets/vn-harness-20260926-095353.png" alt="The vn-harness desktop window while the pinned harness starts: a dark splash showing the mark, the name and a &quot;Starting the harness…&quot; line" width="49%">
1919
<img src="assets/vn-harness-20260925-084844.png" alt="The same window once the harness is up, showing the pack's app in the light theme" width="49%">
2020
<br>
2121
<em><code>run-desktop.bat</code>: the shell's splash while <code>npx</code> works, and the same window once the harness is listening.</em>
@@ -165,6 +165,64 @@ To remove the pack, run **`uninstall.bat`** (Windows) or **`./uninstall.sh`**
165165
(macOS/Linux); both take the same `-Plugin` / `-DshHome` / `-ProfileName`
166166
switches. Removing a bundle also removes its patch layer.
167167

168+
## Distribution
169+
170+
A release archive **is** the app: there is no installer to run and nothing to
171+
compile. The only prerequisite is Node.js 22 or newer — the pinned harness
172+
itself is fetched by `npx` on the first run.
173+
174+
**On a machine that has never had the pack**
175+
176+
1. Download the archive for the platform from
177+
[Releases](https://github.com/vecnode/vn-harness/releases) —
178+
`vn-harness-<version>-win-x64.zip`, `…-mac-x64.zip`, `…-mac-arm64.zip`,
179+
`…-linux-x64.zip` (the ARM64 archives, `…-win-arm64.zip` and
180+
`…-linux-arm64.zip`, appear too once those two experimental legs are green) —
181+
and **extract it somewhere permanent**. The folder is the application: the
182+
profile installs every bundle as a live link into `packages/`, so keep
183+
`vn-harness.exe` beside everything it arrived with.
184+
2. Install Node.js 22+ from [nodejs.org](https://nodejs.org) if it is not already
185+
on `PATH`. Windows additionally needs the WebView2 runtime, which Windows 10
186+
and 11 already have.
187+
3. **Windows:** double-click `START-HERE.bat`. **macOS/Linux:** `./START-HERE.sh`.
188+
That one step installs every bundle into the harness web profile
189+
(`~/.dsh/profiles/web`) and the bundled skills into `~/.dsh/skills`, then
190+
opens the app in its native window. It is safe to run again.
191+
4. Every run after that is just the app: `vn-harness.exe` (`./vn-harness`), or
192+
`run-desktop.bat` for the same native window from a console, or `run-web.bat`
193+
for a browser tab instead. All of them take `-Help`.
194+
5. Optional but recommended: check the download against `SHA256SUMS.txt`;
195+
`BUILD-INFO.json` names the pack version, the harness pin, the commit and the
196+
toolchain it was built with.
197+
198+
Installing a newer release over an older one is a non-event, and the startup
199+
window says so before the harness is even listening: it names **which harness home
200+
this run will use** (`~/.dsh`) and whether a **DeepSeek key** was found there —
201+
green when it was, naming the layer it came from, amber with a pointer to
202+
*Settings → Models* when it was not. Sessions, settings and the key all live in
203+
that home rather than in the folder that was replaced, so a new download keeps
204+
everything you had. The key's value itself is never read out, logged or shown —
205+
only which layer supplied it.
206+
207+
Three files, three different jobs — this is the part that is easy to get wrong:
208+
209+
- **`START-HERE.bat` installs *and* runs.** It is the file to click first on a
210+
new machine.
211+
- **`install.bat` only installs.** It never opens the app; it is what you re-run
212+
after the folder moves.
213+
- **`vn-harness.exe` only runs.** On a machine that never installed the pack it
214+
opens the plain harness with none of the plugins in it.
215+
216+
**Cutting a release.** The tag must name the version in `package.json`, or the
217+
release job refuses to attach anything to it: this cut is `v0.1.0` against
218+
`"version": "0.1.0"`. Publishing a GitHub Release for that tag builds all six
219+
matrix targets — the four required legs (win-x64, mac-x64, mac-arm64, linux-x64)
220+
and the two non-blocking ARM64 ones — and attaches their archives plus
221+
`SHA256SUMS.txt`. The same thing can be run by hand from *Actions → distribute →
222+
Run workflow* with **release** ticked. What a distribution contains, what it
223+
deliberately leaves out and how it is verified end to end is in
224+
[`docs/DISTRIBUTE.md`](docs/DISTRIBUTE.md).
225+
168226
## Security & license
169227

170228
- MIT — see [LICENSE](LICENSE). Plugins are authored by **vecnode**.

‎SECURITY.md‎

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ is in the state you think it is.
2929

3030
## The short version
3131

32-
- **No secrets in this repository.** API keys live in your own harness settings
33-
(`Settings → Models`); no plugin or installer reads, writes, prompts for or
34-
transmits them. The pack writes no token, password or credential anywhere.
32+
- **No secrets in this repository — and that is enforced, not promised.** API keys
33+
live in your own harness settings (`Settings → Models`) or in
34+
`$DSH_HOME/.credentials.yaml`, which is outside this tree; no plugin or
35+
installer reads, writes, prompts for or transmits them. The pack writes no
36+
token, password or credential anywhere, and
37+
`scripts/checks/check-no-secrets.mjs` fails the build if a credential-shaped
38+
string reaches anything `git add -A` would stage. See
39+
[Secrets never enter this repository](#secrets-never-enter-this-repository).
3540
- **No core patching.** Every plugin is a standard dsh **bundle**
3641
(`dsh.bundle` + `cordis.patch.yml` + a `dsh.client` browser half). The pack owns
3742
its right bar by **forking** the shipped bar bundles into this repo and
@@ -397,10 +402,62 @@ cookie leaks, rotate the signing secret.
397402
option-injection guard on git, the screenshot write refusals, the diagram
398403
budget, and the terminal's refusal of an unauthenticated upgrade.
399404

405+
## Secrets never enter this repository
406+
407+
A secret in a commit is a secret on GitHub for as long as the repository exists —
408+
in every fork, every clone and every cache — even after the file is deleted. So
409+
this is a rule with a check behind it rather than a good intention.
410+
411+
**Where credentials actually live.** The harness keeps them in
412+
`$DSH_HOME/.credentials.yaml`, and `$DSH_HOME` defaults to `~/.dsh`, which is
413+
**outside this tree** and never part of a distribution. The pack reads that file
414+
in exactly one place, and only to answer "is a key present": `keystate.rs` reports
415+
a boolean and the name of the layer that supplied it, never the value — pinned by
416+
`a_real_shaped_key_never_reaches_the_page`, which feeds a key-shaped value through
417+
and asserts it appears in neither the startup window's payload nor the console
418+
line. The launch token is held to the same rule (see
419+
[The launch token](#the-launch-token)).
420+
421+
**The check.** `scripts/checks/check-no-secrets.mjs` runs in CI on every push and
422+
scans `git ls-files -co --exclude-standard` — which is exactly what `git add -A`
423+
would stage, so an unignored credentials file is caught *before* it is committed.
424+
It fails on:
425+
426+
- a key-shaped string, a credential ref with a value attached, a generic
427+
`apiKey`/`password`/`client_secret` assignment, a launch token pasted into a URL,
428+
a GitHub/AWS/Slack/Stripe key, a private-key block or a bearer JWT;
429+
- a missing credential rule in `.gitignore` (`.env`, `.credentials.yaml`,
430+
`*.pem`, `*.key`, `id_rsa*`, `.npmrc`, …);
431+
- **its own silence** — a self-test asserts the scanner fires on a synthetic key
432+
and stays quiet on clean text, because a guard that has never been shown to fire
433+
is decoration.
434+
435+
It never prints what it found: a failure names the file, the line, the rule and a
436+
masked preview, because a scanner that echoes the secret into a CI log has moved
437+
the leak rather than closed it. Its allowlist is empty by design — if a fixture
438+
trips a rule, the fix is to make the fixture not look like a credential (the test
439+
values in `keystate.rs` carry a `/` for exactly that reason), not to loosen the
440+
rule for everyone.
441+
442+
**Two things the check cannot do.** It skips binary files, so **committed
443+
screenshots are reviewed by hand** — they are whole-screen captures and can show a
444+
session, a path, a notification or a key. And it sees the working tree, not the
445+
past.
446+
447+
**If a credential is ever committed.** Rotate it **first** — assume it is public
448+
the moment it is pushed, because it is. Then remove it from history
449+
(`git filter-repo` or the BFG) and force-push; deleting the file in a new commit
450+
does not remove it. GitHub's own secret scanning and push protection are worth
451+
enabling on this repository as a second net behind this check.
452+
400453
## What this pack does not do
401454

402-
- No API keys, no credentials store access beyond the harness's own
403-
browser-session record, no prompts for secrets.
455+
- No API keys are read, and the **only** credential file the pack itself touches
456+
is `$DSH_HOME/.credentials.yaml` — through the desktop shell's `keystate.rs`,
457+
and for PRESENCE only: a boolean and the name of the layer that supplied the
458+
key, never its value (see
459+
[Secrets never enter this repository](#secrets-never-enter-this-repository)).
460+
No prompts for secrets.
404461
- **No way to weaken the harness's authentication.** The pack mounts every route
405462
it owns inside the harness's gated `/api` channel, and this repository has no
406463
flag, setting or environment variable that turns the session check off. The

‎app/README.md‎

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ Node.js 22 or newer exactly as the browser launcher does.
4343
remembers its own geometry* below), because it has to be known **before** the
4444
window is built.
4545
4. Opens the window **immediately**, on its own splash (`ui/index.html`), because
46-
the first run of a dsh version spends a while inside `npx`. After 45 seconds
47-
the splash says the wait is long and points at the console.
46+
the first run of a dsh version spends a while inside `npx`. The splash says
47+
which harness home this run will use and whether a DeepSeek key was found
48+
there (see *The splash answers the two questions worth asking first* below).
49+
After 45 seconds it says the wait is long and points at the console.
4850
5. Runs the pinned CLI, streaming its stdout and stderr to this console.
4951
6. Watches that output for `dsh web: http://127.0.0.1:<port>/?token=<token>`,
5052
parses the URL and navigates the window there.
@@ -112,6 +114,62 @@ beside the real `.dsh`. Unit tests in `src-tauri/src/main.rs` pin the rule
112114
(`chosen_home`, `default_dsh_home`, `pick_home_variable`), and the console still
113115
*reports* the resolved home - `~/.dsh` included - without exporting it.
114116

117+
## The splash answers the two questions worth asking first
118+
119+
The window opens before the server exists, on `ui/index.html`, and the two things a
120+
person actually wonders while it starts are *"will the harness find my key"* and
121+
*"is this the profile I have been using"*. So the splash says both:
122+
123+
- **DeepSeek key loaded - from the credentials file** (green), or **No DeepSeek
124+
key yet - add one in Settings → Models** (amber);
125+
- **Harness home: `C:\Users\you\.dsh`**, with the reminder that sessions, settings
126+
and the key live *there* and not in the folder that was replaced. That is what
127+
makes installing a newer distribution a non-event: the plugins come from the new
128+
folder (the profile live-links them), and everything you accumulated stays put.
129+
130+
`keystate.rs` decides, and it walks **the harness's own four layers**, highest
131+
first - precisely because `dsh-credentials-local` owns the real lookup and a splash
132+
that disagreed with it would be worse than no splash at all:
133+
134+
```text
135+
inherited process environment DEEPSEEK_API_KEY=… dsh (wins)
136+
> $DSH_HOME/.credentials.yaml what Settings > Models writes
137+
> <cwd>/.env the launcher's directory
138+
> $DSH_HOME/.env
139+
```
140+
141+
The document's real shape matters and is easy to get wrong: the ref is nested one
142+
level inside `refs:`, **not** at the margin -
143+
144+
```yaml
145+
version: 1
146+
refs:
147+
DEEPSEEK_API_KEY: sk-… <- here
148+
records:
149+
client-connection/browser-session:
150+
payload:
151+
secret: …
152+
```
153+
154+
- and the first cut of this module missed it for exactly that reason: it looked at
155+
column 0, its own tests agreed with it, and the built shell then reported "no key"
156+
on a machine whose key was present. `the_real_document_shape_is_read` pins the real
157+
layout now, and `a_ref_inside_a_record_is_not_a_credential` pins the half that must
158+
*not* count.
159+
160+
**The value never leaves the module.** The splash is handed a boolean and the NAME
161+
of the layer (`the credentials file`), never the key - same rule as the launch
162+
token. `the_splash_script_never_carries_the_secret` feeds a real-looking key in and
163+
asserts it does not appear in the injected script or in the console line, and the
164+
console line is the other place people paste from.
165+
166+
The page still has **no IPC channel and no command**: Tauri's
167+
`initialization_script` injects the payload after the global object exists and
168+
before the document is parsed, which is why the page needs neither. The script
169+
guards on the pathname, because it runs on *every* top-level navigation and this
170+
window is later navigated to the harness URL - the harness page must not inherit
171+
the global.
172+
115173
## The two rules that are load-bearing
116174

117175
The ready line carries the **launch token**, a live credential for the running
@@ -136,10 +194,11 @@ browser, so a rendered document can never replace the app's only window.
136194
|---|---|
137195
| `src-tauri/src/main.rs` | The supervisor: flags, the repository/pin lookup, the port and harness-home choices, spawning npx, the window, the geometry write-back, the exit hook |
138196
| `src-tauri/src/readyline.rs` | The pure half - ANSI stripping, URL extraction, the loopback refusal, `redact` - and its tests |
197+
| `src-tauri/src/keystate.rs` | Whether the harness will find a DeepSeek key, and which of the four layers supplied it (`keystate` docs the layering, which is `dsh-credentials-local`'s own). Parses the credentials document and `.env` files; emits the read-only payload the splash reads. **The key's value never leaves this module** - one test feeds a key in and asserts it does not come out |
139198
| `src-tauri/src/windowstate.rs` | The window's own memory - the record's shape, the five rules that decide whether a record can be trusted, atomic save - and its tests. Refers to no `tauri` type |
140199
| `src-tauri/Cargo.toml` | Two dependencies: `tauri`, and `serde_json` (already in the tree behind tauri) |
141200
| `src-tauri/tauri.conf.json` | Identifier, the `ui/` folder as `frontendDist`, no declared window (it is built in Rust so the navigation filter can live with it), `bundle.active: false` |
142-
| `ui/index.html` | The splash. One file, no request of any kind |
201+
| `ui/index.html` | The splash. One file, no request of any kind. The key line and the harness home are **injected** before the document parses (`initialization_script`), not fetched - the page still has no IPC channel and no command; `scripts/checks/check-splash.mjs` renders it in both states |
143202
| `src-tauri/icons/` | **Generated** by `scripts/make-desktop-icon.mjs` from `assets/vn-harness.svg`, and committed so a clone builds without running the generator |
144203

145204
## Failures
@@ -156,8 +215,9 @@ screen.
156215

157216
Measured on Windows 11 (Rust 1.94, Node 22.20, WebView2 153) rather than assumed:
158217

159-
- `cargo test` - 38/38 (`main.rs` 9, `readyline.rs` 14, `windowstate.rs` 15): the
160-
launch-token rules below plus the home, port and window-geometry rules;
218+
- `cargo test` - 56/56 (`main.rs` 9, `readyline.rs` 14, `windowstate.rs` 15,
219+
`keystate.rs` 18): the launch-token rules below plus the home, port,
220+
window-geometry and key-state rules;
161221
- the port rule: a free port is chosen whenever 3080 is taken (61203, 62066, 60927
162222
across the runs that were measured while the Web GUI was serving 3080), and 3080
163223
itself is asked for first once nothing holds it;
-692 Bytes
Loading
-691 Bytes
Loading
-691 Bytes
Loading

0 commit comments

Comments
 (0)