# Integrating dsh-trust-check (for dsh-market and CI)

How to wire a **pre-install gate** (or CI check) against this package. Field-level JSON contract: **[audit-schema.md](./audit-schema.md)**.

This scanner discloses capabilities. It is not an antivirus and never claims absence of risk. It produces facts; whether to block, prompt, or hide a plugin is your policy, not this package's. See [POSITIONING.md](./POSITIONING.md) (Chinese) for the division of responsibility.

## What you are integrating

```text
you extract the package  →  audit the tree  →  read plugins[0]  →  three-state gate
```

**You** download / extract. **This package** only audits a directory it is given (`--dir` or `collectPlugin`). It does not fetch tarballs.

Pin a released `dsh-trust-check` version in CI / market. Prefer checking **`schemaVersion === 2`** on the JSON payload over guessing from the npm version alone. `schemaVersion` bumps when the **output shape** breaks or a field becomes a stable contract; detection-rule churn does not bump it. Schema 2 adds `facts[]`, whose `id` is a stable filter key. No schema 1 field is removed or renamed, and the capability values and red-line templates are unchanged, so a schema 1 reader only needs to accept `2`.

## Adopting a new release

A catalog pins a scanner version on purpose, so that a release cannot change stored records without review. The rules below are written against the [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) catalog, the integrator that scans on every build today.

**Bump the pin when** that release's [CHANGELOG](../CHANGELOG.md) section **Affects catalog results** is not "None". Until then, stored records lag the current detection.

**Leave the pin** when Affects catalog results is "None". Detection is identical, so staying on the older pin (0.1.13 while 0.1.14 is current, for example) stores the same facts.

**A `schemaVersion` change is not a reason to bump by itself.** It means a bump has to update the catalog's reader in the same pull request. awesome-dsh-plugin's `factsFromScan` returns nothing for a `schemaVersion` it does not list as readable (`SCANNER_SCHEMA` on its main branch; `READABLE_SCHEMAS`, which holds 1 and 2, once its schema 2 reader lands), so bumping the pin before the reader accepts the new value turns every scan into a failure and freezes the existing records.

**Who opens the pull request.** The dsh-trust-check maintainer opens it against the catalog repository, with a before/after sample from [`scripts/catalog-noise.mjs`](../scripts/catalog-noise.mjs), so a reviewer can see which records change without reading the rule table. The same pull request updates the `tool` label (`dsh-trust-check@<version>`, hard-coded in `probe-capabilities.mjs` and in its test fixtures) along with `package.json` and the lockfile; otherwise new records carry the old version in `tool`.

**Rescan everything after a bump that changes detection.** awesome-dsh-plugin's `shouldRescan` never compares `tool`. An npm-published entry is rescanned only when its plugin version changes, never by age; only branch tarballs expire after `RECHECK_DAYS`. A release whose Affects catalog results section is not "None" therefore needs `PROBE_ALL=1` on the next catalog pass, or most records keep the old scanner's facts.

## Two modes

| Mode | Command | Use for |
|---|---|---|
| **`--dir`** (pre-install) | `npx dsh-trust-check --dir <extracted> [--spec <spec>] --json` | Market confirm dialog, CI on a unpacked candidate |
| **`--profile`** (post-install) | `npx dsh-trust-check [--profile web] --json` | Settings “plugin checkup”; may include `acks` |

`--dir` and `--profile` are mutually exclusive.

### Reading `--json`

- Always check `schemaVersion` (currently `2`).
- Always check `errors`: non-empty means that tree (or installed entry) could not be read — treat as scan failure, not `clear`. This includes profiles that do not exist, a missing or corrupt profile `package.json`, a declared plugin directory that is absent, and a package whose scan hits a size limit or cannot read a primary entry.
- `coverageNotes` on a report, when present, describe static imports the scanner could not expand, files whose comments could not be stripped (those files are scanned raw), and dev-only files dropped by a parseable `package.json` `files` field (`filtered by package.json files: …`). They do not change `verdict()`. A hand-written gate keeps using `errors`, `redLines`, `capabilities`, and patch override/disable.
- **`--dir`**: `profile` is `""`, `dir` is the absolute path. Use **`plugins[0]`**. Empty `plugins` with `errors` ⇒ fail closed. A source tree whose `files` field parses exactly omits files npm would not publish, so capabilities can differ from a scan of the whole checkout. An extracted registry tarball already contains only published files.
- **Empty / corrupt extract**: if the directory has no readable `package.json` **and** no scannable source/skill/patch files, collection throws and lands in `errors` (not a silent `clear` report). A `package.json`-only minimal package is still valid and may be `clear`.
- **`--profile`**: iterate `plugins[]`. Optional `acks` is for Settings fingerprints only.

## Pre-install gate (three states only)

Market pre-install has **no** `trust-ack.json`. Call `verdict(report)` with no ack (or reimplement from the table).

| State | When (no ack) | Suggested UI |
|---|---|---|
| **`red`** | `redLines.length > 0` | Block by default; optional “confirm risk” to continue |
| **`review`** | no red lines, but privileged `capabilities` (or patch override/disable) | Show capability list; suggest confirm |
| **`clear`** | no red lines and no privileged capabilities | Nothing detected in this static pass. Not a safety claim: keep the disclosure visible and do not label it “safe” or “verified”. Whether to skip a prompt is your policy. |

