dsh-plugin-guide 0.4.6 → 0.5.1

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/SKILL.md CHANGED
@@ -56,3 +56,9 @@ It defines the three layers and the difference between a check and advice.
56
56
  This project is non-official and has no certification, accreditation, security
57
57
  audit, or market gate. It does not score or rank plugins. When in doubt, link to
58
58
  the source document and state what was not run.
59
+
60
+ For plugin security — capabilities, credentials, egress, install scripts, and
61
+ supply chain — read `docs/security.md`. The security scope is static and shipped
62
+ in `dsh-standards` 1.6.0+; `DSC-15`/`DSC-16`/`DSC-17` can block only under
63
+ `--scope security`, `DSC-21` is WARN-only, and runtime security needs a sandbox,
64
+ not a checker.
package/docs/boost.md CHANGED
@@ -90,6 +90,20 @@ pitfalls”), record it as a WARN or a note. Do not invent a number.
90
90
  loader/context-level fixtures.
91
91
  - BOOST is not a market gate and must not become a certification.
92
92
 
93
+ <a id="security-boundary"></a>
94
+ ## Security boundary
95
+
96
+ BOOST measures Cordis practice, not security. A reversible effect or a seam is
97
+ good engineering, and it reduces blast radius, but it is not a sandbox: the
98
+ plugin still shares the host process, the user, and the network. The static
99
+ `DSC-15`–`DSC-22` checks run in a separate `--scope security` (1.6.0+); outside
100
+ it they are capped to WARN in the separate `security` block. `DSC-15`/`16`/`17`
101
+ can block only under that scope, and `DSC-21` is WARN-only. They never change a
102
+ BOOST result and none of them proves a plugin safe. Runtime questions — actual
103
+ egress, file reads, prompt corruption — require a sandbox with a network policy
104
+ and capture, not an evidence card. Keep the security claim `not-run` instead of
105
+ borrowing BOOST evidence for it. See [security.md](security.md#scope).
106
+
93
107
  ## Run the audit
94
108
 
95
109
  ```sh
package/docs/entry.md CHANGED
@@ -160,6 +160,10 @@ pass.
160
160
  fs write, or subprocess spawn. Do that work in `apply()` and clean up with
161
161
  `ctx.effect()`.
162
162
  - README says how to install and what it needs.
163
+ - No `preinstall`/`install`/`postinstall`/`prepare` script in the published
164
+ package; if a build is needed, run it in CI, not on the user's machine.
165
+ - No `eval`/`new Function`/`vm`; credentials go through `ctx.credentials`, not
166
+ a backing file; third-party endpoints are disclosed.
163
167
  - No “official”, “certified”, “secure”, or ranking claim.
164
168
 
165
169
  ## Declaration fields
@@ -303,7 +307,46 @@ export function apply(ctx) {
303
307
  }
304
308
  ```
305
309
 
310
+ <a id="security-checks"></a>
311
+ ## Security checks (DSC-15 to DSC-22)
312
+
313
+ ENTRY is a compatibility gate; it does not answer "is this safe to install". The
314
+ static security scope ships in `dsh-standards` 1.6.0+ and evaluates eight
315
+ checks; with `--scope security` the report's `checks[]` carries all 22
316
+ (`DSC-01`–`DSC-14` plus `DSC-15`–`DSC-22`).
317
+
318
+ | Check | What it looks for | Verdict in `--scope security` |
319
+ |---|---|---|
320
+ | `DSC-15` | `preinstall`/`install`/`postinstall`/`prepare` that download, decode, or execute (command string and referenced in-package script body, ≤512 KB) | blocking |
321
+ | `DSC-16` | `eval`, `new Function`, `vm`, `module._compile`, `process.binding`; bare `child_process` is WARN in 1.6.1 | blocking |
322
+ | `DSC-17` | reads of `~/.ssh`, `~/.aws`, `.npmrc`, `.netrc`, or credential backing files | blocking |
323
+ | `DSC-18` | non-https, userinfo, bare-IP, webhook/paste/tunnel egress targets | WARN (clue) |
324
+ | `DSC-19` | minified blobs, large base64 blocks, wasm/native without source | advisory |
325
+ | `DSC-20` | tool/web text flowing into the system prompt without a boundary | advisory (static v1 + runtime hook v2) |
326
+ | `DSC-21` | typosquat names, fake scopes, unpinned or script-bearing dependencies | mandatory but **WARN-only, never blocking** (listing included) |
327
+ | `DSC-22` | `dist.integrity`, signatures, provenance attestations | advisory |
328
+
329
+ `--scope security` is released in 1.6.0+; 1.5.x and earlier exit `3` for the
330
+ unknown scope. Outside that scope the eight are not in `checks[]`; a security
331
+ `FAIL` is capped to WARN in the separate `security` block, never enters
332
+ `buckets.blocking`, and cannot change an ENTRY verdict. Only `15`/`16`/`17` can
333
+ block (and only under `--scope security`); `21` never blocks; `18`/`19`/`20`/`22`
334
+ are advice. `DSC-16` subprocess FAIL is scoped to 1.6.2; 1.6.1 keeps a bare
335
+ `child_process` import at WARN.
336
+
337
+ ```sh
338
+ # 1.6.0+; exits 3 only on 1.5.x and earlier
339
+ npx dsh-standards ./my-plugin --scope security --format json
340
+ ```
341
+
342
+ The author, user, and market checklists — with copyable manifests, commands,
343
+ and the boundary between static checks and a runtime sandbox — are in
344
+ [security.md](security.md). ENTRY's own declaration fields are not enforcement,
345
+ and neither is a `DSC-11` PASS.
346
+
306
347
  ## What ENTRY does not do
307
348
 
308
349
  It is not a security audit, sandbox, full contract audit, market gate, score, or
309
- ranking. Those are separate concerns; do not borrow their authority.
350
+ ranking. Those are separate concerns; do not borrow their authority. The static
351
+ `DSC-15`–`DSC-22` checks are a separate security scope, covered in
352
+ [security.md](security.md), not part of the ENTRY verdict.
package/docs/flow.md CHANGED
@@ -201,6 +201,26 @@ the original `command[]`, `exitCode`, the last 20 lines of stderr, and the
201
201
  absolute log path. Then attach the clause anchor the journey maps to (for
202
202
  example `J2.toggle → 32-DSH-FLOW #R2`).
203
203
 
204
+ <a id="security-boundary"></a>
205
+ ## Security boundary
206
+
207
+ J5 checks that permissions are declared consistently; it is static preflight,
208
+ not a security test. The static `DSC-15`–`DSC-22` checks live behind a separate
209
+ `--scope security`, shipped in `dsh-standards` 1.6.0+ (1.5.x and earlier exit
210
+ `3`), and they never turn a FLOW journey into a security pass. Outside that
211
+ scope a security finding is advisory: it is capped to WARN in the separate
212
+ `security` block, never enters `buckets.blocking`, and cannot change the FLOW
213
+ verdict. Under `--scope security` only `DSC-15`/`DSC-16`/`DSC-17` can block;
214
+ `DSC-21` is WARN-only.
215
+
216
+ Static checks also cannot observe what a plugin does at runtime — which URLs it
217
+ reaches, which files it reads, or whether a prompt was corrupted. Those answers
218
+ need a sandbox with a network policy plus capture, and a sandbox that only
219
+ limits subprocess file writes is not that boundary. So do not write “FLOW
220
+ passes, therefore it is safe”; write what was observed, and keep the security
221
+ claim `not-run` until it was actually run. Details:
222
+ [security.md](security.md#scope).
223
+
204
224
  ## Status, not a score
205
225
 
206
226
  | Status | Meaning |
package/docs/levels.md CHANGED
@@ -20,6 +20,7 @@
20
20
  | an ENTRY question | [entry.md](entry.md) — package shape, runtime surface, declarations |
21
21
  | a FLOW journey to record | [flow.md](flow.md) — the `dsh-flow-evidence/2` file and J1–J8 evidence |
22
22
  | a BOOST practice to evidence | [boost.md](boost.md) — the thirteen-field evidence card |
23
+ | a security question — what can a plugin do, is this safe to install, what must a market disclose | [security.md](security.md) — author/user/market checklists and the `DSC-15`–`DSC-22` boundary |
23
24
 
24
25
  The two reference docs exist because a report ID alone does not tell you what to
25
26
  change: [rules.md](rules.md) maps IDs to fixes, and [recipes.md](recipes.md)
@@ -73,6 +74,26 @@ Start here: [BOOST guide](boost.md) and the canonical
73
74
  The card fields are in
74
75
  [boost.md § evidence, not a badge](boost.md#thirteen-fields).
75
76
 
77
+ ## Security is not a fourth layer
78
+
79
+ Security checks are a **scope**, not a level, and they ship in `dsh-standards`
80
+ 1.6.0+ (1.5.x and earlier exit `3` for the unknown scope): `DSC-15`–`DSC-22`
81
+ sit behind `--scope security`. They never change an ENTRY, FLOW, or BOOST
82
+ verdict. Outside that scope a security `FAIL` is capped to WARN in the separate
83
+ `security` block and does not enter `buckets.blocking`; under it only
84
+ `DSC-15`/`16`/`17` can block, and `DSC-21` is WARN-only.
85
+
86
+ - Static checks only match known source shapes; they cannot see runtime egress,
87
+ file reads, or prompt corruption. Runtime security needs a sandbox with a
88
+ network policy and capture.
89
+ - No security `PASS` means “safe”. It means “no known pattern matched in this
90
+ static pass”.
91
+ - The three layers stay about compatibility, user journeys, and Cordis practice.
92
+ Do not turn them into a security certification, and do not add a “security
93
+ level” to the vocabulary.
94
+
95
+ Read [security.md](security.md) for the author, user, and market checklists.
96
+
76
97
  ## Run the checks
77
98
 
78
99
  ```sh
@@ -94,5 +115,6 @@ The legacy build skill/CLI is [`dsh-primer`](https://github.com/PerryLink/dsh-pr
94
115
  - `static` ≠ `runtime`
95
116
  - `not-run` ≠ `pass`
96
117
  - `boost-evidenced` ≠ plugin grade
118
+ - security check ≠ security audit
97
119
  - `A–D` grades (where they exist) are orthogonal to ENTRY/FLOW/BOOST
98
120
  - No official, certified, verified, audited, secure, recommended, or ranked claim
package/docs/rules.md CHANGED
@@ -22,8 +22,10 @@ incomplete/not-run, `3` usage error. `WARN` is not a clean pass and
22
22
  The JSON report also carries a sibling `advice[]` array: one entry per live
23
23
  detail, with `title`, `why`, `fix`, `docsUrl`, `severity`, and `evidence`. It is
24
24
  additive and never changes the verdict (turn it off with `--no-advice`). The
25
- `docsUrl` follows the checker's `DOCS_BASE` (currently `v0.4.4`); if it points
26
- at an older page, this table is the current mapping.
25
+ `docsUrl` follows the checker's `DOCS_BASE`: 1.6.1 pins `v0.4.6`, and the 1.6.2
26
+ release moves it to guide `v0.5.0`. The `docs/security.md` anchors used by the
27
+ `C-15`–`C-22` advice exist in 0.5.0; if a link points at an older page, this
28
+ table is the current mapping.
27
29
 
28
30
  <a id="scope-matrix"></a>
29
31
  ## Which checks run in which scope
@@ -35,6 +37,7 @@ at an older page, this table is the current mapping.
35
37
  | `--scope entry --entry-claim static` | 01, 02, 11, 12, 13 (+ 03 for listing) | 04–09 are attempted and printed (usually `ERROR`/`NA`), 14 is `NA`; a `DSC-11`/`DSC-12`/`DSC-13` FAIL is downgraded to `WARN` |
36
38
  | `--scope entry --entry-claim runtime` | + 14 | 14 carries the runtime verdict, not 04–09 |
37
39
  | `--scope mvp` | 01, 02, 04–09 | the MVP subset, labelled `MVP-13` |
40
+ | `--scope security` (1.6.0+) | + 15, 16, 17, 21; 18/19/20/22 advisory | `checks[]` becomes 22 (14 + 8); DS-C 01–14 are unchanged and the report gains an additive `security` block. `15`/`16`/`17` blocking, `21` mandatory but WARN-only |
38
41
 
39
42
  <a id="table"></a>
40
43
  ## The check table
@@ -55,10 +58,27 @@ at an older page, this table is the current mapping.
55
58
  | **DSC-12** | C-12 credentials + seams | Blocking (ds-c); `WARN` in ENTRY. | Read credentials through `ctx.credentials`, declare `credentials:list` for `listRecords()` and `credentials:provider` for a provider, and never read the backing file. | [entry.md § credentials and seams](entry.md#credentials-and-seams) |
56
59
  | **DSC-13** | C-13 labels + mirror | Blocking (ds-c); `WARN` in ENTRY, advisory as a WARN. | Use only the five labels, add the label each capability requires, point `dsh.upstream` at an https URL that is not this package **and** add `mirror`, and drop endorsement words. | [entry.md § labels and mirror](entry.md#labels-and-mirror) |
57
60
  | **DSC-14** | C-14 entry run | Blocking only for a runtime claim (`--scope entry`). | Supply the real host: `--host` for a bundle (G0–G3) or `--runtime` for a plugin (L1); a client or preset cannot claim runtime. | [entry.md § runtime surface](entry.md#runtime-surface) |
61
+ | **DSC-15** | C-15 lifecycle scripts | **Blocking only under `--scope security`** (1.6.0+); not run in the default scopes. It reads the referenced in-package script body (≤512 KB) as well as the command string. | Ship no `preinstall`/`install`/`postinstall`/`prepare`; if one is unavoidable, no download-to-shell, `base64 -d`, or `node -e`. | [security.md § author 4](security.md#author-files) |
62
+ | **DSC-16** | C-16 dynamic execution | **Blocking only under `--scope security`**. `eval`/`Function`/`vm`/`_compile`/`process.binding` FAIL; a bare `child_process` import is WARN in 1.6.1, and the dangerous-subprocess FAIL is 1.6.2. | Remove `eval`, `new Function`, `vm.runIn*`, `module._compile`, and `process.binding`; dynamic `import()` takes a static path only. | [security.md § author 4](security.md#author-files) |
63
+ | **DSC-17** | C-17 sensitive files | **Blocking only under `--scope security`**. | Read credentials through `ctx.credentials`; never read `~/.ssh`, `~/.aws`, `.npmrc`, `.netrc`, or a credential backing file. | [security.md § author 2](security.md#author-credentials) |
64
+ | **DSC-18** | C-18 egress endpoints | **WARN** under `--scope security` (clue, not blocking). | Use a constant `https` allowlist, `redirect: 'manual'`, no credentials across origins, and disclose third-party endpoints. | [security.md § author 3](security.md#author-egress) |
65
+ | **DSC-19** | C-19 obfuscation/binary | **Advisory** under `--scope security`. | No minified blobs or binary-only code; ship wasm/native with source, build method, and `sha256`. | [security.md § author 4](security.md#author-files) |
66
+ | **DSC-20** | C-20 prompt injection | **Advisory**: static v1 (external text into `systemPrompt`, instruction-like literals) plus a runtime hook in v2; never blocking. | Treat tool/web/file/session text as untrusted, validate `output.schema`, and wrap external text before it reaches a prompt. | [security.md § author 5](security.md#author-prompt) |
67
+ | **DSC-21** | C-21 supply chain | **Mandatory under `--scope security`, but WARN-only and never blocking** (listing included). | Commit the lockfile, pin dependencies, reject typosquat/lookalike scopes, and treat an unpinned or script-bearing dependency as `not-run`. | [security.md § author 6](security.md#author-publish) |
68
+ | **DSC-22** | C-22 provenance/integrity | **Advisory**, never blocking. | Publish with provenance/attestations; `dist.integrity` only binds the tarball to the registry, not the code to a source. | [security.md § author 6](security.md#author-publish) |
58
69
 
59
70
  `DSC-14` has no `C-14` in the clause text; it is the ENTRY run-set gate
60
71
  (`ENT-14` in 31-DSH-ENTRY). Treat it as "did the thing actually start".
61
72
 
73
+ `DSC-15`–`DSC-22` shipped in `dsh-standards` 1.6.0; 1.5.x and earlier treat
74
+ `--scope security` as an unknown scope and exit `3`. Only `--scope security`
75
+ makes `15`/`16`/`17` blocking; `21` is mandatory but WARN-only and never blocks
76
+ (listing included). In every other scope the eight stay out of `checks[]`; their
77
+ FAILs are capped to WARN in the separate `security` block and never change the
78
+ DS-C verdict. They are static evidence: three can fail mechanically, one only
79
+ warns, four only return clues, and no PASS proves a plugin safe. The
80
+ author/user/market checklists are in [security.md](security.md).
81
+
62
82
  <a id="warn"></a>
63
83
  ## Two things that look like passes but are not
64
84
 
@@ -89,6 +109,11 @@ them:
89
109
  - whether a `red-team` label was actually used for red-team work (`C-13.5`
90
110
  only asks for `dsh.usageScope`)
91
111
  - browser rendering of a client bundle (`G4` is a static shape check)
112
+ - runtime network egress, file reads, or prompt corruption — `DSC-18`/`DSC-20`
113
+ are static clues; observing the behavior needs a sandbox with a network policy
114
+ and capture (see [security.md](security.md#scope))
115
+ - whether a dependency's own install script ran, or whether a package is
116
+ malicious. `DSC-15`/`DSC-21` cover known shapes, not intent.
92
117
 
93
118
  The checker's default `--runtime-version` is `0.2.2-alpha.2`. Pass
94
119
  `--runtime-version <the host you target>` when that differs, or `DSC-02` will
@@ -0,0 +1,427 @@
1
+ # Security: what a plugin can do, and what the checks cannot prove
2
+
3
+ > **Read this first.** A DSH plugin runs in the host's process, as the same
4
+ > user, with the host's full permissions. The host does not sandbox a loaded
5
+ > plugin: the sandbox only restricts file writes made by tool subprocesses, and
6
+ > network/process visibility is outside its vocabulary. `dsh.capabilities` is a
7
+ > declaration the host does not read or enforce. So a plugin can open and write
8
+ > files, spawn processes, reach the network, and read the credential store
9
+ > whether or not it declares that it does.
10
+
11
+ `DSC-15`–`DSC-22` are the **static security checks** shipped in
12
+ `dsh-standards` **1.6.0**; `--scope security` is a released flag
13
+ from 1.6.0 on (1.5.x and earlier treat `security` as an unknown scope and exit
14
+ `3`; 1.6.1 supports it).
15
+ With `--scope security`, `checks[]` carries all **22** checks (`DSC-01`–`DSC-14`
16
+ plus `DSC-15`–`DSC-22`); the default `ds-c` run still carries 14 and reports the
17
+ eight in the additive `security` block. Three can block (`15`, `16`, `17`), one
18
+ is mandatory but **WARN-only and never blocks** (`21`, listing included), and
19
+ four only produce clues (`18`, `19`, `20`, `22`); all eight reduce risk, and
20
+ **none of them proves a plugin is safe**.
21
+
22
+ The `security` block is a **separate view**, not a fourth verdict. In the default
23
+ scope a security `FAIL` is capped to `WARN` inside `security.checks[]`; it is
24
+ listed only in `security.warn[]` (`security.blocking[]` stays empty), does not
25
+ enter `buckets.blocking` or `conclusion.headline`, and changes neither `status`
26
+ nor the exit code. Only `--scope security` puts `15`/`16`/`17` into `checks[]`,
27
+ where a `FAIL` enters `buckets.blocking`, the headline, `status=fail` and exit
28
+ `1`. `DSC-21` never FAILs, so it never blocks in either profile. Read the scope,
29
+ and read the block's `checks[]`, not just its four arrays — see
30
+ [Reading the `security` block](#security-block).
31
+
32
+ ## The trust boundary in one table
33
+
34
+ | Fact | What follows |
35
+ |---|---|
36
+ | plugin and host share a process | `import node:fs`, `fetch`, `child_process`, and `process.env` are always reachable |
37
+ | `dsh.capabilities` is a declaration | declaring `[]` does not remove a capability; over-declaring is a WARN, under-declaring is a static FAIL |
38
+ | the sandbox covers tool subprocess file writes only | it is not a boundary around plugin code, and not a network policy |
39
+ | the checker executes the plugin | `--runtime`/C-4/C-14 import it; the check path has no sandbox and inherits `HOME` |
40
+ | static checks see known source shapes | they cannot see runtime egress, dynamic URLs, or whether a prompt was really corrupted |
41
+ | runtime safety needs containment | a network-policy sandbox plus capture/audit is the only way to observe what code actually does |
42
+
43
+ <a id="author"></a>
44
+ ## Author guide
45
+
46
+ <a id="author-least-privilege"></a>
47
+ ### 1. Least privilege and the capability declaration (DSC-16, DSC-18)
48
+
49
+ Declare only the closed-set IDs the plugin really touches. Every static trigger
50
+ needs its entry (`C-11.2`), and an entry with no trigger is an over-declaration
51
+ WARN (`C-11.5`). There are 15 IDs: `fs:read`, `fs:write`, `shell`, `subprocess`,
52
+ `network:egress`, `network:ingress`, `credentials:read`, `credentials:list`,
53
+ `credentials:provider`, `llm:adapter`, `sessions:read`, `system-prompt:write`,
54
+ `tools:register`, `remote:expose`, `plugin:install`.
55
+
56
+ ```json
57
+ {
58
+ "dsh": {
59
+ "capabilities": ["tools:register"]
60
+ },
61
+ "security": {
62
+ "status": "draft",
63
+ "note": "non-host disclosure only; not read, not enforced",
64
+ "permissions": {
65
+ "files": [],
66
+ "network": [],
67
+ "credentials": [],
68
+ "shell": false,
69
+ "subprocess": false
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ `dsh.permissions` and `security.permissions` are **not host fields**. The
76
+ standard is not fixed, so this guide prints a `security.permissions` block
77
+ marked `draft`: it is disclosure for a human or a market, never enforcement.
78
+ If you do not want to make the disclosure claim, delete the block — an absent
79
+ field means "not declared", not "no permissions". `dsh.commitments` and
80
+ `dsh.disclosure` are the same kind of optional disclosure: `false` means "no
81
+ promise", absent means "not declared". Do not fill every commitment `true`; an
82
+ unsupported claim is worse than a missing one.
83
+
84
+ <a id="author-credentials"></a>
85
+ ### 2. Credentials and sensitive files (DSC-17)
86
+
87
+ Resolve a reference per operation, through the seam, and never print, log,
88
+ persist, or put a value in `argv`. `describe()` is the only shape a UI should
89
+ see. Do not read `$DSH_HOME/.credentials.yaml`, `~/.ssh`, `~/.aws`, `~/.netrc`,
90
+ `.npmrc`, or any other backing file, and do not enumerate unrelated records.
91
+
92
+ ```ts
93
+ import { credentialRef } from '@deepseek-ai/dsh-credentials'
94
+
95
+ const ref = credentialRef('MY_API_KEY')
96
+ const resolved = await ctx.credentials.resolve(ref) // re-resolve per operation
97
+ if (!resolved) throw new Error('MY_API_KEY is not configured') // never print resolved.value
98
+
99
+ // configuration UI: presence and writability only
100
+ const info = await ctx.credentials.describe(ref)
101
+ ```
102
+
103
+ Environment scrubbing applies to `ctx.subprocess`: the child gets a scrubbed
104
+ parent environment, and an explicit `env` entry is a deliberate opt-in. Do not
105
+ forward a token "just in case".
106
+
107
+ <a id="author-egress"></a>
108
+ ### 3. Egress endpoints (DSC-18)
109
+
110
+ Use a constant allowlist of exact hostnames and `https` only. Never fetch a URL
111
+ that came from a model, a tool result, or a user message; use
112
+ `redirect: 'manual'` and refuse a redirect unless every `Location` hop passes the
113
+ same allowlist (bounded), and never send a credential across an origin. Disclose
114
+ third-party endpoints, what is retained, and whether session text is forwarded.
115
+
116
+ ```ts
117
+ const ALLOW = new Set(['api.example.com'])
118
+
119
+ export function checkedUrl(raw: string): URL {
120
+ const url = new URL(raw)
121
+ if (url.protocol !== 'https:' || !ALLOW.has(url.hostname)) {
122
+ throw new Error('blocked endpoint')
123
+ }
124
+ return url
125
+ }
126
+
127
+ const res = await fetch(checkedUrl('https://api.example.com/v1/x'), {
128
+ redirect: 'manual',
129
+ })
130
+ ```
131
+
132
+ The host has no egress policy and `web_fetch` can reach private addresses. This
133
+ allowlist is the plugin's own discipline, not a sandbox. If you must call a
134
+ third party, say so in `dsh.disclosure.endpoints` (when that field exists) and
135
+ in the README.
136
+
137
+ <a id="author-files"></a>
138
+ ### 4. Files, subprocesses, and dynamic execution (DSC-15, DSC-16, DSC-17, DSC-19)
139
+
140
+ - Use `ctx.fs`: `resolve()` the target, then `contains(workspace, target)`.
141
+ Reject `..`, absolute paths, and symlinks (check with `lstat` first), bound the
142
+ size, and keep temp files at `0700`/`0600` (ACL on Windows).
143
+ - Do not start a subprocess by default. When you must, use
144
+ `ctx.subprocess.spawn` with an `argv` array, an absolute executable, `cwd`
145
+ inside the workspace, the scrubbed environment, a timeout/abort, and bounded
146
+ output. Never build a shell command string.
147
+ - Never use `eval`, `new Function`, `vm.runIn*`, `module._compile`, or
148
+ `process.binding`. Dynamic `import()` takes a static path only. Do not publish
149
+ minified blobs or binary-only code; if you ship wasm/native, ship the source,
150
+ the build method, and a `sha256`.
151
+ - Lifecycle scripts are the install-time RCE surface (`DSC-15`): ship none. If a
152
+ build truly needs one, never pipe a download into a shell, and never hide it
153
+ behind `base64 -d` or `node -e`.
154
+
155
+ ```ts
156
+ const workspace = await ctx.fs.resolve(process.cwd())
157
+ const target = await ctx.fs.resolve(userPath)
158
+ if (!ctx.fs.contains(workspace, target)) throw new Error('path escapes the workspace')
159
+
160
+ const child = ctx.subprocess.spawn({
161
+ argv: ['/usr/bin/git', 'status'],
162
+ cwd: process.cwd(),
163
+ stdio: { stdin: 'ignore', stdout: { maxBytes: 64_000 }, stderr: { maxBytes: 64_000 } },
164
+ graceMs: 5_000,
165
+ signal,
166
+ })
167
+ const outcome = await child.done
168
+ ```
169
+
170
+ The sandbox still only covers subprocess file writes; a plugin's own `fetch` and
171
+ `node:fs` bypass every seam. Do not read a declaration as isolation.
172
+
173
+ <a id="author-prompt"></a>
174
+ ### 5. Tool output and prompt injection (DSC-20)
175
+
176
+ Tool results, web pages, files, and session text are **untrusted data**. Validate
177
+ them against the tool's `output.schema` (a mismatch is `INVALID_TOOL_OUTPUT`),
178
+ and wrap external text with a fixed untrusted prefix, a per-call random boundary,
179
+ and JSON escaping before it reaches a prompt. Never put external text in the
180
+ system prompt or turn it into instructions. Instruction hierarchy belongs in
181
+ code: system > user > external data. Model output stays data; validate it, do
182
+ not shell it, and do not let it choose an arbitrary URL.
183
+
184
+ ```ts
185
+ import { randomUUID } from 'node:crypto'
186
+
187
+ const wrap = (s: string) => {
188
+ const id = randomUUID() // per call, so reused wrappers get a fresh boundary
189
+ return `<untrusted-${id}>\n${JSON.stringify(s).replaceAll('<', '\\u003c')}\n</untrusted-${id}>`
190
+ }
191
+ ```
192
+
193
+ Rendering is part of the same boundary: use `textContent`/React escaping, never
194
+ `innerHTML`, and escape log and error text too. Wrapping lowers the success rate
195
+ of injection; it is not isolation.
196
+
197
+ <a id="author-publish"></a>
198
+ ### 6. Dependencies, build, and publish (DSC-15, DSC-21, DSC-22)
199
+
200
+ Commit the lockfile and install with `npm ci --ignore-scripts` (or
201
+ `--frozen-lockfile`). Keep dependencies minimal and check for typosquats —
202
+ lookalike scopes and edit-distance names around `dsh`/`@deepseek-ai/dsh`. Use a
203
+ `files` allowlist, publish the build output, pin Node and the package manager,
204
+ and make a tag rebuild reproducibly.
205
+
206
+ ```sh
207
+ npm ci --ignore-scripts && npm test && npm pack --dry-run
208
+ npm audit --omit=dev && npm audit signatures
209
+ # CI only: trusted publishing, provenance, no long-lived token
210
+ npm publish --access public --provenance
211
+ ```
212
+
213
+ Publish with npm trusted publishing (OIDC) and `--provenance`; keep 2FA on,
214
+ protect tags, require review, and never reuse a version (deprecate a bad one).
215
+ Check that `package.json` has no `preinstall`/`install`/`postinstall`/`prepare`:
216
+
217
+ ```sh
218
+ node -e "const s=require('./package.json').scripts||{}; if(s.preinstall||s.install||s.postinstall||s.prepare) process.exit(1)"
219
+ ```
220
+
221
+ `dist.integrity` only proves the tarball matches the registry, and an
222
+ attestation needs signature verification plus a bound build source. Neither is
223
+ a code-safety proof.
224
+
225
+ <a id="author-response"></a>
226
+ ### 7. Vulnerability response (process, not a DSC)
227
+
228
+ Ship a `SECURITY.md` with a private channel (GitHub Security Advisories), the
229
+ supported versions, a 7-day first response, and 90-day coordinated disclosure.
230
+ Do not take vulnerability reports in public issues. After a fix, publish a
231
+ patch release, an advisory, and credit; revoke and rotate leaked tokens; for a
232
+ malicious version, deprecate it, ask the market to delist, and keep the
233
+ evidence.
234
+
235
+ ```md
236
+ # Security Policy
237
+
238
+ ## Supported versions
239
+ | Version | Supported |
240
+ |---|---|
241
+ | 0.1.x | yes |
242
+
243
+ ## Reporting
244
+ Use <repo>/security/advisories/new; do not open a public issue.
245
+ Include the version, DSH version, impact, and a minimal repro. First response within 7 days.
246
+ ```
247
+
248
+ <a id="user"></a>
249
+ ## User guide
250
+
251
+ The checks tell you what a package declares; they do not tell you what it does.
252
+ Assume a loaded plugin can do anything the user can. The static checks that
253
+ touch this path are `DSC-15` (lifecycle scripts), `DSC-17` (credential files),
254
+ `DSC-21` (typosquat/lookalike names, WARN-only) and `DSC-22`
255
+ (integrity/provenance) before install; `DSC-18` only ever produces a static clue
256
+ about egress. Under `--scope security`, `DSC-15`/`DSC-16`/`DSC-17` can block;
257
+ outside it every security finding is capped to WARN in the separate `security`
258
+ block.
259
+
260
+ **Before installing.** Read the manifest: `dsh.capabilities`, `dsh.labels`,
261
+ `scripts`, `files`, `engines.dsh`, and the source pin. Then look at the source
262
+ and the publisher: an exact npm version with `dist.integrity`, or a git
263
+ 40-hex commit; how recent the release is; whether `dist.signatures` /
264
+ `dist.attestations` exist; and whether an install script is present. An empty
265
+ capabilities list means **not declared**, not "no capabilities". A label such as
266
+ `prompt-modification`, `third-party-model-provider`, `remote-access`,
267
+ `red-team`, or `mirror` is the author telling you which risk to check, not a
268
+ clearance.
269
+
270
+ ```sh
271
+ npm view pkg@1.2.3 version dsh scripts dist.integrity dist.signatures dist.attestations repository.url time.modified --json
272
+
273
+ # the released static security scope (1.6.0+; does not run the plugin)
274
+ npx dsh-standards check ./pkg --scope security --format json
275
+ ```
276
+
277
+ **During install.** Start from a throwaway profile (`dsh plugin --profile lab
278
+ ...` initializes it), add `--ignore-scripts`, and understand that it stops
279
+ install scripts only, not the code the host loads. The GUI build approval runs
280
+ a script with your permissions, but the approval list shows only the package
281
+ name, not the script body: inspect the referenced script first (`npm pack`, then
282
+ read `scripts/*`). The CLI inherits your whole environment, so launch it from a
283
+ clean shell with no tokens. A new session defaults to `workspace-write` + `ask`;
284
+ do not switch to `danger-full-access` with `never`.
285
+ `DSH_PERMISSION_MODE=read-only` constrains tool subprocesses, not plugins.
286
+
287
+ ```sh
288
+ dsh plugin --profile lab add pkg@1.2.3 --ignore-scripts
289
+ dsh --profile lab --dump-config
290
+ dsh plugin --profile lab remove pkg
291
+ ```
292
+
293
+ **After install.** Return to `workspace-write`, watch the install log and the
294
+ session's tool calls, and remove the package when you no longer need it. Check
295
+ the profile's `package.json`/lockfile and the credential records. If something
296
+ looks wrong, disable it, keep the logs and the package hash, and report through
297
+ its `SECURITY.md` (for a malicious npm package, also npm support). Do not
298
+ publish the details first.
299
+
300
+ <a id="market"></a>
301
+ ## Market / repository guide
302
+
303
+ **Record facts before listing.** `name@version`; source (npm integrity or git
304
+ 40-hex plus subdir); `dsh.bundle.patch` (no `!!js`), `files`, `engines.dsh`, and
305
+ whether `scripts` contains an install hook; publisher vs `repository`;
306
+ `SECURITY.md`, 2FA, protected tags; publish time; provenance/attestations;
307
+ capability/label validity; license (advisory). Do not install or execute the
308
+ package to fill the row, and do not grade, score, or rank it. These facts map
309
+ to `DSC-15`–`DSC-22`: a row records them, it does not certify them.
310
+
311
+ ```sh
312
+ npm view pkg@1.2.3 --json
313
+ mkdir -p inspect && npm pack pkg@1.2.3 --ignore-scripts --pack-destination ./inspect && tar -tzf inspect/*.tgz
314
+ git clone --filter=blob:none --no-checkout <url> r && git -C r archive <40hex> | tar -t
315
+ ```
316
+
317
+ **Show risk, not a badge.** Put capabilities in the same view as labels and mark
318
+ them "author declaration, not enforced"; mark unknowns "not declared / not
319
+ checked / not run". The index's only risk field is labels; capabilities must be
320
+ read from the manifest. Missing data is never a pass. State that static checks
321
+ cover known patterns and are not a security audit. The checker can import the
322
+ plugin, so do not run it against an untrusted package on a host you care about.
323
+
324
+ **Disputes and delisting.** Hard-reject only four cases: not a plugin /
325
+ unparseable, source not pinnable, impersonation or infringement, and a malicious
326
+ security hold (manual review with an appeal). List everything else with the
327
+ finding attached. The author can withdraw at any time; a 90-day expiry removes
328
+ only the label; a 14-day appeal window; keep history for at least two years and
329
+ write the reason to `market-actions.jsonl`.
330
+
331
+ **Wording ban.** Never write "secure", "certified", "official", "audited", or
332
+ "100% risk-free", and never imply certification with a shield or a checkmark.
333
+ Use the fixed disclaimer: "unofficial · not certified · not security-audited ·
334
+ not endorsed · no warranty".
335
+
336
+ <a id="template"></a>
337
+ ## Template: minimal low-risk defaults
338
+
339
+ The smallest honest starting point is: no lifecycle scripts, empty
340
+ capabilities, a permissions disclosure marked draft, and a `SECURITY.md` in the
341
+ tarball. The `dsh-scaffold` template ships these defaults.
342
+
343
+ ```json
344
+ {
345
+ "name": "@acme/dsh-hello",
346
+ "version": "0.1.0",
347
+ "license": "Apache-2.0",
348
+ "files": ["lib", "cordis.patch.yml", "SECURITY.md", "LICENSE"],
349
+ "scripts": { "test": "vitest run" },
350
+ "dsh": {
351
+ "capabilities": [],
352
+ "labels": []
353
+ },
354
+ "security": {
355
+ "status": "draft",
356
+ "note": "non-host disclosure only; not read, not enforced",
357
+ "permissions": { "files": [], "network": [], "credentials": [], "shell": false, "subprocess": false }
358
+ }
359
+ }
360
+ ```
361
+
362
+ `SECURITY.md` must name the private channel, the supported versions, and the
363
+ response window — see the author section above. Verify the shape before publish:
364
+
365
+ ```sh
366
+ node -e "const s=require('./package.json').scripts||{}; if(s.preinstall||s.install||s.postinstall||s.prepare) process.exit(1)"
367
+ npm pack --dry-run # SECURITY.md must be in the file list
368
+ ```
369
+
370
+ This shape is the starting point for `DSC-15` (no lifecycle scripts), `DSC-21`
371
+ (no typosquat dependency), and `DSC-22` (provenance publishing); it also gives
372
+ `DSC-17` and `DSC-18` a truthful empty disclosure to read. An empty
373
+ `capabilities` array is correct only while the plugin touches no host surface.
374
+ Once it registers a tool or reads `ctx.fs`, add the ID; a copied template with a
375
+ stale empty array hides the very `C-11.2` failure the check is for.
376
+
377
+ <a id="scope"></a>
378
+ ## Where the security checks sit
379
+
380
+ | Scope | What runs | Verdict |
381
+ |---|---|---|
382
+ | `--scope ds-c` (default) / `entry` / `flow` / `boost` | `DSC-01`–`DSC-14`: only the scope's mandatory subset counts (`DSC-10` is advisory; `03`/`14` can be NA/not-run). `DSC-15`–`DSC-22` run in the separate `security` block (`scope=advisory`) | a security `FAIL` is capped to `WARN` in that block; it is not in `checks[]`/`buckets.blocking` and cannot change `status`/exit |
383
+ | `--scope security` (1.6.0+) | all 22 checks in `checks[]`: DSC-01–14 plus DSC-15–22 | `15`/`16`/`17` blocking (FAIL → exit 1); `21` mandatory **WARN-only**, never blocking (listing too); `18` WARN; `19`/`20`/`22` advisory |
384
+ | runtime observation | sandbox with a network policy + capture | the only evidence that code actually egressed, read a file, or rewrote a prompt |
385
+
386
+ `DSC-16` blocks on `eval`/`new Function`/`vm`/`module._compile`/`process.binding`.
387
+ A bare `child_process` import is WARN in 1.6.1; the FAIL for a statically
388
+ dangerous subprocess command is scoped to 1.6.2.
389
+
390
+ The JSON report carries an additive top-level `security` block; the DS-C verdicts
391
+ stay unchanged. A PASS in that block means "no blocking FAIL, no WARN, and the
392
+ mandatory `15`/`16`/`17`/`21` all produced a result", not "safe".
393
+
394
+ <a id="security-block"></a>
395
+ ### Reading the `security` block
396
+
397
+ - `conclusion.headline`'s `blocking N` and `buckets.blocking[]` are
398
+ **report-level**: they only count `checks[]` FAILs. `security.blocking[]` is
399
+ the block's own list and can only ever hold `DSC-15`/`DSC-16`/`DSC-17`.
400
+ - **Default scope:** the eight run in `security.checks[]` with
401
+ `security.scope="advisory"`. A `FAIL` is capped to `WARN` (listed in
402
+ `security.warn[]`), `security.blocking=[]`, no security row appears in
403
+ `buckets.blocking`, the headline is unchanged, and `status`/exit stay the same.
404
+ - **`--scope security`:** the eight join `checks[]`. A `15`/`16`/`17` FAIL →
405
+ `security.blocking=[...]` plus `buckets.blocking=[...]`, headline `blocking N`,
406
+ `status=fail`, exit `1`. `DSC-21` findings are WARN-only and never block;
407
+ `18`/`19`/`20`/`22` are advisory. A mandatory check with no result makes
408
+ `security.status=incomplete`.
409
+ - **Partial:** 1.6.1 has no `security.partial[]`. A partly-run check is
410
+ `security.checks[].coverage === "partial"` (and top-level `coverage.partial` /
411
+ `buckets.partial`); `security.notRun[]` only carries `NA`/`ERROR`/full
412
+ `not-run`. Do not read the four arrays as complete coverage.
413
+
414
+ <a id="limits"></a>
415
+ ## What no static check can tell you
416
+
417
+ - whether a URL was really fetched, or where a redirect went;
418
+ - whether a file outside the package was read or written;
419
+ - whether a dependency's own install script ran;
420
+ - whether a model was actually steered by injected text;
421
+ - whether the author is trustworthy — signatures are self-asserted evidence
422
+ until the key chain and build provenance are verified by a human.
423
+
424
+ Runtime security needs a sandbox with an egress policy and an audit trail.
425
+ Windows has no `unshare`, so the containment story is weaker there, and a
426
+ sandbox that only limits writes is not a network boundary. Until you have that
427
+ evidence, write `not-run` or `not-measured` — never "secure".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-guide",
3
- "version": "0.4.6",
3
+ "version": "0.5.1",
4
4
  "description": "A practical guide to DSH plugin quality: ENTRY checks, FLOW checks, and BOOST guidance. Community guide; not official, not a certification, not a ranking.",
5
5
  "type": "module",
6
6
  "main": "index.js",