# DEPLOY.md — Deployment manual (DSH)

> **English** | [简体中文](DEPLOY.zh-CN.md)

You are reading the deployment instructions for jev-guard. The goal: make this "pre-execution safety valve" take effect on **DSH**,
and **have acceptable evidence for every step**. **Both WSL and Windows are supported.**

> **Read these two first, then this document:**
> [`docs/DSH-INTEGRATION.md`](./docs/DSH-INTEGRATION.md) — which of DSH's mechanisms it uses, how the four states map,
> the degradation contract, and the retrospective on "the plugin is installed ≠ it is really blocking".
> [`docs/USER-INTERVENTION.md`](./docs/USER-INTERVENTION.md) — the three channels a human can step in through.
>
> **If you are "an AI sent to deploy this", read [START-HERE.md](./START-HERE.md) first** — it has the pasteable task
> prompt and the hard constraints (which files may be changed). When you have finished, come back to this document and follow the steps;
> write the conclusion back to `verification-results/` with `tools/report-result.mjs`.

## 0. How to work (read this first)

1. **Do not skip steps.** The order is: first prove the judging layer works (§2.3) → then install it into DSH (§2.4) → finally the acceptance (§3).
2. **Back up the original value before changing any DSH configuration**, and state clearly in the conclusion how to roll back (§6).
3. **Verify against side effects, not against "it didn't error".** This pack has really had three layers of silent failure: the script exits 0 without a sound,
   the command runs anyway, and the log has nothing at all (see `docs/MEASUREMENTS.md` §10).
4. **Never print the key.** Read it from `secrets.json` or the credentials layer when needed, and report only its length or the last digits of a hash.
5. Every command can first be trial-run read-only (`selftest`, `rules`, `judge`, `status`) — they change no host configuration.

## 1. Prerequisites