To filter on red lines, map each `redLines` string with `classifyRedLine()` to a stable code (`install-script`, `core-tamper`, `creds-network`, `plaintext-http`, `literal-ip`) instead of matching on the message text.

**Do not use for pre-install:**

- `accepted` / `expected` — require a matching ack; Settings-only
- `score` / `band` — a low score can still be `review`; **gate follows `redLines` via `verdict()`, not the numeric band**

Stable fields to hard-parse: `name`, `version`, `spec`, `capabilities`, `redLines`. The wording of `capabilities` values and `redLines` sentences is fixed too, so you can render and translate them — see the [wording contract](./audit-schema.md#wording-contract-capabilities-values-and-redlines-templates).  
Everything else (`destinations`, `evidence`, `score`, …) is display-only and may change as noise rules evolve — see [audit-schema.md](./audit-schema.md).

## Path A — spawn CLI (no TypeScript import)

```sh
# After you extracted the candidate package:
npx dsh-trust-check@<pinned> --dir "$EXTRACTED_DIR" --spec "$INSTALL_SPEC" --json
```

Pseudo-parse:

```js
const body = JSON.parse(stdout)
if (body.schemaVersion !== 2) throw new Error('unsupported audit schema')
if (body.errors?.length) throw new Error(body.errors[0].message)
const report = body.plugins[0]
if (!report) throw new Error('no audit report')

// Gate logic mirrors verdict(); if you can import the package, call verdict(report) instead of re-deriving this.
const gate =
  report.redLines.length > 0 ? 'red'
  : (report.capabilities.length > 0
      || report.injections.some(i => i.kind === 'override' || i.kind === 'disable'))
    ? 'review'
  : 'clear'

if (gate === 'red') { /* block; allow confirm */ }
else if (gate === 'review') { /* show capabilities; suggest confirm */ }
else { /* nothing detected; still not a safety claim */ }
```

Prefer importing `verdict` when you already depend on the package (Path B): the CLI JSON does not embed the verdict string; you derive it.

### `--exit-code` (optional)

Without this flag the process exits 0 even when the scan failed or found a red line, so a shell `&&` chain does not fail closed. Add `--exit-code` to use the same codes as [`scripts/market-gate-demo.mjs`](../scripts/market-gate-demo.mjs):

| Exit | Meaning |
|---|---|
| 0 | `clear`, or (profile mode) `accepted` / `expected`. Still not a safety claim |
| 1 | `review` |
| 2 | `red` |
| 3 | scan failed: `errors` is non-empty, or `--dir` produced no report |

Profile mode takes the worst verdict across plugins and honors `acks`. A profile with no plugins and no errors exits 0. Usage errors (unknown flag, missing directory) stay at exit 1 and print to stderr, before any report.

**Pick one: parse JSON, or read the exit code.** Node's `execFile` / `execFileSync` (and their promisified forms) treat any non-zero exit as a failure and throw. With `--exit-code`, every `review` and `red` package would then land in your error path instead of being read. If you parse `--json` — which is what a catalog or card surface needs — leave `--exit-code` off. Use it only where the exit code *is* the decision, such as a shell `&&` chain or a CI step.

**Working reference:** [`scripts/market-gate-demo.mjs`](../scripts/market-gate-demo.mjs) in this repo implements exactly this Path-A flow — spawn `--dir --json`, check `schemaVersion` + `errors`, read `plugins[0]`, derive the three-state gate, with malformed-report checks that fail closed. It is written only from this document (no API import), so it doubles as a conformance test: if the doc drifts from the CLI output, the demo breaks. Run it on an extracted package:

```sh
node scripts/market-gate-demo.mjs <extracted-dir> [--spec <spec>]
# exit: 0 clear · 1 review · 2 red · 3 scan failed / malformed
```

## Path B — import API (in-process)

```ts
import {
  auditPlugin,
  collectPlugin,
  verdict,
} from 'dsh-trust-check'

const report = auditPlugin(collectPlugin(extractedDir, installSpec))
const gate = verdict(report) // 'red' | 'review' | 'clear' without ack

switch (gate) {
  case 'red':
    // block by default
    break
  case 'review':
    // show report.capabilities (and optional destinations for disclosure)
    break
  case 'clear':
    // nothing detected in this static pass; your policy decides whether to prompt
    break
}
```

`collectPlugin` reads the package tree from disk (same rules as CLI `--dir`). You still must extract first.

Optional: `buildAuditResponse` / `AUDIT_SCHEMA_VERSION` if you assemble a full `AuditResponse` yourself (host route and CLI already do).

## What not to do

- Do not treat empty chips / `clear` as “safe” — only “nothing detected in this static pass”.
- Do not gate on specific destination hosts; placeholder and allowlist rules change (e.g. RFC 2606 `.invalid`).
- Do not treat a skipped dependency tree as safe. Bare imports such as `import 'lodash'` are not expanded. A relative import that stays inside this package, including under its own `node_modules`, is scanned.
- Do not expect this tool to stop install scripts that already ran during your extract/install step; run `--dir` as early as your pipeline allows.

## Post-install (Settings) — brief

Profile mode + UI ack store can yield `accepted` / `expected` when the user confirms a fingerprint. That flow is owned by the dsh-trust-check Settings page, not by market’s install dialog. Market should not invent ack files for pre-install.

## Related

- [audit-schema.md](./audit-schema.md) — field contract and examples
- [THREAT-MODEL.md](../THREAT-MODEL.md) — what static analysis can and cannot claim
- Package README § For integrators
