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