| Item | Requirement | Check |
|---|---|---|
| Node | ≥ 20 (uses the global `fetch`, `AbortSignal.any`) | `node -v` |
| TypeSafe key | of the form `apikey_...`; if you don't have one, apply at https://console.typesafe.ai | §2.2 |
| Directory location | `T:\dsh-jev-guard` recommended (in WSL that is `/mnt/t/dsh-jev-guard`) | `ls /mnt/t/dsh-jev-guard` |
| Network | able to reach `https://api.typesafe.ai` **from an egress the service serves** — it is not reachable from every region: a blocked egress gets `HTTP 451` with `{"title":"Typesafe is not available in your region.","status":451}`, before the key is looked at. Which egress a request goes out from is decided **per executable** on Windows, so routing your shell is not enough: the DSH host process needs it too. The valve classifies this as `region` and degrades globally for 30 minutes rather than failing open (D17); verify with `guard status` and the `errorKind` line of `guard log --stats` | `node bin/guard.mjs judge 'pnpm test'` → must be `source: jev`, not `source: error` |
| DSH | able to install local plugins (the profile's `package.json` has `dsh.profile` / bundles) | `dsh --profile <name> --dump-config` |
| DSH version | **0.1.6-alpha.2**, **0.1.7-rc.2**, **0.2.0-rc.2** and **0.2.1-rc.2** — all verified end to end; what was run is listed in the README's "Verified host versions" paragraph. No host requirement is declared in `package.json`, so the plugin market never blocks install on a different version | `dsh --version` |

## 2. Install and configure

### 2.1 Put the directory in place

Put the whole `jev-guard` directory at `T:\dsh-jev-guard` (on the WSL side that is `/mnt/t/dsh-jev-guard`, **the same files**).
**`npm install` is not needed** — zero dependencies, only Node's built-in modules.

### 2.2 The key

Three sources, highest precedence first:

1. **The DSH credentials layer** (recommended): `ctx.credentials.resolve('TYPESAFE_API_KEY')` — goes through DSH's own credential store,
   and after a rotation **needs no restart**.
2. The environment variable `TYPESAFE_API_KEY` (the name is decided by `apiKeyEnv` in `config.json`).
3. A `secrets.json` **you create yourself in the package root**, containing `{"TYPESAFE_API_KEY": "apikey_..."}`
   (**if `apiKeyFile` is given a relative path it resolves against the package root, independently of the current directory** — true on both Windows and WSL).

Do not commit any of the three into any repository.

Record the third one with `node bin/guard.mjs key set` — it reads the key from **stdin only** (never from an
argument, which would land in your shell history and in `ps`), writes `apiKeyFile` with mode `0600`, keeps any
other keys already in that file, and prints the length and the path, never the value. `node bin/guard.mjs key
status` reports which source resolves and exits 3 when none does, so it doubles as a health check.

**A missing key degrades; it does not go quiet (D15).** With no key resolved the paid semantic layer is
paused — the free L0 rules and the pre-screen keep working — and the state is **sticky**: it does not expire
with time (there is nothing to probe), it ends the moment a key resolves, cleared on the spot with zero
requests and no restart. It is also scoped to the entry that reported it (`'cli'` or `'dsh-adapter'`), so a
CLI that cannot see a key does not stop DSH from judging, and vice versa. `guard status` exits 3 while
degraded, and the DSH session gets a one-line notice in the conversation saying which state the valve is in.

### 2.2b Language (optional, it runs without configuring it)

The copy (verdict reasons / CLI output / degradation warnings) exists in Chinese and English, and `lang` defaults to `'auto'`:
it resolves from `JEV_GUARD_LANG` → `LC_ALL`/`LC_MESSAGES`/`LANG`, **and only takes effect when those variables really name a supported language**
(such as `en_US.UTF-8` / `zh_CN.UTF-8`); otherwise (including `C.UTF-8`, or unset) it always uses `zh-CN`.
**This deliberately does not look at the system locale**: under WSL's common `LANG=C.UTF-8`, Node's `Intl` reports `en-US`,
which would quietly turn a Chinese session's reasons into English (measured 2026-09-20). To choose the language explicitly: write `"lang": "en"` in `config.json`,
set `JEV_GUARD_LANG=en` for the DSH process, or use the CLI's one-off `--lang en`.

**Do not casually change `promptLang`.** What it governs is the question sent to the judging service; it defaults to Chinese, which is exactly the language the 0.5/0.7 thresholds were calibrated against;
measured, switching it to English lowers p by about 0.04 on average, and three probes flip towards allow (`docs/MEASUREMENTS.md` §14).
If you really want to switch: re-calibrate first, or lower both thresholds by about 0.04 together.

### 2.3 First prove the judging layer works (not yet installed into DSH)

```bash
cd /mnt/t/dsh-jev-guard
node bin/guard.mjs selftest                                    # expect: all 12 checks pass (offline)
node bin/guard.mjs rules | head -5                             # expect: 26 deny + 24 ask rules listed
node bin/guard.mjs judge 'ls -la' 'git push --force origin main' 'pnpm test'
```

| Command | Expected action | Expected source |
|---|---|---|
| `ls -la` | `allow` | `prefilter` (offline) |
| `git push --force origin main` | `block` | `static-rule` (offline) |
| `pnpm test` | `allow` | `jev` (online, `p` should be far below 0.5) |

On failure: `source: error` = a key or a network problem (see the `errorKind` classification); a `selftest` failure = the pack is incomplete.

**Then run the whole set of offline self-checks again** (seven of them, cross-platform):

```bash
for t in selftest-entry selftest-i18n selftest-quota selftest-reason selftest-token selftest-rules selftest-audit; do
  printf '%-18s ' "$t"; node tools/$t.mjs | tail -1
done
```

### 2.4 Install it into DSH

```bash
dsh plugin --profile <profile> add /mnt/t/dsh-jev-guard      # on the Windows side use T:\dsh-jev-guard
# then restart DSH — plugins are not hot-reloaded
```

After installing, **confirm the plugin is really mounted** (don't look at "it didn't error"):

1. In a session, run a command that **is certain to be blocked** (for example `git push --force origin main`, which hits an L0 hard rule and costs nothing).
   Expect: refused, with `hard rule git-force-push hit` in the reason.
2. Look at the audit: `node bin/guard.mjs log --tail 3` — that entry should appear, with `policy` and `preset`.

It only counts as installed when both hold. **Only when item 1 does not hold**, first check the three layers of silent failure in `docs/DSH-INTEGRATION.md` §5.

### 2.5 Differences on Windows

| Item | WSL | Windows |
|---|---|---|
| Tool to intercept | `bash` | `pwsh` (**already in the default `tools` list**) |
| Quoting in the authorisation line | POSIX `'\''` | **PowerShell `''`** (the plugin switches automatically by platform) |
| cmd.exe users | — | use `guard allow --command-file cmd.txt` (independent of the shell's quoting rules) |
| State/log directory | `~/.jev-guard/` | `%USERPROFILE%\.jev-guard\` |

## 3. Acceptance (all must pass before continuing)

The acceptance checklist is in **[docs/VERIFICATION.md](./docs/VERIFICATION.md)**, and includes **the three human intervention channels** (U1–U3).
Summary:

| # | Acceptance | Criteria |
|---|---|---|
| 1 | The probe command is blocked after installation | the command really is refused + `guard.log` has the record |
| 2 | The L0 path (offline, cannot be overridden) | `mkfs` / `git push --force` → `block` / `static-rule` |
| 3 | The Jev path (online semantic judgment) | `rm -rf` on a real directory → `revise` or `block`, with `p` in the record |
| 4 | The false-positive defence (prose / redirection not hit by mistake) | a dangerous phrase inside the command text, and a command ending in `2>/dev/null`, are both **not blocked** |
| 5 | Audit log | one JSONL line per verdict; `log --stats` has actions/sources/rules/failure breakdown/cost |
| 6 | The token closed loop | authorise → retry the same one → allowed once → the token disappears, with `source: token` in the record |
| 7 | The authorisation entry point is only on an interactive terminal | a non-TTY is refused and prints the whole copyable command line |
| 8 | The approval prompt (policy `ask`) | the prompt appears and carries the valve's reason text as it stands; after clicking allow the command runs |
| 9 | Quota degradation | 402/401 → degradation, zero requests, `guard status` exit code 3, L0 still blocks; a `403` carrying an HTML page → `edge`, **no** degradation |
| 10 | A human running it by hand ≠ granting the AI permission | zero new audit entries, and an AI retry is **still blocked** |

## 4. Runtime

```bash
node bin/guard.mjs log --tail 20     # the last 20 verdicts
node bin/guard.mjs log --stats       # summary: actions/sources/rules/failure breakdown/cost
node bin/guard.mjs status            # health status (exit code 3 while degraded)
```

## 5. Overall acceptance checklist

- [ ] `node bin/guard.mjs selftest` 12/12
- [ ] all seven `tools/selftest-*.mjs` pass (**run once each on Windows and WSL**)
- [ ] `judge 'ls -la'` = allow / prefilter (zero network calls)
- [ ] `judge 'git push --force origin main'` = block / static-rule
- [ ] `judge 'rm -rf ~/<a real directory>'` = revise or block (online judgment)
- [ ] after installing it into DSH, a command that is certain to be blocked **really is blocked**, and `guard.log` has the record
- [ ] `guard status` outputs "✅ healthy" (health self-check; exit code 3 while degraded)
- [ ] **the three human intervention channels U1–U3** run through once each (token / approval prompt / a human running it by hand)
- [ ] rollback drill: remove it per §6 and confirm the original state is restored

## 6. Rollback

| Action | Command |
|---|---|
| Uninstall the plugin | `dsh plugin --profile <profile> remove jev-guard` + restart |
| One-click return to a configuration snapshot | `dsh-undo-savepoint`'s `undo_list` / `undo_restore` |
| Only want to disable it | `dsh-undo-savepoint`'s SAFE MODE (`undo_safe_mode on`) disables all user plugins |
| Clear state/log | delete `~/.jev-guard/` (it writes nowhere else) |

## 7. Troubleshooting

| Symptom | Cause | Handling |
|---|---|---|
| The plugin is installed but nothing is blocked | the plugin is not mounted, or the package path is wrong | run `selftest-entry` + see whether `guard.log` has records; read `DSH-INTEGRATION.md` §5 |
| `source: error`, with the reason `HTTP 401` | the key is invalid or revoked | change the key; **in the meantime the valve has already degraded automatically for 30 minutes** (it sends no more requests), so once it is fixed either wait for the cooldown to expire and it recovers by itself, or `guard status --clear` |
| `source: error`, with the reason `HTTP 402` | the credit is used up | same as the line above (this one **degrades** rather than retrying every time, which saves money) |
| `source: error`, with the reason `HTTP 403` and an HTML body | a CDN/WAF blocked the request at the edge; it never reached the judging service | nothing to fix on your side: this is classified `edge` and does **not** degrade. If it keeps happening, your egress is being blocked — read the `edge` count in `guard log --stats` |
| `source: degraded` | inside a degradation window | `guard status` will say which class it is + how much is left; the free L0 + pre-screen still work |
| `source: error`, with the reason `HTTP 451` and the body `{"title":"Typesafe is not available in your region.","status":451}` | **the service is region-locked and your egress is in a blocked region** — the request reached the service and was refused by its policy, before the key was checked (a valid key fails identically) | this is classified `region` and **degrades globally for 30 minutes**: `guard status` names the cause. Route the **DSH host process** (not just your shell — the split is per executable) through an egress the service serves, and confirm with `node bin/guard.mjs judge 'ls'` → `source: jev`. The cooldown probes once per 30 minutes and recovers by itself. The valve cannot change your egress; see `DECISIONS.md` D17 |
| `source: error`, with the reason `fetch failed` | the network/proxy is unreachable | check that `https://api.typesafe.ai` is reachable. It does **not degrade** (transient), but it accumulates in the "failure breakdown" |
| `guard status` keeps saying degraded, or a warning says the state file could not be removed | the state file sits in a read-only directory, so neither the cooldown nor the recovery can ever be written down | delete `~/.jev-guard/degraded.json` by hand, or fix that directory's permissions. The valve is otherwise healthy and judging online; until that file is gone, each new process repeats one probe |
| A dangerous command was not blocked | not in L0 and `p < lowThreshold` | look at the `p` in the `judge` output; if necessary lower `lowThreshold` or add an L0 rule for that class of command |
| Everything is blocked and no work can be done | the threshold is too low or L0 is too aggressive | look at the `rule.id` from `judge` first; edit `lib/rules.js` or raise `lowThreshold` |
| The authorisation line pasted into cmd.exe reports a syntax error | cmd does not accept POSIX/PowerShell quoting | switch to `guard allow --command-file cmd.txt` |
| A judging-service hiccup makes work impossible | this should not happen (fail-open) | if it really does happen, check `source: error` and `errorKind` in `guard.log` |

## 8. Security and privacy (must be passed on to the user as it stands)

1. **The script body is sent to TypeSafe's API.** That is the price of "understanding what `node x.mjs` does".
   Sensitive paths (`.env` / `.ssh` / `*.pem` / `*credential*` / `*secret*` / `*token*`) are skipped automatically, with an 8KB per-file cap.
   To turn it off entirely: `inlineScripts: false` in `config.json` (at the cost of those commands dropping back into the p≈0.31 blind spot).
2. **The key is read only from the credentials layer / environment variable / `secrets.json`, and is written into no log and no report.** The command text in a report goes through masking.
3. **Every failure is fail-open**: when the judging service is unavailable the valve blocks nothing — because DSH's own sandbox preset
   (anything except `danger-full-access`) is still in force before execution. If you want "block even when the service is down", thicken the L0 rules rather than changing this policy.
4. **It cannot stop a deliberate bypass.** Rewrapping it, encoding it, or writing `~/.jev-guard/allow.txt` directly can all get around it.
5. **What it guards against is accidents, not an adversary — that scope was fixed explicitly by the user; do not narrow it on your own initiative.**
   There are at least two known and **deliberately kept** bypasses: file-writing tools writing `allow.txt` directly; and writing the same thing another way to get around the judgment.
   The handling is to **record it**, not to seal it. The original decision text and the reasoning: [docs/DECISIONS.md](./docs/DECISIONS.md) **D1**.
