ENTRY is a compatibility gate, not a full quality audit. It is the first thing a market, index, or user hits.
- package manifest: name, version, license, entry,
dsh.bundle.patch - engines/peer range for the DSH and Cordis generation being claimed
- Node
>=24.9.0for the 0.2.2 line;engines.dsh, when declared, decides alone since 0.2.1-alpha.4 (prereleases participate) - the declared entry point actually resolves
- the bundle patch parses and its target row resolves
- display metadata, labels, and forbidden-claim checks
- a runtime claim: plugin start or bundle G0–G3, when evidence is supplied
The exact rule IDs and evidence rules are in
31-DSH-ENTRY
and 40-checker.
Every ID is also listed in rules.md; the copy-paste shapes are in
recipes.md.
DSC-01 is one report line but six subitems; the detail lines say which one
failed. DSC-03 runs only in the listing profile — in the package profile it is
NA with install spec not supplied (package profile), which is allowed.
| Detail | What the report says | Fix |
|---|---|---|
C-1.1 |
name must be a valid npm package name / version must be an exact SemVer version |
use a lowercase npm name and an exact 1.2.3 (or 1.2.3-beta.1); a tag or range is not a version |
C-1.2 |
no resolvable runtime entry / entry escapes the package |
point exports["."] or main at a file that exists inside the package; no ../ target |
C-1.2 |
exports["."] points to a missing file vs entry not present in checkout; source/build hint exists (WARN) |
shipped packages need the built file; if the checker sees src/index.ts or a build/prepare script it downgrades to WARN, do not rely on that |
C-1.2 |
patch-only bundle; no runtime entry by design (NA) |
fine: a package that only ships dsh.bundle.patch has no runtime entry and is not charged for it |
C-1.3 |
no apply/default-class shape found by static scan (WARN) |
export apply (function plugin) or a default class (service plugin), not both; L1 is authoritative |
C-1.4 |
patch path escapes the package / patch file is missing |
keep the patch inside the package and reference it relatively, e.g. "./cordis.patch.yml" |
C-1.5 |
license is missing (WARN) |
add "license": "MIT" or another SPDX id |
C-1.6 |
private:true; not publishable (NA) |
expected for a local profile package, not for a marketplace entry |
G4 |
dsh.client exists but exports["./client"] is missing; a package-root row cannot resolve it |
add exports["./client"] and the package-root row — see recipes.md |
C-2.1 |
neither engines.dsh nor a @deepseek-ai/dsh* peer range is declared |
declare "engines": { "dsh": ">=0.2.2-0 <0.3.0" } |
C-2.2 |
engines.dsh <range> does not satisfy runtime <v> |
widen the range to the host you target, or run the checker with --runtime-version <v> to judge against the right one |
C-2.3 |
engines.dsh must not be a workspace: range |
publish a real SemVer range; workspace: must not reach a tarball |
C-2.4 |
peer <name> range <range> conflicts with engines.dsh (WARN) |
the host decides on engines.dsh alone; align the peer anyway |
C-3.1/C-3.2/C-3.5 |
install spec / source is pinned / market entry |
listing only: exact version or verifiable tag, #<40-hex> for git, and a format:1 index entry with pinned versions |
C-3.3/C-3.4 |
non-registry dependency / install-time build script present (WARN) |
pin git/url/file dependencies and declare what install scripts build |
| Claim | Meaning | What it needs |
|---|---|---|
static / recognized |
the package shape can be identified | manifest/entry inspection only |
runtime |
it actually starts or boots | plugin L1, or bundle G0–G3 |
recognized is not a pass. A bundle is not proven by importing its server
entry; it needs G0–G3. A client-only package is build-only unless a browser
probe exists.
These are the L1–L3 checks: load, provide, effects, unload, restart, and
declared-vs-measured. They apply to kind=plugin; a bundle is verified by
--scope entry G0–G3 instead (in --scope ds-c it is NA), and a client,
preset, skill, theme, or MCP config is NA and must not claim runtime. DSC-14
is the ENTRY run gate: for a plugin it mirrors L1, for a bundle it is G0–G3 and
needs --host, and under --entry-claim static it is NA
(entry static claim: no runtime evidence).
| Detail | What the report says | Fix |
|---|---|---|
C-4.1/C-4.2 |
entry import failed: … / load failed: … / fiber state <n> is not ACTIVE (2) |
the entry must import and apply() without throwing; move timers, watchers, I/O, and subprocesses into apply() |
C-4.3 |
plugin load timed out / killed after <n>ms; plugin blocked the event loop |
do not block on module-scope work or a top-level await; the L1 child is hard-killed |
| — | blocked by missing host dependency (ERROR, not-run) |
environment, not code: the plugin imports a bare package the fixture does not have. Install it or point --runtime at a complete host |
C-5.1/C-5.2 |
no provide declaration (NA) / declared provide <x> was not registered / owned by another fiber |
register the service from this plugin's own fiber: ctx.provide(name, impl) or a Service |
C-6.1 |
direct write to a Cordis private registration surface |
never assign into ctx.events._hooks, ctx.reflect.store, ctx.registry._internal, or fiber._disposables |
C-6.2 |
no registered effects observed / all effects are anonymous (WARN) |
give every effect a label: ctx.effect(fn, 'my-plugin.timer') |
C-7.1–C-7.3 |
getEffects().length=<n> / service store returned to baseline / registry.size <a> -> <b> |
every effect returns a disposer; dispose() must reach zero effects and restore the store and registry |
C-7.5 |
dispose failed: … |
await quiescence; do not fire-and-forget cleanup |
C-8.1–C-8.3 |
state after restart=<n> / effect count <a> -> <b> / revision … -> … |
restart() must re-enter ACTIVE, keep the effect count, advance revision/epoch, and work from a fresh Context |
C-9.1–C-9.3 |
declared provide <x> is missing from measured services / measured service <x> is not declared / inject declares <x> but no static ctx.<x> read was found |
declarations must equal reality: provide ⊆ measured, measured ⊆ declared, and each inject is read |
The runner stubs the services you declare in inject before it calls
ctx.plugin(), so a plugin that only needs declared services can reach ACTIVE.
Anything it cannot stub is reported as ERROR/not-run; that is not your
defect and must not be written as a pass.
export const name = 'hello'
export const inject = ['tools'] // only what you read
export const provide = ['hello'] // only what you register
export function apply(ctx) {
ctx.provide('hello', { greet: () => 'hello' }) // C-5
ctx.effect(() => { // C-6, C-7
const timer = setInterval(poll, 60_000)
return () => clearInterval(timer) // dispose → 0 effects
}, 'hello.timer')
}A bundle is verified by G0–G3, not by L1: G0 parses the patch, G1 composes the
rows, G2 runs the host compatibility gate, G3 boots the target rows. A partial
G3 is ERROR/not-run; only G0-G3 verified; rows=<n>/<n> with
DSC-14 PASS is a runtime claim. G4 is a composition property: with
dsh.client, the composed layer must contain a row whose name is the package
name and exports["./client"] must resolve.
A client package is dual-face: the server half is loaded as a row, the browser
half is a prebuilt file at exports["./client"].
dsh.clientmust be a non-empty object; the host's Web consumer selects it withplatform: "web".exports["./client"]must be present. The host accepts a string, or an object with a stringdefault; the checker only fails when it is missing.- The composed config needs a package-root row: a patch
insertwhosenameequalsmanifest.name. Without an active row the client scanner never sees the package, even though the export exists. - The client file must be a built artifact and must be included in
files, or the tarball ships a manifest that points at nothing. - G4 is static. It does not prove the browser bundle renders; that is a
build/browser probe and belongs in
limitations, not in a runtime claim.
The copy-paste client manifest and patch row are in
recipes.md. A missing export or row is
reported as G4: FAIL under DSC-01 (or as DSC-14 with --host), never as a
pass.
# Recognition only
npx dsh-standards ./my-plugin --scope entry --entry-claim static
# Runtime claim: provide the real host/runtime, not a stub
npx dsh-standards ./my-bundle --scope entry \
--host /path/to/@deepseek-ai/dsh-app-boot/lib/index.js
# Machine-readable output
npx dsh-standards ./my-plugin --scope entry --entry-claim static --format jsonExit codes: 0 completed, 1 attributable FAIL, 2 incomplete/not-run,
3 usage error. A not-run or incomplete result must not be written as a
pass.
package.jsonhas a resolvable entry and, for bundles, adsh.bundle.patch.- The patch target row exists; ordered patch arrays are allowed.
engines.dshor an@deepseek-ai/dsh*peer range is declared. For 0.2.2, prefer>=0.2.2-0 <0.3.0; npm default semver needs an explicit^0.2.2-alpha.3union arm.dsh.capabilitieslists every host surface the plugin touches (see below).dsh.labelsis declared when the plugin modifies prompts, runs red-team workloads, adds a model provider, exposes remote access, or mirrors another project.- The license is present; a missing license is advisory, not a fabricated pass.
- Module scope stays side-effect free: no top-level
await, timer,process.on, fs write, or subprocess spawn. Do that work inapply()and clean up withctx.effect(). - README says how to install and what it needs.
- No
preinstall/install/postinstall/preparescript in the published package; if a build is needed, run it in CI, not on the user's machine. - No
eval/new Function/vm; credentials go throughctx.credentials, not a backing file; third-party endpoints are disclosed. - No “official”, “certified”, “secure”, or ranking claim.
Two optional fields become checks DSC-11 and DSC-13 once they exist:
{
"dsh": {
"capabilities": ["fs:read", "network:egress"],
"labels": ["remote-access"]
}
}Both are standard extensions: this host does not read them yet. They are
declarations plus UI disclosure, not a sandbox and not enforcement. Declaring
[] and importing node:fs directly still passes DSC-11; the check only
compares declarations with static triggers.
Allowed capabilities: fs:read, fs:write, shell, subprocess,
network:egress, network:ingress, credentials:read, credentials:list,
credentials:provider, llm:adapter, sessions:read, system-prompt:write,
tools:register, remote:expose, plugin:install.
Allowed labels: prompt-modification, red-team,
third-party-model-provider, remote-access, mirror.
C-11.2 is the check authors hit most: if dsh.capabilities exists, every
static trigger below must have its capability in the array, or the check FAILs
with static trigger <cap> is not declared in dsh.capabilities.
| Static trigger (source shape) | Declare |
|---|---|
ctx.webServer / ctx.webhookRuntime |
network:ingress |
installBundle( / removeBundle( |
plugin:install |
registerAdapter( / ctx.llm.register |
llm:adapter |
ctx.systemPrompt.section( / .context( / .variable( |
system-prompt:write |
ctx.tools.register( |
tools:register |
@Remote( / @RemoteScope( |
remote:expose |
.listRecords( |
credentials:list |
ctx.sessions / ctx.sessionQuery |
sessions:read |
ctx.shell / ctx.bash |
shell |
ctx.subprocess |
subprocess |
ctx.fs |
fs:read (a declared fs:write satisfies it) |
ctx.web / a bare fetch( |
network:egress |
Reading the other subitems:
C-11.1: an unknown capability id is a FAIL; an empty array is a WARN. A missing field is a transitional WARN, andC-11.2is then not evaluated — so an undeclared array hides the very failure you want to see.C-11.3: a bundle patch that enables@deepseek-ai/dsh-plugin-manager/toolswithoutdisabled: trueis a FAIL.C-11.4: a string literal that is exactlydanger-full-accessis a WARN; a mention in a comment or aconsole.logdescription is not.C-11.5: a declared capability with no matching trigger is a WARN (over- declaration). Keep declarations to what the scan can see; a dynamic call may not match.
The scan covers .js, .mjs, .cjs, .ts, .tsx, and .jsx files in the
package, and skips tests, docs, scripts, examples, fixtures, bench,
tools, and dot-directories, plus the other build and asset directories the
walker ignores. A fetch( in tests/ does not require network:egress; the
same call in lib/client.js does.
DSC-12 is a declaration-consistency check, not a security audit. It looks for
direct access to the credential backing file and for global seam registration.
| Detail | What the report says | Fix |
|---|---|---|
C-12.1 |
credential backing-file access near a file-read API without credentials:provider (a path in text or UI is PASS) |
read through ctx.credentials; if you really are a provider, declare credentials:provider |
C-12.2 |
listRecords() used without credentials:list |
declare credentials:list, or stop enumerating records |
C-12.3 |
global seam provider registration detected (WARN) |
do not provide shell/credentials/sandboxPolicy/sandbox/deepSeekAccount unless that is the plugin's purpose |
C-12.4 |
danger-full-access/--add-dir literal (WARN) |
do not hard-code a sandbox bypass in a command or policy value |
C-12.6 |
dsh.commitments missing (NA) or missing commitment: <id> (WARN) |
optional: a boolean for each of no-session-upload, no-third-party-proxy, no-credential-enumeration, no-silent-prompt-rewrite, no-adapter-bypass, no-webhook-auto-session, no-security-seam-replacement, third-party-endpoint-disclosure, no-danger-full-access |
C-12.7 |
dsh.disclosure missing (NA) or a shape WARN |
optional: endpoints (https URLs), retention (string), systemPrompt (string) |
// prefer: go through the seam
export function apply(ctx) {
ctx.credentials.resolve('acme/api-key')
}
// avoid: the backing file is an implementation detail
// readFileSync(join(process.env.DSH_HOME, '.credentials.yaml'))A label is a declaration, not an endorsement. C-13.2 maps a declared
capability to a required label:
| Declared capability | Required label |
|---|---|
system-prompt:write |
prompt-modification |
llm:adapter and network:egress |
third-party-model-provider |
network:ingress |
remote-access |
dsh.upstream declared |
mirror |
C-13.1: an unknown label is a FAIL; a missingdsh.labelsis a transitional WARN.C-13.3:dsh.upstreammust be anhttps://URL and must not point at this package's ownrepository; a mirror also needs themirrorlabel.C-13.4:official,verified,certified,secure,auditedindsh.labelsis a FAIL. The prose ban is separate: the checker does not scan README ordescription, so keeping those words out is on you.C-13.5: thered-teamlabel should come withdsh.usageScope; without it the check only WARNs.
Do not label a plugin official, verified, certified, secure, or
audited — those words are treated as an endorsement claim and fail DSC-13.
DSC-10 is advisory — it never changes the verdict — but it records module-scope
work that runs before apply(): top-level await, timers, process.on, fs
writes, and subprocess spawns. The check is scoped to the entry module; the same
line in a shipped helper script that the entry does not import is not a hit.
Move the work inside
apply() and tie cleanup to ctx.effect().
// avoid: this runs when the package is imported, before the host can inject
writeFileSync(cacheFile, '')
export function apply(ctx) {
// prefer: runs when the plugin is applied, with a cleanup hook
ctx.effect(() => {
mkdirSync(dir, { recursive: true })
return () => rmSync(dir, { recursive: true, force: true })
})
}ENTRY is a compatibility gate; it does not answer "is this safe to install". The
static security scope ships in dsh-standards 1.6.0+ and evaluates eight
checks; with --scope security the report's checks[] carries all 22
(DSC-01–DSC-14 plus DSC-15–DSC-22).
| Check | What it looks for | Verdict in --scope security |
|---|---|---|
DSC-15 |
preinstall/install/postinstall/prepare that download, decode, or execute (command string and referenced in-package script body, ≤512 KB) |
blocking |
DSC-16 |
eval, new Function, vm, module._compile, process.binding; bare child_process is WARN in 1.6.1 |
blocking |
DSC-17 |
reads of ~/.ssh, ~/.aws, .npmrc, .netrc, or credential backing files |
blocking |
DSC-18 |
non-https, userinfo, bare-IP, webhook/paste/tunnel egress targets | WARN (clue) |
DSC-19 |
minified blobs, large base64 blocks, wasm/native without source | advisory |
DSC-20 |
tool/web text flowing into the system prompt without a boundary | advisory (static v1 + runtime hook v2) |
DSC-21 |
typosquat names, fake scopes, unpinned or script-bearing dependencies | mandatory but WARN-only, never blocking (listing included) |
DSC-22 |
dist.integrity, signatures, provenance attestations |
advisory |
--scope security is released in 1.6.0+; 1.5.x and earlier exit 3 for the
unknown scope. Outside that scope the eight are not in checks[]; a security
FAIL is capped to WARN in the separate security block, never enters
buckets.blocking, and cannot change an ENTRY verdict. Only 15/16/17 can
block (and only under --scope security); 21 never blocks; 18/19/20/22
are advice. DSC-16 subprocess FAIL is scoped to 1.6.2; 1.6.1 keeps a bare
child_process import at WARN.
# 1.6.0+; exits 3 only on 1.5.x and earlier
npx dsh-standards ./my-plugin --scope security --format jsonThe author, user, and market checklists — with copyable manifests, commands,
and the boundary between static checks and a runtime sandbox — are in
security.md. ENTRY's own declaration fields are not enforcement,
and neither is a DSC-11 PASS.
It is not a security audit, sandbox, full contract audit, market gate, score, or
ranking. Those are separate concerns; do not borrow their authority. The static
DSC-15–DSC-22 checks are a separate security scope, covered in
security.md, not part of the ENTRY verdict.